3044 lines
114 KiB
Diff
3044 lines
114 KiB
Diff
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
|