From b0932e5137f702327eb66c6d242f491a1ce64bce Mon Sep 17 00:00:00 2001 From: feng626 <1304903146@qq.com> Date: Mon, 10 Jan 2022 19:02:18 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=80=83=E7=94=9F=E9=80=9A=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/api/__init__.py | 1 + apps/assets/api/backup.py | 55 +++++ .../migrations/0084_auto_20220112_1959.py | 62 +++++ apps/assets/models/__init__.py | 1 + apps/assets/models/backup.py | 145 +++++++++++ apps/assets/notifications.py | 25 ++ apps/assets/serializers/__init__.py | 1 + apps/assets/serializers/backup.py | 52 ++++ apps/assets/serializers/base.py | 22 ++ apps/assets/task_handlers/__init__.py | 1 + apps/assets/task_handlers/backup/__init__.py | 0 apps/assets/task_handlers/backup/handlers.py | 203 ++++++++++++++++ apps/assets/task_handlers/backup/manager.py | 48 ++++ apps/assets/task_handlers/endpoint.py | 10 + apps/assets/tasks/__init__.py | 1 + apps/assets/tasks/backup.py | 20 ++ apps/assets/urls/api_urls.py | 2 + apps/common/db/models.py | 41 ++++ apps/common/utils/file.py | 7 +- apps/locale/zh/LC_MESSAGES/django.mo | 4 +- apps/locale/zh/LC_MESSAGES/django.po | 227 ++++++++++++------ apps/perms/models/base.py | 42 +--- requirements/requirements.txt | 4 +- 23 files changed, 849 insertions(+), 125 deletions(-) create mode 100644 apps/assets/api/backup.py create mode 100644 apps/assets/migrations/0084_auto_20220112_1959.py create mode 100644 apps/assets/models/backup.py create mode 100644 apps/assets/notifications.py create mode 100644 apps/assets/serializers/backup.py create mode 100644 apps/assets/task_handlers/__init__.py create mode 100644 apps/assets/task_handlers/backup/__init__.py create mode 100644 apps/assets/task_handlers/backup/handlers.py create mode 100644 apps/assets/task_handlers/backup/manager.py create mode 100644 apps/assets/task_handlers/endpoint.py create mode 100644 apps/assets/tasks/backup.py diff --git a/apps/assets/api/__init__.py b/apps/assets/api/__init__.py index 3178c8e3f..45afd4379 100644 --- a/apps/assets/api/__init__.py +++ b/apps/assets/api/__init__.py @@ -10,3 +10,4 @@ from .domain import * from .cmd_filter import * from .gathered_user import * from .favorite_asset import * +from .backup import * diff --git a/apps/assets/api/backup.py b/apps/assets/api/backup.py new file mode 100644 index 000000000..cdd26b406 --- /dev/null +++ b/apps/assets/api/backup.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# +from rest_framework import status, mixins, viewsets +from rest_framework.response import Response + +from common.permissions import IsOrgAdmin +from orgs.mixins.api import OrgBulkModelViewSet + +from .. import serializers +from ..tasks import execute_account_backup_plan +from ..models import ( + AccountBackupPlan, AccountBackupPlanExecution +) + +__all__ = [ + 'AccountBackupPlanViewSet', 'AccountBackupPlanExecutionViewSet' +] + + +class AccountBackupPlanViewSet(OrgBulkModelViewSet): + model = AccountBackupPlan + filter_fields = ('name',) + search_fields = filter_fields + ordering_fields = ('name',) + ordering = ('name',) + serializer_class = serializers.AccountBackupPlanSerializer + permission_classes = (IsOrgAdmin,) + + +class AccountBackupPlanExecutionViewSet( + mixins.CreateModelMixin, mixins.ListModelMixin, + mixins.RetrieveModelMixin, viewsets.GenericViewSet +): + serializer_class = serializers.AccountBackupPlanExecutionSerializer + search_fields = ('trigger', 'plan_id') + filterset_fields = search_fields + permission_classes = (IsOrgAdmin,) + + def get_queryset(self): + queryset = AccountBackupPlanExecution.objects.all() + return queryset + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + pid = serializer.data.get('plan') + task = execute_account_backup_plan.delay( + pid=pid, trigger=AccountBackupPlanExecution.Trigger.manual + ) + return Response({'task': task.id}, status=status.HTTP_201_CREATED) + + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + queryset = queryset.order_by('-date_start') + return queryset diff --git a/apps/assets/migrations/0084_auto_20220112_1959.py b/apps/assets/migrations/0084_auto_20220112_1959.py new file mode 100644 index 000000000..4f83637eb --- /dev/null +++ b/apps/assets/migrations/0084_auto_20220112_1959.py @@ -0,0 +1,62 @@ +# Generated by Django 3.1.13 on 2022-01-12 11:59 + +import common.db.encoder +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('assets', '0083_auto_20211215_1436'), + ] + + operations = [ + migrations.CreateModel( + name='AccountBackupPlan', + fields=[ + ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('is_periodic', models.BooleanField(default=False)), + ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), + ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), + ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('types', models.IntegerField(choices=[(255, 'All'), (1, 'Asset'), (2, 'Application')], default=255, verbose_name='Type')), + ('comment', models.TextField(blank=True, verbose_name='Comment')), + ('recipients', models.ManyToManyField(blank=True, related_name='recipient_escape_route_plans', to=settings.AUTH_USER_MODEL, verbose_name='Recipient')), + ], + options={ + 'verbose_name': 'Account backup plan', + 'ordering': ['name'], + 'unique_together': {('name', 'org_id')}, + }, + ), + migrations.AlterField( + model_name='systemuser', + name='protocol', + field=models.CharField(choices=[('ssh', 'SSH'), ('rdp', 'RDP'), ('telnet', 'Telnet'), ('vnc', 'VNC'), ('mysql', 'MySQL'), ('redis', 'Redis'), ('oracle', 'Oracle'), ('mariadb', 'MariaDB'), ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('k8s', 'K8S')], default='ssh', max_length=16, verbose_name='Protocol'), + ), + migrations.CreateModel( + name='AccountBackupPlanExecution', + fields=[ + ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('date_start', models.DateTimeField(auto_now_add=True, verbose_name='Date start')), + ('timedelta', models.FloatField(default=0.0, null=True, verbose_name='Time')), + ('plan_snapshot', models.JSONField(blank=True, default=dict, encoder=common.db.encoder.ModelJSONFieldEncoder, null=True, verbose_name='Escape route snapshot')), + ('trigger', models.CharField(choices=[('manual', 'Manual trigger'), ('timing', 'Timing trigger')], default='manual', max_length=128, verbose_name='Trigger mode')), + ('reason', models.CharField(blank=True, max_length=1024, null=True, verbose_name='Reason')), + ('is_success', models.BooleanField(default=False, verbose_name='Is success')), + ('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='execution', to='assets.accountbackupplan', verbose_name='Account backup plan')), + ], + options={ + 'verbose_name': 'Account backup execution', + }, + ), + ] diff --git a/apps/assets/models/__init__.py b/apps/assets/models/__init__.py index 0f6aec901..d2dd03885 100644 --- a/apps/assets/models/__init__.py +++ b/apps/assets/models/__init__.py @@ -12,3 +12,4 @@ from .utils import * from .authbook import * from .gathered_user import * from .favorite_asset import * +from .backup import * diff --git a/apps/assets/models/backup.py b/apps/assets/models/backup.py new file mode 100644 index 000000000..0afc7b6bb --- /dev/null +++ b/apps/assets/models/backup.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +import uuid + +from celery import current_task +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from orgs.mixins.models import OrgModelMixin +from ops.mixin import PeriodTaskModelMixin +from common.utils import get_logger +from common.db.encoder import ModelJSONFieldEncoder +from common.db.models import BitOperationChoice +from common.mixins.models import CommonModelMixin + +__all__ = ['AccountBackupPlan', 'AccountBackupPlanExecution', 'Type'] + +logger = get_logger(__file__) + + +class Type(BitOperationChoice): + NONE = 0 + ALL = 0xff + + Asset = 0b1 + App = 0b1 << 1 + + DB_CHOICES = ( + (ALL, _('All')), + (Asset, _('Asset')), + (App, _('Application')) + ) + + NAME_MAP = { + ALL: "all", + Asset: "asset", + App: "application" + } + + NAME_MAP_REVERSE = {v: k for k, v in NAME_MAP.items()} + CHOICES = [] + for i, j in DB_CHOICES: + CHOICES.append((NAME_MAP[i], j)) + + +class AccountBackupPlan(CommonModelMixin, PeriodTaskModelMixin, OrgModelMixin): + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + types = models.IntegerField(choices=Type.DB_CHOICES, default=Type.ALL, verbose_name=_('Type')) + recipients = models.ManyToManyField( + 'users.User', related_name='recipient_escape_route_plans', blank=True, + verbose_name=_("Recipient") + ) + comment = models.TextField(blank=True, verbose_name=_('Comment')) + + def __str__(self): + return f'{self.name}({self.org_id})' + + class Meta: + ordering = ['name'] + unique_together = [('name', 'org_id')] + verbose_name = _('Account backup plan') + + def get_register_task(self): + from ..tasks import execute_account_backup_plan + name = "account_backup_plan_period_{}".format(str(self.id)[:8]) + task = execute_account_backup_plan.name + args = (str(self.id), AccountBackupPlanExecution.Trigger.timing) + kwargs = {} + return name, task, args, kwargs + + def to_attr_json(self): + return { + 'name': self.name, + 'is_periodic': self.is_periodic, + 'interval': self.interval, + 'crontab': self.crontab, + 'org_id': self.org_id, + 'created_by': self.created_by, + 'types': Type.value_to_choices(self.types), + 'recipients': { + str(recipient.id): (str(recipient), bool(recipient.secret_key)) + for recipient in self.recipients.all() + } + } + + def execute(self, trigger): + try: + hid = current_task.request.id + except AttributeError: + hid = str(uuid.uuid4()) + execution = AccountBackupPlanExecution.objects.create( + id=hid, plan=self, plan_snapshot=self.to_attr_json(), trigger=trigger + ) + return execution.start() + + +class AccountBackupPlanExecution(OrgModelMixin): + class Trigger(models.TextChoices): + manual = 'manual', _('Manual trigger') + timing = 'timing', _('Timing trigger') + + id = models.UUIDField(default=uuid.uuid4, primary_key=True) + date_start = models.DateTimeField( + auto_now_add=True, verbose_name=_('Date start') + ) + timedelta = models.FloatField( + default=0.0, verbose_name=_('Time'), null=True + ) + plan_snapshot = models.JSONField( + encoder=ModelJSONFieldEncoder, default=dict, + blank=True, null=True, verbose_name=_('Escape route snapshot') + ) + trigger = models.CharField( + max_length=128, default=Trigger.manual, choices=Trigger.choices, + verbose_name=_('Trigger mode') + ) + reason = models.CharField( + max_length=1024, blank=True, null=True, verbose_name=_('Reason') + ) + is_success = models.BooleanField(default=False, verbose_name=_('Is success')) + plan = models.ForeignKey( + 'AccountBackupPlan', related_name='execution', on_delete=models.CASCADE, + verbose_name=_('Account backup plan') + ) + + class Meta: + verbose_name = _('Account backup execution') + + @property + def types(self): + types = self.plan_snapshot.get('types') + return types + + @property + def recipients(self): + recipients = self.plan_snapshot.get('recipients') + if not recipients: + return [] + return recipients.values() + + def start(self): + from ..task_handlers import ExecutionManager + manager = ExecutionManager(execution=self) + return manager.run() diff --git a/apps/assets/notifications.py b/apps/assets/notifications.py new file mode 100644 index 000000000..58c02686c --- /dev/null +++ b/apps/assets/notifications.py @@ -0,0 +1,25 @@ +from django.utils.translation import ugettext_lazy as _ + +from users.models import User +from common.tasks import send_mail_attachment_async + + +class AccountBackupExecutionTaskMsg(object): + subject = _('Notification of account backup route task results') + + def __init__(self, name: str, user: User): + self.name = name + self.user = user + + @property + def message(self): + name = self.name + if self.user.secret_key: + return _('{} - The account backup passage task has been completed. See the attachment for details').format(name) + return _("{} - The account backup passage task has been completed: the encryption password has not been set - " + "please go to personal information -> file encryption password to set the encryption password").format(name) + + def publish(self, attachment_list=None): + send_mail_attachment_async.delay( + self.subject, self.message, [self.user.email], attachment_list + ) diff --git a/apps/assets/serializers/__init__.py b/apps/assets/serializers/__init__.py index 55437f609..f48552c9d 100644 --- a/apps/assets/serializers/__init__.py +++ b/apps/assets/serializers/__init__.py @@ -11,3 +11,4 @@ from .cmd_filter import * from .gathered_user import * from .favorite_asset import * from .account import * +from .backup import * diff --git a/apps/assets/serializers/backup.py b/apps/assets/serializers/backup.py new file mode 100644 index 000000000..c95d0f394 --- /dev/null +++ b/apps/assets/serializers/backup.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# +from django.utils.translation import ugettext as _ +from rest_framework import serializers + +from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from ops.mixin import PeriodTaskSerializerMixin +from common.utils import get_logger + +from .base import TypesField + +from ..models import AccountBackupPlan, AccountBackupPlanExecution + +logger = get_logger(__file__) + +__all__ = ['AccountBackupPlanSerializer', 'AccountBackupPlanExecutionSerializer'] + + +class AccountBackupPlanSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer): + types = TypesField(required=False, allow_null=True, label=_("Actions")) + + class Meta: + model = AccountBackupPlan + fields = [ + 'id', 'name', 'is_periodic', 'interval', 'crontab', 'date_created', + 'date_updated', 'created_by', 'periodic_display', 'comment', + 'recipients', 'types' + ] + extra_kwargs = { + 'name': {'required': True}, + 'periodic_display': {'label': _('Periodic perform')}, + 'recipients': {'label': _('Recipient'), 'help_text': _( + 'Currently only mail sending is supported' + )} + } + + +class AccountBackupPlanExecutionSerializer(serializers.ModelSerializer): + trigger_display = serializers.ReadOnlyField( + source='get_trigger_display', label=_('Trigger mode') + ) + + class Meta: + model = AccountBackupPlanExecution + fields = [ + 'id', 'date_start', 'timedelta', 'plan_snapshot', 'trigger', 'reason', + 'is_success', 'plan', 'org_id', 'recipients', 'trigger_display' + ] + read_only_fields = ( + 'id', 'date_start', 'timedelta', 'plan_snapshot', 'trigger', 'reason', + 'is_success', 'org_id', 'recipients' + ) diff --git a/apps/assets/serializers/base.py b/apps/assets/serializers/base.py index 66590369f..9eabc41a9 100644 --- a/apps/assets/serializers/base.py +++ b/apps/assets/serializers/base.py @@ -6,6 +6,7 @@ from django.utils.translation import ugettext as _ from rest_framework import serializers from common.utils import ssh_pubkey_gen, ssh_private_key_gen, validate_ssh_private_key +from assets.models import Type class AuthSerializer(serializers.ModelSerializer): @@ -70,3 +71,24 @@ class AuthSerializerMixin(serializers.ModelSerializer): def update(self, instance, validated_data): self.clean_auth_fields(validated_data) return super().update(instance, validated_data) + + +class TypesField(serializers.MultipleChoiceField): + def __init__(self, *args, **kwargs): + kwargs['choices'] = Type.CHOICES + super().__init__(*args, **kwargs) + + def to_representation(self, value): + return Type.value_to_choices(value) + + def to_internal_value(self, data): + if data is None: + return data + return Type.choices_to_value(data) + + +class ActionsDisplayField(TypesField): + def to_representation(self, value): + values = super().to_representation(value) + choices = dict(Type.CHOICES) + return [choices.get(i) for i in values] diff --git a/apps/assets/task_handlers/__init__.py b/apps/assets/task_handlers/__init__.py new file mode 100644 index 000000000..d557c449e --- /dev/null +++ b/apps/assets/task_handlers/__init__.py @@ -0,0 +1 @@ +from .endpoint import * diff --git a/apps/assets/task_handlers/backup/__init__.py b/apps/assets/task_handlers/backup/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/assets/task_handlers/backup/handlers.py b/apps/assets/task_handlers/backup/handlers.py new file mode 100644 index 000000000..c1b62a730 --- /dev/null +++ b/apps/assets/task_handlers/backup/handlers.py @@ -0,0 +1,203 @@ +import os +import time +import pandas as pd +from collections import defaultdict + +from django.conf import settings +from django.utils.translation import ugettext_lazy as _ + +from assets.models import AuthBook, Asset, BaseUser, ProtocolsMixin +from assets.notifications import AccountBackupExecutionTaskMsg +from applications.models import Account, Application +from applications.const import AppType +from users.models import User +from common.utils import get_logger +from common.utils.timezone import local_now_display +from common.utils.file import encrypt_and_compress_zip_file + +logger = get_logger(__file__) + +PATH = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp') + + +class AssetAccountHandler: + @staticmethod + def get_filename(plan_name): + filename = os.path.join( + PATH, f'{plan_name}-{_("Asset")}-{local_now_display()}-{time.time()}.xlsx' + ) + return filename + + @staticmethod + def create_df(): + df_dict = defaultdict(list) + label_key = AuthBook._meta.verbose_name + accounts = AuthBook.objects.all().prefetch_related('systemuser', 'asset') + for account in accounts: + account.load_auth() + protocol = account.asset.protocol + protocol_label = getattr(ProtocolsMixin.Protocol, protocol).label + row = { + getattr(Asset, 'hostname').field.verbose_name: account.asset.hostname, + getattr(Asset, 'ip').field.verbose_name: account.asset.ip, + } + secret_row = AccountBackupHandler.create_secret_row(account) + row.update(secret_row) + row.update({ + getattr(Asset, 'protocol').field.verbose_name: protocol_label, + getattr(AuthBook, 'version').field.verbose_name: account.version + }) + df_dict[label_key].append(row) + for k, v in df_dict.items(): + df_dict[k] = pd.DataFrame(v) + return df_dict + + +class AppAccountHandler: + @staticmethod + def get_filename(plan_name): + filename = os.path.join( + PATH, f'{plan_name}-{_("Application")}-{local_now_display()}-{time.time()}.xlsx' + ) + return filename + + @staticmethod + def create_df(): + df_dict = defaultdict(list) + accounts = Account.objects.all().prefetch_related('systemuser', 'app') + for account in accounts: + account.load_auth() + app_type = account.app.type + if app_type == 'postgresql': + label_key = getattr(AppType, 'pgsql').label + else: + label_key = getattr(AppType, app_type).label + row = { + getattr(Application, 'name').field.verbose_name: account.app.name, + getattr(Application, 'attrs').field.verbose_name: account.app.attrs + } + secret_row = AccountBackupHandler.create_secret_row(account) + row.update(secret_row) + row.update({ + getattr(Account, 'version').field.verbose_name: account.version + }) + df_dict[label_key].append(row) + for k, v in df_dict.items(): + df_dict[k] = pd.DataFrame(v) + return df_dict + + +HANDLER_MAP = { + 'asset': AssetAccountHandler, + 'application': AppAccountHandler +} + + +class AccountBackupHandler: + def __init__(self, execution): + self.execution = execution + self.plan_name = self.execution.plan.name + self.is_frozen = False # 任务状态冻结标志 + + def create_excel(self): + logger.info( + '\n' + '\033[32m>>> 正在生成资产及应用相关备份信息文件\033[0m' + '' + ) + # Print task start date + time_start = time.time() + info = {} + for account_type in self.execution.types: + if account_type in HANDLER_MAP: + account_handler = HANDLER_MAP[account_type] + df = account_handler.create_df() + filename = account_handler.get_filename(self.plan_name) + info[filename] = df + for filename, df_dict in info.items(): + with pd.ExcelWriter(filename) as w: + for sheet, df in df_dict.items(): + sheet = sheet.replace(' ', '-') + getattr(df, 'to_excel')(w, sheet_name=sheet, index=False) + timedelta = round((time.time() - time_start), 2) + logger.info('步骤完成: 用时 {}s'.format(timedelta)) + return list(info.keys()) + + def send_backup_mail(self, files): + recipients = self.execution.plan_snapshot.get('recipients') + if not recipients: + return + recipients = User.objects.filter(id__in=list(recipients)) + logger.info( + '\n' + '\033[32m>>> 发送备份邮件\033[0m' + '' + ) + plan_name = self.plan_name + for user in recipients: + if not user.secret_key: + attachment_list = [] + else: + password = user.secret_key.encode('utf8') + attachment = os.path.join(PATH, f'{plan_name}-{local_now_display()}-{time.time()}.zip') + encrypt_and_compress_zip_file(attachment, password, files) + attachment_list = [attachment, ] + AccountBackupExecutionTaskMsg(plan_name, user).publish(attachment_list) + logger.info('邮件已发送至{}({})'.format(user, user.email)) + for file in files: + os.remove(file) + + def step_perform_task_update(self, is_success, reason): + self.execution.reason = reason[:1024] + self.execution.is_success = is_success + self.execution.save() + logger.info('已完成对任务状态的更新') + + def step_finished(self, is_success): + if is_success: + logger.info('任务执行成功') + else: + logger.error('任务执行失败') + + def _run(self): + is_success = False + error = '-' + try: + files = self.create_excel() + self.send_backup_mail(files) + except Exception as e: + self.is_frozen = True + logger.error('任务执行被异常中断') + logger.info('下面打印发生异常的 Traceback 信息 : ') + logger.error(e, exc_info=True) + error = str(e) + else: + is_success = True + finally: + reason = error + self.step_perform_task_update(is_success, reason) + self.step_finished(is_success) + + def run(self): + logger.info('任务开始: {}'.format(local_now_display())) + time_start = time.time() + try: + self._run() + except Exception as e: + logger.error('任务运行出现异常') + logger.error('下面显示异常 Traceback 信息: ') + logger.error(e, exc_info=True) + finally: + logger.info('\n任务结束: {}'.format(local_now_display())) + timedelta = round((time.time() - time_start), 2) + logger.info('用时: {}'.format(timedelta)) + + @staticmethod + def create_secret_row(instance): + row = { + getattr(BaseUser, 'username').field.verbose_name: instance.username, + getattr(BaseUser, 'password').field.verbose_name: instance.password, + getattr(BaseUser, 'private_key').field.verbose_name: instance.private_key, + getattr(BaseUser, 'public_key').field.verbose_name: instance.public_key + } + return row diff --git a/apps/assets/task_handlers/backup/manager.py b/apps/assets/task_handlers/backup/manager.py new file mode 100644 index 000000000..c9558fea0 --- /dev/null +++ b/apps/assets/task_handlers/backup/manager.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# +import time + +from django.utils import timezone + +from common.utils import get_logger +from common.utils.timezone import local_now_display + +from .handlers import AccountBackupHandler + +logger = get_logger(__name__) + + +class AccountBackupExecutionManager: + def __init__(self, execution): + self.execution = execution + self.date_start = timezone.now() + self.time_start = time.time() + self.date_end = None + self.time_end = None + self.timedelta = 0 + + def do_run(self): + execution = self.execution + logger.info('\n\033[33m# 账号备份计划正在执行\033[0m') + handler = AccountBackupHandler(execution) + handler.run() + + def pre_run(self): + self.execution.date_start = self.date_start + self.execution.save() + + def post_run(self): + self.time_end = time.time() + self.date_end = timezone.now() + + logger.info('\n\n' + '-' * 80) + logger.info('计划执行结束 {}\n'.format(local_now_display())) + self.timedelta = self.time_end - self.time_start + logger.info('用时: {}s'.format(self.timedelta)) + self.execution.timedelta = self.timedelta + self.execution.save() + + def run(self): + self.pre_run() + self.do_run() + self.post_run() diff --git a/apps/assets/task_handlers/endpoint.py b/apps/assets/task_handlers/endpoint.py new file mode 100644 index 000000000..729fc8648 --- /dev/null +++ b/apps/assets/task_handlers/endpoint.py @@ -0,0 +1,10 @@ +from .backup.manager import AccountBackupExecutionManager + + +class ExecutionManager: + manager_type = { + 'backup': AccountBackupExecutionManager + } + + def __new__(cls, execution): + return AccountBackupExecutionManager(execution) diff --git a/apps/assets/tasks/__init__.py b/apps/assets/tasks/__init__.py index 7aaed0efd..22ccbf503 100644 --- a/apps/assets/tasks/__init__.py +++ b/apps/assets/tasks/__init__.py @@ -9,3 +9,4 @@ from .gather_asset_hardware_info import * from .push_system_user import * from .system_user_connectivity import * from .nodes_amount import * +from .backup import * diff --git a/apps/assets/tasks/backup.py b/apps/assets/tasks/backup.py new file mode 100644 index 000000000..5d4e91011 --- /dev/null +++ b/apps/assets/tasks/backup.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# +from celery import shared_task + +from common.utils import get_object_or_none, get_logger +from orgs.utils import tmp_to_org, tmp_to_root_org +from assets.models import AccountBackupPlan + +logger = get_logger(__file__) + + +@shared_task +def execute_account_backup_plan(pid, trigger): + with tmp_to_root_org(): + plan = get_object_or_none(AccountBackupPlan, pk=pid) + if not plan: + logger.error("No account backup route plan found: {}".format(pid)) + return + with tmp_to_org(plan.org): + plan.execute(trigger) diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index 8d00e6543..b92903d02 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -26,6 +26,8 @@ router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset') router.register(r'system-users-assets-relations', api.SystemUserAssetRelationViewSet, 'system-users-assets-relation') router.register(r'system-users-nodes-relations', api.SystemUserNodeRelationViewSet, 'system-users-nodes-relation') router.register(r'system-users-users-relations', api.SystemUserUserRelationViewSet, 'system-users-users-relation') +router.register(r'backup', api.AccountBackupPlanViewSet, 'backup') +router.register(r'backup-execution', api.AccountBackupPlanExecutionViewSet, 'backup-execution') cmd_filter_router = routers.NestedDefaultRouter(router, r'cmd-filters', lookup='filter') cmd_filter_router.register(r'rules', api.CommandFilterRuleViewSet, 'cmd-filter-rule') diff --git a/apps/common/db/models.py b/apps/common/db/models.py index 77a49a683..2989f734e 100644 --- a/apps/common/db/models.py +++ b/apps/common/db/models.py @@ -66,6 +66,47 @@ class ChoiceSet(metaclass=ChoiceSetType): choices = None # 用于 Django Model 中的 choices 配置, 为了代码提示在此声明 +class BitOperationChoice: + NONE = 0 + NAME_MAP: dict + DB_CHOICES: tuple + NAME_MAP_REVERSE: dict + + @classmethod + def value_to_choices(cls, value): + if isinstance(value, list): + return value + value = int(value) + choices = [cls.NAME_MAP[i] for i, j in cls.DB_CHOICES if value & i == i] + return choices + + @classmethod + def value_to_choices_display(cls, value): + choices = cls.value_to_choices(value) + return [str(dict(cls.choices())[i]) for i in choices] + + @classmethod + def choices_to_value(cls, value): + if not isinstance(value, list): + return cls.NONE + db_value = [ + cls.NAME_MAP_REVERSE[v] for v in value + if v in cls.NAME_MAP_REVERSE.keys() + ] + if not db_value: + return cls.NONE + + def to_choices(x, y): + return x | y + + result = reduce(to_choices, db_value) + return result + + @classmethod + def choices(cls): + return [(cls.NAME_MAP[i], j) for i, j in cls.DB_CHOICES] + + class JMSBaseModel(Model): created_by = CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by')) updated_by = CharField(max_length=32, null=True, blank=True, verbose_name=_('Updated by')) diff --git a/apps/common/utils/file.py b/apps/common/utils/file.py index cb883ed55..c00293de5 100644 --- a/apps/common/utils/file.py +++ b/apps/common/utils/file.py @@ -10,10 +10,11 @@ def create_csv_file(filename, headers, rows, ): w.writerows(rows) -def encrypt_and_compress_zip_file(filename, secret_password, encrypted_filename): +def encrypt_and_compress_zip_file(filename, secret_password, encrypted_filenames): with pyzipper.AESZipFile( filename, 'w', compression=pyzipper.ZIP_LZMA, encryption=pyzipper.WZ_AES ) as zf: zf.setpassword(secret_password) - with open(encrypted_filename, 'rb') as f: - zf.writestr(os.path.basename(encrypted_filename), f.read()) + for encrypted_filename in encrypted_filenames: + with open(encrypted_filename, 'rb') as f: + zf.writestr(os.path.basename(encrypted_filename), f.read()) diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 379cdee6b..6f98a2f4b 100644 --- a/apps/locale/zh/LC_MESSAGES/django.mo +++ b/apps/locale/zh/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1393555be0b521cb6c09f61c20d5c6f93ce03e376208c1e90f2344421324c422 -size 95321 +oid sha256:09fe9d77decdc75054a7728cae777afe393a4119a42023537be9be7450e630dd +size 95965 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 8e7457e20..2fd5cd7d4 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-12-31 10:28+0800\n" +"POT-Creation-Date: 2022-01-12 15:28+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -18,7 +18,7 @@ msgstr "" "X-Generator: Poedit 2.4.3\n" #: acls/models/base.py:25 acls/serializers/login_asset_acl.py:47 -#: applications/models/application.py:166 assets/models/asset.py:139 +#: applications/models/application.py:202 assets/models/asset.py:139 #: assets/models/base.py:175 assets/models/cluster.py:18 #: assets/models/cmd_filter.py:23 assets/models/domain.py:24 #: assets/models/group.py:20 assets/models/label.py:18 ops/mixin.py:24 @@ -51,16 +51,17 @@ msgstr "优先级可选范围为 1-100 (数值越小越优先)" msgid "Active" msgstr "激活中" -#: acls/models/base.py:32 applications/models/application.py:179 +#: acls/models/base.py:32 applications/models/application.py:215 #: assets/models/asset.py:144 assets/models/asset.py:232 -#: assets/models/base.py:180 assets/models/cluster.py:29 -#: assets/models/cmd_filter.py:44 assets/models/cmd_filter.py:87 -#: assets/models/domain.py:25 assets/models/domain.py:65 -#: assets/models/group.py:23 assets/models/label.py:23 ops/models/adhoc.py:37 -#: orgs/models.py:27 perms/models/base.py:129 settings/models.py:34 -#: terminal/models/storage.py:26 terminal/models/terminal.py:114 -#: tickets/models/ticket.py:71 users/models/group.py:16 -#: users/models/user.py:585 xpack/plugins/change_auth_plan/models/base.py:41 +#: assets/models/backup.py:27 assets/models/base.py:180 +#: assets/models/cluster.py:29 assets/models/cmd_filter.py:44 +#: assets/models/cmd_filter.py:87 assets/models/domain.py:25 +#: assets/models/domain.py:65 assets/models/group.py:23 +#: assets/models/label.py:23 ops/models/adhoc.py:37 orgs/models.py:27 +#: perms/models/base.py:129 settings/models.py:34 terminal/models/storage.py:26 +#: terminal/models/terminal.py:114 tickets/models/ticket.py:71 +#: users/models/group.py:16 users/models/user.py:585 +#: xpack/plugins/change_auth_plan/models/base.py:41 #: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:113 #: xpack/plugins/gathered_user/models.py:26 msgid "Comment" @@ -245,7 +246,7 @@ msgstr "" msgid "Time Period" msgstr "时段" -#: applications/api/mixin.py:20 templates/_nav_user.html:10 +#: applications/api/mixin.py:28 templates/_nav_user.html:10 msgid "My applications" msgstr "我的应用" @@ -291,11 +292,11 @@ msgstr "版本" msgid "Account" msgstr "账户" -#: applications/models/application.py:50 templates/_nav.html:60 +#: applications/models/application.py:60 templates/_nav.html:60 msgid "Applications" msgstr "应用管理" -#: applications/models/application.py:168 +#: applications/models/application.py:204 #: applications/serializers/application.py:88 assets/models/label.py:21 #: perms/models/application_permission.py:20 #: perms/serializers/application/user_permission.py:33 @@ -304,7 +305,7 @@ msgstr "应用管理" msgid "Category" msgstr "类别" -#: applications/models/application.py:171 +#: applications/models/application.py:207 #: applications/serializers/application.py:90 assets/models/cmd_filter.py:76 #: assets/models/user.py:210 perms/models/application_permission.py:23 #: perms/serializers/application/user_permission.py:34 @@ -316,16 +317,16 @@ msgstr "类别" msgid "Type" msgstr "类型" -#: applications/models/application.py:175 assets/models/asset.py:218 +#: applications/models/application.py:211 assets/models/asset.py:218 #: assets/models/domain.py:30 assets/models/domain.py:64 msgid "Domain" msgstr "网域" -#: applications/models/application.py:177 xpack/plugins/cloud/models.py:33 +#: applications/models/application.py:213 xpack/plugins/cloud/models.py:33 msgid "Attrs" -msgstr "" +msgstr "属性" -#: applications/models/application.py:183 assets/models/cmd_filter.py:41 +#: applications/models/application.py:219 assets/models/cmd_filter.py:41 #: perms/models/application_permission.py:27 users/models/user.py:170 msgid "Application" msgstr "应用程序" @@ -578,6 +579,75 @@ msgstr "创建日期" msgid "AuthBook" msgstr "账号" +#: assets/models/backup.py:25 assets/serializers/backup.py:28 +#: xpack/plugins/change_auth_plan/models/app.py:41 +#: xpack/plugins/change_auth_plan/models/asset.py:62 +#: xpack/plugins/change_auth_plan/serializers/base.py:44 +msgid "Recipient" +msgstr "收件人" + +#: assets/models/backup.py:35 assets/models/backup.py:96 +#, fuzzy +#| msgid "Account key" +msgid "Account backup plan" +msgstr "账户密钥" + +#: assets/models/backup.py:72 xpack/plugins/change_auth_plan/models/base.py:104 +msgid "Manual trigger" +msgstr "手动触发" + +#: assets/models/backup.py:73 xpack/plugins/change_auth_plan/models/base.py:105 +msgid "Timing trigger" +msgstr "定时触发" + +#: assets/models/backup.py:77 audits/models.py:43 ops/models/command.py:30 +#: perms/models/base.py:125 terminal/models/session.py:54 +#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:55 +#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:57 +#: xpack/plugins/change_auth_plan/models/base.py:109 +#: xpack/plugins/change_auth_plan/models/base.py:200 +#: xpack/plugins/gathered_user/models.py:76 +msgid "Date start" +msgstr "开始日期" + +#: assets/models/backup.py:80 notifications/notifications.py:187 +#: ops/models/adhoc.py:246 xpack/plugins/change_auth_plan/models/base.py:112 +#: xpack/plugins/change_auth_plan/models/base.py:201 +#: xpack/plugins/gathered_user/models.py:79 +msgid "Time" +msgstr "时间" + +#: assets/models/backup.py:84 +#, fuzzy +#| msgid "Change auth plan snapshot" +msgid "Escape route snapshot" +msgstr "改密计划快照" + +#: assets/models/backup.py:88 assets/serializers/backup.py:36 +#: xpack/plugins/change_auth_plan/models/base.py:122 +#: xpack/plugins/change_auth_plan/serializers/base.py:73 +msgid "Trigger mode" +msgstr "触发模式" + +#: assets/models/backup.py:91 audits/models.py:111 +#: terminal/models/sharing.py:88 +#: xpack/plugins/change_auth_plan/models/base.py:198 +#: xpack/plugins/cloud/models.py:176 +msgid "Reason" +msgstr "原因" + +#: assets/models/backup.py:93 audits/serializers.py:76 audits/serializers.py:91 +#: ops/models/adhoc.py:248 terminal/serializers/session.py:35 +#: xpack/plugins/change_auth_plan/models/base.py:199 +msgid "Is success" +msgstr "是否成功" + +#: assets/models/backup.py:100 +#, fuzzy +#| msgid "Change auth plan execution" +msgid "Account backup execution" +msgstr "改密计划执行" + #: assets/models/base.py:30 assets/tasks/const.py:51 audits/const.py:5 msgid "Unknown" msgstr "未知" @@ -868,6 +938,25 @@ msgstr "切换自" msgid "%(value)s is not an even number" msgstr "%(value)s is not an even number" +#: assets/notifications.py:8 +msgid "Notification of account backup route task results" +msgstr "账号备份任务结果通知" + +#: assets/notifications.py:18 +msgid "" +"{} - The account backup passage task has been completed. See the attachment " +"for details" +msgstr "{} - 账号备份任务已完成, 详情见附件" + +#: assets/notifications.py:19 +msgid "" +"{} - The account backup passage task has been completed: the encryption " +"password has not been set - please go to personal information -> file " +"encryption password to set the encryption password" +msgstr "" +"{} - 账号备份任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设" +"置加密密码" + #: assets/serializers/account.py:31 assets/serializers/account.py:52 msgid "System user display" msgstr "系统用户名称" @@ -904,6 +993,16 @@ msgstr "特权用户名称" msgid "CPU info" msgstr "CPU信息" +#: assets/serializers/backup.py:27 ops/mixin.py:106 ops/mixin.py:147 +#: xpack/plugins/change_auth_plan/serializers/base.py:42 +msgid "Periodic perform" +msgstr "定时执行" + +#: assets/serializers/backup.py:29 +#: xpack/plugins/change_auth_plan/serializers/base.py:45 +msgid "Currently only mail sending is supported" +msgstr "当前只支持邮件发送" + #: assets/serializers/base.py:35 msgid "Key password" msgstr "密钥密码" @@ -1179,16 +1278,6 @@ msgstr "文件名" msgid "Success" msgstr "成功" -#: audits/models.py:43 ops/models/command.py:30 perms/models/base.py:125 -#: terminal/models/session.py:54 -#: tickets/serializers/ticket/meta/ticket_type/apply_application.py:55 -#: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:57 -#: xpack/plugins/change_auth_plan/models/base.py:109 -#: xpack/plugins/change_auth_plan/models/base.py:200 -#: xpack/plugins/gathered_user/models.py:76 -msgid "Date start" -msgstr "开始日期" - #: audits/models.py:51 #: authentication/templates/authentication/_access_key_modal.html:22 msgid "Create" @@ -1255,12 +1344,6 @@ msgstr "用户代理" msgid "MFA" msgstr "MFA" -#: audits/models.py:111 terminal/models/sharing.py:88 -#: xpack/plugins/change_auth_plan/models/base.py:198 -#: xpack/plugins/cloud/models.py:176 -msgid "Reason" -msgstr "原因" - #: audits/models.py:112 tickets/models/ticket.py:57 #: xpack/plugins/cloud/models.py:172 xpack/plugins/cloud/models.py:221 msgid "Status" @@ -1290,12 +1373,6 @@ msgstr "MFA名称" msgid "Reason display" msgstr "原因描述" -#: audits/serializers.py:76 audits/serializers.py:91 ops/models/adhoc.py:248 -#: terminal/serializers/session.py:35 -#: xpack/plugins/change_auth_plan/models/base.py:199 -msgid "Is success" -msgstr "是否成功" - #: audits/serializers.py:78 msgid "Hosts display" msgstr "主机名称" @@ -2394,13 +2471,6 @@ msgstr "邮件" msgid "Site message" msgstr "站内信" -#: notifications/notifications.py:187 ops/models/adhoc.py:246 -#: xpack/plugins/change_auth_plan/models/base.py:112 -#: xpack/plugins/change_auth_plan/models/base.py:201 -#: xpack/plugins/gathered_user/models.py:79 -msgid "Time" -msgstr "时间" - #: ops/api/celery.py:61 ops/api/celery.py:76 msgid "Waiting task start" msgstr "等待任务开始" @@ -2422,11 +2492,6 @@ msgstr "周期执行" msgid "Regularly perform" msgstr "定期执行" -#: ops/mixin.py:106 ops/mixin.py:147 -#: xpack/plugins/change_auth_plan/serializers/base.py:42 -msgid "Periodic perform" -msgstr "定时执行" - #: ops/mixin.py:112 settings/serializers/auth/ldap.py:61 msgid "Interval" msgstr "间隔" @@ -4231,19 +4296,19 @@ msgstr "Jmservisor 是在 windows 远程应用发布服务器中用来拉起远 msgid "Filters" msgstr "过滤" -#: terminal/api/session.py:189 +#: terminal/api/session.py:190 msgid "Session does not exist: {}" msgstr "会话不存在: {}" -#: terminal/api/session.py:192 +#: terminal/api/session.py:193 msgid "Session is finished or the protocol not supported" msgstr "会话已经完成或协议不支持" -#: terminal/api/session.py:197 +#: terminal/api/session.py:198 msgid "User does not exist: {}" msgstr "用户不存在: {}" -#: terminal/api/session.py:201 +#: terminal/api/session.py:205 msgid "User does not have permission" msgstr "用户没有权限" @@ -5617,12 +5682,6 @@ msgstr "参数 'action' 必须是 [{}]" msgid "Change auth plan" msgstr "改密计划" -#: xpack/plugins/change_auth_plan/models/app.py:41 -#: xpack/plugins/change_auth_plan/models/asset.py:62 -#: xpack/plugins/change_auth_plan/serializers/base.py:44 -msgid "Recipient" -msgstr "收件人" - #: xpack/plugins/change_auth_plan/models/app.py:46 #: xpack/plugins/change_auth_plan/models/app.py:95 msgid "Application change auth plan" @@ -5683,23 +5742,10 @@ msgstr "使用不同的随机密码" msgid "Password rules" msgstr "密码规则" -#: xpack/plugins/change_auth_plan/models/base.py:104 -msgid "Manual trigger" -msgstr "手动触发" - -#: xpack/plugins/change_auth_plan/models/base.py:105 -msgid "Timing trigger" -msgstr "定时触发" - #: xpack/plugins/change_auth_plan/models/base.py:115 msgid "Change auth plan snapshot" msgstr "改密计划快照" -#: xpack/plugins/change_auth_plan/models/base.py:122 -#: xpack/plugins/change_auth_plan/serializers/base.py:73 -msgid "Trigger mode" -msgstr "触发模式" - #: xpack/plugins/change_auth_plan/models/base.py:184 msgid "Ready" msgstr "准备" @@ -5755,10 +5801,6 @@ msgstr "修改 SSH Key" msgid "Run times" msgstr "执行次数" -#: xpack/plugins/change_auth_plan/serializers/base.py:45 -msgid "Currently only mail sending is supported" -msgstr "当前只支持邮件发送" - #: xpack/plugins/change_auth_plan/serializers/base.py:57 msgid "* Please enter the correct password length" msgstr "* 请输入正确的密码长度" @@ -6244,3 +6286,28 @@ msgstr "旗舰版" #: xpack/plugins/license/models.py:77 msgid "Community edition" msgstr "社区版" + +#, fuzzy +#~| msgid "Change auth plan execution" +#~ msgid "Account backup plan execution" +#~ msgstr "改密计划执行" + +#, fuzzy +#~| msgid "Change auth plan task" +#~ msgid "Account backup plan task" +#~ msgstr "改密计划任务" + +#, fuzzy +#~| msgid "Change auth plan" +#~ msgid "Escape route plan" +#~ msgstr "改密计划" + +#, fuzzy +#~| msgid "Change auth plan execution" +#~ msgid "Escape route execution" +#~ msgstr "改密计划执行" + +#, fuzzy +#~| msgid "Change auth plan task" +#~ msgid "Escape route plan task" +#~ msgstr "改密计划任务" diff --git a/apps/perms/models/base.py b/apps/perms/models/base.py index 613b83a33..b2d388717 100644 --- a/apps/perms/models/base.py +++ b/apps/perms/models/base.py @@ -2,14 +2,13 @@ # import uuid -from functools import reduce from django.utils.translation import ugettext_lazy as _ from django.db import models from django.db.models import Q from django.utils import timezone from orgs.mixins.models import OrgModelMixin -from common.db.models import UnionQuerySet +from common.db.models import UnionQuerySet, BitOperationChoice from common.utils import date_expired_default, lazyproperty from orgs.mixins.models import OrgManager @@ -40,15 +39,14 @@ class BasePermissionManager(OrgManager): return self.get_queryset().valid() -class Action: - NONE = 0 +class Action(BitOperationChoice): + ALL = 0xff CONNECT = 0b1 UPLOAD = 0b1 << 1 DOWNLOAD = 0b1 << 2 CLIPBOARD_COPY = 0b1 << 3 CLIPBOARD_PASTE = 0b1 << 4 - ALL = 0xff UPDOWNLOAD = UPLOAD | DOWNLOAD CLIPBOARD_COPY_PASTE = CLIPBOARD_COPY | CLIPBOARD_PASTE @@ -79,40 +77,6 @@ class Action: for i, j in DB_CHOICES: CHOICES.append((NAME_MAP[i], j)) - @classmethod - def value_to_choices(cls, value): - if isinstance(value, list): - return value - value = int(value) - choices = [cls.NAME_MAP[i] for i, j in cls.DB_CHOICES if value & i == i] - return choices - - @classmethod - def value_to_choices_display(cls, value): - choices = cls.value_to_choices(value) - return [str(dict(cls.choices())[i]) for i in choices] - - @classmethod - def choices_to_value(cls, value): - if not isinstance(value, list): - return cls.NONE - db_value = [ - cls.NAME_MAP_REVERSE[v] for v in value - if v in cls.NAME_MAP_REVERSE.keys() - ] - if not db_value: - return cls.NONE - - def to_choices(x, y): - return x | y - - result = reduce(to_choices, db_value) - return result - - @classmethod - def choices(cls): - return [(cls.NAME_MAP[i], j) for i, j in cls.DB_CHOICES] - class BasePermission(OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 31123ad82..8f9eb3f42 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -56,7 +56,7 @@ pycryptodome==3.10.1 pycryptodomex==3.10.1 pyotp==2.2.6 PyNaCl==1.2.1 -python-dateutil==2.6.1 +python-dateutil==2.8.2 #python-gssapi==0.6.4 pytz==2018.3 PyYAML==6.0 @@ -127,3 +127,5 @@ python-keystoneclient==4.3.0 pymssql==2.1.5 kubernetes==21.7.0 websocket-client==1.2.3 +numpy==1.22.0 +pandas==1.3.5