diff --git a/ansible_task/inventory/hosts.yml b/ansible_task/inventory/hosts.yml new file mode 100644 index 0000000..ca2b55b --- /dev/null +++ b/ansible_task/inventory/hosts.yml @@ -0,0 +1,4 @@ +all: + children: + sync: + hosts: {} \ No newline at end of file diff --git a/ansible_task/playbook_entries/conf_trace.yml b/ansible_task/playbook_entries/conf_trace.yml new file mode 100644 index 0000000..60ea52f --- /dev/null +++ b/ansible_task/playbook_entries/conf_trace.yml @@ -0,0 +1,14 @@ +- name: sync config to host + hosts: all + remote_user: root + gather_facts: no + max_fail_percentage: 30 + strategy: free + vars: + - ip: "{{ ip }}" + - port: "{{ port }}" + - conf_list_str: "{{ conf_list_str }}" + - domain_name: "{{ domain_name }}" + - host_id: "{{ hostvars[inventory_hostname]['host_id'] }}" + roles: + - ../roles/conf_trace \ No newline at end of file diff --git a/ansible_task/playbook_entries/sync_config.yml b/ansible_task/playbook_entries/sync_config.yml new file mode 100644 index 0000000..7819723 --- /dev/null +++ b/ansible_task/playbook_entries/sync_config.yml @@ -0,0 +1,9 @@ +- name: sync config to host + hosts: all + remote_user: root + gather_facts: no + max_fail_percentage: 30 + serial: "{{ serial_count }}" + strategy: free + roles: + - ../roles/sync_domain_config \ No newline at end of file diff --git a/ansible_task/roles/conf_trace/tasks/main.yml b/ansible_task/roles/conf_trace/tasks/main.yml new file mode 100644 index 0000000..446bbfc --- /dev/null +++ b/ansible_task/roles/conf_trace/tasks/main.yml @@ -0,0 +1,49 @@ +--- +- name: install dependency + dnf: + name: python3-libselinux + state: present + when: action == "start" or action == "update" +- name: copy ragdoll-filetrace bin + copy: + src: /usr/bin/ragdoll-filetrace + dest: /usr/bin/ragdoll-filetrace + owner: root + group: root + mode: '0755' + when: action == "start" or action == "update" +- name: copy ragdoll-filetrace systemctl service config + copy: + src: /usr/lib/systemd/system/ragdoll-filetrace.service + dest: /usr/lib/systemd/system/ragdoll-filetrace.service + owner: root + group: root + mode: '0755' + when: action == "start" or action == "update" +- name: reload systemctl service config + command: systemctl daemon-reload + when: action == "start" or action == "update" +- name: enable ragdoll-filetrace systemd + command: systemctl enable ragdoll-filetrace + when: action == "start" or action == "update" +- name: dependency install + shell: yum install python3-psutil kernel-devel-$(uname -r) bcc-tools bcc python3-bpfcc python3-requests llvm-12.0.1-4.iss22 llvm-libs-12.0.1-4.iss22 -y + when: action == "start" +- name: Ensure /etc/ragdoll-filetrace directory exists + file: + path: /etc/ragdoll-filetrace + state: directory + mode: '0755' + when: action == "update" or action == "start" +- name: update ragdoll-filetrace config + template: + src: agith.config.j2 + dest: /etc/ragdoll-filetrace/ragdoll-filetrace.conf + mode: '0755' + when: action == "update" or action == "start" +- name: stop ragdoll-filetrace when action is update + command: systemctl stop ragdoll-filetrace + when: action == "update" or action == "stop" +- name: start ragdoll-filetrace systemd + command: systemctl start ragdoll-filetrace + when: action == "update" or action == "start" diff --git a/ansible_task/roles/conf_trace/templates/agith.config.j2 b/ansible_task/roles/conf_trace/templates/agith.config.j2 new file mode 100644 index 0000000..c7a1476 --- /dev/null +++ b/ansible_task/roles/conf_trace/templates/agith.config.j2 @@ -0,0 +1,18 @@ +{ + "Repository": { + "concern_syscalls": [ + "write", + "clone", + "unlinkat", + "unlink", + "connect", + "sendto", + "recvfrom", + "mkdir" + ], + "aops_zeus": "http://{{ ip }}:{{ port }}/conftrace/data", + "conf_list": "{{ conf_list_str }}", + "domain_name": "{{ domain_name }}", + "host_id": "{{ host_id }}" + } +} \ No newline at end of file diff --git a/ansible_task/roles/sync_domain_config/tasks/main.yml b/ansible_task/roles/sync_domain_config/tasks/main.yml new file mode 100644 index 0000000..29b8857 --- /dev/null +++ b/ansible_task/roles/sync_domain_config/tasks/main.yml @@ -0,0 +1,18 @@ +--- +- name: Check if software is installed + command: rpm -qi python3-libselinux + register: result + ignore_errors: yes +- name: install dependency + dnf: + name: python3-libselinux + state: present + when: "'not installed' in result.stdout" +- name: sync config to host + copy: + src: "{{ item.key }}" + dest: "{{ item.value }}" + owner: root + group: root + mode: '0644' + with_dict: "{{ file_path_infos }}" \ No newline at end of file diff --git a/aops-zeus.spec b/aops-zeus.spec index 0d62cd6..badc290 100644 --- a/aops-zeus.spec +++ b/aops-zeus.spec @@ -37,6 +37,7 @@ cp -r database %{buildroot}/opt/aops/ %files %doc README.* %attr(0644,root,root) %{_sysconfdir}/aops/zeus.ini +%attr(0644,root,root) %{_sysconfdir}/aops/zeus_crontab.yml %attr(0755,root,root) %{_bindir}/aops-zeus %attr(0755,root,root) %{_unitdir}/aops-zeus.service %{python3_sitelib}/aops_zeus*.egg-info diff --git a/conf/zeus.ini b/conf/zeus.ini index 945d6b4..c87110f 100644 --- a/conf/zeus.ini +++ b/conf/zeus.ini @@ -36,4 +36,11 @@ port=11112 [apollo] ip=127.0.0.1 -port=11116 \ No newline at end of file +port=11116 + +[serial] +serial_count=10 + +[update_sync_status] +update_sync_status_address = "http://127.0.0.1" +update_sync_status_port = 11114 \ No newline at end of file diff --git a/conf/zeus_crontab.yml b/conf/zeus_crontab.yml new file mode 100644 index 0000000..d60d306 --- /dev/null +++ b/conf/zeus_crontab.yml @@ -0,0 +1,31 @@ +# Timed task configuration file specification (YAML): + +# Name of a scheduled task, name should be unique e.g +# task: download security bulletin + +# Task type, only 'update_config_sync_status' are supported +# type: update_config_sync_status + +# Whether scheduled tasks are allowed to run +# enable: true + +# meta info for the task, it's customised for user +# meta: +# cvrf_url: https://repo.openeuler.org/security/data/cvrf + +# Timed config, set the scheduled time and polling policy +# timed: +# value between 0-6, for example, 0 means Monday, 0-6 means everyday +# day_of_week: 0-6 +# value between 0-23, for example, 2 means 2:00 in a day +# hour: 3 +# Polling strategy, The value can only be 'cron' 'date' 'interval', default value is 'cron' +# trigger: cron + +- task: update config sync status + type: update_config_sync_status_task + enable: true + timed: + day_of_week: 0-6 + hour: 3 + trigger: cron \ No newline at end of file diff --git a/database/zeus.sql b/database/zeus.sql index b54e931..6d9a722 100644 --- a/database/zeus.sql +++ b/database/zeus.sql @@ -54,4 +54,23 @@ CREATE TABLE IF NOT EXISTS `host` ( CONSTRAINT `host_ibfk_2` FOREIGN KEY (`host_group_id`) REFERENCES `host_group` (`host_group_id`) ON DELETE RESTRICT ON UPDATE RESTRICT ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic; +CREATE TABLE IF NOT EXISTS `host_conf_sync_status` ( + `host_id` int(11) NOT NULL, + `host_ip` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `domain_name` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `sync_status` int(1) unsigned zerofill NULL DEFAULT NULL, + CONSTRAINT hd_host_sync PRIMARY KEY (host_id,domain_name), + INDEX `sync_status`(`sync_status`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic; + +CREATE TABLE IF NOT EXISTS `conf_trace_info` ( + `UUID` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `domain_name` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `host_id` int(11) NOT NULL, + `conf_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `info` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `create_time` datetime DEFAULT NULL, + PRIMARY KEY (`UUID`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic; + INSERT INTO `user` (`username`, `password`) VALUE('admin', 'pbkdf2:sha256:150000$h1oaTY7K$5b1ff300a896f6f373928294fd8bac8ed6d2a1d6a7c5ea2d2ccd2075e6177896') ON DUPLICATE KEY UPDATE username= 'admin',password='pbkdf2:sha256:150000$h1oaTY7K$5b1ff300a896f6f373928294fd8bac8ed6d2a1d6a7c5ea2d2ccd2075e6177896'; \ No newline at end of file diff --git a/setup.py b/setup.py index 1b45b43..d62ee0c 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,7 @@ setup( author='cmd-lsw-yyy-zyc', data_files=[ ('/etc/aops', ['conf/zeus.ini']), + ('/etc/aops', ['conf/zeus_crontab.yml']), ('/usr/lib/systemd/system', ['aops-zeus.service']), ("/opt/aops/database", ["database/zeus.sql"]), ], diff --git a/zeus/conf/constant.py b/zeus/conf/constant.py index 27aef66..a0e0a98 100644 --- a/zeus/conf/constant.py +++ b/zeus/conf/constant.py @@ -57,9 +57,15 @@ ADD_GROUP = "/manage/host/group/add" DELETE_GROUP = "/manage/host/group/delete" GET_GROUP = "/manage/host/group/get" +ADD_HOST_SYNC_STATUS = "/manage/host/sync/status/add" +DELETE_HOST_SYNC_STATUS = "/manage/host/sync/status/delete" +DELETE_ALL_HOST_SYNC_STATUS = "/manage/all/host/sync/status/delete" +GET_HOST_SYNC_STATUS = "/manage/host/sync/status/get" + COLLECT_CONFIG = '/manage/config/collect' SYNC_CONFIG = '/manage/config/sync' OBJECT_FILE_CONFIG = '/manage/config/objectfile' +BATCH_SYNC_CONFIG = '/manage/config/batch/sync' USER_LOGIN = "/manage/account/login" LOGOUT = "/manage/account/logout" @@ -94,6 +100,17 @@ VUL_TASK_CVE_SCAN_NOTICE = "/vulnerability/task/callback/cve/scan/notice" CHECK_IDENTIFY_SCENE = "/check/scene/identify" CHECK_WORKFLOW_HOST_EXIST = '/check/workflow/host/exist' +# ragdoll +DOMAIN_LIST_API = "/domain/queryDomain" +EXPECTED_CONFS_API = "/confs/queryExpectedConfs" +DOMAIN_CONF_DIFF_API = "/confs/domain/diff" + +# conf trace +CONF_TRACE_MGMT = "/conftrace/mgmt" +CONF_TRACE_QUERY = "/conftrace/query" +CONF_TRACE_DATA = "/conftrace/data" +CONF_TRACE_DELETE = "/conftrace/delete" + # host template file content HOST_TEMPLATE_FILE_CONTENT = """host_ip,ssh_port,ssh_user,password,ssh_pkey,host_name,host_group_name,management 127.0.0.1,22,root,password,private key,test_host,test_host_group,FALSE @@ -106,6 +123,20 @@ HOST_TEMPLATE_FILE_CONTENT = """host_ip,ssh_port,ssh_user,password,ssh_pkey,host "4. 上传本文件前,请删除此部分提示内容",,,,,,, """ +# ansible sync config +PARENT_DIRECTORY = "/opt/aops/ansible_task/" +HOST_PATH_FILE = "/opt/aops/ansible_task/inventory/" +SYNC_CONFIG_YML = "/opt/aops/ansible_task/playbook_entries/sync_config.yml" +CONF_TRACE_YML = "/opt/aops/ansible_task/playbook_entries/conf_trace.yml" +SYNC_LOG_PATH = "/var/log/aops/sync/" +CONF_TRACE_LOG_PATH = "/var/log/aops/conftrace/" +KEY_FILE_PREFIX = "/tmp/" +KEY_FILE_SUFFIX = "_id_dsa" +IP_START_PATTERN = r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}" + +DIRECTORY_FILE_PATH_LIST = ["/etc/pam.d"] + +TIMED_TASK_CONFIG_PATH = "/etc/aops/zeus_crontab.yml" # cve task status class CveTaskStatus: diff --git a/zeus/conf/default_config.py b/zeus/conf/default_config.py index 5e05f64..1e713ae 100644 --- a/zeus/conf/default_config.py +++ b/zeus/conf/default_config.py @@ -32,7 +32,10 @@ apollo = {"IP": "127.0.0.1", "PORT": 11116} redis = {"IP": "127.0.0.1", "PORT": 6379} - prometheus = {"IP": "127.0.0.1", "PORT": 9090, "QUERY_RANGE_STEP": "15s"} agent = {"DEFAULT_INSTANCE_PORT": 8888} + +serial = {"SERIAL_COUNT": 10} + +update_sync_status = {"UPDATE_SYNC_STATUS_ADDRESS": "http://127.0.0.1", "UPDATE_SYNC_STATUS_PORT": 11114} diff --git a/zeus/config_manager/view.py b/zeus/config_manager/view.py index b012c62..28c9d32 100644 --- a/zeus/config_manager/view.py +++ b/zeus/config_manager/view.py @@ -15,17 +15,29 @@ Time: Author: Description: Restful APIs for host """ +import glob import json import os +import queue +import subprocess +import threading +import time +from configparser import RawConfigParser from typing import List, Dict +import yaml +from vulcanus import LOGGER from vulcanus.multi_thread_handler import MultiThreadHandler from vulcanus.restful.resp import state from vulcanus.restful.response import BaseResponse -from zeus.conf.constant import CERES_COLLECT_FILE, CERES_SYNC_CONF, CERES_OBJECT_FILE_CONF + +from zeus.conf import configuration +from zeus.conf.constant import CERES_COLLECT_FILE, CERES_SYNC_CONF, CERES_OBJECT_FILE_CONF, SYNC_LOG_PATH, \ + HOST_PATH_FILE, SYNC_CONFIG_YML, PARENT_DIRECTORY, IP_START_PATTERN, KEY_FILE_PREFIX, KEY_FILE_SUFFIX from zeus.database.proxy.host import HostProxy from zeus.function.model import ClientConnectArgs -from zeus.function.verify.config import CollectConfigSchema, SyncConfigSchema, ObjectFileConfigSchema +from zeus.function.verify.config import CollectConfigSchema, SyncConfigSchema, ObjectFileConfigSchema, \ + BatchSyncConfigSchema from zeus.host_manager.ssh import execute_command_and_parse_its_result, execute_command_sftp_result @@ -147,7 +159,7 @@ class CollectConfig(BaseResponse): return file_content - @BaseResponse.handle(schema=CollectConfigSchema, token=False) + @BaseResponse.handle(schema=CollectConfigSchema, token=True) def post(self, **param): """ Get config @@ -201,16 +213,16 @@ class CollectConfig(BaseResponse): file_content = self.convert_host_id_to_failed_data_format( list(host_id_with_config_file.keys()), host_id_with_config_file ) - return self.response(code=state.DATABASE_CONNECT_ERROR, data={"resp": file_content}) + return self.response(code=state.DATABASE_CONNECT_ERROR, data=file_content) status, host_list = proxy.get_host_info( - {"username": "admin", "host_list": list(host_id_with_config_file.keys())}, True + {"username": param.get("username"), "host_list": list(host_id_with_config_file.keys())}, True ) if status != state.SUCCEED: file_content = self.convert_host_id_to_failed_data_format( list(host_id_with_config_file.keys()), host_id_with_config_file ) - return self.response(code=status, data={"resp": file_content}) + return self.response(code=status, data=file_content) # Get file content tasks = [(host, host_id_with_config_file[host["host_id"]]) for host in host_list] multi_thread = MultiThreadHandler(lambda data: self.get_file_content(*data), tasks, None) @@ -262,7 +274,7 @@ class SyncConfig(BaseResponse): host_info.get("ssh_user"), host_info.get("pkey")), command) return status - @BaseResponse.handle(schema=SyncConfigSchema, token=False) + @BaseResponse.handle(schema=SyncConfigSchema, token=True) def put(self, **params): sync_config_info = dict() @@ -277,19 +289,19 @@ class SyncConfig(BaseResponse): # Query host address from database proxy = HostProxy() if not proxy.connect(): - return self.response(code=state.DATABASE_CONNECT_ERROR, data={"resp": sync_result}) + return self.response(code=state.DATABASE_CONNECT_ERROR, data=sync_result) status, host_list = proxy.get_host_info( - {"username": "admin", "host_list": [params.get('host_id')]}, True) + {"username": params.get("username"), "host_list": [params.get('host_id')]}, True) if status != state.SUCCEED: - return self.response(code=status, data={"resp": sync_result}) + return self.response(code=status, data=sync_result) host_info = host_list[0] status = self.sync_config_content(host_info, sync_config_info) if status == state.SUCCEED: sync_result['sync_result'] = True - return self.response(code=state.SUCCEED, data={"resp": sync_result}) - return self.response(code=state.UNKNOWN_ERROR, data={"resp": sync_result}) + return self.response(code=state.SUCCEED, data=sync_result) + return self.response(code=state.UNKNOWN_ERROR, data=sync_result) class ObjectFileConfig(BaseResponse): @@ -302,7 +314,7 @@ class ObjectFileConfig(BaseResponse): host_info.get("ssh_user"), host_info.get("pkey")), command) return status, content - @BaseResponse.handle(schema=ObjectFileConfigSchema, token=False) + @BaseResponse.handle(schema=ObjectFileConfigSchema, token=True) def post(self, **params): object_file_result = { "object_file_paths": list(), @@ -311,12 +323,12 @@ class ObjectFileConfig(BaseResponse): # Query host address from database proxy = HostProxy() if not proxy.connect(): - return self.response(code=state.DATABASE_CONNECT_ERROR, data={"resp": object_file_result}) + return self.response(code=state.DATABASE_CONNECT_ERROR, data=object_file_result) status, host_list = proxy.get_host_info( - {"username": "admin", "host_list": [params.get('host_id')]}, True) + {"username": params.get("username"), "host_list": [params.get('host_id')]}, True) if status != state.SUCCEED: - return self.response(code=status, data={"resp": object_file_result}) + return self.response(code=status, data=object_file_result) host_info = host_list[0] status, content = self.object_file_config_content(host_info, params.get('file_directory')) @@ -326,5 +338,206 @@ class ObjectFileConfig(BaseResponse): if content_res.get("resp"): resp = content_res.get("resp") object_file_result['object_file_paths'] = resp - return self.response(code=state.SUCCEED, data={"resp": object_file_result}) - return self.response(code=state.UNKNOWN_ERROR, data={"resp": object_file_result}) + return self.response(code=state.SUCCEED, data=object_file_result) + return self.response(code=state.UNKNOWN_ERROR, data=object_file_result) + + +class BatchSyncConfig(BaseResponse): + @staticmethod + def run_subprocess(cmd, result_queue): + try: + completed_process = subprocess.run(cmd, cwd=PARENT_DIRECTORY, shell=True, capture_output=True, text=True) + result_queue.put(completed_process) + except subprocess.CalledProcessError as ex: + result_queue.put(ex) + + @staticmethod + def ansible_handler(now_time, ansible_forks, extra_vars, HOST_FILE): + if not os.path.exists(SYNC_LOG_PATH): + os.makedirs(SYNC_LOG_PATH) + + SYNC_LOG = SYNC_LOG_PATH + "sync_config_" + now_time + ".log" + cmd = f"ansible-playbook -f {ansible_forks} -e '{extra_vars}' " \ + f"-i {HOST_FILE} {SYNC_CONFIG_YML} |tee {SYNC_LOG} " + result_queue = queue.Queue() + thread = threading.Thread(target=BatchSyncConfig.run_subprocess, args=(cmd, result_queue)) + thread.start() + + thread.join() + try: + completed_process = result_queue.get(block=False) + if isinstance(completed_process, subprocess.CalledProcessError): + LOGGER.error("ansible subprocess error:", completed_process) + else: + if completed_process.returncode == 0: + return completed_process.stdout + else: + LOGGER.error("ansible subprocess error:", completed_process) + except queue.Empty: + LOGGER.error("ansible subprocess nothing result") + + @staticmethod + def ansible_sync_domain_config_content(host_list: list, file_path_infos: list): + # 初始化参数和响应 + now_time = str(int(time.time())) + host_ip_sync_result = {} + BatchSyncConfig.generate_config(host_list, host_ip_sync_result, now_time) + + ansible_forks = len(host_list) + # 配置文件中读取并发数量 + # 从内存中获取serial_count + serial_count = configuration.serial.get("SERIAL_COUNT") + # 换种方式 + path_infos = {} + for file_info in file_path_infos: + file_path = file_info.get("file_path") + file_content = file_info.get("content") + # 写临时文件 + src_file_path = "/tmp/" + os.path.basename(file_path) + with open(src_file_path, "w", encoding="UTF-8") as f: + f.write(file_content) + path_infos[src_file_path] = file_path + + # 调用ansible + extra_vars = json.dumps({"serial_count": serial_count, "file_path_infos": path_infos}) + try: + HOST_FILE = HOST_PATH_FILE + "hosts_" + now_time + ".yml" + result = BatchSyncConfig.ansible_handler(now_time, ansible_forks, extra_vars, HOST_FILE) + except Exception as ex: + LOGGER.error("ansible playbook execute error:", ex) + return host_ip_sync_result + + processor_result = result.splitlines() + char_to_filter = 'item=' + filtered_list = [item for item in processor_result if char_to_filter in item] + if not filtered_list: + return host_ip_sync_result + for line in filtered_list: + start_index = line.find("[") + 1 + end_index = line.find("]", start_index) + ip_port = line[start_index:end_index] + sync_results = host_ip_sync_result.get(ip_port) + + start_index1 = line.find("{") + end_index1 = line.find(")", start_index1) + path_str = line[start_index1:end_index1] + file_path = json.loads(path_str.replace("'", "\"")).get("value") + if line.startswith("ok:") or line.startswith("changed:"): + signal_file_sync = { + "filePath": file_path, + "result": "SUCCESS" + } + else: + signal_file_sync = { + "filePath": file_path, + "result": "FAIL" + } + sync_results.append(signal_file_sync) + # 删除中间文件 + try: + # 删除/tmp下面以id_dsa结尾的文件 + file_pattern = "*id_dsa" + tmp_files_to_delete = glob.glob(os.path.join(KEY_FILE_PREFIX, file_pattern)) + for tmp_file_path in tmp_files_to_delete: + os.remove(tmp_file_path) + + # 删除/tmp下面临时写的path_infos的key值文件 + for path in path_infos.keys(): + os.remove(path) + + # 删除临时的HOST_PATH_FILE的临时inventory文件 + os.remove(HOST_FILE) + except OSError as ex: + LOGGER.error("remove file error: %s", ex) + return host_ip_sync_result + + @staticmethod + def generate_config(host_list, host_ip_sync_result, now_time): + # 取出host_ip,并传入ansible的hosts中 + hosts = { + "all": { + "children": { + "sync": { + "hosts": { + + } + } + } + } + } + + for host in host_list: + # 生成临时的密钥key文件用于ansible访问远端主机 + key_file_path = KEY_FILE_PREFIX + host['host_ip'] + KEY_FILE_SUFFIX + with open(key_file_path, 'w', encoding="UTF-8") as keyfile: + os.chmod(key_file_path, 0o600) + keyfile.write(host['pkey']) + host_ip = host['host_ip'] + host_vars = { + "ansible_host": host_ip, + "ansible_ssh_user": "root", + "ansible_ssh_private_key_file": key_file_path, + "ansible_ssh_port": host['ssh_port'], + "ansible_python_interpreter": "/usr/bin/python3", + "host_key_checking": False, + "interpreter_python": "auto_legacy_silent", + "become": True, + "become_method": "sudo", + "become_user": "root", + "become_ask_pass": False, + "ssh_args": "-C -o ControlMaster=auto -o ControlPersist=60s StrictHostKeyChecking=no" + } + + hosts['all']['children']['sync']['hosts'][host_ip + "_" + str(host['ssh_port'])] = host_vars + # 初始化结果 + host_ip_sync_result[host['host_ip'] + "_" + str(host['ssh_port'])] = list() + HOST_FILE = HOST_PATH_FILE + "hosts_" + now_time + ".yml" + with open(HOST_FILE, 'w') as outfile: + yaml.dump(hosts, outfile, default_flow_style=False) + + @staticmethod + def ini2json(ini_path): + json_data = {} + cfg = RawConfigParser() + cfg.read(ini_path) + for s in cfg.sections(): + json_data[s] = dict(cfg.items(s)) + return json_data + + @BaseResponse.handle(schema=BatchSyncConfigSchema, proxy=HostProxy, token=True) + def put(self, callback: HostProxy, **params): + # 初始化响应 + file_path_infos = params.get('file_path_infos') + host_ids = params.get('host_ids') + sync_result = list() + # Query host address from database + if not callback.connect(): + return self.response(code=state.DATABASE_CONNECT_ERROR, data=sync_result) + + # 校验token + status, host_list = callback.get_host_info( + # 校验token 拿到用户 + {"username": params.get("username"), "host_list": host_ids}, True) + if status != state.SUCCEED: + return self.response(code=status, data=sync_result) + + # 将ip和id对应起来 + host_id_ip_dict = dict() + if host_list: + for host in host_list: + key = host['host_ip'] + "_" + str(host['ssh_port']) + host_id_ip_dict[key] = host['host_id'] + + host_ip_sync_result = self.ansible_sync_domain_config_content(host_list, file_path_infos) + + if not host_ip_sync_result: + return self.response(code=state.EXECUTE_COMMAND_ERROR, data=sync_result) + # 处理成id对应结果 + for key, value in host_ip_sync_result.items(): + host_id = host_id_ip_dict.get(key) + single_result = { + "host_id": host_id, + "syncResult": value + } + sync_result.append(single_result) + return self.response(code=state.SUCCEED, data=sync_result) diff --git a/zeus/conftrace_manage/__init__.py b/zeus/conftrace_manage/__init__.py new file mode 100644 index 0000000..0470fef --- /dev/null +++ b/zeus/conftrace_manage/__init__.py @@ -0,0 +1,18 @@ +#!/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: __init__.py.py +@Time: 2024/4/19 9:21 +@Author: JiaoSiMao +Description: +""" diff --git a/zeus/conftrace_manage/view.py b/zeus/conftrace_manage/view.py new file mode 100644 index 0000000..a1faffc --- /dev/null +++ b/zeus/conftrace_manage/view.py @@ -0,0 +1,279 @@ +#!/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. +# ******************************************************************************/ +""" +Time: +Author: +Description: Restful APIs for conf trace +""" +import glob +import json +import os +import queue +import subprocess +import threading +import time + +import yaml +from vulcanus import LOGGER +from vulcanus.restful.resp import state +from vulcanus.restful.resp.state import SUCCEED, SERVER_ERROR +from vulcanus.restful.response import BaseResponse + +from zeus.conf import configuration +from zeus.conf.constant import KEY_FILE_PREFIX, KEY_FILE_SUFFIX, HOST_PATH_FILE, CONF_TRACE_LOG_PATH, \ + PARENT_DIRECTORY, CONF_TRACE_YML +from zeus.database.proxy.conf_trace import ConfTraceProxy +from zeus.database.proxy.host import HostProxy +from zeus.function.verify.conf_trace import ConfTraceMgmtSchema, ConfTraceDataSchema, ConfTraceQuerySchema, \ + ConfTraceDataDeleteSchema + + +class ConfTraceMgmt(BaseResponse): + """ + Interface for register user. + Restful API: post + """ + + @staticmethod + def parse_result(action, result, host_ip_trace_result, HOST_FILE): + code_num = SUCCEED + code_string = f"{action} ragdoll-filetrace succeed" + processor_result = result.splitlines() + char_to_filter = 'unreachable=' + filtered_list = [item for item in processor_result if char_to_filter in item] + if not filtered_list: + code_num = SERVER_ERROR + code_string = f"{action} ragdoll-filetrace error, no result" + for line in filtered_list: + result_start_index = line.find(":") + ip_port = line[0:result_start_index] + trace_result = host_ip_trace_result.get(ip_port.strip()) + print(trace_result) + result_str = line[result_start_index:] + if "unreachable=0" in result_str and "failed=0" in result_str: + host_ip_trace_result[ip_port.strip()] = True + else: + host_ip_trace_result[ip_port.strip()] = False + + # 删除中间文件 + try: + # 删除/tmp下面以id_dsa结尾的文件 + dsa_file_pattern = "*id_dsa" + dsa_tmp_files_to_delete = glob.glob(os.path.join(KEY_FILE_PREFIX, dsa_file_pattern)) + for dsa_tmp_file_path in dsa_tmp_files_to_delete: + os.remove(dsa_tmp_file_path) + + # 删除临时的HOST_PATH_FILE的临时inventory文件 + os.remove(HOST_FILE) + except OSError as ex: + LOGGER.error("remove file error: %s", ex) + return code_num, code_string + + @staticmethod + def run_subprocess(cmd, result_queue): + try: + completed_process = subprocess.run(cmd, cwd=PARENT_DIRECTORY, shell=True, capture_output=True, text=True) + result_queue.put(completed_process) + except subprocess.CalledProcessError as ex: + result_queue.put(ex) + + @staticmethod + def ansible_handler(now_time, ansible_forks, extra_vars, HOST_FILE): + if not os.path.exists(CONF_TRACE_LOG_PATH): + os.makedirs(CONF_TRACE_LOG_PATH) + + CONF_TRACE_LOG = CONF_TRACE_LOG_PATH + "conf_trace_" + now_time + ".log" + + cmd = f"ansible-playbook -f {ansible_forks} -e '{extra_vars}' " \ + f"-i {HOST_FILE} {CONF_TRACE_YML} |tee {CONF_TRACE_LOG} " + result_queue = queue.Queue() + thread = threading.Thread(target=ConfTraceMgmt.run_subprocess, args=(cmd, result_queue)) + thread.start() + + thread.join() + try: + completed_process = result_queue.get(block=False) + if isinstance(completed_process, subprocess.CalledProcessError): + LOGGER.error("ansible subprocess error:", completed_process) + else: + if completed_process.returncode == 0: + return completed_process.stdout + else: + LOGGER.error("ansible subprocess error:", completed_process) + except queue.Empty: + LOGGER.error("ansible subprocess nothing result") + + @staticmethod + def generate_config(host_list, now_time, conf_files, host_ip_trace_result, domain_name): + # 取出host_ip,并传入ansible的hosts中 + hosts = { + "all": { + "children": { + "sync": { + "hosts": { + + } + } + } + } + } + + for host in host_list: + # 生成临时的密钥key文件用于ansible访问远端主机 + key_file_path = KEY_FILE_PREFIX + host['host_ip'] + KEY_FILE_SUFFIX + with open(key_file_path, 'w', encoding="UTF-8") as keyfile: + os.chmod(key_file_path, 0o600) + keyfile.write(host['pkey']) + host_ip = host['host_ip'] + host_vars = { + "ansible_host": host_ip, + "ansible_ssh_user": "root", + "ansible_ssh_private_key_file": key_file_path, + "ansible_ssh_port": host['ssh_port'], + "ansible_python_interpreter": "/usr/bin/python3", + "host_key_checking": False, + "interpreter_python": "auto_legacy_silent", + "become": True, + "become_method": "sudo", + "become_user": "root", + "become_ask_pass": False, + "ssh_args": "-C -o ControlMaster=auto -o ControlPersist=60s StrictHostKeyChecking=no", + "host_id": host['host_id'] + } + + hosts['all']['children']['sync']['hosts'][host_ip + "_" + str(host['ssh_port'])] = host_vars + # 初始化结果 + host_ip_trace_result[host['host_ip'] + "_" + str(host['ssh_port'])] = True + + HOST_FILE = HOST_PATH_FILE + "hosts_" + now_time + ".yml" + with open(HOST_FILE, 'w') as outfile: + yaml.dump(hosts, outfile, default_flow_style=False) + + @staticmethod + def ansible_conf_trace_mgmt(host_list: list, action: str, conf_files: list, domain_name: str): + now_time = str(int(time.time())) + host_ip_trace_result = {} + ConfTraceMgmt.generate_config(host_list, now_time, conf_files, host_ip_trace_result, domain_name) + ansible_forks = len(host_list) + # 配置文件中读取并发数量 + # 从内存中获取serial_count + # serial_count = configuration.serial.get("SERIAL_COUNT") + # 组装ansible执行的extra参数 + ip = configuration.zeus.get('IP') + port = configuration.zeus.get("PORT") + if conf_files: + conf_list_str = ",".join(conf_files) + else: + conf_list_str = "" + extra_vars = f"action={action} ip={ip} port={port} conf_list_str={conf_list_str} " \ + f"domain_name={domain_name} " + # 调用ansible + try: + HOST_FILE = HOST_PATH_FILE + "hosts_" + now_time + ".yml" + result = ConfTraceMgmt.ansible_handler(now_time, ansible_forks, extra_vars, HOST_FILE) + except Exception as ex: + LOGGER.error("ansible playbook execute error:", ex) + conf_trace_mgmt_result = "ragdoll-filetrace ansible playbook execute error" + return SERVER_ERROR, conf_trace_mgmt_result, host_ip_trace_result + # 根据action解析每个result + code_num, code_string = ConfTraceMgmt.parse_result(action, result, host_ip_trace_result, HOST_FILE) + return code_num, code_string, host_ip_trace_result + + @BaseResponse.handle(schema=ConfTraceMgmtSchema, proxy=HostProxy, token=True) + def put(self, callback: HostProxy, **params): + host_ids = params.get("host_ids") + action = params.get("action") + conf_files = params.get("conf_files") + domain_name = params.get("domain_name") + + # 根据id获取host信息 + # Query host address from database + if not callback.connect(): + return self.response(code=state.DATABASE_CONNECT_ERROR, message="database connect error") + + # 校验token + status, host_list = callback.get_host_info( + # 校验token 拿到用户 + {"username": params.get("username"), "host_list": host_ids}, True) + if status != state.SUCCEED: + return self.response(code=status, message="get host info error") + + # 组装ansible外部数据 + code_num, code_string, host_ip_trace_result = self.ansible_conf_trace_mgmt(host_list, action, conf_files, + domain_name) + return self.response(code=code_num, message=code_string, data=host_ip_trace_result) + + +class ConfTraceData(BaseResponse): + @staticmethod + def validate_conf_trace_info(params: dict): + """ + query conf trace info, validate that the host sync status info is valid + return host object + + Args: + params (dict): e.g + { + "domain_name": "aops", + "host_id": 1, + "conf_name": "/etc/hostname", + "info": "" + } + + Returns: + tuple: + status code, host sync status object + """ + # 检查host 是否存在 + host_proxy = HostProxy() + if not host_proxy.connect(): + LOGGER.error("Connect to database error") + return state.DATABASE_CONNECT_ERROR, {} + data = {"host_list": [params.get("host_id")]} + code_num, result_list = host_proxy.get_host_info_by_host_id(data) + if code_num != SUCCEED: + LOGGER.error("query host info error") + return state.DATABASE_QUERY_ERROR, {} + if len(result_list) == 0: + return state.NO_DATA, [] + return code_num, result_list + + @BaseResponse.handle(schema=ConfTraceDataSchema, proxy=ConfTraceProxy, token=False) + def post(self, callback: ConfTraceProxy, **params): + # 校验hostId是否存在 + code_num, result_list = self.validate_conf_trace_info(params) + if code_num != SUCCEED or len(result_list) == 0: + return self.response(code=SERVER_ERROR, message="request param host id does not exist") + + status_code = callback.add_conf_trace_info(params) + if status_code != state.SUCCEED: + return self.response(code=SERVER_ERROR, message="Failed to upload data, service error") + return self.response(code=SUCCEED, message="Succeed to upload conf trace info data") + + +class ConfTraceQuery(BaseResponse): + @BaseResponse.handle(schema=ConfTraceQuerySchema, proxy=ConfTraceProxy, token=True) + def post(self, callback: ConfTraceProxy, **params): + status_code, result = callback.query_conf_trace_info(params) + if status_code != SUCCEED: + return self.response(code=SERVER_ERROR, message="Failed to query data, service error") + return self.response(code=SUCCEED, message="Succeed to query conf trace info data", data=result) + + +class ConfTraceDataDelete(BaseResponse): + @BaseResponse.handle(schema=ConfTraceDataDeleteSchema, proxy=ConfTraceProxy, token=True) + def post(self, callback: ConfTraceProxy, **params): + status_code = callback.delete_conf_trace_info(params) + if status_code != state.SUCCEED: + return self.response(code=SERVER_ERROR, message="Failed to delete data, service error") + return self.response(code=SUCCEED, message="Succeed to delete conf trace info data") diff --git a/zeus/cron/__init__.py b/zeus/cron/__init__.py new file mode 100644 index 0000000..377a23d --- /dev/null +++ b/zeus/cron/__init__.py @@ -0,0 +1,21 @@ +#!/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 16:55 +@Author: JiaoSiMao +Description: +""" +from zeus.cron.update_config_sync_status_task import UpdateConfigSyncStatusTask + +task_meta = {"update_config_sync_status_task": UpdateConfigSyncStatusTask} diff --git a/zeus/cron/update_config_sync_status_task.py b/zeus/cron/update_config_sync_status_task.py new file mode 100644 index 0000000..c836291 --- /dev/null +++ b/zeus/cron/update_config_sync_status_task.py @@ -0,0 +1,267 @@ +#!/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: update_config_sync_status_task.py +@Time: 2024/3/5 16:56 +@Author: JiaoSiMao +Description: +""" +import json + +import requests +from vulcanus.log.log import LOGGER +from vulcanus.timed import TimedTask +from vulcanus.database.helper import make_mysql_engine_url, create_database_engine +from vulcanus.database.proxy import MysqlProxy +from vulcanus.restful.resp import state +from vulcanus.restful.resp.state import SUCCEED + +from zeus.conf import configuration +from zeus.conf.constant import DIRECTORY_FILE_PATH_LIST +from zeus.config_manager.view import ObjectFileConfig, CollectConfig +from zeus.database.proxy.host import HostProxy +from zeus.database.proxy.host_sync_status import HostSyncProxy +from zeus.utils.conf_tools import ConfTools + + +class UpdateConfigSyncStatusTask(TimedTask): + @staticmethod + def get_domain_files(domain_paths: dict, expected_confs_resp: list): + # 获取domain中要获取文件内容的文件路径 + for domain_confs in expected_confs_resp: + domain_name = domain_confs.get("domainName") + conf_base_infos = domain_confs.get("confBaseInfos") + file_list = [] + if conf_base_infos: + for conf_info in conf_base_infos: + file_list.append(conf_info.get("filePath")) + domain_paths[domain_name] = file_list + + @staticmethod + def deal_pam_d_config(host_info, directory_path): + # 先获取/etc/pam.d下有哪些文件 + status, content = ObjectFileConfig.object_file_config_content( + host_info, directory_path + ) + if status == state.SUCCEED: + content_dict = json.loads(content) + directory_paths = content_dict.get("resp") + return directory_paths + return [] + + @staticmethod + def deal_host_file_content(domain_result, host_file_content_result): + host_id = host_file_content_result.get("host_id") + infos = host_file_content_result.get("infos") + file_content_list = [] + pam_d_file_list = [] + if infos: + for info in infos: + pam_d_file = {} + info_path = str(info.get("path")) + for file_path in DIRECTORY_FILE_PATH_LIST: + if info_path.find(file_path) == -1: + signal_file_content = { + "filePath": info.get("path"), + "contents": info.get("content"), + } + file_content_list.append(signal_file_content) + else: + pam_d_file[info_path] = info.get("content") + pam_d_file_list.append(pam_d_file) + if pam_d_file_list: + directory_file_dict = {} + for file_path in DIRECTORY_FILE_PATH_LIST: + directory_file_dict[file_path] = {} + for path, content_list in directory_file_dict.items(): + for pam_d_file in pam_d_file_list: + pam_d_file_path = str(list(pam_d_file.keys())[0]) + if path in pam_d_file_path: + content_list[pam_d_file_path] = pam_d_file.get(pam_d_file_path) + for key, value in directory_file_dict.items(): + pam_d_file_content = {"filePath": key, "contents": json.dumps(value)} + file_content_list.append(pam_d_file_content) + if file_content_list: + domain_result[str(host_id)] = file_content_list + + def collect_file_infos(self, param, host_infos_result): + # 组装host_id和要获取内容的文件列表 一一对应 + domain_result = {} + host_id_with_config_file = {} + for host in param.get("infos"): + host_id_with_config_file[host.get("host_id")] = host.get("config_list") + + for host_id, file_list in host_id_with_config_file.items(): + host_info = host_infos_result.get(host_id) + # 处理/etc/pam.d + for file_path in DIRECTORY_FILE_PATH_LIST: + if file_path in file_list: + file_list.remove(file_path) + object_file_paths = self.deal_pam_d_config(host_info, file_path) + if object_file_paths: + file_list.extend(object_file_paths) + host_file_content_result = CollectConfig.get_file_content( + host_info, file_list + ) + # 处理结果 + self.deal_host_file_content(domain_result, host_file_content_result) + return domain_result + + @staticmethod + def make_database_engine(): + engine_url = make_mysql_engine_url(configuration) + MysqlProxy.engine = create_database_engine( + engine_url, + configuration.mysql.get("POOL_SIZE"), + configuration.mysql.get("POOL_RECYCLE"), + ) + + @staticmethod + def get_domain_host_ids(domain_list_resp, host_sync_proxy): + domain_host_id_dict = {} + for domain in domain_list_resp: + domain_name = domain["domainName"] + status, host_sync_infos = host_sync_proxy.get_domain_host_sync_status( + domain_name + ) + if status != SUCCEED or not host_sync_infos: + continue + host_ids = [host_sync["host_id"] for host_sync in host_sync_infos] + domain_host_id_dict[domain_name] = host_ids + return domain_host_id_dict + + @staticmethod + def get_all_host_infos(): + host_infos_result = {} + proxy = HostProxy() + proxy.connect() + status, host_list = proxy.get_host_info( + {"username": "admin", "host_list": list()}, True + ) + if status != state.SUCCEED: + return {} + for host in host_list: + host_infos_result[host["host_id"]] = host + return host_infos_result + + @staticmethod + def compare_conf(expected_confs_resp, domain_result): + headers = {"Content-Type": "application/json"} + # 获取所有的domain + domain_conf_diff_url = ConfTools.load_url_by_conf().get("domain_conf_diff_url") + # 调用ragdoll接口比对 + try: + request_data = { + "expectedConfsResp": expected_confs_resp, + "domainResult": domain_result, + } + domain_diff_response = requests.post( + domain_conf_diff_url, data=json.dumps(request_data), headers=headers + ) + domain_diff_resp = json.loads(domain_diff_response.text) + if domain_diff_resp.get("data"): + return domain_diff_resp.get("data") + return [] + except requests.exceptions.RequestException as connect_ex: + LOGGER.error(f"Failed to get domain list, an error occurred: {connect_ex}") + return [] + + @staticmethod + def update_sync_status_for_db(domain_diff_resp, host_sync_proxy): + if domain_diff_resp: + status, save_ids = host_sync_proxy.update_domain_host_sync_status( + domain_diff_resp + ) + update_result = sum(save_ids) + if status != SUCCEED or update_result == 0: + LOGGER.error("failed update host sync status data") + if update_result > 0: + LOGGER.info( + "update %s host sync status basic info succeed", update_result + ) + else: + LOGGER.info("no host sync status data need to update") + return + + def execute(self): + headers = {"Content-Type": "application/json"} + # 获取所有的domain + domain_list_url = ConfTools.load_url_by_conf().get("domain_list_url") + try: + domain_list_response = requests.post(domain_list_url, data=json.dumps({}), headers=headers) + domain_list_resp = json.loads(domain_list_response.text) + except requests.exceptions.RequestException as connect_ex: + LOGGER.error(f"Failed to get domain list, an error occurred: {connect_ex}") + return + # 处理响应 + if not domain_list_resp.get("data"): + LOGGER.error( + "Failed to get all domain, please check interface /domain/queryDomain" + ) + return + + # 调用ragdoll query_excepted_confs接口获取所有业务域的基线配置内容 + domain_list_url = ConfTools.load_url_by_conf().get("expected_confs_url") + domain_names = {"domainNames": domain_list_resp.get("data")} + try: + expected_confs_response = requests.post( + domain_list_url, data=json.dumps(domain_names), headers=headers + ) + expected_confs_resp = json.loads(expected_confs_response.text) + except requests.exceptions.RequestException as connect_ex: + LOGGER.error( + f"Failed to get all domain expected conf list, an error occurred: {connect_ex}" + ) + return + if not expected_confs_resp.get("data"): + LOGGER.error( + "Failed to get all domain confs, please check interface /confs/queryExpectedConfs" + ) + return + + # 方式一 创建数据引擎 + self.make_database_engine() + # 方式一 根据domain获取所有的id,从host_conf_sync_status表中读取 + host_sync_proxy = HostSyncProxy() + host_sync_proxy.connect() + domain_host_id_dict = self.get_domain_host_ids( + domain_list_resp.get("data"), host_sync_proxy + ) + if not domain_host_id_dict: + LOGGER.info("no host sync status data need to update") + return + # 获取所有admin下面的ip的信息 + host_infos_result = self.get_all_host_infos() + if not host_infos_result: + LOGGER.info("no host sync status data need to update") + return + + # 方式一 组装参数并调用CollectConfig接口get_file_content获取文件真实内容 + domain_paths = {} + self.get_domain_files(domain_paths, expected_confs_resp.get("data")) + + domain_result = {} + for domain_name, host_id_list in domain_host_id_dict.items(): + data = {"infos": []} + file_paths = domain_paths.get(domain_name) + if file_paths: + for host_id in host_id_list: + data_info = {"host_id": host_id, "config_list": file_paths} + data["infos"].append(data_info) + if data["infos"]: + result = self.collect_file_infos(data, host_infos_result) + domain_result[domain_name] = result + # 调用ragdoll接口进行对比 + domain_diff_resp = self.compare_conf(expected_confs_resp.get("data"), domain_result) + # 根据结果更新数据库 + self.update_sync_status_for_db(domain_diff_resp, host_sync_proxy) diff --git a/zeus/database/__init__.py b/zeus/database/__init__.py index 2b8a163..2077be7 100644 --- a/zeus/database/__init__.py +++ b/zeus/database/__init__.py @@ -15,7 +15,6 @@ Time: Author: Description: """ -from flask import g from sqlalchemy.orm import sessionmaker from sqlalchemy.orm.scoping import scoped_session from vulcanus.database.helper import make_mysql_engine_url diff --git a/zeus/database/proxy/conf_trace.py b/zeus/database/proxy/conf_trace.py new file mode 100644 index 0000000..e0e032f --- /dev/null +++ b/zeus/database/proxy/conf_trace.py @@ -0,0 +1,238 @@ +#!/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: conf_trace.py +@Time: 2024/4/23 14:24 +@Author: JiaoSiMao +Description: +""" +import datetime +import json +import math +import uuid + +import sqlalchemy +from sqlalchemy import desc, asc, func + +from vulcanus.database.proxy import MysqlProxy +from vulcanus.log.log import LOGGER +from vulcanus.restful.resp.state import ( + DATABASE_INSERT_ERROR, + SUCCEED, DATABASE_QUERY_ERROR, DATABASE_DELETE_ERROR, +) + +from zeus.database.table import ConfTraceInfo + + +class ConfTraceProxy(MysqlProxy): + """ + Conf trace related table operation + """ + + def add_conf_trace_info(self, data): + """ + add conf trace info to table + + Args: + data: parameter, e.g. + { + "domain_name": "aops", + "host_id": 1, + "conf_name": "/etc/hostname", + "info": "" + } + + Returns: + int: SUCCEED or DATABASE_INSERT_ERROR + """ + domain_name = data.get('domain_name') + host_id = int(data.get('host_id')) + conf_name = data.get('file') + info = json.dumps(data) + conf_trace_info = ConfTraceInfo(UUID=str(uuid.uuid4()), domain_name=domain_name, host_id=host_id, + conf_name=conf_name, info=info, create_time=datetime.datetime.now()) + try: + + self.session.add(conf_trace_info) + self.session.commit() + LOGGER.info( + f"add {conf_trace_info.domain_name} {conf_trace_info.host_id} {conf_trace_info.conf_name} conf trace " + f"info succeed") + return SUCCEED + except sqlalchemy.exc.SQLAlchemyError as error: + LOGGER.error(error) + LOGGER.error( + f"add {conf_trace_info.domain_name} {conf_trace_info.host_ip} {conf_trace_info.conf_name} conf trace " + f"info fail") + self.session.rollback() + return DATABASE_INSERT_ERROR + + def query_conf_trace_info(self, data): + """ + query conf trace info from table + + Args: + data: parameter, e.g. + { + "domain_name": "aops", + "host_id": 1, + "conf_name": "/etc/hostname", + } + + Returns: + int: SUCCEED or DATABASE_INSERT_ERROR + """ + result = {} + try: + result = self._sort_trace_info_by_column(data) + self.session.commit() + LOGGER.debug("query conf trace info succeed") + return SUCCEED, result + except sqlalchemy.exc.SQLAlchemyError as error: + LOGGER.error(error) + LOGGER.error("query conf trace info fail") + return DATABASE_QUERY_ERROR, result + + def delete_conf_trace_info(self, data): + """ + delete conf trace info from table + + Args: + data: parameter, e.g. + { + "domain_name": "aops", + "host_ids": [1] + } + + Returns: + int: SUCCEED or DATABASE_INSERT_ERROR + """ + domainName = data['domain_name'] + host_ids = data['host_ids'] + try: + # delete matched conf trace info + if host_ids: + conf_trace_filters = {ConfTraceInfo.host_id.in_(host_ids), ConfTraceInfo.domain_name == domainName} + else: + conf_trace_filters = {ConfTraceInfo.domain_name == domainName} + confTraceInfos = self.session.query(ConfTraceInfo).filter(*conf_trace_filters).all() + for confTraceInfo in confTraceInfos: + self.session.delete(confTraceInfo) + self.session.commit() + return SUCCEED + except sqlalchemy.exc.SQLAlchemyError as error: + LOGGER.error(error) + LOGGER.error("delete conf trace info fail") + self.session.rollback() + return DATABASE_DELETE_ERROR + + @staticmethod + def _get_conf_trace_filters(data): + """ + Generate filters + + Args: + data(dict) + + Returns: + set + """ + domain_name = data.get('domain_name') + host_id = data.get('host_id') + conf_name = data.get('conf_name') + filters = {ConfTraceInfo.host_id > 0} + if domain_name: + filters.add(ConfTraceInfo.domain_name == domain_name) + if host_id: + filters.add(ConfTraceInfo.host_id == host_id) + if conf_name: + filters.add(ConfTraceInfo.conf_name == conf_name) + return filters + + def _get_conf_trace_count(self, filters): + """ + Query according to filters + + Args: + filters(set): query filters + + Returns: + int + """ + total_count = self.session.query(func.count(ConfTraceInfo.UUID)).filter(*filters).scalar() + return total_count + + def _sort_trace_info_by_column(self, data): + """ + Sort conf trace info by specified column + + Args: + data(dict): sorted condition info + + Returns: + dict + """ + result = {"total_count": 0, "total_page": 0, "conf_trace_infos": []} + sort = data.get('sort') + direction = desc if data.get('direction') == 'desc' else asc + page = data.get('page') + per_page = data.get('per_page') + total_page = 1 + filters = self._get_conf_trace_filters(data) + total_count = self._get_conf_trace_count(filters) + if total_count == 0: + return result + + if sort: + if page and per_page: + total_page = math.ceil(total_count / per_page) + conf_trace_infos = ( + self.session.query(ConfTraceInfo) + .filter(*filters) + .order_by(direction(getattr(ConfTraceInfo, sort))) + .offset((page - 1) * per_page) + .limit(per_page) + .all() + ) + else: + conf_trace_infos = self.session.query(ConfTraceInfo).filter(*filters).order_by( + direction(getattr(ConfTraceInfo, sort))).all() + else: + if page and per_page: + total_page = math.ceil(total_count / per_page) + conf_trace_infos = self.session.query(ConfTraceInfo).filter(*filters).offset( + (page - 1) * per_page).limit(per_page).all() + else: + conf_trace_infos = self.session.query(ConfTraceInfo).filter(*filters).all() + + LOGGER.error(f"conf_trace_infos is {conf_trace_infos}") + for conf_trace_info in conf_trace_infos: + info_dict = json.loads(conf_trace_info.info) + info_str = f"进程:{info_dict.get('cmd')} 修改了文件:{info_dict.get('file')}" + ptrace_data = "=> ".join(f"{item['cmd']}:{item['pid']}" for item in info_dict.get('ptrace')) + ptrace = f"{info_dict.get('cmd')} => {ptrace_data}" + conf_trace_info = { + "UUID": conf_trace_info.UUID, + "domain_name": conf_trace_info.domain_name, + "host_id": conf_trace_info.host_id, + "conf_name": conf_trace_info.conf_name, + "info": info_str, + "create_time": str(conf_trace_info.create_time), + "ptrace": ptrace + } + result["conf_trace_infos"].append(conf_trace_info) + + result["total_page"] = total_page + result["total_count"] = total_count + LOGGER.error(f"result is {result}") + return result diff --git a/zeus/database/proxy/host.py b/zeus/database/proxy/host.py index 2e4a6ce..1dad1fd 100644 --- a/zeus/database/proxy/host.py +++ b/zeus/database/proxy/host.py @@ -278,6 +278,7 @@ class HostProxy(MysqlProxy): "management": host.management, "scene": host.scene, "os_version": host.os_version, + "status": host.status, "ssh_port": host.ssh_port, } result['host_infos'].append(host_info) @@ -346,6 +347,64 @@ class HostProxy(MysqlProxy): LOGGER.error("query host %s basic info fail", host_list) return DATABASE_QUERY_ERROR, result + def get_host_info_by_host_id(self, data): + """ + Get host basic info according to host id from table + + Args: + data(dict): parameter, e.g. + { + "username": "admin" + "host_list": ["id1", "id2"] + } + is_collect_file (bool) + + Returns: + int: status code + dict: query result + """ + host_list = data.get('host_list') + result = [] + query_fields = [ + Host.host_id, + Host.host_name, + Host.host_ip, + Host.os_version, + Host.ssh_port, + Host.host_group_name, + Host.management, + Host.status, + Host.scene, + Host.pkey, + Host.ssh_user + ] + filters = set() + if host_list: + filters.add(Host.host_id.in_(host_list)) + try: + hosts = self.session.query(*query_fields).filter(*filters).all() + for host in hosts: + host_info = { + "host_id": host.host_id, + "host_group_name": host.host_group_name, + "host_name": host.host_name, + "host_ip": host.host_ip, + "management": host.management, + "status": host.status, + "scene": host.scene, + "os_version": host.os_version, + "ssh_port": host.ssh_port, + "pkey": host.pkey, + "ssh_user": host.ssh_user, + } + result.append(host_info) + LOGGER.debug("query host %s basic info succeed", host_list) + return SUCCEED, result + except sqlalchemy.exc.SQLAlchemyError as error: + LOGGER.error(error) + LOGGER.error("query host %s basic info fail", host_list) + return DATABASE_QUERY_ERROR, result + def get_host_ssh_info(self, data): """ Get host ssh info according to host id from table diff --git a/zeus/database/proxy/host_sync_status.py b/zeus/database/proxy/host_sync_status.py new file mode 100644 index 0000000..7f4e165 --- /dev/null +++ b/zeus/database/proxy/host_sync_status.py @@ -0,0 +1,208 @@ +#!/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. +# ******************************************************************************/ +""" +Time: +Author: +Description: Host table operation +""" +from typing import Tuple + +import sqlalchemy +from vulcanus.database.proxy import MysqlProxy +from vulcanus.log.log import LOGGER +from vulcanus.restful.resp.state import ( + DATABASE_DELETE_ERROR, + DATABASE_INSERT_ERROR, + SUCCEED, DATABASE_QUERY_ERROR, +) +from zeus.database.table import HostSyncStatus + + +class HostSyncProxy(MysqlProxy): + """ + Host related table operation + """ + + def add_host_sync_status(self, data) -> int: + """ + add host to table + + Args: + host_sync_status: parameter, e.g. + { + "host_id": 1, + "host_ip": "192.168.1.1", + "domain_name": "aops", + "sync_status": 0 + } + + Returns: + int: SUCCEED or DATABASE_INSERT_ERROR + """ + host_id = data.get('host_id') + host_ip = str(data.get('host_ip')) + domain_name = data.get('domain_name') + sync_status = data.get('sync_status') + host_sync_status = HostSyncStatus(host_id=host_id, host_ip=host_ip, domain_name=domain_name, + sync_status=sync_status) + try: + + self.session.add(host_sync_status) + self.session.commit() + LOGGER.info(f"add {host_sync_status.domain_name} {host_sync_status.host_ip} host sync status succeed") + return SUCCEED + except sqlalchemy.exc.SQLAlchemyError as error: + LOGGER.error(error) + LOGGER.error(f"add {host_sync_status.domain_name} {host_sync_status.host_ip} host sync status fail") + self.session.rollback() + return DATABASE_INSERT_ERROR + + def add_host_sync_status_batch(self, host_sync_list: list) -> str: + """ + Add host to the table in batches + + Args: + host_sync_list(list): list of host sync status object + + Returns: + str: SUCCEED or DATABASE_INSERT_ERROR + """ + try: + self.session.bulk_save_objects(host_sync_list) + self.session.commit() + LOGGER.info(f"add host {[host_sync_status.host_ip for host_sync_status in host_sync_list]} succeed") + return SUCCEED + except sqlalchemy.exc.SQLAlchemyError as error: + LOGGER.error(error) + self.session.rollback() + return DATABASE_INSERT_ERROR + + def delete_host_sync_status(self, data): + """ + Delete host from table + + Args: + data(dict): parameter, e.g. + { + "host_id": 1, + "domain_name": "aops", + } + + Returns: + int + """ + host_id = data['host_id'] + domain_name = data['domain_name'] + try: + # query matched host sync status + hostSyncStatus = self.session.query(HostSyncStatus). \ + filter(HostSyncStatus.host_id == host_id). \ + filter(HostSyncStatus.domain_name == domain_name). \ + all() + for host_sync in hostSyncStatus: + self.session.delete(host_sync) + self.session.commit() + LOGGER.info(f"delete {domain_name} {host_id} host sync status succeed") + return SUCCEED + except sqlalchemy.exc.SQLAlchemyError as error: + LOGGER.error(error) + LOGGER.error("delete host sync status fail") + self.session.rollback() + return DATABASE_DELETE_ERROR + + def delete_all_host_sync_status(self, data): + """ + Delete host from table + + Args: + data(dict): parameter, e.g. + { + "host_ids": [1], + "domain_name": "aops", + } + + Returns: + int + """ + host_ids = data['host_ids'] + domain_name = data['domain_name'] + try: + # query matched host sync status + if host_ids: + host_conf_sync_filters = {HostSyncStatus.host_id.in_(host_ids), + HostSyncStatus.domain_name == domain_name} + else: + host_conf_sync_filters = {HostSyncStatus.domain_name == domain_name} + hostSyncStatus = self.session.query(HostSyncStatus). \ + filter(*host_conf_sync_filters). \ + all() + for host_sync in hostSyncStatus: + self.session.delete(host_sync) + self.session.commit() + LOGGER.info(f"delete {domain_name} {host_ids} host sync status succeed") + return SUCCEED + except sqlalchemy.exc.SQLAlchemyError as error: + LOGGER.error(error) + LOGGER.error("delete host sync status fail") + self.session.rollback() + return DATABASE_DELETE_ERROR + + def get_host_sync_status(self, data) -> Tuple[int, dict]: + host_id = data['host_id'] + domain_name = data['domain_name'] + try: + host_sync_status = self.session.query(HostSyncStatus). \ + filter(HostSyncStatus.host_id == host_id). \ + filter(HostSyncStatus.domain_name == domain_name).one_or_none() + return SUCCEED, host_sync_status + except sqlalchemy.exc.SQLAlchemyError as error: + LOGGER.error(error) + return DATABASE_QUERY_ERROR, {} + + def get_domain_host_sync_status(self, domain_name: str): + try: + host_sync_status = self.session.query(HostSyncStatus). \ + filter(HostSyncStatus.domain_name == domain_name).all() + result = [] + for host_sync in host_sync_status: + single_host_sync_status = { + "host_id": host_sync.host_id, + "host_ip": host_sync.host_ip, + "domain_name": host_sync.domain_name, + "sync_status": host_sync.sync_status + } + result.append(single_host_sync_status) + self.session.commit() + LOGGER.debug("query host sync status %s basic info succeed", result) + return SUCCEED, result + except sqlalchemy.exc.SQLAlchemyError as error: + LOGGER.error(error) + return DATABASE_QUERY_ERROR, [] + + def update_domain_host_sync_status(self, domain_diff_resp: list): + try: + saved_ids = [] + for domain_diff in domain_diff_resp: + update_count = self.session.query(HostSyncStatus).filter( + HostSyncStatus.host_id == domain_diff.get("host_id")). \ + filter(HostSyncStatus.domain_name == domain_diff.get("domain_name")).update(domain_diff) + saved_ids.append(update_count) + self.session.commit() + LOGGER.debug("update host sync status { %s, %s }basic info succeed", domain_diff.get("host_id"), + domain_diff.get("domain_name")) + if saved_ids: + return SUCCEED, saved_ids + return DATABASE_QUERY_ERROR, [] + except sqlalchemy.exc.SQLAlchemyError as error: + LOGGER.error(error) + return DATABASE_QUERY_ERROR, [] diff --git a/zeus/database/table.py b/zeus/database/table.py index 9cf604b..e9c20ec 100644 --- a/zeus/database/table.py +++ b/zeus/database/table.py @@ -15,7 +15,9 @@ Time: Author: Description: mysql tables """ -from sqlalchemy import Column, ForeignKey +import datetime + +from sqlalchemy import Column, ForeignKey, DateTime, Text from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship from sqlalchemy.sql.sqltypes import Boolean, Integer, String @@ -132,6 +134,33 @@ class Auth(Base, MyBase): username = Column(String(40), ForeignKey('user.username')) +class HostSyncStatus(Base, MyBase): + """ + HostSyncStatus table + """ + + __tablename__ = "host_conf_sync_status" + + host_id = Column(Integer, primary_key=True) + host_ip = Column(String(16), nullable=False) + domain_name = Column(String(16), primary_key=True) + sync_status = Column(Integer, default=0) + + +class ConfTraceInfo(Base, MyBase): + """ + ConfTraceInfo table + """ + __tablename__ = "conf_trace_info" + + UUID = Column(String(36), primary_key=True) + domain_name = Column(String(16)) + host_id = Column(Integer) + conf_name = Column(String(100)) + info = Column(Text) + create_time = Column(DateTime, default=datetime.datetime) + + def create_utils_tables(base, engine): """ Create basic database tables, e.g. user, host, hostgroup @@ -142,6 +171,6 @@ def create_utils_tables(base, engine): engine (instance): _engine.Engine instance """ # pay attention, the sequence of list is important. Base table need to be listed first. - tables = [User, HostGroup, Host, Auth] + tables = [User, HostGroup, Host, Auth, HostSyncStatus, ConfTraceInfo] tables_objects = [base.metadata.tables[table.__tablename__] for table in tables] create_tables(base, engine, tables=tables_objects) diff --git a/zeus/function/verify/conf_trace.py b/zeus/function/verify/conf_trace.py new file mode 100644 index 0000000..932c369 --- /dev/null +++ b/zeus/function/verify/conf_trace.py @@ -0,0 +1,70 @@ +#!/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: conf_trace.py +@Time: 2024/4/19 10:23 +@Author: JiaoSiMao +Description: +""" +from marshmallow import Schema, fields, validate + + +class ConfTraceMgmtSchema(Schema): + """ + validators for parameter of /conftrace/mgmt + """ + host_ids = fields.List(fields.Integer(), required=True) + action = fields.Str(required=True, validate=validate.OneOf(['stop', 'start', 'update'])) + conf_files = fields.List(fields.String(), required=False) + domain_name = fields.String(required=True, validate=lambda s: len(s) > 0) + + +class PtraceSchema(Schema): + cmd = fields.String(required=True, validate=lambda s: len(s) > 0) + pid = fields.Integer(required=True) + + +class ConfTraceDataSchema(Schema): + """ + validators for parameter of /conftrace/data + """ + host_id = fields.Integer(required=True, validate=lambda s: s > 0) + domain_name = fields.String(required=True, validate=lambda s: len(s) > 0) + file = fields.String(required=True, validate=lambda s: len(s) > 0) + syscall = fields.String(required=True) + pid = fields.Integer(required=True, validate=lambda s: s > 0) + inode = fields.Integer(required=True) + cmd = fields.String(required=True, validate=lambda s: len(s) > 0) + ptrace = fields.List(fields.Nested(PtraceSchema()), required=True) + flag = fields.Integer(required=True) + + +class ConfTraceQuerySchema(Schema): + """ + validators for parameter of /conftrace/query + """ + domain_name = fields.String(required=True, validate=lambda s: len(s) > 0) + host_id = fields.Integer(required=True, validate=lambda s: s > 0) + conf_name = fields.String(required=True, validate=lambda s: len(s) > 0) + sort = fields.String(required=False, validate=validate.OneOf(["create_time", "host_id", ""])) + direction = fields.String(required=False, validate=validate.OneOf(["desc", "asc"])) + page = fields.Integer(required=False, validate=lambda s: s > 0) + per_page = fields.Integer(required=False, validate=lambda s: 50 > s > 0) + + +class ConfTraceDataDeleteSchema(Schema): + """ + validators for parameter of /conftrace/delete + """ + domain_name = fields.String(required=True, validate=lambda s: len(s) > 0) + host_ids = fields.List(fields.Integer(), required=False) diff --git a/zeus/function/verify/config.py b/zeus/function/verify/config.py index 1ef7b97..021a45f 100644 --- a/zeus/function/verify/config.py +++ b/zeus/function/verify/config.py @@ -53,3 +53,18 @@ class ObjectFileConfigSchema(Schema): """ host_id = fields.Integer(required=True, validate=lambda s: s > 0) file_directory = fields.String(required=True, validate=lambda s: len(s) > 0) + + +class SingleSyncConfig(Schema): + file_path = fields.String(required=True, validate=lambda s: len(s) > 0) + content = fields.String(required=True, validate=lambda s: len(s) > 0) + + +class BatchSyncConfigSchema(Schema): + """ + validators for SyncConfigSchema + """ + host_ids = fields.List(fields.Integer(required=True, validate=lambda s: s > 0), required=True, + validate=lambda s: len(s) > 0) + file_path_infos = fields.List(fields.Nested(SingleSyncConfig(), required=True), required=True, + validate=lambda s: len(s) > 0) diff --git a/zeus/function/verify/host.py b/zeus/function/verify/host.py index 7dedfee..48c434c 100644 --- a/zeus/function/verify/host.py +++ b/zeus/function/verify/host.py @@ -149,3 +149,39 @@ class UpdateHostSchema(Schema): host_group_name = fields.String(required=False, validate=lambda s: 20 >= len(s) > 0) management = fields.Boolean(required=False, truthy={True}, falsy={False}) ssh_pkey = fields.String(required=False, validate=lambda s: 4096 >= len(s) >= 0) + + +class AddHostSyncStatusSchema(Schema): + """ + validators for parameter of /manage/host/sync/status/add + """ + + host_id = fields.Integer(required=True, validate=lambda s: s > 0) + host_ip = fields.IP(required=True) + domain_name = fields.String(required=True, validate=lambda s: len(s) > 0) + sync_status = fields.Integer(required=True, validate=lambda s: s >= 0) + + +class DeleteHostSyncStatusSchema(Schema): + """ + validators for parameter of /manage/host/sync/status/delete + """ + + host_id = fields.Integer(required=True, validate=lambda s: s > 0) + domain_name = fields.String(required=True, validate=lambda s: len(s) > 0) + + +class DeleteAllHostSyncStatusSchema(Schema): + """ + validators for parameter of /manage/host/sync/status/delete + """ + + host_ids = fields.List(fields.Integer(), required=False) + domain_name = fields.String(required=True, validate=lambda s: len(s) > 0) + + +class GetHostSyncStatusSchema(Schema): + """ + validators for parameter of /manage/host/sync/status/get + """ + domain_name = fields.String(required=True) diff --git a/zeus/host_manager/ssh.py b/zeus/host_manager/ssh.py index a4e7628..fa6d2c0 100644 --- a/zeus/host_manager/ssh.py +++ b/zeus/host_manager/ssh.py @@ -12,14 +12,14 @@ # ******************************************************************************/ import socket from io import StringIO -from typing import Tuple +from typing import Tuple, Union import paramiko from vulcanus.log.log import LOGGER from vulcanus.restful.resp import state -__all__ = ["SSH", "generate_key", "execute_command_and_parse_its_result"] +__all__ = ["SSH", "InteroperableSSH", "generate_key", "execute_command_and_parse_its_result"] from zeus.function.model import ClientConnectArgs @@ -57,13 +57,7 @@ class SSH: """ def __init__(self, ip, username, port, password=None, pkey=None): - self._client_args = { - 'hostname': ip, - 'username': username, - 'port': port, - "password": password, - "pkey": pkey - } + self._client_args = {'hostname': ip, 'username': username, 'port': port, "password": password, "pkey": pkey} self._client = self.client() def client(self): @@ -77,15 +71,15 @@ class SSH: def execute_command(self, command: str, timeout: float = None) -> tuple: """ - create a ssh client, execute command and parse result + create a ssh client, execute command and parse result - Args: - command(str): shell command - timeout(float): the maximum time to wait for the result of command execution + Args: + command(str): shell command + timeout(float): the maximum time to wait for the result of command execution - Returns: - tuple: - status, result, error message + Returns: + tuple: + status, result, error message """ open_channel = self._client.get_transport().open_session(timeout=timeout) open_channel.set_combine_stderr(False) @@ -102,6 +96,94 @@ class SSH: self._client.close() +class InteroperableSSH: + """ + An interactive SSH client used to run command in remote node + + Attributes: + ip(str): host ip address, the field is used to record ip information in method paramiko.SSHClient() + username(str): remote login user + port(int or str): remote login port + password(str) + pkey(str): RSA-KEY string + + Notes: + In this project, the password field is used when connect to the host for the first + time, and the pkey field is used when need to execute the command on the client. + """ + + def __init__( + self, + ip: str, + port: Union[int, str], + username: str = 'root', + password: str = None, + pkey: str = None, + channel_timeout=1000, + recv_buffer: int = 4096, + ) -> None: + self.__client = paramiko.SSHClient() + self.__client.load_system_host_keys() + self.__client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + self.__client.connect( + hostname=ip, + port=int(port), + username=username, + password=password, + pkey=paramiko.RSAKey.from_private_key(StringIO(pkey)), + timeout=5, + ) + + # Open an SSH channel and start a shell + self.__chan = self.__client.get_transport().open_session() + self.__chan.get_pty() + self.__chan.invoke_shell() + self.__chan.settimeout(channel_timeout) + + self.buffer = recv_buffer + + @property + def is_active(self): + """ + Returns the current status of the SSH connection. + + Returns True if the connection is active, False otherwise. + """ + return self.__client.get_transport().is_active() + + def send(self, cmd: str): + """ + Sends a command to the SSH channel. + + cmd: The command to send, e.g., 'ls -l'. + """ + self.__chan.send(cmd) + + def recv(self) -> str: + """ + Receives data from the SSH channel and decodes it into a UTF-8 string. + + Returns: The received data, e.g., 'file1\nfile2\n'. + """ + + return self.__chan.recv(self.buffer).decode("utf-8") + + def close(self): + """ + close open_channel + """ + self.__client.close() + + def resize(self, cols: int, rows: int): + """ + Resizes the terminal size of the SSH channel. + + cols: The number of columns for the terminal. + rows: The number of rows for the terminal. + """ + self.__chan.resize_pty(width=cols, height=rows) + + def execute_command_and_parse_its_result(connect_args: ClientConnectArgs, command: str) -> tuple: """ create a ssh client, execute command and parse result @@ -116,14 +198,13 @@ def execute_command_and_parse_its_result(connect_args: ClientConnectArgs, comman status, result """ if not connect_args.pkey: - return state.SSH_AUTHENTICATION_ERROR, f"ssh authentication failed when connect host " \ - f"{connect_args.host_ip}" + return state.SSH_AUTHENTICATION_ERROR, f"ssh authentication failed when connect host " f"{connect_args.host_ip}" try: client = SSH( ip=connect_args.host_ip, username=connect_args.ssh_user, port=connect_args.ssh_port, - pkey=paramiko.RSAKey.from_private_key(StringIO(connect_args.pkey)) + pkey=paramiko.RSAKey.from_private_key(StringIO(connect_args.pkey)), ) exit_status, stdout, stderr = client.execute_command(command, connect_args.timeout) except socket.error as error: @@ -155,14 +236,13 @@ def execute_command_sftp_result(connect_args: ClientConnectArgs, local_path=None """ global sftp_client, client if not connect_args.pkey: - return state.SSH_AUTHENTICATION_ERROR, f"ssh authentication failed when connect host " \ - f"{connect_args.host_ip}" + return state.SSH_AUTHENTICATION_ERROR, f"ssh authentication failed when connect host " f"{connect_args.host_ip}" try: client = SSH( ip=connect_args.host_ip, username=connect_args.ssh_user, port=connect_args.ssh_port, - pkey=paramiko.RSAKey.from_private_key(StringIO(connect_args.pkey)) + pkey=paramiko.RSAKey.from_private_key(StringIO(connect_args.pkey)), ) sftp_client = client.client().open_sftp() diff --git a/zeus/host_manager/terminal.py b/zeus/host_manager/terminal.py new file mode 100644 index 0000000..e9ce452 --- /dev/null +++ b/zeus/host_manager/terminal.py @@ -0,0 +1,250 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2022-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. +# ******************************************************************************/ +import sqlalchemy +from flask import request +from flask_socketio import SocketIO, Namespace, join_room, leave_room +from vulcanus.log.log import LOGGER +from vulcanus.exceptions import DatabaseConnectionFailed +from zeus.database.proxy.host import HostProxy +from zeus.host_manager.utils.sockets import XtermRoom +from zeus.database.table import Host +from zeus.host_manager.ssh import InteroperableSSH + + +socketio = SocketIO( + cors_allowed_origins="*", + async_mode="gevent", +) + +# init singleton xterm rooms in global properties +# to avoid duplicated initializing in different sessions. +socket_room = XtermRoom(sio=socketio) + + +class TerminalNamspace(Namespace): + def on_open(self, event: dict): + """ + Handle Terminal open event + + Args + event: + ssh_info: + type: object + example: { + host_id(int): 12 + } + room: + type: string + example: abc + + Returns: None + """ + room_id = event.get("room") + ssh_info = event.get("ssh_info") + + if not room_id or not ssh_info: + self._handle_error( + "lack of room or ssh information, \ + fail to establish ssh connection" + ) + + host_info = self._get_host_info(ssh_info.get('host_id')) + + try: + joined = socket_room.join( + room_id=room_id, + namespace=self.namespace, + room_sock=InteroperableSSH( + ip=host_info.get('host_ip', '0.0.0.0'), + port=host_info.get('ssh_port', 22), + username=host_info.get('ssh_user', 'root'), + pkey=host_info.get('pkey'), + ), + ) + if not joined: + raise RuntimeError(f"could not create socket_room[{room_id}]") + join_room(room=room_id) + except Exception as error: + LOGGER.error(error) + socket_room.leave(room_id) + leave_room(room_id) + + def on_join(self, event: dict): + """ + Handle join event + + Args: + event: + room: + type: string + example: abc + + Returns: None + """ + room = event.get("room") + if not room: + LOGGER.error("lack of room token, fail to join in.") + + try: + socket_room.join(room) + join_room(room) + + except Exception as error: + LOGGER.error(error) + socket_room.leave(room) + leave_room(room) + + def on_stdin(self, event: dict): + """ + Handle stdin event + + Args: + event: + room: + type: string + .e.g: abc + data: + type: string + .e.g: 'ls -a' + Returns: None + """ + room = event.get("room") + data = event.get("data") + if not room or not data: + return + + if not socket_room.has(room): + self._handle_error(f"socket_room['{room}'] does not exist") + leave_room(room=room) + + sent = socket_room.send(room_id=room, data=data) + if not sent: + self._handle_error( + f"socket_room['{room}'] does not exist, \ + could not send data to it." + ) + + def on_leave(self, event: dict): + """ + Handle leave room event + + Args: + event: + room: + type: string + .e.g: abc + + Returns: None + """ + room = event.get("room") + if not room or not socket_room.has(room): + return + + socket_room.leave(room_id=room) + leave_room(room) + + def on_resize(self, event: dict): + """ + Handle resize event + + Args: + event: + room: + type: string + .e.g: abc + data: + type: dict + cols: + type: number + .e.g: 30 + cows: + type: number + .e.g: 30 + + Returns: None + """ + room = event.get("room") + data = event.get("data") + if not room or not data: + return + + if not socket_room.has(room): + self._handle_error(f"socket_room[{room}] does not exist") + leave_room(room) + + resized = socket_room.resize(room, data.get("cols"), data.get("rows")) + if not resized: + self._handle_error( + f"socket_room[{room}] does not exist,\ + could not send data to it." + ) + + def _get_host_info(self, host_id: int): + """ + select host_ip, ssh_port, ssh_user, pkey from host table by host id + + Args: + host_id: int e.g. 3 + + Returns: host_info + dict: e.g. + { + "host_ip": "127.0.0.1", + "ssh_port": 22, + "ssh_user": "root", + "pkey": "xxxxxxxxxxxxxxxx" + } + """ + query_fields = [ + Host.host_ip, + Host.ssh_port, + Host.pkey, + Host.ssh_user, + ] + host_info = {} + + try: + with HostProxy() as db_proxy: + host: Host = db_proxy.session.query(*query_fields).filter(Host.host_id == host_id).first() + host_info = { + "host_ip": host.host_ip, + "ssh_port": host.ssh_port, + "pkey": host.pkey, + "ssh_user": host.ssh_user, + } + LOGGER.debug("query host info %s succeed", host_info) + return host_info + except DatabaseConnectionFailed as connect_error: + LOGGER.error('connect database failed, %s', connect_error) + return host_info + except sqlalchemy.exc.SQLAlchemyError as query_error: + LOGGER.error("query host info failed %s", query_error) + return host_info + + def _handle_error(self, err: str): + """ + unified handling of exceptions + """ + LOGGER.error( + "session[ %s ] connects testbox terminal, failed: { %s }", + request.sid, + str(err), + ) + socketio.emit( + "error", + f"connect failed: {str(err)}", + namespace=self.namespace, + ) + + +socketio.on_namespace(TerminalNamspace("/terminal")) diff --git a/zeus/host_manager/utils/__init__.py b/zeus/host_manager/utils/__init__.py new file mode 100644 index 0000000..cb8be16 --- /dev/null +++ b/zeus/host_manager/utils/__init__.py @@ -0,0 +1,18 @@ +#!/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: __init__.py.py +@Time: 2024/5/29 10:13 +@Author: JiaoSiMao +Description: +""" diff --git a/zeus/host_manager/utils/sockets.py b/zeus/host_manager/utils/sockets.py new file mode 100644 index 0000000..5dfaa75 --- /dev/null +++ b/zeus/host_manager/utils/sockets.py @@ -0,0 +1,198 @@ +#!/usr/bin/python3 +# ****************************************************************************** +# Copyright (c) Huawei Technologies Co., Ltd. 2022-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. +# ******************************************************************************/ +import threading +from flask_socketio import SocketIO +from vulcanus.log.log import LOGGER +from zeus.host_manager.ssh import InteroperableSSH + + +class XtermRoom: + """ + This class represents a collection of Xterm rooms. + Each room is a unique SSH connection. + """ + + # The rooms dictionary stores all the active rooms. + # Note: This implementation is only suitable for a single-process server. + # If you need to deploy a multi-process server, consider using a database, + # middleware, or a separate service to manage the rooms. + rooms = {} + + def __init__(self, sio: SocketIO) -> None: + """ + Initialize the XtermRooms instance. + + sio: The SocketIO instance used for communication. + """ + self.sio = sio + self.stop_event = threading.Event() + + def has(self, room_id: str) -> bool: + """ + Check if a room with the given ID exists and is active. + + room_id: The ID of the room to check. + + Returns: True if the room exists and is active, False otherwise. + """ + + room_info = self.rooms.get(room_id) + + if ( + not room_info + or not room_info["socket"].is_active + or room_info["conns"] < 1 + or not room_info["thread"].is_alive() + ): + self._del(room_id) + return False + + return True + + def send(self, room_id: str, data: str) -> bool: + """ + Sends data to the room with the given ID. + + room_id: The ID of the room to send data to. + data: The data to send. + + Returns: True if the operation is successful, False otherwise. + """ + + if not self.rooms.get(room_id): + return False + + self.rooms[room_id]["socket"].send(data) + return True + + def join(self, room_id: str, namespace: str = None, room_sock=None) -> bool: + """ + Join a room with the given ID. If the room does not exist, + create it. + + room_id: The ID of the room to join. + room_sock: The socket of the room to join. + If None, a new socket will be created. + + Returns: True if the operation is successful, False otherwise. + """ + if not self.rooms.get(room_id): + return self._add(room_id, namespace, room_sock) + + self.rooms[room_id]["conns"] += 1 + self.rooms[room_id]["socket"].send("") + return True + + def leave(self, room_id: str) -> bool: + """ + Leave a room with the given ID. If the room is empty after leaving, + delete it. + + room_id: The ID of the room to leave. + + Returns: True if the operation is successful, False otherwise. + """ + if not self.rooms.get(room_id) or self.rooms[room_id]["conns"] < 1: + return False + + self.rooms[room_id]["conns"] -= 1 + if self.rooms[room_id]["conns"] == 0: + return self._del(room_id) + + return True + + def resize(self, room_id: str, cols: int, rows: int) -> bool: + """ + Resizes the terminal size of the room with the given ID. + + room_id: The ID of the room to resize. + cols: The number of columns for the terminal. + rows: The number of rows for the terminal. + + Returns: True if the operation is successful, False otherwise. + """ + if not self.rooms.get(room_id): + return False + + self.rooms[room_id]["socket"].resize(cols, rows) + return True + + def _add(self, room_id: str, namespace: str, room_sock=None) -> bool: + """ + Add a new room with the given ID and socket. + + room_id: The ID of the room to add. + namespace: The namespace of the room to add. + room_sock: The socket of the room to add. + + Returns: True if the operation is successful, False otherwise. + """ + + if self.rooms.get(room_id): + return False + + if not isinstance(room_sock, InteroperableSSH) or not room_sock.is_active: + return False + + self.rooms[room_id] = { + "socket": room_sock, + "conns": 1, + "thread": threading.Thread(target=self._bg_recv, args=(room_id, namespace)), + } + + self.rooms[room_id]["thread"].start() + + return True + + def _del(self, room_id: str) -> bool: + """ + Delete a room with the given ID. + + room_id: The ID of the room to delete. + + Returns: True if the operation is successful, False otherwise. + """ + room_info = self.rooms.get(room_id) + if not room_info: + return False + + try: + if room_info["socket"].is_active: + room_info["socket"].close() + except Exception as error: + LOGGER.error("Error while closing socket: %s", error) + # self.stop_event.set() # Set the event to signal thread termination + # self.rooms[room_id]["thread"].join() # Wait for the thread to finish + self.rooms.pop(room_id) + return True + + def _bg_recv(self, room_id: str, namespace: str): + """ + Continuously receive data from the room's socket in the background and + emit it to the room. + + room_id: The ID of the room to receive data from. + """ + while True: + if len(self.rooms) == 0: + break + is_active = self.rooms[room_id]["socket"].is_active + + if not is_active: + break + self.sio.emit( + "message", + self.rooms[room_id]["socket"].recv(), + namespace=namespace, + to=room_id, # Emit the received data to the room + ) diff --git a/zeus/host_manager/view.py b/zeus/host_manager/view.py index d13868c..f1cc399 100644 --- a/zeus/host_manager/view.py +++ b/zeus/host_manager/view.py @@ -35,7 +35,8 @@ from vulcanus.restful.response import BaseResponse from vulcanus.restful.serialize.validate import validate from zeus.conf.constant import CERES_HOST_INFO, HOST_TEMPLATE_FILE_CONTENT, HostStatus from zeus.database.proxy.host import HostProxy -from zeus.database.table import Host +from zeus.database.proxy.host_sync_status import HostSyncProxy +from zeus.database.table import Host, HostSyncStatus from zeus.function.model import ClientConnectArgs from zeus.function.verify.host import ( AddHostBatchSchema, @@ -47,7 +48,8 @@ from zeus.function.verify.host import ( GetHostInfoSchema, GetHostSchema, GetHostStatusSchema, - UpdateHostSchema, + UpdateHostSchema, AddHostSyncStatusSchema, DeleteHostSyncStatusSchema, GetHostSyncStatusSchema, + DeleteAllHostSyncStatusSchema ) from zeus.host_manager.ssh import SSH, execute_command_and_parse_its_result, generate_key @@ -965,3 +967,109 @@ class UpdateHost(BaseResponse): return self.response(code=state.PARAM_ERROR, message="please update password or authentication key.") return self.response(callback.update_host_info(params.pop("host_id"), params)) + + +class AddHostSyncStatus(BaseResponse): + """ + Interface for add host sync status. + Restful API: POST + """ + + def validate_host_sync_info(self, host_sync_info: dict) -> Tuple[int, dict]: + """ + query host sync status info, validate that the host sync status info is valid + return host object + + Args: + host_sync_info (dict): e.g + { + "host_id": 1, + "host_ip":"192.168.1.1", + "domain_name": "aops", + "sync_status": 0 + } + + Returns: + tuple: + status code, host sync status object + """ + status, host_sync_status = self.proxy.get_host_sync_status(host_sync_info) + if status != state.SUCCEED: + return status, HostSyncStatus() + + if host_sync_status is not None: + return state.DATA_EXIST, host_sync_status + return state.SUCCEED, {} + + @BaseResponse.handle(schema=AddHostSyncStatusSchema, proxy=HostSyncProxy, token=False) + def post(self, callback: HostSyncProxy, **params): + """ + add host sync status + + Args: + host_id (int): host id + host_ip (str): host ip + domain_name (str): domain name + sync_status (int): sync status + + Returns: + dict: response body + """ + self.proxy = callback + + status, host_sync = self.validate_host_sync_info(params) + if status != state.SUCCEED: + return self.response(code=status) + + status_code = self.proxy.add_host_sync_status(params) + return self.response(code=status_code) + + +class DeleteHostSyncStatus(BaseResponse): + @BaseResponse.handle(schema=DeleteHostSyncStatusSchema, proxy=HostSyncProxy, token=False) + def post(self, callback: HostSyncProxy, **params): + """ + Add host sync status + + Args: + host_id (int): host id + domain_name (str): domain name + + Returns: + dict: response body + """ + status_code = callback.delete_host_sync_status(params) + return self.response(code=status_code) + + +class DeleteAllHostSyncStatus(BaseResponse): + @BaseResponse.handle(schema=DeleteAllHostSyncStatusSchema, proxy=HostSyncProxy, token=False) + def post(self, callback: HostSyncProxy, **params): + """ + Add host sync status + + Args: + host_id (int): host id + domain_name (str): domain name + + Returns: + dict: response body + """ + status_code = callback.delete_all_host_sync_status(params) + return self.response(code=status_code) + + +class GetHostSyncStatus(BaseResponse): + @BaseResponse.handle(schema=GetHostSyncStatusSchema, proxy=HostSyncProxy, token=False) + def post(self, callback: HostSyncProxy, **params): + """ + get host sync status + + Args: + domain_name (str): domain name + Returns: + dict: response body + """ + domain_name = params.get("domain_name") + status_code, result = callback.get_domain_host_sync_status(domain_name) + return self.response(code=status_code, data=result) diff --git a/zeus/manage.py b/zeus/manage.py index 7aab56d..222cd3c 100644 --- a/zeus/manage.py +++ b/zeus/manage.py @@ -15,6 +15,7 @@ Time: Author: Description: Manager that start aops-zeus """ + try: from gevent import monkey @@ -22,12 +23,51 @@ try: except: pass -from vulcanus import init_application +from vulcanus import init_application, LOGGER +from vulcanus.timed import TimedTaskManager from zeus.conf import configuration from zeus.url import URLS +from zeus.conf.constant import TIMED_TASK_CONFIG_PATH +from zeus.cron import task_meta +from zeus.host_manager.terminal import socketio + + +def _init_timed_task(application): + """ + Initialize and create a scheduled task + + Args: + application:flask.Application + """ + timed_task = TimedTaskManager(app=application, config_path=TIMED_TASK_CONFIG_PATH) + if not timed_task.timed_config: + LOGGER.warning( + "If you want to start a scheduled task, please add a timed config." + ) + return + + for task_info in timed_task.timed_config.values(): + task_type = task_info.get("type") + if task_type not in task_meta: + continue + meta_class = task_meta[task_type] + timed_task.add_job(meta_class(timed_config=task_info)) + + timed_task.start() + -app = init_application(name="zeus", settings=configuration, register_urls=URLS) +def main(): + _app = init_application(name="zeus", settings=configuration, register_urls=URLS) + socketio.init_app(app=_app) + _init_timed_task(application=_app) + return _app +app = main() if __name__ == "__main__": - app.run(host=configuration.zeus.get('IP'), port=configuration.zeus.get('PORT')) + app.run(host=configuration.zeus.get("IP"), port=configuration.zeus.get("PORT")) + socketio.run( + app, + host=configuration.zeus.get("IP"), + port=configuration.zeus.get("PORT"), + ) diff --git a/zeus/url.py b/zeus/url.py index 5f00ef9..099b6b5 100644 --- a/zeus/url.py +++ b/zeus/url.py @@ -53,11 +53,21 @@ from zeus.conf.constant import ( SYNC_CONFIG, OBJECT_FILE_CONFIG, GET_HOST_STATUS, + BATCH_SYNC_CONFIG, + ADD_HOST_SYNC_STATUS, + DELETE_HOST_SYNC_STATUS, + GET_HOST_SYNC_STATUS, + CONF_TRACE_MGMT, + CONF_TRACE_DATA, + CONF_TRACE_QUERY, + CONF_TRACE_DELETE, + DELETE_ALL_HOST_SYNC_STATUS ) from zeus.config_manager import view as config_view from zeus.host_manager import view as host_view from zeus.metric_manager import view as metric_view from zeus.vulnerability_manage import view as vulnerability_view +from zeus.conftrace_manage import view as conf_trace_view URLS = [] @@ -82,6 +92,10 @@ SPECIFIC_URLS = { (host_view.GetHostInfo, QUERY_HOST_DETAIL), (host_view.GetHostCount, GET_HOST_COUNT), (host_view.GetHostTemplateFile, GET_HOST_TEMPLATE_FILE), + (host_view.AddHostSyncStatus, ADD_HOST_SYNC_STATUS), + (host_view.DeleteHostSyncStatus, DELETE_HOST_SYNC_STATUS), + (host_view.DeleteAllHostSyncStatus, DELETE_ALL_HOST_SYNC_STATUS), + (host_view.GetHostSyncStatus, GET_HOST_SYNC_STATUS) ], "HOST_GROUP_URLS": [ (host_view.AddHostGroup, ADD_GROUP), @@ -92,6 +106,7 @@ SPECIFIC_URLS = { (config_view.CollectConfig, COLLECT_CONFIG), (config_view.SyncConfig, SYNC_CONFIG), (config_view.ObjectFileConfig, OBJECT_FILE_CONFIG), + (config_view.BatchSyncConfig, BATCH_SYNC_CONFIG) ], 'AGENT_URLS': [ (agent_view.AgentPluginInfo, AGENT_PLUGIN_INFO), @@ -111,6 +126,12 @@ SPECIFIC_URLS = { (metric_view.QueryHostMetricData, QUERY_METRIC_DATA), (metric_view.QueryHostMetricList, QUERY_METRIC_LIST), ], + 'CONF_TRACE_URLS': [ + (conf_trace_view.ConfTraceMgmt, CONF_TRACE_MGMT), + (conf_trace_view.ConfTraceData, CONF_TRACE_DATA), + (conf_trace_view.ConfTraceQuery, CONF_TRACE_QUERY), + (conf_trace_view.ConfTraceDataDelete, CONF_TRACE_DELETE), + ] } for _, value in SPECIFIC_URLS.items(): diff --git a/zeus/utils/__init__.py b/zeus/utils/__init__.py new file mode 100644 index 0000000..4b94fcf --- /dev/null +++ b/zeus/utils/__init__.py @@ -0,0 +1,18 @@ +#!/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: __init__.py.py +@Time: 2024/1/22 11:42 +@Author: JiaoSiMao +Description: +""" diff --git a/zeus/utils/conf_tools.py b/zeus/utils/conf_tools.py new file mode 100644 index 0000000..4b9d073 --- /dev/null +++ b/zeus/utils/conf_tools.py @@ -0,0 +1,55 @@ +#!/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: conf_tools.py +@Time: 2024/1/22 13:38 +@Author: JiaoSiMao +Description: +""" +import ast +import configparser +import os + +from zeus.conf import MANAGER_CONFIG_PATH +from zeus.conf.constant import DOMAIN_LIST_API, EXPECTED_CONFS_API, DOMAIN_CONF_DIFF_API + + +class ConfTools(object): + + @staticmethod + def load_url_by_conf(): + """ + desc: get the url of sync conf + """ + cf = configparser.ConfigParser() + if os.path.exists(MANAGER_CONFIG_PATH): + cf.read(MANAGER_CONFIG_PATH, encoding="utf-8") + else: + parent = os.path.dirname(os.path.realpath(__file__)) + conf_path = os.path.join(parent, "../config/zeus.ini") + cf.read(conf_path, encoding="utf-8") + + update_sync_status_address = ast.literal_eval(cf.get("update_sync_status", "update_sync_status_address")) + + update_sync_status_port = str(cf.get("update_sync_status", "update_sync_status_port")) + domain_list_url = "{address}:{port}{api}".format(address=update_sync_status_address, api=DOMAIN_LIST_API, + port=update_sync_status_port) + expected_confs_url = "{address}:{port}{api}".format(address=update_sync_status_address, api=EXPECTED_CONFS_API, + port=update_sync_status_port) + domain_conf_diff_url = "{address}:{port}{api}".format(address=update_sync_status_address, + api=DOMAIN_CONF_DIFF_API, + port=update_sync_status_port) + + url = {"domain_list_url": domain_list_url, "expected_confs_url": expected_confs_url, + "domain_conf_diff_url": domain_conf_diff_url} + return url