Merge pull request #2364 from jumpserver/dev

Dev
pull/2486/head v1.4.7
老广 2019-01-29 17:45:31 +08:00 committed by GitHub
commit 3b56027edc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 1424 additions and 1032 deletions

1
.gitignore vendored
View File

@ -17,6 +17,7 @@ dump.rdb
.idea/
db.sqlite3
config.py
config.yml
*.log
host_rsa_key
*.bat

View File

@ -6,13 +6,13 @@ RUN useradd jumpserver
COPY ./requirements /tmp/requirements
RUN yum -y install epel-release && cd /tmp/requirements && \
RUN yum -y install epel-release openldap-clients telnet && cd /tmp/requirements && \
yum -y install $(cat rpm_requirements.txt)
RUN cd /tmp/requirements && pip install -r requirements.txt
COPY . /opt/jumpserver
COPY config_docker.py /opt/jumpserver/config.py
RUN echo > config.yml
VOLUME /opt/jumpserver/data
VOLUME /opt/jumpserver/logs

View File

@ -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()

View File

@ -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')

View File

@ -43,6 +43,23 @@ class NodeViewSet(viewsets.ModelViewSet):
permission_classes = (IsOrgAdmin,)
serializer_class = serializers.NodeSerializer
def perform_create(self, serializer):
child_key = Node.root().get_next_child_key()
serializer.validated_data["key"] = child_key
serializer.save()
def update(self, request, *args, **kwargs):
node = self.get_object()
if node.is_root():
node_value = node.value
post_value = request.data.get('value')
if node_value != post_value:
return Response(
{"msg": _("You can't update the root node name")},
status=400
)
return super().update(request, *args, **kwargs)
class NodeListAsTreeApi(generics.ListAPIView):
"""
@ -259,7 +276,7 @@ class RefreshNodeHardwareInfoApi(APIView):
def get(self, request, *args, **kwargs):
node_id = kwargs.get('pk')
node = get_object_or_404(self.model, id=node_id)
assets = node.assets.all()
assets = node.get_all_assets()
# task_name = _("更新节点资产硬件信息: {}".format(node.name))
task_name = _("Update node asset hardware information: {}").format(node.name)
task = update_assets_hardware_info_util.delay(assets, task_name=task_name)
@ -273,7 +290,7 @@ class TestNodeConnectiveApi(APIView):
def get(self, request, *args, **kwargs):
node_id = kwargs.get('pk')
node = get_object_or_404(self.model, id=node_id)
assets = node.assets.all()
assets = node.get_all_assets()
# task_name = _("测试节点下资产是否可连接: {}".format(node.name))
task_name = _("Test if the assets under the node are connectable: {}".format(node.name))
task = test_asset_connectivity_util.delay(assets, task_name=task_name)

View File

@ -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()

View File

@ -185,7 +185,7 @@ class Asset(OrgModelMixin):
@property
def connectivity(self):
if not self.is_unixlike():
return self.UNKNOWN
return self.REACHABLE
key = self.CONNECTIVITY_CACHE_KEY.format(str(self.id))
cached = cache.get(key, None)
return cached if cached is not None else self.UNKNOWN

View File

@ -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)

View File

@ -1,16 +1,18 @@
# ~*~ coding: utf-8 ~*~
import json
import re
import time
import os
from celery import shared_task
from django.utils.translation import ugettext as _
from django.core.cache import cache
from common.utils import capacity_convert, \
sum_capacity, encrypt_password, get_logger
from ops.celery.utils import register_as_period_task, after_app_shutdown_clean
from common.utils import (
capacity_convert, sum_capacity, encrypt_password, get_logger
)
from ops.celery.decorator import (
register_as_period_task, after_app_shutdown_clean_periodic
)
from .models import SystemUser, AdminUser, Asset
from . import const
@ -132,7 +134,7 @@ def update_assets_hardware_info_util(assets, task_name=None):
@shared_task
def update_asset_hardware_info_manual(asset):
task_name = _("Update asset hardware info: {}").format(asset.hostname)
return update_assets_hardware_info_util(
update_assets_hardware_info_util(
[asset], task_name=task_name
)
@ -221,12 +223,14 @@ def test_admin_user_connectivity_period():
for admin_user in admin_users:
task_name = _("Test admin user connectivity period: {}").format(admin_user.name)
test_admin_user_connectivity_util(admin_user, task_name)
cache.set(key, 1, 60*40)
@shared_task
def test_admin_user_connectivity_manual(admin_user):
task_name = _("Test admin user connectivity: {}").format(admin_user.name)
return test_admin_user_connectivity_util(admin_user, task_name)
test_admin_user_connectivity_util(admin_user, task_name)
return True
## System user connective ##
@ -394,13 +398,13 @@ def push_system_user_to_assets(system_user, assets):
@shared_task
@after_app_shutdown_clean
@after_app_shutdown_clean_periodic
def test_system_user_connectability_period():
pass
@shared_task
@after_app_shutdown_clean
@after_app_shutdown_clean_periodic
def test_admin_user_connectability_period():
pass
@ -408,7 +412,7 @@ def test_admin_user_connectability_period():
# @shared_task
# @register_as_period_task(interval=3600)
# @after_app_ready_start
# # @after_app_shutdown_clean
# @after_app_shutdown_clean_periodic
# def push_system_user_period():
# for system_user in SystemUser.objects.all():
# push_system_user_related_nodes(system_user)

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
#

View File

@ -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

View File

@ -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...\r\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})

View File

@ -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()

View File

@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
#
import json
from django.dispatch import receiver
from django.db.models.signals import post_save, pre_save
from django.conf import LazySettings, empty
@ -8,7 +10,7 @@ from django.core.cache import cache
from jumpserver.utils import current_request
from .models import Setting
from .utils import get_logger
from .utils import get_logger, ssh_key_gen
from .signals import django_ready
logger = get_logger(__file__)
@ -16,23 +18,25 @@ logger = get_logger(__file__)
@receiver(post_save, sender=Setting, dispatch_uid="my_unique_identifier")
def refresh_settings_on_changed(sender, instance=None, **kwargs):
logger.debug("Receive setting item change")
logger.debug(" - refresh setting: {}".format(instance.name))
if instance:
instance.refresh_setting()
@receiver(django_ready, dispatch_uid="my_unique_identifier")
def refresh_all_settings_on_django_ready(sender, **kwargs):
logger.debug("Receive django ready signal")
logger.debug(" - fresh all settings")
def monkey_patch_settings(sender, **kwargs):
cache_key_prefix = '_SETTING_'
uncached_settings = [
'CACHES', 'DEBUG', 'SECRET_KEY', 'INSTALLED_APPS',
'ROOT_URLCONF', 'TEMPLATES', 'DATABASES', '_wrapped',
'CELERY_LOG_DIR'
]
def monkey_patch_getattr(self, name):
key = cache_key_prefix + name
cached = cache.get(key)
if cached is not None:
return cached
if name not in uncached_settings:
key = cache_key_prefix + name
cached = cache.get(key)
if cached is not None:
return cached
if self._wrapped is empty:
self._setup(name)
val = getattr(self._wrapped, name)
@ -62,6 +66,18 @@ def refresh_all_settings_on_django_ready(sender, **kwargs):
pass
@receiver(django_ready)
def auto_generate_terminal_host_key(sender, **kwargs):
try:
if Setting.objects.filter(name='TERMINAL_HOST_KEY').exists():
return
private_key, public_key = ssh_key_gen()
value = json.dumps(private_key)
Setting.objects.create(name='TERMINAL_HOST_KEY', value=value)
except:
pass
@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:

View File

@ -35,7 +35,7 @@
<option value ="s3">s3</option>
<option value="oss">oss</option>
<option value ="azure">azure</option>
<option value="ceph">ceph</option>
{# <option value="ceph">ceph</option>#}
</select>
</div>
</div>
@ -108,15 +108,21 @@
<label class="col-md-2 control-label" for="id_endpoint">{% trans "Endpoint" %}</label>
<div class="col-md-9">
<input id="id_endpoint" class="form-control" type="text" name="ENDPOINT" value="" placeholder="Endpoint">
<div class="help-block">
<span class="oss">
{% trans 'OSS: http://{REGION_NAME}.aliyuncs.com' %}
<br>
{% trans 'Example: http://oss-cn-hangzhou.aliyuncs.com' %}
</span>
<span class="s3">{% trans 'S3: http://s3.{REGION_NAME}.amazonaws.com' %}<br></span>
<span class="s3">{% trans 'S3(China): http://s3.{REGION_NAME}.amazonaws.com.cn' %}<br></span>
<span class="s3">{% trans 'Example: http://s3.cn-north-1.amazonaws.com.cn' %}<br></span>
</div>
</div>
</div>
<div class="form-group" style="display: none;" >
<label class="col-md-2 control-label" for="id_endpoint_suffix">{% trans "Endpoint suffix" %}</label>
{# <div class="col-md-9">#}
{# <input id="id_endpoint_suffix" class="form-control" type="text" name="ENDPOINT_SUFFIX" value="">#}
{# <div class="help-block">{% trans '' %}</div>#}
{# </div>#}
<div class="col-md-9">
<select id="id_endpoint_suffix" name="ENDPOINT_SUFFIX" class="endpoint-suffix-selector form-control">
<option value="core.chinacloudapi.cn" selected="selected">core.chinacloudapi.cn</option>
@ -129,6 +135,13 @@
<label class="col-md-2 control-label" for="id_region">{% trans "Region" %}</label>
<div class="col-md-9">
<input id="id_region" class="form-control" type="text" name="REGION" value="" placeholder="">
<div class="help-block">
<span class="s3">
{% trans 'Beijing: cn-north-1' %}
{% trans 'Ningxia: cn-northwest-1' %}
<a href="https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html">{% trans 'More' %}</a>
</span>
</div>
</div>
</div>
@ -166,7 +179,6 @@ function hiddenField(field){
}
function getFieldByType(type){
if(type === 'server'){
return need_get_field_of_server
}
@ -211,15 +223,17 @@ $(document).ready(function() {
field_of_all = [name_id, host_id, port_id, bucket_id, access_key_id, secret_key_id, container_name_id, account_name_id, account_key_id, endpoint_id, endpoint_suffix_id, region_id];
need_get_field_of_server = [name_id];
need_get_field_of_s3 = [name_id, bucket_id, access_key_id, secret_key_id, region_id];
need_get_field_of_s3 = [name_id, bucket_id, access_key_id, secret_key_id, region_id, endpoint_id];
need_get_field_of_oss = [name_id, bucket_id, access_key_id, secret_key_id, endpoint_id];
need_get_field_of_azure = [name_id, container_name_id, account_name_id, account_key_id, endpoint_suffix_id];
need_get_field_of_ceph = [name_id, host_id, port_id, bucket_id, access_key_id, secret_key_id, region_id];
})
.on('change', '.selector', function(){
var type = $('.selector').val();
console.log(type);
$("." + type).show();
hiddenField(field_of_all);
$('.help-block').children().hide();
$('.help-block ' + '.' + type).show();
var field = getFieldByType(type);
showField(field)
})

View File

@ -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__)

View File

@ -193,14 +193,16 @@ class Config(dict):
if self.root_path:
filename = os.path.join(self.root_path, filename)
try:
with open(filename) as json_file:
obj = yaml.load(json_file)
with open(filename) as f:
obj = yaml.load(f)
except IOError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
return False
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
raise
return self.from_mapping(obj)
if obj:
return self.from_mapping(obj)
return True
def from_mapping(self, *mapping, **kwargs):
"""Updates the config like :meth:`update` ignoring items with non-upper
@ -278,6 +280,8 @@ class Config(dict):
return value
value = os.environ.get(item, None)
if value is not None:
if value.isdigit():
value = int(value)
return value
return self.defaults.get(item)
@ -286,8 +290,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 +316,7 @@ defaults = {
'SESSION_COOKIE_AGE': 3600 * 24,
'SESSION_EXPIRE_AT_BROWSER_CLOSE': False,
'AUTH_OPENID': False,
'OTP_VALID_WINDOW': 0,
'OTP_ISSUER_NAME': 'Jumpserver',
'EMAIL_SUFFIX': 'jumpserver.org',
'TERMINAL_PASSWORD_AUTH': True,
@ -320,6 +325,7 @@ defaults = {
'TERMINAL_ASSET_LIST_SORT_BY': 'hostname',
'TERMINAL_ASSET_LIST_PAGE_SIZE': 'auto',
'TERMINAL_SESSION_KEEP_DURATION': 9999,
'TERMINAL_HOST_KEY': '',
'SECURITY_MFA_AUTH': False,
'SECURITY_LOGIN_LIMIT_COUNT': 7,
'SECURITY_LOGIN_LIMIT_TIME': 30,
@ -330,21 +336,48 @@ defaults = {
'SECURITY_PASSWORD_LOWER_CASE': False,
'SECURITY_PASSWORD_NUMBER': False,
'SECURITY_PASSWORD_SPECIAL_CHAR': False,
'AUTH_RADIUS': False,
'RADIUS_SERVER': 'localhost',
'RADIUS_PORT': 1812,
'RADIUS_SECRET': '',
'HTTP_BIND_HOST': '0.0.0.0',
'HTTP_LISTEN_PORT': 8080,
}
def load_from_object(config):
try:
from config import config as c
config.from_object(c)
return True
except ImportError:
pass
return False
def load_from_yml(config):
for i in ['config.yml', 'config.yaml']:
if not os.path.isfile(os.path.join(config.root_path, i)):
continue
loaded = config.from_yaml(i)
if loaded:
return True
return False
def load_user_config():
sys.path.insert(0, PROJECT_DIR)
config = Config(PROJECT_DIR, defaults)
try:
from config import config as c
config.from_object(c)
except ImportError:
loaded = load_from_object(config)
if not loaded:
loaded = load_from_yml(config)
if not loaded:
msg = """
Error: No config file found.
You can run `cp config_example.py config.py`, and edit it.
You can run `cp config_example.yml config.yml`, and edit it.
"""
raise ImportError(msg)
return config

View File

@ -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,12 @@ 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')
JUMPSERVER_LOG_FILE = os.path.join(LOG_DIR, 'jumpserver.log')
ANSIBLE_LOG_FILE = os.path.join(LOG_DIR, 'ansible.log')
if not os.path.isdir(LOG_DIR):
os.makedirs(LOG_DIR)
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
@ -209,19 +216,21 @@ LOGGING = {
'formatter': 'main'
},
'file': {
'encoding': 'utf8',
'level': 'DEBUG',
'class': 'logging.handlers.TimedRotatingFileHandler',
'when': "D",
'interval': 1,
"backupCount": 7,
'formatter': 'main',
'filename': os.path.join(PROJECT_DIR, 'logs', 'jumpserver.log')
'filename': JUMPSERVER_LOG_FILE,
},
'ansible_logs': {
'encoding': 'utf8',
'level': 'DEBUG',
'class': 'logging.FileHandler',
'formatter': 'main',
'filename': os.path.join(PROJECT_DIR, 'logs', 'ansible.log')
'filename': ANSIBLE_LOG_FILE,
},
},
'loggers': {
@ -400,6 +409,19 @@ if AUTH_OPENID:
AUTHENTICATION_BACKENDS.insert(0, AUTH_OPENID_BACKENDS[0])
AUTHENTICATION_BACKENDS.insert(0, AUTH_OPENID_BACKENDS[1])
# Radius Auth
AUTH_RADIUS = CONFIG.AUTH_RADIUS
AUTH_RADIUS_BACKEND = 'authentication.radius.backends.RadiusBackend'
RADIUS_SERVER = CONFIG.RADIUS_SERVER
RADIUS_PORT = CONFIG.RADIUS_PORT
RADIUS_SECRET = CONFIG.RADIUS_SECRET
if AUTH_RADIUS:
AUTHENTICATION_BACKENDS.insert(0, AUTH_RADIUS_BACKEND)
# Dump all celery log to here
CELERY_LOG_DIR = os.path.join(PROJECT_DIR, 'data', 'celery')
# Celery using redis as broker
CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/%(db)s' % {
'password': CONFIG.REDIS_PASSWORD,
@ -413,14 +435,16 @@ CELERY_RESULT_BACKEND = CELERY_BROKER_URL
CELERY_ACCEPT_CONTENT = ['json', 'pickle']
CELERY_RESULT_EXPIRES = 3600
# CELERY_WORKER_LOG_FORMAT = '%(asctime)s [%(module)s %(levelname)s] %(message)s'
CELERY_WORKER_LOG_FORMAT = '%(message)s'
# CELERY_WORKER_TASK_LOG_FORMAT = '%(asctime)s [%(module)s %(levelname)s] %(message)s'
CELERY_WORKER_TASK_LOG_FORMAT = '%(message)s'
# CELERY_WORKER_LOG_FORMAT = '%(message)s'
CELERY_WORKER_TASK_LOG_FORMAT = '%(task_id)s %(task_name)s %(message)s'
# CELERY_WORKER_TASK_LOG_FORMAT = '%(message)s'
# CELERY_WORKER_LOG_FORMAT = '%(asctime)s [%(module)s %(levelname)s] %(message)s'
CELERY_WORKER_LOG_FORMAT = '%(message)s'
CELERY_TASK_EAGER_PROPAGATES = True
CELERY_REDIRECT_STDOUTS = True
CELERY_REDIRECT_STDOUTS_LEVEL = "INFO"
CELERY_WORKER_HIJACK_ROOT_LOGGER = False
CELERY_WORKER_REDIRECT_STDOUTS = True
CELERY_WORKER_REDIRECT_STDOUTS_LEVEL = "INFO"
# CELERY_WORKER_HIJACK_ROOT_LOGGER = False
CELERY_WORKER_MAX_TASKS_PER_CHILD = 40
# Cache use redis
CACHES = {
@ -492,6 +516,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 = {

Binary file not shown.

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Jumpserver 0.3.3\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-12-27 15:48+0800\n"
"POT-Creation-Date: 2019-01-28 12:56+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: ibuler <ibuler@qq.com>\n"
"Language-Team: Jumpserver team<ibuler@qq.com>\n"
@ -17,11 +17,15 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: assets/api/node.py:261
#: assets/api/node.py:58
msgid "You can't update the root node name"
msgstr "不能修改根节点名称"
#: assets/api/node.py:281
msgid "Update node asset hardware information: {}"
msgstr "更新节点资产硬件信息: {}"
#: assets/api/node.py:275
#: assets/api/node.py:295
msgid "Test if the assets under the node are connectable: {}"
msgstr "测试节点下资产是否可连接: {}"
@ -65,9 +69,10 @@ msgstr "网域"
#: assets/forms/asset.py:124 assets/models/node.py:31
#: assets/templates/assets/asset_create.html:30
#: assets/templates/assets/asset_update.html:35 perms/forms.py:45
#: perms/forms.py:52 perms/models.py:79
#: perms/forms.py:52 perms/models.py:85
#: perms/templates/perms/asset_permission_list.html:57
#: perms/templates/perms/asset_permission_list.html:117
#: perms/templates/perms/asset_permission_list.html:78
#: perms/templates/perms/asset_permission_list.html:128
#: xpack/plugins/cloud/models.py:123
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:63
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:66
@ -118,8 +123,8 @@ msgstr "端口"
#: perms/models.py:31
#: perms/templates/perms/asset_permission_create_update.html:45
#: perms/templates/perms/asset_permission_list.html:56
#: perms/templates/perms/asset_permission_list.html:114
#: terminal/backends/command/models.py:13 terminal/models.py:141
#: perms/templates/perms/asset_permission_list.html:125
#: terminal/backends/command/models.py:13 terminal/models.py:155
#: terminal/templates/terminal/command_list.html:40
#: terminal/templates/terminal/command_list.html:73
#: terminal/templates/terminal/session_list.html:41
@ -156,10 +161,11 @@ msgstr "不能包含特殊字符"
#: orgs/models.py:12 perms/models.py:28
#: perms/templates/perms/asset_permission_detail.html:62
#: perms/templates/perms/asset_permission_list.html:53
#: perms/templates/perms/asset_permission_user.html:54 terminal/models.py:20
#: terminal/models.py:198 terminal/templates/terminal/terminal_detail.html:43
#: perms/templates/perms/asset_permission_list.html:72
#: perms/templates/perms/asset_permission_user.html:54 terminal/models.py:22
#: terminal/models.py:233 terminal/templates/terminal/terminal_detail.html:43
#: terminal/templates/terminal/terminal_list.html:29 users/models/group.py:14
#: users/models/user.py:53 users/templates/users/_select_user_modal.html:13
#: users/models/user.py:55 users/templates/users/_select_user_modal.html:13
#: users/templates/users/user_detail.html:63
#: users/templates/users/user_group_detail.html:55
#: users/templates/users/user_group_list.html:12
@ -183,8 +189,9 @@ msgstr "名称"
#: assets/templates/assets/system_user_detail.html:62
#: assets/templates/assets/system_user_list.html:30
#: audits/templates/audits/login_log_list.html:49
#: perms/templates/perms/asset_permission_list.html:74
#: perms/templates/perms/asset_permission_user.html:55 users/forms.py:15
#: users/forms.py:33 users/models/authentication.py:77 users/models/user.py:51
#: users/forms.py:33 users/models/authentication.py:77 users/models/user.py:53
#: users/templates/users/_select_user_modal.html:14
#: users/templates/users/login.html:64
#: users/templates/users/user_detail.html:67
@ -210,7 +217,7 @@ msgstr "密码或密钥密码"
msgid "Password"
msgstr "密码"
#: assets/forms/user.py:29 users/models/user.py:80
#: assets/forms/user.py:29 users/models/user.py:82
msgid "Private key"
msgstr "ssh私钥"
@ -274,6 +281,7 @@ msgstr "IP"
#: assets/templates/assets/user_asset_list.html:45
#: assets/templates/assets/user_asset_list.html:150 common/forms.py:130
#: perms/templates/perms/asset_permission_asset.html:54
#: perms/templates/perms/asset_permission_list.html:77
#: users/templates/users/user_granted_asset.html:44
#: users/templates/users/user_group_granted_asset.html:44
msgid "Hostname"
@ -381,8 +389,8 @@ msgstr "标签管理"
#: assets/templates/assets/domain_detail.html:72
#: assets/templates/assets/system_user_detail.html:100
#: ops/templates/ops/adhoc_detail.html:86 orgs/models.py:15 perms/models.py:37
#: perms/models.py:84 perms/templates/perms/asset_permission_detail.html:98
#: users/models/user.py:94 users/templates/users/user_detail.html:111
#: perms/models.py:90 perms/templates/perms/asset_permission_detail.html:98
#: users/models/user.py:96 users/templates/users/user_detail.html:111
#: xpack/plugins/cloud/models.py:55 xpack/plugins/cloud/models.py:127
msgid "Created by"
msgstr "创建者"
@ -394,7 +402,7 @@ msgstr "创建者"
#: assets/templates/assets/domain_detail.html:68
#: assets/templates/assets/system_user_detail.html:96
#: ops/templates/ops/adhoc_detail.html:90 ops/templates/ops/task_detail.html:64
#: orgs/models.py:16 perms/models.py:38 perms/models.py:85
#: orgs/models.py:16 perms/models.py:38 perms/models.py:91
#: perms/templates/perms/asset_permission_detail.html:94
#: terminal/templates/terminal/terminal_detail.html:59 users/models/group.py:17
#: users/templates/users/user_group_detail.html:63
@ -422,9 +430,9 @@ msgstr "创建日期"
#: assets/templates/assets/system_user_list.html:37
#: assets/templates/assets/user_asset_list.html:159 common/models.py:34
#: ops/models/adhoc.py:43 orgs/models.py:17 perms/models.py:39
#: perms/models.py:86 perms/templates/perms/asset_permission_detail.html:102
#: terminal/models.py:30 terminal/templates/terminal/terminal_detail.html:63
#: users/models/group.py:15 users/models/user.py:86
#: perms/models.py:92 perms/templates/perms/asset_permission_detail.html:102
#: terminal/models.py:32 terminal/templates/terminal/terminal_detail.html:63
#: users/models/group.py:15 users/models/user.py:88
#: users/templates/users/user_detail.html:127
#: users/templates/users/user_group_detail.html:67
#: users/templates/users/user_group_list.html:14
@ -457,7 +465,7 @@ msgstr "可连接"
#: assets/models/asset.py:119 assets/models/base.py:36
msgid "Unknown"
msgstr ""
msgstr "未知"
#: assets/models/base.py:25
msgid "SSH private key"
@ -475,7 +483,7 @@ msgstr "带宽"
msgid "Contact"
msgstr "联系人"
#: assets/models/cluster.py:22 users/models/user.py:72
#: assets/models/cluster.py:22 users/models/user.py:74
#: users/templates/users/user_detail.html:76
msgid "Phone"
msgstr "手机"
@ -501,7 +509,7 @@ msgid "Default"
msgstr "默认"
#: assets/models/cluster.py:36 assets/models/label.py:14
#: users/models/user.py:439
#: users/models/user.py:441
msgid "System"
msgstr "系统"
@ -529,8 +537,8 @@ msgstr "BGP全网通"
msgid "Regex"
msgstr "正则表达式"
#: assets/models/cmd_filter.py:36 ops/models/command.py:19
#: ops/templates/ops/command_execution_list.html:60 terminal/models.py:147
#: assets/models/cmd_filter.py:36 ops/models/command.py:21
#: ops/templates/ops/command_execution_list.html:60 terminal/models.py:161
#: terminal/templates/terminal/command_list.html:55
#: terminal/templates/terminal/command_list.html:71
#: terminal/templates/terminal/session_detail.html:48
@ -633,13 +641,13 @@ msgstr "默认资产组"
#: perms/models.py:29
#: perms/templates/perms/asset_permission_create_update.html:41
#: perms/templates/perms/asset_permission_list.html:54
#: perms/templates/perms/asset_permission_list.html:108 templates/index.html:87
#: terminal/backends/command/models.py:12 terminal/models.py:140
#: perms/templates/perms/asset_permission_list.html:119 templates/index.html:87
#: terminal/backends/command/models.py:12 terminal/models.py:154
#: terminal/templates/terminal/command_list.html:32
#: terminal/templates/terminal/command_list.html:72
#: terminal/templates/terminal/session_list.html:33
#: terminal/templates/terminal/session_list.html:71 users/forms.py:303
#: users/models/user.py:33 users/models/user.py:427
#: users/models/user.py:33 users/models/user.py:429
#: users/templates/users/user_group_detail.html:78
#: users/templates/users/user_group_list.html:13 users/views/user.py:386
#: xpack/plugins/orgs/forms.py:26
@ -717,11 +725,12 @@ msgstr "登录模式"
#: assets/models/user.py:247 assets/templates/assets/user_asset_list.html:156
#: audits/models.py:19 audits/templates/audits/ftp_log_list.html:49
#: audits/templates/audits/ftp_log_list.html:72 perms/forms.py:48
#: perms/models.py:33 perms/models.py:81
#: perms/models.py:33 perms/models.py:87
#: perms/templates/perms/asset_permission_detail.html:140
#: perms/templates/perms/asset_permission_list.html:58
#: perms/templates/perms/asset_permission_list.html:120 templates/_nav.html:25
#: terminal/backends/command/models.py:14 terminal/models.py:142
#: perms/templates/perms/asset_permission_list.html:79
#: perms/templates/perms/asset_permission_list.html:131 templates/_nav.html:25
#: terminal/backends/command/models.py:14 terminal/models.py:156
#: terminal/templates/terminal/command_list.html:48
#: terminal/templates/terminal/command_list.html:74
#: terminal/templates/terminal/session_list.html:49
@ -735,68 +744,68 @@ msgstr "系统用户"
msgid "%(value)s is not an even number"
msgstr "%(value)s is not an even number"
#: assets/tasks.py:31
#: assets/tasks.py:33
msgid "Asset has been disabled, skipped: {}"
msgstr "资产或许不支持ansible, 跳过: {}"
#: assets/tasks.py:35
#: assets/tasks.py:37
msgid "Asset may not be support ansible, skipped: {}"
msgstr "资产或许不支持ansible, 跳过: {}"
#: assets/tasks.py:40
#: assets/tasks.py:42
msgid "No assets matched, stop task"
msgstr "没有匹配到资产,结束任务"
#: assets/tasks.py:65
#: assets/tasks.py:67
msgid "Get asset info failed: {}"
msgstr "获取资产信息失败:{}"
#: assets/tasks.py:115
#: assets/tasks.py:117
msgid "Update some assets hardware info"
msgstr "更新资产硬件信息"
#: assets/tasks.py:134
#: assets/tasks.py:136
msgid "Update asset hardware info: {}"
msgstr "更新资产硬件信息: {}"
#: assets/tasks.py:159
#: assets/tasks.py:161
msgid "Test assets connectivity"
msgstr "测试资产可连接性"
#: assets/tasks.py:183
#: assets/tasks.py:185
msgid "Test assets connectivity: {}"
msgstr "测试资产可连接性: {}"
#: assets/tasks.py:222
#: assets/tasks.py:224
msgid "Test admin user connectivity period: {}"
msgstr "定期测试管理账号可连接性: {}"
#: assets/tasks.py:228
#: assets/tasks.py:231
msgid "Test admin user connectivity: {}"
msgstr "测试管理行号可连接性: {}"
#: assets/tasks.py:266
#: assets/tasks.py:270
msgid "Test system user connectivity: {}"
msgstr "测试系统用户可连接性: {}"
#: assets/tasks.py:273
#: assets/tasks.py:277
msgid "Test system user connectivity: {} => {}"
msgstr "测试系统用户可连接性: {} => {}"
#: assets/tasks.py:286
#: assets/tasks.py:290
msgid "Test system user connectivity period: {}"
msgstr "定期测试系统用户可连接性: {}"
#: assets/tasks.py:358
#: assets/tasks.py:362
msgid ""
"Push system user task skip, auto push not enable or protocol is not ssh: {}"
msgstr "推送系统用户任务跳过自动推送没有打开或协议不是ssh: {}"
#: assets/tasks.py:378 assets/tasks.py:392
#: assets/tasks.py:382 assets/tasks.py:396
msgid "Push system users to assets: {}"
msgstr "推送系统用户到入资产: {}"
#: assets/tasks.py:384
#: assets/tasks.py:388
msgid "Push system users to asset: {} => {}"
msgstr "推送系统用户到入资产: {} => {}"
@ -880,7 +889,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 "其它"
@ -898,11 +907,11 @@ msgstr "其它"
#: common/templates/common/command_storage_create.html:79
#: common/templates/common/email_setting.html:62
#: common/templates/common/ldap_setting.html:62
#: common/templates/common/replay_storage_create.html:138
#: common/templates/common/replay_storage_create.html:151
#: 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
@ -931,13 +940,13 @@ msgstr "重置"
#: common/templates/common/command_storage_create.html:80
#: common/templates/common/email_setting.html:63
#: common/templates/common/ldap_setting.html:63
#: common/templates/common/replay_storage_create.html:139
#: common/templates/common/replay_storage_create.html:152
#: common/templates/common/security_setting.html:71
#: common/templates/common/terminal_setting.html:70
#: perms/templates/perms/asset_permission_create_update.html:81
#: terminal/templates/terminal/command_list.html:103
#: terminal/templates/terminal/session_list.html:127
#: terminal/templates/terminal/terminal_update.html:48
#: terminal/templates/terminal/terminal_update.html:46
#: users/templates/users/_user.html:51
#: users/templates/users/forgot_password.html:45
#: users/templates/users/user_bulk_update.html:24
@ -1021,7 +1030,7 @@ msgstr "测试"
#: assets/templates/assets/system_user_detail.html:26
#: assets/templates/assets/system_user_list.html:92 audits/models.py:32
#: perms/templates/perms/asset_permission_detail.html:30
#: perms/templates/perms/asset_permission_list.html:166
#: perms/templates/perms/asset_permission_list.html:177
#: terminal/templates/terminal/terminal_detail.html:16
#: terminal/templates/terminal/terminal_list.html:71
#: users/templates/users/user_detail.html:25
@ -1056,7 +1065,7 @@ msgstr "更新"
#: common/templates/common/terminal_setting.html:112
#: ops/templates/ops/task_list.html:72
#: perms/templates/perms/asset_permission_detail.html:34
#: perms/templates/perms/asset_permission_list.html:167
#: perms/templates/perms/asset_permission_list.html:178
#: terminal/templates/terminal/terminal_list.html:73
#: users/templates/users/user_detail.html:30
#: users/templates/users/user_group_detail.html:32
@ -1166,10 +1175,9 @@ msgstr "快速修改"
#: assets/templates/assets/asset_detail.html:151
#: assets/templates/assets/user_asset_list.html:47 perms/models.py:34
#: perms/models.py:82
#: perms/models.py:88
#: perms/templates/perms/asset_permission_create_update.html:52
#: perms/templates/perms/asset_permission_detail.html:120
#: perms/templates/perms/asset_permission_list.html:59
#: terminal/templates/terminal/terminal_list.html:34
#: users/templates/users/_select_user_modal.html:18
#: users/templates/users/user_detail.html:144
@ -1657,7 +1665,7 @@ msgstr "系统用户资产"
#: audits/templates/audits/ftp_log_list.html:73
#: audits/templates/audits/operate_log_list.html:70
#: audits/templates/audits/password_change_log_list.html:52
#: terminal/models.py:144 terminal/templates/terminal/session_list.html:74
#: terminal/models.py:158 terminal/templates/terminal/session_list.html:74
#: terminal/templates/terminal/terminal_detail.html:47
msgid "Remote addr"
msgstr "远端地址"
@ -1700,7 +1708,7 @@ msgstr "修改者"
#: ops/templates/ops/adhoc_history_detail.html:61
#: ops/templates/ops/command_execution_list.html:65
#: ops/templates/ops/task_history.html:58 perms/models.py:35
#: perms/templates/perms/asset_permission_detail.html:86 terminal/models.py:151
#: perms/templates/perms/asset_permission_detail.html:86 terminal/models.py:165
#: terminal/templates/terminal/session_list.html:78
msgid "Date start"
msgstr "开始日期"
@ -1744,7 +1752,7 @@ msgid "City"
msgstr "城市"
#: audits/templates/audits/login_log_list.html:54 users/forms.py:162
#: users/models/authentication.py:82 users/models/user.py:75
#: users/models/authentication.py:82 users/models/user.py:77
#: users/templates/users/first_login.html:45
msgid "MFA"
msgstr "MFA"
@ -1801,33 +1809,33 @@ msgstr "登录日志"
msgid "Command execution list"
msgstr "命令执行列表"
#: common/api.py:22
#: common/api.py:27
msgid "Test mail sent to {}, please check"
msgstr "邮件已经发送{}, 请检查"
#: common/api.py:46
#: common/api.py:51
msgid "Test ldap success"
msgstr "连接LDAP成功"
#: common/api.py:76
#: common/api.py:81
msgid "Search no entry matched in ou {}"
msgstr "在ou:{}中没有匹配条目"
#: common/api.py:85
#: common/api.py:90
msgid "Match {} s users"
msgstr "匹配 {} 个用户"
#: common/api.py:108 common/api.py:144
#: common/api.py:113 common/api.py:149
msgid ""
"Error: Account invalid (Please make sure the information such as Access key "
"or Secret key is correct)"
msgstr "错误:账户无效 (请确保 Access key 或 Secret key 等信息正确)"
#: common/api.py:114 common/api.py:150
#: common/api.py:119 common/api.py:155
msgid "Create succeed"
msgstr "创建成功"
#: common/api.py:132 common/api.py:170
#: common/api.py:137 common/api.py:175
#: common/templates/common/terminal_setting.html:151
msgid "Delete succeed"
msgstr "删除成功"
@ -2194,23 +2202,58 @@ msgstr "账户密钥"
msgid "Endpoint"
msgstr "端点"
#: common/templates/common/replay_storage_create.html:113
#, python-brace-format
msgid "OSS: http://{REGION_NAME}.aliyuncs.com"
msgstr "OSS: http://{REGION_NAME}.aliyuncs.com"
#: common/templates/common/replay_storage_create.html:115
msgid "Example: http://oss-cn-hangzhou.aliyuncs.com"
msgstr "如: http://oss-cn-hangzhou.aliyuncs.com"
#: common/templates/common/replay_storage_create.html:117
#, python-brace-format
msgid "S3: http://s3.{REGION_NAME}.amazonaws.com"
msgstr "S3: http://s3.{REGION_NAME}.amazonaws.com"
#: common/templates/common/replay_storage_create.html:118
#, python-brace-format
msgid "S3(China): http://s3.{REGION_NAME}.amazonaws.com.cn"
msgstr "S3(中国): http://s3.{REGION_NAME}.amazonaws.com.cn"
#: common/templates/common/replay_storage_create.html:119
msgid "Example: http://s3.cn-north-1.amazonaws.com.cn"
msgstr "如: http://s3.cn-north-1.amazonaws.com.cn"
#: common/templates/common/replay_storage_create.html:125
msgid "Endpoint suffix"
msgstr "端点后缀"
#: common/templates/common/replay_storage_create.html:129
#: common/templates/common/replay_storage_create.html:135
#: xpack/plugins/cloud/models.py:186
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:83
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:64
msgid "Region"
msgstr "地域"
#: common/templates/common/replay_storage_create.html:140
msgid "Beijing: cn-north-1"
msgstr "北京: cn-north-1"
#: common/templates/common/replay_storage_create.html:141
msgid "Ningxia: cn-northwest-1"
msgstr "宁夏: cn-northwest-1"
#: common/templates/common/replay_storage_create.html:142
msgid "More"
msgstr "更多"
#: common/templates/common/security_setting.html:46
msgid "Password check rule"
msgstr "密码校验规则"
#: common/templates/common/terminal_setting.html:76 terminal/forms.py:27
#: terminal/models.py:24
#: terminal/models.py:26
msgid "Command storage"
msgstr "命令存储"
@ -2227,7 +2270,7 @@ msgid "Add"
msgstr "添加"
#: common/templates/common/terminal_setting.html:98 terminal/forms.py:32
#: terminal/models.py:25
#: terminal/models.py:27
msgid "Replay storage"
msgstr "录像存储"
@ -2272,10 +2315,6 @@ msgstr ""
"div><div>如果你看到了这个页面证明你访问的不是nginx监听的端口祝你好运</"
"div>"
#: ops/api/celery.py:32
msgid "Waiting ..."
msgstr ""
#: ops/models/adhoc.py:38
msgid "Interval"
msgstr "间隔"
@ -2320,52 +2359,60 @@ msgstr "Become"
msgid "Create by"
msgstr "创建者"
#: ops/models/adhoc.py:324
#: ops/models/adhoc.py:223
msgid "{} Start task: {}"
msgstr "{} 任务开始: {}"
#: ops/models/adhoc.py:226
msgid "{} Task finish"
msgstr "{} 任务结束"
#: ops/models/adhoc.py:323
msgid "Start time"
msgstr "开始时间"
#: ops/models/adhoc.py:325
#: ops/models/adhoc.py:324
msgid "End time"
msgstr "完成时间"
#: ops/models/adhoc.py:326 ops/templates/ops/adhoc_history.html:57
#: ops/models/adhoc.py:325 ops/templates/ops/adhoc_history.html:57
#: ops/templates/ops/task_history.html:63 ops/templates/ops/task_list.html:41
msgid "Time"
msgstr "时间"
#: ops/models/adhoc.py:327 ops/templates/ops/adhoc_detail.html:106
#: ops/models/adhoc.py:326 ops/templates/ops/adhoc_detail.html:106
#: ops/templates/ops/adhoc_history.html:55
#: ops/templates/ops/adhoc_history_detail.html:69
#: ops/templates/ops/task_detail.html:84 ops/templates/ops/task_history.html:61
msgid "Is finished"
msgstr "是否完成"
#: ops/models/adhoc.py:328 ops/templates/ops/adhoc_history.html:56
#: ops/models/adhoc.py:327 ops/templates/ops/adhoc_history.html:56
#: ops/templates/ops/task_history.html:62
msgid "Is success"
msgstr "是否成功"
#: ops/models/adhoc.py:329
#: ops/models/adhoc.py:328
msgid "Adhoc raw result"
msgstr "结果"
#: ops/models/adhoc.py:330
#: ops/models/adhoc.py:329
msgid "Adhoc result summary"
msgstr "汇总"
#: ops/models/command.py:20 xpack/plugins/cloud/models.py:170
#: ops/models/command.py:22 xpack/plugins/cloud/models.py:170
msgid "Result"
msgstr "结果"
#: ops/models/command.py:55
#: ops/models/command.py:57
msgid "Task start"
msgstr "任务开始"
#: ops/models/command.py:67
#: ops/models/command.py:71
msgid "Command `{}` is forbidden ........"
msgstr "命令 `{}` 不允许被执行 ......."
#: ops/models/command.py:73
#: ops/models/command.py:77
msgid "Task end"
msgstr "任务结束"
@ -2470,12 +2517,42 @@ msgstr "没有资产"
msgid "Success assets"
msgstr "成功资产"
#: ops/templates/ops/celery_task_log.html:4
msgid "Task log"
msgstr "任务列表"
#: ops/templates/ops/command_execution_create.html:71
#: terminal/templates/terminal/session_detail.html:91
#: terminal/templates/terminal/session_detail.html:100
msgid "Go"
msgstr ""
#: ops/templates/ops/command_execution_create.html:144
msgid "Selected assets"
msgstr "已选择资产"
#: ops/templates/ops/command_execution_create.html:147
msgid "In total"
msgstr "总共"
#: ops/templates/ops/command_execution_create.html:182
msgid ""
"Select the left asset, select the running system user, execute command in "
"batch"
msgstr "选择左侧资产, 选择运行的系统用户,批量执行命令"
#: ops/templates/ops/command_execution_create.html:200
msgid "Unselected assets"
msgstr "没有选中资产"
#: ops/templates/ops/command_execution_create.html:204
msgid "No input command"
msgstr "没有输入命令"
#: ops/templates/ops/command_execution_create.html:208
msgid "No system user was selected"
msgstr "没有选择系统用户"
#: ops/templates/ops/command_execution_create.html:253
msgid "Pending"
msgstr ""
@ -2560,10 +2637,11 @@ msgstr "命令执行"
msgid "Organization"
msgstr "组织管理"
#: perms/forms.py:39 perms/models.py:30 perms/models.py:80
#: perms/forms.py:39 perms/models.py:30 perms/models.py:86
#: perms/templates/perms/asset_permission_list.html:55
#: perms/templates/perms/asset_permission_list.html:111 templates/_nav.html:14
#: users/forms.py:273 users/models/group.py:26 users/models/user.py:59
#: perms/templates/perms/asset_permission_list.html:75
#: perms/templates/perms/asset_permission_list.html:122 templates/_nav.html:14
#: users/forms.py:273 users/models/group.py:26 users/models/user.py:61
#: users/templates/users/_select_user_modal.html:16
#: users/templates/users/user_detail.html:213
#: users/templates/users/user_list.html:26
@ -2579,14 +2657,14 @@ msgstr "用户和用户组至少选一个"
msgid "Asset or group at least one required"
msgstr "资产和节点至少选一个"
#: perms/models.py:36 perms/models.py:83
#: perms/models.py:36 perms/models.py:89
#: perms/templates/perms/asset_permission_detail.html:90
#: users/models/user.py:91 users/templates/users/user_detail.html:107
#: users/models/user.py:93 users/templates/users/user_detail.html:107
#: users/templates/users/user_profile.html:116
msgid "Date expired"
msgstr "失效日期"
#: perms/models.py:45 perms/models.py:92 templates/_nav.html:34
#: perms/models.py:45 perms/models.py:98 templates/_nav.html:34
msgid "Asset permission"
msgstr "资产授权"
@ -2647,6 +2725,14 @@ msgstr "选择系统用户"
msgid "Create permission"
msgstr "创建授权规则"
#: perms/templates/perms/asset_permission_list.html:59
#: perms/templates/perms/asset_permission_list.html:73
#: users/templates/users/user_list.html:28 xpack/plugins/cloud/models.py:53
#: xpack/plugins/cloud/templates/cloud/account_detail.html:60
#: xpack/plugins/cloud/templates/cloud/account_list.html:14
msgid "Validity"
msgstr "有效"
#: perms/templates/perms/asset_permission_user.html:35
msgid "User list of "
msgstr "用户列表"
@ -2801,7 +2887,7 @@ msgstr ""
#: users/views/group.py:60 users/views/group.py:76 users/views/group.py:92
#: users/views/login.py:349 users/views/user.py:68 users/views/user.py:83
#: users/views/user.py:113 users/views/user.py:194 users/views/user.py:355
#: users/views/user.py:405 users/views/user.py:444
#: users/views/user.py:405 users/views/user.py:445
msgid "Users"
msgstr "用户管理"
@ -2989,7 +3075,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 +3095,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 +3103,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 +3111,7 @@ msgstr "禁用主机"
#: templates/index.html:418 templates/index.html:470
msgid "Month not logged on host"
msgstr "月未登主机"
msgstr "月未登主机"
#: templates/rest_framework/base.html:128
msgid "Filters"
@ -3054,55 +3140,55 @@ msgstr ""
"录像文件支持存储到服务器端硬盘、AWS S3、 阿里云 OSS 中,默认存储到服务器端硬"
"盘, 更多查看文档"
#: terminal/models.py:21
#: terminal/models.py:23
msgid "Remote Address"
msgstr "远端地址"
#: terminal/models.py:22
#: terminal/models.py:24
msgid "SSH Port"
msgstr "SSH端口"
#: terminal/models.py:23
#: terminal/models.py:25
msgid "HTTP Port"
msgstr "HTTP端口"
#: terminal/models.py:111
#: terminal/models.py:125
msgid "Session Online"
msgstr "在线会话"
#: terminal/models.py:112
#: terminal/models.py:126
msgid "CPU Usage"
msgstr "CPU使用"
#: terminal/models.py:113
#: terminal/models.py:127
msgid "Memory Used"
msgstr "内存使用"
#: terminal/models.py:114
#: terminal/models.py:128
msgid "Connections"
msgstr "连接数"
#: terminal/models.py:115
#: terminal/models.py:129
msgid "Threads"
msgstr "线程数"
#: terminal/models.py:116
#: terminal/models.py:130
msgid "Boot Time"
msgstr "运行时间"
#: terminal/models.py:146 terminal/templates/terminal/session_list.html:104
#: terminal/models.py:160 terminal/templates/terminal/session_list.html:104
msgid "Replay"
msgstr "回放"
#: terminal/models.py:150
#: terminal/models.py:164
msgid "Date last active"
msgstr "最后活跃日期"
#: terminal/models.py:152
#: terminal/models.py:166
msgid "Date end"
msgstr "结束日期"
#: terminal/models.py:199
#: terminal/models.py:234
msgid "Args"
msgstr "参数"
@ -3259,7 +3345,7 @@ msgstr "请先进行用户名和密码验证"
msgid "MFA certification failed"
msgstr "MFA认证失败"
#: users/api/user.py:140
#: users/api/user.py:145
msgid "Could not reset self otp, use profile reset instead"
msgstr "不能再该页面重置MFA, 请去个人信息页面重置"
@ -3318,7 +3404,7 @@ msgstr ""
msgid "MFA code"
msgstr "MFA 验证码"
#: users/forms.py:52 users/models/user.py:63
#: users/forms.py:52 users/models/user.py:65
#: users/templates/users/_select_user_modal.html:15
#: users/templates/users/user_detail.html:87
#: users/templates/users/user_list.html:25
@ -3406,7 +3492,7 @@ msgstr "自动配置并下载SSH密钥"
msgid "Paste your id_rsa.pub here."
msgstr "复制你的公钥到这里"
#: users/forms.py:250 users/models/user.py:83
#: users/forms.py:250 users/models/user.py:85
#: users/templates/users/first_login.html:42
#: users/templates/users/user_password_update.html:46
#: users/templates/users/user_profile.html:68
@ -3473,7 +3559,7 @@ msgstr "Agent"
msgid "Date login"
msgstr "登录日期"
#: users/models/user.py:32 users/models/user.py:435
#: users/models/user.py:32 users/models/user.py:437
msgid "Administrator"
msgstr "管理员"
@ -3496,38 +3582,42 @@ msgstr "启用"
msgid "Force enable"
msgstr "强制启用"
#: users/models/user.py:55 users/templates/users/user_detail.html:71
#: users/models/user.py:57 users/templates/users/user_detail.html:71
#: users/templates/users/user_profile.html:59
msgid "Email"
msgstr "邮件"
#: users/models/user.py:66
#: users/models/user.py:68
msgid "Avatar"
msgstr "头像"
#: users/models/user.py:69 users/templates/users/user_detail.html:82
#: users/models/user.py:71 users/templates/users/user_detail.html:82
msgid "Wechat"
msgstr "微信"
#: users/models/user.py:98 users/templates/users/user_detail.html:103
#: users/models/user.py:100 users/templates/users/user_detail.html:103
#: users/templates/users/user_list.html:27
#: users/templates/users/user_profile.html:100
msgid "Source"
msgstr "用户来源"
#: users/models/user.py:102
#: users/models/user.py:104
msgid "Date password last updated"
msgstr "最后更新密码日期"
#: users/models/user.py:126 users/templates/users/user_update.html:22
#: users/models/user.py:128 users/templates/users/user_update.html:22
#: users/views/login.py:243 users/views/login.py:302 users/views/user.py:418
msgid "User auth from {}, go there change password"
msgstr "用户认证源来自 {}, 请去相应系统修改密码"
#: users/models/user.py:438
#: users/models/user.py:440
msgid "Administrator is the super user of system"
msgstr "Administrator是初始的超级管理员"
#: users/serializers/v2.py:40
msgid "name not unique"
msgstr "名称重复"
#: users/templates/users/_base_otp.html:27
msgid "Home page"
msgstr "首页"
@ -3583,7 +3673,7 @@ msgstr "更新ssh密钥"
#: users/templates/users/first_login.html:19
#: users/templates/users/first_login_done.html:19
msgid "First Login"
msgstr "首次登"
msgstr "首次登"
#: users/templates/users/first_login.html:72
msgid "I agree with the terms and conditions."
@ -3945,12 +4035,6 @@ msgstr "用户组删除"
msgid "UserGroup Deleting failed."
msgstr "用户组删除失败"
#: users/templates/users/user_list.html:28 xpack/plugins/cloud/models.py:53
#: xpack/plugins/cloud/templates/cloud/account_detail.html:60
#: xpack/plugins/cloud/templates/cloud/account_list.html:14
msgid "Validity"
msgstr "账户状态"
#: users/templates/users/user_list.html:203
msgid "This will delete the selected users !!!"
msgstr "删除选中用户 !!!"
@ -4279,7 +4363,7 @@ msgstr "用户组授权资产"
msgid "Please enable cookies and try again."
msgstr "设置你的浏览器支持cookie"
#: users/views/login.py:191 users/views/user.py:531 users/views/user.py:556
#: users/views/login.py:191 users/views/user.py:532 users/views/user.py:557
msgid "MFA code invalid, or ntp sync server time"
msgstr "MFA验证码不正确或者服务器端时间不对"
@ -4320,13 +4404,13 @@ msgstr "Token错误或失效"
msgid "Password not same"
msgstr "密码不一致"
#: users/views/login.py:311 users/views/user.py:128 users/views/user.py:427
#: users/views/login.py:311 users/views/user.py:128 users/views/user.py:428
msgid "* Your password does not meet the requirements"
msgstr "* 您的密码不符合要求"
#: users/views/login.py:349
msgid "First login"
msgstr "首次登"
msgstr "首次登"
#: users/views/user.py:145
msgid "Bulk update user success"
@ -4352,27 +4436,27 @@ msgstr "个人信息设置"
msgid "Password update"
msgstr "密码更新"
#: users/views/user.py:445
#: users/views/user.py:446
msgid "Public key update"
msgstr "密钥更新"
#: users/views/user.py:486
#: users/views/user.py:487
msgid "Password invalid"
msgstr "用户名或密码无效"
#: users/views/user.py:586
#: users/views/user.py:587
msgid "MFA enable success"
msgstr "MFA 绑定成功"
#: users/views/user.py:587
#: users/views/user.py:588
msgid "MFA enable success, return login page"
msgstr "MFA 绑定成功,返回到登录页面"
#: users/views/user.py:589
#: users/views/user.py:590
msgid "MFA disable success"
msgstr "MFA 解绑成功"
#: users/views/user.py:590
#: users/views/user.py:591
msgid "MFA disable success, return login page"
msgstr "MFA 解绑成功,返回登录页面"
@ -4631,6 +4715,9 @@ msgstr "创建组织"
msgid "Update org"
msgstr "更新组织"
#~ msgid "Valid"
#~ msgstr "账户状态"
#~ msgid "Error: Account invalid"
#~ msgstr "错误: 账户无效"
@ -4643,14 +4730,6 @@ msgstr "更新组织"
#~ msgid "No assets, task stop"
#~ msgstr "没有匹配到资产,结束任务"
#, fuzzy
#~| msgid "Validity"
#~ msgid "Valid"
#~ msgstr "账户状态"
#~ msgid "You can't update the root node name"
#~ msgstr "不能修改根节点名称"
#~ msgid "Update assets hardware info period"
#~ msgstr "定期更新资产硬件信息"

View File

@ -118,18 +118,6 @@ class AdHocResultCallback(CallbackMixin, CallbackModule, CMDCallBackModule):
self.gather_result("unreachable", result)
super().v2_runner_on_unreachable(result)
def on_playbook_start(self, name):
date_start = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.display(
"{} Start task: {}\r\n".format(date_start, name)
)
def on_playbook_end(self, name):
date_finished = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.display(
"{} Task finish\r\n".format(date_finished)
)
def display_skipped_hosts(self):
pass

View File

@ -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()

View File

@ -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',

View File

@ -1,46 +1,42 @@
# -*- coding: utf-8 -*-
#
import uuid
import os
from celery.result import AsyncResult
from django.core.cache import cache
from django.utils.translation import ugettext as _
from rest_framework import generics
from rest_framework.views import Response
from common.permissions import IsOrgAdmin, IsValidUser
from common.permissions import IsValidUser
from common.api import LogTailApi
from ..models import CeleryTask
from ..serializers import CeleryResultSerializer
from ..celery.utils import get_celery_task_log_path
__all__ = ['CeleryTaskLogApi', 'CeleryResultApi']
class CeleryTaskLogApi(generics.RetrieveAPIView):
class CeleryTaskLogApi(LogTailApi):
permission_classes = (IsValidUser,)
buff_size = 1024 * 10
end = False
queryset = CeleryTask.objects.all()
task = None
task_id = ''
def get(self, request, *args, **kwargs):
mark = request.query_params.get("mark") or str(uuid.uuid4())
task = self.get_object()
log_path = task.full_log_path
self.task_id = str(kwargs.get('pk'))
self.task = AsyncResult(self.task_id)
return super().get(request, *args, **kwargs)
if not log_path or not os.path.isfile(log_path):
return Response({"data": _("Waiting ...")}, status=203)
def get_log_path(self):
new_path = get_celery_task_log_path(self.task_id)
if new_path and os.path.isfile(new_path):
return new_path
try:
task = CeleryTask.objects.get(id=self.task_id)
except CeleryTask.DoesNotExist:
return None
return task.full_log_path
with open(log_path, 'r') as f:
offset = cache.get(mark, 0)
f.seek(offset)
data = f.read(self.buff_size).replace('\n', '\r\n')
mark = str(uuid.uuid4())
cache.set(mark, f.tell(), 5)
if data == '' and task.is_finished():
self.end = True
return Response({"data": data, 'end': self.end, 'mark': mark})
def is_file_finish_write(self):
return self.task.ready()
class CeleryResultApi(generics.RetrieveAPIView):

View File

@ -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()

View File

@ -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

160
apps/ops/celery/logger.py Normal file
View File

@ -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()

View File

@ -1,103 +1,52 @@
# -*- 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 kombu.utils.encoding import safe_str
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__)
safe_str = lambda x: x
@worker_ready.connect
def on_app_ready(sender=None, headers=None, body=None, **kwargs):
def on_app_ready(sender=None, headers=None, **kwargs):
if cache.get("CELERY_APP_READY", 0) == 1:
return
cache.set("CELERY_APP_READY", 1, 10)
tasks = get_after_app_ready_tasks()
logger.debug("Start need start task: [{}]".format(
", ".join(tasks))
)
logger.debug("Work ready signal recv")
logger.debug("Start need start task: [{}]".format(", ".join(tasks)))
for task in tasks:
subtask(task).delay()
@worker_shutdown.connect
def after_app_shutdown(sender=None, headers=None, body=None, **kwargs):
def after_app_shutdown_periodic_tasks(sender=None, **kwargs):
if cache.get("CELERY_APP_SHUTDOWN", 0) == 1:
return
cache.set("CELERY_APP_SHUTDOWN", 1, 10)
tasks = get_after_app_shutdown_clean_tasks()
logger.debug("App shutdown signal recv")
logger.debug("Clean need cleaned period tasks: [{}]".format(
', '.join(tasks))
)
logger.debug("Worker shutdown signal recv")
logger.debug("Clean period tasks: [{}]".format(', '.join(tasks)))
PeriodicTask.objects.filter(name__in=tasks).delete()
@after_task_publish.connect
def after_task_publish_signal_handler(sender, headers=None, **kwargs):
CeleryTask.objects.create(
id=headers["id"], status=CeleryTask.WAITING, name=headers["task"]
)
cache.set(headers["id"], True, 3600)
@task_prerun.connect
def pre_run_task_signal_handler(sender, task_id=None, task=None, **kwargs):
time.sleep(0.1)
for i in range(5):
if cache.get(task_id, False):
break
else:
time.sleep(0.1)
continue
t = get_object_or_none(CeleryTask, id=task_id)
if t is None:
logger.warn("Not get the task: {}".format(task_id))
@after_setup_logger.connect
def add_celery_logger_handler(sender=None, logger=None, loglevel=None, format=None, **kwargs):
if not logger:
return
now = datetime.datetime.now().strftime("%Y-%m-%d")
log_path = os.path.join(now, task_id + '.log')
full_path = os.path.join(CeleryTask.LOG_DIR, log_path)
if not os.path.exists(os.path.dirname(full_path)):
os.makedirs(os.path.dirname(full_path))
with transaction.atomic():
t.date_start = timezone.now()
t.status = CeleryTask.RUNNING
t.log_path = log_path
t.save()
f = open(full_path, 'w')
tee = TeeObj(f)
sys.stdout = tee
task.log_f = tee
@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()
handler = CeleryTaskFileHandler()
handler.setLevel(loglevel)
formatter = logging.Formatter(format)
handler.setFormatter(formatter)
logger.addHandler(handler)

View File

@ -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

View File

@ -220,10 +220,10 @@ class AdHoc(models.Model):
time_start = time.time()
try:
date_start = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
print("{} Start task: {}\r\n".format(date_start, self.task.name))
print(_("{} Start task: {}").format(date_start, self.task.name))
raw, summary = self._run_only()
date_end = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
print("\r\n{} Task finished".format(date_end))
print(_("{} Task finish").format(date_end))
history.is_finished = True
if summary.get('dark'):
history.is_success = False
@ -235,7 +235,6 @@ class AdHoc(models.Model):
except Exception as e:
return {}, {"dark": {"all": str(e)}, "contacted": []}
finally:
# f.close()
history.date_finished = timezone.now()
history.timedelta = time.time() - time_start
history.save()

View File

@ -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:

View File

@ -1,9 +1,18 @@
# coding: utf-8
import os
import subprocess
from django.conf import settings
from celery import shared_task, subtask
from django.utils import timezone
from common.utils import get_logger, get_object_or_none
from .celery.utils import register_as_period_task, after_app_shutdown_clean
from .models import Task, CommandExecution
from .celery.decorator import (
register_as_period_task, after_app_shutdown_clean_periodic,
after_app_ready_start
)
from .celery.utils import create_or_update_celery_periodic_tasks
from .models import Task, CommandExecution, CeleryTask
logger = get_logger(__file__)
@ -36,8 +45,8 @@ def run_command_execution(cid, **kwargs):
@shared_task
@after_app_shutdown_clean_periodic
@register_as_period_task(interval=3600*24)
@after_app_shutdown_clean
def clean_tasks_adhoc_period():
logger.debug("Start clean task adhoc and run history")
tasks = Task.objects.all()
@ -48,11 +57,42 @@ def clean_tasks_adhoc_period():
ad.delete()
@shared_task
@after_app_shutdown_clean_periodic
@register_as_period_task(interval=3600*24)
def clean_celery_tasks_period():
expire_days = 30
logger.debug("Start clean celery task history")
one_month_ago = timezone.now() - timezone.timedelta(days=expire_days)
tasks = CeleryTask.objects.filter(date_start__lt=one_month_ago)
for task in tasks:
if os.path.isfile(task.full_log_path):
try:
os.remove(task.full_log_path)
except (FileNotFoundError, PermissionError):
pass
task.delete()
tasks = CeleryTask.objects.filter(date_start__isnull=True)
tasks.delete()
command = "find %s -mtime +%s -name '*.log' -type f -exec rm -f {} \\;" % (
settings.CELERY_LOG_DIR, expire_days
)
subprocess.call(command, shell=True)
@shared_task
@after_app_ready_start
def create_or_update_registered_periodic_tasks():
from .celery.decorator import get_register_period_tasks
for task in get_register_period_tasks():
create_or_update_celery_periodic_tasks(task)
@shared_task
def hello(name, callback=None):
import time
time.sleep(10)
print("Hello {}".format(name))
if callback is not None:
subtask(callback).delay("Guahongwei")
@shared_task

View File

@ -1,6 +1,7 @@
{% load static %}
{% load i18n %}
<head>
<title>term.js</title>
<title>{% trans 'Task log' %}</title>
<script src="{% static 'js/jquery-2.1.1.js' %}"></script>
<script src="{% static 'js/plugins/xterm/xterm.js' %}"></script>
<link rel="stylesheet" href="{% static 'js/plugins/xterm/xterm.css' %}" />
@ -15,14 +16,14 @@
}
</style>
</head>
<div id="term" style="height: 100%;width: 100%">
</div>
<div id="term" style="height: 100%;width: 100%">
</div>
<script>
var rowHeight = 18;
var colWidth = 10;
var mark = '';
var url = "{% url 'api-ops:celery-task-log' pk=object.id %}";
var url = "{% url 'api-ops:celery-task-log' pk=task_id %}";
var term;
var end = false;
var error = false;
@ -35,9 +36,9 @@
{#colWidth = 1.00 * t.width() / 6;#}
}
function resize() {
var rows = Math.floor(window.innerHeight / rowHeight) - 1;
var cols = Math.floor(window.innerWidth / colWidth) - 2;
term.resize(cols, rows);
{#var rows = Math.floor(window.innerHeight / rowHeight) - 1;#}
{#var cols = Math.floor(window.innerWidth / colWidth) - 2;#}
{#term.resize(cols, rows);#}
}
function requestAndWrite() {
if (!end && success) {
@ -73,7 +74,7 @@
disableStdin: true
});
term.open(document.getElementById('term'));
term.resize(80, 24);
term.resize(90, 32);
resize();
term.on('data', function (data) {
{#term.write(data.replace('\r', '\r\n'))#}

View File

@ -141,10 +141,10 @@ function onCheck(e, treeId, treeNode) {
var nodes_names = nodes.map(function (node) {
return node.name;
});
var message = "已选择资产: ";
var message = "{% trans 'Selected assets' %}" + ': ';
message += nodes_names.join(", ");
message += "\r\n";
message += "总共: " + nodes_names.length + "个\r\n";
message += "{% trans 'In total' %}" + ': ' + nodes_names.length + "个\r\n";
term.clear();
term.write(message)
}
@ -179,7 +179,7 @@ function initResultTerminal() {
}
});
term.open(document.getElementById('term'));
term.write("选择左侧资产, 选择运行的系统用户,批量执行命令\r\n")
term.write("{% trans 'Select the left asset, select the running system user, execute command in batch' %}" + "\r\n")
}
function wrapperError(msg) {
@ -197,15 +197,15 @@ function execute() {
return node.id;
});
if (hosts.length === 0) {
term.write(wrapperError('没有选中资产'));
term.write(wrapperError("{% trans 'Unselected assets' %}"));
return
}
if (!command) {
term.write(wrapperError('没有输入命令'));
term.write(wrapperError("{% trans 'No input command' %}"));
return
}
if (!run_as) {
term.write(wrapperError('没有选择运行用户'));
term.write(wrapperError("{% trans 'No system user was selected' %}"));
return
}
var data = {

View File

@ -48,7 +48,7 @@ def update_or_create_ansible_task(
hosts_same = old_hosts == new_hosts
if not adhoc or adhoc != new_adhoc or not hosts_same:
logger.info(_("Update task content: {}").format(task_name))
logger.debug(_("Update task content: {}").format(task_name))
new_adhoc.save()
new_adhoc.hosts.set(hosts)
task.latest_adhoc = new_adhoc

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
from django.views.generic import DetailView
from django.views.generic import DetailView, TemplateView
from common.permissions import AdminUserRequiredMixin
from ..models import CeleryTask
@ -9,6 +9,10 @@ from ..models import CeleryTask
__all__ = ['CeleryTaskLogView']
class CeleryTaskLogView(AdminUserRequiredMixin, DetailView):
class CeleryTaskLogView(AdminUserRequiredMixin, TemplateView):
template_name = 'ops/celery_task_log.html'
model = CeleryTask
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({'task_id': self.kwargs.get('pk')})
return context

View File

@ -66,3 +66,4 @@ class OrgMembershipUsersViewSet(OrgMembershipModelViewSetMixin, BulkModelViewSet
serializer_class = OrgMembershipUserSerializer
membership_class = Organization.users.through
permission_classes = (IsSuperUserOrAppUser, )

View File

@ -4,7 +4,7 @@
from werkzeug.local import Local
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.shortcuts import redirect
from django.shortcuts import redirect, get_object_or_404
from django.forms import ModelForm
from django.http.response import HttpResponseForbidden
from django.core.exceptions import ValidationError
@ -191,7 +191,7 @@ class OrgMembershipModelViewSetMixin:
http_method_names = ['get', 'post', 'delete', 'head', 'options']
def dispatch(self, request, *args, **kwargs):
self.org = Organization.objects.get(pk=kwargs.get('org_id'))
self.org = get_object_or_404(Organization, pk=kwargs.get('org_id'))
return super().dispatch(request, *args, **kwargs)
def get_serializer_context(self):
@ -200,4 +200,5 @@ class OrgMembershipModelViewSetMixin:
return context
def get_queryset(self):
return self.membership_class.objects.filter(organization=self.org)
queryset = self.membership_class.objects.filter(organization=self.org)
return queryset

View File

@ -122,3 +122,7 @@ class Organization(models.Model):
return True
else:
return False
def change_to(self):
from .utils import set_current_org
set_current_org(self)

View File

@ -9,11 +9,16 @@ from .. import api
app_name = 'orgs'
router = DefaultRouter()
# 将会删除
router.register(r'org/(?P<org_id>[0-9a-zA-Z\-]{36})/membership/admins',
api.OrgMembershipAdminsViewSet, 'membership-admins')
router.register(r'org/(?P<org_id>[0-9a-zA-Z\-]{36})/membership/users',
api.OrgMembershipUsersViewSet, 'membership-users'),
# 替换为这个
router.register(r'orgs/(?P<org_id>[0-9a-zA-Z\-]{36})/membership/admins',
api.OrgMembershipAdminsViewSet, 'membership-admins-2')
router.register(r'orgs/(?P<org_id>[0-9a-zA-Z\-]{36})/membership/users',
api.OrgMembershipUsersViewSet, 'membership-users-2'),
router.register(r'orgs', api.OrgViewSet, 'org')

View File

@ -2,21 +2,26 @@
#
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.db.models import Q
from rest_framework.views import APIView, Response
from rest_framework.generics import ListAPIView, get_object_or_404, \
RetrieveUpdateAPIView
from rest_framework.generics import (
ListAPIView, get_object_or_404, RetrieveUpdateAPIView
)
from rest_framework import viewsets
from rest_framework.pagination import LimitOffsetPagination
from common.utils import set_or_append_attr_bulk
from common.permissions import IsValidUser, IsOrgAdmin, IsOrgAdminOrAppUser
from common.tree import TreeNode, TreeNodeSerializer
from common.utils import get_object_or_none
from orgs.mixins import RootOrgViewMixin
from orgs.utils import set_to_root_org
from .utils import AssetPermissionUtil
from .models import AssetPermission
from .hands import AssetGrantedSerializer, User, UserGroup, Asset, Node, \
from .hands import (
AssetGrantedSerializer, User, UserGroup, Asset, Node,
SystemUser, NodeSerializer
)
from . import serializers
from .mixins import AssetsFilterMixin
@ -38,6 +43,7 @@ class AssetPermissionViewSet(viewsets.ModelViewSet):
queryset = AssetPermission.objects.all()
serializer_class = serializers.AssetPermissionCreateUpdateSerializer
pagination_class = LimitOffsetPagination
filter_fields = ['name']
permission_classes = (IsOrgAdmin,)
def get_serializer_class(self):
@ -45,36 +51,122 @@ class AssetPermissionViewSet(viewsets.ModelViewSet):
return serializers.AssetPermissionListSerializer
return self.serializer_class
def get_queryset(self):
queryset = super().get_queryset().all()
search = self.request.query_params.get('search')
asset_id = self.request.query_params.get('asset')
node_id = self.request.query_params.get('node')
inherit_nodes = set()
if search:
queryset = queryset.filter(name__icontains=search)
if not asset_id and not node_id:
def filter_valid(self, queryset):
valid = self.request.query_params.get('is_valid', None)
if valid is None:
return queryset
if valid in ['0', 'N', 'false', 'False']:
valid = False
else:
valid = True
now = timezone.now()
if valid:
queryset = queryset.filter(is_active=True).filter(
date_start__lt=now, date_expired__gt=now,
)
else:
queryset = queryset.filter(
Q(is_active=False) |
Q(date_start__gt=now) |
Q(date_expired__lt=now)
)
return queryset
permissions = set()
def filter_system_user(self, queryset):
system_user_id = self.request.query_params.get('system_user_id')
system_user_name = self.request.query_params.get('system_user')
if system_user_id:
system_user = get_object_or_none(SystemUser, pk=system_user_id)
elif system_user_name:
system_user = get_object_or_none(SystemUser, name=system_user_name)
else:
return queryset
if not system_user:
return queryset.none()
queryset = queryset.filter(system_users=system_user)
return queryset
def filter_node(self, queryset):
node_id = self.request.query_params.get('node_id')
node_name = self.request.query_params.get('node')
if node_id:
node = get_object_or_none(Node, pk=node_id)
elif node_name:
node = get_object_or_none(Node, name=node_name)
else:
return queryset
if not node:
return queryset.none()
nodes = node.get_ancestor(with_self=True)
queryset = queryset.filter(nodes__in=nodes)
return queryset
def filter_asset(self, queryset):
asset_id = self.request.query_params.get('asset_id')
hostname = self.request.query_params.get('hostname')
ip = self.request.query_params.get('ip')
if asset_id:
asset = get_object_or_404(Asset, pk=asset_id)
permissions = set(queryset.filter(assets=asset))
assets = Asset.objects.filter(pk=asset_id)
elif hostname:
assets = Asset.objects.filter(hostname=hostname)
elif ip:
assets = Asset.objects.filter(ip=ip)
else:
return queryset
if not assets:
return queryset.none()
inherit_nodes = set()
for asset in assets:
for node in asset.nodes.all():
inherit_nodes.update(set(node.get_ancestor(with_self=True)))
elif node_id:
node = get_object_or_404(Node, pk=node_id)
permissions = set(queryset.filter(nodes=node))
inherit_nodes = node.get_ancestor()
queryset = queryset.filter(Q(assets__in=assets) | Q(nodes__in=inherit_nodes))
return queryset
for n in inherit_nodes:
_permissions = queryset.filter(nodes=n)
set_or_append_attr_bulk(_permissions, "inherit", n.value)
permissions.update(_permissions)
def filter_user(self, queryset):
user_id = self.request.query_params.get('user_id')
username = self.request.query_params.get('username')
if user_id:
user = get_object_or_none(User, pk=user_id)
elif username:
user = get_object_or_none(User, username=username)
else:
return queryset
if not user:
return queryset.none()
return list(permissions)
def filter_user_group(self, queryset):
user_group_id = self.request.query_params.get('user_group_id')
user_group_name = self.request.query_params.get('user_group')
if user_group_id:
group = get_object_or_none(UserGroup, pk=user_group_id)
elif user_group_name:
group = get_object_or_none(UserGroup, name=user_group_name)
else:
return queryset
if not group:
return queryset.none()
queryset = queryset.filter(user_groups=group)
return queryset
def filter_keyword(self, queryset):
keyword = self.request.query_params.get('search')
if not keyword:
return queryset
queryset = queryset.filter(name__icontains=keyword)
return queryset
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
queryset = self.filter_valid(queryset)
queryset = self.filter_keyword(queryset)
queryset = self.filter_asset(queryset)
queryset = self.filter_node(queryset)
queryset = self.filter_system_user(queryset)
queryset = self.filter_user_group(queryset)
return queryset
def get_queryset(self):
return self.queryset.all()
class UserGrantedAssetsApi(AssetsFilterMixin, ListAPIView):

View File

@ -51,9 +51,15 @@ class AssetPermission(OrgModelMixin):
def id_str(self):
return str(self.id)
@property
def is_expired(self):
if self.date_expired > timezone.now() > self.date_start:
return False
return True
@property
def is_valid(self):
if self.date_expired > timezone.now() > self.date_start and self.is_active:
if not self.is_expired and self.is_active:
return True
return False

View File

@ -28,19 +28,13 @@ class AssetPermissionListSerializer(serializers.ModelSerializer):
assets = StringManyToManyField(many=True, read_only=True)
nodes = StringManyToManyField(many=True, read_only=True)
system_users = StringManyToManyField(many=True, read_only=True)
inherit = serializers.SerializerMethodField()
is_valid = serializers.BooleanField()
is_expired = serializers.BooleanField()
class Meta:
model = AssetPermission
fields = '__all__'
@staticmethod
def get_inherit(obj):
if hasattr(obj, 'inherit'):
return obj.inherit
else:
return None
class AssetPermissionUpdateUserSerializer(serializers.ModelSerializer):

View File

@ -56,7 +56,7 @@
<th class="text-center">{% trans 'Asset' %}</th>
<th class="text-center">{% trans 'Node'%}</th>
<th class="text-center">{% trans 'System user' %}</th>
<th class="text-center">{% trans 'Active' %}</th>
<th class="text-center">{% trans 'Validity' %}</th>
<th class="text-center" >{% trans 'Action' %}</th>
</tr>
</thead>
@ -67,6 +67,17 @@
</div>
</div>
</div>
<ul class="dropdown-menu search-help">
<li><a class="search-item" data-value="name">{% trans 'Name' %}</a></li>
<li><a class="search-item" data-value="is_valid">{% trans 'Validity' %}</a></li>
<li><a class="search-item" data-value="username">{% trans 'Username' %}</a></li>
<li><a class="search-item" data-value="user_group">{% trans 'User group' %}</a></li>
<li><a class="search-item" data-value="ip">IP</a></li>
<li><a class="search-item" data-value="hostname">{% trans 'Hostname' %}</a></li>
<li><a class="search-item" data-value="node">{% trans 'Node' %}</a></li>
<li><a class="search-item" data-value="system_user">{% trans 'System user' %}</a></li>
</ul>
{% endblock %}
{% block custom_foot_js %}
@ -79,11 +90,11 @@ function onSelected(event, treeNode) {
setCookie('node_selected', treeNode.id);
var url = table.ajax.url();
if (treeNode.meta.type === 'node') {
url = setUrlParam(url, 'asset', "");
url = setUrlParam(url, 'node', treeNode.meta.node.id)
url = setUrlParam(url, 'asset_id', "");
url = setUrlParam(url, 'node_id', treeNode.meta.node.id)
} else {
url = setUrlParam(url, 'node', "");
url = setUrlParam(url, 'asset', treeNode.meta.asset.id)
url = setUrlParam(url, 'node_id', "");
url = setUrlParam(url, 'asset_id', treeNode.meta.asset.id)
}
setCookie('node_selected', treeNode.node_id);
table.ajax.url(url);
@ -178,7 +189,7 @@ function initTable() {
{data: "id"}, {data: "name"}, {data: "users"},
{data: "user_groups"}, {data: "assets"},
{data: "nodes"}, {data: "system_users"},
{data: "is_active", orderable: false}, {data: "id", orderable: false}
{data: "is_valid", orderable: false}, {data: "id", orderable: false}
],
select: {},
op_html: $('#actions').html()
@ -231,6 +242,7 @@ function toggle() {
$(document).ready(function(){
initTable();
initTree();
})
.on('click', '.btn-del', function () {
var $this = $(this);
@ -279,6 +291,28 @@ $(document).ready(function(){
}
}
}).on('click', '#permission_list_table_filter input', function (e) {
e.preventDefault();
e.stopPropagation();
var position = $('#permission_list_table_filter input').offset();
var y = position['top'];
var x = position['left'];
x -= 220;
y += 30;
$('.search-help').css({"top":y+"px", "left":x+"px", "position": "absolute"});
$('.dropdown-menu.search-help').show();
}).on('click', '.search-item', function (e) {
e.preventDefault();
e.stopPropagation();
var value = $(this).data('value');
var old_value = $('#permission_list_table_filter input').val();
var new_value = old_value + ' ' + value + ':';
$('#permission_list_table_filter input').val(new_value.trim());
$('.dropdown-menu.search-help').hide();
$('#permission_list_table_filter input').focus()
}).on('click', 'body', function (e) {
$('.dropdown-menu.search-help').hide()
})
</script>

View File

@ -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('<div class="alert alert-danger" style="display: none"></div>');
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('<div class="help-block error"></div>');
helpBlockRef = parentRef.children('.help-block.error');
}
if (fieldRef.length === 1 && formGroupRef.length === 1) {
formGroupRef.addClass('has-error');
var help_msg = v.join("<br/>") ;
helpBlockRef.html(help_msg);
} else {
noneFieldErrorMsg += v + '<br/>';
}
});
if (noneFieldErrorRef.length === 1 && noneFieldErrorMsg !== '') {
noneFieldErrorRef.css('display', 'block');
noneFieldErrorRef.html(noneFieldErrorMsg);
}
}
})
}
function APIUpdateAttr(props) {
// props = {url: .., body: , success: , error: , method: ,}
props = props || {};
@ -195,9 +276,6 @@ function APIUpdateAttr(props) {
}).fail(function(jqXHR, textStatus, errorThrown) {
if (flash_message) {
var msg = "";
console.log(jqXHR);
console.log(textStatus);
console.log(errorThrown);
if (user_fail_message) {
msg = user_fail_message;
} else if (jqXHR.responseJSON) {
@ -213,6 +291,7 @@ function APIUpdateAttr(props) {
toastr.error(msg);
}
if (typeof props.error === 'function') {
console.log(jqXHR);
return props.error(jqXHR.responseText, jqXHR.status);
}
});
@ -478,7 +557,7 @@ jumpserver.initServerSideDataTable = function (options) {
url: options.ajax_url ,
data: function (data) {
delete data.columns;
if (data.length !== null ){
if (data.length !== null){
data.limit = data.length;
delete data.length;
}
@ -525,7 +604,7 @@ jumpserver.initServerSideDataTable = function (options) {
columns: options.columns || [],
select: options.select || select,
language: jumpserver.language,
lengthMenu: [[10, 15, 25, 50], [10, 15, 25, 50]]
lengthMenu: [[15, 25, 50, 9999], [15, 25, 50, 'All']]
});
table.selected = [];
table.selected_rows = [];

View File

@ -1,2 +1,2 @@
{% load i18n %}
<strong>Copyright</strong> {% trans ' Beijing Duizhan Tech, Inc. ' %} &copy; 2014-2018
<strong>Copyright</strong> {% trans ' Beijing Duizhan Tech, Inc. ' %} &copy; 2014-2019

View File

@ -5,6 +5,6 @@
<!--<img style="display: none" src="http://www.jumpserver.org/img/evaluate_avatar1.jpg">-->
</div>
<div>
<strong>Copyright</strong> {% trans ' Beijing Duizhan Tech, Inc. ' %}&copy; 2014-2018
<strong>Copyright</strong> {% trans ' Beijing Duizhan Tech, Inc. ' %}&copy; 2014-2019
</div>
</div>

View File

@ -54,7 +54,7 @@
{% include '_copyright.html' %}
</div>
<div class="col-md-6 text-right">
<small>2014-2018</small>
<small>2014-2019</small>
</div>
</div>
</div>

View File

@ -33,16 +33,19 @@ class SessionViewSet(BulkModelViewSet):
permission_classes = (IsOrgAdminOrAppUser,)
def get_queryset(self):
queryset = super().get_queryset()
terminal_id = self.kwargs.get("terminal", None)
if terminal_id:
terminal = get_object_or_404(Terminal, id=terminal_id)
self.queryset = terminal.session_set.all()
return self.queryset.all()
queryset = queryset.filter(terminal=terminal)
return queryset
return queryset
def perform_create(self, serializer):
if hasattr(self.request.user, 'terminal'):
serializer.validated_data["terminal"] = self.request.user.terminal
sid = serializer.validated_data["system_user"]
# guacamole提交的是id
if is_uuid(sid):
_system_user = SystemUser.get_system_user_by_id_or_cached(sid)
if _system_user:

View File

@ -100,52 +100,18 @@ class StatusViewSet(viewsets.ModelViewSet):
task_serializer_class = serializers.TaskSerializer
def create(self, request, *args, **kwargs):
from_gua = self.request.query_params.get("from_guacamole", None)
if not from_gua:
self.handle_sessions()
super().create(request, *args, **kwargs)
self.handle_status(request)
self.handle_sessions()
tasks = self.request.user.terminal.task_set.filter(is_finished=False)
serializer = self.task_serializer_class(tasks, many=True)
return Response(serializer.data, status=201)
def handle_status(self, request):
request.user.terminal.is_alive = True
def handle_sessions(self):
sessions_active = []
for session_data in self.request.data.get("sessions", []):
self.create_or_update_session(session_data)
if not session_data["is_finished"]:
sessions_active.append(session_data["id"])
sessions_in_db_active = Session.objects.filter(
is_finished=False,
terminal=self.request.user.terminal.id
)
for session in sessions_in_db_active:
if str(session.id) not in sessions_active:
session.is_finished = True
session.date_end = timezone.now()
session.save()
def create_or_update_session(self, session_data):
session_data["terminal"] = self.request.user.terminal.id
_id = session_data["id"]
session = get_object_or_none(Session, id=_id)
if session:
serializer = serializers.SessionSerializer(
data=session_data, instance=session
)
else:
serializer = serializers.SessionSerializer(data=session_data)
if serializer.is_valid():
session = serializer.save()
return session
else:
msg = "session data is not valid {}: {}".format(
serializer.errors, str(serializer.data)
)
logger.error(msg)
return None
sessions_id = self.request.data.get('sessions', [])
Session.set_sessions_active(sessions_id)
def get_queryset(self):
terminal_id = self.kwargs.get("terminal", None)

View File

@ -1,12 +1,16 @@
# -*- coding: utf-8 -*-
#
from rest_framework import viewsets
from rest_framework import viewsets, generics
from rest_framework import status
from rest_framework.response import Response
from common.permissions import IsSuperUser, WithBootstrapToken
from ...models import Terminal
from ...serializers import v2 as serializers
__all__ = ['TerminalViewSet', 'TerminalRegistrationViewSet']
__all__ = ['TerminalViewSet', 'TerminalRegistrationApi']
class TerminalViewSet(viewsets.ModelViewSet):
@ -15,8 +19,19 @@ class TerminalViewSet(viewsets.ModelViewSet):
permission_classes = [IsSuperUser]
class TerminalRegistrationViewSet(viewsets.ModelViewSet):
queryset = Terminal.objects.filter(is_deleted=False)
class TerminalRegistrationApi(generics.CreateAPIView):
serializer_class = serializers.TerminalRegistrationSerializer
permission_classes = [WithBootstrapToken]
http_method_names = ['post']
def create(self, request, *args, **kwargs):
data = {k: v for k, v in request.data.items()}
serializer = serializers.TerminalSerializer(
data=data, context={'request': request}
)
serializer.is_valid(raise_exception=True)
terminal = serializer.save()
sa_serializer = serializer.sa_serializer_class(instance=terminal.user)
data['service_account'] = sa_serializer.data
return Response(data, status=status.HTTP_201_CREATED)

View File

@ -1,7 +1,9 @@
# ~*~ coding: utf-8 ~*~
import datetime
from django.db import transaction
from django.utils import timezone
from django.db.utils import OperationalError
from .base import CommandBase
@ -35,7 +37,25 @@ class CommandStore(CommandBase):
input=c["input"], output=c["output"], session=c["session"],
org_id=c["org_id"], timestamp=c["timestamp"]
))
return self.model.objects.bulk_create(_commands)
error = False
try:
with transaction.atomic():
self.model.objects.bulk_create(_commands)
except OperationalError:
error = True
except:
return False
if not error:
return True
for command in _commands:
try:
with transaction.atomic():
command.save()
except OperationalError:
command.output = str(command.output.encode())
command.save()
return True
@staticmethod
def make_filter_kwargs(

View File

@ -39,5 +39,3 @@ class TerminalForm(forms.ModelForm):
'name', 'remote_addr', 'comment',
'command_storage', 'replay_storage',
]
help_texts = {
}

View File

@ -8,10 +8,12 @@ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone
from django.conf import settings
from django.core.files.storage import default_storage
from django.core.cache import cache
from users.models import User
from orgs.mixins import OrgModelMixin
from common.utils import get_command_storage_setting, get_replay_storage_setting
from .backends import get_multi_command_storage
from .backends.command.models import AbstractSessionCommand
@ -28,6 +30,17 @@ class Terminal(models.Model):
is_deleted = models.BooleanField(default=False)
date_created = models.DateTimeField(auto_now_add=True)
comment = models.TextField(blank=True, verbose_name=_('Comment'))
STATUS_KEY_PREFIX = 'terminal_status_'
@property
def is_alive(self):
key = self.STATUS_KEY_PREFIX + str(self.id)
return bool(cache.get(key))
@is_alive.setter
def is_alive(self, value):
key = self.STATUS_KEY_PREFIX + str(self.id)
cache.set(key, value, 60)
@property
def is_active(self):
@ -41,7 +54,7 @@ class Terminal(models.Model):
self.user.is_active = active
self.user.save()
def get_common_storage(self):
def get_command_storage_setting(self):
storage_all = get_command_storage_setting()
if self.command_storage in storage_all:
storage = storage_all.get(self.command_storage)
@ -49,7 +62,7 @@ class Terminal(models.Model):
storage = storage_all.get('default')
return {"TERMINAL_COMMAND_STORAGE": storage}
def get_replay_storage(self):
def get_replay_storage_setting(self):
storage_all = get_replay_storage_setting()
if self.replay_storage in storage_all:
storage = storage_all.get(self.replay_storage)
@ -61,10 +74,11 @@ class Terminal(models.Model):
def config(self):
configs = {}
for k in dir(settings):
if k.startswith('TERMINAL'):
configs[k] = getattr(settings, k)
configs.update(self.get_common_storage())
configs.update(self.get_replay_storage())
if not k.startswith('TERMINAL'):
continue
configs[k] = getattr(settings, k)
configs.update(self.get_command_storage_setting())
configs.update(self.get_replay_storage_setting())
configs.update({
'SECURITY_MAX_IDLE_TIME': settings.SECURITY_MAX_IDLE_TIME
})
@ -152,6 +166,7 @@ class Session(OrgModelMixin):
date_end = models.DateTimeField(verbose_name=_("Date end"), null=True)
upload_to = 'replay'
ACTIVE_CACHE_KEY_PREFIX = 'SESSION_ACTIVE_{}'
def get_rel_replay_path(self, version=2):
"""
@ -181,6 +196,26 @@ class Session(OrgModelMixin):
except OSError as e:
return None, e
@classmethod
def set_sessions_active(cls, sessions_id):
data = {cls.ACTIVE_CACHE_KEY_PREFIX.format(i): i for i in sessions_id}
cache.set_many(data, timeout=5*60)
@classmethod
def get_active_sessions(cls):
return cls.objects.filter(is_finished=False)
def is_active(self):
if self.protocol in ['ssh', 'telnet']:
key = self.ACTIVE_CACHE_KEY_PREFIX.format(self.id)
return bool(cache.get(key))
return True
@property
def command_amount(self):
command_store = get_multi_command_storage()
return command_store.count(session=str(self.id))
class Meta:
db_table = "terminal_session"
ordering = ["-date_start"]

View File

@ -1,17 +1,15 @@
# -*- coding: utf-8 -*-
#
from django.core.cache import cache
from rest_framework import serializers
from rest_framework_bulk.serializers import BulkListSerializer
from common.mixins import BulkSerializerMixin
from ..models import Terminal, Status, Session, Task
from ..backends import get_multi_command_storage
class TerminalSerializer(serializers.ModelSerializer):
session_online = serializers.SerializerMethodField()
is_alive = serializers.SerializerMethodField()
is_alive = serializers.BooleanField(read_only=True)
class Meta:
model = Terminal
@ -23,42 +21,23 @@ class TerminalSerializer(serializers.ModelSerializer):
@staticmethod
def get_session_online(obj):
return Session.objects.filter(terminal=obj.id, is_finished=False).count()
@staticmethod
def get_is_alive(obj):
key = StatusSerializer.CACHE_KEY_PREFIX + str(obj.id)
return cache.get(key)
return Session.objects.filter(terminal=obj, is_finished=False).count()
class SessionSerializer(BulkSerializerMixin, serializers.ModelSerializer):
command_amount = serializers.SerializerMethodField()
command_store = get_multi_command_storage()
command_amount = serializers.IntegerField(read_only=True)
class Meta:
model = Session
list_serializer_class = BulkListSerializer
fields = '__all__'
def get_command_amount(self, obj):
return self.command_store.count(session=str(obj.id))
class StatusSerializer(serializers.ModelSerializer):
CACHE_KEY_PREFIX = 'terminal_status_'
class Meta:
fields = '__all__'
fields = ['id', 'terminal']
model = Status
def create(self, validated_data):
terminal_id = str(validated_data['terminal'].id)
key = self.CACHE_KEY_PREFIX + terminal_id
cache.set(key, 1, 60)
return validated_data
class TaskSerializer(BulkSerializerMixin, serializers.ModelSerializer):
@ -69,6 +48,6 @@ class TaskSerializer(BulkSerializerMixin, serializers.ModelSerializer):
class ReplaySerializer(serializers.Serializer):
file = serializers.FileField()
file = serializers.FileField(allow_empty_file=True)

View File

@ -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)

View File

@ -1,28 +1,3 @@
# -*- coding: utf-8 -*-
#
from celery import shared_task
from django.core.cache import cache
from django.db.utils import ProgrammingError, OperationalError
from common.utils import get_logger
from .const import ASSETS_CACHE_KEY, USERS_CACHE_KEY, SYSTEM_USER_CACHE_KEY
RUNNING = False
logger = get_logger(__file__)
def set_session_info_cache():
logger.debug("")
from .utils import get_session_asset_list, get_session_user_list, \
get_session_system_user_list
try:
assets = get_session_asset_list()
users = get_session_user_list()
system_users = get_session_system_user_list()
cache.set(ASSETS_CACHE_KEY, assets)
cache.set(USERS_CACHE_KEY, users)
cache.set(SYSTEM_USER_CACHE_KEY, system_users)
except (ProgrammingError, OperationalError):
pass

View File

@ -10,8 +10,9 @@ from django.conf import settings
from django.core.files.storage import default_storage
from ops.celery.utils import register_as_period_task, after_app_ready_start, \
after_app_shutdown_clean
from ops.celery.decorator import (
register_as_period_task, after_app_ready_start, after_app_shutdown_clean_periodic
)
from .models import Status, Session, Command
@ -23,28 +24,30 @@ logger = get_task_logger(__name__)
@shared_task
@register_as_period_task(interval=3600)
@after_app_ready_start
@after_app_shutdown_clean
@after_app_shutdown_clean_periodic
def delete_terminal_status_period():
yesterday = timezone.now() - datetime.timedelta(days=3)
yesterday = timezone.now() - datetime.timedelta(days=1)
Status.objects.filter(date_created__lt=yesterday).delete()
@shared_task
@register_as_period_task(interval=3600)
@register_as_period_task(interval=600)
@after_app_ready_start
@after_app_shutdown_clean
@after_app_shutdown_clean_periodic
def clean_orphan_session():
active_sessions = Session.objects.filter(is_finished=False)
for session in active_sessions:
if not session.terminal or not session.terminal.is_active:
session.is_finished = True
session.save()
if not session.is_active():
continue
session.is_finished = True
session.date_end = timezone.now()
session.save()
@shared_task
@register_as_period_task(interval=3600*24)
@after_app_ready_start
@after_app_shutdown_clean
@after_app_shutdown_clean_periodic
def clean_expired_session_period():
logger.info("Start clean expired session record, commands and replay")
days = settings.TERMINAL_SESSION_KEEP_DURATION
@ -64,3 +67,4 @@ def clean_expired_session_period():
default_storage.delete(_local_path)
# 删除session记录
session.delete()

View File

@ -94,7 +94,7 @@
<td class="text-center">{{ session.remote_addr|default:"" }}</td>
<td class="text-center">{{ session.protocol }}</td>
<td class="text-center">{{ session.get_login_from_display }}</td>
<td class="text-center">{{ session.id | get_session_command_amount }}</td>
<td class="text-center">{{ session.command_amount }}</td>
<td class="text-center">{{ session.date_start }}</td>
{# <td class="text-center">{{ session.date_last_active }}</td>#}

View File

@ -33,8 +33,6 @@
<h3>{% trans 'Info' %}</h3>
{% 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 @@
<script>
$(document).ready(function () {
$('.select2').select2();
$('.input-group.date').datepicker({
format: "yyyy-mm-dd",
todayBtn: "linked",
keyboardNavigation: false,
forceParse: false,
calendarWeeks: true,
autoclose: true
}).on('submit', 'form', function (e) {
e.preventDefault();
var form = $('form');
formSubmit({
'url': '{% url 'api-terminal-v2:terminal-detail' pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', '{{ object.id }}'),
'form': form,
'method': 'PUT',
'redirect_to': '{% url "terminal:terminal-list" %}'
});
})
</script>

View File

@ -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

View File

@ -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

View File

@ -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']

View File

@ -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'),
),
]

View File

@ -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(

View File

@ -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

View File

@ -3,10 +3,8 @@
from celery import shared_task
from ops.celery.utils import (
create_or_update_celery_periodic_tasks,
after_app_ready_start
)
from ops.celery.utils import create_or_update_celery_periodic_tasks
from ops.celery.decorator import after_app_ready_start
from .models import User
from common.utils import get_logger
from .utils import write_login_log, send_password_expiration_reminder_mail

View File

@ -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()

View File

@ -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()

68
config_example.yml Normal file
View File

@ -0,0 +1,68 @@
# SECURITY WARNING: keep the secret key used in production secret!
# 加密秘钥 生产环境中请修改为随机字符串,请勿外泄, 可使用命令生成
# $ cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 49;echo
SECRET_KEY:
# SECURITY WARNING: keep the bootstrap token used in production secret!
# 预共享Token coco和guacamole用来注册服务账号不在使用原来的注册接受机制
BOOTSTRAP_TOKEN:
# Development env open this, when error occur display the full process track, Production disable it
# DEBUG 模式 开启DEBUG后遇到错误时可以看到更多日志
# DEBUG: true
# DEBUG, INFO, WARNING, ERROR, CRITICAL can set. See https://docs.djangoproject.com/en/1.10/topics/logging/
# 日志级别
# LOG_LEVEL: DEBUG
# LOG_DIR:
# Session expiration setting, Default 24 hour, Also set expired on on browser close
# 浏览器Session过期时间默认24小时, 也可以设置浏览器关闭则过期
# SESSION_COOKIE_AGE: 3600 * 24
# SESSION_EXPIRE_AT_BROWSER_CLOSE: False
# Database setting, Support sqlite3, mysql, postgres ....
# 数据库设置
# See https://docs.djangoproject.com/en/1.10/ref/settings/#databases
# SQLite setting:
# 使用单文件sqlite数据库
# DB_ENGINE: sqlite3
# DB_NAME:
# MySQL or postgres setting like:
# 使用Mysql作为数据库
DB_ENGINE: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_USER: jumpserver
DB_PASSWORD:
DB_NAME: jumpserver
# When Django start it will bind this host and port
# ./manage.py runserver 127.0.0.1:8080
# 运行时绑定端口
HTTP_BIND_HOST: 0.0.0.0
HTTP_LISTEN_PORT: 8080
# Use Redis as broker for celery and web socket
# Redis配置
REDIS_HOST: 127.0.0.1
REDIS_PORT: 6379
# REDIS_PASSWORD:
# REDIS_DB_CELERY: 3
# REDIS_DB_CACHE: 4
# Use OpenID authorization
# 使用OpenID 来进行认证设置
# BASE_SITE_URL: http://localhost:8080
# AUTH_OPENID: false # True or False
# AUTH_OPENID_SERVER_URL: https://openid-auth-server.com/
# AUTH_OPENID_REALM_NAME: realm-name
# AUTH_OPENID_CLIENT_ID: client-id
# AUTH_OPENID_CLIENT_SECRET: client-secret
# OTP settings
# OTP/MFA 配置
# OTP_VALID_WINDOW: 0
# OTP_ISSUER_NAME: Jumpserver

View File

@ -7,5 +7,10 @@ function cleanup()
fi
}
service="all"
if [ "$1" != "" ];then
service=$1
fi
trap cleanup EXIT
python jms start all
python jms start $service

14
jms
View File

@ -15,9 +15,10 @@ sys.path.append(BASE_DIR)
from apps import __version__
try:
from config import config as CONFIG
from apps.jumpserver.conf import load_user_config
CONFIG = load_user_config()
except ImportError:
print("Could not find config file, `cp config_example.py config.py`")
print("Could not find config file, `cp config_example.yml config.yml`")
sys.exit(1)
os.environ["PYTHONIOENCODING"] = "UTF-8"
@ -107,8 +108,7 @@ def is_running(s, unlink=True):
pid_file = get_pid_file_path(s)
if os.path.isfile(pid_file):
with open(pid_file, 'r') as f:
pid = get_pid(s)
pid = get_pid(s)
if check_pid(pid):
return True
@ -120,12 +120,15 @@ def is_running(s, unlink=True):
def parse_service(s):
if s == 'all':
return all_services
elif "," in s:
return [i.strip() for i in s.split(',')]
else:
return [s]
def start_gunicorn():
print("\n- Start Gunicorn WSGI HTTP Server")
prepare()
service = 'gunicorn'
bind = '{}:{}'.format(HTTP_HOST, HTTP_PORT)
log_format = '%(h)s %(t)s "%(r)s" %(s)s %(b)s '
@ -218,7 +221,6 @@ def start_service(s):
print(time.ctime())
print('Jumpserver version {}, more see https://www.jumpserver.org'.format(
__version__))
prepare()
services_handler = {
"gunicorn": start_gunicorn,
@ -316,7 +318,7 @@ if __name__ == '__main__':
)
parser.add_argument(
"service", type=str, default="all", nargs="?",
choices=("all", "gunicorn", "celery", "beat"),
choices=("all", "gunicorn", "celery", "beat", "celery,beat"),
help="The service to start",
)
parser.add_argument('-d', '--daemon', nargs="?", const=1)

View File

@ -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

View File

@ -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("用户名 或 密码 或 地址 不对")