diff --git a/orchestra/contrib/databases/admin.py b/orchestra/contrib/databases/admin.py index 21ca6da9..42e4952f 100644 --- a/orchestra/contrib/databases/admin.py +++ b/orchestra/contrib/databases/admin.py @@ -49,8 +49,6 @@ class DatabaseAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): add_form = DatabaseCreationForm readonly_fields = ('account_link', 'display_users',) filter_horizontal = ['users'] - # filter_by_account_fields = ('users',) - # list_prefetch_related = ('users',) actions = (list_accounts, save_selected) @mark_safe diff --git a/orchestra/contrib/systemusers/migrations/0003_auto_20230724_1813.py b/orchestra/contrib/systemusers/migrations/0003_auto_20230724_1813.py new file mode 100644 index 00000000..1e1a246f --- /dev/null +++ b/orchestra/contrib/systemusers/migrations/0003_auto_20230724_1813.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.28 on 2023-07-24 16:13 + +from django.db import migrations, models +import orchestra.core.validators + + +class Migration(migrations.Migration): + + dependencies = [ + ('systemusers', '0002_webappusers'), + ] + + operations = [ + migrations.AlterModelOptions( + name='webappusers', + options={'verbose_name': 'WebAppUser', 'verbose_name_plural': 'WebappUsers'}, + ), + migrations.AlterField( + model_name='webappusers', + name='home', + field=models.CharField(blank=True, help_text='name dir webapp /home/<main>/webapps/<DirName>', max_length=256, validators=[orchestra.core.validators.validate_string_dir], verbose_name='WebappDir'), + ), + ] diff --git a/orchestra/contrib/webapps/admin.py b/orchestra/contrib/webapps/admin.py index d1dc7a18..b75ce688 100644 --- a/orchestra/contrib/webapps/admin.py +++ b/orchestra/contrib/webapps/admin.py @@ -57,7 +57,7 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin) list_filter = ('type', HasWebsiteListFilter, DetailListFilter) inlines = [WebAppOptionInline] readonly_fields = ('account_link',) - change_readonly_fields = ('name', 'type', 'display_websites') + change_readonly_fields = ('name', 'type', 'display_websites', 'sftpuser', 'target_server') search_fields = ('name', 'account__username', 'data', 'website__domains__name') list_prefetch_related = ('content_set__website', 'content_set__website__domains') plugin = AppType @@ -67,6 +67,7 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin) display_type = display_plugin_field('type') + @mark_safe def display_websites(self, webapp): websites = [] diff --git a/orchestra/contrib/webapps/backends/static.py b/orchestra/contrib/webapps/backends/static.py index f424d505..0756759c 100644 --- a/orchestra/contrib/webapps/backends/static.py +++ b/orchestra/contrib/webapps/backends/static.py @@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _ from orchestra.contrib.orchestration import ServiceController from . import WebAppServiceMixin - +from ..settings import WEBAPP_NEW_SERVERS class StaticController(WebAppServiceMixin, ServiceController): """ @@ -15,9 +15,21 @@ class StaticController(WebAppServiceMixin, ServiceController): def save(self, webapp): context = self.get_context(webapp) - self.create_webapp_dir(context) - self.set_under_construction(context) + if context.get('target_server').name in WEBAPP_NEW_SERVERS: + self.check_webapp_dir(context) + self.set_under_construction(context) + # TODO: crea el usuario sftp + # webapp.name = webapp.sftpuser.directory.replace("webapps/", "") + # webapp.save() + + else: + self.create_webapp_dir(context) + self.set_under_construction(context) def delete(self, webapp): context = self.get_context(webapp) - self.delete_webapp_dir(context) + if context.get('target_server').name not in WEBAPP_NEW_SERVERS: + self.delete_webapp_dir(context) + else: + # TODO: elimina el usuario sftp + pass diff --git a/orchestra/contrib/webapps/migrations/0001_initial.py b/orchestra/contrib/webapps/migrations/0001_initial.py index 54a329d5..041f7932 100644 --- a/orchestra/contrib/webapps/migrations/0001_initial.py +++ b/orchestra/contrib/webapps/migrations/0001_initial.py @@ -1,16 +1,18 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +# Generated by Django 2.2.28 on 2023-07-24 16:08 -from django.db import models, migrations -import django.db.models.deletion -import orchestra.core.validators -import jsonfield.fields from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import jsonfield.fields +import orchestra.core.validators class Migration(migrations.Migration): + initial = True + dependencies = [ + ('orchestration', '__first__'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] @@ -18,36 +20,32 @@ class Migration(migrations.Migration): migrations.CreateModel( name='WebApp', fields=[ - ('id', models.AutoField(verbose_name='ID', auto_created=True, serialize=False, primary_key=True)), - ('name', models.CharField(verbose_name='name', validators=[orchestra.core.validators.validate_name], help_text='The app will be installed in %(home)s/webapps/%(app_name)s', max_length=128)), - ('type', models.CharField(verbose_name='type', max_length=32, choices=[('php', 'PHP'), ('python', 'Python'), ('static', 'Static'), ('symbolic-link', 'Symbolic link'), ('webalizer', 'Webalizer'), ('wordpress-php', 'WordPress')])), - ('data', jsonfield.fields.JSONField(verbose_name='data', blank=True, help_text='Extra information dependent of each service.', default={})), - ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, verbose_name='Account', related_name='webapps', to=settings.AUTH_USER_MODEL)), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='The app will be installed in %(home)s/webapps/%(app_name)s', max_length=128, validators=[orchestra.core.validators.validate_name], verbose_name='name')), + ('type', 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')), + ('data', jsonfield.fields.JSONField(blank=True, default={}, help_text='Extra information dependent of each service.', verbose_name='data')), + ('comments', models.TextField(blank=True, default='')), + ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webapps', to=settings.AUTH_USER_MODEL, verbose_name='Account')), + ('target_server', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webapps', to='orchestration.Server', verbose_name='Target Server')), ], options={ 'verbose_name': 'Web App', 'verbose_name_plural': 'Web Apps', + 'unique_together': {('name', 'account')}, }, ), migrations.CreateModel( name='WebAppOption', fields=[ - ('id', models.AutoField(verbose_name='ID', auto_created=True, serialize=False, primary_key=True)), - ('name', models.CharField(verbose_name='name', max_length=128, choices=[(None, '-------'), ('FileSystem', [('public-root', 'Public root')]), ('Process', [('timeout', 'Process timeout'), ('processes', 'Number of processes')]), ('PHP', [('enable_functions', 'Enable 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'), ('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'), ('zend_extension', 'Zend extension')])])), - ('value', models.CharField(verbose_name='value', max_length=256)), - ('webapp', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, verbose_name='Web application', related_name='options', to='webapps.WebApp')), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', 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'), ('open_basedir', 'Open basedir'), ('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')), + ('value', models.CharField(max_length=256, verbose_name='value')), + ('webapp', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='options', to='webapps.WebApp', verbose_name='Web application')), ], options={ 'verbose_name': 'option', 'verbose_name_plural': 'options', + 'unique_together': {('webapp', 'name')}, }, ), - migrations.AlterUniqueTogether( - name='webappoption', - unique_together=set([('webapp', 'name')]), - ), - migrations.AlterUniqueTogether( - name='webapp', - unique_together=set([('name', 'account')]), - ), ] diff --git a/orchestra/contrib/webapps/migrations/0002_webapp_sftpuser.py b/orchestra/contrib/webapps/migrations/0002_webapp_sftpuser.py new file mode 100644 index 00000000..cfbaec51 --- /dev/null +++ b/orchestra/contrib/webapps/migrations/0002_webapp_sftpuser.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.28 on 2023-07-24 16:13 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('systemusers', '0003_auto_20230724_1813'), + ('webapps', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='webapp', + name='sftpuser', + field=models.ForeignKey(blank=True, help_text='This option is only required for the new webservers.', null=True, on_delete=django.db.models.deletion.CASCADE, to='systemusers.WebappUsers', verbose_name='SFTP user'), + ), + ] diff --git a/orchestra/contrib/webapps/models.py b/orchestra/contrib/webapps/models.py index 21f07979..2f1fe374 100644 --- a/orchestra/contrib/webapps/models.py +++ b/orchestra/contrib/webapps/models.py @@ -26,6 +26,8 @@ class WebApp(models.Model): target_server = models.ForeignKey('orchestration.Server', on_delete=models.CASCADE, verbose_name=_("Target Server"), related_name='webapps') comments = models.TextField(default="", blank=True) + sftpuser = models.ForeignKey('systemusers.WebappUsers', blank=True, null=True, on_delete=models.CASCADE , + verbose_name=_("SFTP user"), help_text=_("This option is only required for the new webservers.")) # 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/types/misc.py b/orchestra/contrib/webapps/types/misc.py index 1619bbf3..cfcde517 100644 --- a/orchestra/contrib/webapps/types/misc.py +++ b/orchestra/contrib/webapps/types/misc.py @@ -2,14 +2,64 @@ import os from django import forms from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ValidationError from rest_framework import serializers +from orchestra.core import validators +from orchestra.plugins.forms import PluginDataForm +from orchestra.utils.python import random_ascii + from ..options import AppOption +from ..settings import WEBAPP_NEW_SERVERS from . import AppType from .php import PHPApp, PHPAppForm, PHPAppSerializer +class StaticForm(PluginDataForm): + username = forms.CharField(label=_("Username"), max_length=16, + required=False, validators=[validators.validate_name], + help_text=_("Required. 16 characters or fewer. Letters, digits and " + "@/./+/-/_ only."), + error_messages={ + 'invalid': _("This value may contain 16 characters or fewer, only letters, numbers and " + "@/./+/-/_ characters.")}) + password1 = forms.CharField(label=_("Password"), required=False, + widget=forms.PasswordInput(attrs={'autocomplete': 'off'}), + validators=[validators.validate_password], + help_text=_("Suggestion: %s") % random_ascii(15)) + password2 = forms.CharField(label=_("Password confirmation"), required=False, + widget=forms.PasswordInput, + help_text=_("Enter the same password as above, for verification.")) + + + def __init__(self, *args, **kwargs): + super(StaticForm, self).__init__(*args, **kwargs) + if self.instance.id is None: + self.fields['sftpuser'].widget = forms.HiddenInput() + else: + self.fields['username'].widget = forms.HiddenInput() + self.fields['password1'].widget = forms.HiddenInput() + self.fields['password2'].widget = forms.HiddenInput() + + def clean(self): + webapp_server = self.cleaned_data.get("target_server") + sftpuser = self.cleaned_data.get('sftpuser') + if webapp_server is None: + self.add_error("target_server", _("choice some target_server")) + else: + if webapp_server.name in WEBAPP_NEW_SERVERS and sftpuser == None: + self.add_error("sftpuser", _("SFTP user is required by new webservers")) + + def clean_password2(self): + password1 = self.cleaned_data.get("password1") + password2 = self.cleaned_data.get("password2") + if password1 and password2 and password1 != password2: + msg = _("The two password fields didn't match.") + raise ValidationError(msg) + return password2 + + class StaticApp(AppType): name = 'static' verbose_name = "Static" @@ -17,7 +67,8 @@ class StaticApp(AppType): "Apache2 will be used to serve static content and execute CGI files.") icon = 'orchestra/icons/apps/Static.png' option_groups = (AppOption.FILESYSTEM,) - + form = StaticForm + def get_directive(self): return ('static', self.instance.get_path())