ESBORRANY - DRAFT - BORRADOR
+A related website will be automatically configured if needed.', verbose_name='custom URL'), + ), + migrations.AlterField( + model_name='saas', + name='name', + field=models.CharField(help_text='Required. 64 characters or fewer. Letters, digits and ./- only.', max_length=64, validators=[orchestra.core.validators.validate_hostname], verbose_name='Name'), + ), + migrations.AlterField( + model_name='saas', + name='service', + field=models.CharField(choices=[('bscw', 'BSCW'), ('dokuwiki', 'Dowkuwiki'), ('drupal', 'Drupal'), ('gitlab', 'GitLab'), ('moodle', 'Moodle'), ('wordpress', 'WordPress'), ('nextcloud', 'nextCloud'), ('owncloud', 'ownCloud'), ('phplist', 'phpList')], max_length=32, verbose_name='service'), + ), + ] diff --git a/orchestra/contrib/saas/models.py b/orchestra/contrib/saas/models.py index b5b4e23e..ee1423b1 100644 --- a/orchestra/contrib/saas/models.py +++ b/orchestra/contrib/saas/models.py @@ -70,7 +70,7 @@ class SaaS(models.Model): self.save(update_fields=('is_active',)) def enable(self): - self.is_active = False + self.is_active = True self.save(update_fields=('is_active',)) def clean(self): diff --git a/orchestra/contrib/saas/services/helpers.py b/orchestra/contrib/saas/services/helpers.py index 03ed1e20..bf081995 100644 --- a/orchestra/contrib/saas/services/helpers.py +++ b/orchestra/contrib/saas/services/helpers.py @@ -6,6 +6,7 @@ from django.utils.translation import ugettext_lazy as _ from orchestra.contrib.websites.models import Website, WebsiteDirective, Content from orchestra.contrib.websites.validators import validate_domain_protocol +from orchestra.contrib.orchestration.models import Server from orchestra.utils.python import AttrDict @@ -54,7 +55,9 @@ def clean_custom_url(saas): (url.netloc, account, domain.account), }) # Create new website for custom_url - website = Website(name=url.netloc, protocol=protocol, account=account) + # Changed by daniel: hardcode target_server to web.pangea.lan, consider putting it into settings.py + tgt_server = Server.objects.get(name='web.pangea.lan') + website = Website(name=url.netloc, protocol=protocol, account=account, target_server=tgt_server) full_clean(website) try: validate_domain_protocol(website, domain, protocol) @@ -110,7 +113,8 @@ def create_or_update_directive(saas): Domain = Website.domains.field.rel.to domain = Domain.objects.get(name=url.netloc) # Create new website for custom_url - website = Website(name=url.netloc, protocol=protocol, account=account) + tgt_server = Server.objects.get(name='web.pangea.lan') + website = Website(name=url.netloc, protocol=protocol, account=account, target_server=tgt_server) website.save() website.domains.add(domain) # get or create directive diff --git a/orchestra/contrib/saas/services/nextcloud.py b/orchestra/contrib/saas/services/nextcloud.py new file mode 100644 index 00000000..3398f0da --- /dev/null +++ b/orchestra/contrib/saas/services/nextcloud.py @@ -0,0 +1,13 @@ +from django import forms +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from .. import settings +from .options import SoftwareService + + +class NextCloudService(SoftwareService): + name = 'nextcloud' + verbose_name = "nextCloud" + icon = 'orchestra/icons/apps/nextCloud.png' + site_domain = settings.SAAS_NEXTCLOUD_DOMAIN diff --git a/orchestra/contrib/saas/settings.py b/orchestra/contrib/saas/settings.py index a15a6bd9..af768867 100644 --- a/orchestra/contrib/saas/settings.py +++ b/orchestra/contrib/saas/settings.py @@ -18,6 +18,7 @@ SAAS_ENABLED_SERVICES = Setting('SAAS_ENABLED_SERVICES', 'orchestra.contrib.saas.services.dokuwiki.DokuWikiService', 'orchestra.contrib.saas.services.drupal.DrupalService', 'orchestra.contrib.saas.services.owncloud.OwnCloudService', + 'orchestra.contrib.saas.services.nextcloud.NextCloudService', # 'orchestra.contrib.saas.services.seafile.SeaFileService', ), # lazy loading @@ -235,6 +236,23 @@ SAAS_OWNCLOUD_LOG_PATH = Setting('SAAS_OWNCLOUD_LOG_PATH', ) +# nextCloud +SAAS_NEXTCLOUD_DOMAIN = Setting('SAAS_NEXTCLOUD_DOMAIN', + 'nextcloud.{}'.format(ORCHESTRA_BASE_DOMAIN), + help_text="Uses ORCHESTRA_BASE_DOMAIN by default.", +) + +SAAS_NEXTCLOUD_API_URL = Setting('SAAS_NEXTCLOUD_API_URL', + 'https://admin:secret@nextcloud.{}/ocs/v1.php/cloud'.format(ORCHESTRA_BASE_DOMAIN), +) + +SAAS_NEXTCLOUD_LOG_PATH = Setting('SAAS_NEXTCLOUD_LOG_PATH', + '', + help_text=_('Filesystem path for the webserver access logs.
' + 'LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Host}i\"" host'), +) + + # BSCW SAAS_BSCW_DOMAIN = Setting('SAAS_BSCW_DOMAIN', diff --git a/orchestra/contrib/services/migrations/0006_auto_20170528_2005.py b/orchestra/contrib/services/migrations/0006_auto_20170528_2005.py new file mode 100644 index 00000000..1f59952b --- /dev/null +++ b/orchestra/contrib/services/migrations/0006_auto_20170528_2005.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-05-28 18:05 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('services', '0005_auto_20160427_1531'), + ] + + operations = [ + migrations.AlterField( + model_name='service', + name='rate_algorithm', + field=models.CharField(choices=[('orchestra.contrib.plans.ratings.best_price', 'Best price'), ('orchestra.contrib.plans.ratings.step_price', 'Step price'), ('orchestra.contrib.plans.ratings.match_price', 'Match price')], default='orchestra.contrib.plans.ratings.step_price', help_text='Algorithm used to interprete the rating table.
Best price: Produces the best possible price given all active rating lines (those with quantity lower or equal to the metric).
Step price: All rates with a quantity lower or equal than the metric are applied. Nominal price will be used when initial block is missing.
Match price: Only the rate with a) inmediate inferior metric and b) lower price is applied. Nominal price will be used when initial block is missing.', max_length=64, verbose_name='rate algorithm'), + ), + ] diff --git a/orchestra/contrib/services/migrations/0007_auto_20170528_2011.py b/orchestra/contrib/services/migrations/0007_auto_20170528_2011.py new file mode 100644 index 00000000..d11e961a --- /dev/null +++ b/orchestra/contrib/services/migrations/0007_auto_20170528_2011.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-05-28 18:11 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('services', '0006_auto_20170528_2005'), + ] + + operations = [ + migrations.AlterField( + model_name='service', + name='rate_algorithm', + field=models.CharField(choices=[('orchestra.contrib.plans.ratings.step_price', 'Step price'), ('orchestra.contrib.plans.ratings.best_price', 'Best price'), ('orchestra.contrib.plans.ratings.match_price', 'Match price')], default='orchestra.contrib.plans.ratings.step_price', help_text='Algorithm used to interprete the rating table.
Step price: All rates with a quantity lower or equal than the metric are applied. Nominal price will be used when initial block is missing.
Best price: Produces the best possible price given all active rating lines (those with quantity lower or equal to the metric).
Match price: Only the rate with a) inmediate inferior metric and b) lower price is applied. Nominal price will be used when initial block is missing.', max_length=64, verbose_name='rate algorithm'), + ), + ] diff --git a/orchestra/contrib/services/migrations/0008_auto_20170625_1813.py b/orchestra/contrib/services/migrations/0008_auto_20170625_1813.py new file mode 100644 index 00000000..a00a05e7 --- /dev/null +++ b/orchestra/contrib/services/migrations/0008_auto_20170625_1813.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-06-25 16:13 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('services', '0007_auto_20170528_2011'), + ] + + operations = [ + migrations.AlterField( + model_name='service', + name='rate_algorithm', + field=models.CharField(choices=[('orchestra.contrib.plans.ratings.best_price', 'Best price'), ('orchestra.contrib.plans.ratings.step_price', 'Step price'), ('orchestra.contrib.plans.ratings.match_price', 'Match price')], default='orchestra.contrib.plans.ratings.step_price', help_text='Algorithm used to interprete the rating table.
Best price: Produces the best possible price given all active rating lines (those with quantity lower or equal to the metric).
Step price: All rates with a quantity lower or equal than the metric are applied. Nominal price will be used when initial block is missing.
Match price: Only the rate with a) inmediate inferior metric and b) lower price is applied. Nominal price will be used when initial block is missing.', max_length=64, verbose_name='rate algorithm'), + ), + ] diff --git a/orchestra/contrib/services/migrations/0009_auto_20170625_1840.py b/orchestra/contrib/services/migrations/0009_auto_20170625_1840.py new file mode 100644 index 00000000..29eb3d52 --- /dev/null +++ b/orchestra/contrib/services/migrations/0009_auto_20170625_1840.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-06-25 16:40 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('services', '0008_auto_20170625_1813'), + ] + + operations = [ + migrations.AlterField( + model_name='service', + name='rate_algorithm', + field=models.CharField(choices=[('orchestra.contrib.plans.ratings.match_price', 'Match price'), ('orchestra.contrib.plans.ratings.best_price', 'Best price'), ('orchestra.contrib.plans.ratings.step_price', 'Step price')], default='orchestra.contrib.plans.ratings.step_price', help_text='Algorithm used to interprete the rating table.
Match price: Only the rate with a) inmediate inferior metric and b) lower price is applied. Nominal price will be used when initial block is missing.
Best price: Produces the best possible price given all active rating lines (those with quantity lower or equal to the metric).
Step price: All rates with a quantity lower or equal than the metric are applied. Nominal price will be used when initial block is missing.', max_length=64, verbose_name='rate algorithm'), + ), + ] diff --git a/orchestra/contrib/services/migrations/0010_auto_20170625_1840.py b/orchestra/contrib/services/migrations/0010_auto_20170625_1840.py new file mode 100644 index 00000000..f59f304d --- /dev/null +++ b/orchestra/contrib/services/migrations/0010_auto_20170625_1840.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-06-25 16:40 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('services', '0009_auto_20170625_1840'), + ] + + operations = [ + migrations.AlterField( + model_name='service', + name='rate_algorithm', + field=models.CharField(choices=[('orchestra.contrib.plans.ratings.best_price', 'Best price'), ('orchestra.contrib.plans.ratings.match_price', 'Match price'), ('orchestra.contrib.plans.ratings.step_price', 'Step price')], default='orchestra.contrib.plans.ratings.step_price', help_text='Algorithm used to interprete the rating table.
Best price: Produces the best possible price given all active rating lines (those with quantity lower or equal to the metric).
Match price: Only the rate with a) inmediate inferior metric and b) lower price is applied. Nominal price will be used when initial block is missing.
Step price: All rates with a quantity lower or equal than the metric are applied. Nominal price will be used when initial block is missing.', max_length=64, verbose_name='rate algorithm'), + ), + ] diff --git a/orchestra/contrib/services/migrations/0011_auto_20170625_1840.py b/orchestra/contrib/services/migrations/0011_auto_20170625_1840.py new file mode 100644 index 00000000..6018a17e --- /dev/null +++ b/orchestra/contrib/services/migrations/0011_auto_20170625_1840.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-06-25 16:40 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('services', '0010_auto_20170625_1840'), + ] + + operations = [ + migrations.AlterField( + model_name='service', + name='rate_algorithm', + field=models.CharField(choices=[('orchestra.contrib.plans.ratings.match_price', 'Match price'), ('orchestra.contrib.plans.ratings.best_price', 'Best price'), ('orchestra.contrib.plans.ratings.step_price', 'Step price')], default='orchestra.contrib.plans.ratings.step_price', help_text='Algorithm used to interprete the rating table.
Match price: Only the rate with a) inmediate inferior metric and b) lower price is applied. Nominal price will be used when initial block is missing.
Best price: Produces the best possible price given all active rating lines (those with quantity lower or equal to the metric).
Step price: All rates with a quantity lower or equal than the metric are applied. Nominal price will be used when initial block is missing.', max_length=64, verbose_name='rate algorithm'), + ), + ] diff --git a/orchestra/contrib/services/migrations/0012_auto_20170625_1841.py b/orchestra/contrib/services/migrations/0012_auto_20170625_1841.py new file mode 100644 index 00000000..e4cd2468 --- /dev/null +++ b/orchestra/contrib/services/migrations/0012_auto_20170625_1841.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-06-25 16:41 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('services', '0011_auto_20170625_1840'), + ] + + operations = [ + migrations.AlterField( + model_name='service', + name='rate_algorithm', + field=models.CharField(choices=[('orchestra.contrib.plans.ratings.best_price', 'Best price'), ('orchestra.contrib.plans.ratings.step_price', 'Step price'), ('orchestra.contrib.plans.ratings.match_price', 'Match price')], default='orchestra.contrib.plans.ratings.step_price', help_text='Algorithm used to interprete the rating table.
Best price: Produces the best possible price given all active rating lines (those with quantity lower or equal to the metric).
Step price: All rates with a quantity lower or equal than the metric are applied. Nominal price will be used when initial block is missing.
Match price: Only the rate with a) inmediate inferior metric and b) lower price is applied. Nominal price will be used when initial block is missing.', max_length=64, verbose_name='rate algorithm'), + ), + ] diff --git a/orchestra/contrib/services/migrations/0013_auto_20190805_1134.py b/orchestra/contrib/services/migrations/0013_auto_20190805_1134.py new file mode 100644 index 00000000..e568b4d5 --- /dev/null +++ b/orchestra/contrib/services/migrations/0013_auto_20190805_1134.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2019-08-05 09:34 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('services', '0012_auto_20170625_1841'), + ] + + operations = [ + migrations.AlterField( + model_name='service', + name='rate_algorithm', + field=models.CharField(choices=[('orchestra.contrib.plans.ratings.match_price', 'Match price'), ('orchestra.contrib.plans.ratings.best_price', 'Best price'), ('orchestra.contrib.plans.ratings.step_price', 'Step price')], default='orchestra.contrib.plans.ratings.step_price', help_text='Algorithm used to interprete the rating table.
Match price: Only the rate with a) inmediate inferior metric and b) lower price is applied. Nominal price will be used when initial block is missing.
Best price: Produces the best possible price given all active rating lines (those with quantity lower or equal to the metric).
Step price: All rates with a quantity lower or equal than the metric are applied. Nominal price will be used when initial block is missing.', max_length=64, verbose_name='rate algorithm'), + ), + ] diff --git a/orchestra/contrib/services/migrations/0014_auto_20200204_1218.py b/orchestra/contrib/services/migrations/0014_auto_20200204_1218.py new file mode 100644 index 00000000..057b1955 --- /dev/null +++ b/orchestra/contrib/services/migrations/0014_auto_20200204_1218.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2020-02-04 11:18 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('services', '0013_auto_20190805_1134'), + ] + + operations = [ + migrations.AlterField( + model_name='service', + name='rate_algorithm', + field=models.CharField(choices=[('orchestra.contrib.plans.ratings.best_price', 'Best price'), ('orchestra.contrib.plans.ratings.match_price', 'Match price'), ('orchestra.contrib.plans.ratings.step_price', 'Step price')], default='orchestra.contrib.plans.ratings.step_price', help_text='Algorithm used to interprete the rating table.
Best price: Produces the best possible price given all active rating lines (those with quantity lower or equal to the metric).
Match price: Only the rate with a) inmediate inferior metric and b) lower price is applied. Nominal price will be used when initial block is missing.
Step price: All rates with a quantity lower or equal than the metric are applied. Nominal price will be used when initial block is missing.', max_length=64, verbose_name='rate algorithm'), + ), + ] diff --git a/orchestra/contrib/systemusers/backends.py b/orchestra/contrib/systemusers/backends.py index f8b9f875..22dde957 100644 --- a/orchestra/contrib/systemusers/backends.py +++ b/orchestra/contrib/systemusers/backends.py @@ -28,6 +28,14 @@ class UNIXUserController(ServiceController): context = self.get_context(user) if not context['user']: return + if not user.active: + self.append(textwrap.dedent(""" + #Just disable that user, if it exists + if id %(user)s ; then + usermod %(user)s --password '%(password)s' + fi + """) % context) + return # TODO userd add will fail if %(user)s group already exists self.append(textwrap.dedent(""" # Update/create user state for %(user)s @@ -61,7 +69,8 @@ class UNIXUserController(ServiceController): if context['home'] != context['base_home']: self.append(textwrap.dedent("""\ # Set extra permissions: %(user)s home is inside %(mainuser)s home - if mount | grep "^$(df %(home)s|grep '^/'|cut -d' ' -f1)\s" | grep acl > /dev/null; then + if true; then +# if mount | grep "^$(df %(home)s|grep '^/'|cut -d' ' -f1)\s" | grep acl > /dev/null; then # Account group as the owner chown %(mainuser)s:%(mainuser)s '%(home)s' chmod g+s '%(home)s' diff --git a/orchestra/contrib/systemusers/migrations/0003_auto_20170528_2011.py b/orchestra/contrib/systemusers/migrations/0003_auto_20170528_2011.py new file mode 100644 index 00000000..0556f593 --- /dev/null +++ b/orchestra/contrib/systemusers/migrations/0003_auto_20170528_2011.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-05-28 18:11 +from __future__ import unicode_literals + +from django.db import migrations, models +import orchestra.core.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('systemusers', '0002_auto_20150429_1413'), + ] + + operations = [ + migrations.AlterField( + model_name='systemuser', + name='shell', + field=models.CharField(choices=[('/dev/null', 'No shell, FTP only'), ('/bin/rssh', 'No shell, SFTP/RSYNC only'), ('/usr/bin/git-shell', 'No shell, GIT only'), ('/bin/bash', '/bin/bash')], default='/dev/null', max_length=32, verbose_name='shell'), + ), + migrations.AlterField( + model_name='systemuser', + name='username', + field=models.CharField(help_text='Required. 32 characters or fewer. Letters, digits and ./-/_ only.', max_length=32, unique=True, validators=[orchestra.core.validators.validate_username], verbose_name='username'), + ), + ] diff --git a/orchestra/contrib/systemusers/models.py b/orchestra/contrib/systemusers/models.py index ebd15def..de60a698 100644 --- a/orchestra/contrib/systemusers/models.py +++ b/orchestra/contrib/systemusers/models.py @@ -88,7 +88,7 @@ class SystemUser(models.Model): self.save(update_fields=('is_active',)) def enable(self): - self.is_active = False + self.is_active = True self.save(update_fields=('is_active',)) def get_description(self): diff --git a/orchestra/contrib/vps/backends.py b/orchestra/contrib/vps/backends.py index 2ead22c3..8b817b40 100644 --- a/orchestra/contrib/vps/backends.py +++ b/orchestra/contrib/vps/backends.py @@ -133,3 +133,22 @@ class ProxmoxOpenVZTraffic(ServiceMonitor): 'object_id': vps.id, 'hostname': vps.hostname, } + + +class LxcController(ServiceController): + model = 'vps.VPS' + + RESOURCES = ( + ('memory', 'mem'), + ('disk', 'disk'), + ('vcpu', 'vcpu') + ) + + def prepare(self): + super(LxcController, self).prepare() + + def save(self, vps): + # TODO create the container + pass + + diff --git a/orchestra/contrib/vps/backends.py.new b/orchestra/contrib/vps/backends.py.new new file mode 100644 index 00000000..2ead22c3 --- /dev/null +++ b/orchestra/contrib/vps/backends.py.new @@ -0,0 +1,135 @@ +import decimal +import textwrap + +from orchestra.contrib.orchestration import ServiceController +from orchestra.contrib.resources import ServiceMonitor + +from . import settings + + +class ProxmoxOVZ(ServiceController): + model = 'vps.VPS' + + RESOURCES = ( + ('memory', 'mem'), + ('swap', 'swap'), + ('disk', 'disk') + ) + GET_PROXMOX_INFO = textwrap.dedent(""" + function get_vz_info () { + hostname=$1 + version=$(pveversion | cut -d '/' -f2 | cut -d'.' -f1) + if [[ $version -lt 2 ]]; then + conf=$(grep "CID\\|:$hostname:" /var/lib/pve-manager/vzlist | grep -B1 ":$hostname:") + CID=$(echo "$conf" | head -n1 | cut -d':' -f2) + CTID=$(echo "$conf" | tail -n1 | cut -d':' -f1) + node=$(pveca -l | grep "^\\s*$CID\\s*:" | awk {'print $3'}) + else + conf=$(grep -r "HOSTNAME=\\"$hostname\\"" /etc/pve/nodes/*/openvz/*.conf) + node=$(echo "${conf}" | cut -d"/" -f5) + CTID=$(echo "${conf}" | cut -d"/" -f7 | cut -d"\\." -f1) + fi + echo $CTID $node + }""") + + def prepare(self): + super(ProxmoxOVZ, self).prepare() + self.append(self.GET_PROXMOX_INFO) + + def get_vzset_args(self, context): + args = list(settings.VPS_DEFAULT_VZSET_ARGS) + for resource, arg_name in self.RESOURCES: + try: + allocation = context[resource] + except KeyError: + pass + else: + args.append('--%s %i' % (arg_name, allocation)) + return ' '.join(args) + + def run_ssh_commands(self, ssh_commands): + commands = '\n '.join(ssh_commands) + self.append(textwrap.dedent("""\ + cat << EOF | ssh root@${info[1]} + %s + EOF""") % commands + ) + + def save(self, vps): + # TODO create the container + context = self.get_context(vps) + self.append(textwrap.dedent(""" + info=( $(get_vz_info %(hostname)s) ) + echo "Managing ${info[@]}"\ + """) % context + ) + ssh_commands = [] + vzset_args = self.get_vzset_args(context) + if vzset_args: + context['vzset_args'] = vzset_args + ssh_commands.append("pvectl vzset ${info[0]} %(vzset_args)s" % context) + if hasattr(vps, 'password'): + context['password'] = vps.password.replace('$', '\\$') + ssh_commands.append(textwrap.dedent("""\ + echo 'root:%(password)s' \\ + | chroot /var/lib/vz/private/${info[0]} chpasswd -e""") % context + ) + self.run_ssh_commands(ssh_commands) + + def get_context(self, vps): + context = { + 'hostname': vps.hostname, + } + for resource, __ in self.RESOURCES: + try: + allocation = getattr(vps.resources, resource).allocated + except AttributeError: + pass + else: + context[resource] = allocation + return context + + +class ProxmoxOpenVZTraffic(ServiceMonitor): + model = 'vps.VPS' + resource = ServiceMonitor.TRAFFIC + monthly_sum_old_values = True + GET_PROXMOX_INFO = ProxmoxOVZ.GET_PROXMOX_INFO + + def prepare(self): + super(ProxmoxOpenVZTraffic, self).prepare() + self.append(self.GET_PROXMOX_INFO) + self.append(textwrap.dedent(""" + function monitor () { + object_id=$1 + hostname=$2 + info=( $(get_vz_info $hostname) ) + cat << EOF | ssh root@${info[1]} + vzctl exec ${info[0]} cat /proc/net/dev \\ + | grep venet0 \\ + | tr ':' ' ' \\ + | awk '{sum=\\$2+\\$10} END {printf ("$object_id %0.0f\\n", sum)}' + EOF + } + """) + ) + + def process(self, line): + """ diff with last stored state """ + object_id, value, state = super(ProxmoxOpenVZTraffic, self).process(line) + value = decimal.Decimal(value) + last = self.get_last_data(object_id) + if not last or last.state > value: + return object_id, value, value + return object_id, value-last.state, value + + def monitor(self, vps): + """ Get OpenVZ container traffic on a Proxmox cluster """ + context = self.get_context(vps) + self.append('monitor %(object_id)s %(hostname)s' % context) + + def get_context(self, vps): + return { + 'object_id': vps.id, + 'hostname': vps.hostname, + } diff --git a/orchestra/contrib/vps/migrations/0004_auto_20170528_2005.py b/orchestra/contrib/vps/migrations/0004_auto_20170528_2005.py new file mode 100644 index 00000000..ff91cc96 --- /dev/null +++ b/orchestra/contrib/vps/migrations/0004_auto_20170528_2005.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-05-28 18:05 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('vps', '0003_vps_is_active'), + ] + + operations = [ + migrations.AlterField( + model_name='vps', + name='template', + field=models.CharField(choices=[('debian7', 'Debian 7 - Wheezy'), ('placeholder', 'LXC placeholder')], default='placeholder', help_text='Initial template.', max_length=64, verbose_name='template'), + ), + migrations.AlterField( + model_name='vps', + name='type', + field=models.CharField(choices=[('openvz', 'OpenVZ container'), ('lxc', 'LXC container')], default='lxc', max_length=64, verbose_name='type'), + ), + ] diff --git a/orchestra/contrib/vps/settings.py b/orchestra/contrib/vps/settings.py index 5e65fe34..ec0e2a1a 100644 --- a/orchestra/contrib/vps/settings.py +++ b/orchestra/contrib/vps/settings.py @@ -4,13 +4,14 @@ from orchestra.contrib.settings import Setting VPS_TYPES = Setting('VPS_TYPES', ( ('openvz', 'OpenVZ container'), + ('lxc', 'LXC container') ), validators=[Setting.validate_choices] ) VPS_DEFAULT_TYPE = Setting('VPS_DEFAULT_TYPE', - 'openvz', + 'lxc', choices=VPS_TYPES ) @@ -18,13 +19,14 @@ VPS_DEFAULT_TYPE = Setting('VPS_DEFAULT_TYPE', VPS_TEMPLATES = Setting('VPS_TEMPLATES', ( ('debian7', 'Debian 7 - Wheezy'), + ('placeholder', 'LXC placeholder') ), validators=[Setting.validate_choices] ) VPS_DEFAULT_TEMPLATE = Setting('VPS_DEFAULT_TEMPLATE', - 'debian7', + 'placeholder', choices=VPS_TEMPLATES ) diff --git a/orchestra/contrib/webapps/backends/php.py b/orchestra/contrib/webapps/backends/php.py index 88395db9..d0383beb 100644 --- a/orchestra/contrib/webapps/backends/php.py +++ b/orchestra/contrib/webapps/backends/php.py @@ -32,6 +32,7 @@ class PHPController(WebAppServiceMixin, ServiceController): )) def save(self, webapp): + self.delete_old_config(webapp) context = self.get_context(webapp) self.create_webapp_dir(context) if webapp.type_instance.is_fpm: @@ -40,11 +41,32 @@ class PHPController(WebAppServiceMixin, ServiceController): self.save_fcgid(webapp, context) else: raise TypeError("Unknown PHP execution type") - self.append("# Clean non-used PHP FCGID wrappers and FPM pools") - self.delete_fcgid(webapp, context, preserve=True) - self.delete_fpm(webapp, context, preserve=True) +# LEGACY CLEANUP FUNCTIONS. TODO REMOVE WHEN SURE NOT NEEDED. +# self.delete_fcgid(webapp, context, preserve=True) +# self.delete_fpm(webapp, context, preserve=True) self.set_under_construction(context) - + + def delete_config(self,webapp): + context = self.get_context(webapp) + to_delete = [] + if webapp.type_instance.is_fpm: + to_delete.append(settings.WEBAPPS_PHPFPM_POOL_PATH % context) + to_delete.append(settings.WEBAPPS_FPM_LISTEN % context) + elif webapp.type_instance.is_fcgid: + to_delete.append(settings.WEBAPPS_FCGID_WRAPPER_PATH % context) + to_delete.append(settings.WEBAPPS_FCGID_CMD_OPTIONS_PATH % context) + for item in to_delete: + self.append('rm -f "{}"'.format(item)) + + def delete_old_config(self,webapp): + # Check if we loaded the old version of the webapp. If so, we're updating + # rather than creating, so we must make sure the old config files are removed. + if hasattr(webapp, '_old_self'): + self.append("# Clean old configuration files") + self.delete_config(webapp._old_self) + else: + self.append("# No old config files to delete") + def save_fpm(self, webapp, context): self.append(textwrap.dedent(""" # Generate FPM configuration @@ -99,10 +121,11 @@ class PHPController(WebAppServiceMixin, ServiceController): def delete(self, webapp): context = self.get_context(webapp) - if webapp.type_instance.is_fpm: - self.delete_fpm(webapp, context) - elif webapp.type_instance.is_fcgid: - self.delete_fcgid(webapp, context) + self.delete_old_config(webapp) +# if webapp.type_instance.is_fpm: +# self.delete_fpm(webapp, context) +# elif webapp.type_instance.is_fcgid: +# self.delete_fcgid(webapp, context) self.delete_webapp_dir(context) def has_sibilings(self, webapp, context): @@ -205,7 +228,7 @@ class PHPController(WebAppServiceMixin, ServiceController): context['fpm_listen'] = webapp.type_instance.FPM_LISTEN % context fpm_config = Template(textwrap.dedent("""\ ;; {{ banner }} - [{{ user }}] + [{{ user }}-{{app_name}}] user = {{ user }} group = {{ group }} diff --git a/orchestra/contrib/webapps/migrations/0002_auto_20170528_2011.py b/orchestra/contrib/webapps/migrations/0002_auto_20170528_2011.py new file mode 100644 index 00000000..18300cad --- /dev/null +++ b/orchestra/contrib/webapps/migrations/0002_auto_20170528_2011.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-05-28 18:11 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('webapps', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='webapp', + name='type', + field=models.CharField(choices=[('moodle-php', 'Moodle'), ('php', 'PHP'), ('python', 'Python'), ('static', 'Static'), ('symbolic-link', 'Symbolic link'), ('webalizer', 'Webalizer'), ('wordpress-php', 'WordPress')], max_length=32, verbose_name='type'), + ), + migrations.AlterField( + model_name='webappoption', + name='name', + field=models.CharField(choices=[(None, '-------'), ('FileSystem', [('public-root', 'Public root')]), ('Process', [('timeout', 'Process timeout'), ('processes', 'Number of processes')]), ('PHP', [('enable_functions', 'Enable functions'), ('disable_functions', 'Disable functions'), ('allow_url_include', 'Allow URL include'), ('allow_url_fopen', 'Allow URL fopen'), ('auto_append_file', 'Auto append file'), ('auto_prepend_file', 'Auto prepend file'), ('date.timezone', 'date.timezone'), ('default_socket_timeout', 'Default socket timeout'), ('display_errors', 'Display errors'), ('extension', 'Extension'), ('include_path', 'Include path'), ('magic_quotes_gpc', 'Magic quotes GPC'), ('magic_quotes_runtime', 'Magic quotes runtime'), ('magic_quotes_sybase', 'Magic quotes sybase'), ('max_input_time', 'Max input time'), ('max_input_vars', 'Max input vars'), ('memory_limit', 'Memory limit'), ('mysql.connect_timeout', 'Mysql connect timeout'), ('output_buffering', 'Output buffering'), ('register_globals', 'Register globals'), ('post_max_size', 'Post max size'), ('sendmail_path', 'Sendmail path'), ('session.bug_compat_warn', 'Session bug compat warning'), ('session.auto_start', 'Session auto start'), ('safe_mode', 'Safe mode'), ('suhosin.post.max_vars', 'Suhosin POST max vars'), ('suhosin.get.max_vars', 'Suhosin GET max vars'), ('suhosin.request.max_vars', 'Suhosin request max vars'), ('suhosin.session.encrypt', 'Suhosin session encrypt'), ('suhosin.simulation', 'Suhosin simulation'), ('suhosin.executor.include.whitelist', 'Suhosin executor include whitelist'), ('upload_max_filesize', 'Upload max filesize'), ('upload_tmp_dir', 'Upload tmp dir'), ('zend_extension', 'Zend extension')])], max_length=128, verbose_name='name'), + ), + ] diff --git a/orchestra/contrib/webapps/migrations/0003_webapp_target_server.py b/orchestra/contrib/webapps/migrations/0003_webapp_target_server.py new file mode 100644 index 00000000..4bddb317 --- /dev/null +++ b/orchestra/contrib/webapps/migrations/0003_webapp_target_server.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-07-04 08:49 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('orchestration', '0007_auto_20170528_2011'), + ('webapps', '0002_auto_20170528_2011'), + ] + + operations = [ + migrations.AddField( + model_name='webapp', + name='target_server', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='webapps', to='orchestration.Server', verbose_name='Target Server'), + preserve_default=False, + ), + ] diff --git a/orchestra/contrib/webapps/migrations/0004_webapp_comments.py b/orchestra/contrib/webapps/migrations/0004_webapp_comments.py new file mode 100644 index 00000000..25ad65d7 --- /dev/null +++ b/orchestra/contrib/webapps/migrations/0004_webapp_comments.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2020-02-04 11:17 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('webapps', '0003_webapp_target_server'), + ] + + operations = [ + migrations.AddField( + model_name='webapp', + name='comments', + field=models.TextField(default=''), + preserve_default=False, + ), + ] diff --git a/orchestra/contrib/webapps/migrations/0005_auto_20200204_1218.py b/orchestra/contrib/webapps/migrations/0005_auto_20200204_1218.py new file mode 100644 index 00000000..80b315f7 --- /dev/null +++ b/orchestra/contrib/webapps/migrations/0005_auto_20200204_1218.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2020-02-04 11:18 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('webapps', '0004_webapp_comments'), + ] + + operations = [ + migrations.AlterField( + model_name='webapp', + name='comments', + field=models.TextField(default=''), + ), + ] diff --git a/orchestra/contrib/webapps/models.py b/orchestra/contrib/webapps/models.py index 48e21511..2180e01b 100644 --- a/orchestra/contrib/webapps/models.py +++ b/orchestra/contrib/webapps/models.py @@ -23,6 +23,9 @@ class WebApp(models.Model): related_name='webapps') data = JSONField(_("data"), blank=True, default={}, help_text=_("Extra information dependent of each service.")) + target_server = models.ForeignKey('orchestration.Server', verbose_name=_("Target Server"), + related_name='webapps') + comments = models.TextField(default="", blank=True) # CMS webapps usually need a database and dbuser, with these virtual fields we tell the ORM to delete them databases = VirtualDatabaseRelation('databases.Database') diff --git a/orchestra/contrib/webapps/options.py b/orchestra/contrib/webapps/options.py index 79bd2802..dad0ec87 100644 --- a/orchestra/contrib/webapps/options.py +++ b/orchestra/contrib/webapps/options.py @@ -102,8 +102,8 @@ class Processes(AppOption): # FCGID MaxProcesses # FPM pm.max_children verbose_name = _("Number of processes") - help_text = _("Maximum number of children that can be alive at the same time (a number between 0 and 9).") - regex = r'^[0-9]{1,2}$' + help_text = _("Maximum number of children that can be alive at the same time (a number between 0 and 99).") + regex = r'^[0-9]{1,3}$' group = AppOption.PROCESS diff --git a/orchestra/contrib/webapps/serializers.py b/orchestra/contrib/webapps/serializers.py index cd9f657a..abfe9dc6 100644 --- a/orchestra/contrib/webapps/serializers.py +++ b/orchestra/contrib/webapps/serializers.py @@ -30,7 +30,7 @@ class WebAppSerializer(AccountSerializerMixin, HyperlinkedModelSerializer): class Meta: model = WebApp - fields = ('url', 'id', 'name', 'type', 'options', 'data') + fields = ('url', 'id', 'name', 'type', 'options', 'data',) postonly_fields = ('name', 'type') def __init__(self, *args, **kwargs): diff --git a/orchestra/contrib/webapps/settings.py b/orchestra/contrib/webapps/settings.py index b8614dc2..625de96f 100644 --- a/orchestra/contrib/webapps/settings.py +++ b/orchestra/contrib/webapps/settings.py @@ -99,6 +99,7 @@ WEBAPPS_PHP_VERSIONS = Setting('WEBAPPS_PHP_VERSIONS', ( ('5.3-cgi', 'PHP 5.3 FCGID'), ('5.2-cgi', 'PHP 5.2 FCGID'), ('4-cgi', 'PHP 4 FCGID'), + ('7-fpm', 'PHP 7 FPM') ), help_text="Execution modle choose by ending -fpm or -cgi.", validators=[Setting.validate_choices] diff --git a/orchestra/contrib/webapps/signals.py b/orchestra/contrib/webapps/signals.py index ff849139..9bffd7d5 100644 --- a/orchestra/contrib/webapps/signals.py +++ b/orchestra/contrib/webapps/signals.py @@ -3,19 +3,22 @@ from django.dispatch import receiver from .models import WebApp - # Admin bulk deletion doesn't call model.delete() # So, signals are used instead of model method overriding @receiver(pre_save, sender=WebApp, dispatch_uid='webapps.type.save') def type_save(sender, *args, **kwargs): instance = kwargs['instance'] + # Since a webapp might need to cleanup its old config files, the data + # from the OLD VERSION of the webapp is needed. + if instance.pk: + instance._old_self = type(instance).objects.get(id=instance.pk) instance.type_instance.save() - @receiver(pre_delete, sender=WebApp, dispatch_uid='webapps.type.delete') def type_delete(sender, *args, **kwargs): instance = kwargs['instance'] + instance._old_self = type(instance).objects.get(id=instance.pk) try: instance.type_instance.delete() except KeyError: diff --git a/orchestra/contrib/websites/admin.py b/orchestra/contrib/websites/admin.py index fed31707..e447683a 100644 --- a/orchestra/contrib/websites/admin.py +++ b/orchestra/contrib/websites/admin.py @@ -69,7 +69,7 @@ class WebsiteAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): fieldsets = ( (None, { 'classes': ('extrapretty',), - 'fields': ('account_link', 'name', 'protocol', 'domains', 'is_active'), + 'fields': ('account_link', 'name', 'protocol', 'target_server', 'domains', 'is_active', 'comments'), }), ) form = WebsiteAdminForm diff --git a/orchestra/contrib/websites/backends/apache.py b/orchestra/contrib/websites/backends/apache.py index 90e9d8be..2cee8c4e 100644 --- a/orchestra/contrib/websites/backends/apache.py +++ b/orchestra/contrib/websites/backends/apache.py @@ -58,7 +58,8 @@ class Apache2Controller(ServiceController): context.update({ 'port': self.HTTPS_PORT if ssl else self.HTTP_PORT, 'vhost_set_fcgid': False, - 'server_alias_lines': ' \\\n '.join(context['server_alias']) + 'server_alias_lines': ' \\\n '.join(context['server_alias']), + 'suexec_needed': site.target_server == 'web.pangea.lan' }) context['extra_conf'] = self.get_extra_conf(site, context, ssl) return Template(textwrap.dedent("""\ @@ -71,7 +72,8 @@ class Apache2Controller(ServiceController): CustomLog {{ access_log }} common{% endif %}\ {% if error_log %} ErrorLog {{ error_log }}{% endif %} - SuexecUserGroup {{ user }} {{ group }}\ + {% if suexec_needed %} + SuexecUserGroup {{ user }} {{ group }}{% endif %}\ {% for line in extra_conf.splitlines %} {{ line | safe }}{% endfor %} @@ -225,15 +227,18 @@ class Apache2Controller(ServiceController): target = 'fcgi://%(socket)s%(app_path)s/$1' else: # UNIX socket - target = 'unix:%(socket)s|fcgi://127.0.0.1%(app_path)s/' - if context['location']: - # FIXME unix sockets do not support $1 - target = 'unix:%(socket)s|fcgi://127.0.0.1%(app_path)s/$1' + target = 'unix:%(socket)s|fcgi://127.0.0.1/' context.update({ 'app_path': os.path.normpath(app_path), 'socket': socket, }) - directives = "ProxyPassMatch ^%(location)s/(.*\.php(/.*)?)$ {target}\n".format(target=target) % context + directives = textwrap.dedent(""" +