diff --git a/.gitignore b/.gitignore index e341a0d63..1b4a445d5 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ dump.rdb .idea/ db.sqlite3 config.py +config.yml *.log host_rsa_key *.bat diff --git a/Dockerfile b/Dockerfile index a520a4ba4..3742a47f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,13 +6,13 @@ RUN useradd jumpserver COPY ./requirements /tmp/requirements -RUN yum -y install epel-release && cd /tmp/requirements && \ +RUN yum -y install epel-release openldap-clients telnet && cd /tmp/requirements && \ yum -y install $(cat rpm_requirements.txt) RUN cd /tmp/requirements && pip install -r requirements.txt COPY . /opt/jumpserver -COPY config_docker.py /opt/jumpserver/config.py +RUN echo > config.yml VOLUME /opt/jumpserver/data VOLUME /opt/jumpserver/logs diff --git a/apps/assets/api/admin_user.py b/apps/assets/api/admin_user.py index 263d669fd..f2229022f 100644 --- a/apps/assets/api/admin_user.py +++ b/apps/assets/api/admin_user.py @@ -87,6 +87,7 @@ class AdminUserTestConnectiveApi(generics.RetrieveAPIView): """ queryset = AdminUser.objects.all() permission_classes = (IsOrgAdmin,) + serializer_class = serializers.TaskIDSerializer def retrieve(self, request, *args, **kwargs): admin_user = self.get_object() diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py index cd343b2a5..abb0243db 100644 --- a/apps/assets/api/asset.py +++ b/apps/assets/api/asset.py @@ -113,6 +113,7 @@ class AssetAdminUserTestApi(generics.RetrieveAPIView): """ queryset = Asset.objects.all() permission_classes = (IsOrgAdmin,) + serializer_class = serializers.TaskIDSerializer def retrieve(self, request, *args, **kwargs): asset_id = kwargs.get('pk') @@ -124,6 +125,7 @@ class AssetAdminUserTestApi(generics.RetrieveAPIView): class AssetGatewayApi(generics.RetrieveAPIView): queryset = Asset.objects.all() permission_classes = (IsOrgAdminOrAppUser,) + serializer_class = serializers.GatewayWithAuthSerializer def retrieve(self, request, *args, **kwargs): asset_id = kwargs.get('pk') diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index e66e4bfc9..5c131f400 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -117,6 +117,7 @@ class SystemUserAssetsListView(generics.ListAPIView): class SystemUserPushToAssetApi(generics.RetrieveAPIView): queryset = SystemUser.objects.all() permission_classes = (IsOrgAdmin,) + serializer_class = serializers.TaskIDSerializer def retrieve(self, request, *args, **kwargs): system_user = self.get_object() @@ -129,6 +130,7 @@ class SystemUserPushToAssetApi(generics.RetrieveAPIView): class SystemUserTestAssetConnectivityApi(generics.RetrieveAPIView): queryset = SystemUser.objects.all() permission_classes = (IsOrgAdmin,) + serializer_class = serializers.TaskIDSerializer def retrieve(self, request, *args, **kwargs): system_user = self.get_object() diff --git a/apps/assets/serializers/admin_user.py b/apps/assets/serializers/admin_user.py index e1ecdf1c3..009caa1ce 100644 --- a/apps/assets/serializers/admin_user.py +++ b/apps/assets/serializers/admin_user.py @@ -58,7 +58,7 @@ class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer): 管理用户更新关联到的集群 """ nodes = serializers.PrimaryKeyRelatedField( - many=True, queryset = Node.objects.all() + many=True, queryset=Node.objects.all() ) class Meta: @@ -66,4 +66,5 @@ class ReplaceNodeAdminUserSerializer(serializers.ModelSerializer): fields = ['id', 'nodes'] - +class TaskIDSerializer(serializers.Serializer): + task = serializers.CharField(read_only=True) diff --git a/apps/assets/tasks.py b/apps/assets/tasks.py index ce5be0b6f..b75472a00 100644 --- a/apps/assets/tasks.py +++ b/apps/assets/tasks.py @@ -1,16 +1,18 @@ # ~*~ coding: utf-8 ~*~ import json import re -import time import os from celery import shared_task from django.utils.translation import ugettext as _ from django.core.cache import cache -from common.utils import capacity_convert, \ - sum_capacity, encrypt_password, get_logger -from ops.celery.utils import register_as_period_task, after_app_shutdown_clean +from common.utils import ( + capacity_convert, sum_capacity, encrypt_password, get_logger +) +from ops.celery.decorator import ( + register_as_period_task, after_app_shutdown_clean_periodic +) from .models import SystemUser, AdminUser, Asset from . import const @@ -132,7 +134,7 @@ def update_assets_hardware_info_util(assets, task_name=None): @shared_task def update_asset_hardware_info_manual(asset): task_name = _("Update asset hardware info: {}").format(asset.hostname) - return update_assets_hardware_info_util( + update_assets_hardware_info_util( [asset], task_name=task_name ) @@ -221,12 +223,14 @@ def test_admin_user_connectivity_period(): for admin_user in admin_users: task_name = _("Test admin user connectivity period: {}").format(admin_user.name) test_admin_user_connectivity_util(admin_user, task_name) + cache.set(key, 1, 60*40) @shared_task def test_admin_user_connectivity_manual(admin_user): task_name = _("Test admin user connectivity: {}").format(admin_user.name) - return test_admin_user_connectivity_util(admin_user, task_name) + test_admin_user_connectivity_util(admin_user, task_name) + return True ## System user connective ## @@ -394,13 +398,13 @@ def push_system_user_to_assets(system_user, assets): @shared_task -@after_app_shutdown_clean +@after_app_shutdown_clean_periodic def test_system_user_connectability_period(): pass @shared_task -@after_app_shutdown_clean +@after_app_shutdown_clean_periodic def test_admin_user_connectability_period(): pass @@ -408,7 +412,7 @@ def test_admin_user_connectability_period(): # @shared_task # @register_as_period_task(interval=3600) # @after_app_ready_start -# # @after_app_shutdown_clean +# @after_app_shutdown_clean_periodic # def push_system_user_period(): # for system_user in SystemUser.objects.all(): # push_system_user_related_nodes(system_user) diff --git a/apps/authentication/radius/__init__.py b/apps/authentication/radius/__init__.py new file mode 100644 index 000000000..ec51c5a2b --- /dev/null +++ b/apps/authentication/radius/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# diff --git a/apps/authentication/radius/backends.py b/apps/authentication/radius/backends.py new file mode 100644 index 000000000..6c95bf108 --- /dev/null +++ b/apps/authentication/radius/backends.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# + +from django.contrib.auth import get_user_model +from radiusauth.backends import RADIUSBackend, RADIUSRealmBackend +from django.conf import settings + +User = get_user_model() + + +class CreateUserMixin: + def get_django_user(self, username, password=None): + if isinstance(username, bytes): + username = username.decode() + try: + user = User.objects.get(username=username) + except User.DoesNotExist: + if '@' in username: + email = username + else: + email_suffix = settings.EMAIL_SUFFIX + email = '{}@{}'.format(username, email_suffix) + user = User(username=username, name=username, email=email) + user.source = user.SOURCE_RADIUS + user.save() + return user + + +class RadiusBackend(CreateUserMixin, RADIUSBackend): + pass + + +class RadiusRealmBackend(CreateUserMixin, RADIUSRealmBackend): + pass diff --git a/apps/common/api.py b/apps/common/api.py index 4aa8f82ce..ba7820505 100644 --- a/apps/common/api.py +++ b/apps/common/api.py @@ -4,15 +4,20 @@ import os import json import jms_storage +import uuid from rest_framework.views import Response, APIView +from rest_framework import generics from ldap3 import Server, Connection -from django.core.mail import get_connection, send_mail +from django.core.mail import send_mail +from django.core.cache import cache from django.utils.translation import ugettext_lazy as _ from django.conf import settings from .permissions import IsOrgAdmin, IsSuperUser -from .serializers import MailTestSerializer, LDAPTestSerializer +from .serializers import ( + MailTestSerializer, LDAPTestSerializer, OutputSerializer +) from .models import Setting @@ -189,4 +194,39 @@ class DjangoSettingsAPI(APIView): return Response(data) +class LogTailApi(generics.RetrieveAPIView): + permission_classes = () + buff_size = 1024 * 10 + serializer_class = OutputSerializer + end = False + def is_file_finish_write(self): + return True + + def get_log_path(self): + raise NotImplementedError() + + def get(self, request, *args, **kwargs): + mark = request.query_params.get("mark") or str(uuid.uuid4()) + log_path = self.get_log_path() + + if not log_path or not os.path.isfile(log_path): + if self.is_file_finish_write(): + return Response({ + "data": 'Not found the log', + 'end': True, + 'mark': mark} + ) + else: + return Response({"data": _("Waiting ...\n")}, status=200) + + with open(log_path, 'r') as f: + offset = cache.get(mark, 0) + f.seek(offset) + data = f.read(self.buff_size).replace('\n', '\r\n') + mark = str(uuid.uuid4()) + cache.set(mark, f.tell(), 5) + + if data == '' and self.is_file_finish_write(): + self.end = True + return Response({"data": data, 'end': self.end, 'mark': mark}) diff --git a/apps/common/serializers.py b/apps/common/serializers.py index 9d389776d..23ff9fb90 100644 --- a/apps/common/serializers.py +++ b/apps/common/serializers.py @@ -19,3 +19,8 @@ class LDAPTestSerializer(serializers.Serializer): AUTH_LDAP_USER_ATTR_MAP = serializers.CharField() AUTH_LDAP_START_TLS = serializers.BooleanField(required=False) + +class OutputSerializer(serializers.Serializer): + output = serializers.CharField() + is_end = serializers.BooleanField() + mark = serializers.CharField() diff --git a/apps/common/signals_handler.py b/apps/common/signals_handler.py index 96142e394..7879f94b2 100644 --- a/apps/common/signals_handler.py +++ b/apps/common/signals_handler.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # +import json + from django.dispatch import receiver from django.db.models.signals import post_save, pre_save from django.conf import LazySettings, empty @@ -8,7 +10,7 @@ from django.core.cache import cache from jumpserver.utils import current_request from .models import Setting -from .utils import get_logger +from .utils import get_logger, ssh_key_gen from .signals import django_ready logger = get_logger(__file__) @@ -16,23 +18,25 @@ logger = get_logger(__file__) @receiver(post_save, sender=Setting, dispatch_uid="my_unique_identifier") def refresh_settings_on_changed(sender, instance=None, **kwargs): - logger.debug("Receive setting item change") - logger.debug(" - refresh setting: {}".format(instance.name)) if instance: instance.refresh_setting() @receiver(django_ready, dispatch_uid="my_unique_identifier") -def refresh_all_settings_on_django_ready(sender, **kwargs): - logger.debug("Receive django ready signal") - logger.debug(" - fresh all settings") +def monkey_patch_settings(sender, **kwargs): cache_key_prefix = '_SETTING_' + uncached_settings = [ + 'CACHES', 'DEBUG', 'SECRET_KEY', 'INSTALLED_APPS', + 'ROOT_URLCONF', 'TEMPLATES', 'DATABASES', '_wrapped', + 'CELERY_LOG_DIR' + ] def monkey_patch_getattr(self, name): - key = cache_key_prefix + name - cached = cache.get(key) - if cached is not None: - return cached + if name not in uncached_settings: + key = cache_key_prefix + name + cached = cache.get(key) + if cached is not None: + return cached if self._wrapped is empty: self._setup(name) val = getattr(self._wrapped, name) @@ -62,6 +66,18 @@ def refresh_all_settings_on_django_ready(sender, **kwargs): pass +@receiver(django_ready) +def auto_generate_terminal_host_key(sender, **kwargs): + try: + if Setting.objects.filter(name='TERMINAL_HOST_KEY').exists(): + return + except ProgrammingError: + return + private_key, public_key = ssh_key_gen() + value = json.dumps(private_key) + Setting.objects.create(name='TERMINAL_HOST_KEY', value=value) + + @receiver(pre_save, dispatch_uid="my_unique_identifier") def on_create_set_created_by(sender, instance=None, **kwargs): if getattr(instance, '_ignore_auto_created_by', False) is True: diff --git a/apps/common/utils.py b/apps/common/utils.py index 71913d196..2668c2ff2 100644 --- a/apps/common/utils.py +++ b/apps/common/utils.py @@ -406,24 +406,6 @@ def get_replay_storage_setting(): return value -class TeeObj: - origin_stdout = sys.stdout - - def __init__(self, file_obj): - self.file_obj = file_obj - - def write(self, msg): - self.origin_stdout.write(msg) - self.file_obj.write(msg.replace('*', '')) - - def flush(self): - self.origin_stdout.flush() - self.file_obj.flush() - - def close(self): - self.file_obj.close() - - def with_cache(func): cache = {} key = "_{}.{}".format(func.__module__, func.__name__) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index ed19f3567..7a749cd77 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -193,14 +193,16 @@ class Config(dict): if self.root_path: filename = os.path.join(self.root_path, filename) try: - with open(filename) as json_file: - obj = yaml.load(json_file) + with open(filename) as f: + obj = yaml.load(f) except IOError as e: if silent and e.errno in (errno.ENOENT, errno.EISDIR): return False e.strerror = 'Unable to load configuration file (%s)' % e.strerror raise - return self.from_mapping(obj) + if obj: + return self.from_mapping(obj) + return True def from_mapping(self, *mapping, **kwargs): """Updates the config like :meth:`update` ignoring items with non-upper @@ -286,8 +288,8 @@ class Config(dict): defaults = { - 'SECRET_KEY': '2vym+ky!997d5kkcc64mnz06y1mmui3lut#(^wd=%s_qj$1%x', - 'BOOTSTRAP_TOKEN': 'PleaseChangeMe', + 'SECRET_KEY': '', + 'BOOTSTRAP_TOKEN': '', 'DEBUG': True, 'SITE_URL': 'http://localhost', 'LOG_LEVEL': 'DEBUG', @@ -312,6 +314,7 @@ defaults = { 'SESSION_COOKIE_AGE': 3600 * 24, 'SESSION_EXPIRE_AT_BROWSER_CLOSE': False, 'AUTH_OPENID': False, + 'OTP_VALID_WINDOW': 0, 'OTP_ISSUER_NAME': 'Jumpserver', 'EMAIL_SUFFIX': 'jumpserver.org', 'TERMINAL_PASSWORD_AUTH': True, @@ -320,6 +323,7 @@ defaults = { 'TERMINAL_ASSET_LIST_SORT_BY': 'hostname', 'TERMINAL_ASSET_LIST_PAGE_SIZE': 'auto', 'TERMINAL_SESSION_KEEP_DURATION': 9999, + 'TERMINAL_HOST_KEY': '', 'SECURITY_MFA_AUTH': False, 'SECURITY_LOGIN_LIMIT_COUNT': 7, 'SECURITY_LOGIN_LIMIT_TIME': 30, @@ -330,21 +334,48 @@ defaults = { 'SECURITY_PASSWORD_LOWER_CASE': False, 'SECURITY_PASSWORD_NUMBER': False, 'SECURITY_PASSWORD_SPECIAL_CHAR': False, + 'AUTH_RADIUS': False, + 'RADIUS_SERVER': 'localhost', + 'RADIUS_PORT': 1812, + 'RADIUS_SECRET': '', + 'HTTP_BIND_HOST': '0.0.0.0', + 'HTTP_LISTEN_PORT': 8080, } +def load_from_object(config): + try: + from config import config as c + config.from_object(c) + return True + except ImportError: + pass + return False + + +def load_from_yml(config): + for i in ['config.yml', 'config.yaml']: + if not os.path.isfile(os.path.join(config.root_path, i)): + continue + loaded = config.from_yaml(i) + if loaded: + return True + return False + + def load_user_config(): sys.path.insert(0, PROJECT_DIR) config = Config(PROJECT_DIR, defaults) - try: - from config import config as c - config.from_object(c) - except ImportError: + + loaded = load_from_object(config) + if not loaded: + loaded = load_from_yml(config) + if not loaded: msg = """ Error: No config file found. - You can run `cp config_example.py config.py`, and edit it. + You can run `cp config_example.yml config.yml`, and edit it. """ raise ImportError(msg) return config diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index ca3d5afa3..a7aef986e 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/1.10/ref/settings/ import os import sys +import socket import ldap # from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion @@ -23,6 +24,13 @@ from .conf import load_user_config BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) PROJECT_DIR = os.path.dirname(BASE_DIR) CONFIG = load_user_config() +LOG_DIR = os.path.join(PROJECT_DIR, 'logs') +HOSTNAME = socket.gethostname() +JUMPSERVER_LOG_FILE = os.path.join(LOG_DIR, 'jumpserver-{}.log'.format(HOSTNAME)) +ANSIBLE_LOG_FILE = os.path.join(LOG_DIR, 'ansible-{}.log'.format(HOSTNAME)) + +if not os.path.isdir(LOG_DIR): + os.makedirs(LOG_DIR) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ @@ -209,19 +217,21 @@ LOGGING = { 'formatter': 'main' }, 'file': { + 'encoding': 'utf8', 'level': 'DEBUG', 'class': 'logging.handlers.TimedRotatingFileHandler', 'when': "D", 'interval': 1, "backupCount": 7, 'formatter': 'main', - 'filename': os.path.join(PROJECT_DIR, 'logs', 'jumpserver.log') + 'filename': JUMPSERVER_LOG_FILE, }, 'ansible_logs': { + 'encoding': 'utf8', 'level': 'DEBUG', 'class': 'logging.FileHandler', 'formatter': 'main', - 'filename': os.path.join(PROJECT_DIR, 'logs', 'ansible.log') + 'filename': ANSIBLE_LOG_FILE, }, }, 'loggers': { @@ -400,6 +410,19 @@ if AUTH_OPENID: AUTHENTICATION_BACKENDS.insert(0, AUTH_OPENID_BACKENDS[0]) AUTHENTICATION_BACKENDS.insert(0, AUTH_OPENID_BACKENDS[1]) +# Radius Auth +AUTH_RADIUS = CONFIG.AUTH_RADIUS +AUTH_RADIUS_BACKEND = 'authentication.radius.backends.RadiusBackend' +RADIUS_SERVER = CONFIG.RADIUS_SERVER +RADIUS_PORT = CONFIG.RADIUS_PORT +RADIUS_SECRET = CONFIG.RADIUS_SECRET + +if AUTH_RADIUS: + AUTHENTICATION_BACKENDS.insert(0, AUTH_RADIUS_BACKEND) + +# Dump all celery log to here +CELERY_LOG_DIR = os.path.join(PROJECT_DIR, 'data', 'celery') + # Celery using redis as broker CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % { 'password': CONFIG.REDIS_PASSWORD, @@ -413,14 +436,16 @@ CELERY_RESULT_BACKEND = CELERY_BROKER_URL CELERY_ACCEPT_CONTENT = ['json', 'pickle'] CELERY_RESULT_EXPIRES = 3600 # CELERY_WORKER_LOG_FORMAT = '%(asctime)s [%(module)s %(levelname)s] %(message)s' -CELERY_WORKER_LOG_FORMAT = '%(message)s' -# CELERY_WORKER_TASK_LOG_FORMAT = '%(asctime)s [%(module)s %(levelname)s] %(message)s' -CELERY_WORKER_TASK_LOG_FORMAT = '%(message)s' +# CELERY_WORKER_LOG_FORMAT = '%(message)s' +CELERY_WORKER_TASK_LOG_FORMAT = '%(task_id)s %(task_name)s %(message)s' +# CELERY_WORKER_TASK_LOG_FORMAT = '%(message)s' # CELERY_WORKER_LOG_FORMAT = '%(asctime)s [%(module)s %(levelname)s] %(message)s' +CELERY_WORKER_LOG_FORMAT = '%(message)s' CELERY_TASK_EAGER_PROPAGATES = True -CELERY_REDIRECT_STDOUTS = True -CELERY_REDIRECT_STDOUTS_LEVEL = "INFO" -CELERY_WORKER_HIJACK_ROOT_LOGGER = False +CELERY_WORKER_REDIRECT_STDOUTS = True +CELERY_WORKER_REDIRECT_STDOUTS_LEVEL = "INFO" +# CELERY_WORKER_HIJACK_ROOT_LOGGER = False +CELERY_WORKER_MAX_TASKS_PER_CHILD = 40 # Cache use redis CACHES = { @@ -492,6 +517,7 @@ TERMINAL_HEARTBEAT_INTERVAL = CONFIG.TERMINAL_HEARTBEAT_INTERVAL TERMINAL_ASSET_LIST_SORT_BY = CONFIG.TERMINAL_ASSET_LIST_SORT_BY TERMINAL_ASSET_LIST_PAGE_SIZE = CONFIG.TERMINAL_ASSET_LIST_PAGE_SIZE TERMINAL_SESSION_KEEP_DURATION = CONFIG.TERMINAL_SESSION_KEEP_DURATION +TERMINAL_HOST_KEY = CONFIG.TERMINAL_HOST_KEY # Django bootstrap3 setting, more see http://django-bootstrap3.readthedocs.io/en/latest/settings.html BOOTSTRAP3 = { diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index a6518874e..cf3c41fc8 100644 Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 98e838293..92e081d08 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Jumpserver 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-12-27 15:48+0800\n" +"POT-Creation-Date: 2019-01-16 17:58+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: Jumpserver team\n" @@ -17,11 +17,11 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: assets/api/node.py:261 +#: assets/api/node.py:264 msgid "Update node asset hardware information: {}" msgstr "更新节点资产硬件信息: {}" -#: assets/api/node.py:275 +#: assets/api/node.py:278 msgid "Test if the assets under the node are connectable: {}" msgstr "测试节点下资产是否可连接: {}" @@ -65,9 +65,10 @@ msgstr "网域" #: assets/forms/asset.py:124 assets/models/node.py:31 #: assets/templates/assets/asset_create.html:30 #: assets/templates/assets/asset_update.html:35 perms/forms.py:45 -#: perms/forms.py:52 perms/models.py:79 +#: perms/forms.py:52 perms/models.py:85 #: perms/templates/perms/asset_permission_list.html:57 -#: perms/templates/perms/asset_permission_list.html:117 +#: perms/templates/perms/asset_permission_list.html:78 +#: perms/templates/perms/asset_permission_list.html:128 #: xpack/plugins/cloud/models.py:123 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:63 #: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:66 @@ -118,8 +119,8 @@ msgstr "端口" #: perms/models.py:31 #: perms/templates/perms/asset_permission_create_update.html:45 #: perms/templates/perms/asset_permission_list.html:56 -#: perms/templates/perms/asset_permission_list.html:114 -#: terminal/backends/command/models.py:13 terminal/models.py:141 +#: perms/templates/perms/asset_permission_list.html:125 +#: terminal/backends/command/models.py:13 terminal/models.py:143 #: terminal/templates/terminal/command_list.html:40 #: terminal/templates/terminal/command_list.html:73 #: terminal/templates/terminal/session_list.html:41 @@ -156,10 +157,11 @@ msgstr "不能包含特殊字符" #: orgs/models.py:12 perms/models.py:28 #: perms/templates/perms/asset_permission_detail.html:62 #: perms/templates/perms/asset_permission_list.html:53 -#: perms/templates/perms/asset_permission_user.html:54 terminal/models.py:20 -#: terminal/models.py:198 terminal/templates/terminal/terminal_detail.html:43 +#: perms/templates/perms/asset_permission_list.html:72 +#: perms/templates/perms/asset_permission_user.html:54 terminal/models.py:21 +#: terminal/models.py:212 terminal/templates/terminal/terminal_detail.html:43 #: terminal/templates/terminal/terminal_list.html:29 users/models/group.py:14 -#: users/models/user.py:53 users/templates/users/_select_user_modal.html:13 +#: users/models/user.py:55 users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_detail.html:63 #: users/templates/users/user_group_detail.html:55 #: users/templates/users/user_group_list.html:12 @@ -183,8 +185,9 @@ msgstr "名称" #: assets/templates/assets/system_user_detail.html:62 #: assets/templates/assets/system_user_list.html:30 #: audits/templates/audits/login_log_list.html:49 +#: perms/templates/perms/asset_permission_list.html:74 #: perms/templates/perms/asset_permission_user.html:55 users/forms.py:15 -#: users/forms.py:33 users/models/authentication.py:77 users/models/user.py:51 +#: users/forms.py:33 users/models/authentication.py:77 users/models/user.py:53 #: users/templates/users/_select_user_modal.html:14 #: users/templates/users/login.html:64 #: users/templates/users/user_detail.html:67 @@ -210,7 +213,7 @@ msgstr "密码或密钥密码" msgid "Password" msgstr "密码" -#: assets/forms/user.py:29 users/models/user.py:80 +#: assets/forms/user.py:29 users/models/user.py:82 msgid "Private key" msgstr "ssh私钥" @@ -274,6 +277,7 @@ msgstr "IP" #: assets/templates/assets/user_asset_list.html:45 #: assets/templates/assets/user_asset_list.html:150 common/forms.py:130 #: perms/templates/perms/asset_permission_asset.html:54 +#: perms/templates/perms/asset_permission_list.html:77 #: users/templates/users/user_granted_asset.html:44 #: users/templates/users/user_group_granted_asset.html:44 msgid "Hostname" @@ -381,8 +385,8 @@ msgstr "标签管理" #: assets/templates/assets/domain_detail.html:72 #: assets/templates/assets/system_user_detail.html:100 #: ops/templates/ops/adhoc_detail.html:86 orgs/models.py:15 perms/models.py:37 -#: perms/models.py:84 perms/templates/perms/asset_permission_detail.html:98 -#: users/models/user.py:94 users/templates/users/user_detail.html:111 +#: perms/models.py:90 perms/templates/perms/asset_permission_detail.html:98 +#: users/models/user.py:96 users/templates/users/user_detail.html:111 #: xpack/plugins/cloud/models.py:55 xpack/plugins/cloud/models.py:127 msgid "Created by" msgstr "创建者" @@ -394,7 +398,7 @@ msgstr "创建者" #: assets/templates/assets/domain_detail.html:68 #: assets/templates/assets/system_user_detail.html:96 #: ops/templates/ops/adhoc_detail.html:90 ops/templates/ops/task_detail.html:64 -#: orgs/models.py:16 perms/models.py:38 perms/models.py:85 +#: orgs/models.py:16 perms/models.py:38 perms/models.py:91 #: perms/templates/perms/asset_permission_detail.html:94 #: terminal/templates/terminal/terminal_detail.html:59 users/models/group.py:17 #: users/templates/users/user_group_detail.html:63 @@ -422,9 +426,9 @@ msgstr "创建日期" #: assets/templates/assets/system_user_list.html:37 #: assets/templates/assets/user_asset_list.html:159 common/models.py:34 #: ops/models/adhoc.py:43 orgs/models.py:17 perms/models.py:39 -#: perms/models.py:86 perms/templates/perms/asset_permission_detail.html:102 -#: terminal/models.py:30 terminal/templates/terminal/terminal_detail.html:63 -#: users/models/group.py:15 users/models/user.py:86 +#: perms/models.py:92 perms/templates/perms/asset_permission_detail.html:102 +#: terminal/models.py:31 terminal/templates/terminal/terminal_detail.html:63 +#: users/models/group.py:15 users/models/user.py:88 #: users/templates/users/user_detail.html:127 #: users/templates/users/user_group_detail.html:67 #: users/templates/users/user_group_list.html:14 @@ -475,7 +479,7 @@ msgstr "带宽" msgid "Contact" msgstr "联系人" -#: assets/models/cluster.py:22 users/models/user.py:72 +#: assets/models/cluster.py:22 users/models/user.py:74 #: users/templates/users/user_detail.html:76 msgid "Phone" msgstr "手机" @@ -501,7 +505,7 @@ msgid "Default" msgstr "默认" #: assets/models/cluster.py:36 assets/models/label.py:14 -#: users/models/user.py:439 +#: users/models/user.py:441 msgid "System" msgstr "系统" @@ -529,8 +533,8 @@ msgstr "BGP全网通" msgid "Regex" msgstr "正则表达式" -#: assets/models/cmd_filter.py:36 ops/models/command.py:19 -#: ops/templates/ops/command_execution_list.html:60 terminal/models.py:147 +#: assets/models/cmd_filter.py:36 ops/models/command.py:21 +#: ops/templates/ops/command_execution_list.html:60 terminal/models.py:149 #: terminal/templates/terminal/command_list.html:55 #: terminal/templates/terminal/command_list.html:71 #: terminal/templates/terminal/session_detail.html:48 @@ -633,13 +637,13 @@ msgstr "默认资产组" #: perms/models.py:29 #: perms/templates/perms/asset_permission_create_update.html:41 #: perms/templates/perms/asset_permission_list.html:54 -#: perms/templates/perms/asset_permission_list.html:108 templates/index.html:87 -#: terminal/backends/command/models.py:12 terminal/models.py:140 +#: perms/templates/perms/asset_permission_list.html:119 templates/index.html:87 +#: terminal/backends/command/models.py:12 terminal/models.py:142 #: terminal/templates/terminal/command_list.html:32 #: terminal/templates/terminal/command_list.html:72 #: terminal/templates/terminal/session_list.html:33 #: terminal/templates/terminal/session_list.html:71 users/forms.py:303 -#: users/models/user.py:33 users/models/user.py:427 +#: users/models/user.py:33 users/models/user.py:429 #: users/templates/users/user_group_detail.html:78 #: users/templates/users/user_group_list.html:13 users/views/user.py:386 #: xpack/plugins/orgs/forms.py:26 @@ -717,11 +721,12 @@ msgstr "登录模式" #: assets/models/user.py:247 assets/templates/assets/user_asset_list.html:156 #: audits/models.py:19 audits/templates/audits/ftp_log_list.html:49 #: audits/templates/audits/ftp_log_list.html:72 perms/forms.py:48 -#: perms/models.py:33 perms/models.py:81 +#: perms/models.py:33 perms/models.py:87 #: perms/templates/perms/asset_permission_detail.html:140 #: perms/templates/perms/asset_permission_list.html:58 -#: perms/templates/perms/asset_permission_list.html:120 templates/_nav.html:25 -#: terminal/backends/command/models.py:14 terminal/models.py:142 +#: perms/templates/perms/asset_permission_list.html:79 +#: perms/templates/perms/asset_permission_list.html:131 templates/_nav.html:25 +#: terminal/backends/command/models.py:14 terminal/models.py:144 #: terminal/templates/terminal/command_list.html:48 #: terminal/templates/terminal/command_list.html:74 #: terminal/templates/terminal/session_list.html:49 @@ -735,68 +740,68 @@ msgstr "系统用户" msgid "%(value)s is not an even number" msgstr "%(value)s is not an even number" -#: assets/tasks.py:31 +#: assets/tasks.py:33 msgid "Asset has been disabled, skipped: {}" msgstr "资产或许不支持ansible, 跳过: {}" -#: assets/tasks.py:35 +#: assets/tasks.py:37 msgid "Asset may not be support ansible, skipped: {}" msgstr "资产或许不支持ansible, 跳过: {}" -#: assets/tasks.py:40 +#: assets/tasks.py:42 msgid "No assets matched, stop task" msgstr "没有匹配到资产,结束任务" -#: assets/tasks.py:65 +#: assets/tasks.py:67 msgid "Get asset info failed: {}" msgstr "获取资产信息失败:{}" -#: assets/tasks.py:115 +#: assets/tasks.py:117 msgid "Update some assets hardware info" msgstr "更新资产硬件信息" -#: assets/tasks.py:134 +#: assets/tasks.py:136 msgid "Update asset hardware info: {}" msgstr "更新资产硬件信息: {}" -#: assets/tasks.py:159 +#: assets/tasks.py:161 msgid "Test assets connectivity" msgstr "测试资产可连接性" -#: assets/tasks.py:183 +#: assets/tasks.py:185 msgid "Test assets connectivity: {}" msgstr "测试资产可连接性: {}" -#: assets/tasks.py:222 +#: assets/tasks.py:224 msgid "Test admin user connectivity period: {}" msgstr "定期测试管理账号可连接性: {}" -#: assets/tasks.py:228 +#: assets/tasks.py:231 msgid "Test admin user connectivity: {}" msgstr "测试管理行号可连接性: {}" -#: assets/tasks.py:266 +#: assets/tasks.py:270 msgid "Test system user connectivity: {}" msgstr "测试系统用户可连接性: {}" -#: assets/tasks.py:273 +#: assets/tasks.py:277 msgid "Test system user connectivity: {} => {}" msgstr "测试系统用户可连接性: {} => {}" -#: assets/tasks.py:286 +#: assets/tasks.py:290 msgid "Test system user connectivity period: {}" msgstr "定期测试系统用户可连接性: {}" -#: assets/tasks.py:358 +#: assets/tasks.py:362 msgid "" "Push system user task skip, auto push not enable or protocol is not ssh: {}" msgstr "推送系统用户任务跳过,自动推送没有打开,或协议不是ssh: {}" -#: assets/tasks.py:378 assets/tasks.py:392 +#: assets/tasks.py:382 assets/tasks.py:396 msgid "Push system users to assets: {}" msgstr "推送系统用户到入资产: {}" -#: assets/tasks.py:384 +#: assets/tasks.py:388 msgid "Push system users to asset: {} => {}" msgstr "推送系统用户到入资产: {} => {}" @@ -880,7 +885,7 @@ msgstr "自动生成密钥" #: assets/templates/assets/asset_update.html:64 #: assets/templates/assets/gateway_create_update.html:53 #: perms/templates/perms/asset_permission_create_update.html:50 -#: terminal/templates/terminal/terminal_update.html:42 +#: terminal/templates/terminal/terminal_update.html:40 msgid "Other" msgstr "其它" @@ -902,7 +907,7 @@ msgstr "其它" #: common/templates/common/security_setting.html:70 #: common/templates/common/terminal_setting.html:68 #: perms/templates/perms/asset_permission_create_update.html:80 -#: terminal/templates/terminal/terminal_update.html:47 +#: terminal/templates/terminal/terminal_update.html:45 #: users/templates/users/_user.html:50 #: users/templates/users/user_bulk_update.html:23 #: users/templates/users/user_detail.html:176 @@ -937,7 +942,7 @@ msgstr "重置" #: perms/templates/perms/asset_permission_create_update.html:81 #: terminal/templates/terminal/command_list.html:103 #: terminal/templates/terminal/session_list.html:127 -#: terminal/templates/terminal/terminal_update.html:48 +#: terminal/templates/terminal/terminal_update.html:46 #: users/templates/users/_user.html:51 #: users/templates/users/forgot_password.html:45 #: users/templates/users/user_bulk_update.html:24 @@ -1021,7 +1026,7 @@ msgstr "测试" #: assets/templates/assets/system_user_detail.html:26 #: assets/templates/assets/system_user_list.html:92 audits/models.py:32 #: perms/templates/perms/asset_permission_detail.html:30 -#: perms/templates/perms/asset_permission_list.html:166 +#: perms/templates/perms/asset_permission_list.html:177 #: terminal/templates/terminal/terminal_detail.html:16 #: terminal/templates/terminal/terminal_list.html:71 #: users/templates/users/user_detail.html:25 @@ -1056,7 +1061,7 @@ msgstr "更新" #: common/templates/common/terminal_setting.html:112 #: ops/templates/ops/task_list.html:72 #: perms/templates/perms/asset_permission_detail.html:34 -#: perms/templates/perms/asset_permission_list.html:167 +#: perms/templates/perms/asset_permission_list.html:178 #: terminal/templates/terminal/terminal_list.html:73 #: users/templates/users/user_detail.html:30 #: users/templates/users/user_group_detail.html:32 @@ -1166,10 +1171,9 @@ msgstr "快速修改" #: assets/templates/assets/asset_detail.html:151 #: assets/templates/assets/user_asset_list.html:47 perms/models.py:34 -#: perms/models.py:82 +#: perms/models.py:88 #: perms/templates/perms/asset_permission_create_update.html:52 #: perms/templates/perms/asset_permission_detail.html:120 -#: perms/templates/perms/asset_permission_list.html:59 #: terminal/templates/terminal/terminal_list.html:34 #: users/templates/users/_select_user_modal.html:18 #: users/templates/users/user_detail.html:144 @@ -1657,7 +1661,7 @@ msgstr "系统用户资产" #: audits/templates/audits/ftp_log_list.html:73 #: audits/templates/audits/operate_log_list.html:70 #: audits/templates/audits/password_change_log_list.html:52 -#: terminal/models.py:144 terminal/templates/terminal/session_list.html:74 +#: terminal/models.py:146 terminal/templates/terminal/session_list.html:74 #: terminal/templates/terminal/terminal_detail.html:47 msgid "Remote addr" msgstr "远端地址" @@ -1700,7 +1704,7 @@ msgstr "修改者" #: ops/templates/ops/adhoc_history_detail.html:61 #: ops/templates/ops/command_execution_list.html:65 #: ops/templates/ops/task_history.html:58 perms/models.py:35 -#: perms/templates/perms/asset_permission_detail.html:86 terminal/models.py:151 +#: perms/templates/perms/asset_permission_detail.html:86 terminal/models.py:153 #: terminal/templates/terminal/session_list.html:78 msgid "Date start" msgstr "开始日期" @@ -1744,7 +1748,7 @@ msgid "City" msgstr "城市" #: audits/templates/audits/login_log_list.html:54 users/forms.py:162 -#: users/models/authentication.py:82 users/models/user.py:75 +#: users/models/authentication.py:82 users/models/user.py:77 #: users/templates/users/first_login.html:45 msgid "MFA" msgstr "MFA" @@ -1801,37 +1805,41 @@ msgstr "登录日志" msgid "Command execution list" msgstr "命令执行列表" -#: common/api.py:22 +#: common/api.py:27 msgid "Test mail sent to {}, please check" msgstr "邮件已经发送{}, 请检查" -#: common/api.py:46 +#: common/api.py:51 msgid "Test ldap success" msgstr "连接LDAP成功" -#: common/api.py:76 +#: common/api.py:81 msgid "Search no entry matched in ou {}" msgstr "在ou:{}中没有匹配条目" -#: common/api.py:85 +#: common/api.py:90 msgid "Match {} s users" msgstr "匹配 {} 个用户" -#: common/api.py:108 common/api.py:144 +#: common/api.py:113 common/api.py:149 msgid "" "Error: Account invalid (Please make sure the information such as Access key " "or Secret key is correct)" msgstr "错误:账户无效 (请确保 Access key 或 Secret key 等信息正确)" -#: common/api.py:114 common/api.py:150 +#: common/api.py:119 common/api.py:155 msgid "Create succeed" msgstr "创建成功" -#: common/api.py:132 common/api.py:170 +#: common/api.py:137 common/api.py:175 #: common/templates/common/terminal_setting.html:151 msgid "Delete succeed" msgstr "删除成功" +#: common/api.py:221 +msgid "Waiting ...\n" +msgstr "" + #: common/const.py:6 #, python-format msgid "%(name)s was created successfully" @@ -2210,7 +2218,7 @@ msgid "Password check rule" msgstr "密码校验规则" #: common/templates/common/terminal_setting.html:76 terminal/forms.py:27 -#: terminal/models.py:24 +#: terminal/models.py:25 msgid "Command storage" msgstr "命令存储" @@ -2227,7 +2235,7 @@ msgid "Add" msgstr "添加" #: common/templates/common/terminal_setting.html:98 terminal/forms.py:32 -#: terminal/models.py:25 +#: terminal/models.py:26 msgid "Replay storage" msgstr "录像存储" @@ -2272,10 +2280,6 @@ msgstr "" "div>
如果你看到了这个页面,证明你访问的不是nginx监听的端口,祝你好运" -#: ops/api/celery.py:32 -msgid "Waiting ..." -msgstr "" - #: ops/models/adhoc.py:38 msgid "Interval" msgstr "间隔" @@ -2353,19 +2357,19 @@ msgstr "结果" msgid "Adhoc result summary" msgstr "汇总" -#: ops/models/command.py:20 xpack/plugins/cloud/models.py:170 +#: ops/models/command.py:22 xpack/plugins/cloud/models.py:170 msgid "Result" msgstr "结果" -#: ops/models/command.py:55 +#: ops/models/command.py:57 msgid "Task start" msgstr "任务开始" -#: ops/models/command.py:67 +#: ops/models/command.py:71 msgid "Command `{}` is forbidden ........" msgstr "命令 `{}` 不允许被执行 ......." -#: ops/models/command.py:73 +#: ops/models/command.py:77 msgid "Task end" msgstr "任务结束" @@ -2470,6 +2474,12 @@ msgstr "没有资产" msgid "Success assets" msgstr "成功资产" +#: ops/templates/ops/celery_task_log.html:4 +#, fuzzy +#| msgid "Task list" +msgid "Task log" +msgstr "任务列表" + #: ops/templates/ops/command_execution_create.html:71 #: terminal/templates/terminal/session_detail.html:91 #: terminal/templates/terminal/session_detail.html:100 @@ -2560,10 +2570,11 @@ msgstr "命令执行" msgid "Organization" msgstr "组织管理" -#: perms/forms.py:39 perms/models.py:30 perms/models.py:80 +#: perms/forms.py:39 perms/models.py:30 perms/models.py:86 #: perms/templates/perms/asset_permission_list.html:55 -#: perms/templates/perms/asset_permission_list.html:111 templates/_nav.html:14 -#: users/forms.py:273 users/models/group.py:26 users/models/user.py:59 +#: perms/templates/perms/asset_permission_list.html:75 +#: perms/templates/perms/asset_permission_list.html:122 templates/_nav.html:14 +#: users/forms.py:273 users/models/group.py:26 users/models/user.py:61 #: users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_detail.html:213 #: users/templates/users/user_list.html:26 @@ -2579,14 +2590,14 @@ msgstr "用户和用户组至少选一个" msgid "Asset or group at least one required" msgstr "资产和节点至少选一个" -#: perms/models.py:36 perms/models.py:83 +#: perms/models.py:36 perms/models.py:89 #: perms/templates/perms/asset_permission_detail.html:90 -#: users/models/user.py:91 users/templates/users/user_detail.html:107 +#: users/models/user.py:93 users/templates/users/user_detail.html:107 #: users/templates/users/user_profile.html:116 msgid "Date expired" msgstr "失效日期" -#: perms/models.py:45 perms/models.py:92 templates/_nav.html:34 +#: perms/models.py:45 perms/models.py:98 templates/_nav.html:34 msgid "Asset permission" msgstr "资产授权" @@ -2647,6 +2658,14 @@ msgstr "选择系统用户" msgid "Create permission" msgstr "创建授权规则" +#: perms/templates/perms/asset_permission_list.html:59 +#: perms/templates/perms/asset_permission_list.html:73 +#: users/templates/users/user_list.html:28 xpack/plugins/cloud/models.py:53 +#: xpack/plugins/cloud/templates/cloud/account_detail.html:60 +#: xpack/plugins/cloud/templates/cloud/account_list.html:14 +msgid "Validity" +msgstr "有效" + #: perms/templates/perms/asset_permission_user.html:35 msgid "User list of " msgstr "用户列表" @@ -2801,7 +2820,7 @@ msgstr "" #: users/views/group.py:60 users/views/group.py:76 users/views/group.py:92 #: users/views/login.py:349 users/views/user.py:68 users/views/user.py:83 #: users/views/user.py:113 users/views/user.py:194 users/views/user.py:355 -#: users/views/user.py:405 users/views/user.py:444 +#: users/views/user.py:405 users/views/user.py:445 msgid "Users" msgstr "用户管理" @@ -2989,7 +3008,7 @@ msgstr "一个月内历史汇总" #: templates/index.html:277 templates/index.html:301 msgid "Login count" -msgstr "登陆次数" +msgstr "登录次数" #: templates/index.html:277 templates/index.html:308 msgid "Active users" @@ -3009,7 +3028,7 @@ msgstr "禁用用户" #: templates/index.html:342 templates/index.html:394 msgid "Month not logged in user" -msgstr "月未登陆用户" +msgstr "月未登录用户" #: templates/index.html:368 templates/index.html:444 msgid "Access to the source" @@ -3017,7 +3036,7 @@ msgstr "访问来源" #: templates/index.html:418 templates/index.html:468 msgid "Month is logged into the host" -msgstr "月被登陆主机" +msgstr "月被登录主机" #: templates/index.html:418 templates/index.html:469 msgid "Disable host" @@ -3025,7 +3044,7 @@ msgstr "禁用主机" #: templates/index.html:418 templates/index.html:470 msgid "Month not logged on host" -msgstr "月未登陆主机" +msgstr "月未登录主机" #: templates/rest_framework/base.html:128 msgid "Filters" @@ -3054,55 +3073,55 @@ msgstr "" "录像文件支持存储到服务器端硬盘、AWS S3、 阿里云 OSS 中,默认存储到服务器端硬" "盘, 更多查看文档" -#: terminal/models.py:21 +#: terminal/models.py:22 msgid "Remote Address" msgstr "远端地址" -#: terminal/models.py:22 +#: terminal/models.py:23 msgid "SSH Port" msgstr "SSH端口" -#: terminal/models.py:23 +#: terminal/models.py:24 msgid "HTTP Port" msgstr "HTTP端口" -#: terminal/models.py:111 +#: terminal/models.py:113 msgid "Session Online" msgstr "在线会话" -#: terminal/models.py:112 +#: terminal/models.py:114 msgid "CPU Usage" msgstr "CPU使用" -#: terminal/models.py:113 +#: terminal/models.py:115 msgid "Memory Used" msgstr "内存使用" -#: terminal/models.py:114 +#: terminal/models.py:116 msgid "Connections" msgstr "连接数" -#: terminal/models.py:115 +#: terminal/models.py:117 msgid "Threads" msgstr "线程数" -#: terminal/models.py:116 +#: terminal/models.py:118 msgid "Boot Time" msgstr "运行时间" -#: terminal/models.py:146 terminal/templates/terminal/session_list.html:104 +#: terminal/models.py:148 terminal/templates/terminal/session_list.html:104 msgid "Replay" msgstr "回放" -#: terminal/models.py:150 +#: terminal/models.py:152 msgid "Date last active" msgstr "最后活跃日期" -#: terminal/models.py:152 +#: terminal/models.py:154 msgid "Date end" msgstr "结束日期" -#: terminal/models.py:199 +#: terminal/models.py:213 msgid "Args" msgstr "参数" @@ -3259,7 +3278,7 @@ msgstr "请先进行用户名和密码验证" msgid "MFA certification failed" msgstr "MFA认证失败" -#: users/api/user.py:140 +#: users/api/user.py:145 msgid "Could not reset self otp, use profile reset instead" msgstr "不能再该页面重置MFA, 请去个人信息页面重置" @@ -3318,7 +3337,7 @@ msgstr "" msgid "MFA code" msgstr "MFA 验证码" -#: users/forms.py:52 users/models/user.py:63 +#: users/forms.py:52 users/models/user.py:65 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:87 #: users/templates/users/user_list.html:25 @@ -3406,7 +3425,7 @@ msgstr "自动配置并下载SSH密钥" msgid "Paste your id_rsa.pub here." msgstr "复制你的公钥到这里" -#: users/forms.py:250 users/models/user.py:83 +#: users/forms.py:250 users/models/user.py:85 #: users/templates/users/first_login.html:42 #: users/templates/users/user_password_update.html:46 #: users/templates/users/user_profile.html:68 @@ -3473,7 +3492,7 @@ msgstr "Agent" msgid "Date login" msgstr "登录日期" -#: users/models/user.py:32 users/models/user.py:435 +#: users/models/user.py:32 users/models/user.py:437 msgid "Administrator" msgstr "管理员" @@ -3496,38 +3515,42 @@ msgstr "启用" msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:55 users/templates/users/user_detail.html:71 +#: users/models/user.py:57 users/templates/users/user_detail.html:71 #: users/templates/users/user_profile.html:59 msgid "Email" msgstr "邮件" -#: users/models/user.py:66 +#: users/models/user.py:68 msgid "Avatar" msgstr "头像" -#: users/models/user.py:69 users/templates/users/user_detail.html:82 +#: users/models/user.py:71 users/templates/users/user_detail.html:82 msgid "Wechat" msgstr "微信" -#: users/models/user.py:98 users/templates/users/user_detail.html:103 +#: users/models/user.py:100 users/templates/users/user_detail.html:103 #: users/templates/users/user_list.html:27 #: users/templates/users/user_profile.html:100 msgid "Source" msgstr "用户来源" -#: users/models/user.py:102 +#: users/models/user.py:104 msgid "Date password last updated" msgstr "最后更新密码日期" -#: users/models/user.py:126 users/templates/users/user_update.html:22 +#: users/models/user.py:128 users/templates/users/user_update.html:22 #: users/views/login.py:243 users/views/login.py:302 users/views/user.py:418 msgid "User auth from {}, go there change password" msgstr "用户认证源来自 {}, 请去相应系统修改密码" -#: users/models/user.py:438 +#: users/models/user.py:440 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" +#: users/serializers/v2.py:40 +msgid "name not unique" +msgstr "名称重复" + #: users/templates/users/_base_otp.html:27 msgid "Home page" msgstr "首页" @@ -3583,7 +3606,7 @@ msgstr "更新ssh密钥" #: users/templates/users/first_login.html:19 #: users/templates/users/first_login_done.html:19 msgid "First Login" -msgstr "首次登陆" +msgstr "首次登录" #: users/templates/users/first_login.html:72 msgid "I agree with the terms and conditions." @@ -3945,12 +3968,6 @@ msgstr "用户组删除" msgid "UserGroup Deleting failed." msgstr "用户组删除失败" -#: users/templates/users/user_list.html:28 xpack/plugins/cloud/models.py:53 -#: xpack/plugins/cloud/templates/cloud/account_detail.html:60 -#: xpack/plugins/cloud/templates/cloud/account_list.html:14 -msgid "Validity" -msgstr "账户状态" - #: users/templates/users/user_list.html:203 msgid "This will delete the selected users !!!" msgstr "删除选中用户 !!!" @@ -4279,7 +4296,7 @@ msgstr "用户组授权资产" msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: users/views/login.py:191 users/views/user.py:531 users/views/user.py:556 +#: users/views/login.py:191 users/views/user.py:532 users/views/user.py:557 msgid "MFA code invalid, or ntp sync server time" msgstr "MFA验证码不正确,或者服务器端时间不对" @@ -4320,13 +4337,13 @@ msgstr "Token错误或失效" msgid "Password not same" msgstr "密码不一致" -#: users/views/login.py:311 users/views/user.py:128 users/views/user.py:427 +#: users/views/login.py:311 users/views/user.py:128 users/views/user.py:428 msgid "* Your password does not meet the requirements" msgstr "* 您的密码不符合要求" #: users/views/login.py:349 msgid "First login" -msgstr "首次登陆" +msgstr "首次登录" #: users/views/user.py:145 msgid "Bulk update user success" @@ -4352,27 +4369,27 @@ msgstr "个人信息设置" msgid "Password update" msgstr "密码更新" -#: users/views/user.py:445 +#: users/views/user.py:446 msgid "Public key update" msgstr "密钥更新" -#: users/views/user.py:486 +#: users/views/user.py:487 msgid "Password invalid" msgstr "用户名或密码无效" -#: users/views/user.py:586 +#: users/views/user.py:587 msgid "MFA enable success" msgstr "MFA 绑定成功" -#: users/views/user.py:587 +#: users/views/user.py:588 msgid "MFA enable success, return login page" msgstr "MFA 绑定成功,返回到登录页面" -#: users/views/user.py:589 +#: users/views/user.py:590 msgid "MFA disable success" msgstr "MFA 解绑成功" -#: users/views/user.py:590 +#: users/views/user.py:591 msgid "MFA disable success, return login page" msgstr "MFA 解绑成功,返回登录页面" @@ -4631,6 +4648,9 @@ msgstr "创建组织" msgid "Update org" msgstr "更新组织" +#~ msgid "Valid" +#~ msgstr "账户状态" + #~ msgid "Error: Account invalid" #~ msgstr "错误: 账户无效" @@ -4643,11 +4663,6 @@ msgstr "更新组织" #~ msgid "No assets, task stop" #~ msgstr "没有匹配到资产,结束任务" -#, fuzzy -#~| msgid "Validity" -#~ msgid "Valid" -#~ msgstr "账户状态" - #~ msgid "You can't update the root node name" #~ msgstr "不能修改根节点名称" diff --git a/apps/ops/ansible/display.py b/apps/ops/ansible/display.py deleted file mode 100644 index 1494eb5ef..000000000 --- a/apps/ops/ansible/display.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -# - -import sys - - -class TeeObj: - origin_stdout = sys.stdout - - def __init__(self, file_obj): - self.file_obj = file_obj - - def write(self, msg): - self.origin_stdout.write(msg) - self.file_obj.write(msg.replace('*', '')) - - def flush(self): - self.origin_stdout.flush() - self.file_obj.flush() diff --git a/apps/ops/ansible/runner.py b/apps/ops/ansible/runner.py index 7931b72ee..4c8f87888 100644 --- a/apps/ops/ansible/runner.py +++ b/apps/ops/ansible/runner.py @@ -9,10 +9,10 @@ from ansible.parsing.dataloader import DataLoader from ansible.executor.playbook_executor import PlaybookExecutor from ansible.playbook.play import Play import ansible.constants as C -from ansible.utils.display import Display -from .callback import AdHocResultCallback, PlaybookResultCallBack, \ - CommandResultCallback +from .callback import ( + AdHocResultCallback, PlaybookResultCallBack, CommandResultCallback +) from common.utils import get_logger from .exceptions import AnsibleError @@ -22,13 +22,6 @@ C.HOST_KEY_CHECKING = False logger = get_logger(__name__) -class CustomDisplay(Display): - def display(self, msg, color=None, stderr=False, screen_only=False, log_only=False): - pass - -display = CustomDisplay() - - Options = namedtuple('Options', [ 'listtags', 'listtasks', 'listhosts', 'syntax', 'connection', 'module_path', 'forks', 'remote_user', 'private_key_file', 'timeout', diff --git a/apps/ops/api/celery.py b/apps/ops/api/celery.py index 2640cffec..d2053f9cb 100644 --- a/apps/ops/api/celery.py +++ b/apps/ops/api/celery.py @@ -1,46 +1,42 @@ # -*- coding: utf-8 -*- # -import uuid + import os - from celery.result import AsyncResult -from django.core.cache import cache -from django.utils.translation import ugettext as _ from rest_framework import generics -from rest_framework.views import Response -from common.permissions import IsOrgAdmin, IsValidUser +from common.permissions import IsValidUser +from common.api import LogTailApi from ..models import CeleryTask from ..serializers import CeleryResultSerializer +from ..celery.utils import get_celery_task_log_path __all__ = ['CeleryTaskLogApi', 'CeleryResultApi'] -class CeleryTaskLogApi(generics.RetrieveAPIView): +class CeleryTaskLogApi(LogTailApi): permission_classes = (IsValidUser,) - buff_size = 1024 * 10 - end = False - queryset = CeleryTask.objects.all() + task = None + task_id = '' def get(self, request, *args, **kwargs): - mark = request.query_params.get("mark") or str(uuid.uuid4()) - task = self.get_object() - log_path = task.full_log_path + self.task_id = str(kwargs.get('pk')) + self.task = AsyncResult(self.task_id) + return super().get(request, *args, **kwargs) - if not log_path or not os.path.isfile(log_path): - return Response({"data": _("Waiting ...")}, status=203) + def get_log_path(self): + new_path = get_celery_task_log_path(self.task_id) + if new_path and os.path.isfile(new_path): + return new_path + try: + task = CeleryTask.objects.get(id=self.task_id) + except CeleryTask.DoesNotExist: + return None + return task.full_log_path - with open(log_path, 'r') as f: - offset = cache.get(mark, 0) - f.seek(offset) - data = f.read(self.buff_size).replace('\n', '\r\n') - mark = str(uuid.uuid4()) - cache.set(mark, f.tell(), 5) - - if data == '' and task.is_finished(): - self.end = True - return Response({"data": data, 'end': self.end, 'mark': mark}) + def is_file_finish_write(self): + return self.task.ready() class CeleryResultApi(generics.RetrieveAPIView): diff --git a/apps/ops/apps.py b/apps/ops/apps.py index 8a70780f0..01dfd05fa 100644 --- a/apps/ops/apps.py +++ b/apps/ops/apps.py @@ -10,6 +10,5 @@ class OpsConfig(AppConfig): from orgs.models import Organization from orgs.utils import set_current_org set_current_org(Organization.root()) - - super().ready() from .celery import signal_handler + super().ready() diff --git a/apps/ops/celery/decorator.py b/apps/ops/celery/decorator.py new file mode 100644 index 000000000..c2052f832 --- /dev/null +++ b/apps/ops/celery/decorator.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +# +from functools import wraps + +_need_registered_period_tasks = [] +_after_app_ready_start_tasks = [] +_after_app_shutdown_clean_periodic_tasks = [] + + +def add_register_period_task(task): + _need_registered_period_tasks.append(task) + # key = "__REGISTER_PERIODIC_TASKS" + # value = cache.get(key, []) + # value.append(name) + # cache.set(key, value) + + +def get_register_period_tasks(): + # key = "__REGISTER_PERIODIC_TASKS" + # return cache.get(key, []) + return _need_registered_period_tasks + + +def add_after_app_shutdown_clean_task(name): + # key = "__AFTER_APP_SHUTDOWN_CLEAN_TASKS" + # value = cache.get(key, []) + # value.append(name) + # cache.set(key, value) + _after_app_shutdown_clean_periodic_tasks.append(name) + + +def get_after_app_shutdown_clean_tasks(): + # key = "__AFTER_APP_SHUTDOWN_CLEAN_TASKS" + # return cache.get(key, []) + return _after_app_shutdown_clean_periodic_tasks + + +def add_after_app_ready_task(name): + # key = "__AFTER_APP_READY_RUN_TASKS" + # value = cache.get(key, []) + # value.append(name) + # cache.set(key, value) + _after_app_ready_start_tasks.append(name) + + +def get_after_app_ready_tasks(): + # key = "__AFTER_APP_READY_RUN_TASKS" + # return cache.get(key, []) + return _after_app_ready_start_tasks + + +def register_as_period_task(crontab=None, interval=None): + """ + Warning: Task must be have not any args and kwargs + :param crontab: "* * * * *" + :param interval: 60*60*60 + :return: + """ + if crontab is None and interval is None: + raise SyntaxError("Must set crontab or interval one") + + def decorate(func): + if crontab is None and interval is None: + raise SyntaxError("Interval and crontab must set one") + + # Because when this decorator run, the task was not created, + # So we can't use func.name + name = '{func.__module__}.{func.__name__}'.format(func=func) + add_register_period_task({ + name: { + 'task': name, + 'interval': interval, + 'crontab': crontab, + 'args': (), + 'enabled': True, + } + }) + + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + return wrapper + return decorate + + +def after_app_ready_start(func): + # Because when this decorator run, the task was not created, + # So we can't use func.name + name = '{func.__module__}.{func.__name__}'.format(func=func) + if name not in _after_app_ready_start_tasks: + add_after_app_ready_task(name) + + @wraps(func) + def decorate(*args, **kwargs): + return func(*args, **kwargs) + return decorate + + +def after_app_shutdown_clean_periodic(func): + # Because when this decorator run, the task was not created, + # So we can't use func.name + name = '{func.__module__}.{func.__name__}'.format(func=func) + if name not in _after_app_shutdown_clean_periodic_tasks: + add_after_app_shutdown_clean_task(name) + + @wraps(func) + def decorate(*args, **kwargs): + return func(*args, **kwargs) + return decorate diff --git a/apps/ops/celery/logger.py b/apps/ops/celery/logger.py new file mode 100644 index 000000000..bfe713d6d --- /dev/null +++ b/apps/ops/celery/logger.py @@ -0,0 +1,160 @@ +from logging import StreamHandler + +from django.conf import settings +from celery import current_task +from celery.signals import task_prerun, task_postrun +from kombu import Connection, Exchange, Queue, Producer +from kombu.mixins import ConsumerMixin + +from .utils import get_celery_task_log_path + +routing_key = 'celery_log' +celery_log_exchange = Exchange('celery_log_exchange', type='direct') +celery_log_queue = [Queue('celery_log', celery_log_exchange, routing_key=routing_key)] + + +class CeleryLoggerConsumer(ConsumerMixin): + def __init__(self): + self.connection = Connection(settings.CELERY_LOG_BROKER_URL) + + def get_consumers(self, Consumer, channel): + return [Consumer(queues=celery_log_queue, + accept=['pickle', 'json'], + callbacks=[self.process_task]) + ] + + def handle_task_start(self, task_id, message): + pass + + def handle_task_end(self, task_id, message): + pass + + def handle_task_log(self, task_id, msg, message): + pass + + def process_task(self, body, message): + action = body.get('action') + task_id = body.get('task_id') + msg = body.get('msg') + if action == CeleryLoggerProducer.ACTION_TASK_LOG: + self.handle_task_log(task_id, msg, message) + elif action == CeleryLoggerProducer.ACTION_TASK_START: + self.handle_task_start(task_id, message) + elif action == CeleryLoggerProducer.ACTION_TASK_END: + self.handle_task_end(task_id, message) + + +class CeleryLoggerProducer: + ACTION_TASK_START, ACTION_TASK_LOG, ACTION_TASK_END = range(3) + + def __init__(self): + self.connection = Connection(settings.CELERY_LOG_BROKER_URL) + + @property + def producer(self): + return Producer(self.connection) + + def publish(self, payload): + self.producer.publish( + payload, serializer='json', exchange=celery_log_exchange, + declare=[celery_log_exchange], routing_key=routing_key + ) + + def log(self, task_id, msg): + payload = {'task_id': task_id, 'msg': msg, 'action': self.ACTION_TASK_LOG} + return self.publish(payload) + + def read(self): + pass + + def flush(self): + pass + + def task_end(self, task_id): + payload = {'task_id': task_id, 'action': self.ACTION_TASK_END} + return self.publish(payload) + + def task_start(self, task_id): + payload = {'task_id': task_id, 'action': self.ACTION_TASK_START} + return self.publish(payload) + + +class CeleryTaskLoggerHandler(StreamHandler): + terminator = '\r\n' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + task_prerun.connect(self.on_task_start) + task_postrun.connect(self.on_start_end) + + @staticmethod + def get_current_task_id(): + if not current_task: + return + task_id = current_task.request.root_id + return task_id + + def on_task_start(self, sender, task_id, **kwargs): + return self.handle_task_start(task_id) + + def on_start_end(self, sender, task_id, **kwargs): + return self.handle_task_end(task_id) + + def after_task_publish(self, sender, body, **kwargs): + pass + + def emit(self, record): + task_id = self.get_current_task_id() + if not task_id: + return + try: + self.write_task_log(task_id, record) + self.flush() + except Exception: + self.handleError(record) + + def write_task_log(self, task_id, msg): + pass + + def handle_task_start(self, task_id): + pass + + def handle_task_end(self, task_id): + pass + + +class CeleryTaskMQLoggerHandler(CeleryTaskLoggerHandler): + def __init__(self): + self.producer = CeleryLoggerProducer() + super().__init__(stream=None) + + def write_task_log(self, task_id, record): + msg = self.format(record) + self.producer.log(task_id, msg) + + def flush(self): + self.producer.flush() + + +class CeleryTaskFileHandler(CeleryTaskLoggerHandler): + def __init__(self): + self.f = None + super().__init__(stream=None) + + def emit(self, record): + msg = self.format(record) + if not self.f: + return + self.f.write(msg) + self.f.write(self.terminator) + self.flush() + + def flush(self): + self.f and self.f.flush() + + def handle_task_start(self, task_id): + log_path = get_celery_task_log_path(task_id) + self.f = open(log_path, 'a') + + def handle_task_end(self, task_id): + self.f and self.f.close() diff --git a/apps/ops/celery/signal_handler.py b/apps/ops/celery/signal_handler.py index 961c18ad3..b2a6ddf37 100644 --- a/apps/ops/celery/signal_handler.py +++ b/apps/ops/celery/signal_handler.py @@ -1,103 +1,105 @@ # -*- coding: utf-8 -*- # -import os -import datetime -import sys -import time +import logging -from django.conf import settings -from django.utils import timezone from django.core.cache import cache -from django.db import transaction from celery import subtask -from celery.signals import worker_ready, worker_shutdown, task_prerun, \ - task_postrun, after_task_publish +from celery.signals import ( + worker_ready, worker_shutdown, after_setup_logger +) from django_celery_beat.models import PeriodicTask -from common.utils import get_logger, TeeObj, get_object_or_none -from common.const import celery_task_pre_key -from .utils import get_after_app_ready_tasks, get_after_app_shutdown_clean_tasks -from ..models import CeleryTask +from common.utils import get_logger +from .decorator import get_after_app_ready_tasks, get_after_app_shutdown_clean_tasks +from .logger import CeleryTaskFileHandler logger = get_logger(__file__) @worker_ready.connect -def on_app_ready(sender=None, headers=None, body=None, **kwargs): +def on_app_ready(sender=None, headers=None, **kwargs): if cache.get("CELERY_APP_READY", 0) == 1: return cache.set("CELERY_APP_READY", 1, 10) tasks = get_after_app_ready_tasks() - logger.debug("Start need start task: [{}]".format( - ", ".join(tasks)) - ) + logger.debug("Work ready signal recv") + logger.debug("Start need start task: [{}]".format(", ".join(tasks))) for task in tasks: subtask(task).delay() @worker_shutdown.connect -def after_app_shutdown(sender=None, headers=None, body=None, **kwargs): +def after_app_shutdown_periodic_tasks(sender=None, **kwargs): if cache.get("CELERY_APP_SHUTDOWN", 0) == 1: return cache.set("CELERY_APP_SHUTDOWN", 1, 10) tasks = get_after_app_shutdown_clean_tasks() - logger.debug("App shutdown signal recv") - logger.debug("Clean need cleaned period tasks: [{}]".format( - ', '.join(tasks)) - ) + logger.debug("Worker shutdown signal recv") + logger.debug("Clean period tasks: [{}]".format(', '.join(tasks))) PeriodicTask.objects.filter(name__in=tasks).delete() -@after_task_publish.connect -def after_task_publish_signal_handler(sender, headers=None, **kwargs): - CeleryTask.objects.create( - id=headers["id"], status=CeleryTask.WAITING, name=headers["task"] - ) - cache.set(headers["id"], True, 3600) - - -@task_prerun.connect -def pre_run_task_signal_handler(sender, task_id=None, task=None, **kwargs): - time.sleep(0.1) - for i in range(5): - if cache.get(task_id, False): - break - else: - time.sleep(0.1) - continue - - t = get_object_or_none(CeleryTask, id=task_id) - if t is None: - logger.warn("Not get the task: {}".format(task_id)) +@after_setup_logger.connect +def add_celery_logger_handler(sender=None, logger=None, loglevel=None, format=None, **kwargs): + if not logger: return - now = datetime.datetime.now().strftime("%Y-%m-%d") - log_path = os.path.join(now, task_id + '.log') - full_path = os.path.join(CeleryTask.LOG_DIR, log_path) - - if not os.path.exists(os.path.dirname(full_path)): - os.makedirs(os.path.dirname(full_path)) - with transaction.atomic(): - t.date_start = timezone.now() - t.status = CeleryTask.RUNNING - t.log_path = log_path - t.save() - f = open(full_path, 'w') - tee = TeeObj(f) - sys.stdout = tee - task.log_f = tee + handler = CeleryTaskFileHandler() + handler.setLevel(loglevel) + formatter = logging.Formatter(format) + handler.setFormatter(formatter) + logger.addHandler(handler) -@task_postrun.connect -def post_run_task_signal_handler(sender, task_id=None, task=None, **kwargs): - t = get_object_or_none(CeleryTask, id=task_id) - if t is None: - logger.warn("Not get the task: {}".format(task_id)) - return - with transaction.atomic(): - t.status = CeleryTask.FINISHED - t.date_finished = timezone.now() - t.save() - task.log_f.flush() - sys.stdout = task.log_f.origin_stdout - task.log_f.close() +# @after_task_publish.connect +# def after_task_publish_signal_handler(sender, headers=None, **kwargs): +# CeleryTask.objects.create( +# id=headers["id"], status=CeleryTask.WAITING, name=headers["task"] +# ) +# cache.set(headers["id"], True, 3600) +# +# +# @task_prerun.connect +# def pre_run_task_signal_handler(sender, task_id=None, task=None, **kwargs): +# time.sleep(0.1) +# for i in range(5): +# if cache.get(task_id, False): +# break +# else: +# time.sleep(0.1) +# continue +# +# t = get_object_or_none(CeleryTask, id=task_id) +# if t is None: +# logger.warn("Not get the task: {}".format(task_id)) +# return +# now = datetime.datetime.now().strftime("%Y-%m-%d") +# log_path = os.path.join(now, task_id + '.log') +# full_path = os.path.join(CeleryTask.LOG_DIR, log_path) +# +# if not os.path.exists(os.path.dirname(full_path)): +# os.makedirs(os.path.dirname(full_path)) +# with transaction.atomic(): +# t.date_start = timezone.now() +# t.status = CeleryTask.RUNNING +# t.log_path = log_path +# t.save() +# f = open(full_path, 'w', encoding="utf-8") +# tee = TeeObj(f) +# sys.stdout = tee +# task.log_f = tee +# +# +# @task_postrun.connect +# def post_run_task_signal_handler(sender, task_id=None, task=None, **kwargs): +# t = get_object_or_none(CeleryTask, id=task_id) +# if t is None: +# logger.warn("Not get the task: {}".format(task_id)) +# return +# with transaction.atomic(): +# t.status = CeleryTask.FINISHED +# t.date_finished = timezone.now() +# t.save() +# task.log_f.flush() +# sys.stdout = task.log_f.origin_stdout +# task.log_f.close() diff --git a/apps/ops/celery/utils.py b/apps/ops/celery/utils.py index b4f5a80db..1fc2fc103 100644 --- a/apps/ops/celery/utils.py +++ b/apps/ops/celery/utils.py @@ -1,49 +1,13 @@ # -*- coding: utf-8 -*- # import json -from functools import wraps +import os +from django.conf import settings from django.db.utils import ProgrammingError, OperationalError -from django.core.cache import cache from django_celery_beat.models import PeriodicTask, IntervalSchedule, CrontabSchedule -def add_register_period_task(name): - key = "__REGISTER_PERIODIC_TASKS" - value = cache.get(key, []) - value.append(name) - cache.set(key, value) - - -def get_register_period_tasks(): - key = "__REGISTER_PERIODIC_TASKS" - return cache.get(key, []) - - -def add_after_app_shutdown_clean_task(name): - key = "__AFTER_APP_SHUTDOWN_CLEAN_TASKS" - value = cache.get(key, []) - value.append(name) - cache.set(key, value) - - -def get_after_app_shutdown_clean_tasks(): - key = "__AFTER_APP_SHUTDOWN_CLEAN_TASKS" - return cache.get(key, []) - - -def add_after_app_ready_task(name): - key = "__AFTER_APP_READY_RUN_TASKS" - value = cache.get(key, []) - value.append(name) - cache.set(key, value) - - -def get_after_app_ready_tasks(): - key = "__AFTER_APP_READY_RUN_TASKS" - return cache.get(key, []) - - def create_or_update_celery_periodic_tasks(tasks): """ :param tasks: { @@ -123,63 +87,10 @@ def delete_celery_periodic_task(task_name): PeriodicTask.objects.filter(name=task_name).delete() -def register_as_period_task(crontab=None, interval=None): - """ - Warning: Task must be have not any args and kwargs - :param crontab: "* * * * *" - :param interval: 60*60*60 - :return: - """ - if crontab is None and interval is None: - raise SyntaxError("Must set crontab or interval one") +def get_celery_task_log_path(task_id): + task_id = str(task_id) + rel_path = os.path.join(task_id[0], task_id[1], task_id + '.log') + path = os.path.join(settings.CELERY_LOG_DIR, rel_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + return path - def decorate(func): - if crontab is None and interval is None: - raise SyntaxError("Interval and crontab must set one") - - # Because when this decorator run, the task was not created, - # So we can't use func.name - name = '{func.__module__}.{func.__name__}'.format(func=func) - if name not in get_register_period_tasks(): - create_or_update_celery_periodic_tasks({ - name: { - 'task': name, - 'interval': interval, - 'crontab': crontab, - 'args': (), - 'enabled': True, - } - }) - add_register_period_task(name) - - @wraps(func) - def wrapper(*args, **kwargs): - return func(*args, **kwargs) - return wrapper - return decorate - - -def after_app_ready_start(func): - # Because when this decorator run, the task was not created, - # So we can't use func.name - name = '{func.__module__}.{func.__name__}'.format(func=func) - if name not in get_after_app_ready_tasks(): - add_after_app_ready_task(name) - - @wraps(func) - def decorate(*args, **kwargs): - return func(*args, **kwargs) - return decorate - - -def after_app_shutdown_clean(func): - # Because when this decorator run, the task was not created, - # So we can't use func.name - name = '{func.__module__}.{func.__name__}'.format(func=func) - if name not in get_after_app_shutdown_clean_tasks(): - add_after_app_shutdown_clean_task(name) - - @wraps(func) - def decorate(*args, **kwargs): - return func(*args, **kwargs) - return decorate diff --git a/apps/ops/models/command.py b/apps/ops/models/command.py index d2d132094..623dd39bb 100644 --- a/apps/ops/models/command.py +++ b/apps/ops/models/command.py @@ -8,6 +8,8 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext from django.db import models + +from orgs.models import Organization from ..ansible.runner import CommandRunner from ..inventory import JMSInventory @@ -53,6 +55,8 @@ class CommandExecution(models.Model): def run(self): print('-'*10 + ' ' + ugettext('Task start') + ' ' + '-'*10) + org = Organization.get_instance(self.run_as.org_id) + org.change_to() self.date_start = timezone.now() ok, msg = self.run_as.is_command_can_run(self.command) if ok: diff --git a/apps/ops/tasks.py b/apps/ops/tasks.py index ac0bdce89..f50f17946 100644 --- a/apps/ops/tasks.py +++ b/apps/ops/tasks.py @@ -1,9 +1,18 @@ # coding: utf-8 +import os +import subprocess + +from django.conf import settings from celery import shared_task, subtask +from django.utils import timezone from common.utils import get_logger, get_object_or_none -from .celery.utils import register_as_period_task, after_app_shutdown_clean -from .models import Task, CommandExecution +from .celery.decorator import ( + register_as_period_task, after_app_shutdown_clean_periodic, + after_app_ready_start +) +from .celery.utils import create_or_update_celery_periodic_tasks +from .models import Task, CommandExecution, CeleryTask logger = get_logger(__file__) @@ -36,8 +45,8 @@ def run_command_execution(cid, **kwargs): @shared_task +@after_app_shutdown_clean_periodic @register_as_period_task(interval=3600*24) -@after_app_shutdown_clean def clean_tasks_adhoc_period(): logger.debug("Start clean task adhoc and run history") tasks = Task.objects.all() @@ -48,11 +57,42 @@ def clean_tasks_adhoc_period(): ad.delete() +@shared_task +@after_app_shutdown_clean_periodic +@register_as_period_task(interval=3600*24) +def clean_celery_tasks_period(): + expire_days = 30 + logger.debug("Start clean celery task history") + one_month_ago = timezone.now() - timezone.timedelta(days=expire_days) + tasks = CeleryTask.objects.filter(date_start__lt=one_month_ago) + for task in tasks: + if os.path.isfile(task.full_log_path): + try: + os.remove(task.full_log_path) + except (FileNotFoundError, PermissionError): + pass + task.delete() + tasks = CeleryTask.objects.filter(date_start__isnull=True) + tasks.delete() + command = "find %s -mtime +%s -name '*.log' -type f -exec rm -f {} \\;" % ( + settings.CELERY_LOG_DIR, expire_days + ) + subprocess.call(command, shell=True) + + +@shared_task +@after_app_ready_start +def create_or_update_registered_periodic_tasks(): + from .celery.decorator import get_register_period_tasks + for task in get_register_period_tasks(): + create_or_update_celery_periodic_tasks(task) + + @shared_task def hello(name, callback=None): + import time + time.sleep(10) print("Hello {}".format(name)) - if callback is not None: - subtask(callback).delay("Guahongwei") @shared_task diff --git a/apps/ops/templates/ops/celery_task_log.html b/apps/ops/templates/ops/celery_task_log.html index 66b3177c7..a182789fa 100644 --- a/apps/ops/templates/ops/celery_task_log.html +++ b/apps/ops/templates/ops/celery_task_log.html @@ -1,6 +1,7 @@ {% load static %} +{% load i18n %} - term.js + {% trans 'Task log' %} @@ -15,14 +16,14 @@ } -
-
+
+
diff --git a/apps/static/js/jumpserver.js b/apps/static/js/jumpserver.js index e46b32d13..5e602dd4f 100644 --- a/apps/static/js/jumpserver.js +++ b/apps/static/js/jumpserver.js @@ -161,6 +161,87 @@ function activeNav() { } } +function formSubmit(props) { + /* + { + "form": $("form"), + "url": "", + "method": "POST", + "redirect_to": "", + "success": function(data, textStatue, jqXHR){}, + "error": function(jqXHR, textStatus, errorThrown) {} + } + */ + props = props || {}; + var data = props.data || props.form.serializeObject(); + var redirect_to = props.redirect_to; + $.ajax({ + url: props.url, + type: props.method || 'POST', + data: JSON.stringify(data), + contentType: props.content_type || "application/json; charset=utf-8", + dataType: props.data_type || "json" + }).done(function (data, textState, jqXHR) { + if (redirect_to) { + location.href = redirect_to; + } else if (typeof props.success === 'function') { + return props.success(data, textState, jqXHR); + } + }).fail(function(jqXHR, textStatus, errorThrown) { + if (typeof props.error === 'function') { + return props.error(jqXHR, textStatus, errorThrown) + } + if (!props.form) { + alert(jqXHR.responseText); + return + } + if (jqXHR.status === 400) { + var errors = jqXHR.responseJSON; + var noneFieldErrorRef = props.form.children('.alert-danger'); + if (noneFieldErrorRef.length !== 1) { + props.form.prepend(''); + noneFieldErrorRef = props.form.children('.alert-danger'); + } + var noneFieldErrorMsg = ""; + noneFieldErrorRef.css("display", "none"); + noneFieldErrorRef.html(""); + props.form.find(".help-block.error").html(""); + props.form.find(".form-group.has-error").removeClass("has-error"); + + if (typeof errors !== "object") { + noneFieldErrorMsg = errors; + if (noneFieldErrorRef.length === 1) { + noneFieldErrorRef.css('display', 'block'); + noneFieldErrorRef.html(noneFieldErrorMsg); + } + return + } + $.each(errors, function (k, v) { + var fieldRef = props.form.find('input[name="' + k + '"]'); + var formGroupRef = fieldRef.parents('.form-group'); + var parentRef = fieldRef.parent(); + var helpBlockRef = parentRef.children('.help-block.error'); + if (helpBlockRef.length === 0) { + parentRef.append('
'); + helpBlockRef = parentRef.children('.help-block.error'); + } + if (fieldRef.length === 1 && formGroupRef.length === 1) { + formGroupRef.addClass('has-error'); + var help_msg = v.join("
") ; + helpBlockRef.html(help_msg); + } else { + noneFieldErrorMsg += v + '
'; + } + }); + if (noneFieldErrorRef.length === 1 && noneFieldErrorMsg !== '') { + noneFieldErrorRef.css('display', 'block'); + noneFieldErrorRef.html(noneFieldErrorMsg); + } + } + + }) +} + function APIUpdateAttr(props) { // props = {url: .., body: , success: , error: , method: ,} props = props || {}; @@ -195,9 +276,6 @@ function APIUpdateAttr(props) { }).fail(function(jqXHR, textStatus, errorThrown) { if (flash_message) { var msg = ""; - console.log(jqXHR); - console.log(textStatus); - console.log(errorThrown); if (user_fail_message) { msg = user_fail_message; } else if (jqXHR.responseJSON) { @@ -213,6 +291,7 @@ function APIUpdateAttr(props) { toastr.error(msg); } if (typeof props.error === 'function') { + console.log(jqXHR); return props.error(jqXHR.responseText, jqXHR.status); } }); @@ -478,7 +557,7 @@ jumpserver.initServerSideDataTable = function (options) { url: options.ajax_url , data: function (data) { delete data.columns; - if (data.length !== null ){ + if (data.length !== null){ data.limit = data.length; delete data.length; } @@ -525,7 +604,7 @@ jumpserver.initServerSideDataTable = function (options) { columns: options.columns || [], select: options.select || select, language: jumpserver.language, - lengthMenu: [[10, 15, 25, 50], [10, 15, 25, 50]] + lengthMenu: [[15, 25, 50, 9999], [15, 25, 50, 'All']] }); table.selected = []; table.selected_rows = []; diff --git a/apps/templates/_copyright.html b/apps/templates/_copyright.html index b98dacb03..327e67bd3 100644 --- a/apps/templates/_copyright.html +++ b/apps/templates/_copyright.html @@ -1,2 +1,2 @@ {% load i18n %} -Copyright {% trans ' Beijing Duizhan Tech, Inc. ' %} © 2014-2018 \ No newline at end of file +Copyright {% trans ' Beijing Duizhan Tech, Inc. ' %} © 2014-2019 \ No newline at end of file diff --git a/apps/templates/_footer.html b/apps/templates/_footer.html index 449ba9e8c..9cf09b532 100644 --- a/apps/templates/_footer.html +++ b/apps/templates/_footer.html @@ -5,6 +5,6 @@
- Copyright {% trans ' Beijing Duizhan Tech, Inc. ' %}© 2014-2018 + Copyright {% trans ' Beijing Duizhan Tech, Inc. ' %}© 2014-2019
diff --git a/apps/templates/flash_message_standalone.html b/apps/templates/flash_message_standalone.html index 13794eea7..91989afe7 100644 --- a/apps/templates/flash_message_standalone.html +++ b/apps/templates/flash_message_standalone.html @@ -54,7 +54,7 @@ {% include '_copyright.html' %}
- 2014-2018 + 2014-2019
diff --git a/apps/terminal/api/v1/session.py b/apps/terminal/api/v1/session.py index ac14699a3..e6ead0915 100644 --- a/apps/terminal/api/v1/session.py +++ b/apps/terminal/api/v1/session.py @@ -33,16 +33,19 @@ class SessionViewSet(BulkModelViewSet): permission_classes = (IsOrgAdminOrAppUser,) def get_queryset(self): + queryset = super().get_queryset() terminal_id = self.kwargs.get("terminal", None) if terminal_id: terminal = get_object_or_404(Terminal, id=terminal_id) - self.queryset = terminal.session_set.all() - return self.queryset.all() + queryset = queryset.filter(terminal=terminal) + return queryset + return queryset def perform_create(self, serializer): if hasattr(self.request.user, 'terminal'): serializer.validated_data["terminal"] = self.request.user.terminal sid = serializer.validated_data["system_user"] + # guacamole提交的是id if is_uuid(sid): _system_user = SystemUser.get_system_user_by_id_or_cached(sid) if _system_user: diff --git a/apps/terminal/api/v1/terminal.py b/apps/terminal/api/v1/terminal.py index ac56b3ea9..b83c2d844 100644 --- a/apps/terminal/api/v1/terminal.py +++ b/apps/terminal/api/v1/terminal.py @@ -100,52 +100,15 @@ class StatusViewSet(viewsets.ModelViewSet): task_serializer_class = serializers.TaskSerializer def create(self, request, *args, **kwargs): - from_gua = self.request.query_params.get("from_guacamole", None) - if not from_gua: - self.handle_sessions() super().create(request, *args, **kwargs) + self.handle_sessions() tasks = self.request.user.terminal.task_set.filter(is_finished=False) serializer = self.task_serializer_class(tasks, many=True) return Response(serializer.data, status=201) def handle_sessions(self): - sessions_active = [] - for session_data in self.request.data.get("sessions", []): - self.create_or_update_session(session_data) - if not session_data["is_finished"]: - sessions_active.append(session_data["id"]) - - sessions_in_db_active = Session.objects.filter( - is_finished=False, - terminal=self.request.user.terminal.id - ) - - for session in sessions_in_db_active: - if str(session.id) not in sessions_active: - session.is_finished = True - session.date_end = timezone.now() - session.save() - - def create_or_update_session(self, session_data): - session_data["terminal"] = self.request.user.terminal.id - _id = session_data["id"] - session = get_object_or_none(Session, id=_id) - if session: - serializer = serializers.SessionSerializer( - data=session_data, instance=session - ) - else: - serializer = serializers.SessionSerializer(data=session_data) - - if serializer.is_valid(): - session = serializer.save() - return session - else: - msg = "session data is not valid {}: {}".format( - serializer.errors, str(serializer.data) - ) - logger.error(msg) - return None + sessions_id = self.request.data.get('sessions', []) + Session.set_active_sessions(sessions_id) def get_queryset(self): terminal_id = self.kwargs.get("terminal", None) diff --git a/apps/terminal/api/v2/terminal.py b/apps/terminal/api/v2/terminal.py index 3fd42a6ab..11157a0df 100644 --- a/apps/terminal/api/v2/terminal.py +++ b/apps/terminal/api/v2/terminal.py @@ -1,12 +1,16 @@ # -*- coding: utf-8 -*- # -from rest_framework import viewsets +from rest_framework import viewsets, generics +from rest_framework import status +from rest_framework.response import Response from common.permissions import IsSuperUser, WithBootstrapToken + + from ...models import Terminal from ...serializers import v2 as serializers -__all__ = ['TerminalViewSet', 'TerminalRegistrationViewSet'] +__all__ = ['TerminalViewSet', 'TerminalRegistrationApi'] class TerminalViewSet(viewsets.ModelViewSet): @@ -15,8 +19,19 @@ class TerminalViewSet(viewsets.ModelViewSet): permission_classes = [IsSuperUser] -class TerminalRegistrationViewSet(viewsets.ModelViewSet): - queryset = Terminal.objects.filter(is_deleted=False) +class TerminalRegistrationApi(generics.CreateAPIView): serializer_class = serializers.TerminalRegistrationSerializer permission_classes = [WithBootstrapToken] http_method_names = ['post'] + + def create(self, request, *args, **kwargs): + data = {k: v for k, v in request.data.items()} + serializer = serializers.TerminalSerializer( + data=data, context={'request': request} + ) + serializer.is_valid(raise_exception=True) + terminal = serializer.save() + sa_serializer = serializer.sa_serializer_class(instance=terminal.user) + data['service_account'] = sa_serializer.data + return Response(data, status=status.HTTP_201_CREATED) + diff --git a/apps/terminal/backends/command/db.py b/apps/terminal/backends/command/db.py index 29d322c1a..a2d3f7e32 100644 --- a/apps/terminal/backends/command/db.py +++ b/apps/terminal/backends/command/db.py @@ -1,7 +1,9 @@ # ~*~ coding: utf-8 ~*~ import datetime +from django.db import transaction from django.utils import timezone +from django.db.utils import OperationalError from .base import CommandBase @@ -35,7 +37,25 @@ class CommandStore(CommandBase): input=c["input"], output=c["output"], session=c["session"], org_id=c["org_id"], timestamp=c["timestamp"] )) - return self.model.objects.bulk_create(_commands) + error = False + try: + with transaction.atomic(): + self.model.objects.bulk_create(_commands) + except OperationalError: + error = True + except: + return False + + if not error: + return True + for command in _commands: + try: + with transaction.atomic(): + command.save() + except OperationalError: + command.output = str(command.output.encode()) + command.save() + return True @staticmethod def make_filter_kwargs( diff --git a/apps/terminal/forms.py b/apps/terminal/forms.py index 207363ab0..b2f81fbb5 100644 --- a/apps/terminal/forms.py +++ b/apps/terminal/forms.py @@ -39,5 +39,3 @@ class TerminalForm(forms.ModelForm): 'name', 'remote_addr', 'comment', 'command_storage', 'replay_storage', ] - help_texts = { - } diff --git a/apps/terminal/models.py b/apps/terminal/models.py index 880637efe..3107882d3 100644 --- a/apps/terminal/models.py +++ b/apps/terminal/models.py @@ -8,6 +8,7 @@ from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from django.conf import settings from django.core.files.storage import default_storage +from django.core.cache import cache from users.models import User from orgs.mixins import OrgModelMixin @@ -61,8 +62,9 @@ class Terminal(models.Model): def config(self): configs = {} for k in dir(settings): - if k.startswith('TERMINAL'): - configs[k] = getattr(settings, k) + if not k.startswith('TERMINAL'): + continue + configs[k] = getattr(settings, k) configs.update(self.get_common_storage()) configs.update(self.get_replay_storage()) configs.update({ @@ -152,6 +154,7 @@ class Session(OrgModelMixin): date_end = models.DateTimeField(verbose_name=_("Date end"), null=True) upload_to = 'replay' + ACTIVE_CACHE_KEY_PREFIX = 'SESSION_ACTIVE_{}' def get_rel_replay_path(self, version=2): """ @@ -181,6 +184,17 @@ class Session(OrgModelMixin): except OSError as e: return None, e + @classmethod + def set_active_sessions(cls, sessions_id): + data = {cls.ACTIVE_CACHE_KEY_PREFIX.format(i): i for i in sessions_id} + cache.set_many(data, timeout=5*60) + + def is_active(self): + if self.protocol in ['ssh', 'telnet']: + key = self.ACTIVE_CACHE_KEY_PREFIX.format(self.id) + return bool(cache.get(key)) + return True + class Meta: db_table = "terminal_session" ordering = ["-date_start"] diff --git a/apps/terminal/serializers/v1.py b/apps/terminal/serializers/v1.py index 25cd0cd1e..688453ac1 100644 --- a/apps/terminal/serializers/v1.py +++ b/apps/terminal/serializers/v1.py @@ -31,8 +31,6 @@ class TerminalSerializer(serializers.ModelSerializer): return cache.get(key) - - class SessionSerializer(BulkSerializerMixin, serializers.ModelSerializer): command_amount = serializers.SerializerMethodField() command_store = get_multi_command_storage() @@ -69,6 +67,6 @@ class TaskSerializer(BulkSerializerMixin, serializers.ModelSerializer): class ReplaySerializer(serializers.Serializer): - file = serializers.FileField() + file = serializers.FileField(allow_empty_file=True) diff --git a/apps/terminal/serializers/v2.py b/apps/terminal/serializers/v2.py index 7e0365800..242c07268 100644 --- a/apps/terminal/serializers/v2.py +++ b/apps/terminal/serializers/v2.py @@ -3,7 +3,7 @@ from rest_framework import serializers from common.utils import get_request_ip -from users.serializers.v2 import ServiceAccountRegistrationSerializer +from users.serializers.v2 import ServiceAccountSerializer from ..models import Terminal @@ -11,36 +11,48 @@ __all__ = ['TerminalSerializer', 'TerminalRegistrationSerializer'] class TerminalSerializer(serializers.ModelSerializer): - class Meta: - model = Terminal - fields = [ - 'id', 'name', 'remote_addr', 'comment', - ] - read_only_fields = ['id', 'remote_addr'] - - -class TerminalRegistrationSerializer(serializers.ModelSerializer): - service_account = ServiceAccountRegistrationSerializer(read_only=True) - service_account_serializer = None + sa_serializer_class = ServiceAccountSerializer + sa_serializer = None class Meta: model = Terminal fields = [ - 'id', 'name', 'remote_addr', 'comment', 'service_account' + 'id', 'name', 'remote_addr', 'command_storage', + 'replay_storage', 'user', 'is_accepted', 'is_deleted', + 'date_created', 'comment' ] - read_only_fields = ['id', 'remote_addr', 'service_account'] + read_only_fields = ['id', 'remote_addr', 'user', 'date_created'] - def validate(self, attrs): - self.service_account_serializer = ServiceAccountRegistrationSerializer(data=attrs) - self.service_account_serializer.is_valid(raise_exception=True) - return attrs + def is_valid(self, raise_exception=False): + valid = super().is_valid(raise_exception=raise_exception) + if not valid: + return valid + data = {'name': self.validated_data.get('name')} + kwargs = {'data': data} + if self.instance and self.instance.user: + kwargs['instance'] = self.instance.user + self.sa_serializer = ServiceAccountSerializer(**kwargs) + valid = self.sa_serializer.is_valid(raise_exception=True) + return valid + + def save(self, **kwargs): + instance = super().save(**kwargs) + sa = self.sa_serializer.save() + instance.user = sa + instance.save() + return instance def create(self, validated_data): request = self.context.get('request') - sa = self.service_account_serializer.save() instance = super().create(validated_data) instance.is_accepted = True - instance.user = sa - instance.remote_addr = get_request_ip(request) + if request: + instance.remote_addr = get_request_ip(request) instance.save() return instance + + +class TerminalRegistrationSerializer(serializers.Serializer): + name = serializers.CharField(max_length=128) + comment = serializers.CharField(max_length=128) + service_account = ServiceAccountSerializer(read_only=True) diff --git a/apps/terminal/tasks.py b/apps/terminal/tasks.py index 77aa66226..4603a5240 100644 --- a/apps/terminal/tasks.py +++ b/apps/terminal/tasks.py @@ -10,8 +10,9 @@ from django.conf import settings from django.core.files.storage import default_storage -from ops.celery.utils import register_as_period_task, after_app_ready_start, \ - after_app_shutdown_clean +from ops.celery.decorator import ( + register_as_period_task, after_app_ready_start, after_app_shutdown_clean_periodic +) from .models import Status, Session, Command @@ -23,28 +24,30 @@ logger = get_task_logger(__name__) @shared_task @register_as_period_task(interval=3600) @after_app_ready_start -@after_app_shutdown_clean +@after_app_shutdown_clean_periodic def delete_terminal_status_period(): - yesterday = timezone.now() - datetime.timedelta(days=3) + yesterday = timezone.now() - datetime.timedelta(days=1) Status.objects.filter(date_created__lt=yesterday).delete() @shared_task -@register_as_period_task(interval=3600) +@register_as_period_task(interval=600) @after_app_ready_start -@after_app_shutdown_clean +@after_app_shutdown_clean_periodic def clean_orphan_session(): active_sessions = Session.objects.filter(is_finished=False) for session in active_sessions: - if not session.terminal or not session.terminal.is_active: - session.is_finished = True - session.save() + if not session.is_active(): + continue + session.is_finished = True + session.date_end = timezone.now() + session.save() @shared_task @register_as_period_task(interval=3600*24) @after_app_ready_start -@after_app_shutdown_clean +@after_app_shutdown_clean_periodic def clean_expired_session_period(): logger.info("Start clean expired session record, commands and replay") days = settings.TERMINAL_SESSION_KEEP_DURATION @@ -64,3 +67,4 @@ def clean_expired_session_period(): default_storage.delete(_local_path) # 删除session记录 session.delete() + diff --git a/apps/terminal/templates/terminal/terminal_update.html b/apps/terminal/templates/terminal/terminal_update.html index bde86563d..f9554fda4 100644 --- a/apps/terminal/templates/terminal/terminal_update.html +++ b/apps/terminal/templates/terminal/terminal_update.html @@ -33,8 +33,6 @@

{% trans 'Info' %}

{% bootstrap_field form.name layout="horizontal" %} {% bootstrap_field form.remote_addr layout="horizontal" %} -{# {% bootstrap_field form.ssh_port layout="horizontal" %}#} -{# {% bootstrap_field form.http_port layout="horizontal" %}#} {% bootstrap_field form.command_storage layout="horizontal" %} {% bootstrap_field form.replay_storage layout="horizontal" %} @@ -60,14 +58,14 @@ diff --git a/apps/terminal/urls/api_urls_v2.py b/apps/terminal/urls/api_urls_v2.py index 600ad3611..15254d216 100644 --- a/apps/terminal/urls/api_urls_v2.py +++ b/apps/terminal/urls/api_urls_v2.py @@ -11,10 +11,11 @@ app_name = 'terminal' router = BulkRouter() router.register(r'terminal', api.TerminalViewSet, 'terminal') -router.register(r'terminal-registrations', api.TerminalRegistrationViewSet, 'terminal-registration') urlpatterns = [ + path('terminal-registrations/', api.TerminalRegistrationApi.as_view(), + name='terminal-registration') ] urlpatterns += router.urls diff --git a/apps/users/api/user.py b/apps/users/api/user.py index a1119e3a7..36050710e 100644 --- a/apps/users/api/user.py +++ b/apps/users/api/user.py @@ -19,6 +19,7 @@ from orgs.utils import current_org from ..serializers import UserSerializer, UserPKUpdateSerializer, \ UserUpdateGroupSerializer, ChangeUserPasswordSerializer from ..models import User +from ..signals import post_user_create logger = get_logger(__name__) @@ -37,6 +38,10 @@ class UserViewSet(IDInFilterMixin, BulkModelViewSet): permission_classes = (IsOrgAdmin,) pagination_class = LimitOffsetPagination + def perform_create(self, serializer): + user = serializer.save() + post_user_create.send(self.__class__, user=user) + def get_queryset(self): queryset = current_org.get_org_users() return queryset diff --git a/apps/users/api/v2/user.py b/apps/users/api/v2/user.py index 6c883861e..b0f121809 100644 --- a/apps/users/api/v2/user.py +++ b/apps/users/api/v2/user.py @@ -7,6 +7,6 @@ from ...serializers import v2 as serializers class ServiceAccountRegistrationViewSet(viewsets.ModelViewSet): - serializer_class = serializers.ServiceAccountRegistrationSerializer + serializer_class = serializers.ServiceAccountSerializer permission_classes = (WithBootstrapToken,) http_method_names = ['post'] diff --git a/apps/users/migrations/0018_auto_20190107_1912.py b/apps/users/migrations/0018_auto_20190107_1912.py new file mode 100644 index 000000000..244de970d --- /dev/null +++ b/apps/users/migrations/0018_auto_20190107_1912.py @@ -0,0 +1,18 @@ +# Generated by Django 2.1.4 on 2019-01-07 11:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0017_auto_20181123_1113'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='source', + field=models.CharField(choices=[('local', 'Local'), ('ldap', 'LDAP/AD'), ('openid', 'OpenID'), ('radius', 'Radius')], default='local', max_length=30, verbose_name='Source'), + ), + ] diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 851ea2ccc..5daa7b510 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -41,10 +41,12 @@ class User(AbstractUser): SOURCE_LOCAL = 'local' SOURCE_LDAP = 'ldap' SOURCE_OPENID = 'openid' + SOURCE_RADIUS = 'radius' SOURCE_CHOICES = ( (SOURCE_LOCAL, 'Local'), (SOURCE_LDAP, 'LDAP/AD'), (SOURCE_OPENID, 'OpenID'), + (SOURCE_RADIUS, 'Radius'), ) id = models.UUIDField(default=uuid.uuid4, primary_key=True) username = models.CharField( diff --git a/apps/users/serializers/v2.py b/apps/users/serializers/v2.py index f9931d5dc..f936dc7bc 100644 --- a/apps/users/serializers/v2.py +++ b/apps/users/serializers/v2.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # +from django.utils.translation import ugettext as _ from rest_framework import serializers from ..models import User, AccessKey @@ -12,7 +13,7 @@ class AccessKeySerializer(serializers.ModelSerializer): read_only_fields = ['id', 'secret'] -class ServiceAccountRegistrationSerializer(serializers.ModelSerializer): +class ServiceAccountSerializer(serializers.ModelSerializer): access_key = AccessKeySerializer(read_only=True) class Meta: @@ -30,15 +31,22 @@ class ServiceAccountRegistrationSerializer(serializers.ModelSerializer): def validate_name(self, name): email = self.get_email() username = self.get_username() - if User.objects.filter(email=email) or \ - User.objects.filter(username=username): - raise serializers.ValidationError('name not unique', code='unique') + if self.instance: + users = User.objects.exclude(id=self.instance.id) + else: + users = User.objects.all() + if users.filter(email=email) or \ + users.filter(username=username): + raise serializers.ValidationError(_('name not unique'), code='unique') return name + def save(self, **kwargs): + self.validated_data['email'] = self.get_email() + self.validated_data['username'] = self.get_username() + self.validated_data['role'] = User.ROLE_APP + return super().save(**kwargs) + def create(self, validated_data): - validated_data['email'] = self.get_email() - validated_data['username'] = self.get_username() - validated_data['role'] = User.ROLE_APP instance = super().create(validated_data) instance.create_access_key() return instance diff --git a/apps/users/tasks.py b/apps/users/tasks.py index 78a6bdc1a..769d01ed7 100644 --- a/apps/users/tasks.py +++ b/apps/users/tasks.py @@ -3,10 +3,8 @@ from celery import shared_task -from ops.celery.utils import ( - create_or_update_celery_periodic_tasks, - after_app_ready_start -) +from ops.celery.utils import create_or_update_celery_periodic_tasks +from ops.celery.decorator import after_app_ready_start from .models import User from common.utils import get_logger from .utils import write_login_log, send_password_expiration_reminder_mail diff --git a/config_docker.py b/config_docker.py deleted file mode 100644 index 643c11055..000000000 --- a/config_docker.py +++ /dev/null @@ -1,212 +0,0 @@ -""" - jumpserver.config - ~~~~~~~~~~~~~~~~~ - - Jumpserver project setting file - - :copyright: (c) 2014-2017 by Jumpserver Team - :license: GPL v2, see LICENSE for more details. -""" -import os -import json - -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) - - -class Config: - # Use it to encrypt or decrypt data - # SECURITY WARNING: keep the secret key used in production secret! - SECRET_KEY = os.environ.get('SECRET_KEY') or '2vym+ky!997d5kkcc64mnz06y1mmui3lut#(^wd=%s_qj$1%x' - - # How many line display every page if using django pager, default 25 - DISPLAY_PER_PAGE = 25 - - # It's used to identify your site, When we send a create mail to user, we only know login url is /login/ - # But we should know the absolute url like: http://jms.jumpserver.org/login/, so SITE_URL is - # HTTP_PROTOCOL://HOST[:PORT] - SITE_URL = 'http://localhost' - - # Django security setting, if your disable debug model, you should setting that - ALLOWED_HOSTS = ['*'] - - # Development env open this, when error occur display the full process track, Production disable it - DEBUG = True - - # DEBUG, INFO, WARNING, ERROR, CRITICAL can set. See https://docs.djangoproject.com/en/1.10/topics/logging/ - LOG_LEVEL = 'DEBUG' - LOG_DIR = os.path.join(BASE_DIR, 'logs') - - # Database setting, Support sqlite3, mysql, postgres .... - # See https://docs.djangoproject.com/en/1.10/ref/settings/#databases - - # SQLite setting: - DB_ENGINE = 'sqlite3' - DB_NAME = os.path.join(BASE_DIR, 'data', 'db.sqlite3') - - # MySQL or postgres setting like: - # DB_ENGINE = 'mysql' - # DB_HOST = '127.0.0.1' - # DB_PORT = 3306 - # DB_USER = 'root' - # DB_PASSWORD = '' - # DB_NAME = 'jumpserver' - - # When Django start it will bind this host and port - # ./manage.py runserver 127.0.0.1:8080 - HTTP_BIND_HOST = '0.0.0.0' - HTTP_LISTEN_PORT = 8080 - - # Use Redis as broker for celery and web socket - REDIS_HOST = '127.0.0.1' - REDIS_PORT = 6379 - REDIS_PASSWORD = '' - BROKER_URL = 'redis://%(password)s%(host)s:%(port)s/3' % { - 'password': REDIS_PASSWORD, - 'host': REDIS_HOST, - 'port': REDIS_PORT, - } - - # Api token expiration when create, Jumpserver refresh time when request arrive - TOKEN_EXPIRATION = 3600 - - # Session and csrf domain settings - SESSION_COOKIE_AGE = 3600*24 - - # Email SMTP setting, we only support smtp send mail - EMAIL_HOST = 'smtp.163.com' - EMAIL_PORT = 25 - EMAIL_HOST_USER = '' - EMAIL_HOST_PASSWORD = '' # Caution: Some SMTP server using `Authorization Code` except password - EMAIL_USE_SSL = True if EMAIL_PORT == 465 else False - EMAIL_USE_TLS = True if EMAIL_PORT == 587 else False - EMAIL_SUBJECT_PREFIX = '[Jumpserver] ' - - CAPTCHA_TEST_MODE = False - - # You can set jumpserver usage url here, that when user submit wizard redirect to - USER_GUIDE_URL = '' - - # LDAP Auth settings - AUTH_LDAP = False - AUTH_LDAP_SERVER_URI = 'ldap://localhost:389' - AUTH_LDAP_BIND_DN = 'cn=admin,dc=jumpserver,dc=org' - AUTH_LDAP_BIND_PASSWORD = '' - AUTH_LDAP_SEARCH_OU = 'ou=tech,dc=jumpserver,dc=org' - AUTH_LDAP_SEARCH_FILTER = '(cn=%(user)s)' - AUTH_LDAP_USER_ATTR_MAP = { - "username": "cn", - "name": "sn", - "email": "mail" - } - AUTH_LDAP_START_TLS = False - - # - # OTP_VALID_WINDOW = 0 - - def __init__(self): - pass - - def __getattr__(self, item): - return None - - -class DockerConfig(Config): - """ - 配置文件默认从环境变量里读取,如果没有会使用后面的默认值 - """ - # 用来加密数据的key, 可以修改,但务必保存好这个字符串,丢失它后加密会无法解开 - # SECRET_KEY = "SOME_KEY_NO_ONE_GUESS" - SECRET_KEY = os.environ.get("SECRET_KEY") or "MD923lkSDi8213kl),3()&^%aM2q1mz;223lkM0o1" - # 访问的域名, 格式 http[s]://域名[:端口号] - # SITE_URL = "http://jumpserver.fit2cloud.com" - SITE_URL = os.environ.get("SITE_URL") or 'http://localhost' - # 是否开启DEBUG模式 - # DEBUG = True, or DEBUG = False, - DEBUG = bool(os.environ.get("DEBUG")) if os.environ.get("DEBUG") else False - # 日志级别, 默认 INFO - # LOG_LEVEL = WARN - LOG_LEVEL = os.environ.get("LOG_LEVEL") or "INFO" - # 使用的数据库类型,支持 SQLite, MySQL, PostgreSQL, Oracle - # 数据库设置, 如果使用外部的mysql请设置,否则不要改动 - - # DB_ENGINE = "oracle" | "postgre" | "mysql" | "sqlite3" - DB_ENGINE = os.environ.get("DB_ENGINE") or 'mysql' - # DB_HOST = "192.168.1.1" - DB_HOST = os.environ.get("DB_HOST") or 'mysql' - # 端口号 - # DB_PORT = 3306 - DB_PORT = os.environ.get("DB_PORT") or 3306 - # 数据库账号 - # DB_USER = "jumpserver" - DB_USER = os.environ.get("DB_USER") or 'root' - # 数据库密码 - # DB_PASSWORD = "db_jumpserver_password" - DB_PASSWORD = os.environ.get("DB_PASSWORD") or '' - # 数据库名称 - # DB_NAME = "jumpserver" - DB_NAME = os.environ.get("DB_NAME") or 'jumpserver' - - # Redis配置,如果不使用外部redis不要改动 - # Redis地址 - # REDIS_HOST = "192.168.1.1" - REDIS_HOST = os.environ.get("REDIS_HOST") or 'redis' - # Redis端口号 - # REDIS_PORT = 6380 - REDIS_PORT = os.environ.get("REDIS_PORT") or 6379 - # Redis密码 - # REDIS_PASSWORD = "redis_password" - REDIS_PASSWORD = os.environ.get("REDIS_PASSWORD") or '' - - # 邮箱SMTP设置, 可以参考各运营商配置文档 - # SMTP服务器地址 - # EMAIL_HOST = 'smtp.qq.com' - EMAIL_HOST = 'smtp.163.com' - # SMTP端口号 - # EMAIL_PORT = 465 - EMAIL_PORT = 25 - # SMTP连接邮箱地址 - # EMAIL_HOST_USER = "noreply@jumpserver.org" - EMAIL_HOST_USER = '' - # SMTP邮箱的密码, 注意 一些运营商通常要求使用授权码来发SMTP邮件 - EMAIL_HOST_PASSWORD = '' - # 是否启用SSL, 如果端口号是 465通常设置为True - # EMAIL_USE_SSL = True - EMAIL_USE_SSL = True if EMAIL_PORT == 465 else False - # 是否启用TLS, 如果端口号是 587通常设置为True - # EMAIL_USE_TLS = True - EMAIL_USE_TLS = True if EMAIL_PORT == 587 else False - # 邮件的主题前缀 - EMAIL_SUBJECT_PREFIX = '[Jumpserver] ' - - # 认证启用LDAP的设置 - # 是否启用LDAP,默认不启用 - # AUTH_LDAP = True - AUTH_LDAP = False - # LDAP的地址 - AUTH_LDAP_SERVER_URI = 'ldap://localhost:389' - # LDAP绑定的查询账户 - AUTH_LDAP_BIND_DN = 'cn=admin,dc=jumpserver,dc=org' - # 密码 - AUTH_LDAP_BIND_PASSWORD = '' - # 用户所在的ou - AUTH_LDAP_SEARCH_OU = 'ou=tech,dc=jumpserver,dc=org' - # 查询时使用的过滤器, 仅可以修改前面的表示符,可能是cn或uid, 也就是登录用户名所在字段 - # AUTH_LDAP_SEARCH_FILTER = '(uid=%(user)s)' - AUTH_LDAP_SEARCH_FILTER = '(cn=%(user)s)' - # LDAP用户信息映射到Jumpserver - AUTH_LDAP_USER_ATTR_MAP = { - "username": "cn", # 将LDAP信息中的 `cn` 字段映射为 `username(用户名)` - "name": "sn", # 将 LDAP信息中的 `sn` 映射为 `name(姓名)` - "email": "mail" # 将 LDAP信息中的 `mail` 映射为 `email(邮箱地址)` - } - # 是否启用TLS加密 - AUTH_LDAP_START_TLS = False - - - # - OTP_VALID_WINDOW = int(os.environ.get("OTP_VALID_WINDOW")) if os.environ.get("OTP_VALID_WINDOW") else 0 - - -# Default using Config settings, you can write if/else for different env -config = DockerConfig() - diff --git a/config_example.py b/config_example.py deleted file mode 100644 index 5f5301f0e..000000000 --- a/config_example.py +++ /dev/null @@ -1,117 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" - jumpserver.config - ~~~~~~~~~~~~~~~~~ - - Jumpserver project setting file - - :copyright: (c) 2014-2017 by Jumpserver Team - :license: GPL v2, see LICENSE for more details. -""" -import os - -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) - - -class Config: - """ - Jumpserver Config File - Jumpserver 配置文件 - - Jumpserver use this config for drive django framework running, - You can set is value or set the same envirment value, - Jumpserver look for config order: file => env => default - - Jumpserver使用配置来驱动Django框架的运行, - 你可以在该文件中设置,或者设置同样名称的环境变量, - Jumpserver使用配置的顺序: 文件 => 环境变量 => 默认值 - """ - # SECURITY WARNING: keep the secret key used in production secret! - # 加密秘钥 生产环境中请修改为随机字符串,请勿外泄 - SECRET_KEY = '2vym+ky!997d5kkcc64mnz06y1mmui3lut#(^wd=%s_qj$1%x' - - # SECURITY WARNING: keep the bootstrap token used in production secret! - # 预共享Token coco和guacamole用来注册服务账号,不在使用原来的注册接受机制 - BOOTSTRAP_TOKEN = 'PleaseChangeMe' - - # Development env open this, when error occur display the full process track, Production disable it - # DEBUG 模式 开启DEBUG后遇到错误时可以看到更多日志 - # DEBUG = True - - # DEBUG, INFO, WARNING, ERROR, CRITICAL can set. See https://docs.djangoproject.com/en/1.10/topics/logging/ - # 日志级别 - # LOG_LEVEL = 'DEBUG' - # LOG_DIR = os.path.join(BASE_DIR, 'logs') - - # Session expiration setting, Default 24 hour, Also set expired on on browser close - # 浏览器Session过期时间,默认24小时, 也可以设置浏览器关闭则过期 - # SESSION_COOKIE_AGE = 3600 * 24 - # SESSION_EXPIRE_AT_BROWSER_CLOSE = False - - # Database setting, Support sqlite3, mysql, postgres .... - # 数据库设置 - # See https://docs.djangoproject.com/en/1.10/ref/settings/#databases - - # SQLite setting: - # 使用单文件sqlite数据库 - # DB_ENGINE = 'sqlite3' - # DB_NAME = os.path.join(BASE_DIR, 'data', 'db.sqlite3') - - # MySQL or postgres setting like: - # 使用Mysql作为数据库 - DB_ENGINE = 'mysql' - DB_HOST = '127.0.0.1' - DB_PORT = 3306 - DB_USER = 'jumpserver' - DB_PASSWORD = '' - DB_NAME = 'jumpserver' - - # When Django start it will bind this host and port - # ./manage.py runserver 127.0.0.1:8080 - # 运行时绑定端口 - HTTP_BIND_HOST = '0.0.0.0' - HTTP_LISTEN_PORT = 8080 - - # Use Redis as broker for celery and web socket - # Redis配置 - REDIS_HOST = '127.0.0.1' - REDIS_PORT = 6379 - # REDIS_PASSWORD = '' - # REDIS_DB_CELERY = 3 - # REDIS_DB_CACHE = 4 - - # Use OpenID authorization - # 使用OpenID 来进行认证设置 - # BASE_SITE_URL = 'http://localhost:8080' - # AUTH_OPENID = False # True or False - # AUTH_OPENID_SERVER_URL = 'https://openid-auth-server.com/' - # AUTH_OPENID_REALM_NAME = 'realm-name' - # AUTH_OPENID_CLIENT_ID = 'client-id' - # AUTH_OPENID_CLIENT_SECRET = 'client-secret' - - # - # OTP_VALID_WINDOW = 0 - - def __init__(self): - pass - - def __getattr__(self, item): - return None - - -class DevelopmentConfig(Config): - pass - - -class TestConfig(Config): - pass - - -class ProductionConfig(Config): - pass - - -# Default using Config settings, you can write if/else for different env -config = DevelopmentConfig() - diff --git a/config_example.yml b/config_example.yml new file mode 100644 index 000000000..a88dc8596 --- /dev/null +++ b/config_example.yml @@ -0,0 +1,68 @@ +# SECURITY WARNING: keep the secret key used in production secret! +# 加密秘钥 生产环境中请修改为随机字符串,请勿外泄, 可使用命令生成 +# $ cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 49;echo +SECRET_KEY: + +# SECURITY WARNING: keep the bootstrap token used in production secret! +# 预共享Token coco和guacamole用来注册服务账号,不在使用原来的注册接受机制 +BOOTSTRAP_TOKEN: + +# Development env open this, when error occur display the full process track, Production disable it +# DEBUG 模式 开启DEBUG后遇到错误时可以看到更多日志 +# DEBUG: true + +# DEBUG, INFO, WARNING, ERROR, CRITICAL can set. See https://docs.djangoproject.com/en/1.10/topics/logging/ +# 日志级别 +# LOG_LEVEL: DEBUG +# LOG_DIR: + +# Session expiration setting, Default 24 hour, Also set expired on on browser close +# 浏览器Session过期时间,默认24小时, 也可以设置浏览器关闭则过期 +# SESSION_COOKIE_AGE: 3600 * 24 +# SESSION_EXPIRE_AT_BROWSER_CLOSE: False + +# Database setting, Support sqlite3, mysql, postgres .... +# 数据库设置 +# See https://docs.djangoproject.com/en/1.10/ref/settings/#databases + +# SQLite setting: +# 使用单文件sqlite数据库 +# DB_ENGINE: sqlite3 +# DB_NAME: + +# MySQL or postgres setting like: +# 使用Mysql作为数据库 +DB_ENGINE: mysql +DB_HOST: 127.0.0.1 +DB_PORT: 3306 +DB_USER: jumpserver +DB_PASSWORD: +DB_NAME: jumpserver + +# When Django start it will bind this host and port +# ./manage.py runserver 127.0.0.1:8080 +# 运行时绑定端口 +HTTP_BIND_HOST: 0.0.0.0 +HTTP_LISTEN_PORT: 8080 + +# Use Redis as broker for celery and web socket +# Redis配置 +REDIS_HOST: 127.0.0.1 +REDIS_PORT: 6379 +# REDIS_PASSWORD: +# REDIS_DB_CELERY: 3 +# REDIS_DB_CACHE: 4 + +# Use OpenID authorization +# 使用OpenID 来进行认证设置 +# BASE_SITE_URL: http://localhost:8080 +# AUTH_OPENID: false # True or False +# AUTH_OPENID_SERVER_URL: https://openid-auth-server.com/ +# AUTH_OPENID_REALM_NAME: realm-name +# AUTH_OPENID_CLIENT_ID: client-id +# AUTH_OPENID_CLIENT_SECRET: client-secret + +# OTP settings +# OTP/MFA 配置 +# OTP_VALID_WINDOW: 0 +# OTP_ISSUER_NAME: Jumpserver diff --git a/jms b/jms index f1d0a6944..a5d105c75 100755 --- a/jms +++ b/jms @@ -15,9 +15,10 @@ sys.path.append(BASE_DIR) from apps import __version__ try: - from config import config as CONFIG + from apps.jumpserver.conf import load_user_config + CONFIG = load_user_config() except ImportError: - print("Could not find config file, `cp config_example.py config.py`") + print("Could not find config file, `cp config_example.yml config.yml`") sys.exit(1) os.environ["PYTHONIOENCODING"] = "UTF-8" @@ -107,8 +108,7 @@ def is_running(s, unlink=True): pid_file = get_pid_file_path(s) if os.path.isfile(pid_file): - with open(pid_file, 'r') as f: - pid = get_pid(s) + pid = get_pid(s) if check_pid(pid): return True diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 8c6a5087b..c956bc9d4 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -78,3 +78,4 @@ python-keycloak-client==0.1.3 rest_condition==1.0.3 python-ldap==3.1.0 tencentcloud-sdk-python==3.0.40 +django-radius==1.3.3 diff --git a/utils/create_assets_user/bulk_create_user.py b/utils/create_assets_user/bulk_create_user.py index 4a0a60e81..14cbca820 100644 --- a/utils/create_assets_user/bulk_create_user.py +++ b/utils/create_assets_user/bulk_create_user.py @@ -17,13 +17,13 @@ class UserCreation: self.domain = domain def auth(self): - url = "{}/api/users/v1/token/".format(self.domain) + url = "{}/api/users/v1/auth/".format(self.domain) data = {"username": self.username, "password": self.password} resp = requests.post(url, data=data) if resp.status_code == 200: data = resp.json() self.headers.update({ - 'Authorization': '{} {}'.format(data['Keyword'], data['Token']) + 'Authorization': '{} {}'.format('Bearer', data['token']) }) else: print("用户名 或 密码 或 地址 不对")