parent
d6c9620b54
commit
7d9805869d
|
@ -9,8 +9,8 @@ from orchestra.contrib.accounts.filters import IsActiveListFilter
|
||||||
|
|
||||||
from .actions import set_permission, create_link
|
from .actions import set_permission, create_link
|
||||||
from .filters import IsMainListFilter
|
from .filters import IsMainListFilter
|
||||||
from .forms import SystemUserCreationForm, SystemUserChangeForm
|
from .forms import SystemUserCreationForm, SystemUserChangeForm, WebappUserChangeForm, WebappUserCreationForm
|
||||||
from .models import SystemUser
|
from .models import SystemUser, WebappUsers
|
||||||
|
|
||||||
|
|
||||||
class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModelAdmin):
|
class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
|
@ -78,4 +78,34 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende
|
||||||
return super(SystemUserAdmin, self).has_delete_permission(request, obj)
|
return super(SystemUserAdmin, self).has_delete_permission(request, obj)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class WebappUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
'username', 'account_link', 'shell', 'home', 'target_server'
|
||||||
|
)
|
||||||
|
fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'fields': ('account_link', 'username', 'password', )
|
||||||
|
}),
|
||||||
|
(_("System"), {
|
||||||
|
'fields': ('shell', 'home', 'target_server'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
add_fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'fields': ('account_link', 'username', 'password1', 'password2')
|
||||||
|
}),
|
||||||
|
(_("System"), {
|
||||||
|
'fields': ('shell', 'home', 'target_server'),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
search_fields = ('username', 'account__username')
|
||||||
|
readonly_fields = ('account_link',)
|
||||||
|
change_readonly_fields = ('username', 'home', 'target_server')
|
||||||
|
add_form = WebappUserCreationForm
|
||||||
|
form = WebappUserChangeForm
|
||||||
|
ordering = ('-id',)
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(SystemUser, SystemUserAdmin)
|
admin.site.register(SystemUser, SystemUserAdmin)
|
||||||
|
admin.site.register(WebappUsers, WebappUserAdmin)
|
|
@ -2,6 +2,7 @@ import sys
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.db.models.signals import post_migrate
|
from django.db.models.signals import post_migrate
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from orchestra.core import services
|
from orchestra.core import services
|
||||||
|
|
||||||
|
@ -11,11 +12,12 @@ class SystemUsersConfig(AppConfig):
|
||||||
verbose_name = "System users"
|
verbose_name = "System users"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from .models import SystemUser
|
from .models import SystemUser, WebappUsers
|
||||||
services.register(SystemUser, icon='roleplaying.png')
|
services.register(SystemUser, icon='roleplaying.png')
|
||||||
if 'migrate' in sys.argv and 'accounts' not in sys.argv:
|
if 'migrate' in sys.argv and 'accounts' not in sys.argv:
|
||||||
post_migrate.connect(self.create_initial_systemuser,
|
post_migrate.connect(self.create_initial_systemuser,
|
||||||
dispatch_uid="orchestra.contrib.systemusers.apps.create_initial_systemuser")
|
dispatch_uid="orchestra.contrib.systemusers.apps.create_initial_systemuser")
|
||||||
|
services.register(WebappUsers, icon='roleplaying.png', verbose_name =_('WebApp User'), verbose_name_plural=_("Webapp users"))
|
||||||
|
|
||||||
def create_initial_systemuser(self, **kwargs):
|
def create_initial_systemuser(self, **kwargs):
|
||||||
from .models import SystemUser
|
from .models import SystemUser
|
||||||
|
|
|
@ -554,43 +554,6 @@ class UNIXUserControllerNewServers(ServiceController):
|
||||||
done
|
done
|
||||||
""") % context
|
""") % context
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
self.append(textwrap.dedent("""
|
|
||||||
check_code=0
|
|
||||||
# Ensure no processes running as user to modify/create
|
|
||||||
if ps -u %(user)s &> /dev/null; then
|
|
||||||
pkill -u %(user)s; sleep 3;
|
|
||||||
pkill -9 -u %(user)s; sleep 2;
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update/create user state for %(user)s
|
|
||||||
if id %(user)s &> /dev/null; then
|
|
||||||
usermod %(user)s \\
|
|
||||||
--password '%(password)s' \\
|
|
||||||
--shell '%(shell)s' \\
|
|
||||||
--groups '%(groups)s' || check_code=$?
|
|
||||||
else
|
|
||||||
useradd %(user)s --home '/%(user)s' \\
|
|
||||||
--password '%(password)s' \\
|
|
||||||
--shell '%(shell)s' \\
|
|
||||||
--groups '%(groups)s' || check_code=$?
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $check_code -ne 0 ]]; then
|
|
||||||
exit check_code
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure homedir exists and has correct perms
|
|
||||||
mkdir -p %(home)s
|
|
||||||
chown %(user)s:%(user)s %(home)s
|
|
||||||
chmod 750 %(home)s
|
|
||||||
|
|
||||||
# Create /chroots/$uid symlink into /home/$user.parent/webapps/
|
|
||||||
uid=$(id -u "%(user)s")
|
|
||||||
ln -n -f -s %(mainuser_home)s/webapps /chroots/$uid
|
|
||||||
""") % context
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
for member in settings.SYSTEMUSERS_DEFAULT_GROUP_MEMBERS:
|
for member in settings.SYSTEMUSERS_DEFAULT_GROUP_MEMBERS:
|
||||||
context['member'] = member
|
context['member'] = member
|
||||||
|
@ -739,8 +702,6 @@ class UNIXUserControllerNewServers(ServiceController):
|
||||||
if user.is_main:
|
if user.is_main:
|
||||||
groups = list(user.account.systemusers.exclude(username=user.username).values_list('username', flat=True))
|
groups = list(user.account.systemusers.exclude(username=user.username).values_list('username', flat=True))
|
||||||
groups.append("main-systemusers")
|
groups.append("main-systemusers")
|
||||||
# groups = list(user.groups.values_list('username', flat=True))
|
|
||||||
# groups.append("webapp-systemusers")
|
|
||||||
return groups
|
return groups
|
||||||
|
|
||||||
def get_context(self, user):
|
def get_context(self, user):
|
||||||
|
@ -756,5 +717,116 @@ class UNIXUserControllerNewServers(ServiceController):
|
||||||
'base_home': user.get_base_home(),
|
'base_home': user.get_base_home(),
|
||||||
'mainuser_home': user.main.get_home(),
|
'mainuser_home': user.main.get_home(),
|
||||||
}
|
}
|
||||||
# context['deleted_home'] = settings.SYSTEMUSERS_MOVE_ON_DELETE_PATH % context
|
context['deleted_home'] = settings.SYSTEMUSERS_MOVE_ON_DELETE_PATH % context
|
||||||
|
return replace(context, "'", '"')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class WebappUserController(ServiceController):
|
||||||
|
"""
|
||||||
|
Basic UNIX system user/group support based on <tt>useradd</tt>, <tt>usermod</tt>, <tt>userdel</tt> and <tt>groupdel</tt>.
|
||||||
|
Autodetects and uses ACL if available, for better permission management.
|
||||||
|
"""
|
||||||
|
verbose_name = _("SFTP Webapp user")
|
||||||
|
model = 'systemusers.WebappUsers'
|
||||||
|
actions = ('save', 'delete',)
|
||||||
|
doc_settings = (settings, (
|
||||||
|
'SYSTEMUSERS_DEFAULT_GROUP_MEMBERS',
|
||||||
|
'SYSTEMUSERS_MOVE_ON_DELETE_PATH',
|
||||||
|
'SYSTEMUSERS_FORBIDDEN_PATHS'
|
||||||
|
))
|
||||||
|
|
||||||
|
def save(self, user):
|
||||||
|
context = self.get_context(user)
|
||||||
|
if not context['user']:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.append(textwrap.dedent("""
|
||||||
|
# Update/create user state for %(user)s
|
||||||
|
if id %(user)s &> /dev/null; then
|
||||||
|
usermod %(user)s --home '/%(home)s' \\
|
||||||
|
--password '%(password)s' \\
|
||||||
|
--shell '%(shell)s' \\
|
||||||
|
--groups '%(groups)s'
|
||||||
|
else
|
||||||
|
useradd_code=0
|
||||||
|
useradd %(user)s --home '/%(home)s' \\
|
||||||
|
--password '%(password)s' \\
|
||||||
|
--shell '%(shell)s' \\
|
||||||
|
--groups '%(groups)s' || useradd_code=$?
|
||||||
|
if [[ $useradd_code -eq 8 ]]; then
|
||||||
|
# User is logged in, kill and retry
|
||||||
|
pkill -u %(user)s; sleep 2
|
||||||
|
pkill -9 -u %(user)s; sleep 1
|
||||||
|
useradd %(user)s --home '/%(home)s' \\
|
||||||
|
--password '%(password)s' \\
|
||||||
|
--shell '%(shell)s' \\
|
||||||
|
--groups '%(groups)s'
|
||||||
|
elif [[ $useradd_code -ne 0 ]]; then
|
||||||
|
exit $useradd_code
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
usermod -aG %(user)s %(parent)s
|
||||||
|
|
||||||
|
# Ensure homedir exists and has correct perms
|
||||||
|
mkdir -p '%(webapp_path)s' || exit_code=1
|
||||||
|
chown %(user)s:%(user)s %(webapp_path)s || exit_code=1
|
||||||
|
chmod 750 '%(webapp_path)s' || exit_code=1
|
||||||
|
|
||||||
|
# Create /chroots/$uid symlink into /home/$user.parent/webapps/
|
||||||
|
uid=$(id -u "%(user)s")
|
||||||
|
ln -n -f -s %(base_home)s/webapps /chroots/$uid || exit_code=1
|
||||||
|
""") % context
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def delete(self, user):
|
||||||
|
context = self.get_context(user)
|
||||||
|
if not context['user']:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.append(textwrap.dedent("""\
|
||||||
|
# Delete %(user)s user
|
||||||
|
uid=$(id -u "%(user)s")
|
||||||
|
|
||||||
|
nohup bash -c 'sleep 2 && killall -u %(user)s -s KILL' &> /dev/null &
|
||||||
|
killall -u %(user)s || true
|
||||||
|
userdel %(user)s || exit_code=$?
|
||||||
|
groupdel %(group)s || exit_code=$?
|
||||||
|
|
||||||
|
# Delete /chroots/$uid symlink into /home/$user.parent/webapps/
|
||||||
|
rm /chroots/$uid
|
||||||
|
""") % context
|
||||||
|
)
|
||||||
|
if context['deleted_home']:
|
||||||
|
self.append(textwrap.dedent("""\
|
||||||
|
# Move home into SYSTEMUSERS_MOVE_ON_DELETE_PATH, nesting if exists.
|
||||||
|
mv '%(webapp_path)s' '%(deleted_home)s' || exit_code=$?
|
||||||
|
""") % context
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.append("rm -fr -- '%(webapp_path)s'" % context)
|
||||||
|
|
||||||
|
|
||||||
|
def get_groups(self, user):
|
||||||
|
groups = []
|
||||||
|
groups = list(user.account.systemusers.exclude(username=user.username).values_list('username', flat=True))
|
||||||
|
groups.append("webapp-systemusers")
|
||||||
|
return groups
|
||||||
|
|
||||||
|
def get_context(self, user):
|
||||||
|
context = {
|
||||||
|
'object_id': user.pk,
|
||||||
|
'user': user.username,
|
||||||
|
'group': user.username,
|
||||||
|
'groups': ','.join(self.get_groups(user)),
|
||||||
|
'password': user.password, #if user.active else '*%s' % user.password,
|
||||||
|
'shell': user.shell,
|
||||||
|
'home': user.home,
|
||||||
|
'base_home': user.get_base_home(),
|
||||||
|
'webapp_path': os.path.normpath(user.get_base_home() + "/webapps/" + user.home),
|
||||||
|
'parent': user.get_parent(),
|
||||||
|
}
|
||||||
|
context['deleted_home'] = context['webapp_path'] + ".delete"
|
||||||
return replace(context, "'", '"')
|
return replace(context, "'", '"')
|
||||||
|
|
|
@ -6,6 +6,7 @@ from django.core.exceptions import ValidationError
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from orchestra.forms import UserCreationForm, UserChangeForm
|
from orchestra.forms import UserCreationForm, UserChangeForm
|
||||||
|
from orchestra.contrib.webapps.settings import WEBAPP_NEW_SERVERS
|
||||||
|
|
||||||
from . import settings
|
from . import settings
|
||||||
from .models import SystemUser
|
from .models import SystemUser
|
||||||
|
@ -162,3 +163,27 @@ class PermissionForm(LinkForm):
|
||||||
('r', _("Read only")),
|
('r', _("Read only")),
|
||||||
('w', _("Write only"))
|
('w', _("Write only"))
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class WebappUserFormMixin(object):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(WebappUserFormMixin, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if not self.instance.pk:
|
||||||
|
server = self.cleaned_data.get('target_server')
|
||||||
|
if server:
|
||||||
|
if server.name not in WEBAPP_NEW_SERVERS:
|
||||||
|
self.add_error("target_server", _(f"{server} does not belong to the new servers"))
|
||||||
|
return self.cleaned_data
|
||||||
|
|
||||||
|
class WebappUserCreationForm(WebappUserFormMixin, UserCreationForm):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class WebappUserChangeForm(WebappUserFormMixin, UserChangeForm):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
|
@ -1,30 +1,32 @@
|
||||||
# -*- coding: utf-8 -*-
|
# Generated by Django 2.2.28 on 2023-07-22 08:04
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import models, migrations
|
|
||||||
import orchestra.core.validators
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import orchestra.core.validators
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
# migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='SystemUser',
|
name='SystemUser',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(primary_key=True, serialize=False, auto_created=True, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('username', models.CharField(validators=[orchestra.core.validators.validate_username], unique=True, help_text='Required. 64 characters or fewer. Letters, digits and ./-/_ only.', max_length=32, verbose_name='username')),
|
('username', 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')),
|
||||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
('home', models.CharField(blank=True, max_length=256, help_text='Starting location when login with this no-shell user.', verbose_name='home')),
|
('home', models.CharField(blank=True, help_text='Starting location when login with this no-shell user.', max_length=256, verbose_name='home')),
|
||||||
('directory', models.CharField(blank=True, max_length=256, help_text="Optional directory relative to user's home.", verbose_name='directory')),
|
('directory', models.CharField(blank=True, help_text="Optional directory relative to user's home.", max_length=256, verbose_name='directory')),
|
||||||
('shell', models.CharField(default='/dev/null', max_length=32, choices=[('/dev/null', 'No shell, FTP only'), ('/bin/rssh', 'No shell, SFTP/RSYNC only'), ('/bin/bash', '/bin/bash'), ('/bin/sh', '/bin/sh')], verbose_name='shell')),
|
('shell', 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')),
|
||||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this account should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
# ('account', models.ForeignKey(to=settings.AUTH_USER_MODEL, related_name='systemusers', verbose_name='Account')),
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='systemusers', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
|
||||||
('groups', models.ManyToManyField(to='systemusers.SystemUser', blank=True, help_text='A new group will be created for the user. Which additional groups would you like them to be a member of?')),
|
('groups', models.ManyToManyField(blank=True, help_text='A new group will be created for the user. Which additional groups would you like them to be a member of?', to='systemusers.SystemUser')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Generated by Django 2.2.28 on 2023-07-22 08:05
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import orchestra.core.validators
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('orchestration', '__first__'),
|
||||||
|
('systemusers', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='WebappUsers',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('username', 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')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('home', models.CharField(blank=True, help_text='Starting location when login with this no-shell user.', max_length=256, verbose_name='home')),
|
||||||
|
('shell', 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')),
|
||||||
|
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accounts', to=settings.AUTH_USER_MODEL, verbose_name='Account')),
|
||||||
|
('target_server', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='orchestration.Server', verbose_name='Server')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('username', 'target_server')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -136,3 +136,43 @@ class SystemUser(models.Model):
|
||||||
|
|
||||||
def get_home(self):
|
def get_home(self):
|
||||||
return os.path.normpath(os.path.join(self.home, self.directory))
|
return os.path.normpath(os.path.join(self.home, self.directory))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------
|
||||||
|
|
||||||
|
class WebappUsers(models.Model):
|
||||||
|
"""
|
||||||
|
System users for webapp
|
||||||
|
Username max_length determined by LINUX system user/group lentgh: 32
|
||||||
|
"""
|
||||||
|
username = models.CharField(_("username"), max_length=32, unique=True,
|
||||||
|
help_text=_("Required. 32 characters or fewer. Letters, digits and ./-/_ only."),
|
||||||
|
validators=[validators.validate_username])
|
||||||
|
password = models.CharField(_("password"), max_length=128)
|
||||||
|
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
|
||||||
|
related_name='accounts', on_delete=models.CASCADE)
|
||||||
|
home = models.CharField(_("WebappDir"), max_length=256, blank=True,
|
||||||
|
help_text=_("name dir webapp /home/<main>/webapps/<DirName>"),
|
||||||
|
validators=[validators.validate_string_dir])
|
||||||
|
shell = models.CharField(_("shell"), max_length=32, choices=settings.WEBAPPUSERS_SHELLS,
|
||||||
|
default='/dev/null')
|
||||||
|
target_server = models.ForeignKey('orchestration.Server', on_delete=models.CASCADE,
|
||||||
|
verbose_name=_("Server"))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = ('username', 'target_server')
|
||||||
|
verbose_name = 'WebAppUser'
|
||||||
|
verbose_name_plural = 'WebappUsers'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.username
|
||||||
|
|
||||||
|
def set_password(self, raw_password):
|
||||||
|
self.password = make_password(raw_password)
|
||||||
|
|
||||||
|
def get_base_home(self):
|
||||||
|
return os.path.normpath(self.account.main_systemuser.home)
|
||||||
|
|
||||||
|
def get_parent(self):
|
||||||
|
return self.account.main_systemuser
|
|
@ -7,6 +7,13 @@ _names = ('user', 'username')
|
||||||
_backend_names = _names + ('group', 'shell', 'mainuser', 'home', 'base_home')
|
_backend_names = _names + ('group', 'shell', 'mainuser', 'home', 'base_home')
|
||||||
|
|
||||||
|
|
||||||
|
WEBAPPUSERS_SHELLS = Setting('SYSTEMUSERS_SHELLS',
|
||||||
|
(
|
||||||
|
('/dev/null', _("No shell, SFTP only")),
|
||||||
|
('/bin/bash', "/bin/bash"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
SYSTEMUSERS_SHELLS = Setting('SYSTEMUSERS_SHELLS',
|
SYSTEMUSERS_SHELLS = Setting('SYSTEMUSERS_SHELLS',
|
||||||
(
|
(
|
||||||
('/dev/null', _("No shell, FTP only")),
|
('/dev/null', _("No shell, FTP only")),
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
|
@ -282,3 +282,11 @@ WEBAPPS_CMS_CACHE_DIR = Setting('WEBAPPS_CMS_CACHE_DIR',
|
||||||
'/tmp/orchestra_cms_cache',
|
'/tmp/orchestra_cms_cache',
|
||||||
help_text="Server-side cache directori for CMS tarballs.",
|
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',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
|
@ -177,3 +177,10 @@ def validate_phone(value, country):
|
||||||
raise ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
if not phonenumbers.is_valid_number(number):
|
if not phonenumbers.is_valid_number(number):
|
||||||
raise ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
|
|
||||||
|
def validate_string_dir(value):
|
||||||
|
"""
|
||||||
|
A single non-empty line of free-form text with no whitespace.
|
||||||
|
"""
|
||||||
|
validators.RegexValidator('^[\_\-0-9a-z]+$',
|
||||||
|
_("Enter a valid name dir (spaceless lowercase text, number and _- )"), 'invalid')(value)
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import textwrap
|
||||||
|
|
||||||
|
from django.templatetags.static import static
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from orchestra.utils.sys import run
|
||||||
|
|
||||||
|
|
||||||
|
def html_to_pdf(html, pagination=False):
|
||||||
|
""" converts HTL to PDF using wkhtmltopdf """
|
||||||
|
context = {
|
||||||
|
'pagination': textwrap.dedent("""\
|
||||||
|
--footer-center "Page [page] of [topage]" \\
|
||||||
|
--footer-font-name sans \\
|
||||||
|
--footer-font-size 7 \\
|
||||||
|
--footer-spacing 7"""
|
||||||
|
) if pagination else '',
|
||||||
|
}
|
||||||
|
cmd = textwrap.dedent("""\
|
||||||
|
PATH=$PATH:/usr/local/bin/
|
||||||
|
xvfb-run -a -s "-screen 0 2480x3508x16" wkhtmltopdf -q \\
|
||||||
|
--use-xserver \\
|
||||||
|
%(pagination)s \\
|
||||||
|
--margin-bottom 22 \\
|
||||||
|
--margin-top 20 - - \
|
||||||
|
""") % context
|
||||||
|
return run(cmd, stdin=html.encode('utf-8')).stdout
|
||||||
|
|
||||||
|
|
||||||
|
def get_on_site_link(url):
|
||||||
|
context = {
|
||||||
|
'title': _("View on site %s") % url,
|
||||||
|
'url': url,
|
||||||
|
'image': format_html('<img src="{}"></img>', static('orchestra/images/view-on-site.png')),
|
||||||
|
}
|
||||||
|
return format_html('<a href="{url}" title="{title}">{image}</a>', **context)
|
Loading…
Reference in New Issue