From 35403086ab054fd900a56fa8864f3c7fa1fbf931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=B9=BF?= Date: Mon, 7 Jan 2019 18:55:02 +0800 Subject: [PATCH 01/14] Config (#2322) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] 修改配置文件 * [Update] 修改配置文件形式 --- .gitignore | 1 + apps/jumpserver/conf.py | 26 ++++- config_docker.py | 212 ---------------------------------------- config_docker.yml | 65 ++++++++++++ config_example.py | 117 ---------------------- config_example.yml | 67 +++++++++++++ 6 files changed, 156 insertions(+), 332 deletions(-) delete mode 100644 config_docker.py create mode 100644 config_docker.yml delete mode 100644 config_example.py create mode 100644 config_example.yml 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/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index ed19f3567..49b74d961 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -286,8 +286,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 +312,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, @@ -330,21 +331,40 @@ defaults = { 'SECURITY_PASSWORD_LOWER_CASE': False, 'SECURITY_PASSWORD_NUMBER': False, 'SECURITY_PASSWORD_SPECIAL_CHAR': False, + 'HTTP_BIND_HOST': '0.0.0.0', + 'HTTP_LISTEN_PORT': 8080, } def load_user_config(): sys.path.insert(0, PROJECT_DIR) config = Config(PROJECT_DIR, defaults) + loaded = False + + for i in ['config.yml', 'config.yaml']: + if os.path.isfile(os.path.join(config.root_path, i)): + config.from_yaml(i) + loaded = True + try: from config import config as c config.from_object(c) + loaded = True except ImportError: + pass + + try: + config.from_yaml('config.yml') + loaded = True + except IOError: + pass + + 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/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_docker.yml b/config_docker.yml new file mode 100644 index 000000000..29c0acb58 --- /dev/null +++ b/config_docker.yml @@ -0,0 +1,65 @@ +# SECURITY WARNING: keep the secret key used in production secret! +# 加密秘钥 生产环境中请修改为随机字符串,请勿外泄 +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校验窗口大小,可以避免服务器时间稍有差异引起OTP校验失败 +# OTP_VALID_WINDOW: 0 \ No newline at end of file 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..219a4f024 --- /dev/null +++ b/config_example.yml @@ -0,0 +1,67 @@ +# SECURITY WARNING: keep the secret key used in production secret! +# 加密秘钥 生产环境中请修改为随机字符串,请勿外泄 +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 \ No newline at end of file From 0e1d3f93ffb730c7c746d11d5a99c5e7ea700e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=B9=BF?= Date: Mon, 7 Jan 2019 19:20:39 +0800 Subject: [PATCH 02/14] =?UTF-8?q?[Update]=20=E6=94=AF=E6=8C=81radius?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=20(#2323)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] 支持radius认证 * [Update] 支持radius * [Update] 增加requirements * [Update] 修改copyright * [Update] 修改migrations --- Dockerfile | 2 +- apps/authentication/radius/__init__.py | 2 + apps/authentication/radius/backends.py | 34 ++++++++++ apps/jumpserver/conf.py | 4 ++ apps/jumpserver/settings.py | 10 +++ apps/templates/_copyright.html | 2 +- apps/templates/_footer.html | 2 +- apps/templates/flash_message_standalone.html | 2 +- .../migrations/0018_auto_20190107_1912.py | 18 +++++ apps/users/models/user.py | 2 + config_docker.yml | 65 ------------------- requirements/requirements.txt | 1 + 12 files changed, 75 insertions(+), 69 deletions(-) create mode 100644 apps/authentication/radius/__init__.py create mode 100644 apps/authentication/radius/backends.py create mode 100644 apps/users/migrations/0018_auto_20190107_1912.py delete mode 100644 config_docker.yml diff --git a/Dockerfile b/Dockerfile index a520a4ba4..7d40f06aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN yum -y install epel-release && cd /tmp/requirements && \ RUN cd /tmp/requirements && pip install -r requirements.txt COPY . /opt/jumpserver -COPY config_docker.py /opt/jumpserver/config.py +COPY config_example.yml /opt/jumpserver/config.yml VOLUME /opt/jumpserver/data VOLUME /opt/jumpserver/logs 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/jumpserver/conf.py b/apps/jumpserver/conf.py index 49b74d961..7e96ff91b 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -331,6 +331,10 @@ 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, } diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index ca3d5afa3..2a0f44f96 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -400,6 +400,16 @@ 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) + # Celery using redis as broker CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % { 'password': CONFIG.REDIS_PASSWORD, 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/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/config_docker.yml b/config_docker.yml deleted file mode 100644 index 29c0acb58..000000000 --- a/config_docker.yml +++ /dev/null @@ -1,65 +0,0 @@ -# SECURITY WARNING: keep the secret key used in production secret! -# 加密秘钥 生产环境中请修改为随机字符串,请勿外泄 -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校验窗口大小,可以避免服务器时间稍有差异引起OTP校验失败 -# OTP_VALID_WINDOW: 0 \ No newline at end of file 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 From 49a166552e42090af865e9b40e42ee08e2eb5fc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=B9=BF?= Date: Tue, 8 Jan 2019 11:15:09 +0800 Subject: [PATCH 03/14] Bugfix (#2327) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Bugfix] 修复两个配置文件冲突问题 * [Update] Docker中不再提供配置文件 --- Dockerfile | 1 - apps/jumpserver/conf.py | 36 ++++++++++++++++++++---------------- apps/ops/tasks.py | 23 ++++++++++++++++++++++- apps/terminal/tasks.py | 2 +- jms | 3 ++- 5 files changed, 45 insertions(+), 20 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7d40f06aa..e6c2ed82d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,6 @@ RUN yum -y install epel-release && cd /tmp/requirements && \ RUN cd /tmp/requirements && pip install -r requirements.txt COPY . /opt/jumpserver -COPY config_example.yml /opt/jumpserver/config.yml VOLUME /opt/jumpserver/data VOLUME /opt/jumpserver/logs diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index 7e96ff91b..b4f3ff0a1 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -340,29 +340,33 @@ defaults = { } -def load_user_config(): - sys.path.insert(0, PROJECT_DIR) - config = Config(PROJECT_DIR, defaults) - loaded = False - - for i in ['config.yml', 'config.yaml']: - if os.path.isfile(os.path.join(config.root_path, i)): - config.from_yaml(i) - loaded = True - +def load_from_object(config): try: from config import config as c config.from_object(c) - loaded = True + return True except ImportError: pass + return False - try: - config.from_yaml('config.yml') - loaded = True - except IOError: - pass +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) + + loaded = load_from_object(config) + if not loaded: + loaded = load_from_yml(config) if not loaded: msg = """ diff --git a/apps/ops/tasks.py b/apps/ops/tasks.py index ac0bdce89..06bf3d51d 100644 --- a/apps/ops/tasks.py +++ b/apps/ops/tasks.py @@ -1,9 +1,12 @@ # coding: utf-8 +import os + 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 .models import Task, CommandExecution, CeleryTask logger = get_logger(__file__) @@ -48,6 +51,24 @@ def clean_tasks_adhoc_period(): ad.delete() +@shared_task +@register_as_period_task(interval=3600*24) +@after_app_shutdown_clean +def clean_celery_tasks_period(): + logger.debug("Start clean celery task history") + one_month_ago = timezone.now() - timezone.timedelta(days=30) + 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() + + @shared_task def hello(name, callback=None): print("Hello {}".format(name)) diff --git a/apps/terminal/tasks.py b/apps/terminal/tasks.py index 77aa66226..e47803b3c 100644 --- a/apps/terminal/tasks.py +++ b/apps/terminal/tasks.py @@ -25,7 +25,7 @@ logger = get_task_logger(__name__) @after_app_ready_start @after_app_shutdown_clean 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() diff --git a/jms b/jms index f1d0a6944..cd9ad396d 100755 --- a/jms +++ b/jms @@ -15,7 +15,8 @@ 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`") sys.exit(1) From 4bf0dfcf7b541edb01da32f7e32965516fd79ddd Mon Sep 17 00:00:00 2001 From: xiaomao Date: Tue, 8 Jan 2019 15:27:15 +0800 Subject: [PATCH 04/14] =?UTF-8?q?[bugfix]=20=E8=A7=A3=E5=86=B3=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E4=B8=AD=E6=96=87=E6=8A=A5=E9=94=99=E4=BB=A5=E5=8F=8A?= =?UTF-8?q?=E5=90=84=E7=A7=8Dutf8=E9=97=AE=E9=A2=98=20(#2328)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jumpserver/settings.py | 6 ++++-- apps/ops/api/celery.py | 2 +- apps/ops/celery/signal_handler.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index 2a0f44f96..db1822839 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -209,19 +209,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': os.path.join(PROJECT_DIR, 'logs', 'jumpserver.log'), }, 'ansible_logs': { + 'encoding': 'utf8', 'level': 'DEBUG', 'class': 'logging.FileHandler', 'formatter': 'main', - 'filename': os.path.join(PROJECT_DIR, 'logs', 'ansible.log') + 'filename': os.path.join(PROJECT_DIR, 'logs', 'ansible.log'), }, }, 'loggers': { diff --git a/apps/ops/api/celery.py b/apps/ops/api/celery.py index 2640cffec..ae8f479d6 100644 --- a/apps/ops/api/celery.py +++ b/apps/ops/api/celery.py @@ -31,7 +31,7 @@ class CeleryTaskLogApi(generics.RetrieveAPIView): if not log_path or not os.path.isfile(log_path): return Response({"data": _("Waiting ...")}, status=203) - with open(log_path, 'r') as f: + with open(log_path, 'r', encoding="utf8") as f: offset = cache.get(mark, 0) f.seek(offset) data = f.read(self.buff_size).replace('\n', '\r\n') diff --git a/apps/ops/celery/signal_handler.py b/apps/ops/celery/signal_handler.py index 961c18ad3..ae7acc2ea 100644 --- a/apps/ops/celery/signal_handler.py +++ b/apps/ops/celery/signal_handler.py @@ -81,7 +81,7 @@ def pre_run_task_signal_handler(sender, task_id=None, task=None, **kwargs): t.status = CeleryTask.RUNNING t.log_path = log_path t.save() - f = open(full_path, 'w') + f = open(full_path, 'w', encoding="utf-8") tee = TeeObj(f) sys.stdout = tee task.log_f = tee From 131e588d82db1797fc1cb0c4626cce12b6b33ded Mon Sep 17 00:00:00 2001 From: mago960806 Date: Thu, 10 Jan 2019 10:21:05 +0800 Subject: [PATCH 05/14] =?UTF-8?q?is=5Frunning()=E9=87=8C=E7=9A=84=E6=89=93?= =?UTF-8?q?=E5=BC=80pid=E6=96=87=E4=BB=B6=E7=9A=84=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E5=B7=B2=E7=BB=8F=E5=9C=A8get=5Fpid()=E4=B8=AD=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E8=BF=87=E4=BA=86,=E4=B8=8D=E9=9C=80=E8=A6=81?= =?UTF-8?q?=E5=86=8D=E6=AC=A1=E6=89=A7=E8=A1=8C=20(#2335)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jms | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/jms b/jms index cd9ad396d..0c2ae8fd3 100755 --- a/jms +++ b/jms @@ -108,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 From 50c1b3ed4a359d3639e9b9cb01614289b9f68e2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=B9=BF?= Date: Thu, 10 Jan 2019 11:50:08 +0800 Subject: [PATCH 06/14] =?UTF-8?q?[Update]=20=E7=BB=9F=E4=B8=80coco=20host?= =?UTF-8?q?=20key=20(#2336)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/common/signals_handler.py | 13 ++++++++++++- apps/jumpserver/conf.py | 1 + apps/jumpserver/settings.py | 1 + apps/locale/zh/LC_MESSAGES/django.mo | Bin 60399 -> 60399 bytes apps/locale/zh/LC_MESSAGES/django.po | 12 ++++++------ apps/terminal/models.py | 5 +++-- apps/users/api/user.py | 5 +++++ config_example.yml | 5 +++-- 8 files changed, 31 insertions(+), 11 deletions(-) diff --git a/apps/common/signals_handler.py b/apps/common/signals_handler.py index 96142e394..96355a771 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__) @@ -62,6 +64,15 @@ def refresh_all_settings_on_django_ready(sender, **kwargs): pass +@receiver(django_ready) +def auto_generate_terminal_host_key(sender, **kwargs): + if Setting.objects.filter(name='TERMINAL_HOST_KEY').exists(): + 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/jumpserver/conf.py b/apps/jumpserver/conf.py index b4f3ff0a1..c940337da 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -321,6 +321,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, diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index db1822839..518b4617e 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -504,6 +504,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 a6518874eff0d18bf78d4278827e25b5eb29bf5f..71cba8f0c095c95860932dac6c98b61f30f8d12d 100644 GIT binary patch delta 100 zcmaEVo%#KB<_$r!+07LUO|48UHb>63loxoqcPhinWz(LmS@?YBZXjp#qFqHC%pivN oVR63loxn8vyI{9vT4uOEPOt5H;}V=(XJv6W)MUC ouso9{knv>4?q_p$F+7{o@odg2h@$6HRy^z2jv}-<{^$fX06Ahf5C8xG diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 98e838293..c4289ffe0 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -2989,7 +2989,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 +3009,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 +3017,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 +3025,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" @@ -3583,7 +3583,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." @@ -4326,7 +4326,7 @@ msgstr "* 您的密码不符合要求" #: users/views/login.py:349 msgid "First login" -msgstr "首次登陆" +msgstr "首次登录" #: users/views/user.py:145 msgid "Bulk update user success" diff --git a/apps/terminal/models.py b/apps/terminal/models.py index 880637efe..ad72dd583 100644 --- a/apps/terminal/models.py +++ b/apps/terminal/models.py @@ -61,8 +61,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({ 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/config_example.yml b/config_example.yml index 219a4f024..a88dc8596 100644 --- a/config_example.yml +++ b/config_example.yml @@ -1,5 +1,6 @@ # 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! @@ -64,4 +65,4 @@ REDIS_PORT: 6379 # OTP settings # OTP/MFA 配置 # OTP_VALID_WINDOW: 0 -# OTP_ISSUER_NAME: Jumpserver \ No newline at end of file +# OTP_ISSUER_NAME: Jumpserver From d026b31c9fcd1f6ce23028f1aca7ac5856897e63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=B9=BF?= Date: Tue, 15 Jan 2019 10:23:30 +0800 Subject: [PATCH 07/14] Bugfix (#2346) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] 修改command Post导致的output错误和定时任务创建问题 * [Update] 修改celery 日志 * [Update] 修改task日志方式 * [Update] 修改Docker file --- Dockerfile | 2 +- apps/assets/api/admin_user.py | 1 + apps/assets/api/asset.py | 2 + apps/assets/api/system_user.py | 2 + apps/assets/serializers/admin_user.py | 5 +- apps/assets/tasks.py | 19 ++- apps/common/api.py | 44 +++++- apps/common/serializers.py | 5 + apps/common/signals_handler.py | 25 +-- apps/common/utils.py | 18 --- apps/jumpserver/settings.py | 17 ++- apps/ops/ansible/display.py | 19 --- apps/ops/ansible/runner.py | 13 +- apps/ops/api/celery.py | 46 +++--- apps/ops/apps.py | 3 +- apps/ops/celery/decorator.py | 109 +++++++++++++ apps/ops/celery/logger.py | 160 ++++++++++++++++++++ apps/ops/celery/signal_handler.py | 144 +++++++++--------- apps/ops/celery/utils.py | 105 +------------ apps/ops/models/command.py | 4 + apps/ops/tasks.py | 22 ++- apps/ops/templates/ops/celery_task_log.html | 17 ++- apps/ops/views/celery.py | 10 +- apps/orgs/models.py | 4 + apps/terminal/api/v1/session.py | 7 +- apps/terminal/api/v1/terminal.py | 43 +----- apps/terminal/backends/command/db.py | 22 ++- apps/terminal/models.py | 13 ++ apps/terminal/serializers/v1.py | 2 +- apps/terminal/tasks.py | 22 +-- apps/users/tasks.py | 6 +- 31 files changed, 567 insertions(+), 344 deletions(-) delete mode 100644 apps/ops/ansible/display.py create mode 100644 apps/ops/celery/decorator.py create mode 100644 apps/ops/celery/logger.py diff --git a/Dockerfile b/Dockerfile index e6c2ed82d..54b094a18 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ 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 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..f7b03cce8 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,6 +223,7 @@ 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 @@ -394,13 +397,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 +411,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/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 96355a771..7879f94b2 100644 --- a/apps/common/signals_handler.py +++ b/apps/common/signals_handler.py @@ -18,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) @@ -66,7 +68,10 @@ def refresh_all_settings_on_django_ready(sender, **kwargs): @receiver(django_ready) def auto_generate_terminal_host_key(sender, **kwargs): - if Setting.objects.filter(name='TERMINAL_HOST_KEY').exists(): + 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) 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/settings.py b/apps/jumpserver/settings.py index 518b4617e..24e4ea7f5 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -412,6 +412,9 @@ 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, @@ -425,14 +428,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 = { 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 ae8f479d6..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', encoding="utf8") 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 ae7acc2ea..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', encoding="utf-8") - 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 06bf3d51d..799364563 100644 --- a/apps/ops/tasks.py +++ b/apps/ops/tasks.py @@ -5,7 +5,11 @@ 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 .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__) @@ -39,8 +43,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() @@ -52,8 +56,8 @@ def clean_tasks_adhoc_period(): @shared_task +@after_app_shutdown_clean_periodic @register_as_period_task(interval=3600*24) -@after_app_shutdown_clean def clean_celery_tasks_period(): logger.debug("Start clean celery task history") one_month_ago = timezone.now() - timezone.timedelta(days=30) @@ -69,11 +73,19 @@ def clean_celery_tasks_period(): tasks.delete() +@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..9a570d841 100644 --- a/apps/static/js/jumpserver.js +++ b/apps/static/js/jumpserver.js @@ -478,7 +478,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 +525,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/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("用户名 或 密码 或 地址 不对") From 2a0e68c58fee5cb7244d8d27af5e7b0a18320197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=B9=BF?= Date: Wed, 16 Jan 2019 18:13:16 +0800 Subject: [PATCH 09/14] Bugfix (#2350) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] 权限页面增加过滤规则 * [Update] 修改terminal注册,更新以后使用api完成 * [Update] 修改terminal注册,更新以后使用api完成 * [Update] 修改更新注册逻辑 --- apps/locale/zh/LC_MESSAGES/django.mo | Bin 60393 -> 60438 bytes apps/locale/zh/LC_MESSAGES/django.po | 36 ++++---- apps/static/js/jumpserver.js | 85 +++++++++++++++++- apps/terminal/api/v2/terminal.py | 23 ++++- apps/terminal/forms.py | 2 - apps/terminal/serializers/v1.py | 2 - apps/terminal/serializers/v2.py | 54 ++++++----- .../templates/terminal/terminal_update.html | 18 ++-- apps/terminal/urls/api_urls_v2.py | 3 +- apps/users/api/v2/user.py | 2 +- apps/users/serializers/v2.py | 22 +++-- 11 files changed, 181 insertions(+), 66 deletions(-) diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index fc4e8e96f82b708c2f8f26926011c27b46df5048..cf3c41fc8a31b9470cd64970f815eca91b8c55c9 100644 GIT binary patch delta 13541 zcmXZh37n5r`^WKnRx_9xGnO)AnUQ72j3LHE_OWD5c1_umiOSZDB+HFtZHmS&L`ak+ zJw=41D58vticFF;HOQ|Mz{J``p(#=Q`)UfAe_G{}3|&ScrczE_9y9 z|7C@EUIk2x_q;x#p4T`-t)923wddvIW_%Hwweh@}_%Tkz)(?4JVUXv2fqQUqJI_l9 zCPxR_xj&|p%iHN;&x<2|8q4B%4EH?WE8rrEhQ(L{*J3&R04w7GWIo&4#Zcn8vc#Rc<&?57I>2QX;l7pT|BQe_P}t)_l9txm5;zOn2*J9B}U^q)Pviw zBz|rlK_&VHS&4Vm{1>&Lpst=5gE4pyRz^Ml08(tP6-F|?*Om)Spex4VaE!pon1HiT z6L{O&w_p|G9jNDiK~?BBYT`lNJg*c+q81d7VOSfBV|}wZ`mr>0;6gL)gHPa4)C#`A z>Ua`UFswV1!`hgO%aJj?(mgznE?$oLF{(YfC#%H)s0l1WRcbk^0&99wf8AI|Lo3{b z+SBW(iB;|8R-TDE3t3nipFmCUdDP0Mpz_Q>mH0K(*1dt{a6PJmd#wEss^5uT)L)4% zSch9!l{mDwD^&_AQ3lq+mKcd6Q3=MP`cFrd{5{l`?L?jOACVWzyMkJIiAPIM!Eu4>aaE;yn0hO=F?w_;vTd0cp z!P%|?5vY#k%!;UlRWVQj)P!214q02g2OmL|cra>0J}S=))I?`vd0dIA&aD1NN>mLSVkT<95vYlcMorvD^?wbOenptUgx7qxf-?7jTn!gqbhLAiG+(#J5o^FV^2xDh4&-MAYG} ziCR!?R3#es_gzD48kDdrs>FR!6Bv$4JOQ=R85XZXCEkcixDA!?3)F-Tqb5>>+UxVE zGw~0q;!y)!rONnRXy7WSj_Ig@o1$ji3H3l%+<}jx5|`sUt${0Ia^?VQ1 zM6*!~c@h(GI95V`HW%%=*oar~UsR%t16_h^s0sXo+FS2QH$XH7CXVW#jt}7jSQC9~ zUx)h8Y(ee$HcY`oNIu`Y#f1h4ALL3Og_>z4)JjuO1Ke*uh^kaebxX{Y(qDmT-<4P2diW{L4w8e1jf=bXEHDG^he-`x? z^I7po92Fh9l)#3!&0rVL^KHKXhyp4Ss+VmmCta7=yL4Oka7(Pp>}JK&2L z_KfFM#EGa0Eki9}mBsI%@^8dq_zOnhS=8CM@(lHlgTYVxR)nJ_j|CH&EY`75FZ`kHfIvQ1^3x$mc={k6=kWiCW1;)Bt~>Dsda5 zFodUd$YN3VlTlyJdZ?APv$!kjEDc8We*vTMHPn_Yv)KQX3l01YY9c34iO*v-yoQ=k z{BYNvi0YS$TJZzc-WOG&A*hMuT0G9~PeqM0$Kqwk!hG)?E~2@y36pRahF}ru$LI{| z^aqV_r#1ogYgY?3P#a9a&Zw;#gPK@A>hynrTF5@sIDcR%yp4hPKWd~aaT(Og5>P9y zfjTs`Fb38~U94WA;9&_rE0k9pjkWgasDAmVgbPspKEPP~1oix3)D|8^E#w!}89IaNcL`O2JLv0IEhg8! zw>2@aVpM|qs0lR3IP8QfVSm($hodSp1=VjZ*25L3Jl|p9Ora)v9<{Z9+x;84?7tG< zu^X{@Zs4-02NF?XRQHq%iP&hWX= z7BoRsqBl0fXRrpY$EsL}8t@jX0yRdv1Zie{)JmJ8O5PE*1-(%d>yH|6ggFLvmi)|a1-l_m5E2AwqgNl%a)nzQ7hky8gM77-+sJT z@Bd*gG}Cja*W(ImD`LjD$|Rx^G(tVt0@c5*#gCx&yccTaIan5-L!E)?*1i?hZ!ace zAqL+6+gxPR5W+8tI`lP%q6Qv^n)zf@1zy2=xB}JxJ1mF4VH*C2`c5Q|b?t3XrSF5< zq8!vh@}%DXv0SLclTiasL%k;X>VR`lC0&S`$ZFI|H=@qWc56Rm9zzZEtHoE$f2}=q zoSRS_`kGk+7ux%(s1;?P25yOo*dBEjoBrt@FXf>E~?)&)QSsG{nwx>w*@&&-d1dd-(mumo8VsK zx~Pe?$G}^G%HMwi_1DF48gytTpjJ2+wb!ex<2KYp_MFwK%fvCh!TbygYh#F`TY6UYaUVxhTa!kezs7m~R>i09MQWwns z!+7FS6J148Q5E!Ca-l=@2x?EWF|gOD8IMP;dU3$Y?@K&|i~s{b);j~6ViKgmtt zA-qa^HY)$alik4mkmr5xSuV8l7f=(KWX?i8Fb`F#HK-YHMy+rss?-Nj&mS>=LT%x1 zsD9T_d4gYZ`AefJQxRhs->bt#A`Mxn5)MS|*{Yk8GVfD#6{?9Mx|!DnKeYM{0V#jXJSL#gT?SR zhGOU}wJC$;nPonV z(cI5Q4Lk&caTtc+NGyS)P=|FAYNFFoXJt03GD|TU|3&qWe9iSQ`x^BxPD2G6bh>Mx zUXv!M2YRFS@+nlpF{r~c6)WK))JnFZp8p2*{O=frL9<<+7}WEXuqLLW#_j2I5yQm* z)IcLGehKvjE5JnDh&mJBpeAqzHSjIe^QGpvl_jF`q@gO&)NF&1#1G>$*c+A4-^+zc zdmL4olcyB~Q z7vuH*kK{rFzJjH38Ajmys1k2S?R6oB;olgj3e89mZ<(6 zP~&7{;NSm-aiPO88nwbHsF}`2z5k1GGd7;@w(2sf|5elg-U9bN7sDjNOl*g}u~ZQM z;)%_Pw=8t8<29_Sev7ETIu7ANd%6Y7;vv*be?tv$74^Wsn2xm;y9qvy+JYR^%10v~ zLvI@D`4UU`?+i>J9`&Ic{6^rPkGyj@hPcyGn)y6?`_r+T z;zDeV<=41xd_Qv>eogx#ROz33+nI;q#FJ1Hnr`h&tbMJ;8_gZ&m*)4rU7SWGx`NvK zo7Nt*)(sSmdK(ff&Or5Vfhv7lYwwNP!a){~HK(Kc&9iv5#s2%&um_d!u=%}t!u$<2 zv5Tk*TsMQ?aVw88tKcB+*T+a)f>m)fCgWGw3$J2-9g7a@+<)VF*Sut=t#|FuVnZHy z7d64(F%ti<_@;RWAEZ6(UH7^+N4;kKP!rCzc%nHMqjW6Sa-qZ@nxB|^u_WyW%wy&m zR4Mi>@!vdP6!<~?Q-YC)+M*WE<@GiYdRH%8ix7chqQ$=3e5wJ$=Q=C?2t_uKuuW;owz zRU!&CP`sIh)rpgF7|O^j!F=Zx}RjGqqeM(*$efF&P9Iuz4uW4+irEw_b?wvwGTw)AF-AFSHnc>kdOL? z&&3*e*xGNJcQBUrux;*xbr0(KhNy|Q!BW@>wUvFXeK4xuaEr%T{L(h+-f|F)Bp*nfvcegNVB*h>I^iu_HL;D{mrLQ&*k}cG1i=n_i|%~#Vb*X z-!VT#Rpv8`FPgVdB@Nr|`jOh3yD)M13o}a zpb(Yt6zaF)ENX?3JKX&WsEMbbR+@$C-_h)5?b)b_KIz(hFOLgNU@R)ZRMdaRTW+qz zbmGc?R-fJE( zk6;VhkE0SL>~{T>iO^N{yFnH#?v1BnafiVmA|Ta|7X-+e=0S#hNn=6ClB=p!USud zX3jNNm>bOPs0r*xo$8-31jF~b??G`h0hK=uwKbW0slPt0ZD`2E?$&V@RwI7H;ytK| z7NS;g8%ty4=e9DlD(Zf!#SP46W*gK*JDL4`yU0b&XsS8i+Sg!N+P9&$V@-+i#+Q2oJlWdBE4!)S8?YNjvaeYg;naEJM&-Txl7lJge-WA{sb=_*+sbw=u<`n9q4 zo~SJyu2}E?%hq8TYM>36fZNRzDrCZO7@US6Evrq$fws;t7z_F-2ld&t# zJ7DksIU1_a5PQ%i%0La!)a-yt&>J;SKZ}Q;4($kwXP^e2jT&g9#d}a&P=w0!mwD?T z^;g4P8cJgL*Ur+Y2NLmqtYh&L=5wflC!?NUf-m7pRG#XGob^!mo1-TF5GrpM)c0n9 z&xIaXh??iCWQd^R)Sgc@s6kpl{qC9uXL*j9JfFP&=)xuknXk`$UW(&PP4J z)Y?~>o6Vi(0aT%mS^O6&-yMr%zjgU4p~g=~t7y~r@F<>P|tP5Ug-C;hL6mz@H970Sp4aCF40l5$h>U+XGVPQ`js`SU^IDZ zqs~-Q)C3O9|t59Fm&33;KlZk&seaAw6uoaulumbJ9 zQ5778s=#DSW_)iR7o}*}iIwp%>O=NtpaZKo<`O@E8YmkZ;!D^AKS3ofeVqRl1{1Lt zeuVd8%#Y4w)cqEiiN~-O<9jhbxkJ$eQ;EA_F8Y{&S22+2XE*a?R7o3QIqZOK@d<2= z>rtouFH{BpLrpOF1V4NjgZv5O`RFg?qV-ApAFD7+9e!~u>xyNF*J3PwVje~%EMf~&dM*+5w$(UA{nb#H2JJ~hd=Wch3EYQTNg=A=35$P2Rp7FD z2Q`uCB9|ut6{lmMQdohwgWVsDWr@cY`L4usXwU=inID_`P>1TM#W&5+UtRxLvl40n zsi>`LYVEzO{YlhTjkfkz%_Tk;n&Dd1O1GMOP%A!&-LMcfKn>nawLfUKKux?Y>V6MY zzQGuQFQD>HLT$w?)M1~46VPADMFTF9eshVk&8JX_^6){Nf*N2KD&Y~k|Fd}sRnprQ z$DMKgs-W_unT=5s?&$QrN4cm)LymQri%G<*TnFz9YcE6%aN6R3%*fx}K;=;rybmj4 zCTgJW);<6O3&L>Pr(mSs|5;pw&@eC1z;6JC6TgK@u+IF*+V`3V%p>M;97F$8NLnxZ ztjqf&s$wTG9M54j{1a2vvFtg%PS^r3<6>-!GtTpmqj&_2~0Ye8R4jZ1AIBM9Ck)s9`9$4-l2Z08Z`q2F z$ddI%S(>tw?AaoslJNfi&$-?{efpm3oO7RZo#lR>nQ6wckSj++{L?X^AA0=LIK=Zx zV432c*E!VlDyON|^FC?jd6RJg4#$+{p7%B`!_io?h3EYg6pdK1Sgz)PqYg0>3i1 zqXs&Nti(HSUPUeF7UsuCSQPU=>z*%%Y_^w%d6?g;LqQ2N!5Hj{;W!xMa0DuWkF0(X zCK0bdJ$DebL)TG>-@<6ThgwkHww{*@OJQCtYgR+Q02TEqDAT902X;oSU@Io$0jz*` z@l`C9>3KtNDl&%`@|@?<Ie6zcQOeMN|9)b*2|l zi4|(^R-TNy3pKDHK8s4QH)`cssByAUN93c9?p-W`vr#*^+UmEU`t5Je{%fEU*5NXi zA-;p!s@M*0pfXs6I2H3?chms=QT>OZwtOz?$iBeB_#N^>d1p{7zmMAK;OAXJF+K%N z6pz}H3aE}%P%BGAO;p$7=BS-{5|v12jK@Bxf#1YxIK$d^qsGgz_G4Cm8MR~nZ3^0f zzfm2-Iyxg!0~SJUWf@dLsi;d<2a94O)E0L@CG;w4oNQF0qcIlWNA1uGEQXtr1^eC+ z3VQH_dBMD4{%r<#a&JWhYM{bc7n4yFc0(oB2bK7%sJF;RjXw!>x8BDT+=iig|8G&y zR{V+D>VHuK<$1wb0QI`XVsmVOTG=?%%0EQy&her97oc`%BdXs4)bl4$17An2{4dl_JwhcM-OXLz z;;4j5p?0EjH{Vs%q(TEWK|RP}oi?f3)KPKEm2-NZ4dj`668tD;`F$50P6!A;l_HE7cZx=uj7Ept)Nt-=)i3N_zF^tFP&C}_ZtzOG{eYT&XKr(ig78Y=V0Fc-E$ z^~=Cy?1;fQ#vG4H#1qYB_$2XutcJ1uIDci-svqw@4#$?5gJBr=lAACIm1qiX!1_2G z?_xtAhEk zHs-}AEN+c~9kBW_s6^gHUFvDL0vF;d*k*wHy5Hhc(16=90uP|J_9SY8v#6c8j*<8W z>XHR9irR~zJ~>OHR#w;ICaAmA0oA`RMxl>7k}3a>zoejvx1ti+j~e(mmcZ4Kp;!rCtXZ5X7JM=s%k)9U6Z0)b1<{4x06l7t(HEfiJGVuR=|d+qw0rBY!vGDFGeloYt%eHV>DjJ!2ACoP~f}W88FjY17L5!G)o7QoL@&wqE3zP&;q~eSNDvqM-M- z_z+t$YJjq+1gc>SK8D)D4AhFdqIM<=)o(ne;xyFG?ZUvFLM3_}b+qTK{n8N5UjyH; zhM+7raW2#Y(WrrAQMW%CwZb&i%Icz4))3XdIV!<6n1WqU&rQLCILBOt`h?t>#s2Fo zPf?*Qxod_EbsdVLwk!qp+B7y_#Hz#-t$iJ8C-$H&?J@HXYA2#!a|stS<53Bh@hRvC zs-Siv9UEdNEQhnP4DLWpcp0?=MPGLVlrYPpR$3LcyfCA6R`$J%)Nc&Y+Is5o%|mv)urdP!Co|^{-=bBh;CTJk4K?5{EP{uyGG0S{P81vA>T994{%O<^ zwL>lBMXC3{KLu^^VAO=Kqh6Cy>VRWVTRI7q$cLzv&PUyu}YvZqA92yN%yUxEo$J77WXt?LQOOXwSsJmC!!Lcisf-0YA1H1`t3vQ)Cu!9 zEKcl=b~_S>+Ce{+f-Y4f)S0xxz*(d2zyQ?Bvrs!U2}|NU)CxDD`tQM3c*5ecV_X8Y z@ek@-p~ioFtedzE^1Sc8KtU_-i%MjWIRf>-1k_f|KxMoDwZbn@TfGVO{C4wu)Da#+ z^}B!?=Qe8mkZ~@tNGzapNuZEGLk-jxwnd#;2df{AdcPN-&Tc)Hz#P;>H_V4vlQ?R; z+mWW2MBEN_m$FeuH3bv#Yb?k7-f;^3Fld6iz5P)StilDj3x{E!H|-M=OA|lBZ?N=4 z&wBw+V;0t*q^Kp!# zy%lQW=P?+&U#HH^o%P%Bx2dONnFoRz@3{}SIMlq&eG2(0JcF93 zyTwCLpJ3xK0q3Lc#8y-Shfxz>Mm_IMb1RERjZ*@(6IIPxsN4TI_Q!P8c>Y=n+SQSA4G~ z1r5{^qcIZ;U@z2Vc^$RF_fP|UV(|)8|4pcs?MEeY6qUe5)Pw~-aBoWjs$Wx#!PZz@ z?|*j+ns6i*#3>k#3sGCV9Cg+^Fc+S~z|LS`XHfkkKXi!}LG?>UU9!rkol8aauaBCi z6$bwP*M)*EM<3J*vrw6iM!o-U`IqJU)%R&*sv+i;flN@mmZ2KA(%h3%+UrACGicy~y4E zmlnH;-a(z+91O*ks2y92I)bgJquh(7@TAq>$BM+E|8b_EzQj8IhxA5M7)C`iyoV*Q z;it|Qa6fT2YRg-E=FCLxOm|d5eXV|k)laf`syWA8YOXVP_!KnI52!OgY7J-28>qM7 zzQu)>xc-T#x1u~Mu{x-uYi4mLvoEUOP>Uy6{JzEhLJCS?rMb@BZ0F9+zQz{0X~ZrDcI%bbRk!SMc_k5zF0gKu=*^I=+ia@V}S` z_gj3_JdO3JzleHG6IQs_t05}kwib6YvrwNClQ8h}|BOI^Ka-h@F@g@u&GqIk)K>mz z@i~icq7r+AT4|oo-SaifhN$|dEFNl(#dy6QQ?23))LCytZS5h8f3SPM zT_WMA1S8B?%uAe%;aDAYS?jIj{Z}EwI`&3wNbzVTDZj8e=#qiR{WdA|Cm91 zZYNV8j;~+}s^2`+0+;&~G~pK1P8={VpuYeAK|N4pwR@m6Dz0Tdfl4&P;`SE5X!f`I z!KitBi>LlSoOVFv~#G|!vATl)ixqu08AB~cSrG#|7246`%p?!073v1nGs*PIMz%?O^|9fxBBN%iS)MEH{X?d|7TLr1fN>N z=N5m1k<@QPo!vpJ{{@xECG!tcKYOb_5;b0NGZFdb_R3ql9o26y`YIf;hV!V2u3G#L zsy&GBVi|?HlyPQl)JJq%l-&g1S;WJ)E!8$`czc^MrJEizf7}}Z-pLMjD~&|k40tvmN^5p zGm9+#-aLld(u)?~MNJUA(LJ9B^^;B!RDBv&#QLa@WWTpH48k}nhM~R#K0qa~2KC@p z)Yk7tt?-Jq|BFgI&nCChBvk*(W~$ZKLnYeO>NAl9e6JG)4bU6)yPa>2#j3;;%(d8= z_&hek#+#jEu?g`G?2TbtTml173&^r~49+B;g#9t;TYV65{!=I@L?yV4b;x!&er~# z#pBF5sOP`3_C4ldEKdD7)Hn~UK6HnhrvU0FqWB%`zb0-$gBzL;6cOhzSI#cX7@^(pAG^fq6!hBvV=^)pdNveMjRo;CkLC06ji zZsG*gO3Rtm&HAW*&9DqUYq9@21x+v>!*H%OEH=Ni`Yosx>_Y9-F^jLD2L9K~v)jc* z%+jdms-YJ8gvD)LyYKaO1#bv8q{Cv}o8?~ZC7N55EYZyoUZHuG!xQPp!NoI=K z05xt)Gb149-`*;^pfc@+rEwT)Wpm7>*1it4lD!rmxAyC(oxF#-BQbkjzhqQ>E!2^= zvACDoncsVZf+m`RTG4FtYgEQRS^Ei#f3^4qYQXyz7x>P7N5o@1_02I6d!f#JB5J%9 zm#@aX1$BeAoeJK~#GJD)DltajIi2Y>etZ z%%`A?CtyYV7&XBT)Y+aw4Rjmzz&(q@4!ZhCvyd5QCZYOQFl(7lm>H;f{mxb~5c5(o z%A90QH|Jq~+LxkMw87kA?l+I35<_Bj%GXY5;*sC%ZLp_&-eX+j9i*vY!YS>JL)^-Gy$Qe}b zznXVY69xU~>I<2PW>vGknT{IoIn;Pvt-d$rA|8U;^XwnFhLtIdw+>&T653+%QPcnz z%xf4zeB0tb%?D=aPp*GH)B<8qx2Y6{Voj^BhcU!0d@FQBO+3JS6E(nma|P;w4dy;n zV!v2?3Dy4|YMih`E-r<7t}?dAh8BNh`pYPsr(v^IEI8~QTx)JK511#-%jR7(_=x)x zWPa2gh({$@)vSq1uz}gq>3i+0q9^Ksffm1Rjz@i%PP6tksL!x(P#0c_z#?c-f_3J zA7L@Vn`Zb4*Pe*As9%p&)Q*L*~(@3PZ3f>TBpH3=D+IJp7c~(l{(aTnW>$ z2{yxbP`5n?wSy;737*4O@h0-qir4jLKIX9OY4^XUwxHic9VlpJHGXmbZ(|A$kkOhrEY47HLqsE(U0 z-if+Y2h7u`L~fu4x^HpRS=%WrL474udpc^%JE3-X5bF8KXMIos|4cr}d6#Y@ReGtBZW3dhfU2x<0^{mhWHBctj!xvE#%tsBl+S)gn z`%qhY%HrFoe!&;rI1y$ERKk_bx~R9Hxoh{mEDDKKjJFP-x(06zYJwdWA2+X{Cc1}8 zFyxZ`qK%p;4OQP50}H}1;ulfR^~Vq#8i+anYzkpij6)6Zw)v6OFE*E(tIZ9VP5-UP zFkZc1-N4_VCfb5wxChJPL9B##u>&Sv<^t=%;S}^f_q)Q!Bd*4(coVf%@mJlgeG0X* zzNkb;n3M2v;tw$s51|sMaLx5kMZNdUQT4xI5S9$~yy;EThQXRGY005Mjn`bP^G--m jqcwvXgmsxdan`lmH501Wu2-|xn(W!XwV6KT+M@pfK0^rL diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index eff28a6d0..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: 2019-01-15 17:56+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" @@ -67,8 +67,8 @@ msgstr "网域" #: assets/templates/assets/asset_update.html:35 perms/forms.py:45 #: perms/forms.py:52 perms/models.py:85 #: perms/templates/perms/asset_permission_list.html:57 -#: perms/templates/perms/asset_permission_list.html:77 -#: perms/templates/perms/asset_permission_list.html:126 +#: 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 @@ -119,7 +119,7 @@ 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:123 +#: 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 @@ -185,7 +185,7 @@ 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:76 +#: 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:53 #: users/templates/users/_select_user_modal.html:14 @@ -277,7 +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:75 +#: 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" @@ -637,7 +637,7 @@ 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:117 templates/index.html:87 +#: 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 @@ -724,7 +724,8 @@ msgstr "登录模式" #: 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:129 templates/_nav.html:25 +#: 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 @@ -884,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 "其它" @@ -906,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 @@ -941,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 @@ -1025,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:175 +#: 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 @@ -1060,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:176 +#: 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 @@ -1173,7 +1174,6 @@ msgstr "快速修改" #: 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:73 #: terminal/templates/terminal/terminal_list.html:34 #: users/templates/users/_select_user_modal.html:18 #: users/templates/users/user_detail.html:144 @@ -2572,7 +2572,8 @@ msgstr "组织管理" #: 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:120 templates/_nav.html:14 +#: 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 @@ -2658,6 +2659,7 @@ 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 @@ -3545,6 +3547,10 @@ msgstr "用户认证源来自 {}, 请去相应系统修改密码" 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 "首页" diff --git a/apps/static/js/jumpserver.js b/apps/static/js/jumpserver.js index 9a570d841..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); } }); diff --git a/apps/terminal/api/v2/terminal.py b/apps/terminal/api/v2/terminal.py index 3fd42a6ab..7e7a61c2a 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 = request.data + 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/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/serializers/v1.py b/apps/terminal/serializers/v1.py index ac3324097..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() 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/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/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/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 From b2717133ee932b4798b17768994a88b28ca44339 Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 17 Jan 2019 18:06:47 +0800 Subject: [PATCH 10/14] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jumpserver/settings.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index 24e4ea7f5..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/ @@ -216,14 +224,14 @@ LOGGING = { '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': { From 6e061d2da55f8dbbd874f08b6acbe42838a5b551 Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 17 Jan 2019 18:39:38 +0800 Subject: [PATCH 11/14] =?UTF-8?q?[Bugfix]=20=E4=BF=AE=E6=94=B9=E5=B0=8Fbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/terminal/api/v2/terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/terminal/api/v2/terminal.py b/apps/terminal/api/v2/terminal.py index 7e7a61c2a..11157a0df 100644 --- a/apps/terminal/api/v2/terminal.py +++ b/apps/terminal/api/v2/terminal.py @@ -25,7 +25,7 @@ class TerminalRegistrationApi(generics.CreateAPIView): http_method_names = ['post'] def create(self, request, *args, **kwargs): - data = request.data + data = {k: v for k, v in request.data.items()} serializer = serializers.TerminalSerializer( data=data, context={'request': request} ) From 0f32e788913e2e05bcea67b87c9c9508666c220a Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 17 Jan 2019 20:26:47 +0800 Subject: [PATCH 12/14] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9dockerfile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 54b094a18..3742a47f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,7 @@ RUN yum -y install epel-release openldap-clients telnet && cd /tmp/requirements RUN cd /tmp/requirements && pip install -r requirements.txt COPY . /opt/jumpserver +RUN echo > config.yml VOLUME /opt/jumpserver/data VOLUME /opt/jumpserver/logs From d664018bd70f00e30fb0a79ec0e7ab75ecbe2c94 Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 17 Jan 2019 20:32:10 +0800 Subject: [PATCH 13/14] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9config.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jumpserver/conf.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index c940337da..c0d94ee69 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -200,7 +200,10 @@ class Config(dict): 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) + else: + return False def from_mapping(self, *mapping, **kwargs): """Updates the config like :meth:`update` ignoring items with non-upper From d8069f47f2b8928c0f730adeb413fffe43a8850a Mon Sep 17 00:00:00 2001 From: ibuler Date: Thu, 17 Jan 2019 20:38:56 +0800 Subject: [PATCH 14/14] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=96=87=E4=BB=B6=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/jumpserver/conf.py | 7 +++---- jms | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index c0d94ee69..7a749cd77 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -193,8 +193,8 @@ 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 @@ -202,8 +202,7 @@ class Config(dict): raise if obj: return self.from_mapping(obj) - else: - return False + return True def from_mapping(self, *mapping, **kwargs): """Updates the config like :meth:`update` ignoring items with non-upper diff --git a/jms b/jms index 0c2ae8fd3..a5d105c75 100755 --- a/jms +++ b/jms @@ -18,7 +18,7 @@ try: 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"