diff --git a/config/gala-ragdoll.conf b/config/gala-ragdoll.conf index 57ace32..8cb693a 100644 --- a/config/gala-ragdoll.conf +++ b/config/gala-ragdoll.conf @@ -5,21 +5,57 @@ user_email = "user_email" [collect] collect_address = "http://0.0.0.0" -collect_api = "/demo/collectConf" -collect_port = 11114 +collect_api = "/manage/config/collect" +collect_port = 11111 [sync] sync_address = "http://0.0.0.0" -sync_api = "/demo/syncConf" -sync_port = 11114 +sync_api = "/manage/config/sync" +batch_sync_address = "http://0.0.0.0" +batch_sync_api = "/manage/config/batch/sync" +sync_port = 11111 [objectFile] object_file_address = "http://0.0.0.0" object_file_api = "/manage/config/objectfile" object_file_port = 11111 +[sync_status] +host_sync_status_address = "http://0.0.0.0" +add_host_sync_status_api = "/manage/host/sync/status/add" +delete_host_sync_status_api = "/manage/host/sync/status/delete" +delete_all_host_sync_status_api = "/manage/all/host/sync/status/delete" +host_sync_status_port = 11111 + +[conf_trace] +conf_trace_mgmt_address = "http://0.0.0.0" +conf_trace_mgmt_api = "/conftrace/mgmt" +conf_trace_delete_api = "/conftrace/delete" +conf_trace_port = 11111 + [ragdoll] -port = 11114 +ip=127.0.0.1 +port=11114 + +[mysql] +ip=127.0.0.1 +port=3306 +database_name=aops +engine_format=mysql+pymysql://@%s:%s/%s +pool_size=100 +pool_recycle=7200 + +[redis] +ip=127.0.0.1 +port=6379 + +[uwsgi] +wsgi-file=manage.py +daemonize=/var/log/aops/uwsgi/ragdoll.log +http-timeout=600 +harakiri=600 +processes=2 +gevent=100 [log] log_level = INFO diff --git a/gala-ragdoll.spec b/gala-ragdoll.spec index bdde9ce..33bbcdb 100644 --- a/gala-ragdoll.spec +++ b/gala-ragdoll.spec @@ -45,6 +45,9 @@ mkdir %{buildroot}/%{python3_sitelib}/ragdoll/config install config/*.conf %{buildroot}/%{python3_sitelib}/ragdoll/config mkdir -p %{buildroot}/%{_prefix}/lib/systemd/system install service/gala-ragdoll.service %{buildroot}/%{_prefix}/lib/systemd/system +install service/ragdoll %{buildroot}/%{_prefix}/bin/ +install service/ragdoll-filetrace %{buildroot}/%{_prefix}/bin/ +install service/ragdoll-filetrace.service %{buildroot}/%{_prefix}/lib/systemd/system %pre @@ -67,7 +70,11 @@ fi %license LICENSE /%{_sysconfdir}/ragdoll/gala-ragdoll.conf %{_bindir}/ragdoll +%{_bindir}/ragdoll-filetrace %{_prefix}/lib/systemd/system/gala-ragdoll.service +%{_prefix}/lib/systemd/system/ragdoll-filetrace.service +%{_prefix}/bin/ragdoll +%{_prefix}/bin/ragdoll-filetrace %files -n python3-gala-ragdoll @@ -77,6 +84,9 @@ fi %changelog +* Thu June 27 2024 zhangdaolong - v1.4.0-4 +- Added real-time monitoring file function + * Mon Apr 17 2023 wenxin - v1.3.0-3 - update the host id validate method for ragdoll diff --git a/ragdoll/__main__.py b/ragdoll/__main__.py deleted file mode 100644 index df65acb..0000000 --- a/ragdoll/__main__.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 - -import connexion -import configparser -import os -import ast - -from ragdoll import encoder -from ragdoll.const.conf_handler_const import CONFIG -from ragdoll.utils.yang_module import YangModule -from ragdoll.utils.prepare import Prepare - - -def main(): - # prepare to load config - load_prepare() - # load yang modules - load_yang() - # load port for ragdoll - ragdoll_port = load_port() - app = connexion.App(__name__, specification_dir='./swagger/') - app.app.json_encoder = encoder.JSONEncoder - app.add_api('swagger.yaml', arguments={'title': 'Configuration traceability'}) - app.run(port=ragdoll_port) - - -def load_prepare(): - git_dir, git_user_name, git_user_email = load_conf() - prepare = Prepare(git_dir) - prepare.mdkir_git_warehose(git_user_name, git_user_email) - - -def load_yang(): - yang_modules = YangModule() - - -def load_conf(): - cf = configparser.ConfigParser() - if os.path.exists(CONFIG): - cf.read(CONFIG, encoding="utf-8") - else: - cf.read("config/gala-ragdoll.conf", encoding="utf-8") - git_dir = ast.literal_eval(cf.get("git", "git_dir")) - git_user_name = ast.literal_eval(cf.get("git", "user_name")) - git_user_email = ast.literal_eval(cf.get("git", "user_email")) - return git_dir, git_user_name, git_user_email - - -def load_port(): - cf = configparser.ConfigParser() - if os.path.exists(CONFIG): - cf.read(CONFIG, encoding="utf-8") - else: - cf.read("config/gala-ragdoll.conf", encoding="utf-8") - ragdoll_port = cf.get("ragdoll", "port") - return ragdoll_port - - -if __name__ == '__main__': - main() diff --git a/ragdoll/conf/__init__.py b/ragdoll/conf/__init__.py new file mode 100644 index 0000000..0c44a2e --- /dev/null +++ b/ragdoll/conf/__init__.py @@ -0,0 +1,26 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +@FileName: __init__.py.py +@Time: 2024/3/4 9:37 +@Author: JiaoSiMao +Description: +""" +from vulcanus.conf import Config + +from ragdoll.conf import default_config +from ragdoll.conf.constant import MANAGER_CONFIG_PATH + +# read manager configuration +configuration = Config(MANAGER_CONFIG_PATH, default_config) + diff --git a/ragdoll/conf/constant.py b/ragdoll/conf/constant.py new file mode 100644 index 0000000..a14a0f9 --- /dev/null +++ b/ragdoll/conf/constant.py @@ -0,0 +1,53 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +Time: +Author: +Description: manager constant +""" +import os + +from ragdoll.utils.git_tools import GitTools + +BASE_CONFIG_PATH = "/etc/ragdoll/" + +# path of manager configuration +MANAGER_CONFIG_PATH = os.path.join(BASE_CONFIG_PATH, 'gala-ragdoll.conf') + +TARGETDIR = GitTools().target_dir + +# domain +CREATE_DOMAIN = "/domain/createDomain" +DELETE_DOMAIN = "/domain/deleteDomain" +QUERY_DOMAIN = "/domain/queryDomain" + +# host +ADD_HOST_IN_DOMAIN = "/host/addHost" +DELETE_HOST_IN_DOMAIN = "/host/deleteHost" +GET_HOST_BY_DOMAIN = "/host/getHost" + +# management conf +ADD_MANAGEMENT_CONFS_IN_DOMAIN = "/management/addManagementConf" +UPLOAD_MANAGEMENT_CONFS_IN_DOMAIN = "/management/uploadManagementConf" +DELETE_MANAGEMENT_CONFS_IN_DOMAIN = "/management/deleteManagementConf" +GET_MANAGEMENT_CONFS_IN_DOMAIN = "/management/getManagementConf" +QUERY_CHANGELOG_OF_MANAGEMENT_CONFS_IN_DOMAIN = "/management/queryManageConfChange" + +# confs +GET_SYNC_STATUS = "/confs/getDomainStatus" +QUERY_EXCEPTED_CONFS = "/confs/queryExpectedConfs" +QUERY_REAL_CONFS = "/confs/queryRealConfs" +SYNC_CONF_TO_HOST_FROM_DOMAIN = "/confs/syncConf" +QUERY_SUPPORTED_CONFS = "/confs/querySupportedConfs" +COMPARE_CONF_DIFF = "/confs/domain/diff" +BATCH_SYNC_CONF_TO_HOST_FROM_DOMAIN = "/confs/batch/syncConf" diff --git a/ragdoll/conf/default_config.py b/ragdoll/conf/default_config.py new file mode 100644 index 0000000..094111a --- /dev/null +++ b/ragdoll/conf/default_config.py @@ -0,0 +1,59 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2021-2021. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +Time: +Author: +Description: default config of manager +""" +git = {"GIT_DIR": "/home/confTraceTest", "USER_NAME": "user_name", "USER_EMAIL": "user_email"} + +collect = { + "COLLECT_ADDRESS": "http://127.0.0.1", + "COLLECT_API": "/manage/config/collect", + "COLLECT_PORT": 11111 +} + +sync = { + "SYNC_ADDRESS": "http://127.0.0.1", + "SYNC_API": "/manage/config/sync", + "BATCH_SYNC_ADDRESS": "http://127.0.0.1", + "BATCH_SYNC_API": "/manage/config/batch/sync", + "SYNC_PORT": 11111 +} + +objectFile = {"OBJECT_FILE_ADDRESS": "http://127.0.0.1", "OBJECT_FILE_API": "/manage/config/objectfile", + "OBJECT_FILE_PORT": 11111} + +sync_status = { + "HOST_SYNC_STATUS_ADDRESS": "http://127.0.0.1", + "ADD_HOST_SYNC_STATUS_API": "/manage/host/sync/status/add", + "DELETE_HOST_SYNC_STATUS_API": "/manage/host/sync/status/delete", + "HOST_SYNC_STATUS_PORT": 11111 +} + +ragdoll = {"IP": "127.0.0.1", "PORT": 11114} + +mysql = { + "IP": "127.0.0.1", + "PORT": 3306, + "DATABASE_NAME": "aops", + "ENGINE_FORMAT": "mysql+pymysql://@%s:%s/%s", + "POOL_SIZE": 100, + "POOL_RECYCLE": 7200, +} + +redis = {"IP": "127.0.0.1", "PORT": 6379} + +log = {"LOG_LEVEL": "INFO", "LOG_DIR": "/var/log/aops", "MAX_BYTES": 31457280, "BACKUP_COUNT": 40} + + diff --git a/ragdoll/config_model/bash_config.py b/ragdoll/config_model/bash_config.py index 73c1777..36016a9 100644 --- a/ragdoll/config_model/bash_config.py +++ b/ragdoll/config_model/bash_config.py @@ -59,8 +59,7 @@ class BashConfig(BaseHandlerConfig): dst_conf_dict = json.loads(dst_conf) src_conf_dict = json.loads(src_conf) for src_conf in src_conf_dict: - str_src_conf = str(src_conf) - if str(dst_conf_dict).find(str_src_conf) == -1: + if src_conf not in dst_conf_dict: res = NOT_SYNCHRONIZE break return res diff --git a/ragdoll/config_model/fstab_config.py b/ragdoll/config_model/fstab_config.py index 416fd62..5f712e8 100644 --- a/ragdoll/config_model/fstab_config.py +++ b/ragdoll/config_model/fstab_config.py @@ -13,7 +13,6 @@ import re from ragdoll.config_model.base_handler_config import BaseHandlerConfig from ragdoll.const.conf_handler_const import FSTAB_COLUMN_NUM -from ragdoll.log.log import LOGGER class FstabConfig(BaseHandlerConfig): """ diff --git a/ragdoll/config_model/hostname_config.py b/ragdoll/config_model/hostname_config.py new file mode 100644 index 0000000..bca3877 --- /dev/null +++ b/ragdoll/config_model/hostname_config.py @@ -0,0 +1,68 @@ +# ****************************************************************************** +# Copyright (C) 2023 isoftstone Technologies Co., Ltd. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +Time: 2023-07-19 11:23:00 +Author: liulei +Description: text type config analyze +""" +import json + +from ragdoll.config_model.base_handler_config import BaseHandlerConfig +from ragdoll.const.conf_handler_const import SYNCHRONIZED, NOT_SYNCHRONIZE +from ragdoll.log.log import LOGGER + + +class HostnameConfig(BaseHandlerConfig): + @staticmethod + def parse_conf_to_dict(conf_info): + """ + 将配置信息conf_info转为list,但是并未校验配置项是否合法 + """ + conf_dict_list = list() + + conf_list = conf_info.strip().splitlines() + for line in conf_list: + if line is None or line.strip() == '' or line.strip()[0] in '#;': + continue + + strip_line = str(line.strip()).replace("\t", " ") + conf_dict_list.append(strip_line) + return conf_dict_list + + def read_conf(self, conf_info): + conf_dict_list = self.parse_conf_to_dict(conf_info) + if conf_dict_list: + self.conf = conf_dict_list + + def write_conf(self): + content = "" + for value in self.conf: + if value is not None: + content = content + value + "\n" + return content + + def conf_compare(self, src_conf, dst_conf): + """ + desc: 比较dst_conf和src_conf是否相同,dst_conf和src_conf均为序列化后的配置信息。 + return:dst_conf和src_conf相同返回SYNCHRONIZED + dst_conf和src_conf不同返回NOT_SYNCHRONIZE + """ + res = SYNCHRONIZED + dst_conf_dict = json.loads(dst_conf) + src_conf_dict = json.loads(src_conf) + if not dst_conf_dict or not src_conf_dict: + res = NOT_SYNCHRONIZE + return res + if dst_conf_dict[0] != src_conf_dict[0]: + res = NOT_SYNCHRONIZE + + return res diff --git a/ragdoll/config_model/hosts_config.py b/ragdoll/config_model/hosts_config.py index 50660ec..1bc9452 100644 --- a/ragdoll/config_model/hosts_config.py +++ b/ragdoll/config_model/hosts_config.py @@ -19,7 +19,6 @@ import json from ragdoll.config_model.base_handler_config import BaseHandlerConfig from ragdoll.log.log import LOGGER -from ragdoll.utils.yang_module import YangModule from ragdoll.const.conf_handler_const import NOT_SYNCHRONIZE, SYNCHRONIZED ipv4 = re.compile('^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$') @@ -64,7 +63,7 @@ class HostsConfig(BaseHandlerConfig): ip_domain = re.split("\s+", line) if len(ip_domain) == 1: error_conf = True - LOGGER.warning("ip_domain contains incorrect formatting") + LOGGER.error("Ip_domain contains incorrect formatting") break ip = ip_domain[0] if ipv4.match(ip) or ipv6.match(ip): @@ -73,7 +72,7 @@ class HostsConfig(BaseHandlerConfig): res[ip] = str_value else: error_conf = True - LOGGER.warning("ip does not meet the ipv4 or ipv6 format") + LOGGER.error("Ip does not meet the ipv4 or ipv6 format") break return error_conf, res @@ -84,7 +83,7 @@ class HostsConfig(BaseHandlerConfig): self.conf = dict_res @staticmethod - def conf_compare(dst_conf, src_conf): + def conf_compare(src_conf, dst_conf): res = SYNCHRONIZED dst_conf_dict = json.loads(dst_conf) src_conf_dict = json.loads(src_conf) diff --git a/ragdoll/config_model/ini_config.py b/ragdoll/config_model/ini_config.py index 9f9dc3d..0c0bd50 100644 --- a/ragdoll/config_model/ini_config.py +++ b/ragdoll/config_model/ini_config.py @@ -11,7 +11,6 @@ # ******************************************************************************/ import re -import json import copy from collections import OrderedDict as _default_dict diff --git a/ragdoll/config_model/sshd_config.py b/ragdoll/config_model/sshd_config.py index e499bb2..35ed872 100644 --- a/ragdoll/config_model/sshd_config.py +++ b/ragdoll/config_model/sshd_config.py @@ -83,7 +83,7 @@ class SshdConfig(): self.conf = conf_list @staticmethod - def conf_compare(dst_conf, src_conf): + def conf_compare(src_conf, dst_conf): """ desc: 比较dst_conf和src_conf是否相同,dst_conf和src_conf均为序列化后的配置信息。 return:dst_conf和src_conf相同返回SYNCHRONIZED @@ -93,9 +93,9 @@ class SshdConfig(): dst_conf_dict = json.loads(dst_conf) src_conf_dict = json.loads(src_conf) - for dst_conf in dst_conf_dict: - str_dst_conf = str(dst_conf) - if str(src_conf_dict).find(str_dst_conf) == -1: + for src_conf in src_conf_dict: + str_src_conf = str(src_conf) + if str(dst_conf_dict).find(str_src_conf) == -1: res = NOT_SYNCHRONIZE break return res diff --git a/ragdoll/config_model/text_config.py b/ragdoll/config_model/text_config.py index dadd915..dd4165a 100644 --- a/ragdoll/config_model/text_config.py +++ b/ragdoll/config_model/text_config.py @@ -44,5 +44,4 @@ class TextConfig(BaseHandlerConfig): for value in self.conf: if value is not None: content = content + value + "\n" - content = content + '\n' return content \ No newline at end of file diff --git a/ragdoll/confs_manage/__init__.py b/ragdoll/confs_manage/__init__.py new file mode 100644 index 0000000..25b0334 --- /dev/null +++ b/ragdoll/confs_manage/__init__.py @@ -0,0 +1,18 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +@FileName: __init__.py.py +@Time: 2024/3/11 9:01 +@Author: JiaoSiMao +Description: +""" diff --git a/ragdoll/confs_manage/view.py b/ragdoll/confs_manage/view.py new file mode 100644 index 0000000..4280fa7 --- /dev/null +++ b/ragdoll/confs_manage/view.py @@ -0,0 +1,479 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +@FileName: view.py +@Time: 2024/3/11 9:01 +@Author: JiaoSiMao +Description: +""" +import os + +import connexion +from vulcanus.restful.resp.state import SUCCEED, SERVER_ERROR, PARAM_ERROR, NO_DATA +from vulcanus.restful.response import BaseResponse + +from ragdoll.conf.constant import TARGETDIR +from ragdoll.const.conf_files import yang_conf_list +from ragdoll.const.conf_handler_const import NOT_SYNCHRONIZE +from ragdoll.function.verify.confs import GetSyncStatusSchema, QueryExceptedConfsSchema, QueryRealConfsSchema, \ + SyncConfToHostFromDomainSchema, QuerySupportedConfsSchema, CompareConfDiffSchema, \ + BatchSyncConfToHostFromDomainSchema +from ragdoll.log.log import LOGGER +from ragdoll.utils.conf_tools import ConfTools +from ragdoll.utils.format import Format +from ragdoll.utils.host_tools import HostTools +from ragdoll.utils.yang_module import YangModule + + +class GetTheSyncStatusOfDomain(BaseResponse): + @BaseResponse.handle(schema=GetSyncStatusSchema, token=True) + def post(self, **params): + """ + get the status of the domain + get the status of whether the domain has been synchronized # noqa: E501 + + :param body: + :type body: dict | bytes + + :rtype: SyncStatus + """ + access_token = connexion.request.headers.get("access_token") + domain = params.get("domainName") + ip = params.get("ip") + # check domain + base_rsp, code_num = Format.check_domain_param(domain) + if code_num != 200: + return base_rsp, code_num + + # get manage confs in domain + code_num, code_string, manage_confs = Format.get_domain_conf(domain) + if not manage_confs: + return self.response(code=SUCCEED, message=code_string, data=manage_confs) + + # get real conf in host + host_id = Format.get_host_id_by_ip(ip, domain) + real_conf_res_text = Format.get_realconf_by_domain_and_host(domain, [host_id], access_token) + if real_conf_res_text is None: + return self.response(code=SERVER_ERROR, message="get real conf failed") + + # compare manage conf with real conf + sync_status = Format.diff_mangeconf_with_realconf(domain, real_conf_res_text, manage_confs) + + # deal with not found files + man_conf_list = [] + for d_man_conf in manage_confs: + man_conf_list.append(d_man_conf.get("file_path").split(":")[-1]) + for d_host in sync_status["hostStatus"]: + d_sync_status = d_host["syncStatus"] + file_list = [] + for d_file in d_sync_status: + file_path = d_file["file_path"] + file_list.append(file_path) + for d_man_conf in man_conf_list: + if d_man_conf in file_list: + continue + else: + comp_res = "NOT FOUND" + conf_is_synced = {"file_path": d_man_conf, "isSynced": comp_res} + d_sync_status.append(conf_is_synced) + return self.response(code=SUCCEED, message="successfully get the sync status of domain", data=sync_status) + + +class QueryExceptedConfs(BaseResponse): + @BaseResponse.handle(schema=QueryExceptedConfsSchema, token=True) + def post(self, **params): + """ + query the supported configurations in the current project + queryExpectedConfs # noqa: E501 + + :rtype: List[ExceptedConfInfo] + """ + # 直接从入参中读取domain列表 + domain_names = params.get("domainNames") + if len(domain_names) == 0: + code_num = PARAM_ERROR + code_string = "The current domain does not exist, please create the domain first." + return self.response(code=code_num, message=code_string) + + all_domain_expected_files = [] + yang_modules = YangModule() + for d_domain in domain_names: + domain_path = os.path.join(TARGETDIR, d_domain["domainName"]) + expected_conf_lists = {"domainName": d_domain["domainName"], "confBaseInfos": []} + # Traverse all files in the source management repository + for root, dirs, files in os.walk(domain_path): + # Domain also contains host cache files, so we need to add hierarchical judgment for root + if len(files) > 0 and len(root.split('/')) > 3: + if "hostRecord.txt" in files: + continue + for d_file in files: + feature = os.path.join(root.split('/')[-1], d_file) + d_module = yang_modules.getModuleByFeature(feature) + file_lists = yang_modules.getFilePathInModdule(yang_modules.module_list) + file_path = file_lists.get(d_module.name()).split(":")[-1] + d_file_path = os.path.join(root, d_file) + expected_value = Format.get_file_content_by_read(d_file_path) + conf_base_info = {"filePath": file_path, "expectedContents": expected_value} + expected_conf_lists.get("confBaseInfos").append(conf_base_info) + all_domain_expected_files.append(expected_conf_lists) + + LOGGER.debug("all_domain_expected_files is : {}".format(all_domain_expected_files)) + + if len(all_domain_expected_files) == 0: + code_num = PARAM_ERROR + code_string = "The current domain does not exist, please create the domain first." + return self.response(code=code_num, message=code_string) + + return self.response(code=SUCCEED, message="Successfully get the expected configuration file information.", + data=all_domain_expected_files) + + +class QueryRealConfs(BaseResponse): + @BaseResponse.handle(schema=QueryRealConfsSchema, token=True) + def post(self, **params): + """ + query the real configuration value in the current hostId node + + query the real configuration value in the current hostId node # noqa: E501 + + :param body: + :type body: dict | bytes + + :rtype: List[RealConfInfo] + """ + access_token = connexion.request.headers.get("access_token") + domain = params.get("domainName") + host_list = params.get("hostIds") + + check_res = Format.domainCheck(domain) + if not check_res: + codeNum = PARAM_ERROR + codeString = "Failed to verify the input parameter, please check the input parameters." + return self.response(code=codeNum, message=codeString) + + # check the domain is Exist + is_exist = Format.isDomainExist(domain) + if not is_exist: + codeNum = PARAM_ERROR + codeString = "The current domain does not exist, please create the domain first." + return self.response(code=codeNum, message=codeString) + + # check whether the host is configured in the domain + is_host_list_exist = Format.isHostInDomain(domain) + LOGGER.debug("is_host_list_exist is : {}".format(is_host_list_exist)) + if not is_host_list_exist: + codeNum = PARAM_ERROR + codeString = "The host information is not set in the current domain. Please add the host information first" + return self.response(code=codeNum, message=codeString) + + # get all hosts managed by the current domain. + # If host_list is empty, query all hosts in the current domain. + # If host_list is not empty, the actual contents of the currently given host are queried. + exist_host = [] + failed_host = [] + if len(host_list) > 0: + host_tool = HostTools() + exist_host, failed_host = host_tool.getHostExistStatus(domain, host_list) + else: + res_text = Format.get_hostinfo_by_domain(domain) + if len(res_text) == 0: + code_num = NO_DATA + code_string = "The host currently controlled in the domain is empty. Please add host information to " \ + "the domain. " + return self.response(code=code_num, message=code_string) + + if len(exist_host) == 0 or len(failed_host) == len(host_list): + codeNum = PARAM_ERROR + codeString = "The host information is not set in the current domain. Please add the host information first" + return self.response(code=codeNum, message=codeString) + + # get the management conf in domain + res = Format.get_realconf_by_domain_and_host(domain, exist_host, access_token) + if len(res) == 0: + codeNum = NO_DATA + codeString = "Real configuration query failed. The failure reason is : The real configuration does not " \ + "found. " + return self.response(code=codeNum, message=codeString) + + return self.response(code=SUCCEED, message="Successfully query real confs", data=res) + + +class SyncConfToHostFromDomain(BaseResponse): + @BaseResponse.handle(schema=SyncConfToHostFromDomainSchema, token=True) + def put(self, **params): + """ + synchronize the configuration information of the configuration domain to the host # noqa: E501 + + :param body: + :type body: dict | bytes + + :rtype: List[HostSyncResult] + """ + access_token = connexion.request.headers.get("access_token") + domain = params.get("domainName") + sync_list = params.get("syncList") + + host_sync_confs = dict() + + for sync in sync_list: + host_sync_confs[sync["hostId"]] = sync["syncConfigs"] + + # check the input domain + check_res = Format.domainCheck(domain) + if not check_res: + code_num = PARAM_ERROR + code_string = "Failed to verify the input parameter, please check the input parameters." + return self.response(code=code_num, message=code_string) + + # check whether the domain exists + is_exist = Format.isDomainExist(domain) + if not is_exist: + code_num = NO_DATA + code_string = "The current domain does not exist, please create the domain first." + return self.response(code=code_num, message=code_string) + + # get the management host in domain + res_host_text = Format.get_hostinfo_by_domain(domain) + if len(res_host_text) == 0: + code_num = NO_DATA + code_string = "The host currently controlled in the domain is empty. Please add host information to the " \ + "domain. " + return self.response(code=code_num, message=code_string) + + # Check whether the host is in the managed host list + exist_host = [] + if len(host_sync_confs) > 0: + host_ids = host_sync_confs.keys() + for host_id in host_ids: + for d_host in res_host_text: + if host_id == d_host.get("host_id"): + exist_host.append(host_id) + else: + for d_host in res_host_text: + temp_host = {} + temp_host["hostId"] = d_host.get("host_id") + exist_host.append(temp_host) + LOGGER.debug("exist_host is : {}".format(exist_host)) + + if len(exist_host) == 0: + code_num = PARAM_ERROR + code_string = "The host information is not set in the current domain. Please add the host information first" + return self.response(code=code_num, message=code_string) + + # get the management conf in domain + man_conf_res_text = Format.get_manageconf_by_domain(domain) + LOGGER.debug("man_conf_res_text is : {}".format(man_conf_res_text)) + manage_confs = man_conf_res_text.get("conf_files") + + # Deserialize and reverse parse the expected configuration + conf_tools = ConfTools() + # 组装入参 + file_path_infos = dict() + for host_id in exist_host: + sync_confs = host_sync_confs.get(host_id) + for d_man_conf in manage_confs: + file_path = d_man_conf.get("file_path").split(":")[-1] + if file_path in sync_confs: + contents = d_man_conf.get("contents") + file_path_infos[file_path] = contents + + code_num, code_string, sync_res = Format.deal_batch_sync_res(conf_tools, exist_host, file_path_infos, + access_token) + if code_num != 200: + return self.response(code=SERVER_ERROR, message=code_string, data=sync_res) + return self.response(code=SUCCEED, message=code_string, data=sync_res) + + +class QuerySupportedConfs(BaseResponse): + @BaseResponse.handle(schema=QuerySupportedConfsSchema, token=True) + def post(self, **params): + """ + query supported configuration list # noqa: E501 + + :param body: + :type body: dict | bytes + + :rtype: List + """ + domain = params.get("domainName") + check_res = Format.domainCheck(domain) + if not check_res: + code_num = PARAM_ERROR + code_string = "Failed to verify the input parameter, please check the input parameters." + return self.response(code=code_num, message=code_string) + + is_exist = Format.isDomainExist(domain) + if not is_exist: + code_num = NO_DATA + code_string = "The current domain does not exist, please create the domain first." + return self.response(code=code_num, message=code_string) + + conf_files = Format.get_manageconf_by_domain(domain) + conf_files = conf_files.get("conf_files") + if len(conf_files) == 0: + return yang_conf_list + + exist_conf_list = [] + for conf in conf_files: + exist_conf_list.append(conf.get('file_path')) + + return list(set(yang_conf_list).difference(set(exist_conf_list))) + + +class CompareConfDiff(BaseResponse): + @BaseResponse.handle(schema=CompareConfDiffSchema, token=True) + def post(self, **params): + """ + compare conf different, return host sync status + + :param body: + :type body: dict + + :rtype: + """ + expected_confs_resp_list = params.get("expectedConfsResp") + domain_result = params.get("domainResult") + expected_confs_resp_dict = Format.deal_expected_confs_resp(expected_confs_resp_list) + + real_conf_res_text_dict = Format.deal_domain_result(domain_result) + # 循环real_conf_res_text_list 取出每一个domain的domain_result与expected_confs_resp_dict的expected_confs_resp做对比 + sync_result = [] + for domain_name, real_conf_res_text_list in real_conf_res_text_dict.items(): + expected_confs_resp = expected_confs_resp_dict.get(domain_name) + sync_status = Format.diff_mangeconf_with_realconf_for_db(domain_name, real_conf_res_text_list, + expected_confs_resp) + domain_name = sync_status["domainName"] + host_status_list = sync_status["hostStatus"] + + for signal_status in host_status_list: + host_id = signal_status["hostId"] + domain_host_sync_status = 1 + sync_status_list = signal_status["syncStatus"] + for single_sync_status in sync_status_list: + if single_sync_status["isSynced"] == NOT_SYNCHRONIZE: + domain_host_sync_status = 0 + break + single_domain_host_status = {"domain_name": domain_name, "host_id": host_id, + "sync_status": domain_host_sync_status} + sync_result.append(single_domain_host_status) + return self.response(code=SUCCEED, message="successfully compare conf diff", data=sync_result) + + +class BatchSyncConfToHostFromDomain(BaseResponse): + @BaseResponse.handle(schema=BatchSyncConfToHostFromDomainSchema, token=True) + def put(self, **params): + """ + synchronize the configuration information of the configuration domain to the host # noqa: E501 + + :param body: + :type body: dict | bytes + + :rtype: List[HostSyncResult] + """ + access_token = connexion.request.headers.get("access_token") + domain = params.get("domainName") + host_ids = params.get("hostIds") + # check domain + base_rsp, code_num = Format.check_domain_param(domain) + if code_num != 200: + return base_rsp, code_num + + # 根据domain和ip获取有哪些不同步的文件 + # get manage confs in domain + code_num, code_string, manage_confs = Format.get_domain_conf(domain) + if not manage_confs: + return self.response(code=SUCCEED, message=code_string, data=manage_confs) + + # get real conf in host + real_conf_res_text = Format.get_realconf_by_domain_and_host(domain, host_ids, access_token) + # compare manage conf with real conf + sync_status = Format.diff_mangeconf_with_realconf(domain, real_conf_res_text, manage_confs) + # 解析sync_status,取出未同步的数据 + host_sync_confs = dict() + host_status = sync_status["hostStatus"] + for host_result in host_status: + host_id = host_result["hostId"] + sync_status = host_result["syncStatus"] + sync_configs = [] + for sync_result in sync_status: + if sync_result["isSynced"] == NOT_SYNCHRONIZE: + sync_configs.append(sync_result["file_path"]) + host_sync_confs[host_id] = sync_configs + + # check the input domain + check_res = Format.domainCheck(domain) + if not check_res: + code_num = PARAM_ERROR + code_string = "Failed to verify the input parameter, please check the input parameters." + return self.response(code=code_num, message=code_string) + + # check whether the domain exists + is_exist = Format.isDomainExist(domain) + if not is_exist: + code_num = NO_DATA + code_string = "The current domain does not exist, please create the domain first." + return self.response(code=code_num, message=code_string) + + # get the management host in domain + res_host_text = Format.get_hostinfo_by_domain(domain) + if len(res_host_text) == 0: + code_num = NO_DATA + code_string = "The host currently controlled in the domain is empty. Please add host information to the " \ + "domain. " + return self.response(code=code_num, message=code_string) + # Check whether the host is in the managed host list + exist_host = [] + if len(host_sync_confs) > 0: + host_ids = host_sync_confs.keys() + for host_id in host_ids: + for d_host in res_host_text: + if host_id == d_host.get("host_id"): + exist_host.append(host_id) + else: + for d_host in res_host_text: + tmp_host = {"hostId": d_host.get("host_id")} + exist_host.append(tmp_host) + LOGGER.debug("exist_host is : {}".format(exist_host)) + + if len(exist_host) == 0: + code_num = PARAM_ERROR + code_string = "The host information is not set in the current domain. Please add the host information first" + return self.response(code=code_num, message=code_string) + + # get the management conf in domain + man_conf_res_text = Format.get_manageconf_by_domain(domain) + LOGGER.debug("man_conf_res_text is : {}".format(man_conf_res_text)) + manage_confs = man_conf_res_text.get("conf_files") + + # Deserialize and reverse parse the expected configuration + conf_tools = ConfTools() + # 组装入参 + file_path_infos = dict() + for host_id in exist_host: + sync_confs = host_sync_confs.get(host_id) + for d_man_conf in manage_confs: + file_path = d_man_conf.get("file_path").split(":")[-1] + if file_path in sync_confs: + contents = d_man_conf.get("contents") + file_path_infos[file_path] = contents + + if not file_path_infos: + code_num = PARAM_ERROR + code_string = "No config needs to be synchronized" + return self.response(code=code_num, message=code_string) + code_num, code_string, sync_res = Format.deal_batch_sync_res(conf_tools, exist_host, file_path_infos, + access_token) + + if code_num != 200: + return self.response(code=SERVER_ERROR, message=code_string, data=sync_res) + return self.response(code=SUCCEED, message=code_string, data=sync_res) diff --git a/ragdoll/controllers/__init__.py b/ragdoll/controllers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/ragdoll/controllers/confs_controller.py b/ragdoll/controllers/confs_controller.py deleted file mode 100644 index 44269f9..0000000 --- a/ragdoll/controllers/confs_controller.py +++ /dev/null @@ -1,339 +0,0 @@ -import connexion -import os - -from ragdoll.log.log import LOGGER -from ragdoll.models.base_response import BaseResponse # noqa: E501 -from ragdoll.models.conf_host import ConfHost # noqa: E501 -from ragdoll.models.domain_name import DomainName # noqa: E501 -from ragdoll.models.excepted_conf_info import ExceptedConfInfo # noqa: E501 -from ragdoll.models.sync_req import SyncReq -from ragdoll.models.sync_status import SyncStatus # noqa: E501 -from ragdoll.models.conf_base_info import ConfBaseInfo -from ragdoll.models.conf_is_synced import ConfIsSynced -from ragdoll.models.host_sync_result import HostSyncResult - -from ragdoll.controllers.format import Format -from ragdoll.utils.git_tools import GitTools -from ragdoll.utils.yang_module import YangModule -from ragdoll.utils.conf_tools import ConfTools -from ragdoll.utils.host_tools import HostTools -from ragdoll.utils.object_parse import ObjectParse -from ragdoll.const.conf_files import yang_conf_list - -TARGETDIR = GitTools().target_dir - - -def get_the_sync_status_of_domain(body=None): # noqa: E501 - """ - get the status of the domain - get the status of whether the domain has been synchronized # noqa: E501 - - :param body: - :type body: dict | bytes - - :rtype: SyncStatus - """ - - if connexion.request.is_json: - body = DomainName.from_dict(connexion.request.get_json()) # noqa: E501 - - domain = body.domain_name - # check domain - code_num = 200 - base_rsp = None - base_rsp, code_num = Format.check_domain_param(domain) - if code_num != 200: - return base_rsp, code_num - - # get manage confs in domain - LOGGER.debug("############## get the confs in domain ##############") - base_rsp, code_num, manage_confs = Format._get_domain_conf(domain) - if code_num != 200: - return base_rsp, code_num - - # get real conf in host - LOGGER.debug("############## query the real conf ##############") - host_ids = Format.get_hostid_list_by_domain(domain) - real_conf_res_text = Format.get_realconf_by_domain_and_host(domain, host_ids) - - # compare manage conf with real conf - sync_status = Format.diff_mangeconf_with_realconf(domain, real_conf_res_text, manage_confs) - - # deal with not found files - man_conf_list = [] - for d_man_conf in manage_confs: - man_conf_list.append(d_man_conf.get("file_path").split(":")[-1]) - for d_host in sync_status.host_status: - d_sync_status = d_host.sync_status - file_list = [] - for d_file in d_sync_status: - file_path = d_file.file_path - file_list.append(file_path) - for d_man_conf in man_conf_list: - if d_man_conf in file_list: - continue - else: - comp_res = "NOT FOUND" - conf_is_synced = ConfIsSynced(file_path=d_man_conf, - is_synced=comp_res) - d_sync_status.append(conf_is_synced) - - return sync_status - - -def query_excepted_confs(): # noqa: E501 - """ - query the supported configurations in the current project - queryExpectedConfs # noqa: E501 - - :rtype: List[ExceptedConfInfo] - """ - # get all domain - LOGGER.debug("############## get all domain ##############") - cmd = "ls {}".format(TARGETDIR) - git_tools = GitTools() - res_domain = git_tools.run_shell_return_output(cmd).decode().split() - - if len(res_domain) == 0: - code_num = 400 - base_rsp = BaseResponse(code_num, "The current domain does not exist, please create the domain first.") - return base_rsp, code_num - - success_domain = [] - all_domain_expected_files = [] - yang_modules = YangModule() - for d_domian in res_domain: - domain_path = os.path.join(TARGETDIR, d_domian) - expected_conf_lists = ExceptedConfInfo(domain_name=d_domian, - conf_base_infos=[]) - # Traverse all files in the source management repository - for root, dirs, files in os.walk(domain_path): - # Domain also contains host cache files, so we need to add hierarchical judgment for root - if len(files) > 0 and len(root.split('/')) > 3: - if "hostRecord.txt" in files: - continue - for d_file in files: - feature = os.path.join(root.split('/')[-1], d_file) - d_module = yang_modules.getModuleByFeature(feature) - file_lists = yang_modules.getFilePathInModdule(yang_modules.module_list) - file_path = file_lists.get(d_module.name()).split(":")[-1] - d_file_path = os.path.join(root, d_file) - expected_value = Format.get_file_content_by_read(d_file_path) - - git_tools = GitTools() - git_message = git_tools.getLogMessageByPath(d_file_path) - - conf_base_info = ConfBaseInfo(file_path=file_path, - expected_contents=expected_value, - change_log=git_message) - expected_conf_lists.conf_base_infos.append(conf_base_info) - all_domain_expected_files.append(expected_conf_lists) - - LOGGER.debug("########################## expetedConfInfo ####################") - LOGGER.debug("all_domain_expected_files is : {}".format(all_domain_expected_files)) - LOGGER.debug("########################## expetedConfInfo end ####################") - - if len(all_domain_expected_files) == 0: - code_num = 400 - base_rsp = BaseResponse(code_num, "The current domain does not exist, please create the domain first.") - return base_rsp, code_num - - return all_domain_expected_files - - -def query_real_confs(body=None): # noqa: E501 - """ - query the real configuration value in the current hostId node - - query the real configuration value in the current hostId node # noqa: E501 - - :param body: - :type body: dict | bytes - - :rtype: List[RealConfInfo] - """ - if connexion.request.is_json: - body = ConfHost.from_dict(connexion.request.get_json()) # noqa: E501 - - domain = body.domain_name - host_list = body.host_ids - - check_res = Format.domainCheck(domain) - if not check_res: - num = 400 - base_rsp = BaseResponse(num, "Failed to verify the input parameter, please check the input parameters.") - return base_rsp, num - - # check the domain is Exist - is_exist = Format.isDomainExist(domain) - if not is_exist: - code_num = 400 - base_rsp = BaseResponse(code_num, "The current domain does not exist, please create the domain first.") - return base_rsp, code_num - - # check whether the host is configured in the domain - is_host_list_exist = Format.isHostInDomain(domain) - LOGGER.debug("is_host_list_exist is : {}".format(is_host_list_exist)) - if not is_host_list_exist: - code_num = 400 - base_rsp = BaseResponse(code_num, "The host information is not set in the current domain." + - "Please add the host information first") - return base_rsp, code_num - - # get all hosts managed by the current domain. - # If host_list is empty, query all hosts in the current domain. - # If host_list is not empty, the actual contents of the currently given host are queried. - exist_host = [] - failed_host = [] - if len(host_list) > 0: - host_tool = HostTools() - exist_host, failed_host = host_tool.getHostExistStatus(domain, host_list) - else: - LOGGER.debug("############## get the host in domain ##############") - res_text = Format.get_hostinfo_by_domain(domain) - if len(res_text) == 0: - code_num = 404 - base_rsp = BaseResponse(code_num, "The host currently controlled in the domain is empty." + - "Please add host information to the domain.") - - if len(exist_host) == 0 or len(failed_host) == len(host_list): - code_num = 400 - base_rsp = BaseResponse(code_num, "The host information is not set in the current domain." + - "Please add the host information first") - return base_rsp, code_num - - # get the management conf in domain - LOGGER.debug("############## get the management conf in domain ##############") - res = Format.get_realconf_by_domain_and_host(domain, exist_host) - if len(res) == 0: - code_num = 400 - res_text = "The real configuration does not found." - base_rsp = BaseResponse(code_num, "Real configuration query failed." + - "The failure reason is : " + res_text) - return base_rsp, code_num - - return res - - -def sync_conf_to_host_from_domain(body=None): # noqa: E501 - """ - synchronize the configuration information of the configuration domain to the host # noqa: E501 - - :param body: - :type body: dict | bytes - - :rtype: List[HostSyncResult] - """ - if connexion.request.is_json: - body = SyncReq.from_dict(connexion.request.get_json()) # noqa: E501 - - domain = body.domain_name - sync_list = body.sync_list - - host_sync_confs = dict() - - for sync in sync_list: - host_sync_confs[sync.host_id] = sync.sync_configs - - # check the input domain - check_res = Format.domainCheck(domain) - if not check_res: - num = 400 - base_rsp = BaseResponse(num, "Failed to verify the input parameter, please check the input parameters.") - return base_rsp, num - - # check whether the domain exists - is_exist = Format.isDomainExist(domain) - if not is_exist: - code_num = 404 - base_rsp = BaseResponse(code_num, "The current domain does not exist, please create the domain first.") - return base_rsp, code_num - - # get the management host in domain - res_host_text = Format.get_hostinfo_by_domain(domain) - if len(res_host_text) == 0: - code_num = 404 - base_rsp = BaseResponse(code_num, "The host currently controlled in the domain is empty." + - "Please add host information to the domain.") - # Check whether the host is in the managed host list - exist_host = [] - if len(host_sync_confs) > 0: - host_ids = host_sync_confs.keys() - for host_id in host_ids: - for d_host in res_host_text: - if host_id == d_host.get("host_id"): - exist_host.append(host_id) - else: - for d_host in res_host_text: - temp_host = {} - temp_host["hostId"] = d_host.get("host_id") - exist_host.append(temp_host) - LOGGER.debug("exist_host is : {}".format(exist_host)) - - if len(exist_host) == 0: - code_num = 400 - base_rsp = BaseResponse(code_num, "The host information is not set in the current domain." + - "Please add the host information first") - return base_rsp, code_num - - # get the management conf in domain - LOGGER.debug("############## get management conf in domain ##############") - man_conf_res_text = Format.get_manageconf_by_domain(domain) - LOGGER.debug("man_conf_res_text is : {}".format(man_conf_res_text)) - manage_confs = man_conf_res_text.get("conf_files") - LOGGER.debug("manage_confs is : {}".format(manage_confs)) - - # Deserialize and reverse parse the expected configuration - conf_tools = ConfTools() - sync_res = [] - for host_id in exist_host: - host_sync_result = HostSyncResult(host_id=host_id, - sync_result=[]) - sync_confs = host_sync_confs.get(host_id) - for d_man_conf in manage_confs: - file_path = d_man_conf.get("file_path").split(":")[-1] - if file_path in sync_confs: - contents = d_man_conf.get("contents") - object_parse = ObjectParse() - Format.deal_sync_res(conf_tools, contents, file_path, host_id, host_sync_result, object_parse) - sync_res.append(host_sync_result) - - return sync_res - - -def query_supported_confs(body=None): - """ - query supported configuration list # noqa: E501 - - :param body: - :type body: dict | bytes - - :rtype: List - """ - if connexion.request.is_json: - body = DomainName.from_dict(connexion.request.get_json()) - - domain = body.domain_name - - check_res = Format.domainCheck(domain) - if not check_res: - code_num = 400 - base_rsp = BaseResponse(code_num, "Failed to verify the input parameter, please check the input parameters.") - return base_rsp, code_num - - is_exist = Format.isDomainExist(domain) - if not is_exist: - code_num = 404 - base_rsp = BaseResponse(code_num, "The current domain does not exist, please create the domain first.") - return base_rsp, code_num - - conf_files = Format.get_manageconf_by_domain(domain) - conf_files = conf_files.get("conf_files") - if len(conf_files) == 0: - return yang_conf_list - - exist_conf_list = [] - for conf in conf_files: - exist_conf_list.append(conf.get('file_path')) - - return list(set(yang_conf_list).difference(set(exist_conf_list))) diff --git a/ragdoll/controllers/domain_controller.py b/ragdoll/controllers/domain_controller.py deleted file mode 100644 index fd6a7e5..0000000 --- a/ragdoll/controllers/domain_controller.py +++ /dev/null @@ -1,124 +0,0 @@ -import connexion -import six -import os -import shutil -import logging - -from ragdoll.models.base_response import BaseResponse # noqa: E501 -from ragdoll.models.domain import Domain # noqa: E501 -from ragdoll import util -from ragdoll.controllers.format import Format -from ragdoll.utils.git_tools import GitTools - -TARGETDIR = GitTools().target_dir - -# logging.basicConfig(filename='log.log', -# format='%(asctime)s - %(name)s - %(levelname)s -%(module)s: %(message)s', -# datefmt='%Y-%m-%d %H:%M:%S %p', -# level=10) - -def create_domain(body=None): # noqa: E501 - """create domain - - create domain # noqa: E501 - - :param body: domain info - :type body: list | bytes - - :rtype: BaseResponse - """ - if connexion.request.is_json: - body = [Domain.from_dict(d) for d in connexion.request.get_json()] # noqa: E501 - - if len(body) == 0: - base_rsp = BaseResponse(400, "The input domain cannot be empty, please check the domain.") - return base_rsp - - successDomain = [] - failedDomain = [] - - for domain in body: - tempDomainName = domain.domain_name - checkRes = Format.domainCheck(tempDomainName) - isExist = Format.isDomainExist(tempDomainName) - if isExist or not checkRes: - failedDomain.append(tempDomainName) - else: - successDomain.append(tempDomainName) - domainPath = os.path.join(TARGETDIR, tempDomainName) - os.umask(0o077) - os.mkdir(domainPath) - - if len(failedDomain) == 0: - codeNum = 200 - codeString = Format.spliceAllSuccString("domain", "created", successDomain) - else: - codeNum = 400 - if len(body) == 1: - if isExist: - codeString = "domain {} create failed because it has been existed.".format(failedDomain[0]) - elif not checkRes: - codeString = "domain {} create failed because format is incorrect.".format(failedDomain[0]) - else: - codeString = Format.splicErrorString("domain", "created", successDomain, failedDomain) - - base_rsp = BaseResponse(codeNum, codeString) - - return base_rsp, codeNum - - -def delete_domain(domainName): # noqa: E501 - """delete domain - - delete domain # noqa: E501 - - :param domainName: the domain that needs to be deleted - :type domainName: List[str] - - :rtype: BaseResponse - """ - if len(domainName) == 0: - codeNum = 400 - base_rsp = BaseResponse(codeNum, "The entered domian is empty") - return base_rsp, codeNum - - successDomain = [] - failedDomain = [] - - for tempDomainName in domainName: - checkRes = Format.domainCheck(tempDomainName) - isExist = Format.isDomainExist(tempDomainName) - if checkRes and isExist: - domainPath = os.path.join(TARGETDIR, tempDomainName) - successDomain.append(tempDomainName) - shutil.rmtree(domainPath) - else: - failedDomain.append(tempDomainName) - - if len(failedDomain) == 0: - codeNum = 200 - codeString = Format.spliceAllSuccString("domain", "delete", successDomain) - else: - codeNum = 400 - codeString = Format.splicErrorString("domain", "delete", successDomain, failedDomain) - - base_rsp = BaseResponse(codeNum, codeString) - return base_rsp, codeNum - - -def query_domain(): # noqa: E501 - """ - query the list of all configuration domain # noqa: E501 - :rtype: List[Domain] - """ - domain_list = [] - cmd = "ls {}".format(TARGETDIR) - gitTools = GitTools() - ls_res = gitTools.run_shell_return_output(cmd).decode() - ll_list = ls_res.split('\n') - for d_ll in ll_list: - if d_ll: - domain = Domain(domain_name = d_ll) - domain_list.append(domain) - - return domain_list, 200 diff --git a/ragdoll/controllers/format.py b/ragdoll/controllers/format.py deleted file mode 100644 index 9676296..0000000 --- a/ragdoll/controllers/format.py +++ /dev/null @@ -1,614 +0,0 @@ -import os -import re -import json -import configparser -import ast -import requests -from ragdoll.log.log import LOGGER - -from ragdoll.const.conf_handler_const import NOT_SYNCHRONIZE, SYNCHRONIZED, CONFIG, \ - DIRECTORY_FILE_PATH_LIST -from ragdoll.models import ConfSyncedRes -from ragdoll.models.base_response import BaseResponse # noqa: E501 -from ragdoll.models.conf_file import ConfFile -from ragdoll.models.conf_files import ConfFiles -from ragdoll.models.realconf_base_info import RealconfBaseInfo -from ragdoll.models.real_conf_info import RealConfInfo # noqa: E501 -from ragdoll.models.conf_is_synced import ConfIsSynced -from ragdoll.models.host_sync_status import HostSyncStatus -from ragdoll.models.single_config import SingleConfig -from ragdoll.models.sync_status import SyncStatus # noqa: E501 -from ragdoll.models.host import Host # noqa: E501 -from ragdoll.utils.host_tools import HostTools - - -class Format(object): - - @staticmethod - def domainCheck(domainName): - res = True - if not re.match(r"^[A-Za-z0-9_\.-]*$", domainName) or domainName == "" or len(domainName) > 255: - res = False - return res - - @staticmethod - def isDomainExist(domainName): - TARGETDIR = Format.get_git_dir() - domainPath = os.path.join(TARGETDIR, domainName) - if os.path.exists(domainPath): - return True - - return False - - @staticmethod - def spliceAllSuccString(obj, operation, succDomain): - """ - docstring - """ - codeString = "All {obj} {oper} successfully, {succ} {obj} in total.".format( \ - obj=obj, oper=operation, succ=len(succDomain)) - return codeString - - @staticmethod - def splicErrorString(obj, operation, succDomain, failDomain): - """ - docstring - """ - codeString = "{succ} {obj} {oper} successfully, {fail} {obj} {oper} failed.".format( \ - succ=len(succDomain), obj=obj, oper=operation, fail=len(failDomain)) - - succString = "\n" - if len(succDomain) > 0: - succString = "These are successful: " - for succName in succDomain: - succString += succName + " " - succString += "." - - if len(failDomain) > 0: - failString = "These are failed: " - for failName in failDomain: - failString += failName + " " - return codeString + succString + failString - - return codeString + succString - - @staticmethod - def two_abs_join(abs1, abs2): - """ - Absolute path Joins two absolute paths together - :param abs1: main path - :param abs2: the spliced path - :return: together the path - """ - # 1. Format path (change \\ in path to \) - abs2 = os.fspath(abs2) - - # 2. Split the path file - abs2 = os.path.splitdrive(abs2)[1] - # 3. Remove the beginning '/' - abs2 = abs2.strip('\\/') or abs2 - return os.path.abspath(os.path.join(abs1, abs2)) - - @staticmethod - def isContainedHostIdInfile(f_file, content): - isContained = False - with open(f_file, 'r') as d_file: - for line in d_file.readlines(): - line_dict = json.loads(str(ast.literal_eval(line)).replace("'", "\"")) - if content == line_dict["host_id"]: - isContained = True - break - return isContained - - @staticmethod - def addHostToFile(d_file, host): - info_json = json.dumps(str(host), sort_keys=False, indent=4, separators=(',', ': ')) - os.umask(0o077) - with open(d_file, 'a+') as host_file: - host_file.write(info_json) - host_file.write("\n") - - @staticmethod - def getSubDirFiles(path): - """ - desc: Subdirectory records and files need to be logged to the successConf - """ - fileRealPathList = [] - fileXPathlist = [] - for root, dirs, files in os.walk(path): - if len(files) > 0: - preXpath = root.split('/', 3)[3] - for d_file in files: - xpath = os.path.join(preXpath, d_file) - fileXPathlist.append(xpath) - realPath = os.path.join(root, d_file) - fileRealPathList.append(realPath) - - return fileRealPathList, fileXPathlist - - @staticmethod - def isHostInDomain(domainName): - """ - desc: Query domain Whether host information is configured in the domain - """ - isHostInDomain = False - TARGETDIR = Format.get_git_dir() - domainPath = os.path.join(TARGETDIR, domainName) - hostPath = os.path.join(domainPath, "hostRecord.txt") - if os.path.isfile(hostPath): - isHostInDomain = True - - return isHostInDomain - - @staticmethod - def isHostIdExist(hostPath, hostId): - """ - desc: Query hostId exists within the current host domain management - """ - isHostIdExist = False - if os.path.isfile(hostPath) and os.stat(hostPath).st_size > 0: - with open(hostPath) as h_file: - for line in h_file.readlines(): - if hostId in line: - isHostIdExist = True - break - - return isHostIdExist - - @staticmethod - def is_exists_file(d_file): - if os.path.exists(d_file): - return True - if os.path.islink(d_file): - LOGGER.debug("file: %s is a symlink, skipped!", d_file) - return False - LOGGER.error("file: %s does not exist.", d_file) - return False - - @staticmethod - def get_file_content_by_readlines(d_file): - """ - desc: remove empty lines and comments from d_file - """ - res = [] - try: - with open(d_file, 'r') as s_f: - lines = s_f.readlines() - for line in lines: - tmp = line.strip() - if not len(tmp) or tmp.startswith("#"): - continue - res.append(line) - except FileNotFoundError: - LOGGER.error(f"File not found: {d_file}") - except IOError as e: - LOGGER.error(f"IO error: {e}") - except Exception as e: - LOGGER.error(f"An error occurred: {e}") - return res - - @staticmethod - def get_file_content_by_read(d_file): - """ - desc: return a string after read the d_file - """ - if not os.path.exists(d_file): - return "" - with open(d_file, 'r') as s_f: - lines = s_f.read() - return lines - - @staticmethod - def rsplit(_str, seps): - """ - Splits _str by the first sep in seps that is found from the right side. - Returns a tuple without the separator. - """ - for idx, ch in enumerate(reversed(_str)): - if ch in seps: - return _str[0:-idx - 1], _str[-idx:] - - @staticmethod - def arch_sep(package_string): - """ - Helper method for finding if arch separator is '.' or '-' - - Args: - package_string (str): dash separated package string such as 'bash-4.2.39-3.el7'. - - Returns: - str: arch separator - """ - return '.' if package_string.rfind('.') > package_string.rfind('-') else '-' - - @staticmethod - def set_file_content_by_path(content, path): - res = 0 - if os.path.exists(path): - with open(path, 'w+') as d_file: - for d_cont in content: - d_file.write(d_cont) - d_file.write("\n") - res = 1 - return res - - @staticmethod - def get_git_dir(): - cf = configparser.ConfigParser() - if os.path.exists(CONFIG): - cf.read(CONFIG, encoding="utf-8") - else: - parent = os.path.dirname(os.path.realpath(__file__)) - conf_path = os.path.join(parent, "../../config/gala-ragdoll.conf") - cf.read(conf_path, encoding="utf-8") - git_dir = ast.literal_eval(cf.get("git", "git_dir")) - return git_dir - - @staticmethod - def get_hostinfo_by_domain(domainName): - """ - desc: Query hostinfo by domainname - """ - TARGETDIR = Format.get_git_dir() - hostlist = [] - domainPath = os.path.join(TARGETDIR, domainName) - hostPath = os.path.join(domainPath, "hostRecord.txt") - if not os.path.isfile(hostPath) or os.stat(hostPath).st_size == 0: - return hostlist - try: - with open(hostPath, 'r') as d_file: - for line in d_file.readlines(): - json_str = json.loads(line) - host_json = ast.literal_eval(json_str) - hostId = host_json["host_id"] - ip = host_json["ip"] - ipv6 = host_json["ipv6"] - host = Host(host_id=hostId, ip=ip, ipv6=ipv6) - hostlist.append(host.to_dict()) - except OSError as err: - LOGGER.error("OS error: {0}".format(err)) - return hostlist - if len(hostlist) == 0: - LOGGER.debug("hostlist is empty : {}".format(hostlist)) - else: - LOGGER.debug("hostlist is : {}".format(hostlist)) - return hostlist - - @staticmethod - def get_manageconf_by_domain(domain): - expected_conf_lists = ConfFiles(domain_name=domain, conf_files=[]) - TARGETDIR = Format.get_git_dir() - domainPath = os.path.join(TARGETDIR, domain) - from ragdoll.utils.yang_module import YangModule - for root, dirs, files in os.walk(domainPath): - if len(files) > 0 and len(root.split('/')) > 3: - if "hostRecord.txt" in files: - continue - for d_file in files: - d_file_path = os.path.join(root, d_file) - contents = Format.get_file_content_by_read(d_file_path) - feature = os.path.join(root.split('/')[-1], d_file) - yang_modules = YangModule() - d_module = yang_modules.getModuleByFeature(feature) - file_lists = yang_modules.getFilePathInModdule(yang_modules.module_list) - file_path = file_lists.get(d_module.name()).split(":")[-1] - - conf = ConfFile(file_path=file_path, contents=contents) - expected_conf_lists.conf_files.append(conf.to_dict()) - - LOGGER.debug("expected_conf_lists is :{}".format(expected_conf_lists)) - return expected_conf_lists.to_dict() - - @staticmethod - def get_realconf_by_domain_and_host(domain, exist_host): - res = [] - conf_files = Format.get_manageconf_by_domain(domain) - - # get the real conf in host - conf_list = [] - from ragdoll.utils.conf_tools import ConfTools - from ragdoll.utils.object_parse import ObjectParse - conf_tools = ConfTools() - for d_conf in conf_files.get("conf_files"): - file_path = d_conf.get("file_path").split(":")[-1] - if file_path not in DIRECTORY_FILE_PATH_LIST: - conf_list.append(file_path) - else: - d_conf_cs = d_conf.get("contents") - d_conf_contents = json.loads(d_conf_cs) - for d_conf_key, d_conf_value in d_conf_contents.items(): - conf_list.append(d_conf_key) - LOGGER.debug("############## get the real conf in host ##############") - get_real_conf_body = {} - get_real_conf_body_info = [] - for d_host in exist_host: - get_real_conf_body_infos = {} - get_real_conf_body_infos["host_id"] = d_host - get_real_conf_body_infos["config_list"] = conf_list - get_real_conf_body_info.append(get_real_conf_body_infos) - get_real_conf_body["infos"] = get_real_conf_body_info - url = conf_tools.load_url_by_conf().get("collect_url") - headers = {"Content-Type": "application/json"} - try: - response = requests.post(url, data=json.dumps(get_real_conf_body), headers=headers) # post request - except requests.exceptions.RequestException as connect_ex: - LOGGER.error(f"An error occurred: {connect_ex}") - codeNum = 500 - codeString = "Failed to obtain the actual configuration, please check the interface of config/collect." - base_rsp = BaseResponse(codeNum, codeString) - return base_rsp, codeNum - resp = json.loads(response.text).get("data") - resp_code = json.loads(response.text).get("code") - if (resp_code != "200") and (resp_code != "206"): - return res - - if not resp or len(resp) == 0: - return res - - success_lists = {} - failed_lists = {} - - for d_res in resp: - d_host_id = d_res.get("host_id") - fail_files = d_res.get("fail_files") - if len(fail_files) > 0: - failed_lists["host_id"] = d_host_id - failed_lists_conf = [] - for d_failed in fail_files: - failed_lists_conf.append(d_failed) - failed_lists["failed_conf"] = failed_lists_conf - failed_lists["success_conf"] = [] - else: - success_lists["host_id"] = d_host_id - success_lists["success_conf"] = [] - success_lists["failed_conf"] = [] - - read_conf_info = RealConfInfo(domain_name=domain, - host_id=d_host_id, - conf_base_infos=[]) - d_res_infos = d_res.get("infos") - - real_directory_conf = {} - real_directory_conf_list = {} - object_parse = ObjectParse() - for d_file in d_res_infos: - content = d_file.get("content") - file_path = d_file.get("path") - file_atrr = d_file.get("file_attr").get("mode") - file_owner = "({}, {})".format(d_file.get("file_attr").get("group"), - d_file.get("file_attr").get("owner")) - directory_flag = False - for dir_path in DIRECTORY_FILE_PATH_LIST: - if str(file_path).find(dir_path) != -1: - if real_directory_conf.get(dir_path) is None: - real_directory_conf_list[dir_path] = list() - real_directory_conf[dir_path] = RealconfBaseInfo(file_path=dir_path, - file_attr=file_atrr, - file_owner=file_owner, - conf_contens="") - - directory_conf = dict() - directory_conf["path"] = file_path - directory_conf["content"] = content - real_directory_conf_list.get(dir_path).append(directory_conf) - directory_flag = True - break - if not directory_flag: - Format.deal_conf_list_content(content, d_file, file_path, object_parse, read_conf_info) - if len(fail_files) > 0: - failed_lists.get("success_conf").append(file_path) - else: - success_lists.get("success_conf").append(file_path) - - for dir_path, dir_value in real_directory_conf_list.items(): - content_string = object_parse.parse_directory_single_conf_to_json(dir_value, - real_directory_conf[ - dir_path].file_path) - real_directory_conf[dir_path].conf_contens = content_string - real_conf_base_info = real_directory_conf.get(dir_path) - - read_conf_info.conf_base_infos.append(real_conf_base_info) - res.append(read_conf_info) - return res - - @staticmethod - def deal_conf_list_content(content, d_file, file_path, object_parse, read_conf_info): - content_string = object_parse.parse_conf_to_json(file_path, content) - file_atrr = d_file.get("file_attr").get("mode") - file_owner = "({}, {})".format(d_file.get("file_attr").get("group"), - d_file.get("file_attr").get("owner")) - real_conf_base_info = RealconfBaseInfo(path=file_path, - file_path=file_path, - file_attr=file_atrr, - file_owner=file_owner, - conf_contens=content_string) - read_conf_info.conf_base_infos.append(real_conf_base_info) - - @staticmethod - def check_domain_param(domain): - code_num = 200 - base_resp = None - check_res = Format.domainCheck(domain) - if not check_res: - num = 400 - base_rsp = BaseResponse(num, "Failed to verify the input parameter, please check the input parameters.") - return base_rsp, num - - # check the domian is exist - is_exist = Format.isDomainExist(domain) - if not is_exist: - code_num = 404 - base_rsp = BaseResponse(code_num, "The current domain does not exist, please create the domain first.") - return base_rsp, code_num - - # get the exist result of the host in domain - is_host_list_exist = Format.isHostInDomain(domain) - if not is_host_list_exist: - code_num = 404 - base_rsp = BaseResponse(code_num, "The host information is not set in the current domain." + - "Please add the host information first") - return base_resp, code_num - - @staticmethod - def get_hostid_list_by_domain(domain): - host_ids = [] - res_text = Format.get_hostinfo_by_domain(domain) - if len(res_text) == 0: - return host_ids - - host_tools = HostTools() - host_ids = host_tools.getHostList(res_text) - return host_ids - - @staticmethod - def _get_domain_conf(domain): - code_num = 200 - base_resp = None - # get the host info in domain - LOGGER.debug("############## get the host in domain ##############") - host_ids = Format.get_hostid_list_by_domain(domain) - if not host_ids: - code_num = 404 - base_resp = BaseResponse(code_num, "The host currently controlled in the domain is empty." + - "Please add host information to the domain.") - return base_resp, code_num, list() - - # get the managent conf in domain - LOGGER.debug("############## get the managent conf in domain ##############") - man_conf_res_text = Format.get_manageconf_by_domain(domain) - manage_confs = man_conf_res_text.get("conf_files") - - if len(manage_confs) == 0: - code_num = 404 - base_resp = BaseResponse(code_num, "The configuration is not set in the current domain." + - "Please add the configuration information first.") - return base_resp, code_num, list() - return base_resp, code_num, manage_confs - - @staticmethod - def diff_mangeconf_with_realconf(domain, real_conf_res_text, manage_confs): - sync_status = SyncStatus(domain_name=domain, - host_status=[]) - from ragdoll.utils.object_parse import ObjectParse - - for d_real_conf in real_conf_res_text: - host_id = d_real_conf.host_id - host_sync_status = HostSyncStatus(host_id=host_id, - sync_status=[]) - d_real_conf_base = d_real_conf.conf_base_infos - for d_conf in d_real_conf_base: - directory_conf_is_synced = ConfIsSynced(file_path="", is_synced="", single_conf=[]) - d_conf_path = d_conf.file_path - - object_parse = ObjectParse() - # get the conf type and model - conf_type, conf_model = Format.get_conf_type_model(d_conf_path, object_parse) - - Format.deal_conf_sync_status(conf_model, d_conf, d_conf_path, directory_conf_is_synced, - host_sync_status, manage_confs) - - if len(directory_conf_is_synced.single_conf) > 0: - synced_flag = SYNCHRONIZED - for single_config in directory_conf_is_synced.single_conf: - if single_config.single_is_synced == SYNCHRONIZED: - continue - else: - synced_flag = NOT_SYNCHRONIZE - directory_conf_is_synced.is_synced = synced_flag - host_sync_status.sync_status.append(directory_conf_is_synced) - sync_status.host_status.append(host_sync_status) - return sync_status - - @staticmethod - def deal_conf_sync_status(conf_model, d_conf, d_conf_path, directory_conf_is_synced, host_sync_status, - manage_confs): - comp_res = "" - if d_conf_path in DIRECTORY_FILE_PATH_LIST: - confContents = json.loads(d_conf.conf_contens) - directory_conf_contents = "" - for d_man_conf in manage_confs: - d_man_conf_path = d_man_conf.get("file_path") - if d_man_conf_path != d_conf_path: - # if d_man_conf_path not in DIRECTORY_FILE_PATH_LIST: - continue - else: - directory_conf_is_synced.file_path = d_conf_path - directory_conf_contents = d_man_conf.get("contents") - - directory_conf_contents_dict = json.loads(directory_conf_contents) - - for dir_conf_content_key, dir_conf_content_value in directory_conf_contents_dict.items(): - if dir_conf_content_key not in confContents.keys(): - single_conf = SingleConfig(single_file_path=dir_conf_content_key, - single_is_synced=NOT_SYNCHRONIZE) - directory_conf_is_synced.single_conf.append(single_conf) - else: - dst_conf = confContents.get(dir_conf_content_key) - comp_res = conf_model.conf_compare(dir_conf_content_value, dst_conf) - single_conf = SingleConfig(single_file_path=dir_conf_content_key, single_is_synced=comp_res) - directory_conf_is_synced.single_conf.append(single_conf) - else: - for d_man_conf in manage_confs: - if d_man_conf.get("file_path").split(":")[-1] != d_conf_path: - continue - comp_res = conf_model.conf_compare(d_man_conf.get("contents"), d_conf.conf_contens) - conf_is_synced = ConfIsSynced(file_path=d_conf_path, - is_synced=comp_res) - host_sync_status.sync_status.append(conf_is_synced) - - @staticmethod - def get_conf_type_model(d_conf_path, object_parse): - for dir_path in DIRECTORY_FILE_PATH_LIST: - if str(d_conf_path).find(dir_path) != -1: - conf_type = object_parse.get_conf_type_by_conf_path(dir_path) - conf_model = object_parse.create_conf_model_by_type(conf_type) - else: - conf_type = object_parse.get_conf_type_by_conf_path(d_conf_path) - conf_model = object_parse.create_conf_model_by_type(conf_type) - return conf_type, conf_model - - @staticmethod - def deal_sync_res(conf_tools, contents, file_path, host_id, host_sync_result, object_parse): - sync_conf_url = conf_tools.load_url_by_conf().get("sync_url") - headers = {"Content-Type": "application/json"} - if file_path in DIRECTORY_FILE_PATH_LIST: - conf_sync_res_list = [] - for directory_file_path, directory_content in json.loads(contents).items(): - content = object_parse.parse_json_to_conf(directory_file_path, directory_content) - # Configuration to the host - data = {"host_id": host_id, "file_path": directory_file_path, "content": content} - try: - sync_response = requests.put(sync_conf_url, data=json.dumps(data), headers=headers) - except requests.exceptions.RequestException as connect_ex: - LOGGER.error(f"An error occurred: {connect_ex}") - codeNum = 500 - codeString = "Failed to sync configuration, please check the interface of config/sync." - base_rsp = BaseResponse(codeNum, codeString) - return base_rsp, codeNum - resp_code = json.loads(sync_response.text).get('code') - resp = json.loads(sync_response.text).get('data').get('resp') - - if resp_code == "200" and resp.get('sync_result') is True: - conf_sync_res_list.append("SUCCESS") - else: - conf_sync_res_list.append("FAILED") - if "FAILED" in conf_sync_res_list: - conf_sync_res = ConfSyncedRes(file_path=file_path, result="FAILED") - else: - conf_sync_res = ConfSyncedRes(file_path=file_path, result="SUCCESS") - host_sync_result.sync_result.append(conf_sync_res) - else: - content = object_parse.parse_json_to_conf(file_path, contents) - # Configuration to the host - data = {"host_id": host_id, "file_path": file_path, "content": content} - sync_response = requests.put(sync_conf_url, data=json.dumps(data), headers=headers) - - resp_code = json.loads(sync_response.text).get('code') - resp = json.loads(sync_response.text).get('data').get('resp') - conf_sync_res = ConfSyncedRes(file_path=file_path, - result="") - if resp_code == "200" and resp.get('sync_result') is True: - conf_sync_res.result = "SUCCESS" - else: - conf_sync_res.result = "FAILED" - host_sync_result.sync_result.append(conf_sync_res) diff --git a/ragdoll/controllers/host_controller.py b/ragdoll/controllers/host_controller.py deleted file mode 100644 index 1f491fe..0000000 --- a/ragdoll/controllers/host_controller.py +++ /dev/null @@ -1,273 +0,0 @@ -import connexion -import six -import os -import json -import re -import ast - -from ragdoll.log.log import LOGGER -from ragdoll.models.base_response import BaseResponse # noqa: E501 -from ragdoll.models.domain_name import DomainName # noqa: E501 -from ragdoll.models.host import Host # noqa: E501 -from ragdoll.models.host_infos import HostInfos # noqa: E501 -from ragdoll import util -from ragdoll.controllers.format import Format -from ragdoll.utils.git_tools import GitTools - -TARGETDIR = GitTools().target_dir - -def add_host_in_domain(body=None): # noqa: E501 - """add host in the configuration domain - - add host in the configuration domain # noqa: E501 - - :param body: domain info - :type body: dict | bytes - - :rtype: BaseResponse - """ - if connexion.request.is_json: - body = HostInfos.from_dict(connexion.request.get_json()) # noqa: E501 - - domain = body.domain_name - host_infos = body.host_infos - - # check whether host_infos is empty - if len(host_infos) == 0: - num = 400 - base_rsp = BaseResponse(num, "Enter host info cannot be empty, please check the host info.") - return base_rsp, num - - checkRes = Format.domainCheck(domain) - if not checkRes: - num = 400 - base_rsp = BaseResponse(num, "Failed to verify the input parameter, please check the input parameters.") - return base_rsp, num - - # check whether the domain exists - isExist = Format.isDomainExist(domain) - if not isExist: - num = 400 - base_rsp = BaseResponse(num, "The current domain does not exist, please create the domain first.") - return base_rsp, num - - successHost = [] - failedHost = [] - domainPath = os.path.join(TARGETDIR, domain) - - # Check whether the current host exists in the domain. - for host in host_infos: - hostPath = os.path.join(domainPath, "hostRecord.txt") - if os.path.isfile(hostPath): - isContained = Format.isContainedHostIdInfile(hostPath, host.host_id) - if isContained: - LOGGER.debug("##########isContained###############") - failedHost.append(host.host_id) - else: - Format.addHostToFile(hostPath, host) - successHost.append(host.host_id) - else: - Format.addHostToFile(hostPath, host) - successHost.append(host.host_id) - - if len(failedHost) == len(host_infos): - codeNum = 400 - base_rsp = BaseResponse(codeNum, "The all host already exists in the administrative scope of the domain.") - return base_rsp, codeNum - - # Joining together the returned codenum codeMessage - if len(failedHost) == 0: - codeNum = 200 - codeString = Format.spliceAllSuccString("host", "add hosts", successHost) - else: - codeNum = 202 - codeString = Format.splicErrorString("host", "add hosts", successHost, failedHost) - - # git commit maessage - if len(host_infos) > 0: - git_tools = GitTools() - commit_code = git_tools.gitCommit("Add the host in {} domian, ".format(domain) + - "the host including : {}".format(successHost)) - - base_rsp = BaseResponse(codeNum, codeString) - - return base_rsp, codeNum - - -def delete_host_in_domain(body=None): # noqa: E501 - """delete host in the configuration domain - - delete the host in the configuration domain # noqa: E501 - - :param body: domain info - :type body: dict | bytes - - :rtype: BaseResponse - """ - if connexion.request.is_json: - body = HostInfos.from_dict(connexion.request.get_json()) # noqa: E501 - - domain = body.domain_name - hostInfos = body.host_infos - - # check the input domain - checkRes = Format.domainCheck(domain) - if not checkRes: - num = 400 - base_rsp = BaseResponse(num, "Failed to verify the input parameter, please check the input parameters.") - return base_rsp, num - - # check whether the domain exists - isExist = Format.isDomainExist(domain) - if not isExist: - codeNum = 400 - base_rsp = BaseResponse(codeNum, "The current domain does not exist, please create the domain first.") - return base_rsp, codeNum - - # Whether the host information added within the current domain is empty while ain exists - domainPath = os.path.join(TARGETDIR, domain) - hostPath = os.path.join(domainPath, "hostRecord.txt") - if not os.path.isfile(hostPath) or (os.path.isfile(hostPath) and os.stat(hostPath).st_size == 0): - codeNum = 400 - base_rsp = BaseResponse(codeNum, "The host information is not set in the current domain." + - "Please add the host information first") - return base_rsp, codeNum - - # If the input host information is empty, the host information of the whole domain is cleared - if len(hostInfos) == 0: - if os.path.isfile(hostPath): - try: - os.remove(hostPath) - except OSError as ex: - #logging.error("the host delete failed") - codeNum = 500 - base_rsp = BaseResponse(codeNum, "The host delete failed.") - return base_rsp, codeNum - codeNum = 200 - base_rsp = BaseResponse(codeNum, "All hosts are deleted in the current domain.") - return base_rsp, codeNum - - # If the domain exists, check whether the current input parameter host belongs to the corresponding - # domain. If the host is in the domain, the host is deleted. If the host is no longer in the domain, - # the host is added to the failure range - containedInHost = [] - notContainedInHost = [] - os.umask(0o077) - for hostInfo in hostInfos: - hostId = hostInfo.host_id - isContained = False - try: - with open(hostPath, 'r') as d_file: - lines = d_file.readlines() - with open(hostPath, 'w') as w_file: - for line in lines: - line_host_id = json.loads(str(ast.literal_eval(line)).replace("'", "\""))['host_id'] - if hostId != line_host_id: - w_file.write(line) - else: - isContained = True - except OSError as err: - LOGGER.error("OS error: {0}".format(err)) - codeNum = 500 - base_rsp = BaseResponse(codeNum, "OS error: {0}".format(err)) - return base_rsp, codeNum - - if isContained: - containedInHost.append(hostId) - else: - notContainedInHost.append(hostId) - - # All hosts do not belong to the domain - if len(notContainedInHost) == len(hostInfos): - codeNum = 400 - base_rsp = BaseResponse(codeNum, "All the host does not belong to the domain control, " + - "please enter the host again") - return base_rsp, codeNum - - # Some hosts belong to domains, and some hosts do not belong to domains. - if len(notContainedInHost) == 0: - codeNum = 200 - codeString = Format.spliceAllSuccString("host", "delete", containedInHost) - else: - codeNum = 400 - codeString = Format.splicErrorString("host", "delete", containedInHost, notContainedInHost) - - # git commit message - if len(containedInHost) > 0: - git_tools = GitTools() - commit_code = git_tools.gitCommit("Delet the host in {} domian, ".format(domain) + - "the host including : {}".format(containedInHost)) - - base_rsp = BaseResponse(codeNum, codeString) - - return base_rsp, codeNum - - -def get_host_by_domain_name(body=None): # noqa: E501 - """get host by domainName - - get the host information of the configuration domain # noqa: E501 - - :param body: domain info - :type body: dict | bytes - - :rtype: List[Host] - """ - if connexion.request.is_json: - body = DomainName.from_dict(connexion.request.get_json()) # noqa: E501 - - domain = body.domain_name - - # check the input domain - checkRes = Format.domainCheck(domain) - if not checkRes: - num = 400 - base_rsp = BaseResponse(num, "Failed to verify the input parameter, please check the input parameters.") - return base_rsp, num - - # check whether the domain exists - isExist = Format.isDomainExist(domain) - if not isExist: - codeNum = 400 - base_rsp = BaseResponse(codeNum, "The current domain does not exist, please create the domain first.") - return base_rsp, codeNum - - # The domain exists, but the host information is empty - domainPath = os.path.join(TARGETDIR, domain) - hostPath = os.path.join(domainPath, "hostRecord.txt") - if not os.path.isfile(hostPath) or (os.path.isfile(hostPath) and os.stat(hostPath).st_size == 0): - codeNum = 400 - base_rsp = BaseResponse(codeNum, "The host information is not set in the current domain." + - "Please add the host information first.") - return base_rsp, codeNum - - # The domain exists, and the host information exists and is not empty - hostlist = [] - LOGGER.debug("hostPath is : {}".format(hostPath)) - try: - with open(hostPath, 'r') as d_file: - for line in d_file.readlines(): - json_str = json.loads(line) - host_json = ast.literal_eval(json_str) - hostId = host_json["host_id"] - ip = host_json["ip"] - ipv6 = host_json["ipv6"] - host = Host(host_id=hostId, ip=ip, ipv6=ipv6) - hostlist.append(host) - except OSError as err: - LOGGER.error("OS error: {0}".format(err)) - codeNum = 500 - base_rsp = BaseResponse(codeNum, "OS error: {0}".format(err)) - return base_rsp, codeNum - - # Joining together the returned codenum codeMessag - if len(hostlist) == 0: - codeNum = 500 - base_rsp = BaseResponse(codeNum, "Some unknown problems.") - return base_rsp, codeNum - else: - LOGGER.debug("hostlist is : {}".format(hostlist)) - codeNum = 200 - base_rsp = BaseResponse(codeNum, "Get host info in the domain succeccfully") - - return hostlist diff --git a/ragdoll/controllers/management_controller.py b/ragdoll/controllers/management_controller.py deleted file mode 100644 index 101802a..0000000 --- a/ragdoll/controllers/management_controller.py +++ /dev/null @@ -1,601 +0,0 @@ -import io - -import connexion -import os -import json -import requests - -from ragdoll.const.conf_handler_const import DIRECTORY_FILE_PATH_LIST -from ragdoll.log.log import LOGGER -from ragdoll.models.base_response import BaseResponse # noqa: E501 -from ragdoll.models.confs import Confs -from ragdoll.models.conf_file import ConfFile -from ragdoll.models.conf_files import ConfFiles -from ragdoll.models.conf_base_info import ConfBaseInfo -from ragdoll.models.excepted_conf_info import ExceptedConfInfo -from ragdoll.models.domain_name import DomainName # noqa: E501 -from ragdoll.models.manage_confs import ManageConfs -from ragdoll.controllers.format import Format -from ragdoll.utils.conf_tools import ConfTools -from ragdoll.utils.git_tools import GitTools -from ragdoll.utils.yang_module import YangModule -from ragdoll.utils.object_parse import ObjectParse - -TARGETDIR = GitTools().target_dir - - -def add_management_confs_in_domain(body=None): # noqa: E501 - """add management configuration items and expected values in the domain - - add management configuration items and expected values in the domain # noqa: E501 - - :param body: domain info - :type body: dict | bytes - - :rtype: BaseResponse - """ - if connexion.request.is_json: - body = Confs.from_dict(connexion.request.get_json()) # noqa: E501 - - domain = body.domain_name - conf_files = body.conf_files - - # check the input domain - checkRes = Format.domainCheck(domain) - if not checkRes: - num = 400 - base_rsp = BaseResponse(num, "Failed to verify the input parameter, please check the input parameters.") - return base_rsp, num - - # check whether the domain exists - isExist = Format.isDomainExist(domain) - if not isExist: - codeNum = 400 - base_rsp = BaseResponse(codeNum, "The current domain does not exist, please create the domain first.") - return base_rsp, codeNum - - # check whether the conf_files is null - if len(conf_files) == 0: - codeNum = 400 - base_rsp = BaseResponse(codeNum, "The path of file can't be empty") - return base_rsp, codeNum - - # Check all conf_files and check whether contents is empty. If so, the query actual configuration - # interface is called. If not, the conversion is performed directly. - # Content and host_id can be set to either content or host_id. - # If they are both empty, invalid input is returned. - contents_list_null = [] - contents_list_non_null = [] - for d_conf in conf_files: - if d_conf.contents: - contents_list_non_null.append(d_conf) - elif d_conf.host_id: - contents_list_null.append(d_conf) - else: - codeNum = 400 - base_rsp = BaseResponse(codeNum, "The input parameters are not compliant, " + - "please check the input parameters.") - return base_rsp, codeNum - - successConf = [] - failedConf = [] - object_parse = ObjectParse() - yang_module = YangModule() - conf_tools = ConfTools() - # Content is not an empty scene and is directly analyed and parsed - if len(contents_list_non_null) > 0: - for d_conf in contents_list_non_null: - if not d_conf.contents.strip(): - codeNum = 400 - base_rsp = BaseResponse(codeNum, "The input parameters are not compliant, " + - "please check the input parameters.") - return base_rsp, codeNum - content_string = object_parse.parse_conf_to_json(d_conf.file_path, d_conf.contents) - if not content_string or not json.loads(content_string): - failedConf.append(d_conf.file_path) - else: - # create the file and expected value in domain - feature_path = yang_module.get_feature_by_real_path(domain, d_conf.file_path) - result = conf_tools.wirteFileInPath(feature_path, content_string + '\n') - if result: - successConf.append(d_conf.file_path) - else: - failedConf.append(d_conf.file_path) - - # content is empty - if len(contents_list_null) > 0: - # get the real conf in host - LOGGER.debug("############## get the real conf in host ##############") - get_real_conf_body = {} - get_real_conf_body_info = [] - LOGGER.debug("contents_list_null is : {}".format(contents_list_null)) - exist_host = dict() - for d_conf in contents_list_null: - host_id = int(d_conf.host_id) - if host_id in exist_host: - if d_conf.file_path not in DIRECTORY_FILE_PATH_LIST: - exist_host[host_id].append(d_conf.file_path) - else: - codeNum, codeString, file_paths = object_parse.get_directory_files(d_conf, host_id) - if len(file_paths) == 0: - base_rsp = BaseResponse(codeNum, codeString) - return base_rsp, codeNum - else: - for file_path in file_paths: - exist_host[host_id].append(file_path) - else: - if d_conf.file_path not in DIRECTORY_FILE_PATH_LIST: - conf_list = list() - conf_list.append(d_conf.file_path) - exist_host[host_id] = conf_list - else: - codeNum, codeString, file_paths = object_parse.get_directory_files(d_conf, host_id) - if len(file_paths) == 0: - base_rsp = BaseResponse(codeNum, codeString) - return base_rsp, codeNum - else: - exist_host[host_id] = file_paths - - for k, v in exist_host.items(): - confs = dict() - confs["host_id"] = k - confs["config_list"] = v - get_real_conf_body_info.append(confs) - - get_real_conf_body["infos"] = get_real_conf_body_info - - url = conf_tools.load_url_by_conf().get("collect_url") - headers = {"Content-Type": "application/json"} - try: - response = requests.post(url, data=json.dumps(get_real_conf_body), headers=headers) # post request - except requests.exceptions.RequestException as connect_ex: - LOGGER.error(f"An error occurred: {connect_ex}") - codeNum = 500 - codeString = "Failed to obtain the actual configuration, please check the interface of config/collect." - base_rsp = BaseResponse(codeNum, codeString) - return base_rsp, codeNum - - response_code = json.loads(response.text).get("code") - if response_code == None: - codeNum = 500 - codeString = "Failed to obtain the actual configuration, please check the interface of conf/collect." - base_rsp = BaseResponse(codeNum, codeString) - return base_rsp, codeNum - - if (response_code != "200") and (response_code != "206"): - codeNum = 500 - codeString = "Failed to obtain the actual configuration, please check the file exists." - base_rsp = BaseResponse(codeNum, codeString) - return base_rsp, codeNum - - reps = json.loads(response.text).get("data") - if not reps or len(reps) == 0: - codeNum = 500 - codeString = "Failed to obtain the actual configuration, please check the host info for conf/collect." - base_rsp = BaseResponse(codeNum, codeString) - return base_rsp, codeNum - - directory_d_file = [] - directory_d_files = {} - for d_res in reps: - failedlist = d_res.get("fail_files") - if len(failedlist) > 0: - for d_failed in failedlist: - failedConf.append(d_failed) - continue - d_res_infos = d_res.get("infos") - for d_file in d_res_infos: - for dir_path in DIRECTORY_FILE_PATH_LIST: - if str(d_file.get("path")).find(dir_path) == -1: - file_path = d_file.get("path") - content = d_file.get("content") - content_string = object_parse.parse_conf_to_json(file_path, content) - # create the file and expected value in domain - if not content_string or not json.loads(content_string): - failedConf.append(file_path) - else: - feature_path = yang_module.get_feature_by_real_path(domain, file_path) - result = conf_tools.wirteFileInPath(feature_path, content_string + '\n') - if result: - successConf.append(file_path) - else: - failedConf.append(file_path) - else: - directory_d_file.append(d_file) - directory_d_files[dir_path] = directory_d_file - if len(directory_d_files) > 0: - for dir_path, directory_d_file in directory_d_files.items(): - content_string = object_parse.parse_dir_conf_to_json(dir_path, directory_d_file) - if not content_string or not json.loads(content_string): - failedConf.append(dir_path) - else: - feature_path = yang_module.get_feature_by_real_path(domain, dir_path) - result = conf_tools.wirteFileInPath(feature_path, content_string + '\n') - if result: - successConf.append(dir_path) - else: - failedConf.append(dir_path) - # git commit message - if len(successConf) > 0: - git_tools = GitTools() - succ_conf = "" - for d_conf in successConf: - succ_conf = succ_conf + d_conf + " " - commit_code = git_tools.gitCommit("Add the conf in {} domian, ".format(domain) + - "the path including : {}".format(succ_conf)) - - # Joinin together the returned codenum and codeMessage - LOGGER.debug("*******************************************") - LOGGER.debug("successConf is : {}".format(successConf)) - LOGGER.debug("failedConf is : {}".format(failedConf)) - if len(successConf) == 0: - codeNum = 400 - codeString = "All configurations failed to be added." - elif len(failedConf) > 0: - codeNum = 206 - codeString = Format.splicErrorString("confs", "add management conf", successConf, failedConf) - else: - codeNum = 200 - codeString = Format.spliceAllSuccString("confs", "add management conf", successConf) - - base_rsp = BaseResponse(codeNum, codeString) - - return base_rsp, codeNum - - -def upload_management_confs_in_domain(): # noqa: E501 - """upload management configuration items and expected values in the domain - - upload management configuration items and expected values in the domain # noqa: E501 - - :param body: file info - :type body: FileStorage - - :rtype: BaseResponse - """ - file = connexion.request.files['file'] - filePath = connexion.request.form.get("filePath") - domainName = connexion.request.form.get("domainName") - - # check the input domainName - checkRes = Format.domainCheck(domainName) - if not checkRes: - num = 400 - base_rsp = BaseResponse(num, "Failed to verify the input parameter, please check the input parameters.") - return base_rsp, num - - # check whether the domainName exists - isExist = Format.isDomainExist(domainName) - if not isExist: - codeNum = 400 - base_rsp = BaseResponse(codeNum, "The current domain does not exist, please create the domain first.") - return base_rsp, codeNum - - # check whether the file is null - if file is None: - codeNum = 400 - base_rsp = BaseResponse(codeNum, "The file of conf can't be empty") - return base_rsp, codeNum - - # check whether the conf is null - if filePath is None: - codeNum = 400 - base_rsp = BaseResponse(codeNum, "The conf body of conf can't be empty") - return base_rsp, codeNum - - successConf = [] - failedConf = [] - object_parse = ObjectParse() - yang_module = YangModule() - conf_tools = ConfTools() - - # content is file - if file: - if not filePath.strip(): - codeNum = 400 - base_rsp = BaseResponse(codeNum, "The input parameters are not compliant, " + - "please check the input parameters.") - return base_rsp, codeNum - try: - file_bytes = file.read() - if len(file_bytes) > 1024 * 1024: - codeNum = 400 - base_rsp = BaseResponse(codeNum, "The size of the uploaded file must be less than 1MB") - return base_rsp, codeNum - byte_stream = io.BytesIO(file_bytes) - - # Read the contents of the byte stream - line_content = byte_stream.read().decode("UTF-8") - except OSError as err: - LOGGER.error("OS error: {}".format(err)) - codeNum = 500 - base_rsp = BaseResponse(codeNum, "OS error: {0}".format(err)) - return base_rsp, codeNum - except Exception as ex: - LOGGER.error("OS error: {}".format(ex)) - codeNum = 500 - base_rsp = BaseResponse(codeNum, "read file error: {0}".format(ex)) - return base_rsp, codeNum - - content_string = object_parse.parse_conf_to_json(filePath, line_content) - if not content_string or not json.loads(content_string): - failedConf.append(filePath) - else: - # create the file and expected value in domain - feature_path = yang_module.get_feature_by_real_path(domainName, filePath) - result = conf_tools.wirteFileInPath(feature_path, content_string + '\n') - if result: - successConf.append(filePath) - else: - failedConf.append(filePath) - - # git commit message - if len(successConf) > 0: - git_tools = GitTools() - succ_conf = "" - for d_conf in successConf: - succ_conf = succ_conf + d_conf + " " - commit_code = git_tools.gitCommit("Add the conf in {} domian, ".format(domainName) + - "the path including : {}".format(succ_conf)) - - # Joinin together the returned codenum and codeMessage - LOGGER.debug("*******************************************") - LOGGER.debug("successConf is : {}".format(successConf)) - LOGGER.debug("failedConf is : {}".format(failedConf)) - if len(successConf) == 0: - codeNum = 400 - codeString = "All configurations failed to be added." - elif len(failedConf) > 0: - codeNum = 206 - codeString = Format.splicErrorString("confs", "add management conf", successConf, failedConf) - else: - codeNum = 200 - codeString = Format.spliceAllSuccString("confs", "add management conf", successConf) - - base_rsp = BaseResponse(codeNum, codeString) - - return base_rsp, codeNum - - -def delete_management_confs_in_domain(body=None): # noqa: E501 - """delete management configuration items and expected values in the domain - - delete management configuration items and expected values in the domain # noqa: E501 - - :param body: domain info - :type body: dict | bytes - - :rtype: BaseResponse - """ - if connexion.request.is_json: - body = ManageConfs.from_dict(connexion.request.get_json()) # noqa: E501 - - # check whether the domain exists - domain = body.domain_name - - # check the input domain - checkRes = Format.domainCheck(domain) - if not checkRes: - num = 400 - base_rsp = BaseResponse(num, "Failed to verify the input parameter, please check the input parameters.") - return base_rsp, num - - isExist = Format.isDomainExist(domain) - if not isExist: - codeNum = 400 - base_rsp = BaseResponse(codeNum, "The current domain does not exist") - return base_rsp, codeNum - - # Check whether path is null in advance - conf_files = body.conf_files - if len(conf_files) == 0: - codeNum = 400 - base_rsp = BaseResponse(codeNum, "The conf_files path can't be empty") - return base_rsp, codeNum - - # Conf to record successes and failures - successConf = [] - failedConf = [] - - # Check whether path exists in the domain. There are two possible paths : - # (1)xpath path - # (2) configuration item - domain_path = os.path.join(TARGETDIR, domain) - LOGGER.debug("conf_files is : {}".format(conf_files)) - - yang_modules = YangModule() - module_lists = yang_modules.module_list - if len(module_lists) == 0: - base_rsp = BaseResponse(400, "The yang module does not exist") - return base_rsp - - file_path_list = yang_modules.getFilePathInModdule(module_lists) - LOGGER.debug("module_lists is : {}".format(module_lists)) - for conf in conf_files: - module = yang_modules.getModuleByFilePath(conf.file_path) - features = yang_modules.getFeatureInModule(module) - features_path = os.path.join(domain_path, "/".join(features)) - LOGGER.debug("domain_path is : {}".format(domain_path)) - - if os.path.isfile(features_path): - LOGGER.debug("it's a normal file") - try: - os.remove(features_path) - except OSError as ex: - # logging.error("the path remove failed") - break - successConf.append(conf.file_path) - else: - failedConf.append(conf.file_path) - - # git commit message - if len(successConf) > 0: - git_tools = GitTools() - succ_conf = "" - for d_conf in successConf: - succ_conf = succ_conf + d_conf + " " - commit_code = git_tools.gitCommit("delete the conf in {} domian, ".format(domain) + - "the path including : {}".format(succ_conf)) - - # Joinin together the returned codenum and codeMessage - if len(failedConf) == 0: - codeNum = 200 - codeString = Format.spliceAllSuccString("confs", "delete management conf", successConf) - else: - codeNum = 400 - codeString = Format.splicErrorString("confs", "delete management conf", successConf, failedConf) - codeString += "\n The reason for the failure is: these paths do not exist." - base_rsp = BaseResponse(codeNum, codeString) - # logging.info('delete management conf in {domain}'.format(domain=domain)) - - return base_rsp, codeNum - - -def get_management_confs_in_domain(body=None): # noqa: E501 - """get management configuration items and expected values in the domain - - get management configuration items and expected values in the domain # noqa: E501 - - :param body: domain info - :type body: dict | bytes - - :rtype: ConfFiles - """ - if connexion.request.is_json: - body = DomainName.from_dict(connexion.request.get_json()) # noqa: E501 - - # Check whether the domain exists - domain = body.domain_name - - # check the input domain - checkRes = Format.domainCheck(domain) - if not checkRes: - num = 400 - base_rsp = BaseResponse(num, "Failed to verify the input parameter, please check the input parameters.") - return base_rsp, num - - isExist = Format.isDomainExist(domain) - if not isExist: - base_rsp = BaseResponse(400, "The current domain does not exist") - return base_rsp, 400 - - # The parameters of the initial return value assignment - expected_conf_lists = ConfFiles(domain_name=domain, - conf_files=[]) - - # get the path in domain - domainPath = os.path.join(TARGETDIR, domain) - - # When there is a file path is the path of judgment for the configuration items - for root, dirs, files in os.walk(domainPath): - if len(files) > 0 and len(root.split('/')) > 3: - if "hostRecord.txt" in files: - continue - for d_file in files: - d_file_path = os.path.join(root, d_file) - contents = Format.get_file_content_by_read(d_file_path) - feature = os.path.join(root.split('/')[-1], d_file) - yang_modules = YangModule() - d_module = yang_modules.getModuleByFeature(feature) - file_lists = yang_modules.getFilePathInModdule(yang_modules.module_list) - file_path = file_lists.get(d_module.name()).split(":")[-1] - - conf = ConfFile(file_path=file_path, contents=contents) - expected_conf_lists.conf_files.append(conf) - LOGGER.debug("expected_conf_lists is :{}".format(expected_conf_lists)) - - if len(expected_conf_lists.domain_name) > 0: - base_rsp = BaseResponse(200, "Get management configuration items and expected " + - "values in the domain succeccfully") - else: - base_rsp = BaseResponse(400, "The file is Null in this domain") - - return expected_conf_lists - - -def query_changelog_of_management_confs_in_domain(body=None): # noqa: E501 - """query the change log of management config in domain - - query the change log of management config in domain # noqa: E501 - - :param body: domain info - :type body: dict | bytes - - :rtype: ExceptedConfInfo - """ - if connexion.request.is_json: - body = ManageConfs.from_dict(connexion.request.get_json()) # noqa: E501 - - # check whether the domain exists - domain = body.domain_name - LOGGER.debug("body is : {}".format(body)) - - # check the input domain - checkRes = Format.domainCheck(domain) - if not checkRes: - num = 400 - base_rsp = BaseResponse(num, "Failed to verify the input parameter, please check the input parameters.") - return base_rsp, num - - isExist = Format.isDomainExist(domain) - if not isExist: - base_rsp = BaseResponse(400, "The current domain does not exist") - return base_rsp - - # Check whether path is empty in advance. If path is empty, the configuration in the - # entire domain is queried. Otherwise, the historical records of the specified file are queried. - conf_files = body.conf_files - LOGGER.debug("conf_files is : {}".format(conf_files)) - LOGGER.debug("conf_files's type is : {}".format(type(conf_files))) - conf_files_list = [] - if conf_files: - for d_conf in conf_files: - LOGGER.debug("d_conf is : {}".format(d_conf)) - LOGGER.debug("d_conf type is : {}".format(type(d_conf))) - conf_files_list.append(d_conf.file_path) - success_conf = [] - failed_conf = [] - domain_path = os.path.join(TARGETDIR, domain) - expected_conf_lists = ExceptedConfInfo(domain_name=domain, - conf_base_infos=[]) - yang_modules = YangModule() - for root, dirs, files in os.walk(domain_path): - conf_base_infos = [] - if len(files) > 0 and len(root.split('/')) > 3: - if "hostRecord.txt" in files: - continue - confPath = root.split('/', 3)[3] - for d_file in files: - feature = os.path.join(root.split('/')[-1], d_file) - d_module = yang_modules.getModuleByFeature(feature) - file_lists = yang_modules.getFilePathInModdule(yang_modules.module_list) - file_path = file_lists.get(d_module.name()).split(":")[-1] - if (conf_files_list) and (file_path not in conf_files_list): - continue - d_file_path = os.path.join(root, d_file) - expectedValue = Format.get_file_content_by_read(d_file_path) - git_tools = GitTools() - gitMessage = git_tools.getLogMessageByPath(d_file_path) - if gitMessage and expectedValue: - success_conf.append(file_path) - else: - failed_conf.append(file_path) - conf_base_info = ConfBaseInfo(file_path=file_path, - expected_contents=expectedValue, - change_log=gitMessage) - expected_conf_lists.conf_base_infos.append(conf_base_info) - - LOGGER.debug("########################## expetedConfInfo ####################") - LOGGER.debug("expected_conf_lists is : {}".format(expected_conf_lists)) - LOGGER.debug("########################## expetedConfInfo end ####################") - - if len(success_conf) == 0: - codeNum = 500 - base_rsp = BaseResponse(codeNum, "Faled to uery the changelog of the configure in the domain.") - return base_rsp, codeNum - if len(failed_conf) > 0: - codeNum = 400 - else: - codeNum = 200 - - return expected_conf_lists, codeNum diff --git a/ragdoll/demo/conf_manage.py b/ragdoll/demo/conf_manage.py index d4f5d05..fb5fd78 100644 --- a/ragdoll/demo/conf_manage.py +++ b/ragdoll/demo/conf_manage.py @@ -2,7 +2,6 @@ import requests import json from ragdoll.log.log import LOGGER -from ragdoll.models.domain import Domain from ragdoll.models.domain_name import DomainName from ragdoll.models.conf import Conf from ragdoll.models.confs import Confs @@ -19,7 +18,7 @@ class ConfManage(object): contents_list = args.contents host_id_list = args.host_id if not domain_name or not file_path_list: - LOGGER.error("ERROR: Input error!\n") + LOGGER.error("ERROR: Input error for conf_add!\n") return conf_file = [] @@ -32,7 +31,7 @@ class ConfManage(object): conf = Conf(file_path=file_path_list[i], host_id=host_id_list[i]) conf_file.append(conf) else: - LOGGER.error("ERROR: Input error!\n") + LOGGER.error("ERROR: Input error as invlid param!\n") return data = Confs(domain_name=domain_name, conf_files=conf_file) @@ -46,7 +45,7 @@ class ConfManage(object): def conf_query(self, args): domain_name = args.domain_name if not domain_name: - LOGGER.error("ERROR: Input error!\n") + LOGGER.error("ERROR: Input error for conf_query!\n") return data = DomainName(domain_name=domain_name) @@ -64,7 +63,7 @@ class ConfManage(object): domain_name = args.domain_name file_path_list = args.file_path if not domain_name or not file_path_list: - LOGGER.error("ERROR: Input error!\n") + LOGGER.error("ERROR: Input error for conf_delete!\n") return conf_files = [] @@ -87,7 +86,7 @@ class ConfManage(object): domain_name = args.domain_name file_path_list = args.file_path if not domain_name or not file_path_list: - LOGGER.error("ERROR: Input error!\n") + LOGGER.error("ERROR: Input error for conf_changelog!\n") return conf_files = [] diff --git a/ragdoll/demo/conf_sync.py b/ragdoll/demo/conf_sync.py index 42cefdf..ec293b6 100644 --- a/ragdoll/demo/conf_sync.py +++ b/ragdoll/demo/conf_sync.py @@ -2,7 +2,6 @@ import requests import json from ragdoll.log.log import LOGGER -from ragdoll.models.domain import Domain from ragdoll.models.domain_name import DomainName from ragdoll.models.conf_host import ConfHost from ragdoll.demo.conf import server_port @@ -14,7 +13,7 @@ class ConfSync(object): domain_name = args.domain_name host_id_list = args.host_id if not domain_name or not host_id_list: - LOGGER.error("ERROR: Input error!\n") + LOGGER.error("ERROR: Input error for sync_conf!\n") return host_ids = [] @@ -34,7 +33,7 @@ class ConfSync(object): def sync_status(self, args): domain_name = args.domain_name if not domain_name: - LOGGER.error("ERROR: Input error!\n") + LOGGER.error("ERROR: Input error for sync_status!\n") return data = DomainName(domain_name=domain_name) @@ -52,7 +51,7 @@ class ConfSync(object): domain_name = args.domain_name host_id_list = args.host_id if not domain_name or not host_id_list: - LOGGER.error("ERROR: Input error!\n") + LOGGER.error("ERROR: Input error for query_real_conf!\n") return host_ids = [] diff --git a/ragdoll/demo/demo_server.py b/ragdoll/demo/demo_server.py index 9ac6c08..c20a7d9 100644 --- a/ragdoll/demo/demo_server.py +++ b/ragdoll/demo/demo_server.py @@ -1,6 +1,5 @@ import connexion -from ragdoll.models.conf_host import ConfHost from ragdoll.log.log import LOGGER def _read_file(path): @@ -88,4 +87,4 @@ def sync_conf(body=None): rsp = {"code": 500, "msg": "Failed to synchronize the configuration. ERROR:{}".format(error_info), "status": False} - return rsp \ No newline at end of file + return rsp diff --git a/ragdoll/demo/domain.py b/ragdoll/demo/domain.py index 6574fe2..5e65a72 100644 --- a/ragdoll/demo/domain.py +++ b/ragdoll/demo/domain.py @@ -12,7 +12,7 @@ class DomainManage(object): domain_name_list = args.domain_name priority_list = args.priority if len(domain_name_list) != len(priority_list): - LOGGER.error("ERROR: Input error!\n") + LOGGER.error("ERROR: Input error for domain_create!\n") return data = [] diff --git a/ragdoll/demo/host.py b/ragdoll/demo/host.py index 4f70980..9e4ab49 100644 --- a/ragdoll/demo/host.py +++ b/ragdoll/demo/host.py @@ -2,7 +2,6 @@ import requests import json from ragdoll.log.log import LOGGER -from ragdoll.models.domain import Domain from ragdoll.models.domain_name import DomainName from ragdoll.models.host import Host from ragdoll.models.host_infos import HostInfos @@ -17,7 +16,7 @@ class HostManage(object): ipv6_list = args.ipv6 if len(host_id_list) != len(ip_list): - LOGGER.error("ERROR: Input error!\n") + LOGGER.error("ERROR: Input error for adding host!\n") return host_infos = [] @@ -35,7 +34,7 @@ class HostManage(object): def host_query(self, args): domain_name = args.domain_name if not domain_name: - LOGGER.error("ERROR: Input error!\n") + LOGGER.error("ERROR: Input error for qeurying host!\n") return data = DomainName(domain_name=domain_name) @@ -44,7 +43,7 @@ class HostManage(object): response = requests.post(url, data=json.dumps(data, cls=JSONEncoder), headers=headers) if response.status_code != 200: - LOGGER.warning(json.loads(response.text).get("msg")) + LOGGER.error(json.loads(response.text).get("msg")) else: LOGGER.debug("The following host are managed in domain_name:{}.".format(json.loads(response.text))) return @@ -56,7 +55,7 @@ class HostManage(object): ipv6_list = args.ipv6 if len(host_id_list) != len(ip_list): - LOGGER.error("ERROR: Input error!\n") + LOGGER.error("ERROR: Input error for deleting host!\n") return host_infos = [] @@ -69,4 +68,4 @@ class HostManage(object): headers = {"Content-Type": "application/json"} response = requests.delete(url, data=json.dumps(data, cls=JSONEncoder), headers=headers) LOGGER.debug(json.loads(response.text).get("msg")) - return \ No newline at end of file + return diff --git a/ragdoll/domain_conf_manage/__init__.py b/ragdoll/domain_conf_manage/__init__.py new file mode 100644 index 0000000..0f38672 --- /dev/null +++ b/ragdoll/domain_conf_manage/__init__.py @@ -0,0 +1,18 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +@FileName: __init__.py.py +@Time: 2024/3/8 11:39 +@Author: JiaoSiMao +Description: +""" diff --git a/ragdoll/domain_conf_manage/view.py b/ragdoll/domain_conf_manage/view.py new file mode 100644 index 0000000..81015fb --- /dev/null +++ b/ragdoll/domain_conf_manage/view.py @@ -0,0 +1,601 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +@FileName: view.py +@Time: 2024/3/8 11:40 +@Author: JiaoSiMao +Description: +""" +import io +import json +import os + +import connexion +import requests +from flask import request +from vulcanus.restful.resp.state import PARAM_ERROR, SUCCEED, PARTIAL_SUCCEED, SERVER_ERROR +from vulcanus.restful.response import BaseResponse + +from ragdoll.conf.constant import TARGETDIR +from ragdoll.const.conf_handler_const import DIRECTORY_FILE_PATH_LIST +from ragdoll.function.verify.domain_conf import AddManagementConfsSchema, \ + DeleteManagementConfsSchema, GetManagementConfsSchema, QueryChangelogSchema +from ragdoll.log.log import LOGGER +from ragdoll.models import ConfFiles, ExceptedConfInfo +from ragdoll.utils.conf_tools import ConfTools +from ragdoll.utils.format import Format +from ragdoll.utils.git_tools import GitTools +from ragdoll.utils.object_parse import ObjectParse +from ragdoll.utils.yang_module import YangModule + + +class AddManagementConfsInDomain(BaseResponse): + @BaseResponse.handle(schema=AddManagementConfsSchema, token=True) + def post(self, **params): + """add management configuration items and expected values in the domain + + add management configuration items and expected values in the domain # noqa: E501 + + :param body: domain info + :type body: dict | bytes + + :rtype: BaseResponse + """ + global file_paths, reps + accessToken = request.headers.get("access_token") + domain = params.get("domainName") + conf_files = params.get("confFiles") + + # check the input domain + checkRes = Format.domainCheck(domain) + if not checkRes: + codeNum = PARAM_ERROR + return self.response(code=codeNum, message="Failed to verify the input parameter, please check the input " + "parameters.") + + # check whether the domain exists + isExist = Format.isDomainExist(domain) + if not isExist: + codeNum = PARAM_ERROR + return self.response(code=codeNum, message="The current domain does not exist, please create the domain " + "first.") + + # check whether the conf_files is null + if len(conf_files) == 0: + codeNum = PARAM_ERROR + return self.response(code=codeNum, message="The path of file can't be empty") + + # Check all conf_files and check whether contents is empty. If so, the query actual configuration + # interface is called. If not, the conversion is performed directly. + # Content and host_id can be set to either content or host_id. + # If they are both empty, invalid input is returned. + contents_list_null = [] + contents_list_non_null = [] + for d_conf in conf_files: + if "contents" in d_conf: + contents_list_non_null.append(d_conf) + elif d_conf["hostId"]: + contents_list_null.append(d_conf) + else: + codeNum = PARAM_ERROR + return self.response(code=codeNum, message="The input parameters are not compliant, please check the " + "input parameters.") + + successConf = [] + failedConf = [] + object_parse = ObjectParse() + yang_module = YangModule() + conf_tools = ConfTools() + # Content is not an empty scene and is directly analyed and parsed + if len(contents_list_non_null) > 0: + for d_conf in contents_list_non_null: + if not d_conf["contents"].strip(): + codeNum = PARAM_ERROR + return self.response(code=codeNum, message="The input parameters are not compliant, please check " + "the input parameters.") + content_string = object_parse.parse_conf_to_json(d_conf["filePath"], d_conf["contents"]) + if not content_string or not json.loads(content_string): + failedConf.append(d_conf["filePath"]) + else: + # create the file and expected value in domain + feature_path = yang_module.get_feature_by_real_path(domain, d_conf["filePath"]) + result = conf_tools.wirteFileInPath(feature_path, content_string + '\n') + if result: + successConf.append(d_conf["filePath"]) + else: + failedConf.append(d_conf["filePath"]) + + # content is empty + if len(contents_list_null) > 0: + # get the real conf in host + get_real_conf_body = {} + get_real_conf_body_info = [] + LOGGER.debug("contents_list_null is : {}".format(contents_list_null)) + exist_host = dict() + for d_conf in contents_list_null: + host_id = int(d_conf["hostId"]) + if host_id in exist_host: + if d_conf["filePath"] not in DIRECTORY_FILE_PATH_LIST: + exist_host[host_id].append(d_conf["filePath"]) + else: + codeNum, codeString, file_paths = object_parse.get_directory_files(d_conf, host_id, accessToken) + if len(file_paths) == 0: + return self.response(code=codeNum, message=codeString) + else: + for file_path in file_paths: + exist_host[host_id].append(file_path) + else: + if d_conf["filePath"] not in DIRECTORY_FILE_PATH_LIST: + conf_list = list() + conf_list.append(d_conf["filePath"]) + exist_host[host_id] = conf_list + else: + codeNum, codeString, file_paths = object_parse.get_directory_files(d_conf, host_id, accessToken) + if len(file_paths) == 0: + return self.response(code=codeNum, message=codeString) + else: + exist_host[host_id] = file_paths + + for k, v in exist_host.items(): + confs = dict() + confs["host_id"] = k + confs["config_list"] = v + get_real_conf_body_info.append(confs) + + get_real_conf_body["infos"] = get_real_conf_body_info + + url = conf_tools.load_url_by_conf().get("collect_url") + headers = {"Content-Type": "application/json", "access_token": accessToken} + try: + response = requests.post(url, data=json.dumps(get_real_conf_body), headers=headers) # post request + except requests.exceptions.RequestException as connect_ex: + LOGGER.error(f"An error occurred: {connect_ex}") + codeNum = PARAM_ERROR + codeString = "Failed to obtain the actual configuration, please check the interface of config/collect." + return self.response(code=codeNum, message=codeString) + + response_code = json.loads(response.text).get("code") + if response_code == None: + codeNum = PARAM_ERROR + codeString = "Failed to obtain the actual configuration, please check the interface of conf/collect." + return self.response(code=codeNum, message=codeString) + + if (response_code != "200") and (response_code != "206"): + codeNum = PARAM_ERROR + codeString = "Failed to obtain the actual configuration, please check the file exists." + return self.response(code=codeNum, message=codeString) + + reps = json.loads(response.text).get("data") + if not reps or len(reps) == 0: + codeNum = PARAM_ERROR + codeString = "Failed to obtain the actual configuration, please check the host info for conf/collect." + return self.response(code=codeNum, message=codeString) + + directory_d_file = [] + directory_d_files = {} + for d_res in reps: + failedlist = d_res.get("fail_files") + if len(failedlist) > 0: + for d_failed in failedlist: + failedConf.append(d_failed) + continue + d_res_infos = d_res.get("infos") + for d_file in d_res_infos: + for dir_path in DIRECTORY_FILE_PATH_LIST: + if str(d_file.get("path")).find(dir_path) == -1: + file_path = d_file.get("path") + content = d_file.get("content") + content_string = object_parse.parse_conf_to_json(file_path, content) + # create the file and expected value in domain + if not content_string or not json.loads(content_string): + failedConf.append(file_path) + else: + feature_path = yang_module.get_feature_by_real_path(domain, file_path) + result = conf_tools.wirteFileInPath(feature_path, content_string + '\n') + if result: + successConf.append(file_path) + else: + failedConf.append(file_path) + else: + directory_d_file.append(d_file) + directory_d_files[dir_path] = directory_d_file + if len(directory_d_files) > 0: + for dir_path, directory_d_file in directory_d_files.items(): + content_string = object_parse.parse_dir_conf_to_json(dir_path, directory_d_file) + if not content_string or not json.loads(content_string): + failedConf.append(dir_path) + else: + feature_path = yang_module.get_feature_by_real_path(domain, dir_path) + result = conf_tools.wirteFileInPath(feature_path, content_string + '\n') + if result: + successConf.append(dir_path) + else: + failedConf.append(dir_path) + # git commit message + if len(successConf) > 0: + git_tools = GitTools() + succ_conf = "" + for d_conf in successConf: + succ_conf = succ_conf + d_conf + " " + commit_code = git_tools.gitCommit("Add the conf in {} domian, ".format(domain) + + "the path including : {}".format(succ_conf)) + + # Joinin together the returned codenum and codeMessage + LOGGER.debug("successConf is : {}".format(successConf)) + LOGGER.debug("failedConf is : {}".format(failedConf)) + if len(successConf) == 0: + codeNum = PARAM_ERROR + codeString = "All configurations failed to be added." + elif len(failedConf) > 0: + codeNum = PARTIAL_SUCCEED + codeString = Format.splicErrorString("confs", "add management conf", successConf, failedConf) + else: + codeNum = SUCCEED + codeString = Format.spliceAllSuccString("confs", "add management conf", successConf) + + # 根据agith_success_conf 更新agith的配置 + # 获取domain最新的有哪些配置 [1] + conf_files_list = Format.get_conf_files_list(domain, accessToken) + if len(conf_files_list) > 0: + Format.update_agith(accessToken, conf_files_list, domain) + return self.response(code=codeNum, message=codeString) + + +class UploadManagementConfsInDomain(BaseResponse): + @BaseResponse.handle(token=True) + def post(self, **params): + """upload management configuration items and expected values in the domain + + upload management configuration items and expected values in the domain # noqa: E501 + + :param body: file info + :type body: FileStorage + + :rtype: BaseResponse + """ + access_token = connexion.request.headers.get("access_token") + file = connexion.request.files['file'] + + filePath = connexion.request.form.get("filePath") + domainName = connexion.request.form.get("domainName") + # check the input domainName + checkRes = Format.domainCheck(domainName) + if not checkRes: + codeNum = PARAM_ERROR + return self.response(code=codeNum, message="Failed to verify the input parameter, please check the input " + "parameters.") + + # check whether the domainName exists + isExist = Format.isDomainExist(domainName) + if not isExist: + codeNum = PARAM_ERROR + return self.response(code=codeNum, message="The current domain does not exist, please create the domain " + "first.") + + # check whether the file is null + if file is None: + codeNum = PARAM_ERROR + return self.response(code=codeNum, message="The file of conf can't be empty") + + # check whether the conf is null + if filePath is None: + codeNum = PARAM_ERROR + return self.response(code=codeNum, message="The conf body of conf can't be empty") + + successConf = [] + failedConf = [] + object_parse = ObjectParse() + yang_module = YangModule() + conf_tools = ConfTools() + + # content is file + if file: + if not filePath.strip(): + codeNum = PARAM_ERROR + return self.response(code=codeNum, message="The input parameters are not compliant, please check the " + "input parameters.") + try: + file_bytes = file.read() + if len(file_bytes) > 1024 * 1024: + codeNum = PARAM_ERROR + return self.response(code=codeNum, message="The size of the uploaded file must be less than 1MB") + byte_stream = io.BytesIO(file_bytes) + + # Read the contents of the byte stream + line_content = byte_stream.read().decode("UTF-8") + except OSError as err: + LOGGER.error("OS error: {}".format(err)) + codeNum = SERVER_ERROR + return self.response(code=codeNum, message="OS error: {0}".format(err)) + except Exception as ex: + LOGGER.error("Other error: {}".format(ex)) + codeNum = SERVER_ERROR + return self.response(code=codeNum, message="read file error: {0}".format(ex)) + + content_string = object_parse.parse_conf_to_json(filePath, line_content) + if not content_string or not json.loads(content_string): + failedConf.append(filePath) + else: + # create the file and expected value in domain + feature_path = yang_module.get_feature_by_real_path(domainName, filePath) + result = conf_tools.wirteFileInPath(feature_path, content_string + '\n') + if result: + successConf.append(filePath) + else: + failedConf.append(filePath) + + # git commit message + if len(successConf) > 0: + git_tools = GitTools() + succ_conf = "" + for d_conf in successConf: + succ_conf = succ_conf + d_conf + " " + commit_code = git_tools.gitCommit("Add the conf in {} domian, ".format(domainName) + + "the path including : {}".format(succ_conf)) + + # Joinin together the returned codenum and codeMessage + LOGGER.debug("successConf is : {}".format(successConf)) + LOGGER.debug("failedConf is : {}".format(failedConf)) + if len(successConf) == 0: + codeNum = PARAM_ERROR + codeString = "All configurations failed to be added." + elif len(failedConf) > 0: + codeNum = PARTIAL_SUCCEED + codeString = Format.splicErrorString("confs", "add management conf", successConf, failedConf) + else: + codeNum = SUCCEED + codeString = Format.spliceAllSuccString("confs", "add management conf", successConf) + + # 获取domain最新的有哪些配置 [1] + conf_files_list = Format.get_conf_files_list(domainName, access_token) + if len(conf_files_list) > 0: + Format.update_agith(access_token, conf_files_list, domainName) + + return self.response(code=codeNum, message=codeString) + + +class DeleteManagementConfsInDomain(BaseResponse): + @BaseResponse.handle(schema=DeleteManagementConfsSchema, token=True) + def delete(self, **params): + """delete management configuration items and expected values in the domain + + delete management configuration items and expected values in the domain # noqa: E501 + + :param body: domain info + :type body: dict | bytes + + :rtype: BaseResponse + """ + access_token = connexion.request.headers.get("access_token") + # check whether the domain exists + domain = params.get("domainName") + + # check the input domain + checkRes = Format.domainCheck(domain) + if not checkRes: + codeNum = PARAM_ERROR + return self.response(code=codeNum, message="Failed to verify the input parameter, please check the input " + "parameters.") + + isExist = Format.isDomainExist(domain) + if not isExist: + codeNum = PARAM_ERROR + return self.response(code=codeNum, message="The current domain does not exist") + + # Check whether path is null in advance + conf_files = params.get("confFiles") + if len(conf_files) == 0: + codeNum = PARAM_ERROR + return self.response(code=codeNum, message="The conf_files path can't be empty") + + # Conf to record successes and failures + successConf = [] + failedConf = [] + + # Check whether path exists in the domain. There are two possible paths : + # (1)xpath path + # (2) configuration item + domain_path = os.path.join(TARGETDIR, domain) + LOGGER.debug("conf_files is : {}".format(conf_files)) + + yang_modules = YangModule() + module_lists = yang_modules.module_list + if len(module_lists) == 0: + codeNum = PARAM_ERROR + return self.response(code=codeNum, message="The yang module does not exist") + + LOGGER.debug("module_lists is : {}".format(module_lists)) + for conf in conf_files: + module = yang_modules.getModuleByFilePath(conf["filePath"]) + features = yang_modules.getFeatureInModule(module) + features_path = os.path.join(domain_path, "/".join(features)) + + if os.path.isfile(features_path): + LOGGER.info("It's a normal file : {}".format(features_path)) + try: + os.remove(features_path) + except OSError as ex: + LOGGER.error("Failed to remove path, as OSError: {}".format(str(ex))) + break + successConf.append(conf["filePath"]) + else: + LOGGER.error("It's a not normal file : {}".format(features_path)) + failedConf.append(conf["filePath"]) + + # git commit message + if len(successConf) > 0: + git_tools = GitTools() + succ_conf = "" + for d_conf in successConf: + succ_conf = succ_conf + d_conf + " " + commit_code = git_tools.gitCommit("delete the conf in {} domian, ".format(domain) + + "the path including : {}".format(succ_conf)) + + # Joinin together the returned codenum and codeMessage + if len(failedConf) == 0: + codeNum = SUCCEED + codeString = Format.spliceAllSuccString("confs", "delete management conf", successConf) + else: + codeNum = PARAM_ERROR + codeString = Format.splicErrorString("confs", "delete management conf", successConf, failedConf) + codeString += "\n The reason for the failure is: these paths do not exist." + + # 获取domain最新的有哪些配置 [1] + conf_files_list = Format.get_conf_files_list(domain, access_token) + Format.update_agith(access_token, conf_files_list, domain) + + return self.response(code=codeNum, message=codeString) + + +class GetManagementConfsInDomain(BaseResponse): + @BaseResponse.handle(schema=GetManagementConfsSchema, token=True) + def post(self, **params): + """get management configuration items and expected values in the domain + + get management configuration items and expected values in the domain # noqa: E501 + + :param body: domain info + :type body: dict | bytes + + :rtype: ConfFiles + """ + # Check whether the domain exists + domain = params.get("domainName") + + # check the input domain + checkRes = Format.domainCheck(domain) + if not checkRes: + codeNum = PARAM_ERROR + return self.response(code=codeNum, message="Failed to verify the input parameter, please check the input " + "parameters.") + + isExist = Format.isDomainExist(domain) + if not isExist: + codeNum = PARAM_ERROR + return self.response(code=codeNum, message="The current domain does not exist") + + # The parameters of the initial return value assignment + # expected_conf_lists = ConfFiles(domain_name=domain, conf_files=[]) + expected_conf_lists = {"domainName": domain, "confFiles": []} + + # get the path in domain + domainPath = os.path.join(TARGETDIR, domain) + + # When there is a file path is the path of judgment for the configuration items + for root, dirs, files in os.walk(domainPath): + if len(files) > 0 and len(root.split('/')) > 3: + if "hostRecord.txt" in files: + continue + for d_file in files: + d_file_path = os.path.join(root, d_file) + contents = Format.get_file_content_by_read(d_file_path) + feature = os.path.join(root.split('/')[-1], d_file) + yang_modules = YangModule() + d_module = yang_modules.getModuleByFeature(feature) + file_lists = yang_modules.getFilePathInModdule(yang_modules.module_list) + file_path = file_lists.get(d_module.name()).split(":")[-1] + + # conf = ConfFile(file_path=file_path, contents=contents) + conf = {"filePath": file_path, "contents": contents} + # expected_conf_lists.conf_files.append(conf) + expected_conf_lists.get("confFiles").append(conf) + LOGGER.debug("expected_conf_lists is :{}".format(expected_conf_lists)) + + if len(expected_conf_lists.get("domainName")) > 0: + codeNum = SUCCEED + codeString = "Get management configuration items and expected values in the domain successfully" + return self.response(code=codeNum, message=codeString, data=expected_conf_lists) + + else: + codeNum = PARAM_ERROR + return self.response(code=codeNum, message="The file is Null in this domain") + + +class QueryChangelogOfManagementConfsInDomain(BaseResponse): + @BaseResponse.handle(schema=QueryChangelogSchema, token=True) + def post(self, **params): + """query the change log of management config in domain + + query the change log of management config in domain # noqa: E501 + + :param body: domain info + :type body: dict | bytes + + :rtype: ExceptedConfInfo + """ + # check whether the domain exists + domain = params.get("domainName") + LOGGER.debug("Query changelog of conf body is : {}".format(params)) + + # check the input domain + checkRes = Format.domainCheck(domain) + if not checkRes: + codeNum = PARAM_ERROR + return self.response(code=codeNum, message="Failed to verify the input parameter, please check the input " + "parameters.") + + isExist = Format.isDomainExist(domain) + if not isExist: + codeNum = PARAM_ERROR + return self.response(code=codeNum, message="The current domain does not exist") + + # Check whether path is empty in advance. If path is empty, the configuration in the + # entire domain is queried. Otherwise, the historical records of the specified file are queried. + conf_files = params.get("confFiles") + LOGGER.debug("conf_files is : {}".format(conf_files)) + conf_files_list = [] + if conf_files: + for d_conf in conf_files: + LOGGER.debug("d_conf is : {}".format(d_conf)) + conf_files_list.append(d_conf["filePath"]) + success_conf = [] + failed_conf = [] + domain_path = os.path.join(TARGETDIR, domain) + # expected_conf_lists = ExceptedConfInfo(domain_name=domain, conf_base_infos=[]) + expected_conf_lists = {"domainName": domain, "confBaseInfos": []} + yang_modules = YangModule() + for root, dirs, files in os.walk(domain_path): + if len(files) > 0 and len(root.split('/')) > 3: + if "hostRecord.txt" in files: + continue + for d_file in files: + feature = os.path.join(root.split('/')[-1], d_file) + d_module = yang_modules.getModuleByFeature(feature) + file_lists = yang_modules.getFilePathInModdule(yang_modules.module_list) + file_path = file_lists.get(d_module.name()).split(":")[-1] + if conf_files_list and (file_path not in conf_files_list): + continue + d_file_path = os.path.join(root, d_file) + expectedValue = Format.get_file_content_by_read(d_file_path) + git_tools = GitTools() + gitMessage = git_tools.getLogMessageByPath(d_file_path) + if gitMessage and expectedValue: + success_conf.append(file_path) + else: + failed_conf.append(file_path) + + conf_base_info = {"filePath": file_path, "expectedContents": expectedValue, "changeLog": gitMessage} + expected_conf_lists.get("confBaseInfos").append(conf_base_info) + + LOGGER.debug("expected_conf_lists is : {}".format(expected_conf_lists)) + + if len(success_conf) == 0: + codeNum = SERVER_ERROR + return self.response(code=codeNum, message="Failed to query the changelog of the configure in the domain.") + if len(failed_conf) > 0: + codeNum = PARAM_ERROR + else: + codeNum = SUCCEED + + return self.response(code=codeNum, message="Succeed to query the changelog of the configure in the domain.", + data=expected_conf_lists) diff --git a/ragdoll/domain_manage/__init__.py b/ragdoll/domain_manage/__init__.py new file mode 100644 index 0000000..f0935bb --- /dev/null +++ b/ragdoll/domain_manage/__init__.py @@ -0,0 +1,18 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +@FileName: __init__.py.py +@Time: 2024/3/4 10:34 +@Author: JiaoSiMao +Description: +""" diff --git a/ragdoll/domain_manage/view.py b/ragdoll/domain_manage/view.py new file mode 100644 index 0000000..dd507cb --- /dev/null +++ b/ragdoll/domain_manage/view.py @@ -0,0 +1,135 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +@FileName: view.py +@Time: 2024/3/4 10:34 +@Author: JiaoSiMao +Description: +""" +import os +import shutil + +import connexion +from vulcanus.restful.resp.state import SUCCEED, SERVER_ERROR, PARAM_ERROR +from vulcanus.restful.response import BaseResponse + +from ragdoll.conf.constant import TARGETDIR +from ragdoll.function.verify.domain import CreateDomainSchema, DeleteDomainSchema +from ragdoll.utils.format import Format +from ragdoll.utils.git_tools import GitTools + + +class CreateDomain(BaseResponse): + + @BaseResponse.handle(schema=CreateDomainSchema, token=True) + def post(self, **params): + """ + create domain + + Args: + args (dict): e.g + { + "domainName":"xxx", + "priority":"" + } + + Returns: + dict: response body + """ + successDomain = [] + failedDomain = [] + tempDomainName = params.get("domainName") + checkRes = Format.domainCheck(tempDomainName) + isExist = Format.isDomainExist(tempDomainName) + + if isExist or not checkRes: + failedDomain.append(tempDomainName) + else: + successDomain.append(tempDomainName) + domainPath = os.path.join(TARGETDIR, tempDomainName) + os.umask(0o077) + os.mkdir(domainPath) + + codeString = "" + if len(failedDomain) == 0: + codeNum = SUCCEED + codeString = Format.spliceAllSuccString("domain", "created", successDomain) + else: + codeNum = SERVER_ERROR + if params: + if isExist: + codeString = "domain {} create failed because it has been existed.".format(failedDomain[0]) + elif not checkRes: + codeString = "domain {} create failed because format is incorrect.".format(failedDomain[0]) + else: + codeString = Format.splicErrorString("domain", "created", successDomain, failedDomain) + + # 对successDomain成功的domain添加文件监控开关、告警开关 + Format.add_domain_conf_trace_flag(params, successDomain, tempDomainName) + + return self.response(code=codeNum, message=codeString) + + +class DeleteDomain(BaseResponse): + + @BaseResponse.handle(schema=DeleteDomainSchema, token=True) + def delete(self, **params): + access_token = connexion.request.headers.get("access_token") + domainName = params.get("domainName") + + if not domainName: + codeString = "The entered domain is empty" + return self.response(code=PARAM_ERROR, message=codeString) + # 1.清理agith + # 获取domain下的host ids + host_ids = Format.get_hostid_list_by_domain(domainName) + if len(host_ids) > 0: + Format.uninstall_trace(access_token, host_ids, domainName) + successDomain = [] + failedDomain = [] + + checkRes = Format.domainCheck(domainName) + isExist = Format.isDomainExist(domainName) + if checkRes and isExist: + domainPath = os.path.join(TARGETDIR, domainName) + successDomain.append(domainName) + shutil.rmtree(domainPath) + else: + failedDomain.append(domainName) + + if len(failedDomain) == 0: + codeNum = SUCCEED + codeString = Format.spliceAllSuccString("domain", "delete", successDomain) + else: + codeNum = SERVER_ERROR + codeString = Format.splicErrorString("domain", "delete", successDomain, failedDomain) + + # 删除业务域,对successDomain成功的业务域进行redis的key值清理,以及domain下的主机进行agith的清理 + Format.clear_all_domain_data(access_token, domainName, successDomain, host_ids) + + return self.response(code=codeNum, message=codeString) + + +class QueryDomain(BaseResponse): + @BaseResponse.handle(token=True) + def post(self, **params): + domain_list = [] + cmd = "ls {}".format(TARGETDIR) + gitTools = GitTools() + ls_res = gitTools.run_shell_return_output(cmd).decode() + ll_list = ls_res.split('\n') + for d_ll in ll_list: + if d_ll: + domain = {"domainName": d_ll} + domain_list.append(domain) + return self.response(code=SUCCEED, data=domain_list) diff --git a/ragdoll/function/__init__.py b/ragdoll/function/__init__.py new file mode 100644 index 0000000..1a316c9 --- /dev/null +++ b/ragdoll/function/__init__.py @@ -0,0 +1,18 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +@FileName: __init__.py.py +@Time: 2024/3/4 10:48 +@Author: JiaoSiMao +Description: +""" diff --git a/ragdoll/function/verify/__init__.py b/ragdoll/function/verify/__init__.py new file mode 100644 index 0000000..1a316c9 --- /dev/null +++ b/ragdoll/function/verify/__init__.py @@ -0,0 +1,18 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +@FileName: __init__.py.py +@Time: 2024/3/4 10:48 +@Author: JiaoSiMao +Description: +""" diff --git a/ragdoll/function/verify/confs.py b/ragdoll/function/verify/confs.py new file mode 100644 index 0000000..654ede4 --- /dev/null +++ b/ragdoll/function/verify/confs.py @@ -0,0 +1,82 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +@FileName: confs.py +@Time: 2024/3/11 9:02 +@Author: JiaoSiMao +Description: +""" +from marshmallow import Schema, fields + + +class DomainNameSchema(Schema): + domainName = fields.String(required=True, validate=lambda s: len(s) > 0) + + +class HostIdSchema(Schema): + hostId = fields.Integer(required=True, validate=lambda s: s >= 0) + + +class SyncHostConfsSchema(Schema): + hostId = fields.Integer(required=True, validate=lambda s: s >= 0) + syncConfigs = fields.List(fields.String(required=True, validate=lambda s: len(s) > 0), required=True, + validate=lambda s: len(s) > 0) + + +class ConfBaseSchema(Schema): + filePath = fields.String(required=True, validate=lambda s: len(s) > 0) + expectedContents = fields.String(required=True, validate=lambda s: len(s) > 0) + + +class DomainConfBaseInfosSchema(Schema): + domainName = fields.String(required=True, validate=lambda s: len(s) > 0) + confBaseInfos = fields.List(fields.Nested(ConfBaseSchema(), required=True), required=True, + validate=lambda s: len(s) > 0) + + +class GetSyncStatusSchema(Schema): + domainName = fields.String(required=True, validate=lambda s: len(s) > 0) + ip = fields.String(required=True, validate=lambda s: len(s) > 0) + + +class QueryExceptedConfsSchema(Schema): + domainNames = fields.List(fields.Nested(DomainNameSchema(), required=True), required=True, + validate=lambda s: len(s) > 0) + + +class QueryRealConfsSchema(Schema): + domainName = fields.String(required=True, validate=lambda s: len(s) > 0) + hostIds = fields.List(fields.Nested(HostIdSchema(), required=True), required=True, + validate=lambda s: len(s) > 0) + + +class SyncConfToHostFromDomainSchema(Schema): + domainName = fields.String(required=True, validate=lambda s: len(s) > 0) + syncList = fields.List(fields.Nested(SyncHostConfsSchema(), required=True), required=True, + validate=lambda s: len(s) > 0) + + +class QuerySupportedConfsSchema(Schema): + domainName = fields.String(required=True, validate=lambda s: len(s) > 0) + + +class CompareConfDiffSchema(Schema): + expectedConfsResp = fields.List(fields.Nested(DomainConfBaseInfosSchema(), required=True), required=True, + validate=lambda s: len(s) > 0) + domainResult = fields.Dict(required=True, validate=lambda s: len(s) > 0) + + +class BatchSyncConfToHostFromDomainSchema(Schema): + domainName = fields.String(required=True, validate=lambda s: len(s) > 0) + hostIds = fields.List(fields.Integer(required=True, validate=lambda s: s >= 0), required=True, + validate=lambda s: len(s) > 0) diff --git a/ragdoll/function/verify/domain.py b/ragdoll/function/verify/domain.py new file mode 100644 index 0000000..c4c1323 --- /dev/null +++ b/ragdoll/function/verify/domain.py @@ -0,0 +1,36 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (C) 2023 isoftstone Technologies Co., Ltd. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +@FileName: domain.py +@Time: 2024/3/4 10:48 +@Author: JiaoSiMao +Description: +""" +from marshmallow import Schema, fields, validate + + +class CreateDomainSchema(Schema): + """ + validators for parameter of /domain/createDomain + """ + domainName = fields.String(required=True, validate=lambda s: len(s) > 0) + priority = fields.Integer(required=True, validate=lambda s: s >= 0) + conf_change_flag = fields.Boolean(required=True, default=False, validate=validate.OneOf([True, False])) + report_flag = fields.Boolean(required=True, default=False, validate=validate.OneOf([True, False])) + + +class DeleteDomainSchema(Schema): + """ + validators for parameter of /domain/deleteDomain + """ + domainName = fields.String(required=True, validate=lambda s: len(s) > 0) diff --git a/ragdoll/function/verify/domain_conf.py b/ragdoll/function/verify/domain_conf.py new file mode 100644 index 0000000..698772b --- /dev/null +++ b/ragdoll/function/verify/domain_conf.py @@ -0,0 +1,53 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +@FileName: domain_conf.py +@Time: 2024/3/8 11:40 +@Author: JiaoSiMao +Description: +""" +from marshmallow import Schema, fields + + +class ConfSchema(Schema): + filePath = fields.String(required=False, validate=lambda s: len(s) > 0) + contents = fields.String(required=False, validate=lambda s: len(s) > 0) + hostId = fields.Integer(required=False, validate=lambda s: s >= 0) + + +class ManageConfSchema(Schema): + filePath = fields.String(required=False, validate=lambda s: len(s) > 0) + + +class AddManagementConfsSchema(Schema): + domainName = fields.String(required=True, validate=lambda s: len(s) > 0) + confFiles = fields.List(fields.Nested(ConfSchema(), required=True), required=True, validate=lambda s: len(s) > 0) + + +class UploadManagementConfsSchema(Schema): + filePath = fields.String(required=False, validate=lambda s: len(s) > 0) + domainName = fields.String(required=False, validate=lambda s: len(s) > 0) + + +class DeleteManagementConfsSchema(Schema): + domainName = fields.String(required=False, validate=lambda s: len(s) > 0) + confFiles = fields.List(fields.Nested(ManageConfSchema(), required=True), required=True, validate=lambda s: len(s) > 0) + + +class GetManagementConfsSchema(Schema): + domainName = fields.String(required=False, validate=lambda s: len(s) > 0) + + +class QueryChangelogSchema(Schema): + domainName = fields.String(required=False, validate=lambda s: len(s) > 0) + confFiles = fields.List(fields.Nested(ManageConfSchema(), required=True), required=True, validate=lambda s: len(s) > 0) diff --git a/ragdoll/function/verify/host.py b/ragdoll/function/verify/host.py new file mode 100644 index 0000000..1ea927c --- /dev/null +++ b/ragdoll/function/verify/host.py @@ -0,0 +1,58 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +@FileName: host.py +@Time: 2024/3/5 9:44 +@Author: JiaoSiMao +Description: +""" +from marshmallow import Schema, fields + + +class HostSchema(Schema): + """ + validators for parameter of host + """ + hostId = fields.Integer(required=True, validate=lambda s: s >= 0) + ip = fields.String(required=True, validate=lambda s: len(s) > 0) + ipv6 = fields.String(required=True, validate=lambda s: len(s) > 0) + + +class SingleDeleteHostSchema(Schema): + """ + validators for parameter of /host/deleteHost + """ + hostId = fields.Integer(required=True, validate=lambda s: s >= 0) + + +class AddHostSchema(Schema): + """ + validators for parameter of /host/addHost + """ + domainName = fields.String(required=True, validate=lambda s: len(s) > 0) + hostInfos = fields.List(fields.Nested(HostSchema(), required=True), required=True, validate=lambda s: len(s) > 0) + + +class DeleteHostSchema(Schema): + """ + validators for parameter of /host/deleteHost + """ + domainName = fields.String(required=True, validate=lambda s: len(s) > 0) + hostInfos = fields.List(fields.Nested(SingleDeleteHostSchema(), required=True), required=True, validate=lambda s: len(s) > 0) + + +class GetHostSchema(Schema): + """ + validators for parameter of /host/getHost + """ + domainName = fields.String(required=True, validate=lambda s: len(s) > 0) \ No newline at end of file diff --git a/ragdoll/host_manage/__init__.py b/ragdoll/host_manage/__init__.py new file mode 100644 index 0000000..ec92ca5 --- /dev/null +++ b/ragdoll/host_manage/__init__.py @@ -0,0 +1,18 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +@FileName: __init__.py.py +@Time: 2024/3/5 9:39 +@Author: JiaoSiMao +Description: +""" diff --git a/ragdoll/host_manage/view.py b/ragdoll/host_manage/view.py new file mode 100644 index 0000000..ead42f7 --- /dev/null +++ b/ragdoll/host_manage/view.py @@ -0,0 +1,292 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +@FileName: view.py +@Time: 2024/3/5 9:40 +@Author: JiaoSiMao +Description: +""" +import ast +import json +import os + +import connexion +from vulcanus.restful.resp.state import PARAM_ERROR, SUCCEED, PARTIAL_SUCCEED, SERVER_ERROR +from vulcanus.restful.response import BaseResponse + +from ragdoll.conf.constant import TARGETDIR +from ragdoll.function.verify.host import AddHostSchema, DeleteHostSchema, GetHostSchema +from ragdoll.log.log import LOGGER +from ragdoll.utils.conf_tools import ConfTools +from ragdoll.utils.format import Format +from ragdoll.utils.git_tools import GitTools + + +class AddHostInDomain(BaseResponse): + @BaseResponse.handle(schema=AddHostSchema, token=True) + def post(self, **params): + """ + add host in the configuration domain + + add host in the configuration domain # noqa: E501 + + :param body: domain info + :type body: dict | bytes + + :rtype: BaseResponse + """ + access_token = connexion.request.headers.get("access_token") + domain = params.get("domainName") + host_infos = params.get("hostInfos") + + # check whether host_infos is empty + if len(host_infos) == 0: + codeNum = PARAM_ERROR + return self.response(code=codeNum, message="Enter host info cannot be empty, please check the host info.") + + checkRes = Format.domainCheck(domain) + if not checkRes: + codeNum = PARAM_ERROR + return self.response(code=codeNum, + message="Failed to verify the input parameter, please check the input parameters.") + + # check whether the domain exists + isExist = Format.isDomainExist(domain) + if not isExist: + codeNum = PARAM_ERROR + return self.response(code=codeNum, + message="The current domain does not exist, please create the domain first.") + + successHost = [] + failedHost = [] + domainPath = os.path.join(TARGETDIR, domain) + # 将domainName 和host信息入库 + conf_tools = ConfTools() + Format.addHostSyncStatus(conf_tools, domain, host_infos) + + # Check whether the current host exists in the domain. + for host in host_infos: + # 判断这个hostId是否在其他业务域中 + contained_flag = Format.isContainedHostIdInOtherDomain(host.get("hostId")) + if contained_flag: + failedHost.append(host.get("hostId")) + else: + hostPath = os.path.join(domainPath, "hostRecord.txt") + if os.path.isfile(hostPath): + isContained = Format.isContainedHostIdInfile(hostPath, host.get("hostId")) + if isContained: + failedHost.append(host.get("hostId")) + else: + Format.addHostToFile(hostPath, host) + successHost.append(host.get("hostId")) + else: + Format.addHostToFile(hostPath, host) + successHost.append(host.get("hostId")) + + if len(failedHost) == len(host_infos): + codeNum = PARAM_ERROR + return self.response(code=codeNum, + message="The all host already exists in the administrative scope of the domain.") + + # Joining together the returned codenum codeMessage + if len(failedHost) == 0: + codeNum = SUCCEED + codeString = Format.spliceAllSuccString("host", "add hosts", successHost) + else: + codeNum = PARTIAL_SUCCEED + codeString = Format.splicErrorString("host", "add hosts", successHost, failedHost) + + # git commit maessage + if len(host_infos) > 0: + git_tools = GitTools() + commit_code = git_tools.gitCommit("Add the host in {} domian, ".format(domain) + + "the host including : {}".format(successHost)) + + # 针对successHost 添加成功的host, 安装agith并启动agith,如果当前业务域有配置,配置agith,如果没有就不配置 + Format.install_update_agith(access_token, domain, successHost) + + return self.response(code=codeNum, message=codeString) + + +class DeleteHostInDomain(BaseResponse): + @BaseResponse.handle(schema=DeleteHostSchema, token=True) + def delete(self, **params): + """delete host in the configuration domain + + delete the host in the configuration domain # noqa: E501 + + :param body: domain info + :type body: dict | bytes + + :rtype: BaseResponse + """ + access_token = connexion.request.headers.get("access_token") + domain = params.get("domainName") + hostInfos = params.get("hostInfos") + + # check the input domain + checkRes = Format.domainCheck(domain) + if not checkRes: + codeNum = PARAM_ERROR + return self.response(code=codeNum, + message="Failed to verify the input parameter, please check the input parameters.") + + # check whether the domain exists + isExist = Format.isDomainExist(domain) + if not isExist: + codeNum = PARAM_ERROR + return self.response(code=codeNum, + message="The current domain does not exist, please create the domain first.") + + # 将host sync status从库中删除 + conf_tools = ConfTools() + Format.deleteHostSyncStatus(conf_tools, domain, hostInfos) + + # Whether the host information added within the current domain is empty while ain exists + domainPath = os.path.join(TARGETDIR, domain) + hostPath = os.path.join(domainPath, "hostRecord.txt") + if not os.path.isfile(hostPath) or (os.path.isfile(hostPath) and os.stat(hostPath).st_size == 0): + codeNum = PARAM_ERROR + return self.response(code=codeNum, + message="The host information is not set in the current domain. Please add the host " + "information first") + + # If the input host information is empty, the host information of the whole domain is cleared + if len(hostInfos) == 0: + if os.path.isfile(hostPath): + try: + os.remove(hostPath) + except OSError as ex: + LOGGER.error("Failed to delete hostpath as OS error: {0}".format(ex)) + codeNum = SERVER_ERROR + return self.response(code=codeNum, message="The host delete failed.") + codeNum = SUCCEED + return self.response(code=codeNum, message="All hosts are deleted in the current domain.") + + # If the domain exists, check whether the current input parameter host belongs to the corresponding + # domain. If the host is in the domain, the host is deleted. If the host is no longer in the domain, + # the host is added to the failure range + containedInHost = [] + notContainedInHost = [] + os.umask(0o077) + for hostInfo in hostInfos: + hostId = hostInfo.get("hostId") + isContained = False + try: + with open(hostPath, 'r') as d_file: + lines = d_file.readlines() + with open(hostPath, 'w') as w_file: + for line in lines: + line_host_id = json.loads(str(ast.literal_eval(line)).replace("'", "\""))['host_id'] + if hostId != line_host_id: + w_file.write(line) + else: + isContained = True + except OSError as err: + LOGGER.error("OS error: {0}".format(err)) + codeNum = SERVER_ERROR + return self.response(code=codeNum, message="OS error: {0}".format(err)) + + if isContained: + containedInHost.append(hostId) + else: + notContainedInHost.append(hostId) + + # All hosts do not belong to the domain + if len(notContainedInHost) == len(hostInfos): + codeNum = PARAM_ERROR + return self.response(code=codeNum, + message="All the host does not belong to the domain control, please enter the host " + "again") + + # Some hosts belong to domains, and some hosts do not belong to domains. + if len(notContainedInHost) == 0: + codeNum = SUCCEED + codeString = Format.spliceAllSuccString("host", "delete", containedInHost) + else: + codeNum = PARAM_ERROR + codeString = Format.splicErrorString("host", "delete", containedInHost, notContainedInHost) + + # git commit message + if len(containedInHost) > 0: + git_tools = GitTools() + commit_code = git_tools.gitCommit("Delete the host in {} domian, ".format(domain) + + "the host including : {}".format(containedInHost)) + # # 根据containedInHost 停止agith服务,删除agith,删除redis key值 + Format.uninstall_hosts_agith(access_token, containedInHost, domain) + + return self.response(code=codeNum, message=codeString) + + +class GetHostByDomainName(BaseResponse): + @BaseResponse.handle(schema=GetHostSchema, token=True) + def post(self, **params): + """get host by domainName + + get the host information of the configuration domain # noqa: E501 + + :param body: domain info + :type body: dict | bytes + + :rtype: List[Host] + """ + domain = params.get("domainName") + # check the input domain + checkRes = Format.domainCheck(domain) + if not checkRes: + codeNum = PARAM_ERROR + return self.response(code=codeNum, + message="Failed to verify the input parameter, please check the input parameters.") + + # check whether the domain exists + isExist = Format.isDomainExist(domain) + if not isExist: + codeNum = PARAM_ERROR + return self.response(code=codeNum, + message="The current domain does not exist, please create the domain first.") + + # The domain exists, but the host information is empty + domainPath = os.path.join(TARGETDIR, domain) + hostPath = os.path.join(domainPath, "hostRecord.txt") + if not os.path.isfile(hostPath) or (os.path.isfile(hostPath) and os.stat(hostPath).st_size == 0): + codeNum = SUCCEED + return self.response(code=codeNum, + message="The host information is not set in the current domain. Please add the host " + "information first.") + + # The domain exists, and the host information exists and is not empty + hostlist = [] + LOGGER.debug("hostPath is : {}".format(hostPath)) + try: + with open(hostPath, 'r') as d_file: + for line in d_file.readlines(): + json_str = json.loads(line) + host_json = ast.literal_eval(json_str) + hostId = host_json["host_id"] + ip = host_json["ip"] + ipv6 = host_json["ipv6"] + host = {"hostId": hostId, "ip": ip, "ipv6": ipv6} + hostlist.append(host) + except OSError as err: + LOGGER.error("OS error: {0}".format(err)) + codeNum = SERVER_ERROR + return self.response(code=codeNum, message="OS error: {0}".format(err)) + + # Joining together the returned codeNum codeMessage + if len(hostlist) == 0: + codeNum = SERVER_ERROR + return self.response(code=codeNum, message="Some unknown problems.") + else: + LOGGER.debug("hostlist is : {}".format(hostlist)) + codeNum = SUCCEED + return self.response(code=codeNum, message="Get host info in the domain successfully", data=hostlist) diff --git a/ragdoll/manage.py b/ragdoll/manage.py new file mode 100644 index 0000000..d646f77 --- /dev/null +++ b/ragdoll/manage.py @@ -0,0 +1,42 @@ +#!/usr/bin/python3 +try: + from gevent import monkey, pywsgi + + monkey.patch_all(ssl=False) +except: + pass + +from vulcanus.manage import init_application +from ragdoll.conf import configuration +from ragdoll.url import URLS +from ragdoll.utils.prepare import Prepare +from ragdoll.utils.yang_module import YangModule + + +def load_prepare(): + git_dir = configuration.git.get("GIT_DIR").replace("\"", "") + git_user_name = configuration.git.get("USER_NAME").replace("\"", "") + git_user_email = configuration.git.get("USER_EMAIL").replace("\"", "") + + prepare = Prepare(git_dir) + prepare.mkdir_git_warehose(git_user_name, git_user_email) + + +def load_yang(): + yang_modules = YangModule() + + +def main(): + _app = init_application(name="ragdoll", settings=configuration, register_urls=URLS) + # prepare to load config + load_prepare() + # load yang modules + load_yang() + print("configuration.ragdoll.get('ip') is ", configuration.ragdoll.get('IP')) + return _app + + +app = main() + +if __name__ == "__main__": + app.run(host=configuration.ragdoll.get('IP'), port=configuration.ragdoll.get("PORT")) diff --git a/ragdoll/models/batch_sync_req.py b/ragdoll/models/batch_sync_req.py new file mode 100644 index 0000000..bca0e65 --- /dev/null +++ b/ragdoll/models/batch_sync_req.py @@ -0,0 +1,95 @@ +# coding: utf-8 + +from __future__ import absolute_import +from datetime import date, datetime # noqa: F401 + +from typing import List, Dict # noqa: F401 + +from ragdoll.models.base_model_ import Model +from ragdoll import util +from ragdoll.models.sync_host_confs import SyncHostConfs + + +class BatchSyncReq(Model): + """NOTE: This class is auto generated by the swagger code generator program. + + Do not edit the class manually. + """ + + def __init__(self, domain_name: str = None, host_ids: List[int] = None): # noqa: E501 + + """ConfHost - a model defined in Swagger + + :param domain_name: The domain_name of this BatchSyncReq. # noqa: E501 + :type domain_name: str + :param host_ids: The host_ids of this BatchSyncReq. # noqa: E501 + :type host_ids: List[int] + """ + self.swagger_types = { + 'domain_name': str, + 'host_ids': List[int] + } + + self.attribute_map = { + 'domain_name': 'domainName', + 'host_ids': 'hostIds' + } + + self._domain_name = domain_name + self._host_ids = host_ids + + @classmethod + def from_dict(cls, dikt) -> 'BatchSyncReq': + """Returns the dict as a model + + :param dikt: A dict. + :type: dict + :return: The BatchSyncReq of this BatchSyncReq. # noqa: E501 + :rtype: BatchSyncReq + """ + return util.deserialize_model(dikt, cls) + + @property + def domain_name(self) -> str: + """Gets the domain_name of this BatchSyncReq. + + domain name # noqa: E501 + + :return: The domain_name of this BatchSyncReq. + :rtype: str + """ + return self._domain_name + + @domain_name.setter + def domain_name(self, domain_name: str): + """Sets the domain_name of this BatchSyncReq. + + domain name # noqa: E501 + + :param domain_name: The domain_name of this BatchSyncReq. + :type domain_name: str + """ + + self._domain_name = domain_name + + @property + def host_ids(self) -> List[int]: + """Gets the host_ids of this BatchSyncReq. + + + :return: The host_ids of this BatchSyncReq. + :rtype: List[int] + """ + + return self._host_ids + + @host_ids.setter + def host_ids(self, host_ids: List[int]): + """Sets the sync_list of this BatchSyncReq. + + + :param host_ids: The host_ids of this BatchSyncReq. + :type host_ids: List[int] + """ + + self._host_ids = host_ids diff --git a/ragdoll/models/compare_conf_diff.py b/ragdoll/models/compare_conf_diff.py new file mode 100644 index 0000000..c68c8b7 --- /dev/null +++ b/ragdoll/models/compare_conf_diff.py @@ -0,0 +1,104 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +@FileName: compare_conf_diff.py +@Time: 2024/1/25 10:05 +@Author: JiaoSiMao +Description: +""" +from typing import List +from ragdoll import util +from ragdoll.models.base_model_ import Model +from ragdoll.models.domain_conf_base_infos import DomainConfBaseInfos + + +class CompareConfDiff(Model): + """NOTE: This class is auto generated by the swagger code generator program. + + Do not edit the class manually. + """ + + def __init__(self, expected_confs_resp: List[DomainConfBaseInfos] = None, domain_result: object = None): # noqa: E501 + """CompareConfDiff - a model defined in Swagger + + :param expected_confs_resp: The all domain conf. # noqa: E501 + :type expected_confs_resp: str + + :param domain_result: The all domain host real conf. # noqa: E501 + :type domain_result: str + """ + self.swagger_types = { + 'expected_confs_resp': List[DomainConfBaseInfos], + 'domain_result': object + } + + self.attribute_map = { + 'expected_confs_resp': 'expectedConfsResp', + 'domain_result': 'domainResult' + } + + self._expected_confs_resp = expected_confs_resp + self._domain_result = domain_result + + @classmethod + def from_dict(cls, dikt) -> 'CompareConfDiff': + """Returns the dict as a model + + :param dikt: A dict. + :type: dict + :return: the CompareConfDiff of CompareConfDiff. # noqa: E501 + :rtype: CompareConfDiff + """ + return util.deserialize_model(dikt, cls) + + @property + def expected_confs_resp(self) -> List[DomainConfBaseInfos]: + """Gets expected_confs_resp of this CompareConfDiff. + + + :return: The expected_confs_resp of this CompareConfDiff. + :rtype: List[DomainConfBaseInfos] + """ + return self._expected_confs_resp + + @expected_confs_resp.setter + def expected_confs_resp(self, expected_confs_resp: List[DomainConfBaseInfos]): + """Sets expected_confs_resp of this CompareConfDiff. + + + :param expected_confs_resp: The expected_confs_resp of this CompareConfDiff. + type expected_confs_resp: List[DomainConfBaseInfos] + """ + + self._expected_confs_resp = expected_confs_resp + + @property + def domain_result(self) -> object: + """Gets domain_result of this CompareConfDiff. + + + :return: The domain_result of this CompareConfDiff. + :rtype: str + """ + return self._domain_result + + @domain_result.setter + def domain_result(self, domain_result: object): + """Sets domain_result of this CompareConfDiff. + + + :param domain_result: The domain_result of this CompareConfDiff. + type domain_result: str + """ + + self._domain_result = domain_result diff --git a/ragdoll/models/conf_base.py b/ragdoll/models/conf_base.py new file mode 100644 index 0000000..21d2ec6 --- /dev/null +++ b/ragdoll/models/conf_base.py @@ -0,0 +1,105 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +@FileName: conf_base.py +@Time: 2024/1/25 15:09 +@Author: JiaoSiMao +Description: +""" +from ragdoll import util +from ragdoll.models.base_model_ import Model + + +class ConfBase(Model): + """NOTE: This class is auto generated by the swagger code generator program. + + Do not edit the class manually. + """ + + def __init__(self, file_path: str = None, expected_contents: str = None): # noqa: E501 + """ConfBaseInfo - a model defined in Swagger + + :param file_path: The file_path of this ConfBase. # noqa: E501 + :type file_path: str + :param expected_contents: The expected_contents of this ConfBase. # noqa: E501 + :type expected_contents: str + """ + self.swagger_types = { + 'file_path': str, + 'expected_contents': str + } + + self.attribute_map = { + 'file_path': 'filePath', + 'expected_contents': 'expectedContents' + } + + self._file_path = file_path + self._expected_contents = expected_contents + + @classmethod + def from_dict(cls, dikt) -> 'ConfBase': + """Returns the dict as a model + + :param dikt: A dict. + :type: dict + :return: The ConfBase of this ConfBase. # noqa: E501 + :rtype: ConfBase + """ + return util.deserialize_model(dikt, cls) + + @property + def file_path(self) -> str: + """Gets the file_path of this ConfBase. + + the path of a configuration file # noqa: E501 + + :return: The file_path of this ConfBase. + :rtype: str + """ + return self._file_path + + @file_path.setter + def file_path(self, file_path: str): + """Sets the file_path of this ConfBase. + + the path of a configuration file # noqa: E501 + + :param file_path: The file_path of this ConfBase. + :type file_path: str + """ + + self._file_path = file_path + + @property + def expected_contents(self) -> str: + """Gets the expected_contents of this ConfBase. + + expected configuration value of configuration item # noqa: E501 + + :return: The expected_contents of this ConfBase. + :rtype: str + """ + return self._expected_contents + + @expected_contents.setter + def expected_contents(self, expected_contents: str): + """Sets the expected_contents of this ConfBase. + + expected configuration value of configuration item # noqa: E501 + + :param expected_contents: The expected_contents of this ConfBase. + :type expected_contents: str + """ + + self._expected_contents = expected_contents diff --git a/ragdoll/models/domain_conf_base_infos.py b/ragdoll/models/domain_conf_base_infos.py new file mode 100644 index 0000000..e49e8d2 --- /dev/null +++ b/ragdoll/models/domain_conf_base_infos.py @@ -0,0 +1,105 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +@FileName: domain_conf_base_infos.py +@Time: 2024/1/25 15:06 +@Author: JiaoSiMao +Description: +""" +from typing import List + +from ragdoll.models.base_model_ import Model +from ragdoll.models.conf_base import ConfBase +from ragdoll import util + + +class DomainConfBaseInfos(Model): + """NOTE: This class is auto generated by the swagger code generator program. + + Do not edit the class manually. + """ + + def __init__(self, conf_base_infos: List[ConfBase] = None, domain_name: str = None): # noqa: E501 + """DomainConfBaseInfos - a model defined in Swagger + + :param conf_base_infos: The all domain conf. # noqa: E501 + :type List[ConfBase] + + :param domain_name: The domain_name of DomainConfBaseInfos. # noqa: E501 + :type domain_name: str + """ + self.swagger_types = { + 'conf_base_infos': List[ConfBase], + 'domain_name': str + } + + self.attribute_map = { + 'conf_base_infos': 'confBaseInfos', + 'domain_name': 'domainName' + } + + self._conf_base_infos = conf_base_infos + self._domain_name = domain_name + + @classmethod + def from_dict(cls, dikt) -> 'DomainConfBaseInfos': + """Returns the dict as a model + + :param dikt: A dict. + :type: dict + :return: the DomainConfBaseInfos of DomainConfBaseInfos. # noqa: E501 + :rtype: DomainConfBaseInfos + """ + return util.deserialize_model(dikt, cls) + + @property + def conf_base_infos(self) -> List[ConfBase]: + """Gets conf_base_infos of this DomainConfBaseInfos. + + + :return: The conf_base_infos of this DomainConfBaseInfos. + :rtype: List[ConfBase] + """ + return self._conf_base_infos + + @conf_base_infos.setter + def conf_base_infos(self, conf_base_infos: List[ConfBase]): + """Sets conf_base_infos of this DomainConfBaseInfos. + + + :param conf_base_infos: The conf_base_infos of this DomainConfBaseInfos. + type expected_confs_resp: List[ConfBase] + """ + + self._conf_base_infos = conf_base_infos + + @property + def domain_name(self) -> str: + """Gets domain_name of this DomainConfBaseInfos. + + + :return: The domain_name of this DomainConfBaseInfos. + :rtype: str + """ + return self._domain_name + + @domain_name.setter + def domain_name(self, domain_name: str): + """Sets domain_name of this DomainConfBaseInfos. + + + :param domain_name: The domain_result of this DomainConfBaseInfos. + type domain_result: str + """ + + self._domain_name = domain_name diff --git a/ragdoll/models/domain_config_sync_result.py b/ragdoll/models/domain_config_sync_result.py new file mode 100644 index 0000000..c7dc741 --- /dev/null +++ b/ragdoll/models/domain_config_sync_result.py @@ -0,0 +1,131 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +@FileName: domain_config_sync_result.py +@Time: 2024/1/25 11:31 +@Author: JiaoSiMao +Description: +""" +# coding: utf-8 + +from __future__ import absolute_import +from datetime import date, datetime # noqa: F401 +from typing import List, Dict # noqa: F401 +from ragdoll.models.base_model_ import Model +from ragdoll import util + + +class DomainConfigSyncResult(Model): + """NOTE: This class is auto generated by the swagger code generator program. + + Do not edit the class manually. + """ + + def __init__(self, host_id: int = None, domain_name: str = None, sync_status: int = None): # noqa: E501 + """DomainConfigSyncResult - a model defined in Swagger + + :param host_id: The host_id of this HostSyncResult. # noqa: E501 + :type host_id: int + :param domain_name: The domain_name of this DomainConfigSyncResult. # noqa: E501 + :type domain_name: str + :param sync_status: The sync_status of this DomainConfigSyncResult. # noqa: E501 + :type sync_status: int + """ + self.swagger_types = { + 'host_id': int, + 'domain_name': str, + 'sync_status': int + } + + self.attribute_map = { + 'host_id': 'hostId', + 'domain_name': 'domainName', + 'sync_status': 'syncStatus' + } + + self._host_id = host_id + self._domain_name = domain_name + self._sync_status = sync_status + + @classmethod + def from_dict(cls, dikt) -> 'DomainConfigSyncResult': + """Returns the dict as a model + + :param dikt: A dict. + :type: dict + :return: The DomainConfigSyncResult of this DomainConfigSyncResult. # noqa: E501 + :rtype: DomainConfigSyncResult + """ + return util.deserialize_model(dikt, cls) + + @property + def host_id(self) -> int: + """Gets the host_id of this DomainConfigSyncResult. + + the id of host # noqa: E501 + + :return: The host_id of this DomainConfigSyncResult. + :rtype: int + """ + return self._host_id + + @host_id.setter + def host_id(self, host_id: int): + """Sets the host_id of this DomainConfigSyncResult. + + the id of host # noqa: E501 + + :param host_id: The host_id of this DomainConfigSyncResult. + :type host_id: str + """ + + self._host_id = host_id + + @property + def domain_name(self) -> str: + """Gets the domain_name of this DomainConfigSyncResult. + + :return: The domain_name of this DomainConfigSyncResult. + :rtype: str + """ + return self._domain_name + + @domain_name.setter + def domain_name(self, domain_name: str): + """Sets the domain_name of this DomainConfigSyncResult. + + :param domain_name: The domain_name of this DomainConfigSyncResult. + :type domain_name: str + """ + + self._domain_name = domain_name + + @property + def sync_status(self) -> int: + """Gets the sync_status of this DomainConfigSyncResult. + + :return: The sync_status of this DomainConfigSyncResult. + :rtype: int + """ + return self._sync_status + + @sync_status.setter + def sync_status(self, sync_status: int): + """Sets the sync_status of this DomainConfigSyncResult. + + + :param sync_status: The sync_status of this DomainConfigSyncResult. + :type sync_status: str + """ + + self._sync_status = sync_status diff --git a/ragdoll/models/domain_ip.py b/ragdoll/models/domain_ip.py new file mode 100644 index 0000000..932afec --- /dev/null +++ b/ragdoll/models/domain_ip.py @@ -0,0 +1,92 @@ +# coding: utf-8 + +from __future__ import absolute_import +from datetime import date, datetime # noqa: F401 + +from typing import List, Dict # noqa: F401 + +from ragdoll.models.base_model_ import Model +from ragdoll import util + + +class DomainIp(Model): + """NOTE: This class is auto generated by the swagger code generator program. + + Do not edit the class manually. + """ + + def __init__(self, domain_name: str=None, ip: str=None): # noqa: E501 + """DomainName - a model defined in Swagger + + :param domain_name: The domain_name of this DomainName. # noqa: E501 + :type domain_name: str + """ + self.swagger_types = { + 'domain_name': str, + 'ip': str + } + + self.attribute_map = { + 'domain_name': 'domainName', + 'ip': 'ip' + } + + self._domain_name = domain_name + self._ip = ip + + @classmethod + def from_dict(cls, dikt) -> 'DomainName': + """Returns the dict as a model + + :param dikt: A dict. + :type: dict + :return: The DomainName of this DomainName. # noqa: E501 + :rtype: DomainName + """ + return util.deserialize_model(dikt, cls) + + @property + def domain_name(self) -> str: + """Gets the domain_name of this DomainName. + + domain name # noqa: E501 + + :return: The domain_name of this DomainName. + :rtype: str + """ + return self._domain_name + + @domain_name.setter + def domain_name(self, domain_name: str): + """Sets the domain_name of this DomainName. + + domain name # noqa: E501 + + :param domain_name: The domain_name of this DomainName. + :type domain_name: str + """ + + self._domain_name = domain_name + + @property + def ip(self) -> str: + """Gets the ip in this domain. + + the ipv4 address in this domain # noqa: E501 + + :return: The ip in this domain. + :rtype: str + """ + return self._ip + + @ip.setter + def ip(self, ip: str): + """Sets the ip in this domain. + + the ipv4 address in this domain # noqa: E501 + + :param ip: The ip in this domain. + :type ip: str + """ + + self._ip = ip diff --git a/ragdoll/models/domain_names.py b/ragdoll/models/domain_names.py new file mode 100644 index 0000000..096cd81 --- /dev/null +++ b/ragdoll/models/domain_names.py @@ -0,0 +1,65 @@ +# coding: utf-8 + +from __future__ import absolute_import +from datetime import date, datetime # noqa: F401 + +from typing import List, Dict # noqa: F401 + +from ragdoll.models import DomainName +from ragdoll.models.base_model_ import Model +from ragdoll import util + + +class DomainNames(Model): + """NOTE: This class is auto generated by the swagger code generator program. + + Do not edit the class manually. + """ + + def __init__(self, domain_names: List[DomainName]=None): # noqa: E501 + """DomainName - a model defined in Swagger + + :param domain_names: The domain_names of this DomainNames. # noqa: E501 + :type domain_names: List[DomainName] + """ + self.swagger_types = { + 'domain_names': List[DomainName] + } + + self.attribute_map = { + 'domain_names': 'domainNames' + } + + self._domain_names = domain_names + + @classmethod + def from_dict(cls, dikt) -> 'DomainNames': + """Returns the dict as a model + + :param dikt: A dict. + :type: dict + :return: The DomainNames of this DomainNames. # noqa: E501 + :rtype: DomainNames + """ + return util.deserialize_model(dikt, cls) + + @property + def domain_names(self) -> List[DomainName]: + """Gets the domain_names of this DomainNames. + + + :return: The domain_names of this DomainNames. + :rtype: List[DomainName] + """ + return self._domain_names + + @domain_names.setter + def domain_names(self, domain_names: List[DomainName]): + """Sets the domain_names of this DomainNames. + + + :param domain_names: The domain_names of this DomainNames. + type domain_names: List[DomainName] + """ + + self._domain_names = domain_names diff --git a/ragdoll/models/realconf_base_info.py b/ragdoll/models/realconf_base_info.py index 8fe74f6..6139866 100644 --- a/ragdoll/models/realconf_base_info.py +++ b/ragdoll/models/realconf_base_info.py @@ -15,7 +15,7 @@ class RealconfBaseInfo(Model): Do not edit the class manually. """ - def __init__(self, path: str=None, file_path: str=None, rpm_name: str=None, rpm_version: str=None, rpm_release: str=None, file_attr: str=None, file_owner: str=None, conf_type: str=None, spacer: str=None, conf_contens: str=None): # noqa: E501 + def __init__(self, path: str=None, file_path: str=None, rpm_name: str=None, rpm_version: str=None, rpm_release: str=None, file_attr: str=None, file_owner: str=None, conf_type: str=None, spacer: str=None, conf_contents: str=None): # noqa: E501 """RealconfBaseInfo - a model defined in Swagger :param path: The path of this RealconfBaseInfo. # noqa: E501 @@ -36,8 +36,8 @@ class RealconfBaseInfo(Model): :type conf_type: str :param spacer: The spacer of this RealconfBaseInfo. # noqa: E501 :type spacer: str - :param conf_contens: The conf_contens of this RealconfBaseInfo. # noqa: E501 - :type conf_contens: str + :param conf_contents: The conf_contents of this RealconfBaseInfo. # noqa: E501 + :type conf_contents: str """ self.swagger_types = { 'path': str, @@ -49,7 +49,7 @@ class RealconfBaseInfo(Model): 'file_owner': str, 'conf_type': str, 'spacer': str, - 'conf_contens': str + 'conf_contents': str } self.attribute_map = { @@ -62,7 +62,7 @@ class RealconfBaseInfo(Model): 'file_owner': 'fileOwner', 'conf_type': 'confType', 'spacer': 'spacer', - 'conf_contens': 'confContents' + 'conf_contents': 'confContents' } self._path = path @@ -74,7 +74,7 @@ class RealconfBaseInfo(Model): self._file_owner = file_owner self._conf_type = conf_type self._spacer = spacer - self._conf_contens = conf_contens + self._conf_contents = conf_contents @classmethod def from_dict(cls, dikt) -> 'RealconfBaseInfo': @@ -293,24 +293,24 @@ class RealconfBaseInfo(Model): self._spacer = spacer @property - def conf_contens(self) -> str: - """Gets the conf_contens of this RealconfBaseInfo. + def conf_contents(self) -> str: + """Gets the conf_contents of this RealconfBaseInfo. the specific content of the configuration item # noqa: E501 - :return: The conf_contens of this RealconfBaseInfo. + :return: The conf_contents of this RealconfBaseInfo. :rtype: str """ - return self._conf_contens + return self._conf_contents - @conf_contens.setter - def conf_contens(self, conf_contens: str): - """Sets the conf_contens of this RealconfBaseInfo. + @conf_contents.setter + def conf_contents(self, conf_contents: str): + """Sets the conf_contents of this RealconfBaseInfo. the specific content of the configuration item # noqa: E501 - :param conf_contens: The conf_contens of this RealconfBaseInfo. - :type conf_contens: str + :param conf_contents: The conf_contents of this RealconfBaseInfo. + :type conf_contents: str """ - self._conf_contens = conf_contens + self._conf_contents = conf_contents diff --git a/ragdoll/swagger/swagger.yaml b/ragdoll/swagger/swagger.yaml index 8fd5dcb..e90201f 100644 --- a/ragdoll/swagger/swagger.yaml +++ b/ragdoll/swagger/swagger.yaml @@ -303,7 +303,12 @@ paths: summary: "query expected configuration value in the current hostId node" description: "queryExpectedConfs" operationId: "query_excepted_confs" - parameters: [ ] + parameters: + - in: "body" + name: "body" + required: false + schema: + $ref: "#/definitions/DomainNames" responses: "200": description: "query expected configuration value successfully" @@ -337,6 +342,50 @@ paths: items: $ref: "#/definitions/HostSyncResult" x-swagger-router-controller: "ragdoll.controllers.confs_controller" + /confs/batch/syncConf: + put: + tags: + - "confs" + summary: "batch synchronize the configuration information of the configuration domain\ + \ to the host" + description: "batch synchronize the configuration information of the configuration\ + \ domain to the host" + operationId: "batch_sync_conf_to_host_from_domain" + parameters: + - in: "body" + name: "body" + required: false + schema: + $ref: "#/definitions/BatchSyncReq" + responses: + "200": + description: "synchronize the configuration items successfully" + schema: + type: "array" + items: + $ref: "#/definitions/HostSyncResult" + x-swagger-router-controller: "ragdoll.controllers.confs_controller" + /confs/domain/diff: + post: + tags: + - "confs" + summary: "compare domain conf different" + description: "compare domain conf different" + operationId: "compare_conf_diff" + parameters: + - in: "body" + name: "body" + required: false + schema: + $ref: "#/definitions/CompareConfDiff" + responses: + "200": + description: "compare domain conf different successfully" + schema: + type: "array" + items: + $ref: "#/definitions/DomainConfigSyncResult" + x-swagger-router-controller: "ragdoll.controllers.confs_controller" /confs/getDomainStatus: post: tags: @@ -349,7 +398,7 @@ paths: name: "body" required: false schema: - $ref: "#/definitions/DomainName" + $ref: "#/definitions/DomainIp" responses: "200": description: "get the status of the domain successfully" @@ -418,6 +467,22 @@ definitions: domainName: type: "string" description: "domain name" + DomainNames: + type: "object" + properties: + domainNames: + type: "array" + items: + $ref: "#/definitions/DomainName" + DomainIp: + type: "object" + properties: + domainName: + type: "string" + description: "domain name" + ip: + type: "string" + description: "ip" HostInfos: type: "object" properties: @@ -780,6 +845,18 @@ definitions: example: hostId: "hostId" syncStatus: "SUCCESS" + DomainConfigSyncResult: + type: "object" + properties: + hostId: + type: "integer" + description: "the id of host" + domainName: + type: "string" + description: "the domainName of host" + syncStatus: + type: "integer" + description: "the syncStatus of host" CollectInfo: type: "object" properties: @@ -820,6 +897,35 @@ definitions: type: "array" items: type: "string" + CompareConfDiff: + type: "object" + properties: + expectedConfsResp: + type: "array" + items: + $ref: "#/definitions/DomainConfBaseInfos" + domainResult: + type: object + description: "domain real config" + DomainConfBaseInfos: + type: "object" + properties: + confBaseInfos: + type: array + items: + $ref: "#/definitions/ConfBase" + domainName: + type: "string" + description: "domain name" + ConfBase: + type: "object" + properties: + filePath: + type: "string" + description: "file path" + expectedContents: + type: "string" + description: "expected contents" SyncReq: type: "object" properties: @@ -830,6 +936,16 @@ definitions: type: "array" items: $ref: "#/definitions/SyncHostConfs" + BatchSyncReq: + type: "object" + properties: + domainName: + type: "string" + description: "domain name" + hostIds: + type: "array" + items: + type: "integer" SingleConf: type: object properties: diff --git a/ragdoll/test/test_conf_model.py b/ragdoll/test/test_conf_model.py index 35d6a0b..e29b2f8 100644 --- a/ragdoll/test/test_conf_model.py +++ b/ragdoll/test/test_conf_model.py @@ -13,7 +13,7 @@ from ragdoll.log.log import LOGGER from ragdoll.test import BaseTestCase from ragdoll.utils.yang_module import YangModule from ragdoll.utils.object_parse import ObjectParse -from ragdoll.controllers.format import Format +from ragdoll.utils.format import Format class TestConfModel(): """ Test config_model """ diff --git a/ragdoll/url.py b/ragdoll/url.py new file mode 100644 index 0000000..4272bba --- /dev/null +++ b/ragdoll/url.py @@ -0,0 +1,62 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved. +# licensed under the Mulan PSL v2. +# You can use this software according to the terms and conditions of the Mulan PSL v2. +# You may obtain a copy of Mulan PSL v2 at: +# http://license.coscl.org.cn/MulanPSL2 +# THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +# PURPOSE. +# See the Mulan PSL v2 for more details. +# ******************************************************************************/ +""" +@FileName: url.py +@Time: 2024/3/4 10:31 +@Author: JiaoSiMao +Description: +""" +from ragdoll.conf.constant import ( + CREATE_DOMAIN, DELETE_DOMAIN, QUERY_DOMAIN, ADD_HOST_IN_DOMAIN, DELETE_HOST_IN_DOMAIN, GET_HOST_BY_DOMAIN, + ADD_MANAGEMENT_CONFS_IN_DOMAIN, UPLOAD_MANAGEMENT_CONFS_IN_DOMAIN, DELETE_MANAGEMENT_CONFS_IN_DOMAIN, + GET_MANAGEMENT_CONFS_IN_DOMAIN, QUERY_CHANGELOG_OF_MANAGEMENT_CONFS_IN_DOMAIN, GET_SYNC_STATUS, + QUERY_EXCEPTED_CONFS, QUERY_REAL_CONFS, SYNC_CONF_TO_HOST_FROM_DOMAIN, QUERY_SUPPORTED_CONFS, COMPARE_CONF_DIFF, + BATCH_SYNC_CONF_TO_HOST_FROM_DOMAIN +) +from ragdoll.domain_manage import view as domain_view +from ragdoll.host_manage import view as host_view +from ragdoll.domain_conf_manage import view as domain_conf_view +from ragdoll.confs_manage import view as confs_view +URLS = [] + +SPECIFIC_URLS = { + "DOMAIN_URLS": [ + (domain_view.CreateDomain, CREATE_DOMAIN), + (domain_view.DeleteDomain, DELETE_DOMAIN), + (domain_view.QueryDomain, QUERY_DOMAIN), + ], + "HOST_URLS": [ + (host_view.AddHostInDomain, ADD_HOST_IN_DOMAIN), + (host_view.DeleteHostInDomain, DELETE_HOST_IN_DOMAIN), + (host_view.GetHostByDomainName, GET_HOST_BY_DOMAIN), + ], + "MANAGEMENT_URLS": [ + (domain_conf_view.AddManagementConfsInDomain, ADD_MANAGEMENT_CONFS_IN_DOMAIN), + (domain_conf_view.UploadManagementConfsInDomain, UPLOAD_MANAGEMENT_CONFS_IN_DOMAIN), + (domain_conf_view.DeleteManagementConfsInDomain, DELETE_MANAGEMENT_CONFS_IN_DOMAIN), + (domain_conf_view.GetManagementConfsInDomain, GET_MANAGEMENT_CONFS_IN_DOMAIN), + (domain_conf_view.QueryChangelogOfManagementConfsInDomain, QUERY_CHANGELOG_OF_MANAGEMENT_CONFS_IN_DOMAIN), + ], + "CONFS_URLS": [ + (confs_view.GetTheSyncStatusOfDomain, GET_SYNC_STATUS), + (confs_view.QueryExceptedConfs, QUERY_EXCEPTED_CONFS), + (confs_view.QueryRealConfs, QUERY_REAL_CONFS), + (confs_view.SyncConfToHostFromDomain, SYNC_CONF_TO_HOST_FROM_DOMAIN), + (confs_view.QuerySupportedConfs, QUERY_SUPPORTED_CONFS), + (confs_view.CompareConfDiff, COMPARE_CONF_DIFF), + (confs_view.BatchSyncConfToHostFromDomain, BATCH_SYNC_CONF_TO_HOST_FROM_DOMAIN), + ] +} + +for _, value in SPECIFIC_URLS.items(): + URLS.extend(value) diff --git a/ragdoll/util.py b/ragdoll/util.py index edb2251..6d671c3 100644 --- a/ragdoll/util.py +++ b/ragdoll/util.py @@ -1,7 +1,6 @@ import datetime import six -import typing def _deserialize(data, klass): diff --git a/ragdoll/utils/conf_tools.py b/ragdoll/utils/conf_tools.py index 0ad1f8f..2fe0689 100644 --- a/ragdoll/utils/conf_tools.py +++ b/ragdoll/utils/conf_tools.py @@ -6,7 +6,7 @@ from enum import Enum from ragdoll.const.conf_handler_const import CONFIG from ragdoll.utils.git_tools import GitTools -from ragdoll.controllers.format import Format +from ragdoll.utils.format import Format from ragdoll.log.log import LOGGER from ragdoll.models.real_conf import RealConf from ragdoll.models.real_conf_path import RealConfPath @@ -80,10 +80,7 @@ class ConfTools(object): def listToDict(self, manaConfs): res = {} LOGGER.debug("manaConfs is : {}".format(manaConfs)) - LOGGER.debug("the typr of manaConfs is : {}".format(type(manaConfs))) for d_conf in manaConfs: - LOGGER.debug("d_conf is : {}".format(d_conf)) - LOGGER.debug("the type of d_conf is : {}".format(type(d_conf))) path = d_conf.get(PATH) value = d_conf.get(EXCEPTED_VALUE).strip() level = path.split("/") @@ -120,8 +117,6 @@ class ConfTools(object): """ realConfWithFeature = {} LOGGER.debug("featureList is : {}".format(featureList)) - lenFeature = len(featureList) - tempRealConf = realConfDict d_real_file = {} d_real_file[featureList[1]] = realConfDict d_real_feature = {} @@ -173,11 +168,8 @@ class ConfTools(object): ] """ res = [] - conf_nums = len(realConfResText) LOGGER.debug("realConfResText is : {}".format(realConfResText)) for d_conf in realConfResText: - LOGGER.debug("d_conf is : {}".format(d_conf)) - LOGGER.debug("d_conf 's type is : {}".format(type(d_conf))) domainName = d_conf.get("domainName") hostId = d_conf.get("hostID") conf_base_infos = d_conf.get("confBaseInfos") @@ -187,7 +179,6 @@ class ConfTools(object): for d_conf_info in conf_base_infos: paths = d_conf_info.get("path").split(" ") confContents = json.loads(d_conf_info.get("confContents")) - LOGGER.debug("confContents is : {}".format(confContents)) for d_path in paths: x_path = os.path.join(domainName, d_path) remove_feature_path = d_path.split("/")[2:] @@ -237,13 +228,11 @@ class ConfTools(object): dict1 = json.loads(real_conf) dict2 = json.loads(man_conf) - res = "" + res = SYNCHRONIZED for src_list, dst_list in zip(sorted(dict1), sorted(dict2)): if str(dict1[src_list]) != str(dict2[dst_list]): res = NOTSYNCHRONIZE - if not res: - res = SYNCHRONIZED - + break return res def getRpmInfo(self, path): @@ -253,15 +242,12 @@ class ConfTools(object): input: '/etc/yum.repos.d/openEuler.repo' output: openEuler-repos 1.0 3.0.oe1.aarch64 """ - res = "" if not os.path.exists(path): return None cmd = "rpm -qf {}".format(path) gitTools = GitTools() package_string = gitTools.run_shell_return_output(cmd).decode() LOGGER.debug("package_string is : {}".format(package_string)) - # lines = returnCode.rsplit(STRIKETHROUGH) - # res = lines[0] if 'not owned by any package' in package_string: return None, None, None pkg, arch = Format.rsplit(package_string, Format.arch_sep(package_string)) @@ -270,7 +256,6 @@ class ConfTools(object): pkg, release = Format.rsplit(pkg, '-') name, version = Format.rsplit(pkg, '-') # If the value of epoch needs to be returned separately, - # epoch, version = version.split(':', 1) if ":" in version else ['0', version] return name, release, version def getFileAttr(self, path): @@ -315,7 +300,6 @@ class ConfTools(object): d_lines = line.split(RightParen + TWOSPACE) for d_line in d_lines: d_line = d_line.lstrip() - # print("d_line is : {}".format(d_line)) if d_line.startswith(ACCESS): fileAttr = d_line.split(FS)[0].split(LeftParen)[1] elif d_line.startswith(UID): @@ -355,15 +339,15 @@ class ConfTools(object): ll_res_list = ll_res.split(SPACE) fileType = ll_res_list[0] - permssions = "0" + permissions = "0" for perm in range(0, PERMISSION): items = fileType[1 + perm * PERMISSION: (perm + 1) * PERMISSION + 1] value = 0 for d_item in items: d_item_value = self.switch_perm(d_item) value = value + d_item_value - permssions = permssions + str(value) - LOGGER.debug("the perssion is : {}".format(permssions)) + permissions = permissions + str(value) + LOGGER.debug("the permission is : {}".format(permissions)) fileOwner = LeftParen + ll_res_list[2] + SPACE + ll_res_list[3] + RightParen LOGGER.debug("the fileOwner is : {}".format(fileOwner)) @@ -446,9 +430,6 @@ class ConfTools(object): value = "" for count in range(0, len(real_conf)): d_real = real_conf[count] - # print("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&") - # print("d_real is : {}".format(d_real)) - # print("path is : {}".format(path)) if d_real.path == path: index = count value = d_real.real_value.strip() @@ -622,7 +603,43 @@ class ConfTools(object): object_file_url = "{address}:{port}{api}".format(address=object_file_address, api=object_file_api, port=object_file_port) - url = {"collect_url": collect_url, "sync_url": sync_url, "object_file_url": object_file_url} + batch_sync_address = ast.literal_eval(cf.get("sync", "batch_sync_address")) + batch_sync_api = ast.literal_eval(cf.get("sync", "batch_sync_api")) + batch_sync_port = str(cf.get("sync", "sync_port")) + batch_sync_url = "{address}:{port}{api}".format(address=batch_sync_address, api=batch_sync_api, + port=batch_sync_port) + + host_sync_status_address = ast.literal_eval(cf.get("sync_status", "host_sync_status_address")) + add_host_sync_status_api = ast.literal_eval(cf.get("sync_status", "add_host_sync_status_api")) + delete_host_sync_status_api = ast.literal_eval(cf.get("sync_status", "delete_host_sync_status_api")) + delete_all_host_sync_status_api = ast.literal_eval(cf.get("sync_status", "delete_all_host_sync_status_api")) + host_sync_status_port = str(cf.get("sync_status", "host_sync_status_port")) + add_host_sync_status_url = "{address}:{port}{api}".format(address=host_sync_status_address, + api=add_host_sync_status_api, + port=host_sync_status_port) + delete_host_sync_status_url = "{address}:{port}{api}".format(address=host_sync_status_address, + api=delete_host_sync_status_api, + port=host_sync_status_port) + delete_all_host_sync_status_url = "{address}:{port}{api}".format(address=host_sync_status_address, + api=delete_all_host_sync_status_api, + port=host_sync_status_port) + + conf_trace_mgmt_address = ast.literal_eval(cf.get("conf_trace", "conf_trace_mgmt_address")) + conf_trace_mgmt_api = ast.literal_eval(cf.get("conf_trace", "conf_trace_mgmt_api")) + conf_trace_delete_api = ast.literal_eval(cf.get("conf_trace", "conf_trace_delete_api")) + conf_trace_port = str(cf.get("conf_trace", "conf_trace_port")) + conf_trace_mgmt_url = "{address}:{port}{api}".format(address=conf_trace_mgmt_address, + api=conf_trace_mgmt_api, + port=conf_trace_port) + conf_trace_delete_url = "{address}:{port}{api}".format(address=conf_trace_mgmt_address, + api=conf_trace_delete_api, + port=conf_trace_port) + + url = {"collect_url": collect_url, "sync_url": sync_url, "object_file_url": object_file_url, + "batch_sync_url": batch_sync_url, "add_host_sync_status_url": add_host_sync_status_url, + "delete_host_sync_status_url": delete_host_sync_status_url, + "delete_all_host_sync_status_url": delete_all_host_sync_status_url, + "conf_trace_mgmt_url": conf_trace_mgmt_url, "conf_trace_delete_url": conf_trace_delete_url} return url def load_port_by_conf(self): diff --git a/ragdoll/utils/format.py b/ragdoll/utils/format.py new file mode 100644 index 0000000..a674b44 --- /dev/null +++ b/ragdoll/utils/format.py @@ -0,0 +1,1138 @@ +import os +import re +import json +import configparser +import ast + +import requests + +from ragdoll.log.log import LOGGER + +from ragdoll.const.conf_handler_const import NOT_SYNCHRONIZE, SYNCHRONIZED, CONFIG, \ + DIRECTORY_FILE_PATH_LIST +from ragdoll.models import ConfSyncedRes +from ragdoll.models.base_response import BaseResponse # noqa: E501 +from ragdoll.models.conf_file import ConfFile +from ragdoll.models.conf_files import ConfFiles +from ragdoll.models.host import Host # noqa: E501 +from ragdoll.utils.host_tools import HostTools + + +class Format(object): + + @staticmethod + def domainCheck(domainName): + res = True + if not re.match(r"^[A-Za-z0-9_\.-]*$", domainName) or domainName == "" or len(domainName) > 255: + res = False + return res + + @staticmethod + def isDomainExist(domainName): + TARGETDIR = Format.get_git_dir() + domainPath = os.path.join(TARGETDIR, domainName) + if os.path.exists(domainPath): + return True + + return False + + @staticmethod + def spliceAllSuccString(obj, operation, succDomain): + """ + docstring + """ + codeString = "All {obj} {oper} successfully, {succ} {obj} in total.".format( \ + obj=obj, oper=operation, succ=len(succDomain)) + return codeString + + @staticmethod + def splicErrorString(obj, operation, succDomain, failDomain): + """ + docstring + """ + codeString = "{succ} {obj} {oper} successfully, {fail} {obj} {oper} failed.".format( \ + succ=len(succDomain), obj=obj, oper=operation, fail=len(failDomain)) + + succString = "\n" + if len(succDomain) > 0: + succString = "These are successful: " + for succName in succDomain: + succString += succName + " " + succString += "." + + if len(failDomain) > 0: + failString = "These are failed: " + for failName in failDomain: + failString += failName + " " + return codeString + succString + failString + + return codeString + succString + + @staticmethod + def two_abs_join(abs1, abs2): + """ + Absolute path Joins two absolute paths together + :param abs1: main path + :param abs2: the spliced path + :return: together the path + """ + # 1. Format path (change \\ in path to \) + abs2 = os.fspath(abs2) + + # 2. Split the path file + abs2 = os.path.splitdrive(abs2)[1] + # 3. Remove the beginning '/' + abs2 = abs2.strip('\\/') or abs2 + return os.path.abspath(os.path.join(abs1, abs2)) + + @staticmethod + def isContainedHostIdInfile(f_file, content): + isContained = False + with open(f_file, 'r') as d_file: + for line in d_file.readlines(): + line_dict = json.loads(str(ast.literal_eval(line)).replace("'", "\"")) + if content == line_dict["host_id"]: + isContained = True + break + return isContained + + @staticmethod + def isContainedHostIdInOtherDomain(content): + from ragdoll.conf.constant import TARGETDIR + isContained = False + contents = os.listdir(TARGETDIR) + folders = [f for f in contents if os.path.isdir(os.path.join(TARGETDIR, f))] + for folder in folders: + hostPath = os.path.join(os.path.join(TARGETDIR, folder), "hostRecord.txt") + if os.path.isfile(hostPath): + with open(hostPath, 'r') as d_file: + for line in d_file.readlines(): + line_dict = json.loads(str(ast.literal_eval(line)).replace("'", "\"")) + if content == line_dict["host_id"]: + isContained = True + break + return isContained + + @staticmethod + def addHostToFile(d_file, host): + host = {'host_id': host["hostId"], 'ip': host["ip"], 'ipv6': host["ipv6"]} + info_json = json.dumps(str(host), sort_keys=False, indent=4, separators=(',', ': ')) + os.umask(0o077) + with open(d_file, 'a+') as host_file: + host_file.write(info_json) + host_file.write("\n") + + @staticmethod + def getSubDirFiles(path): + """ + desc: Subdirectory records and files need to be logged to the successConf + """ + fileRealPathList = [] + fileXPathlist = [] + for root, dirs, files in os.walk(path): + if len(files) > 0: + preXpath = root.split('/', 3)[3] + for d_file in files: + xpath = os.path.join(preXpath, d_file) + fileXPathlist.append(xpath) + realPath = os.path.join(root, d_file) + fileRealPathList.append(realPath) + + return fileRealPathList, fileXPathlist + + @staticmethod + def isHostInDomain(domainName): + """ + desc: Query domain Whether host information is configured in the domain + """ + isHostInDomain = False + TARGETDIR = Format.get_git_dir() + domainPath = os.path.join(TARGETDIR, domainName) + hostPath = os.path.join(domainPath, "hostRecord.txt") + if os.path.isfile(hostPath): + isHostInDomain = True + + return isHostInDomain + + @staticmethod + def isHostIdExist(hostPath, hostId): + """ + desc: Query hostId exists within the current host domain management + """ + isHostIdExist = False + if os.path.isfile(hostPath) and os.stat(hostPath).st_size > 0: + with open(hostPath) as h_file: + for line in h_file.readlines(): + if hostId in line: + isHostIdExist = True + break + + return isHostIdExist + + @staticmethod + def get_file_content_by_readlines(d_file): + """ + desc: remove empty lines and comments from d_file + """ + res = [] + try: + with open(d_file, 'r') as s_f: + lines = s_f.readlines() + for line in lines: + tmp = line.strip() + if not len(tmp) or tmp.startswith("#"): + continue + res.append(line) + except FileNotFoundError: + LOGGER.error(f"File not found: {d_file}") + except IOError as e: + LOGGER.error(f"IO error: {e}") + except Exception as e: + LOGGER.error(f"An error occurred: {e}") + return res + + @staticmethod + def get_file_content_by_read(d_file): + """ + desc: return a string after read the d_file + """ + if not os.path.exists(d_file): + return "" + with open(d_file, 'r') as s_f: + lines = s_f.read() + return lines + + @staticmethod + def rsplit(_str, seps): + """ + Splits _str by the first sep in seps that is found from the right side. + Returns a tuple without the separator. + """ + for idx, ch in enumerate(reversed(_str)): + if ch in seps: + return _str[0:-idx - 1], _str[-idx:] + + @staticmethod + def arch_sep(package_string): + """ + Helper method for finding if arch separator is '.' or '-' + + Args: + package_string (str): dash separated package string such as 'bash-4.2.39-3.el7'. + + Returns: + str: arch separator + """ + return '.' if package_string.rfind('.') > package_string.rfind('-') else '-' + + @staticmethod + def set_file_content_by_path(content, path): + res = 0 + if os.path.exists(path): + with open(path, 'w+') as d_file: + for d_cont in content: + d_file.write(d_cont) + d_file.write("\n") + res = 1 + return res + + @staticmethod + def get_git_dir(): + cf = configparser.ConfigParser() + if os.path.exists(CONFIG): + cf.read(CONFIG, encoding="utf-8") + else: + parent = os.path.dirname(os.path.realpath(__file__)) + conf_path = os.path.join(parent, "../../config/gala-ragdoll.conf") + cf.read(conf_path, encoding="utf-8") + git_dir = ast.literal_eval(cf.get("git", "git_dir")) + return git_dir + + @staticmethod + def get_hostinfo_by_domain(domainName): + """ + desc: Query hostinfo by domainname + """ + LOGGER.debug("Get hostinfo by domain : {}".format(domainName)) + TARGETDIR = Format.get_git_dir() + hostlist = [] + domainPath = os.path.join(TARGETDIR, domainName) + hostPath = os.path.join(domainPath, "hostRecord.txt") + if not os.path.exists(hostPath): + return hostlist + try: + with open(hostPath, 'r') as d_file: + for line in d_file.readlines(): + json_str = json.loads(line) + host_json = ast.literal_eval(json_str) + hostId = host_json["host_id"] + ip = host_json["ip"] + ipv6 = host_json["ipv6"] + host = Host(host_id=hostId, ip=ip, ipv6=ipv6) + hostlist.append(host.to_dict()) + except OSError as err: + LOGGER.error("OS error: {0}".format(err)) + return hostlist + if len(hostlist) == 0: + LOGGER.debug("Hostlist is empty !") + else: + LOGGER.debug("Hostlist is : {}".format(hostlist)) + return hostlist + + @staticmethod + def get_host_id_by_ip(ip, domainName): + """ + desc: Query hostinfo by host ip + """ + LOGGER.debug("Get hostinfo by ip : {}".format(ip)) + TARGET_DIR = Format.get_git_dir() + hostlist = [] + domainPath = os.path.join(TARGET_DIR, domainName) + hostPath = os.path.join(domainPath, "hostRecord.txt") + if not os.path.isfile(hostPath) or os.stat(hostPath).st_size == 0: + return hostlist + try: + with open(hostPath, 'r') as d_file: + for line in d_file.readlines(): + json_str = json.loads(line) + host_json = ast.literal_eval(json_str) + if host_json["ip"] == ip: + return host_json["host_id"] + except OSError as err: + LOGGER.error("OS error: {0}".format(err)) + + @staticmethod + def get_manageconf_by_domain(domain): + LOGGER.debug("Get managerconf by domain : {}".format(domain)) + expected_conf_lists = ConfFiles(domain_name=domain, conf_files=[]) + TARGETDIR = Format.get_git_dir() + domainPath = os.path.join(TARGETDIR, domain) + from ragdoll.utils.yang_module import YangModule + for root, dirs, files in os.walk(domainPath): + if len(files) > 0 and len(root.split('/')) > 3: + if "hostRecord.txt" in files: + continue + for d_file in files: + d_file_path = os.path.join(root, d_file) + contents = Format.get_file_content_by_read(d_file_path) + feature = os.path.join(root.split('/')[-1], d_file) + yang_modules = YangModule() + d_module = yang_modules.getModuleByFeature(feature) + file_lists = yang_modules.getFilePathInModdule(yang_modules.module_list) + file_path = file_lists.get(d_module.name()).split(":")[-1] + + conf = ConfFile(file_path=file_path, contents=contents) + expected_conf_lists.conf_files.append(conf.to_dict()) + + LOGGER.debug("Expected_conf_lists is :{}".format(expected_conf_lists)) + return expected_conf_lists.to_dict() + + @staticmethod + def get_realconf_by_domain_and_host(domain, exist_host, access_token): + res = [] + conf_files = Format.get_manageconf_by_domain(domain) + # get the real conf in host + conf_list = [] + from ragdoll.utils.conf_tools import ConfTools + from ragdoll.utils.object_parse import ObjectParse + conf_tools = ConfTools() + for d_conf in conf_files.get("conf_files"): + file_path = d_conf.get("file_path").split(":")[-1] + if file_path not in DIRECTORY_FILE_PATH_LIST: + conf_list.append(file_path) + else: + d_conf_cs = d_conf.get("contents") + d_conf_contents = json.loads(d_conf_cs) + for d_conf_key, d_conf_value in d_conf_contents.items(): + conf_list.append(d_conf_key) + get_real_conf_body = {} + get_real_conf_body_info = [] + for d_host in exist_host: + get_real_conf_body_infos = {} + get_real_conf_body_infos["host_id"] = d_host + get_real_conf_body_infos["config_list"] = conf_list + get_real_conf_body_info.append(get_real_conf_body_infos) + get_real_conf_body["infos"] = get_real_conf_body_info + url = conf_tools.load_url_by_conf().get("collect_url") + headers = {"Content-Type": "application/json", "access_token": access_token} + try: + response = requests.post(url, data=json.dumps(get_real_conf_body), headers=headers) # post request + except requests.exceptions.RequestException as connect_ex: + LOGGER.error(f"An error occurred: {connect_ex}") + codeNum = 500 + codeString = "Failed to obtain the actual configuration, please check the interface of config/collect." + base_rsp = BaseResponse(codeNum, codeString) + return base_rsp, codeNum + resp = json.loads(response.text).get("data") + resp_code = json.loads(response.text).get("code") + if (resp_code != "200") and (resp_code != "206"): + return res + + if not resp or len(resp) == 0: + return res + + success_lists = {} + failed_lists = {} + + for d_res in resp: + d_host_id = d_res.get("host_id") + fail_files = d_res.get("fail_files") + if len(fail_files) > 0: + failed_lists["host_id"] = d_host_id + failed_lists_conf = [] + for d_failed in fail_files: + failed_lists_conf.append(d_failed) + failed_lists["failed_conf"] = failed_lists_conf + failed_lists["success_conf"] = [] + else: + success_lists["host_id"] = d_host_id + success_lists["success_conf"] = [] + success_lists["failed_conf"] = [] + + read_conf_info = {"domainName": domain, "hostID": d_host_id, "confBaseInfos": []} + d_res_infos = d_res.get("infos") + + real_directory_conf = {} + real_directory_conf_list = {} + object_parse = ObjectParse() + for d_file in d_res_infos: + content = d_file.get("content") + file_path = d_file.get("path") + file_atrr = d_file.get("file_attr").get("mode") + file_owner = "({}, {})".format(d_file.get("file_attr").get("group"), + d_file.get("file_attr").get("owner")) + directory_flag = False + for dir_path in DIRECTORY_FILE_PATH_LIST: + if str(file_path).find(dir_path) != -1: + if real_directory_conf.get(dir_path) is None: + real_directory_conf_list[dir_path] = list() + real_directory_conf[dir_path] = {"filePath": dir_path, "fileAttr": file_atrr, + "fileOwner": file_owner, "confContents": ""} + directory_conf = dict() + directory_conf["path"] = file_path + directory_conf["content"] = content + real_directory_conf_list.get(dir_path).append(directory_conf) + directory_flag = True + break + if not directory_flag: + Format.deal_conf_list_content(content, d_file, file_path, object_parse, read_conf_info) + if len(fail_files) > 0: + failed_lists.get("success_conf").append(file_path) + else: + success_lists.get("success_conf").append(file_path) + + for dir_path, dir_value in real_directory_conf_list.items(): + content_string = object_parse.parse_directory_single_conf_to_json(dir_value, + real_directory_conf[ + dir_path]["filePath"]) + real_directory_conf[dir_path]["confContents"] = content_string + real_conf_base_info = real_directory_conf.get(dir_path) + + read_conf_info.get("confBaseInfos").append(real_conf_base_info) + res.append(read_conf_info) + return res + + @staticmethod + def deal_conf_list_content(content, d_file, file_path, object_parse, read_conf_info): + content_string = object_parse.parse_conf_to_json(file_path, content) + file_atrr = d_file.get("file_attr").get("mode") + file_owner = "({}, {})".format(d_file.get("file_attr").get("group"), + d_file.get("file_attr").get("owner")) + real_conf_base_info = {"path": file_path, "filePath": file_path, "fileAttr": file_atrr, "fileOwner": file_owner, + "confContents": content_string} + read_conf_info.get("confBaseInfos").append(real_conf_base_info) + + @staticmethod + def check_domain_param(domain): + code_num = 200 + base_resp = None + check_res = Format.domainCheck(domain) + if not check_res: + num = 400 + base_rsp = BaseResponse(num, "Failed to verify the input parameter, please check the input parameters.") + return base_rsp, num + + # check the domian is exist + is_exist = Format.isDomainExist(domain) + if not is_exist: + code_num = 404 + base_rsp = BaseResponse(code_num, "The current domain does not exist, please create the domain first.") + return base_rsp, code_num + + # get the exist result of the host in domain + is_host_list_exist = Format.isHostInDomain(domain) + if not is_host_list_exist: + code_num = 404 + base_resp = BaseResponse(code_num, "The host information is not set in the current domain." + + "Please add the host information first") + return base_resp, code_num + + @staticmethod + def get_hostid_list_by_domain(domain): + host_ids = [] + res_text = Format.get_hostinfo_by_domain(domain) + if len(res_text) == 0: + return host_ids + + host_tools = HostTools() + host_ids = host_tools.getHostList(res_text) + return host_ids + + @staticmethod + def get_domain_conf(domain): + code_num = 200 + base_resp = None + # get the host info in domain + LOGGER.debug("Get the conf by domain: {}.".format(domain)) + code_string = "get domain confs succeed" + host_ids = Format.get_hostid_list_by_domain(domain) + if not host_ids: + code_num = 404 + code_string = "The host currently controlled in the domain is empty. Please add host information to the " \ + "domain. " + return code_num, code_string, list() + + # get the managent conf in domain + man_conf_res_text = Format.get_manageconf_by_domain(domain) + manage_confs = man_conf_res_text.get("conf_files") + + if len(manage_confs) == 0: + code_num = 404 + code_string = "The configuration is not set in the current domain. Please add the configuration " \ + "information first. " + return code_num, code_string, list() + return code_num, code_string, manage_confs + + @staticmethod + def diff_mangeconf_with_realconf(domain, real_conf_res_text, manage_confs): + sync_status = {"domainName": domain, "hostStatus": []} + + from ragdoll.utils.object_parse import ObjectParse + + for d_real_conf in real_conf_res_text: + host_id = d_real_conf["hostID"] + host_sync_status = {"hostId": host_id, "syncStatus": []} + d_real_conf_base = d_real_conf["confBaseInfos"] + for d_conf in d_real_conf_base: + directory_conf_is_synced = {"file_path": "", "isSynced": "", "singleConf": []} + d_conf_path = d_conf["filePath"] + + object_parse = ObjectParse() + # get the conf type and model + conf_type, conf_model = Format.get_conf_type_model(d_conf_path, object_parse) + + Format.deal_conf_sync_status(conf_model, d_conf, d_conf_path, directory_conf_is_synced, + host_sync_status, manage_confs) + + if len(directory_conf_is_synced.get("singleConf")) > 0: + synced_flag = SYNCHRONIZED + for single_config in directory_conf_is_synced.get("singleConf"): + if single_config.get("singleIsSynced") == SYNCHRONIZED: + continue + else: + synced_flag = NOT_SYNCHRONIZE + directory_conf_is_synced["isSynced"] = synced_flag + host_sync_status.get("syncStatus").append(directory_conf_is_synced) + sync_status.get("hostStatus").append(host_sync_status) + return sync_status + + @staticmethod + def deal_conf_sync_status(conf_model, d_conf, d_conf_path, directory_conf_is_synced, host_sync_status, + manage_confs): + comp_res = "" + if d_conf_path in DIRECTORY_FILE_PATH_LIST: + confContents = json.loads(d_conf["confContents"]) + directory_conf_contents = "" + for d_man_conf in manage_confs: + d_man_conf_path = d_man_conf.get("file_path") + if d_man_conf_path != d_conf_path: + continue + else: + directory_conf_is_synced["file_path"] = d_conf_path + directory_conf_contents = d_man_conf.get("contents") + + directory_conf_contents_dict = json.loads(directory_conf_contents) + + for dir_conf_content_key, dir_conf_content_value in directory_conf_contents_dict.items(): + if dir_conf_content_key not in confContents.keys(): + single_conf = {"singleFilePath": dir_conf_content_key, "singleIsSynced": NOT_SYNCHRONIZE} + directory_conf_is_synced.get("singleConf").append(single_conf) + else: + dst_conf = confContents.get(dir_conf_content_key) + comp_res = conf_model.conf_compare(dir_conf_content_value, dst_conf) + single_conf = {"singleFilePath": dir_conf_content_key, "singleIsSynced": comp_res} + directory_conf_is_synced.get("singleConf").append(single_conf) + else: + for d_man_conf in manage_confs: + if d_man_conf.get("file_path").split(":")[-1] != d_conf_path: + continue + comp_res = conf_model.conf_compare(d_man_conf.get("contents"), d_conf["confContents"]) + conf_is_synced = {"file_path": d_conf_path, "isSynced": comp_res} + host_sync_status.get("syncStatus").append(conf_is_synced) + + @staticmethod + def convert_real_conf(conf_model, conf_type, conf_info, conf_path, parse): + # load yang model info + yang_info = parse._yang_modules.getModuleByFilePath(conf_path) + conf_model.load_yang_model(yang_info) + + # load conf info + if conf_type == "kv": + spacer_type = parse._yang_modules.getSpacerInModdule([yang_info]) + conf_model.read_conf(conf_info, spacer_type, yang_info) + else: + conf_model.read_conf(conf_info) + + @staticmethod + def deal_conf_sync_status_for_db(conf_model, d_conf, d_conf_path, directory_conf_is_synced, host_sync_status, + manage_confs): + comp_res = "" + if d_conf_path in DIRECTORY_FILE_PATH_LIST: + confContents = d_conf.get("conf_contents") + directory_conf_contents = "" + for d_man_conf in manage_confs: + d_man_conf_path = d_man_conf.get("file_path") + if d_man_conf_path != d_conf_path: + continue + else: + directory_conf_is_synced["file_path"] = d_conf_path + directory_conf_contents = d_man_conf.get("contents") + + directory_conf_contents_dict = json.loads(directory_conf_contents) + + for dir_conf_content_key, dir_conf_content_value in directory_conf_contents_dict.items(): + if dir_conf_content_key not in confContents.keys(): + single_conf = {"singleFilePath": dir_conf_content_key, "singleIsSynced": NOT_SYNCHRONIZE} + directory_conf_is_synced["singleConf"].append(single_conf) + else: + dst_conf = confContents.get(dir_conf_content_key) + comp_res = conf_model.conf_compare(dir_conf_content_value, dst_conf) + single_conf = {"singleFilePath": dir_conf_content_key, "singleIsSynced": comp_res} + directory_conf_is_synced["singleConf"].append(single_conf) + else: + for d_man_conf in manage_confs: + if d_man_conf.get("file_path").split(":")[-1] != d_conf_path: + continue + contents = d_man_conf.get("contents") + comp_res = conf_model.conf_compare(contents, json.dumps(d_conf.get("conf_contents"))) + conf_is_synced = {"file_path": d_conf_path, "isSynced": comp_res} + host_sync_status["syncStatus"].append(conf_is_synced) + + @staticmethod + def get_conf_type_model(d_conf_path, object_parse): + for dir_path in DIRECTORY_FILE_PATH_LIST: + if str(d_conf_path).find(dir_path) != -1: + conf_type = object_parse.get_conf_type_by_conf_path(dir_path) + conf_model = object_parse.create_conf_model_by_type(conf_type) + else: + conf_type = object_parse.get_conf_type_by_conf_path(d_conf_path) + conf_model = object_parse.create_conf_model_by_type(conf_type) + return conf_type, conf_model + + @staticmethod + def deal_sync_res(conf_tools, contents, file_path, host_id, host_sync_result, object_parse, access_token): + sync_conf_url = conf_tools.load_url_by_conf().get("sync_url") + headers = {"Content-Type": "application/json", "access_token": access_token} + if file_path in DIRECTORY_FILE_PATH_LIST: + conf_sync_res_list = [] + for directory_file_path, directory_content in json.loads(contents).items(): + content = object_parse.parse_json_to_conf(directory_file_path, directory_content) + # Configuration to the host + data = {"host_id": host_id, "file_path": directory_file_path, "content": content} + try: + sync_response = requests.put(sync_conf_url, data=json.dumps(data), headers=headers) + except requests.exceptions.RequestException as connect_ex: + LOGGER.error(f"An error occurred: {connect_ex}") + codeNum = 500 + codeString = "Failed to sync configuration, please check the interface of config/sync." + base_rsp = BaseResponse(codeNum, codeString) + return base_rsp, codeNum + resp_code = json.loads(sync_response.text).get('code') + resp = json.loads(sync_response.text).get('data').get('resp') + + if resp_code == "200" and resp.get('sync_result') is True: + conf_sync_res_list.append("SUCCESS") + else: + conf_sync_res_list.append("FAILED") + if "FAILED" in conf_sync_res_list: + conf_sync_res = ConfSyncedRes(file_path=file_path, result="FAILED") + else: + conf_sync_res = ConfSyncedRes(file_path=file_path, result="SUCCESS") + host_sync_result.sync_result.append(conf_sync_res) + else: + content = object_parse.parse_json_to_conf(file_path, contents) + # Configuration to the host + data = {"host_id": host_id, "file_path": file_path, "content": content} + sync_response = requests.put(sync_conf_url, data=json.dumps(data), headers=headers) + + resp_code = json.loads(sync_response.text).get('code') + resp = json.loads(sync_response.text).get('data').get('resp') + conf_sync_res = ConfSyncedRes(file_path=file_path, + result="") + if resp_code == "200" and resp.get('sync_result') is True: + conf_sync_res.result = "SUCCESS" + else: + conf_sync_res.result = "FAILED" + host_sync_result.sync_result.append(conf_sync_res) + + @staticmethod + def deal_batch_sync_res(conf_tools, exist_host, file_path_infos, access_token): + from ragdoll.utils.object_parse import ObjectParse + sync_conf_url = conf_tools.load_url_by_conf().get("batch_sync_url") + headers = {"Content-Type": "application/json", "access_token": access_token} + object_parse = ObjectParse() + codeNum = 200 + codeString = "sync config succeed " + # 组装参数 + sync_config_request = {"host_ids": exist_host, "file_path_infos": list()} + for file_path, contents in file_path_infos.items(): + if file_path in DIRECTORY_FILE_PATH_LIST: + for directory_file_path, directory_content in json.loads(contents).items(): + content = object_parse.parse_json_to_conf(directory_file_path, directory_content) + single_file_path_info = {"file_path": directory_file_path, "content": content} + sync_config_request["file_path_infos"].append(single_file_path_info) + else: + content = object_parse.parse_json_to_conf(file_path, contents) + single_file_path_info = {"file_path": file_path, "content": content} + sync_config_request["file_path_infos"].append(single_file_path_info) + # 调用zeus接口 + try: + sync_response = requests.put(sync_conf_url, data=json.dumps(sync_config_request), headers=headers) + except requests.exceptions.RequestException as connect_ex: + LOGGER.error(f"An error occurred: {connect_ex}") + codeNum = 500 + codeString = "Failed to sync configuration, please check the interface of config/sync." + return codeNum, codeString, [] + # 处理响应 + resp_code = json.loads(sync_response.text).get('code') + resp = json.loads(sync_response.text).get('data') + if resp_code != "200": + codeNum = 500 + codeString = f"Failed to sync configuration, reason is {json.loads(sync_response.text).get('message')}, " \ + f"please check the interface of config/sync. " + return codeNum, codeString, [] + + # 重新构建返回值,目录文件返回值重新构造 + sync_res = [] + for host_result in resp: + syncResult = [] + conf_sync_res_list = [] + sync_result_list = host_result.get("syncResult") + dir_name = "" + for single_result in sync_result_list: + dir_name = os.path.dirname(single_result.get("filePath")) + if dir_name in DIRECTORY_FILE_PATH_LIST and single_result.get("result") == "SUCCESS": + conf_sync_res_list.append("SUCCESS") + elif dir_name in DIRECTORY_FILE_PATH_LIST and single_result.get("result") == "FAIL": + conf_sync_res_list.append("FAILED") + else: + syncResult.append(single_result) + if conf_sync_res_list: + if "FAILED" in conf_sync_res_list: + directory_sync_result = {"filePath": dir_name, "result": "FAILED"} + else: + directory_sync_result = {"filePath": dir_name, "result": "SUCCESS"} + syncResult.append(directory_sync_result) + single_host_sync_result = {"host_id": host_result.get("host_id"), "syncResult": syncResult} + sync_res.append(single_host_sync_result) + return codeNum, codeString, sync_res + + @staticmethod + def addHostSyncStatus(conf_tools, domain, host_infos): + add_host_sync_status_url = conf_tools.load_url_by_conf().get("add_host_sync_status_url") + headers = {"Content-Type": "application/json"} + # 数据入库 + try: + for host in host_infos: + contained_flag = Format.isContainedHostIdInOtherDomain(host.get("hostId")) + if contained_flag: + continue + host_sync_status = { + "host_id": host.get("hostId"), + "host_ip": host.get("ip"), + "domain_name": domain, + "sync_status": 0 + } + add_host_sync_status_response = requests.post(add_host_sync_status_url, + data=json.dumps(host_sync_status), headers=headers) + resp_code = json.loads(add_host_sync_status_response.text).get('code') + if resp_code != "200": + LOGGER.error( + "Failed to add host sync status, please check the interface of /manage/host/sync/status/add.") + except requests.exceptions.RequestException as connect_ex: + LOGGER.error(f"An error occurred: {connect_ex}") + LOGGER.error("Failed to add host sync status, please check the interface of /manage/host/sync/status/add.") + + @staticmethod + def deleteHostSyncStatus(conf_tools, domain, hostInfos): + delete_host_sync_status_url = conf_tools.load_url_by_conf().get("delete_host_sync_status_url") + headers = {"Content-Type": "application/json"} + # 数据入库 + try: + for host in hostInfos: + delete_host_sync_status = { + "host_id": host.get("hostId"), + "domain_name": domain + } + delete_host_sync_status_response = requests.post(delete_host_sync_status_url, + data=json.dumps(delete_host_sync_status), + headers=headers) + resp_code = json.loads(delete_host_sync_status_response.text).get('code') + if resp_code != "200": + LOGGER.error( + "Failed to delete host sync status, please check the interface of " + "/manage/host/sync/status/delete.") + except requests.exceptions.RequestException as connect_ex: + LOGGER.error(f"An error occurred: {connect_ex}") + LOGGER.error( + "Failed to delete host sync status, please check the interface of " + "/manage/host/sync/status/delete.") + + @staticmethod + def diff_mangeconf_with_realconf_for_db(domain, real_conf_res_text, manage_confs): + sync_status = {"domainName": domain, "hostStatus": []} + from ragdoll.utils.object_parse import ObjectParse + + for d_real_conf in real_conf_res_text: + host_id = d_real_conf.get('host_id') + host_sync_status = {"hostId": host_id, "syncStatus": []} + d_real_conf_base = d_real_conf.get('conf_base_infos') + for d_conf in d_real_conf_base: + directory_conf_is_synced = {"file_path": "", "isSynced": "", "singleConf": []} + d_conf_path = d_conf.get('file_path') + object_parse = ObjectParse() + # get the conf type and model + conf_type, conf_model = Format.get_conf_type_model(d_conf_path, object_parse) + Format.deal_conf_sync_status_for_db(conf_model, d_conf, d_conf_path, directory_conf_is_synced, + host_sync_status, manage_confs) + + if len(directory_conf_is_synced["singleConf"]) > 0: + synced_flag = SYNCHRONIZED + for single_config in directory_conf_is_synced["singleConf"]: + if single_config["singleIsSynced"] == SYNCHRONIZED: + continue + else: + synced_flag = NOT_SYNCHRONIZE + directory_conf_is_synced["isSynced"] = synced_flag + host_sync_status["syncStatus"].append(directory_conf_is_synced) + sync_status.get("hostStatus").append(host_sync_status) + return sync_status + + @staticmethod + def deal_expected_confs_resp(expected_confs_resp_list): + """" + deal the excepted confs resp. + + Args: + expected_confs_resp_list (list): e.g + [ + { + "domainName": "xx" + "confBaseInfos": [ + { + "filePath": "xx", + "expectedContents": "xxx" + } + ] + } + ] + Returns: + dict: e.g + { + "aops": [ + { + "file_path": "xxx", + "contents": "xxx" + } + ] + } + """ + # 处理expected_confs_resp,将其处理成[{"file_path": "xxx", "contents": "xxx"}], 每个domain都有一个manage_confs + expected_confs_resp_dict = {} + for expected_confs_resp in expected_confs_resp_list: + manage_confs = [] + domain_name = expected_confs_resp["domainName"] + confBaseInfos = expected_confs_resp["confBaseInfos"] + for singleConfBaseInfo in confBaseInfos: + file_path = singleConfBaseInfo["filePath"] + contents = singleConfBaseInfo["expectedContents"] + single_manage_conf = {"file_path": file_path, "contents": contents} + manage_confs.append(single_manage_conf) + expected_confs_resp_dict[domain_name] = manage_confs + return expected_confs_resp_dict + + @staticmethod + def deal_domain_result(domain_result): + """" + deal the domain result. + + Args: + domain_result (object): e.g + { + "aops": { + "1": [ + { + "filePath": "xxx", + "contents": "xxxx" + } + ] + } + } + Returns: + dict: e.g + { + "aops": [ + { + "domain_name": "xxx", + "host_id": 1, + "conf_base_infos": [ + { + "file_path": "xxx", + "conf_contents": {} or [] + } + ] + } + ] + } + """ + # 处理domain_result,将其处理成[{"domain_name": "aops","host_id": 7, "conf_base_infos": [{"conf_contents": "xxx", "file_path": "xxx"}]}] + from ragdoll.utils.object_parse import ObjectParse + real_conf_res_text_dict = {} + parse = ObjectParse() + for domain, host_infos in domain_result.items(): + real_conf_res_text_list = [] + for host_id, confs in host_infos.items(): + signal_host_infos = {"domain_name": domain, "host_id": int(host_id), "conf_base_infos": []} + for conf in confs: + conf_path = conf["filePath"] + conf_info = conf["contents"] + conf_type = parse.get_conf_type_by_conf_path(conf_path) + if not conf_type: + return + # create conf model + conf_model = parse.create_conf_model_by_type(conf_type) + # 转换解析 + if conf_path not in DIRECTORY_FILE_PATH_LIST: + Format.convert_real_conf(conf_model, conf_type, conf_info, conf_path, parse) + else: + pam_res_infos = [] + for path, content in json.loads(conf_info).items(): + signal_res_info = {"path": path, "content": content} + pam_res_infos.append(signal_res_info) + Format.convert_real_conf(conf_model, conf_type, pam_res_infos, conf_path, parse) + signal_conf = {"file_path": conf["filePath"], "conf_contents": conf_model.conf} + signal_host_infos["conf_base_infos"].append(signal_conf) + real_conf_res_text_list.append(signal_host_infos) + real_conf_res_text_dict[domain] = real_conf_res_text_list + return real_conf_res_text_dict + + @staticmethod + def add_domain_conf_trace_flag(params, successDomain, tempDomainName): + # 对successDomain成功的domain添加文件监控开关、告警开关 + if len(successDomain) > 0: + from vulcanus.database.proxy import RedisProxy + # 文件监控开关 + conf_change_flag = params.get("conf_change_flag") + if conf_change_flag: + RedisProxy.redis_connect.set(tempDomainName + "_conf_change", 1) + else: + RedisProxy.redis_connect.set(tempDomainName + "_conf_change", 0) + # 告警开关 + report_flag = params.get("report_flag") + if report_flag: + RedisProxy.redis_connect.set(tempDomainName + "_report", 1) + else: + RedisProxy.redis_connect.set(tempDomainName + "_report", 0) + + @staticmethod + def uninstall_trace(access_token, host_ids, domain): + from ragdoll.utils.conf_tools import ConfTools + conf_tools = ConfTools() + conf_trace_mgmt_url = conf_tools.load_url_by_conf().get("conf_trace_mgmt_url") + headers = {"Content-Type": "application/json", "access_token": access_token} + # 数据入库 + try: + conf_trace_mgmt_data = { + "host_ids": host_ids, + "action": "stop", + "domain_name": domain + } + conf_trace_mgmt_response = requests.put(conf_trace_mgmt_url, + data=json.dumps(conf_trace_mgmt_data), headers=headers) + resp_code = json.loads(conf_trace_mgmt_response.text).get('code') + if resp_code != "200": + LOGGER.error( + "Failed to conf trace mgmt, please check the interface of /conftrace/mgmt.") + except requests.exceptions.RequestException as connect_ex: + LOGGER.error(f"An error occurred: {connect_ex}") + LOGGER.error( + "Failed to conf trace mgmt, please check the interface of /conftrace/mgmt.") + + @staticmethod + def clear_all_domain_data(access_token, domainName, successDomain, host_ids): + # 删除业务域,对successDomain成功的业务域进行redis的key值清理,以及domain下的主机进行agith的清理 + if len(successDomain) > 0: + from vulcanus.database.proxy import RedisProxy + # 1.清理redis key值 + RedisProxy.redis_connect.delete(domainName + "_conf_change") + RedisProxy.redis_connect.delete(domainName + "_report") + # 2.清理数据库数据,以避免再次添加业务域的时候还能看到以往的记录 + Format.delete_conf_trace_infos(access_token, [], domainName) + # 3.清理domain下面的host sync记录 + Format.delete_host_conf_sync_status(access_token, domainName, host_ids) + + @staticmethod + def get_conf_change_flag(domain): + from vulcanus.database.proxy import RedisProxy + domain_conf_change = RedisProxy.redis_connect.get(domain + "_conf_change") + return domain_conf_change + + @staticmethod + def install_update_agith(access_token, domain, host_ids): + # 针对successHost 添加成功的host, 安装agith并启动agith,如果当前业务域有配置,配置agith,如果没有就不配置 + install_resp_code = "200" + # 获取domain的文件监控开关 + domain_conf_change = Format.get_conf_change_flag(domain) + conf_files_list = Format.get_conf_files_list(domain, access_token) + if len(host_ids) > 0 and int(domain_conf_change) == 1 and len(conf_files_list) > 0: + # 安装并启动agith + from ragdoll.utils.conf_tools import ConfTools + conf_tools = ConfTools() + conf_trace_mgmt_url = conf_tools.load_url_by_conf().get("conf_trace_mgmt_url") + headers = {"Content-Type": "application/json", "access_token": access_token} + try: + conf_trace_mgmt_data = { + "domain_name": domain, + "host_ids": host_ids, + "action": "start", + "conf_files": conf_files_list + } + conf_trace_mgmt_response = requests.put(conf_trace_mgmt_url, + data=json.dumps(conf_trace_mgmt_data), headers=headers) + install_resp_code = json.loads(conf_trace_mgmt_response.text).get('code') + LOGGER.info(f"install_resp_code is {install_resp_code}") + if install_resp_code != "200": + LOGGER.error( + "Failed to conf trace mgmt, please check the interface of /conftrace/mgmt.") + except requests.exceptions.RequestException as connect_ex: + LOGGER.error(f"An error occurred: {connect_ex}") + LOGGER.error( + "Failed to conf trace mgmt, please check the interface of /conftrace/mgmt.") + + # 根据业务有是否有配置,有配置并且agith启动成功情况下进行agith的配置 + conf_files_list = Format.get_conf_files_list(domain, access_token) + if len(conf_files_list) > 0 and install_resp_code == "200": + Format.update_agith_conf(conf_files_list, conf_trace_mgmt_url, headers, host_ids, domain) + + @staticmethod + def uninstall_hosts_agith(access_token, containedInHost, domain): + # 根据containedInHost 停止agith服务,删除agith,删除redis key值 + if len(containedInHost) > 0: + # 1.根据containedInHost 停止agith服务,删除agith + from vulcanus.database.proxy import RedisProxy + Format.uninstall_trace(access_token, containedInHost, domain) + # 2.清理数据库数据,以避免再次添加业务域的时候还能看到以往的记录 + Format.delete_conf_trace_infos(access_token, containedInHost, domain) + # 3.清理host sync记录 + Format.delete_host_conf_sync_status(access_token, domain, containedInHost) + + @staticmethod + def delete_conf_trace_infos(access_token, containedInHost, domain): + from ragdoll.utils.conf_tools import ConfTools + conf_tools = ConfTools() + conf_trace_delete_url = conf_tools.load_url_by_conf().get("conf_trace_delete_url") + headers = {"Content-Type": "application/json", "access_token": access_token} + # 数据入库 + try: + conf_trace_delete_data = { + "domain_name": domain, + "host_ids": containedInHost + } + conf_trace_delete_response = requests.post(conf_trace_delete_url, + data=json.dumps(conf_trace_delete_data), headers=headers) + resp_code = json.loads(conf_trace_delete_response.text).get('code') + if resp_code != "200": + LOGGER.error( + "Failed to delete trace info, please check the interface of /conftrace/delete.") + except requests.exceptions.RequestException as connect_ex: + LOGGER.error(f"An error occurred: {connect_ex}") + LOGGER.error( + "Failed to delete trace info, please check the interface of /conftrace/delete.") + + @staticmethod + def get_conf_files_list(domain, access_token): + conf_files_list = [] + conf_files = Format.get_manageconf_by_domain(domain).get("conf_files") + for conf_file in conf_files: + if conf_file.get("file_path") in DIRECTORY_FILE_PATH_LIST: + # 获取文件夹下面的配置文件 + from ragdoll.utils.object_parse import ObjectParse + object_parse = ObjectParse() + d_conf = {"filePath": conf_file.get("file_path")} + host_ids = Format.get_hostid_list_by_domain(domain) + if len(host_ids): + _, _, file_paths = object_parse.get_directory_files(d_conf, host_ids[0], access_token) + if len(file_paths) > 0: + conf_files_list.extend(file_paths) + else: + conf_files_list.append(conf_file.get("file_path")) + return conf_files_list + + @staticmethod + def update_agith(access_token, conf_files_list, domain): + # 根据containedInHost,监控开关 停止agith服务,删除agith,删除redis key值 + domain_conf_change_flag = Format.get_conf_change_flag(domain) + if int(domain_conf_change_flag) == 1: + from ragdoll.utils.conf_tools import ConfTools + # 根据containedInHost 停止agith服务,删除agith + conf_tools = ConfTools() + conf_trace_mgmt_url = conf_tools.load_url_by_conf().get("conf_trace_mgmt_url") + headers = {"Content-Type": "application/json", "access_token": access_token} + # 获取domain下的主机id + host_ids = Format.get_hostid_list_by_domain(domain) + # 数据入库 + if len(host_ids) > 0: + Format.update_agith_conf(conf_files_list, conf_trace_mgmt_url, headers, host_ids, domain) + + @staticmethod + def update_agith_conf(conf_files_list, conf_trace_mgmt_url, headers, host_ids, domain_name): + try: + conf_trace_mgmt_data = { + "host_ids": host_ids, + "action": "update", + "conf_files": conf_files_list, + "domain_name": domain_name + } + conf_trace_mgmt_response = requests.put(conf_trace_mgmt_url, + data=json.dumps(conf_trace_mgmt_data), headers=headers) + resp_code = json.loads(conf_trace_mgmt_response.text).get('code') + if resp_code != "200": + LOGGER.error( + "Failed to conf trace mgmt, please check the interface of /conftrace/mgmt.") + except requests.exceptions.RequestException as connect_ex: + LOGGER.error(f"An error occurred: {connect_ex}") + LOGGER.error( + "Failed to conf trace mgmt, please check the interface of /conftrace/mgmt.") + + @staticmethod + def delete_host_conf_sync_status(access_token, domainName, hostIds): + try: + from ragdoll.utils.conf_tools import ConfTools + conf_tools = ConfTools() + delete_all_host_sync_status_url = conf_tools.load_url_by_conf().get("delete_all_host_sync_status_url") + headers = {"Content-Type": "application/json", "access_token": access_token} + delete_host_conf_sync_status_data = { + "host_ids": hostIds, + "domain_name": domainName + } + delete_sync_status_response = requests.post(delete_all_host_sync_status_url, + data=json.dumps(delete_host_conf_sync_status_data), + headers=headers) + resp_code = json.loads(delete_sync_status_response.text).get('code') + if resp_code != "200": + LOGGER.error( + "Failed to delete sync status, please check the interface of /manage/all/host/sync/status/delete.") + except requests.exceptions.RequestException as connect_ex: + LOGGER.error(f"An error occurred: {connect_ex}") + LOGGER.error( + "Failed to delete sync status, please check the interface of /manage/all/host/sync/status/delete.") diff --git a/ragdoll/utils/git_tools.py b/ragdoll/utils/git_tools.py index 049d450..f6200af 100644 --- a/ragdoll/utils/git_tools.py +++ b/ragdoll/utils/git_tools.py @@ -6,8 +6,7 @@ import ast from ragdoll.const.conf_handler_const import CONFIG from ragdoll.log.log import LOGGER -from ragdoll.models.git_log_message import GitLogMessage -from ragdoll.controllers.format import Format +from ragdoll.utils.format import Format class GitTools(object): @@ -18,7 +17,6 @@ class GitTools(object): self._target_dir = self.load_git_dir() def load_git_dir(self): - cf = configparser.ConfigParser() cf = configparser.ConfigParser() if os.path.exists(CONFIG): cf.read(CONFIG, encoding="utf-8") @@ -93,16 +91,14 @@ class GitTools(object): # Execute the shell command and return the execution node and output def run_shell_return_output(self, shell): cmd = subprocess.Popen(shell, stdout=subprocess.PIPE, shell=True) - LOGGER.debug("################# shell cmd ################") LOGGER.debug("subprocess.Popen({shell}, stdout=subprocess.PIPE, shell=True)".format(shell=shell)) - LOGGER.debug("################# shell cmd end ################") output, err = cmd.communicate() return output def makeGitMessage(self, path, logMessage): if len(logMessage) == 0: return "the logMessage is null" - LOGGER.debug("AAAA path is : {}".format(path)) + LOGGER.debug("path is : {}".format(path)) cwdDir = os.getcwd() os.chdir(self._target_dir) LOGGER.debug(os.getcwd()) @@ -113,26 +109,26 @@ class GitTools(object): count = logMessage.count("commit") lines = logMessage.split('\n') + LOGGER.debug("count is : {}".format(count)) for index in range(0, count): - LOGGER.debug("AAAAAAAAAAAAAAA count is : {}".format(index)) - gitMessage = GitLogMessage() + gitMessage = {} for temp in range(0, singleLogLen): line = lines[index * singleLogLen + temp] value = line.split(" ", 1)[-1] if "commit" in line: - gitMessage.change_id = value + gitMessage["changeId"] = value if "Author" in line: - gitMessage.author = value + gitMessage["author"] = value if "Date" in line: - gitMessage._date = value[2:] - gitMessage.change_reason = lines[index * singleLogLen + 4] + gitMessage["date"] = value[2:] + gitMessage["changeReason"] = lines[index * singleLogLen + 4] LOGGER.debug("gitMessage is : {}".format(gitMessage)) gitLogMessageList.append(gitMessage) LOGGER.debug("################# gitMessage start ################") if count == 1: last_message = gitLogMessageList[0] - last_message.post_value = Format.get_file_content_by_read(path) + last_message["postValue"] = Format.get_file_content_by_read(path) os.chdir(cwdDir) return gitLogMessageList @@ -140,13 +136,13 @@ class GitTools(object): LOGGER.debug("index is : {}".format(index)) message = gitLogMessageList[index] next_message = gitLogMessageList[index + 1] - message.post_value = Format.get_file_content_by_read(path) - shell = ['git checkout {}'.format(next_message.change_id)] + message["postValue"] = Format.get_file_content_by_read(path) + shell = ['git checkout {}'.format(next_message["changeId"])] output = self.run_shell_return_output(shell) - message.pre_value = Format.get_file_content_by_read(path) + message["preValue"] = Format.get_file_content_by_read(path) # the last changlog first_message = gitLogMessageList[count - 1] - first_message.post_value = Format.get_file_content_by_read(path) + first_message["postValue"] = Format.get_file_content_by_read(path) LOGGER.debug("################# gitMessage end ################") os.chdir(cwdDir) diff --git a/ragdoll/utils/host_tools.py b/ragdoll/utils/host_tools.py index c467994..f561523 100644 --- a/ragdoll/utils/host_tools.py +++ b/ragdoll/utils/host_tools.py @@ -75,11 +75,9 @@ class HostTools(object): }] """ res = [] + LOGGER.debug("The domainHost is : {}".format(domainHost)) for d_host in domainHost: hostId = int(d_host.get('host_id')) - LOGGER.debug("the host Id is : {}".format(hostId)) - d_host = {} - d_host["hostId"] = hostId res.append(hostId) return res diff --git a/ragdoll/utils/object_parse.py b/ragdoll/utils/object_parse.py index 6cc4564..719dc68 100644 --- a/ragdoll/utils/object_parse.py +++ b/ragdoll/utils/object_parse.py @@ -174,14 +174,14 @@ class ObjectParse(object): return conf_info - def get_directory_files(self, d_conf, host_id): + def get_directory_files(self, d_conf, host_id, access_token): file_paths = list() conf_tools = ConfTools() file_directory = dict() - file_directory['file_directory'] = d_conf.file_path + file_directory['file_directory'] = d_conf["filePath"] file_directory['host_id'] = host_id url = conf_tools.load_url_by_conf().get("object_file_url") - headers = {"Content-Type": "application/json"} + headers = {"Content-Type": "application/json", "access_token": access_token} try: response = requests.post(url, data=json.dumps(file_directory), headers=headers) except requests.exceptions.RequestException as connect_ex: @@ -191,7 +191,7 @@ class ObjectParse(object): base_rsp = BaseResponse(codeNum, codeString) return base_rsp, codeNum response_code = json.loads(response.text).get("code") - if response_code == None: + if response_code is None: codeNum = 500 codeString = "Failed to obtain the actual configuration, please check the interface of conf/objectFile." return codeNum, codeString, file_paths @@ -207,5 +207,5 @@ class ObjectParse(object): return codeNum, codeString, file_paths codeNum = 200 codeString = "Success get pam.d file paths." - file_paths = file_path_reps.get('resp').get('object_file_paths') + file_paths = file_path_reps.get('object_file_paths') return codeNum, codeString, file_paths diff --git a/ragdoll/utils/prepare.py b/ragdoll/utils/prepare.py index 4e61489..132ea7b 100644 --- a/ragdoll/utils/prepare.py +++ b/ragdoll/utils/prepare.py @@ -15,7 +15,7 @@ class Prepare(object): def target_dir(self, target_dir): self._target_dir = target_dir - def mdkir_git_warehose(self, username, useremail): + def mkdir_git_warehose(self, username, useremail): res = True LOGGER.debug("self._target_dir is : {}".format(self._target_dir)) if os.path.exists(self._target_dir): @@ -26,7 +26,7 @@ class Prepare(object): git_tools = GitTools(self._target_dir) mkdir_code = git_tools.run_shell_return_code(cmd1) git_code = self.git_init(username, useremail) - if mkdir_code != 0: + if mkdir_code != 0 or not git_code: res = False return res diff --git a/ragdoll/utils/yang_module.py b/ragdoll/utils/yang_module.py index de0d9b5..f0f21ab 100644 --- a/ragdoll/utils/yang_module.py +++ b/ragdoll/utils/yang_module.py @@ -1,7 +1,5 @@ import libyang import os -import sys -import importlib import operator from ragdoll.log.log import LOGGER @@ -95,10 +93,6 @@ class YangModule(object): if files_tail != "yang": continue modulePath = os.path.join(self._yang_dir, d_file) - # grammar_res = self.check_yang_grammar(modulePath) - # print("grammar_res is : {}".format(grammar_res)) - # if not grammar_res: - # continue fo = open(modulePath, 'r+') module = self._ctx.parse_module_file(fo) module_list.append(module) @@ -183,7 +177,6 @@ class YangModule(object): continue xpath.append(path) - # print("xpath is : {}".format(xpath)) return xpath def getFeatureInModule(self, modules): @@ -250,9 +243,8 @@ class YangModule(object): } """ res = {} + LOGGER.debug("modules are : {}".format(modules)) for d_mod in modules: - LOGGER.debug("d_mod is : {}".format(d_mod)) - LOGGER.debug("d_mod's type is : {}".format(type(d_mod))) feature_list = self.getFeatureInModule(d_mod) module_name = d_mod.name() xpath = "" diff --git a/service/gala-ragdoll.service b/service/gala-ragdoll.service index 0fbaf2e..2dfc4f1 100644 --- a/service/gala-ragdoll.service +++ b/service/gala-ragdoll.service @@ -3,8 +3,9 @@ Description=a-ops gala ragdoll service After=network.target [Service] -Type=exec -ExecStart=/usr/bin/ragdoll +Type=forking +ExecStart=/usr/bin/ragdoll start +ExecStop=/usr/bin/ragdoll stop Restart=on-failure RestartSec=1 RemainAfterExit=yes diff --git a/service/ragdoll b/service/ragdoll new file mode 100644 index 0000000..92a5b7c --- /dev/null +++ b/service/ragdoll @@ -0,0 +1,15 @@ +#!/bin/bash +. /usr/bin/aops-vulcanus + +MANAGER_CONSTANT="ragdoll" +MANAGER_CONFIG_FILE=/etc/ragdoll/gala-ragdoll.conf + +function main() { + if [ "${OPERATION}" = "start" ]; then + create_config_file "${MANAGER_CONFIG_FILE}" "ragdoll" + fi + start_or_stop_service "${MANAGER_CONSTANT}" + exit $? +} + +main diff --git a/service/ragdoll-filetrace b/service/ragdoll-filetrace new file mode 100755 index 0000000..cf1a0bd --- /dev/null +++ b/service/ragdoll-filetrace @@ -0,0 +1,782 @@ +#!/usr/bin/python3 +from bpfcc import BPF +import json +import os +import requests +import threading +import psutil +import ctypes + +import logging +import logging.handlers +logger = logging.getLogger(__name__) +syslog_handler = logging.handlers.SysLogHandler(address='/dev/log') +syslog_handler.setLevel(logging.DEBUG) +syslog_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) +logger.addHandler(syslog_handler) + + +bpf_text=""" +//#define randomized_struct_fields_start struct { +//#define randomized_struct_fields_end }; + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_DEPTH 4 +#define MAX_DIRNAME_LEN 16 +#define MAX_FILENAME_LEN 32 +#define MAX_CMD_LEN 32 +#define MAX_TASK_COMM_LEN 32 + +#define ARGSIZE 16 +#define MAX_ARGS 4 + + +struct pinfo_t { + char comm[MAX_CMD_LEN]; + unsigned int ppid; + char arg1[ARGSIZE]; + char arg2[ARGSIZE]; + char arg3[ARGSIZE]; + char arg4[ARGSIZE]; + +}; + + +struct event { + unsigned int pid; + unsigned int ppid; + char cmd[16]; + char pcmd[16]; + unsigned long i_ino; + char filename[MAX_FILENAME_LEN]; + char dir1[MAX_DIRNAME_LEN]; + char dir2[MAX_DIRNAME_LEN]; + char dir3[MAX_DIRNAME_LEN]; + char dir4[MAX_DIRNAME_LEN]; + + char oldfilename[MAX_FILENAME_LEN]; + char odir1[MAX_DIRNAME_LEN]; + char odir2[MAX_DIRNAME_LEN]; + char odir3[MAX_DIRNAME_LEN]; + char odir4[MAX_DIRNAME_LEN]; + int flag; +}; + +BPF_HASH(exec_map, u32, struct pinfo_t); +BPF_PERF_OUTPUT(events); + +//for rm command +TRACEPOINT_PROBE(syscalls, sys_enter_unlinkat) { + struct task_struct* t; + struct task_struct* p; + + struct event e = {}; + e.flag = 4; + + t = (struct task_struct*)bpf_get_current_task(); + bpf_probe_read(&e.pid, sizeof(e.pid), &t->tgid); + bpf_probe_read(&e.cmd, sizeof(e.cmd), &t->comm); + + bpf_probe_read(&p, sizeof(p), &t->real_parent); + bpf_probe_read(&e.ppid, sizeof(e.ppid), &p->tgid); + bpf_probe_read(&e.pcmd, sizeof(e.pcmd), &p->comm); + + bpf_probe_read_str((void*)&e.filename, sizeof(e.filename), (const void*)args->pathname); + bpf_trace_printk("Process calling sys_enter_rename newfilename:%s \\n", e.filename); + events.perf_submit((struct pt_regs *)args, &e, sizeof(e)); + return 0; +} + +//for copy command +TRACEPOINT_PROBE(syscalls, sys_enter_copy_file_range) { + + struct task_struct* t; + struct task_struct* p; + struct files_struct* f; + struct fdtable* fdt; + struct file** fdd; + struct file* file; + struct path path; + struct dentry* dentry; + struct inode* inode; + struct qstr pathname; + umode_t mode; + unsigned long i_ino; + + //char filename[128]; + struct event e = {}; + e.flag = 2; + + int fd =args->fd_out; + t = (struct task_struct*)bpf_get_current_task(); + if(t){ + bpf_probe_read(&f, sizeof(f), &(t->files)); + bpf_probe_read(&fdt, sizeof(fdt), (void*)&f->fdt); + int ret = bpf_probe_read(&fdd, sizeof(fdd), (void*)&fdt->fd); + if (ret) { + //bpf_trace_printk("bpf_probe_read failed: %d\\n", ret); + return 0; + } + bpf_probe_read(&file, sizeof(file), (void*)&fdd[fd]); + + //file file ppid pcmd + bpf_probe_read(&p, sizeof(p), &t->real_parent); + bpf_probe_read(&e.ppid, sizeof(e.ppid), &p->tgid); + bpf_probe_read(&e.pcmd, sizeof(e.pcmd), &p->comm); + + //fill file ino + bpf_probe_read(&inode, sizeof(inode), &file->f_inode); + bpf_probe_read(&e.i_ino, sizeof(i_ino), &inode->i_ino); + bpf_probe_read(&mode, sizeof(mode), &inode->i_mode); + if(!S_ISREG(mode)){ + return 0; + } + + //file process info + bpf_probe_read(&e.pid, sizeof(e.pid), &t->tgid); + bpf_probe_read(&e.cmd, sizeof(e.cmd), &t->comm); + + //get filename + bpf_probe_read(&path, sizeof(path), (const void*)&file->f_path); + bpf_probe_read(&dentry, sizeof(dentry), (const void*)&path.dentry); + bpf_probe_read(&pathname, sizeof(pathname), (const void*)&dentry->d_name); + + struct dentry* d_parent; + + #pragma unroll + for (int i = 0; i < MAX_DEPTH; i++) { + bpf_probe_read(&d_parent, sizeof(d_parent), (const void*)&dentry->d_parent); + if (d_parent == dentry) { + break; + } + //fix me + if(i == 0){ + bpf_probe_read(&e.dir1, sizeof(d_parent->d_iname), (const void*)&d_parent->d_iname); + }else if(i == 1){ + bpf_probe_read(&e.dir2, sizeof(d_parent->d_iname), (const void*)&d_parent->d_iname); + }else if(i == 2){ + bpf_probe_read(&e.dir3, sizeof(d_parent->d_iname), (const void*)&d_parent->d_iname); + }else if(i == 3){ + bpf_probe_read(&e.dir4, sizeof(d_parent->d_iname), (const void*)&d_parent->d_iname); + } + + dentry = d_parent; + } + bpf_probe_read_str((void*)&e.filename, sizeof(e.filename), (const void*)pathname.name); + events.perf_submit((struct pt_regs *)args, &e, sizeof(e)); + return 0; + } + return 0; +} +//for sed command +TRACEPOINT_PROBE(syscalls, sys_enter_rename) { + struct task_struct* t; + struct task_struct* p; + + struct event e = {}; + e.flag = 1; + + t = (struct task_struct*)bpf_get_current_task(); + bpf_probe_read(&e.pid, sizeof(e.pid), &t->tgid); + bpf_probe_read(&e.cmd, sizeof(e.cmd), &t->comm); + + bpf_probe_read(&p, sizeof(p), &t->real_parent); + bpf_probe_read(&e.ppid, sizeof(e.ppid), &p->tgid); + bpf_probe_read(&e.pcmd, sizeof(e.pcmd), &p->comm); + + bpf_probe_read_str((void*)&e.filename, sizeof(e.filename), (const void*)args->newname); + //bpf_trace_printk("Process calling sys_enter_rename newfilename:%s \\n", e.filename); + events.perf_submit((struct pt_regs *)args, &e, sizeof(e)); + return 0; +} + +TRACEPOINT_PROBE(syscalls, sys_enter_renameat) { + char comm[TASK_COMM_LEN]; + bpf_get_current_comm(&comm, sizeof(comm)); + bpf_trace_printk("Process %s is calling renameat\\n", comm); + return 0; +} + +//for move command +TRACEPOINT_PROBE(syscalls, sys_enter_renameat2) { + struct event e = {}; + e.flag = 3; + + struct task_struct* t; + struct task_struct* p; + + t = (struct task_struct*)bpf_get_current_task(); + bpf_probe_read(&e.pid, sizeof(e.pid), &t->tgid); + bpf_probe_read(&e.cmd, sizeof(e.cmd), &t->comm); + + bpf_probe_read(&p, sizeof(p), &t->real_parent); + bpf_probe_read(&e.ppid, sizeof(e.ppid), &p->tgid); + bpf_probe_read(&e.pcmd, sizeof(e.pcmd), &p->comm); + + bpf_probe_read_str((void*)&e.filename, sizeof(e.filename), (const void*)args->newname); + bpf_probe_read_str((void*)&e.oldfilename, sizeof(e.oldfilename), (const void*)args->oldname); + + struct fs_struct *fs; + struct path pwd; + bpf_probe_read(&fs, sizeof(fs), (const void*)&t->fs); + bpf_probe_read(&pwd, sizeof(pwd), (const void*)&fs->pwd); + + struct dentry* dentry; + bpf_probe_read(&dentry, sizeof(dentry), (const void*)&pwd.dentry); + + struct dentry* d_parent; + + int olddfd = args->olddfd; + int newdfd = args->newdfd; + if (newdfd == AT_FDCWD) { + #pragma unroll + for (int i = 0; i < MAX_DEPTH; i++) { + bpf_probe_read(&d_parent, sizeof(d_parent), (const void*)&dentry->d_parent); + if (d_parent == dentry) { + break; + } + //fix me + if(i == 0){ + bpf_probe_read(&e.dir1, sizeof(d_parent->d_iname), (const void*)&d_parent->d_iname); + }else if(i == 1){ + bpf_probe_read(&e.dir2, sizeof(d_parent->d_iname), (const void*)&d_parent->d_iname); + }else if(i == 2){ + bpf_probe_read(&e.dir3, sizeof(d_parent->d_iname), (const void*)&d_parent->d_iname); + }else if(i == 3){ + bpf_probe_read(&e.dir4, sizeof(d_parent->d_iname), (const void*)&d_parent->d_iname); + } + dentry = d_parent; + } + bpf_trace_printk("newfilename relative to CWD: %s\\n", e.filename); + } + + if (olddfd == AT_FDCWD) { + bpf_trace_printk("oldfilename relative to CWD: %s\\n", e.oldfilename); + } + + events.perf_submit((struct pt_regs *)args, &e, sizeof(e)); + + return 0; +} + +TRACEPOINT_PROBE(syscalls, sys_exit_execve) { + u64 tgid_pid; + u32 tgid, pid; + tgid_pid = bpf_get_current_pid_tgid(); + tgid = tgid_pid >> 32; + pid = (u32)tgid_pid; + exec_map.delete(&pid); + return 0; +} + +TRACEPOINT_PROBE(syscalls, sys_enter_execve) { + //args, args->filename, args->argv, args->envp + struct pinfo_t p = {}; + + u64 tgid_pid; + u32 tgid, pid; + tgid_pid = bpf_get_current_pid_tgid(); + tgid = tgid_pid >> 32; + pid = (u32)tgid_pid; + + struct task_struct *t = (struct task_struct *)bpf_get_current_task(); + struct task_struct *pp; + bpf_probe_read(&pp, sizeof(pp), &t->real_parent); + bpf_probe_read(&p.ppid, sizeof(p.ppid), &pp->tgid); + + bpf_get_current_comm(&p.comm, sizeof(p.comm)); + + //int i; + //for (i = 1; i <= MAX_ARGS; i++) { + // const char __user *argp; + // bpf_probe_read_user(&argp, sizeof(argp), &args->argv[i]); + // if (!argp) { + // break; + // } + // if(i == 1){ + // bpf_probe_read_user_str(&p.arg1, sizeof(p.args1), argp); + // }else if(i == 2){ + // bpf_probe_read_user_str(&p.arg2, sizeof(p.args2), argp); + // }else if(i == 3){ + // bpf_probe_read_user_str(&p.arg3, sizeof(p.args3), argp); + // }else if(i == 4){ + // bpf_probe_read_user_str(&p.arg4, sizeof(p.args4), argp); + // } + //} + + const char *const * argv = args->argv; + if(!argv[1]){ + return 0; + } + bpf_probe_read_user(&p.arg1, sizeof(p.arg1), (void *)argv[1]); + + if(!argv[2]){ + return 0; + } + bpf_probe_read_user(&p.arg2, sizeof(p.arg2), (void *)argv[2]); + + if(!argv[3]){ + return 0; + } + bpf_probe_read_user(&p.arg3, sizeof(p.arg3), (void *)argv[3]); + + if(!argv[4]){ + return 0; + } + bpf_probe_read_user(&p.arg4, sizeof(p.arg4), (void *)argv[4]); + + exec_map.update(&pid, &p); + return 0; +} + +//for vim echo ... +TRACEPOINT_PROBE(syscalls, sys_enter_write) { + unsigned int fd; + struct task_struct* t; + struct task_struct* p; + struct files_struct* f; + struct fdtable* fdt; + struct file** fdd; + struct file* file; + struct path path; + struct dentry* dentry; + struct inode* inode; + struct qstr pathname; + umode_t mode; + unsigned long i_ino; + char filename[128]; + struct event e = {}; + e.flag = 0; + + + pid_t ppid; + char pcomm[16]; + + fd =args->fd; + t = (struct task_struct*)bpf_get_current_task(); + if(t){ + bpf_probe_read(&f, sizeof(f), &(t->files)); + bpf_probe_read(&fdt, sizeof(fdt), (void*)&f->fdt); + int ret = bpf_probe_read(&fdd, sizeof(fdd), (void*)&fdt->fd); + if (ret) { + //bpf_trace_printk("bpf_probe_read failed: %d\\n", ret); + return 0; + } + bpf_probe_read(&file, sizeof(file), (void*)&fdd[fd]); + + //file file ppid pcmd + bpf_probe_read(&p, sizeof(p), &t->real_parent); + bpf_probe_read(&e.ppid, sizeof(e.ppid), &p->tgid); + bpf_probe_read(&e.pcmd, sizeof(e.pcmd), &p->comm); + + //fill file ino + bpf_probe_read(&inode, sizeof(inode), &file->f_inode); + bpf_probe_read(&e.i_ino, sizeof(i_ino), &inode->i_ino); + bpf_probe_read(&mode, sizeof(mode), &inode->i_mode); + if(!S_ISREG(mode)){ + return 0; + } + + //file process info + bpf_probe_read(&e.pid, sizeof(e.pid), &t->tgid); + bpf_probe_read(&e.cmd, sizeof(e.cmd), &t->comm); + //bpf_probe_read(&e.pcmd, sizeof(e.pcmd), &p->comm); + + //get filename + bpf_probe_read(&path, sizeof(path), (const void*)&file->f_path); + bpf_probe_read(&dentry, sizeof(dentry), (const void*)&path.dentry); + bpf_probe_read(&pathname, sizeof(pathname), (const void*)&dentry->d_name); + //fill name event + //bpf_probe_read_str((void*)&e.filename, sizeof(e.filename), (const void*)pathname.name); + + struct dentry* d_parent; + + #pragma unroll + for (int i = 0; i < MAX_DEPTH; i++) { + bpf_probe_read(&d_parent, sizeof(d_parent), (const void*)&dentry->d_parent); + if (d_parent == dentry) { + break; + } + //fix me + if(i == 0){ + bpf_probe_read(&e.dir1, sizeof(d_parent->d_iname), (const void*)&d_parent->d_iname); + }else if(i == 1){ + bpf_probe_read(&e.dir2, sizeof(d_parent->d_iname), (const void*)&d_parent->d_iname); + }else if(i == 2){ + bpf_probe_read(&e.dir3, sizeof(d_parent->d_iname), (const void*)&d_parent->d_iname); + }else if(i == 3){ + bpf_probe_read(&e.dir4, sizeof(d_parent->d_iname), (const void*)&d_parent->d_iname); + } + + dentry = d_parent; + } + bpf_probe_read_str((void*)&e.filename, sizeof(e.filename), (const void*)pathname.name); + //bpf_trace_printk("filename parent e.filename: %s\\n", e.filename); + events.perf_submit((struct pt_regs *)args, &e, sizeof(e)); + + return 0; + } + return 0; +} +""" + + +def get_conf(): + CONF = "/etc/agith/agith.config" + conf_data = {} + try: + with open(CONF) as user_file: + conf_data = json.load(user_file) + except FileNotFoundError: + print(f"[{CONF}] does not exist!") + exit(1) + return conf_data + #print(json.dumps(conf_data, indent = 4)) + +def get_ino(filename_list): + global logger + filenames = filename_list.split(",") + ino_name_map = {} + for f in filenames: + try: + stat_info = os.stat(f) + i = stat_info.st_ino + ino_name_map [str(i)] = f + except FileNotFoundError: + print(f"File not found: [{f}]") + exit(1) + except Exception as e: + print(f"An error occurred with file {f}: {e}") + exit(1) + logger.warning("g_map:%s", ino_name_map) + return ino_name_map + + +conf_data = get_conf() +g_map = get_ino(conf_data["Repository"]["conf_list"]) + +def postdata(data=None): + global conf_data + global g_map + global logger + #logger.warning('post data: %s', data) + g_map = get_ino(conf_data["Repository"]["conf_list"]) + try: + aops_zeus = conf_data["Repository"]["aops_zeus"] + response = requests.post(aops_zeus, json=data, timeout=5) + response.raise_for_status() + if response.status_code != 200: + logger.info('POST request failed:', response.status_code) + except requests.exceptions.HTTPError as http_err: + logger.error(f"HTTP error occurred: {http_err}") + except requests.exceptions.ConnectionError as conn_err: + logger.error(f"Connection error occurred: {conn_err}") + except requests.exceptions.Timeout as timeout_err: + logger.error(f"Timeout error occurred: {timeout_err}") + except requests.exceptions.RequestException as req_err: + logger.error(f"An error occurred: {req_err}") + +def get_oldfile_full_path(e): + #dir depth 4 + filename = "" + if e.flag != 3: + return filename + dir1 = "" + dir2 = "" + dir3 = "" + dir4 = "" + try: + dir1 = e.odir1.decode('utf-8') + dir2 = e.odir2.decode('utf-8') + dir3 = e.odir3.decode('utf-8') + dir4 = e.odir4.decode('utf-8') + except UnicodeDecodeError as ex: + print(f"UnicodeDecodeError: {ex}") + + filename = e.oldfilename.decode('utf-8') + if not filename: + return "" + + if dir1 == "/": + filename = dir1 + filename + return filename + + if dir2 == "/": + filename = dir2 + dir1 + "/" + filename + return filename + + if dir3 == "/": + filename = dir3 + dir2 + "/" + dir1 + "/" + filename + return filename + + if dir4 == "/": + filename = dir4 + dir3 + "/" + dir2 + "/" + dir1 + "/" + filename + return filename + return filename + +def get_file_full_path(e): + #dir depth 4 + dir1 = "" + dir2 = "" + dir3 = "" + dir4 = "" + try: + dir1 = e.dir1.decode('utf-8') + except UnicodeDecodeError as ex: + print(f"UnicodeDecodeError: {ex}") + try: + dir2 = e.dir2.decode('utf-8') + except UnicodeDecodeError as ex: + print(f"UnicodeDecodeError: {ex}") + try: + dir3 = e.dir3.decode('utf-8') + except UnicodeDecodeError as ex: + print(f"UnicodeDecodeError: {ex}") + try: + dir4 = e.dir4.decode('utf-8') + except UnicodeDecodeError as ex: + print(f"UnicodeDecodeError: {ex}") + + filename = e.filename.decode('utf-8') + if not filename: + return "" + #filename is full path + if os.path.exists(filename): + return filename + + if dir1 == "/": + filename = dir1 + filename + return filename + + if dir2 == "/": + filename = dir2 + dir1 + "/" + filename + return filename + + if dir3 == "/": + filename = dir3 + dir2 + "/" + dir1 + "/" + filename + return filename + + if dir4 == "/": + filename = dir4 + dir3 + "/" + dir2 + "/" + dir1 + "/" + filename + return filename + return filename + +class Data(ctypes.Structure): + _fields_ = [ + ("comm", ctypes.c_char * 64), + ("ppid", ctypes.c_uint32), + ("arg1", ctypes.c_char * 16), + ("arg2", ctypes.c_char * 16), + ("arg3", ctypes.c_char * 16), + ("arg4", ctypes.c_char * 16), + ] + + +def get_process_info_from_map(procid): + global exec_map + pid = procid + try: + pid = ctypes.c_int(pid) + info = exec_map[pid] + #info = ctypes.cast(ctypes.pointer(data), ctypes.POINTER(Data)).contents + pid = int(pid.value) + if info: + #print(f"PID: {pid}, Command: {info.comm}, Args: {info.args.decode('utf-8', 'ignore')}") + #cmd = info.comm.decode('utf-8') + " " + info.args.decode('utf-8', 'ignore') + #cmd = info.args.decode('utf-8', 'ignore') + str1 = info.arg1.decode('utf-8') + str2 = info.arg2.decode('utf-8') + str3 = info.arg3.decode('utf-8') + str4 = info.arg4.decode('utf-8') + cmd = str1 + " " + str2 + " " + str3 + " " + str4 + return { + 'pid': pid, + 'cmd': cmd, + } + else: + #print(f"No information found for PID {pid}") + return { + 'pid': pid, + 'error': 'error:No such process' + } + except KeyError: + pid = int(pid.value) + #print(f"key error for PID {pid}") + return { + 'pid': pid, + 'error': 'error:keyerror.' + } + +def get_ppid_by_pid(procid): + global exec_map + pid = procid + try: + pid = ctypes.c_int(pid) + info = exec_map[pid] + if info: + return info.ppid + else: + #print("not found,get ppid form local") + return get_parent_pid(pid) + except KeyError: + #print("keyerror ,get ppid form local") + return get_parent_pid(pid) + return 0 + +def get_parent_pid(pid): + if not isinstance(pid, int): + pid = int(pid.value) + try: + with open(f"/proc/{pid}/status", "r") as f: + for line in f: + if line.startswith("PPid:"): + parent_pid = int(line.split()[1]) + return parent_pid + except FileNotFoundError: + print(f"FileNotFoundError:/proc/{pid}/status") + return 0 + +def get_process_info_from_local(pid): + try: + process = psutil.Process(pid) + name = process.name() + cmdline = process.cmdline() + return { + 'pid': pid, + 'cmd': name + " " + ''.join(map(str, cmdline)), + } + except psutil.NoSuchProcess: + return { + 'pid': pid, + 'error': 'error:No such process' + } + except psutil.AccessDenied: + return { + 'pid': pid, + 'error': 'error:Access denied' + } + +def make_process_tree(procid): + info = [] + tmp = {} + flag = True + pid = procid + while flag: + tmp = get_process_info_from_map(pid) + if "error" in tmp: + tmp = get_process_info_from_local(pid) + if "error" in tmp: + break; + else: + info.append(tmp) + else: + info.append(tmp) + + ppid = get_ppid_by_pid(pid) + if ppid == 0: + break; + else: + pid = ppid + return info + +def check_filename(newfile=None, oldfile=None): + global conf_data + conf_file = conf_data["Repository"]["conf_list"] + if os.path.isdir(newfile): + return False + + newfile = remove_dot_slash(newfile) + if newfile and newfile in conf_file: + return True + + if oldfile and oldfile in conf_file: + return True + + return False + +def remove_dot_slash(path): + if path.startswith('./'): + return path[2:] + return path + +def get_filename(newfile=None, oldfile=None): + global conf_data + conf_file = conf_data["Repository"]["conf_list"] + if not oldfile: + return newfile + + if oldfile in conf_file: + return oldfile + newfile = remove_dot_slash(newfile) + if newfile in conf_file: + return newfile + +def process_event(cpu, data, size): + global b + global conf_data + global g_map + global logger + global executor + + e = b["events"].event(data) + fname = get_file_full_path(e) + oldname = get_oldfile_full_path(e) + filename = get_filename(fname, oldname) + #print(f'post event filename:{fname} e.pid: {e.pid}') + #fixme + if check_filename(fname, oldname): + #pid = ctypes.c_int(e.pid) + #get_process_info_map(pid) + aops_zeus = conf_data["Repository"]["aops_zeus"] + d = {} + d["host_id"] = int(conf_data["Repository"]["host_id"]) + d["domain_name"] = conf_data["Repository"]["domain_name"] + #d["file"] = e.filename.decode('utf-8') + d["flag"] = e.flag + d["file"] = filename + d["syscall"] = "write" + d["pid"] = e.pid + #d["dir1"] = e.dir1.decode('utf-8') + #d["dir2"] = e.dir2.decode('utf-8') + #d["dir3"] = e.dir3.decode('utf-8') + #d["dir4"] = e.dir4.decode('utf-8') + #d["inode"] = e.i_ino + d["inode"] = 0 + d["cmd"] = e.cmd.decode('utf-8') + d["ptrace"] = make_process_tree(e.ppid) + #tmp = {"pid": e.ppid, "cmd": e.pcmd.decode('utf-8')} + #d["ptrace"].append(tmp) + print(d) + #aops_zeus = conf_data["Repository"]["aops_zeus"] + #response = requests.post(aops_zeus, json=d, timeout=5) + #if response.status_code != 200: + # print('POST request failed:', response.status_code) + t = threading.Thread(target=postdata, args=(d,)) + t.deamon = True + t.start() + +#load ebpf +b = BPF(text=bpf_text, cflags=["-Wno-macro-redefined"]) +#exec_map = b["exec_map"] +exec_map = b.get_table("exec_map") + +if __name__ == "__main__": + + #print(json.dumps(conf_data, indent = 4)) + aops_zeus = conf_data["Repository"]["aops_zeus"] + conf_list = conf_data["Repository"]["conf_list"] + host_id = conf_data["Repository"]["host_id"] + domain_name = conf_data["Repository"]["domain_name"] + + b["events"].open_perf_buffer(process_event) + while True: + b.perf_buffer_poll() diff --git a/service/ragdoll-filetrace.service b/service/ragdoll-filetrace.service new file mode 100644 index 0000000..1ac3366 --- /dev/null +++ b/service/ragdoll-filetrace.service @@ -0,0 +1,19 @@ +[Unit] +Description=ragdoll-filetrace Service +After=network.target +StartLimitIntervalSec=30 + +[Service] +Type=simple +ExecStartPre=/usr/bin/test -z "$(pgrep -f /usr/bin/ragdoll-filetrace)" +ExecStart=/usr/bin/python3 /usr/bin/ragdoll-filetrace +ExecStop=/bin/bash -c 'kill `pgrep -d " " -f ragdoll-filetrace>/dev/null`' +Restart=on-failure +RestartSec=5s +StartLimitBurst=3 + +Environment=PYTHONUNBUFFERED=1 + +[Install] +WantedBy=multi-user.target + diff --git a/setup.py b/setup.py index d21e2b6..1bd488a 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,5 @@ # coding: utf-8 -import sys from setuptools import setup, find_packages NAME = "ragdoll" @@ -20,16 +19,15 @@ setup( version=VERSION, description="Configuration traceability", author_email="", - url="", + url="https://gitee.com/openeuler/gala-ragdoll", keywords=["Swagger", "Configuration traceability"], install_requires=REQUIRES, packages=find_packages(), package_data={'': ['swagger/swagger.yaml']}, include_package_data=True, entry_points={ - 'console_scripts': ['ragdoll=ragdoll.__main__:main']}, + 'console_scripts': ['ragdoll=ragdoll.manage:main']}, long_description="""\ A """ ) - diff --git a/yang_modules/openEuler-hostname.yang b/yang_modules/openEuler-hostname.yang index 5b0f2ca..ca39557 100644 --- a/yang_modules/openEuler-hostname.yang +++ b/yang_modules/openEuler-hostname.yang @@ -61,7 +61,7 @@ module openEuler-hostname { description "The file name is hostname"; hostname:path "openEuler:/etc/hostname"; - hostname:type "text"; + hostname:type "hostname"; hostname:spacer ""; } }