diff --git a/settings/dev.py b/settings/dev.py index 7578975..9f25c05 100644 --- a/settings/dev.py +++ b/settings/dev.py @@ -53,6 +53,15 @@ LOGGING = { 'backupCount': 5, 'formatter': 'verbose', }, + 'views': { + 'level': 'DEBUG', + 'class': 'logging.handlers.RotatingFileHandler', + 'filename': os.path.join(os.path.dirname(BASE_DIR), 'logs/views.log'), + 'encoding': 'utf-8', + 'maxBytes': 1024 * 1024 * 5, + 'backupCount': 5, + 'formatter': 'verbose', + }, }, 'loggers': { 'root': { @@ -64,6 +73,11 @@ LOGGING = { 'handlers': ['common'], 'level': 'DEBUG', 'propagate': False, + }, + 'views': { # 视图的日志 + 'handlers': ['views'], + 'level': 'DEBUG', + 'propagate': False, } } } diff --git a/src/api/v1/asset.py b/src/api/v1/asset.py index 2e63243..de4b111 100644 --- a/src/api/v1/asset.py +++ b/src/api/v1/asset.py @@ -13,6 +13,9 @@ api.add_resource(views.HostDetailViews, '/host//', endpoint="host-det api.add_resource(views.MySQLInstanceViews, '/database/mysql/', endpoint="db-mysql") api.add_resource(views.MySQLInstanceDetail, '/database/mysql//', endpoint="db-mysql-detail") +# 数据实例中的库详情 +api.add_resource(views.DatabaseViews, '/database//db/', endpoint="db-database") +api.add_resource(views.DatabaseDetailViews, '/database//db//', endpoint="db-database-detail") api.add_resource(views.RedisInstanceViews, '/database/redis/', endpoint="db-redis") api.add_resource(views.RedisInstanceDetail, '/database/redis//', endpoint="db-redis-detail") diff --git a/src/common/views.py b/src/common/views.py index 89768e1..3bbacde 100644 --- a/src/common/views.py +++ b/src/common/views.py @@ -1,10 +1,13 @@ +import logging from typing import List, Dict -from flask import request, current_app as app +from flask import request from flask_restful import abort, Resource, marshal, fields, reqparse from common.utils import abort_response +logger = logging.getLogger("views") + def parse_uniq_fields(args, many_field) -> List[Dict]: """ 注意,请再请求解析中判断必须的字段!!! @@ -132,7 +135,7 @@ class ModelViewBase(Resource): assert int(val, 16), f"{key} 不合法" assert model.objects(**{pk: val}).first(), f"{key} 不存在" except (AssertionError, ValueError, TypeError): - app.logger.exception(f"{key} 异常") + logger.exception(f"{key} 异常") abort_response(400, 1002, msg=f"{key} 不存在") def validate_relation_fields(self, args): @@ -190,7 +193,7 @@ class ListMixin(ModelViewBase): except: # 返回空 queryset queryset = self.model.objects.none() - app.logger.exception(f"查询出错 {self.model} query_params={query_params}") + logger.exception(f"查询出错 {self.model} query_params={query_params}") return queryset def get(self): @@ -216,7 +219,7 @@ class CreateMixin(ModelViewBase): """创建前钩子,接收参数,可以对参数进行处理,最后保存此方法返回的数据""" return args - def post(self): + def post(self, *args, **kwargs): """创建对象""" assert self.request_parse is not None, "缺少校验规则" @@ -239,7 +242,7 @@ class CreateMixin(ModelViewBase): obj = self.model(**validated_data) obj.save() except Exception as e: - app.logger.exception(f"{self.model} 创建对象失败! data={args}") + logger.exception(f"{self.model} 创建对象失败! data={args}") abort_response(500, 1500, msg=f"保存对象失败!{str(e)}") return # 返回创建信息 @@ -285,7 +288,7 @@ class UpdateMixin(ModelViewBase): # 重新读取数据 obj.reload() except Exception as e: - app.logger.exception(f"{self.model} 保存对象失败!pk={pk} data={args}") + logger.exception(f"{self.model} 保存对象失败!pk={pk} data={args}") abort_response(500, 1500, msg=f"保存对象失败!{str(e)}") return diff --git a/src/controller/asset/parsers.py b/src/controller/asset/parsers.py index 3abcf7f..a83d25e 100644 --- a/src/controller/asset/parsers.py +++ b/src/controller/asset/parsers.py @@ -44,6 +44,19 @@ class DatabaseServerParse: self.request_parse.add_argument("labels", required=False, type=dict, location='json') +class DatabaseParse: + request_parse = None + + def init_parse(self): + self.request_parse = reqparse.RequestParser() + self.request_parse.add_argument("username", type=str, location='json') + self.request_parse.add_argument("password", type=str, location='json') + + self.request_parse.add_argument("data", required=False, type=dict, location='json') + self.request_parse.add_argument("tags", required=False, type=list, location='json') + self.request_parse.add_argument("labels", required=False, type=dict, location='json') + + class MiddlewareParse: request_parse = None # 给子类用的唯一字段,用于校验 diff --git a/src/controller/asset/views.py b/src/controller/asset/views.py index c64c4dd..e7834a1 100644 --- a/src/controller/asset/views.py +++ b/src/controller/asset/views.py @@ -1,12 +1,18 @@ import datetime +import logging -from flask_restful import reqparse +from flask_restful import reqparse, marshal, Resource from models.asset import fields as assetField from models.asset import models as assetModel -from common.views import ListCreateViewSet, DetailViewSet +from common.views import ListCreateViewSet, DetailViewSet, CreateMixin, UpdateMixin, DestroyMixin from common.permission import session_or_token_required +from common.utils import abort_response from controller.asset import parsers +from common.crypto import quick_crypto +from settings.common import SECRET_KEY + +logger = logging.getLogger("views") class HostViews(parsers.HostParse, ListCreateViewSet): @@ -37,7 +43,8 @@ class MySQLInstanceViews(parsers.DatabaseServerParse, ListCreateViewSet): model = assetModel.MySQLInstance fields = assetField.MySQLInstanceFields method_decorators = [session_or_token_required] - filter_fields = (("name", "icontains"), ("host", "icontains"), ("manage", ""), ("tags", "")) + filter_fields = (("name", "icontains"), ("host", "icontains"), ("manage", ""), ("tags", ""), + ("databases__name", ""), ) def __init__(self): self.init_parse() @@ -53,6 +60,14 @@ class MySQLInstanceViews(parsers.DatabaseServerParse, ListCreateViewSet): self.request_parse.add_argument("core", required=True, type=int, location='json') super(MySQLInstanceViews, self).__init__() + def pre_create(self, args): + """加密保存密码,若提交了密码,必须加密存储""" + args = super().pre_create(args) + if "password" in args: + password = args.get("password") + args["password"] = quick_crypto(password) + return args + class MySQLInstanceDetail(parsers.DatabaseServerParse, DetailViewSet): model = assetModel.MySQLInstance @@ -74,6 +89,112 @@ class MySQLInstanceDetail(parsers.DatabaseServerParse, DetailViewSet): self.request_parse.add_argument("core", required=False, type=int, location='json') super(MySQLInstanceDetail, self).__init__() + def pre_update(self, obj: assetModel.MySQLInstance, args: dict): + """加密保存密码,若提交了密码,必须加密存储""" + data = super().pre_update(obj, args) + if "password" in data: + password = data.get("password") + if password != obj.password: + data["password"] = quick_crypto(password) + return data + + +class DatabaseViews(parsers.DatabaseParse, Resource): + model = assetModel.DatabaseServer + db_model = assetModel.Database + db_fields = assetField.DatabaseFields + + def __init__(self): + self.init_parse() + self.request_parse.add_argument("name", type=str, location='json', required=True) + + def post(self, pk): + """创建实例中的内嵌数据库对象""" + # 解析参数 + args = self.request_parse.parse_args() + + # 找到数据库服务器示例,传入的pk是数据库实例的id + try: + db_server = assetModel.DatabaseServer.objects(id=pk).first_or_404(message="db instance not found.") + except: + abort_response(404, code=1404, msg="db instance not found.") + return + + # 库名不能重复 + name = args.get("name") + exists = db_server.databases.filter(name=name).first() + if exists: + abort_response(400, 1400, msg=f"数据库{name}在实例中已存在!") + + # 加密存储密码 + if "password" in args: + password = args["password"] + if password: + args["password"] = quick_crypto(password) + + # 保存对象,追加到实例的数据库列表中 + try: + obj = self.db_model(**args) + db_server.databases.append(obj) + db_server.save() + except Exception as e: + logger.exception(f"{self.db_model} 创建对象失败! data={args}") + abort_response(500, 1500, msg=f"保存对象失败!{str(e)}") + return + # 返回创建信息 + return marshal(obj, self.db_fields) + + +class DatabaseDetailViews(parsers.DatabaseParse, Resource): + model = assetModel.DatabaseServer + db_model = assetModel.Database + db_fields = assetField.DatabaseFields + + def __init__(self): + self.init_parse() + self.request_parse.add_argument("name", type=str, location='json', required=True) + + def put(self, pk, db): + """创建实例中的内嵌数据库对象""" + # 解析参数 + args = self.request_parse.parse_args() + # 找到数据库服务器示例,传入的pk是数据库实例的id,db为内嵌数据库列表的对象id + try: + db_server = assetModel.DatabaseServer.objects(id=pk).first_or_404(message="db instance not found.") + db_obj = db_server.databases.filter(id=db).first() + assert db_obj + except Exception as e: + print(e) + abort_response(404, code=1404, msg="db not found.") + return + name = args.get("name") + if name: + exists = db_server.databases.filter(name=name).first() + if exists and exists.id != db_obj.id: + abort_response(400, 1400, msg=f"数据库{name}在实例中已存在!") + + # 若更新密码,重新加密保存 + if "password" in args: + password = args["password"] + if password != db_obj.password: + args["password"] = quick_crypto(password) + # 去除id + args.pop("id", "") + + # 更新内嵌对象 + try: + db_server.databases.filter(id=db).update(**args) + db_server.save() + db_server.reload() + except Exception as e: + logger.exception(f"{self.db_model} 更新对象失败! data={args}") + abort_response(500, 1500, msg=f"更新对象失败!{str(e)}") + return + # 重载数据 + db_obj = db_server.databases.filter(id=db).first() + # 返回信息 + return marshal(db_obj, self.db_fields) + class RedisInstanceViews(parsers.DatabaseServerParse, ListCreateViewSet): model = assetModel.RedisInstance diff --git a/src/controller/project/operation.py b/src/controller/project/operation.py index 223f0df..8c5faec 100644 --- a/src/controller/project/operation.py +++ b/src/controller/project/operation.py @@ -1,6 +1,6 @@ +import logging import datetime -from flask import current_app as app from flask_restful import Resource, reqparse, marshal, fields as F from models.asset.models import Host @@ -12,6 +12,8 @@ from common.permission import token_header_required from common.utils import make_response from service.project.sync import sync_project_for_ops1 +logger = logging.getLogger("views") + class ProjectSyncView(Resource): """从 OPS1 同步项目,触发接口""" @@ -112,12 +114,12 @@ class ServerSyncView(CreateMixin): defaults = dict(num=num, channel_id=channel, host_id=host, version=version, port=port, status=status) obj, created = Server.update_or_create(defaults=defaults, num=num, channel_id=channel) if created: # 创建 - app.logger.debug(f"创建 {spid}_{num} 区服信息成功") + logger.debug(f"创建 {spid}_{num} 区服信息成功") continue # 更新对象日志 - app.logger.debug(f"更新 {spid}_{num} 区服信息成功") + logger.debug(f"更新 {spid}_{num} 区服信息成功") except: - app.logger.exception("同步区服出错") + logger.exception("同步区服出错") errors.append(f"{spid}_s{num}") continue diff --git a/src/models/asset/fields.py b/src/models/asset/fields.py index 203d369..2510131 100644 --- a/src/models/asset/fields.py +++ b/src/models/asset/fields.py @@ -43,6 +43,9 @@ DatabaseFields = { "name": fields.String, "username": fields.String, "password": fields.String, + "data": fields.Raw, + "tags": fields.List(fields.String), + "labels": fields.Raw, } MySQLInstanceFields = { diff --git a/src/models/asset/models.py b/src/models/asset/models.py index dc53731..8bfae6d 100644 --- a/src/models/asset/models.py +++ b/src/models/asset/models.py @@ -52,6 +52,9 @@ class Database(mongo.EmbeddedDocument): name = mongo.StringField(required=True) username = mongo.StringField(default="") password = mongo.StringField(default="") + data = mongo.DictField(default=dict) + tags = mongo.ListField(mongo.StringField(), default=list) # tags 默认是空列表 + labels = mongo.DictField(default=dict) class MySQLInstance(DatabaseServer):