diff --git a/orchestra/contrib/systemusers/forms.py b/orchestra/contrib/systemusers/forms.py index ab0b8c49..c22d24c0 100644 --- a/orchestra/contrib/systemusers/forms.py +++ b/orchestra/contrib/systemusers/forms.py @@ -6,7 +6,7 @@ from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ from orchestra.forms import UserCreationForm, UserChangeForm -from orchestra.contrib.webapps.settings import WEBAPP_NEW_SERVERS +from orchestra.settings import NEW_SERVERS from . import settings from .models import SystemUser @@ -176,7 +176,7 @@ class WebappUserFormMixin(object): if not self.instance.pk: server = self.cleaned_data.get('target_server') if server: - if server.name not in WEBAPP_NEW_SERVERS: + if server.name not in NEW_SERVERS: self.add_error("target_server", _(f"{server} does not belong to the new servers")) return self.cleaned_data diff --git a/orchestra/contrib/webapps/admin.py b/orchestra/contrib/webapps/admin.py index 09422085..24bd4948 100644 --- a/orchestra/contrib/webapps/admin.py +++ b/orchestra/contrib/webapps/admin.py @@ -109,7 +109,7 @@ class WebAppAdmin(SelectPluginAdminMixin, AccountAdminMixin, ExtendedModelAdmin) def save_model(self, request, obj, form, change): if not change: - user = form.cleaned_data['username'] + user = form.cleaned_data.get('username') if user: user = WebappUsers( username=form.cleaned_data['username'], diff --git a/orchestra/contrib/webapps/backends/__init__.py b/orchestra/contrib/webapps/backends/__init__.py index 2e120fc0..0beb18a9 100644 --- a/orchestra/contrib/webapps/backends/__init__.py +++ b/orchestra/contrib/webapps/backends/__init__.py @@ -1,8 +1,8 @@ import pkgutil import textwrap - +from django.template import Template, Context from .. import settings - +from orchestra.settings import NEW_SERVERS class WebAppServiceMixin(object): model = 'webapps.WebApp' @@ -19,6 +19,7 @@ class WebAppServiceMixin(object): CREATED=0 if [[ ! -e %(app_path)s ]]; then mkdir -p %(app_path)s + chown %(sftpuser)s:%(sftpuser)s %(app_path)s CREATED=1 elif [[ -z $( ls -A %(app_path)s ) ]]; then CREATED=1 @@ -38,6 +39,15 @@ class WebAppServiceMixin(object): def set_under_construction(self, context): if context['under_construction_path']: + # cambios de permisos en servidores nuevos + perms = Template(textwrap.dedent("""\ + {% if sftpuser %} + chown -R {{ sftpuser }}:{{ sftpuser }} {{ app_path }}/* {% else %} + chown -R {{ user }}:{{ group }} {{ app_path }}/* + {% endif %} + """ + )) + context.update({'perms' : perms.render(Context(context))}) self.append(textwrap.dedent(""" # Set under construction if needed if [[ $CREATED == 1 && ! $(ls -A %(app_path)s | head -n1) ]]; then @@ -46,11 +56,11 @@ class WebAppServiceMixin(object): sleep 2 if [[ ! $(ls -A %(app_path)s | head -n1) ]]; then cp -r %(under_construction_path)s %(app_path)s - chown -R %(user)s:%(group)s %(app_path)s/* + %(perms)s fi' &> /dev/null & fi""") % context ) - + def delete_webapp_dir(self, context): if context['deleted_app_path']: self.append(textwrap.dedent("""\ @@ -68,8 +78,8 @@ class WebAppServiceMixin(object): def get_context(self, webapp): context = webapp.type_instance.get_directive_context() context.update({ - 'user': webapp.sftpuser.username if webapp.target_server.name in settings.WEBAPP_NEW_SERVERS else webapp.get_username(), - 'group': webapp.sftpuser.username if webapp.target_server.name in settings.WEBAPP_NEW_SERVERS else webapp.get_groupname(), + 'user': webapp.get_username(), + 'group': webapp.get_groupname(), 'app_name': webapp.name, 'app_type': webapp.type, 'app_path': webapp.get_path(), @@ -77,6 +87,7 @@ class WebAppServiceMixin(object): 'under_construction_path': settings.WEBAPPS_UNDER_CONSTRUCTION_PATH, 'is_mounted': webapp.content_set.exists(), 'target_server': webapp.target_server, + 'sftpuser' : webapp.sftpuser.username if webapp.target_server.name in NEW_SERVERS else None }) context['deleted_app_path'] = settings.WEBAPPS_MOVE_ON_DELETE_PATH % context return context diff --git a/orchestra/contrib/webapps/backends/php.py b/orchestra/contrib/webapps/backends/php.py index 7478fc41..04018dba 100644 --- a/orchestra/contrib/webapps/backends/php.py +++ b/orchestra/contrib/webapps/backends/php.py @@ -5,6 +5,7 @@ from collections import OrderedDict from django.template import Template, Context from django.utils.translation import gettext_lazy as _ +from orchestra.settings import NEW_SERVERS from orchestra.contrib.orchestration import ServiceController from . import WebAppServiceMixin @@ -34,7 +35,12 @@ class PHPController(WebAppServiceMixin, ServiceController): def save(self, webapp): self.delete_old_config(webapp) context = self.get_context(webapp) - self.create_webapp_dir(context) + + if context.get('target_server').name in NEW_SERVERS: + self.check_webapp_dir(context) + else: + self.create_webapp_dir(context) + if webapp.type_instance.is_fpm: self.save_fpm(webapp, context) elif webapp.type_instance.is_fcgid: @@ -122,11 +128,10 @@ class PHPController(WebAppServiceMixin, ServiceController): def delete(self, webapp): context = self.get_context(webapp) 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) + if context.get('target_server').name in NEW_SERVERS: + webapp.sftpuser.delete() + else: + self.delete_webapp_dir(context) def has_sibilings(self, webapp, context): return type(webapp).objects.filter( @@ -229,12 +234,21 @@ class PHPController(WebAppServiceMixin, ServiceController): fpm_config = Template(textwrap.dedent("""\ ;; {{ banner }} [{{ user }}-{{app_name}}] + {% if sftpuser %} + user = {{ sftpuser }} + group = {{ sftpuser }} + + listen = {{ fpm_listen | safe }} + listen.owner = root + listen.group = {{ sftpuser }} + {% else %} user = {{ user }} group = {{ group }} listen = {{ fpm_listen | safe }} listen.owner = {{ user }} listen.group = {{ group }} + {% endif %} pm = ondemand pm.max_requests = {{ max_requests }} @@ -313,6 +327,7 @@ class PHPController(WebAppServiceMixin, ServiceController): context = super().get_context(webapp) context.update({ 'max_requests': settings.WEBAPPS_PHP_MAX_REQUESTS, + 'target_server': webapp.target_server, }) self.update_fpm_context(webapp, context) self.update_fcgid_context(webapp, context) diff --git a/orchestra/contrib/webapps/backends/static.py b/orchestra/contrib/webapps/backends/static.py index c2f2cb5e..ea2061a0 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 +from orchestra.settings import NEW_SERVERS class StaticController(WebAppServiceMixin, ServiceController): """ @@ -15,7 +15,7 @@ class StaticController(WebAppServiceMixin, ServiceController): def save(self, webapp): context = self.get_context(webapp) - if context.get('target_server').name in WEBAPP_NEW_SERVERS: + if context.get('target_server').name in NEW_SERVERS: self.check_webapp_dir(context) self.set_under_construction(context) else: @@ -24,7 +24,7 @@ class StaticController(WebAppServiceMixin, ServiceController): def delete(self, webapp): context = self.get_context(webapp) - if context.get('target_server').name in WEBAPP_NEW_SERVERS: + if context.get('target_server').name in NEW_SERVERS: webapp.sftpuser.delete() else: self.delete_webapp_dir(context) diff --git a/orchestra/contrib/webapps/migrations/0003_auto_20230728_1639.py b/orchestra/contrib/webapps/migrations/0003_auto_20230728_1639.py new file mode 100644 index 00000000..68e316b2 --- /dev/null +++ b/orchestra/contrib/webapps/migrations/0003_auto_20230728_1639.py @@ -0,0 +1,20 @@ +# Generated by Django 2.2.28 on 2023-07-28 14:39 + +from django.conf import settings +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('orchestration', '__first__'), + ('webapps', '0002_webapp_sftpuser'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='webapp', + unique_together={('name', 'account', 'target_server')}, + ), + ] diff --git a/orchestra/contrib/webapps/models.py b/orchestra/contrib/webapps/models.py index 2f1fe374..642922a4 100644 --- a/orchestra/contrib/webapps/models.py +++ b/orchestra/contrib/webapps/models.py @@ -34,7 +34,7 @@ class WebApp(models.Model): databaseusers = VirtualDatabaseUserRelation('databases.DatabaseUser') class Meta: - unique_together = ('name', 'account') + unique_together = ('name', 'account', 'target_server') verbose_name = _("Web App") verbose_name_plural = _("Web Apps") diff --git a/orchestra/contrib/webapps/settings.py b/orchestra/contrib/webapps/settings.py index a5c5476d..1f5ae9da 100644 --- a/orchestra/contrib/webapps/settings.py +++ b/orchestra/contrib/webapps/settings.py @@ -33,7 +33,7 @@ WEBAPPS_FPM_DEFAULT_MAX_CHILDREN = Setting('WEBAPPS_FPM_DEFAULT_MAX_CHILDREN', WEBAPPS_PHPFPM_POOL_PATH = Setting('WEBAPPS_PHPFPM_POOL_PATH', - '/etc/php%(php_version_number)s/fpm/pool.d/%(user)s-%(app_name)s.conf', + '/etc/php/%(php_version_number)s/fpm/pool.d/%(user)s-%(app_name)s.conf', help_text="Available fromat names: %s" % ', '.join(_php_names), validators=[Setting.string_format_validator(_php_names)], ) @@ -283,10 +283,3 @@ WEBAPPS_CMS_CACHE_DIR = Setting('WEBAPPS_CMS_CACHE_DIR', help_text="Server-side cache directori for CMS tarballs.", ) -WEBAPP_NEW_SERVERS = Setting('WEBAPP_NEW_SERVERS', - ( - 'bookworm', - 'web-11.pangea.lan', - 'web-12.pangea.lan', - ) -) diff --git a/orchestra/contrib/webapps/types/misc.py b/orchestra/contrib/webapps/types/misc.py index 977bac6e..92fd1fa3 100644 --- a/orchestra/contrib/webapps/types/misc.py +++ b/orchestra/contrib/webapps/types/misc.py @@ -2,64 +2,14 @@ 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 orchestra.plugins.forms import ExtendedPluginDataForm 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) - self.fields['sftpuser'].widget = forms.HiddenInput() - if self.instance.id is not None: - self.fields['username'].widget = forms.HiddenInput() - self.fields['password1'].widget = forms.HiddenInput() - self.fields['password2'].widget = forms.HiddenInput() - - def clean(self): - if not self.instance.id: - webapp_server = self.cleaned_data.get("target_server") - username = self.cleaned_data.get('username') - if webapp_server is None: - self.add_error("target_server", _("choice some target_server")) - else: - if webapp_server.name in WEBAPP_NEW_SERVERS and username == '': - self.add_error("username", _("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" @@ -67,7 +17,7 @@ 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 + form = ExtendedPluginDataForm def get_directive(self): return ('static', self.instance.get_path()) diff --git a/orchestra/contrib/webapps/types/php.py b/orchestra/contrib/webapps/types/php.py index accdb0a8..4aff667f 100644 --- a/orchestra/contrib/webapps/types/php.py +++ b/orchestra/contrib/webapps/types/php.py @@ -5,9 +5,10 @@ from django import forms from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from orchestra.plugins.forms import PluginDataForm +from orchestra.plugins.forms import PluginDataForm, ExtendedPluginDataForm from orchestra.utils.functional import cached -from orchestra.utils.python import OrderedSet +from orchestra.utils.python import OrderedSet, random_ascii +from orchestra.settings import NEW_SERVERS from .. import settings, utils from ..options import AppOption @@ -19,13 +20,23 @@ help_message = _("Version of PHP used to execute this webapp.
" "Changing the PHP version may result in application malfunction, " "make sure that everything continue to work as expected.") - -class PHPAppForm(PluginDataForm): +class PHPAppForm(ExtendedPluginDataForm): php_version = forms.ChoiceField(label=_("PHP version"), choices=settings.WEBAPPS_PHP_VERSIONS, initial=settings.WEBAPPS_DEFAULT_PHP_VERSION, help_text=help_message) - + + # def clean_php_version(self): + # # TODO: restriccin PHP diferentes servers + # if not self.instance.id: + # webapp_server = self.cleaned_data.get("target_server") + # php_version = self.cleaned_data.get('php_version') + # if webapp_server is None: + # pass + # else: + # if webapp_server.name in NEW_SERVERS and not username: + # self.add_error("php_version", _(f"Server {webapp_server} not allow {php_version}")) + class PHPAppSerializer(serializers.Serializer): php_version = serializers.ChoiceField(label=_("PHP version"), diff --git a/orchestra/plugins/forms.py b/orchestra/plugins/forms.py index d788f369..16885756 100644 --- a/orchestra/plugins/forms.py +++ b/orchestra/plugins/forms.py @@ -4,6 +4,12 @@ from django.utils.encoding import force_str from orchestra.admin.utils import admin_link from orchestra.forms.widgets import SpanWidget +from django.core.exceptions import ValidationError +from orchestra.core import validators +from orchestra.utils.python import random_ascii +from orchestra.settings import NEW_SERVERS +from django.utils.translation import gettext_lazy as _ + class PluginForm(forms.ModelForm): def __init__(self, *args, **kwargs): @@ -71,3 +77,51 @@ class PluginModelAdapterForm(PluginForm): display = '%s change' % link self.fields[self.plugin_field].widget = SpanWidget(original=value, display=display) help_text = self.fields[self.plugin_field].help_text + + +# -------------------------------------------------- + +class ExtendedPluginDataForm(PluginDataForm): + # aƱade campos de username para creacion de sftpuser en servidores nuevos + 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(ExtendedPluginDataForm, self).__init__(*args, **kwargs) + self.fields['sftpuser'].widget = forms.HiddenInput() + if self.instance.id is not None: + self.fields['username'].widget = forms.HiddenInput() + self.fields['password1'].widget = forms.HiddenInput() + self.fields['password2'].widget = forms.HiddenInput() + + def clean_username(self): + if not self.instance.id: + webapp_server = self.cleaned_data.get("target_server") + username = self.cleaned_data.get('username') + if webapp_server is None: + self.add_error("target_server", _("choice some server")) + else: + if webapp_server.name in NEW_SERVERS and not username: + self.add_error("username", _("SFTP user is required by new webservers")) + return username + + 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 \ No newline at end of file diff --git a/orchestra/settings.py b/orchestra/settings.py index 86c39ca8..be3a191f 100644 --- a/orchestra/settings.py +++ b/orchestra/settings.py @@ -90,3 +90,11 @@ ORCHESTRA_SSH_CONTROL_PATH = Setting('ORCHESTRA_SSH_CONTROL_PATH', '~/.ssh/orchestra-%r-%h-%p', help_text='Location for the control socket used by the multiplexed sessions, used for SSH connection reuse.' ) + +NEW_SERVERS = Setting('NEW_SERVERS', + ( + 'bookworm', + 'web-11.pangea.lan', + 'web-12.pangea.lan', + ) +)