diff --git a/sysom_server/sysom_hotfix/conf/__init__.py b/sysom_server/sysom_hotfix/conf/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/sysom_server/sysom_hotfix/conf/common.py b/sysom_server/sysom_hotfix/conf/common.py new file mode 100644 index 0000000000000000000000000000000000000000..b8d24d5a3d0a1f75e106f8654622ab5ddc8531bd --- /dev/null +++ b/sysom_server/sysom_hotfix/conf/common.py @@ -0,0 +1,274 @@ +import os +import socket +import datetime +from pathlib import Path + + +def get_ip_address(): + """ip address""" + hostname = socket.gethostname() + return socket.gethostbyname(hostname) + + +BASE_DIR = Path(__file__).resolve().parent.parent +SECRET_KEY = 'django-insecure-^d8b9di9w&-mmsbpt@)o#e+2^z+^m4nhf+z8304%9@8y#ko46l' + +ALLOWED_HOSTS = ['*'] + +INSTALLED_APPS = [ + 'apps.hotfix', + + 'rest_framework', + 'corsheaders', + 'drf_yasg', # 在线API文档 + 'channels', + 'django_filters', + 'django_apscheduler', +] + +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.security.SecurityMiddleware', + 'django.middleware.common.CommonMiddleware', +] + +DEBUG = True + +# Mysql数据库 +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'sysom', + 'USER': 'sysom', + 'PASSWORD': 'sysom_admin', + 'HOST': '127.0.0.1', + 'PORT': '3306', + } +} + +ROOT_URLCONF = 'sysom_hotfix.urls' + +AUTH_USER_MODEL = 'accounts.User' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'sysom.wsgi.application' +ASGI_APPLICATION = 'sysom.asgi.application' + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + + +# Database +# https://docs.djangoproject.com/en/3.2/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'sysom', + 'USER': 'sysom', + 'PASSWORD': 'sysom_admin', + 'HOST': '127.0.0.1', + 'PORT': '3306', + } +} + + +# Password validation +# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +LANGUAGE_CODE = 'zh-hans' +TIME_ZONE = 'Asia/Shanghai' +USE_I18N = True +USE_L10N = True +USE_TZ = True +STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'index/status') + +# rest_framework settings +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': ( + # 'rest_framework.permissions.IsAuthenticated' + ), + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'apps.accounts.authentication.Authentication' + ], + 'UNAUTHENTICATED_USER': None, + 'DEFAULT_VERSIONING_CLASS': "rest_framework.versioning.URLPathVersioning", + 'DEFAULT_VERSION': 'v1', # 默认版本 + 'ALLOWED_VERSIONS': ['v1', 'v2'], # 允许的版本 + 'VERSION_PARAM': 'version', + + 'DEFAULT_RENDERER_CLASSES': ( + 'lib.renderers.SysomJsonRender', + ), + 'DEFAULT_PAGINATION_CLASS': 'lib.paginations.Pagination', + 'UNICODE_JSON': True, + 'EXCEPTION_HANDLER': 'lib.exception.exception_handler' +} + +JWT_AUTH = { + 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), +} + +SERVICE_SVG_PATH = os.path.join(BASE_DIR, 'netinfo') + +# upload file +MEDIA_URL = '/uploads/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads') + +SCRIPTS_DIR = os.path.join(BASE_DIR, 'service_scripts') + +SERVER_IP = get_ip_address() + +IS_MICRO_SERVICES = False # 是否微服务 + +# sysom node resource download dir +WEB_DIR = os.path.join(BASE_DIR.parent, 'sysom_web') +DOWNLOAD_DIR = os.path.join(WEB_DIR, 'download') + +################################################################## +# Hotfix Platform settings +################################################################## +HOTFIX_CONFIG_URL = "http://localhost:7003/api/v1/channel/config/get?name=migration_setting" +HOTFIX_CEC_URL = "redis://localhost:6379" +HOTFIX_FILE_STORAGE_REPO = "/usr/local/sysom/server/hotfix/hotfix-nfs" + +################################################################## +# SSH channel settings +################################################################## +SSH_CHANNEL_KEY_DIR = os.path.join(BASE_DIR.parent, 'conf', 'ssh') +if not os.path.exists(SSH_CHANNEL_KEY_DIR): + os.makedirs(SSH_CHANNEL_KEY_DIR) +SSH_CHANNEL_KEY_PRIVATE = os.path.join(SSH_CHANNEL_KEY_DIR, "sysom_id") +SSH_CHANNEL_KEY_PUB = os.path.join(SSH_CHANNEL_KEY_DIR, "sysom_id.pub") + +################################################################## +# Cec settings +################################################################## +SYSOM_CEC_URL = "redis://localhost:6379?cec_default_max_len=1000&cec_auto_mk_topic=true" +SYSOM_CEC_ALARM_TOPIC = "CEC-SYSOM-ALARM" +# 通道模块用于对外开放,投递操作的主题 +SYSOM_CEC_CHANNEL_TOPIC = "SYSOM_CEC_CHANNEL_TOPIC" +# 通道模块用于投递执行结果的主题 +SYSOM_CEC_CHANNEL_RESULT_TOPIC = "SYSOM_CEC_CHANNEL_RESULT_TOPIC" +# 用于分发插件系统相关事件的主题 +SYSOM_CEC_PLUGIN_TOPIC = "SYSOM_CEC_PLUGIN_TOPIC" +# API主机模块消费组 +SYSOM_CEC_API_HOST_CONSUMER_GROUP = "SYSOM_CEC_API_HOST_CONSUMER_GROUP" + +# 主机模块CEC配置 +SYSOM_HOST_LISTEN_TOPIC = "SYSOM_HOST_LISTEN_TOPIC" +SYSOM_HOST_CONSUME_GROUP = "SYSOM_HOST_CONSUME_GROUP" +SYSOM_HOST_CEC_URL = f"{SYSOM_CEC_URL}&channel_job_target_topic={SYSOM_CEC_CHANNEL_TOPIC}&channel_job_listen_topic={SYSOM_HOST_LISTEN_TOPIC}&channel_job_consumer_group={SYSOM_HOST_CONSUME_GROUP}" + + +SERVER_LOGS_FILE = os.path.join(BASE_DIR, 'logs', 'sys_om_info.log') +ERROR_LOGS_FILE = os.path.join(BASE_DIR, 'logs', 'sys_om_error.log') +if not os.path.exists(os.path.join(BASE_DIR, 'logs')): + os.makedirs(os.path.join(BASE_DIR, 'logs')) + + +# JWT Token Decode DIR +JWT_TOKEN_DECODE_DIR = os.path.join(BASE_DIR, 'lib', 'decode') +if not os.path.exists(JWT_TOKEN_DECODE_DIR): + os.makedirs(JWT_TOKEN_DECODE_DIR) + +# 格式:[2020-04-22 23:33:01][micoservice.apps.ready():16] [INFO] 这是一条日志: +# 格式:[日期][模块.函数名称():行号] [级别] 信息 +STANDARD_LOG_FORMAT = '[%(levelname).4s] -- %(asctime)s -- P_%(process) -- d_T_%(thread)d ' \ + '- <%(module)s:%(lineno)d>: %(message)s' +CONSOLE_LOG_FORMAT = '[%(levelname).4s] -- %(asctime)s -- P_%(process) -- d_T_%(thread)d ' \ + '- <%(module)s:%(lineno)d>: %(message)s' + +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'standard': { + 'format': STANDARD_LOG_FORMAT + }, + 'console': { + 'format': CONSOLE_LOG_FORMAT, + 'datefmt': '%Y-%m-%d %H:%M:%S', + }, + 'file': { + 'format': CONSOLE_LOG_FORMAT, + 'datefmt': '%Y-%m-%d %H:%M:%S', + }, + }, + 'handlers': { + 'file': { + 'level': 'INFO', + 'class': 'logging.handlers.RotatingFileHandler', + 'filename': SERVER_LOGS_FILE, + 'maxBytes': 1024 * 1024 * 100, # 100 MB + 'backupCount': 5, # 最多备份5个 + 'formatter': 'standard', + 'encoding': 'utf-8', + }, + 'error': { + 'level': 'ERROR', + 'class': 'logging.handlers.RotatingFileHandler', + 'filename': ERROR_LOGS_FILE, + 'maxBytes': 1024 * 1024 * 100, # 100 MB + 'backupCount': 3, # 最多备份3个 + 'formatter': 'standard', + 'encoding': 'utf-8', + }, + 'console': { + 'level': 'INFO', + 'class': 'logging.StreamHandler', + 'formatter': 'console', + } + }, + 'loggers': { + # default日志 + '': { + 'handlers': ['console', 'error', 'file'], + 'level': 'INFO', + }, + 'django': { + 'handlers': ['console', 'error', 'file'], + 'level': 'INFO', + }, + 'scripts': { + 'handlers': ['console', 'error', 'file'], + 'level': 'INFO', + }, + # 数据库相关日志 + 'django.db.backends': { + 'handlers': [], + 'propagate': True, + 'level': 'INFO', + }, + } +} diff --git a/sysom_server/sysom_hotfix/conf/develop.py b/sysom_server/sysom_hotfix/conf/develop.py new file mode 100644 index 0000000000000000000000000000000000000000..55a9b74d4e988f25d79948ff07002a1a01baf0ef --- /dev/null +++ b/sysom_server/sysom_hotfix/conf/develop.py @@ -0,0 +1,7 @@ +from .common import * + +''' +开发环境配置项 +''' + +DEBUG = True \ No newline at end of file diff --git a/sysom_server/sysom_hotfix/conf/hotfix_gunicorn.py b/sysom_server/sysom_hotfix/conf/hotfix_gunicorn.py new file mode 100644 index 0000000000000000000000000000000000000000..0cb69d1e5a4ae992b3ba58142cc60b98a026f9b5 --- /dev/null +++ b/sysom_server/sysom_hotfix/conf/hotfix_gunicorn.py @@ -0,0 +1,20 @@ +''' +Hotfix Service Gunicorn Settings +''' +workers = 2 # 指定工作进程数 + +threads = 3 + +bind = '127.0.0.1:7007' + +# worker_class = 'gevent' # 工作模式线程, 默认为sync模式 + +max_requests = 2000 # 设置最大并发数量为2000 (每个worker处理请求的工作线程) + +accesslog = '/usr/local/sysom/server/logs/sysom-hotfix-access.log' + +loglevel = 'error' + +proc_name = 'hotfix_service' + +raw_env = 'HOTFIX_SETTINGS_MODULE=sysom_hotfix.settings' diff --git a/sysom_server/sysom_hotfix/conf/product.py b/sysom_server/sysom_hotfix/conf/product.py new file mode 100644 index 0000000000000000000000000000000000000000..d286e3707bc30ed783b0c1f5dba40e48abaa72ae --- /dev/null +++ b/sysom_server/sysom_hotfix/conf/product.py @@ -0,0 +1,7 @@ +from .common import * + +''' +生产环境配置项 +''' + +DEBUG = False diff --git a/sysom_server/sysom_hotfix/conf/testing.py b/sysom_server/sysom_hotfix/conf/testing.py new file mode 100644 index 0000000000000000000000000000000000000000..3039c80c49e162334ccdc91cfb151a3c40b0cd79 --- /dev/null +++ b/sysom_server/sysom_hotfix/conf/testing.py @@ -0,0 +1,6 @@ +from .common import * + +''' +测试环境配置项 +''' +DEBUG = True diff --git a/sysom_server/sysom_hotfix/lib/__init__.py b/sysom_server/sysom_hotfix/lib/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..0cd9337f04c88ed36f9fde61c38f8c263525c4a3 --- /dev/null +++ b/sysom_server/sysom_hotfix/lib/__init__.py @@ -0,0 +1,8 @@ +# -*- encoding: utf-8 -*- +""" +@File : __init__.py.py +@Time : 2021/10/28 11:04 +@Author : DM +@Email : smmic@isoftstone.com +@Software: PyCharm +""" \ No newline at end of file diff --git a/sysom_server/sysom_hotfix/lib/authentications.py b/sysom_server/sysom_hotfix/lib/authentications.py new file mode 100644 index 0000000000000000000000000000000000000000..582821a24d69aba5a75c804c7d4282e202580d97 --- /dev/null +++ b/sysom_server/sysom_hotfix/lib/authentications.py @@ -0,0 +1,50 @@ +import logging +import os +from typing import List +from django.conf import settings +from django.utils.translation import ugettext as _ +from rest_framework.exceptions import AuthenticationFailed +from rest_framework.request import Request +from rest_framework.authentication import BaseAuthentication +from .utils import import_module + + +logger = logging.getLogger(__name__) + + +def get_jwt_decode_classes() -> List[BaseAuthentication]: + jwt_decode_classes = [] + import_strings = [ + f'lib.decode.{f.replace(".py", "")}' for f in os.listdir(settings.JWT_TOKEN_DECODE_DIR) + ] + for string in import_strings: + module = import_module(string) + try: + m = getattr(module, 'JWTTokenDecode') + jwt_decode_classes.append(m) + except Exception as exc: + logger.warn(exc) + return jwt_decode_classes + + +def decode_token(token: str) -> dict: + error_message, success, result = "", False, {} + for auth_class in get_jwt_decode_classes(): + result, success = auth_class().decode(token) + if not success: + error_message += result + else: + break + if not success: + raise AuthenticationFailed(error_message) + return result + + +class TaskAuthentication(BaseAuthentication): + def authenticate(self, request: Request): + token = request.META.get('HTTP_AUTHORIZATION') + payload = decode_token(token) + payload['token'] = token + if 'sub' in payload: + payload['id'] = int(payload['sub']) + return payload, _ diff --git a/sysom_server/sysom_hotfix/lib/base_model.py b/sysom_server/sysom_hotfix/lib/base_model.py new file mode 100644 index 0000000000000000000000000000000000000000..07d6aacae53288db56e509e8e84779daac101727 --- /dev/null +++ b/sysom_server/sysom_hotfix/lib/base_model.py @@ -0,0 +1,20 @@ +# -*- encoding: utf-8 -*- +""" +@File : base_model.py +@Time : 2021/10/28 11:04 +@Author : DM +@Email : smmic@isoftstone.com +@Software: PyCharm +""" + +from django.db import models +from lib.utils import human_datetime + + +class BaseModel(models.Model): + """abstract model""" + created_at = models.CharField(max_length=20, default=human_datetime, verbose_name="创建时间") + deleted_at = models.CharField(max_length=20, null=True) + + class Meta: + abstract = True diff --git a/sysom_server/sysom_hotfix/lib/decode/sysom_decode.py b/sysom_server/sysom_hotfix/lib/decode/sysom_decode.py new file mode 100644 index 0000000000000000000000000000000000000000..12c7c1a06b69542f414da73b7defa6768b538b2f --- /dev/null +++ b/sysom_server/sysom_hotfix/lib/decode/sysom_decode.py @@ -0,0 +1,17 @@ +import jwt +from django.conf import settings + + +class JWTTokenDecode: + """SYSOM TOken解析认证""" + def decode(self, token): + r, s = None, False + try: + r, s = jwt.decode(token, key=settings.SECRET_KEY, algorithms='HS256'), True + except jwt.exceptions.ExpiredSignatureError as e: + r = f'令牌失效: {e}' + except jwt.exceptions.DecodeError as e: + r = f'令牌校验失败: {e}' + except jwt.exceptions.InvalidAlgorithmError as e: + r = f'令牌不合法: {e}' + return r, s \ No newline at end of file diff --git a/sysom_server/sysom_hotfix/lib/excel.py b/sysom_server/sysom_hotfix/lib/excel.py new file mode 100644 index 0000000000000000000000000000000000000000..96f57e7ce4cbf1068a03bacd76c3bbcff7ddce5f --- /dev/null +++ b/sysom_server/sysom_hotfix/lib/excel.py @@ -0,0 +1,55 @@ +import logging +from io import BytesIO +import pandas as pd +from xlwt import Workbook +from django.http import StreamingHttpResponse, HttpResponse + + +logger = logging.getLogger(__name__) + + +class Excel: + def __init__(self, file, row_dict: dict) -> None: + self.file = file + self.row_dict = row_dict + self._file_io = None + self.open() + + def open(self): + if not self._file_io: + self._file_io = pd.read_excel(self.file) + + def values(self): + content = list() + for _, row in self._file_io.iterrows(): + item = {} + for k, v in self.row_dict.items(): + item[k] = row[v] + content.append(item) + return content + + @staticmethod + def export(datalist: list = ..., sheetname: str = 'sheet1', excelname: str = 'host'): + """ + Export excel + Type xlsx + """ + response = HttpResponse(content_type='application/vnd.ms-excel') + response['Content-Disposition'] = 'attachment; filename=%s' % excelname+'.xlsx' + + workbook = Workbook(encoding='utf-8') + sheet = workbook.add_sheet(sheetname) + + for i, k in enumerate([k for k in datalist[0].keys()]): + sheet.write(0, i, k) + + for r, data in enumerate(datalist): + for i, key in enumerate([k for k in data.keys()]): + sheet.write(r+1, i, data[key]) + + io = BytesIO() + workbook.save(io) + io.seek(0) + + response.write(io.getvalue()) + return response diff --git a/sysom_server/sysom_hotfix/lib/exception.py b/sysom_server/sysom_hotfix/lib/exception.py new file mode 100644 index 0000000000000000000000000000000000000000..f660a9e51d5a17d5d3c0ae13c00f9dec195bd57b --- /dev/null +++ b/sysom_server/sysom_hotfix/lib/exception.py @@ -0,0 +1,63 @@ +import logging +import traceback + +from django.db.models import ProtectedError +from rest_framework.views import set_rollback +from rest_framework import exceptions, status +from rest_framework.exceptions import APIException as DRFAPIException, AuthenticationFailed, NotAuthenticated + +from .response import ErrorResponse + + +logger = logging.getLogger(__name__) + + +class APIException(Exception): + def __init__(self, code=400, message='API异常', args=('API异常',)): + self.code = code + self.message = message + self.args = args + + def __str__(self): + return self.message + + +class FileNotFoundException(Exception): + def __init__(self, code=404, message='文件不存在'): + self.code = code + self.message = message + + def __str__(self): + return self.message + + +def exception_handler(exc, context): + """自定义异常处理""" + msg = '' + code = 400 + + if isinstance(exc, FileNotFoundException): + code = exc.code + msg = exc.message + if isinstance(exc, AuthenticationFailed): + code = 403 + msg = exc.detail + elif isinstance(exc, NotAuthenticated): + code = 402 + msg = exc.detail + elif isinstance(exc, DRFAPIException): + set_rollback() + # print(exc.detail) + # msg = {str(e) for e in exc.detail} + msg = exc.detail + elif isinstance(exc, exceptions.APIException): + set_rollback() + msg = exc.detail + elif isinstance(exc, ProtectedError): + set_rollback() + msg = "删除失败:该条数据与其他数据有相关绑定" + elif isinstance(exc, Exception): + logger.error(traceback.format_exc()) + msg = str(exc) # 原样输出错误 + + return ErrorResponse(msg=msg, code=code, status=code) diff --git a/sysom_server/sysom_hotfix/lib/paginations.py b/sysom_server/sysom_hotfix/lib/paginations.py new file mode 100644 index 0000000000000000000000000000000000000000..b14639bb2a3fcd04760920db1308d1af6d614410 --- /dev/null +++ b/sysom_server/sysom_hotfix/lib/paginations.py @@ -0,0 +1,26 @@ +''' +@File: paginations.py +@Time: 2021-12-14 13:46:02 +@Author: DM +@Desc: Local Paginations Class +''' + +from rest_framework.pagination import PageNumberPagination +from lib.response import success + + +class Pagination(PageNumberPagination): + page_query_param = "current" + page_size_query_param = "pageSize" + + def paginate_queryset(self, queryset, request, view=None): + self.max_page_size = queryset.count() + return super().paginate_queryset(queryset, request, view=view) + + def get_paginated_response(self, data): + return success(message="获取成功", result=data, total=self.page.paginator.count) + + def get_page_size(self, request): + if not request.query_params.get(self.page_size_query_param, None): + return self.max_page_size + return super().get_page_size(request) \ No newline at end of file diff --git a/sysom_server/sysom_hotfix/lib/renderers.py b/sysom_server/sysom_hotfix/lib/renderers.py new file mode 100644 index 0000000000000000000000000000000000000000..b24f586fc6354bb1b3c5e3601bd6d5220a8a44a2 --- /dev/null +++ b/sysom_server/sysom_hotfix/lib/renderers.py @@ -0,0 +1,48 @@ +import logging +from django.contrib.auth import get_user_model +from django.shortcuts import get_object_or_404 +from django.core.handlers.asgi import ASGIRequest +from rest_framework.renderers import JSONRenderer +from rest_framework.request import Request + +from apps.accounts.models import HandlerLog + + +logger = logging.getLogger(__name__) +User = get_user_model() + + +class SysomJsonRender(JSONRenderer): + def render(self, data, accepted_media_type=None, renderer_context=None): + if renderer_context: + request = renderer_context.get('request', None) + view = renderer_context.get('view', None) + response = renderer_context.get('response', None) + self.before_response_save_log(request, view, response) + return super().render(data, accepted_media_type, renderer_context) + + def before_response_save_log(self, request: Request, view, response): + user = getattr(request, 'user') or get_object_or_404(User, pk=1) + request: ASGIRequest = getattr(request, '_request', None) + method = request.method + + result = response.data + kwargs = { + 'request_ip': request.META.get('REMOTE_ADDR', None), + 'request_url': request.path, + 'request_browser_agent': request.headers.get('User-Agent', ''), + 'request_method': method, + 'handler_view': view.__class__.__name__, + 'response_status': getattr(response, 'status_code', 200), + } + if 'auth' in request.path: + kwargs['request_option'] = 0 + if result.get('code') == 200: + kwargs['user_id'] = result['data']['id'] + else: + kwargs['request_option'] = 1 + kwargs['user'] = user + try: + HandlerLog.objects.create(**kwargs) + except: + pass diff --git a/sysom_server/sysom_hotfix/lib/response.py b/sysom_server/sysom_hotfix/lib/response.py new file mode 100644 index 0000000000000000000000000000000000000000..a2311a42c2d1fafd7106a1fcd7ed7ae81c9a5467 --- /dev/null +++ b/sysom_server/sysom_hotfix/lib/response.py @@ -0,0 +1,68 @@ +from rest_framework.response import Response +from rest_framework import status +from django.http import FileResponse + + +def _response(data=None, status=None): + return Response(data=data, status=status) + + +def success(result, message="success", success=True, code=status.HTTP_200_OK, **kwargs): + data = { + "code": code, + "message": message, + "data": result, + "success": success + } + data.update(kwargs) + return _response(data=data, status=code) + + +def not_found(code=status.HTTP_404_NOT_FOUND, success=False, message="Not Found"): + data = { + "code": code, + "message": message, + "success": success, + } + + return _response(data=data, status=code) + + +def not_permission(code=status.HTTP_403_FORBIDDEN, success=False, message="Not Permission"): + data = { + "code": code, + "success": success, + "message": message + } + return _response(data=data, status=code) + + +def other_response(result=dict(), message="", success=True, code=status.HTTP_200_OK, **kwargs): + data = { + "code": code, + "message": message, + "data": result, + "success": success + } + data.update(kwargs) + return _response(data=data, status=code) + + +class ErrorResponse(Response): + """ + 标准响应错误的返回,ErrorResponse(msg='xxx') + 默认错误码返回400, 也可以指定其他返回码:ErrorResponse(code=xxx) + """ + + def __init__(self, data=None, msg='error', code=400, status=None, template_name=None, headers=None, + exception=False, content_type=None): + std_data = { + "code": code, + "data": data or {}, + "message": msg + } + super().__init__(std_data, status, template_name, headers, exception, content_type) + + +class FileResponseAlter(FileResponse): + pass diff --git a/sysom_server/sysom_hotfix/lib/ssh.py b/sysom_server/sysom_hotfix/lib/ssh.py new file mode 100644 index 0000000000000000000000000000000000000000..9de53fc47747d071aa8d85e3c1ca82fc71d656e6 --- /dev/null +++ b/sysom_server/sysom_hotfix/lib/ssh.py @@ -0,0 +1,100 @@ +import logging +from typing import Callable +import paramiko +from io import StringIO +from paramiko.client import SSHClient, AutoAddPolicy +from paramiko.rsakey import RSAKey + +DEFAULT_CONNENT_TIMEOUT = 5 # 默认ssh链接超时时间 5s +DEFAULT_NODE_USER = 'root' # 默认节点用户名 root + +logger = logging.getLogger(__name__) + + +class SSH: + """A SSH client used to run command in remote node + + args: + hostname(str): Host name + + Keyword Args: + username(str): User name, default 'root' + port(str): SSH communicate port, default 22 + connect_timeout(int): Connection timeout duration, default 5s + password(str) + """ + + # key_pair cached the key pair generated by initialization stage + _key_pair = {} + _private_key_getter: Callable[[], str] = None + _public_key_getter: Callable[[], str] = None + + def __init__(self, hostname: str, **kwargs) -> None: + self.connect_args = { + 'hostname': hostname, + 'username': kwargs.get('username', DEFAULT_NODE_USER), + 'port': kwargs.get('port', 22), + 'timeout': kwargs.get('timeout', DEFAULT_CONNENT_TIMEOUT), + } + if 'password' in kwargs and kwargs['password'] is not None: + self.connect_args['password'] = kwargs.get('password') + else: + if SSH._private_key_getter is None: + raise Exception("_private_key_getter not set") + self.connect_args['pkey'] = RSAKey.from_private_key( + StringIO(SSH._private_key_getter()) + ) + + self._client: SSHClient = self.client() + + def client(self): + try: + client = SSHClient() + client.set_missing_host_key_policy(AutoAddPolicy) + client.connect(**self.connect_args) + return client + except paramiko.AuthenticationException: + raise Exception('authorization fail, password or pkey error!') + except: + raise Exception('authorization fail!') + + @classmethod + def set_private_key_getter(cls, private_key_getter: Callable[[], str]): + cls._private_key_getter = private_key_getter + + @classmethod + def set_public_key_getter(cls, public_key_getter: Callable[[], str]): + cls._public_key_getter = public_key_getter + + def run_command(self, command): + if self._client: + ssh_session = self._client.get_transport().open_session() + ssh_session.set_combine_stderr(True) + ssh_session.exec_command(command) + stdout = ssh_session.makefile("rb", -1) + statue = ssh_session.recv_exit_status() + output = stdout.read().decode() + return statue, output + else: + raise Exception('No client!') + + def add_public_key(self): + if self._public_key_getter is None: + raise Exception("_public_key_getter not set") + public_key = SSH._public_key_getter() + command = f'mkdir -p -m 700 ~/.ssh && \ + echo {public_key!r} >> ~/.ssh/authorized_keys && \ + chmod 600 ~/.ssh/authorized_keys' + statue, _ = self.run_command(command) + if statue != 0: + raise Exception('add public key faild!') + + @staticmethod + def validate_ssh_host(ip: str, password: str, port: int = 22, username: str = 'root'): + try: + ssh = SSH(hostname=ip, password=password, + port=port, username=username, timeout=2) + ssh.add_public_key() + return True, 'authorization success' + except Exception as e: + return False, f'error: {e}' diff --git a/sysom_server/sysom_hotfix/lib/utils.py b/sysom_server/sysom_hotfix/lib/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..c553c24590b0b7eb558d513bed073b7a8ad0f402 --- /dev/null +++ b/sysom_server/sysom_hotfix/lib/utils.py @@ -0,0 +1,198 @@ + +# -*- encoding: utf-8 -*- +""" +@File : utils.py +@Time : 2021/10/28 11:09 +@Author : DM +@Software: PyCharm +""" +import time +import uuid as UUID +from typing import List +import json +import logging +import jwt +import requests + +from importlib import import_module +from datetime import datetime, date as datetime_date +from decimal import Decimal + +from django.conf import settings +from apscheduler.schedulers.background import BackgroundScheduler +from paramiko.rsakey import RSAKey +from io import StringIO + + +logger = logging.getLogger(__name__) + +job_defaults = { + 'max_instances': 10, + 'misfire_grace_time': None, + 'coalesce': True, +} +scheduler = BackgroundScheduler(job_defaults=job_defaults) +scheduler.start() + + +CHAR_SET = ("a", "b", "c", "d", "e", "f", + "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", + "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", + "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "I", + "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", + "W", "X", "Y", "Z") + + +def human_datetime(date=None): + if date: + assert isinstance(date, datetime) + else: + date = datetime.now() + return date.strftime('%Y-%m-%d %H:%M:%S') + + +# 转换时间格式到字符串 +def datetime_str(date=None): + return datetime.strptime(date, "%Y-%m-%d %H:%M:%S") + + +# 日期json序列化 +class DateTimeEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, datetime): + return o.strftime('%Y-%m-%d %H:%M:%S') + elif isinstance(o, datetime_date): + return o.strftime('%Y-%m-%d') + elif isinstance(o, Decimal): + return float(o) + return json.JSONEncoder.default(self, o) + + +def get_request_real_ip(headers: dict): + x_real_ip = headers.get('x-forwarded-for') + if not x_real_ip: + x_real_ip = headers.get('x-real-ip', '') + return x_real_ip.split(',')[0] + + +def uuid_36(): + """ + 返回36字符的UUID字符串(十六进制,含有-) bc5debab-95c3-4430-933f-2e3b6407ac30 + :return: + """ + return str(UUID.uuid4()) + + +def uuid_32(): + """ + 返回32字符的UUID字符串(十六进制) bc5debab95c34430933f2e3b6407ac30 + :return: + """ + return uuid_36().replace('-', '') + + +def uuid_8(): + """ + 返回8字符的UUID字符串(非进制) 3FNWjtlD + :return: + """ + s = uuid_32() + result = '' + for i in range(0, 8): + sub = s[i * 4: i * 4 + 4] + x = int(sub, 16) + result += CHAR_SET[x % 0x3E] + return result + + +def url_format_dict(url_params: str): + """转化查询参数为dict""" + result = dict() + try: + for item in [{p.split('=')[0]: p.split('=')[1]} for p in url_params.split('&')]: + result.update(item) + except Exception as e: + logger.error(str(e)) + return result + + +def import_string(dotted_path: str): + """ + 优化import_module + Args: + dotted_path 动态导包路径 + Return Package + """ + try: + module_path, class_name = dotted_path.rsplit('.', 1) + except ValueError as err: + raise ImportError("%s doesn't look like a module path" % dotted_path) from err + module = import_module(dotted_path) + + try: + getattr(module, 'Channel') + return module + except AttributeError as err: + raise ImportError('Module "%s" does not define a "%s" attribute/class' % ( + module_path, class_name) + ) from err + +def valid_params(require_params: dict, current_params: dict) -> List[str]: + missing_param_list = [] + for param in require_params: + if param not in current_params: + missing_param_list.append(param) + return missing_param_list + +def generate_key(): + key_obj = StringIO() + key = RSAKey.generate(2048) + key.write_private_key(key_obj) + return key_obj.getvalue(), 'ssh-rsa ' + key.get_base64() + + +class HTTP: + @classmethod + def request(cls, method: str, url: str, token, data: dict, **kwargs): + status, result = 0, '' + headers = { + 'Authorization': token, + 'Content-Type': 'application/json' + } + method = method.upper() + methods = ['GET', 'POST', 'PATCH', 'PUT', 'DELETE'] + if method not in methods: + raise Exception('请求方式不存在!') + data = json.dumps(data) + for _ in range(3): + try: + response = requests.request(method=method, url=url, json=None, headers=headers, data=data, **kwargs) + if response.status_code != 200: + status = response.status_code + data = response.json() + result = data['message'] + break + else: + resp = response.json() + status, result = response.status_code, resp['data'] + break + except requests.exceptions.ConnectTimeout as e: + logger.info('Request Timeout, retry...') + status = 400 + result = '请求超时, 重试三次' + + return status, result + + +class JWT: + @staticmethod + def _encode(payload: dict, exp: int=60 * 5): + """ + 生成JWT Token + :args + payload 载体 + exp 过期时间 (单位秒) 默认时间5分钟 + """ + payload['exp'] = time.time() + exp + # 默认不可逆加密算法为HS256 + return jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256") diff --git a/sysom_server/sysom_hotfix/manage.py b/sysom_server/sysom_hotfix/manage.py new file mode 100755 index 0000000000000000000000000000000000000000..8ea5cac875cb675748406120d2aff829163c560f --- /dev/null +++ b/sysom_server/sysom_hotfix/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sysom_migration.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/sysom_web/config/routes.js b/sysom_web/config/routes.js index d0237d13b5e322e792d0ae479d7c4dd30edf7e8d..680c583be444516d6dc712d83d9ec099fcb88c73 100644 --- a/sysom_web/config/routes.js +++ b/sysom_web/config/routes.js @@ -261,6 +261,25 @@ export default [ }, ], }, + { + path: '/hotfix', + name: 'hotfix', + routes: [ + { + path: '/hotfix', + redirect: '/hotfix/make', + }, + { + path: '/hotfix/make', + name: 'make', + component: './hotfix/Make', + }, + { + path: '/hotfix/hotfix_log/:id?', + component: './hotfix/HotfixLog' + }, + ] + }, { path: '/', redirect: '/welcome', diff --git a/sysom_web/src/locales/zh-CN/menu.js b/sysom_web/src/locales/zh-CN/menu.js index 57917a44177c17814fee66c0bee3cfad9464d9f6..b39d7609d8d815a22128610dfa423ddaf4a12177 100644 --- a/sysom_web/src/locales/zh-CN/menu.js +++ b/sysom_web/src/locales/zh-CN/menu.js @@ -84,5 +84,7 @@ export default { 'menu.journal.alarm': '告警日志', 'menu.journal.task': '任务日志', 'menu.security': '安全中心', - 'menu.security.list': '漏洞中心' + 'menu.security.list': '漏洞中心', + 'menu.hotfix': '热补丁中心', + 'menu.hotfix.make': '热补丁制作' }; diff --git a/sysom_web/src/locales/zh-CN/pages.js b/sysom_web/src/locales/zh-CN/pages.js index 9caf6ad214fb5a9cfaddeee0cc0843e93240c19e..e7e52c2000c5d588dd816f834d6be364fdc81551 100644 --- a/sysom_web/src/locales/zh-CN/pages.js +++ b/sysom_web/src/locales/zh-CN/pages.js @@ -146,4 +146,19 @@ export default { 'pages.security.Historical.fix_fail': 'CVE修复失败,失败原因:', 'pages.security.notification.fix.success': '系统漏洞已修复', 'pages.security.notification.fix.success.content': '如您正在运行漏洞涉及到的服务,建议您重启相关服务使漏洞修复生效。', + 'pages.hotfix.title': '热补丁列表', + 'pages.hotfix.created_at': '创建时间', + 'pages.hotfix.kernel_version': '内核版本', + 'pages.hotfix.creator': '创建人', + 'pages.hotfix.progress': '进度', + 'pages.hotfix.patch_path': '补丁路径', + 'pages.hotfix.delete_hotfix_not_exist': '要删除的hotfixID不存在', + 'pages.hotfix.operation': '操作', + 'pages.hotfix.delete': '删除', + 'pages.hotfix.building_status': '构建状态', + 'pages.hotfix.normal': '转正式包', + 'pages.hotfix.os_type': '操作系统', + 'pages.hotfix.patch_name': '补丁名称', + 'pages.hotfix.upload': '文件上传', + 'pages.hotfix.download': "下载", }; \ No newline at end of file diff --git a/sysom_web/src/pages/hotfix/HotfixLog/index.jsx b/sysom_web/src/pages/hotfix/HotfixLog/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..526710bb0098e4648ecad51466fb4d7c23ba1a85 --- /dev/null +++ b/sysom_web/src/pages/hotfix/HotfixLog/index.jsx @@ -0,0 +1,23 @@ +import { getHotfixLog } from '../service'; +import { useState, useEffect } from 'react'; + +import './index.less' + +const HotfixLog = (props) => { + const [data, setData] = useState(); + const hotFixID = props.match.params.id + + useEffect(() => { + getHotfixLog(hotFixID).then(res => { + setData(res.data) + }) + }, []) + + return ( +
+ {data} +
+ ); +}; + +export default HotfixLog \ No newline at end of file diff --git a/sysom_web/src/pages/hotfix/HotfixLog/index.less b/sysom_web/src/pages/hotfix/HotfixLog/index.less new file mode 100644 index 0000000000000000000000000000000000000000..2933005612822aed79deb87bf9c9ad2105472a7f --- /dev/null +++ b/sysom_web/src/pages/hotfix/HotfixLog/index.less @@ -0,0 +1,5 @@ +@import '~antd/es/style/themes/default.less'; + +.HotfixLog { + white-space: pre-line; +} \ No newline at end of file diff --git a/sysom_web/src/pages/hotfix/Make/index.jsx b/sysom_web/src/pages/hotfix/Make/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e76a4004b479a2d99c74225c7cacdba51e420847 --- /dev/null +++ b/sysom_web/src/pages/hotfix/Make/index.jsx @@ -0,0 +1,231 @@ +import { useRef } from 'react'; +import { useIntl, FormattedMessage } from 'umi'; +import { PageContainer } from '@ant-design/pro-layout'; +import ProTable from '@ant-design/pro-table'; +import { Popconfirm, message, Switch, Upload, Button, Select, Form, Collapse} from 'antd'; +import { getHotfixList, delHotfix, setNormal, uploadProps, normFile, createHotfix, downloadHotfixFile } from '../service'; +import { UploadOutlined } from '@ant-design/icons'; +import { async } from '@antv/x6/es/registry/marker/async'; +import { DownloadOutlined } from '@ant-design/icons'; + +const handleDelHotfix = async (record) => { + const hide = message.loading('正在删除'); + const token = localStorage.getItem('token'); + try { + let res = await delHotfix(record.id, token); + hide(); + if (res.code == 200) { + message.success('删除成功'); + return true; + } else { + message.error(`删除失败: ${res.message}`); + return false; + } + } catch (error) { + hide(); + return false; + } +} + +const changeNormal = (record) => { + const token = localStorage.getItem('token'); + setNormal(record.id, token) +}; + +const submitHotfix = (params) => { + const token = localStorage.getItem('token'); + createHotfix(token, params) + console.log(params) +} + +const downloadHotfix = async (record) => { + const res = await downloadHotfixFile(record.id); + if (res) { + const url = window.URL.createObjectURL(new Blob([res])); + const link = document.createElement('a'); //创建a标签 + link.style.display = 'none'; + link.href = url; // 设置a标签路径 + link.download = res.response.headers.get('content-disposition').split("attachment;filename=")[1]; //设置文件名, 也可以这种写法 (link.setAttribute('download', '名单列表.xls'); + document.body.appendChild(link); + link.click(); + URL.revokeObjectURL(link.href); // 释放 URL对象 + document.body.removeChild(link); + console.log(res.response.headers.get('content-disposition').split("attachment;filename=")[1]) + } + console.log("downloadHotfixFile: ", record.id) +} + +const HotfixList = () => { + const actionRef = useRef(); + const intl = useIntl(); + + const columns = [ + { + title: , + dataIndex: 'created_at', + valueType: 'message', + hideInSearch: true, + }, + { + title: , + dataIndex: 'os_type', + key: 'os_type', + dataIndex: 'os_type', + hideInTable: true, + render: (_, record) => [ + ], + renderFormItem: (item, _a, form) => { + return