diff --git a/add-collect-module-to-sysSentry.patch b/add-collect-module-to-sysSentry.patch new file mode 100644 index 0000000000000000000000000000000000000000..106977d8625c647c6ebe45d2fa59d1d830514d65 --- /dev/null +++ b/add-collect-module-to-sysSentry.patch @@ -0,0 +1,1151 @@ +From bd32dc01000126d593c188d47404cfdbe1df343e Mon Sep 17 00:00:00 2001 +From: zhuofeng +Date: Thu, 12 Sep 2024 11:29:01 +0800 +Subject: [PATCH 1/2] add collect module to sysSentry + +--- + config/collector.conf | 7 + + service/sentryCollector.service | 12 + + src/python/sentryCollector/__init__.py | 0 + src/python/sentryCollector/__main__.py | 17 ++ + src/python/sentryCollector/collect_config.py | 118 ++++++++ + src/python/sentryCollector/collect_io.py | 239 ++++++++++++++++ + src/python/sentryCollector/collect_plugin.py | 276 ++++++++++++++++++ + src/python/sentryCollector/collect_server.py | 285 +++++++++++++++++++ + src/python/sentryCollector/collectd.py | 99 +++++++ + src/python/setup.py | 4 +- + 10 files changed, 1056 insertions(+), 1 deletion(-) + create mode 100644 config/collector.conf + create mode 100644 service/sentryCollector.service + create mode 100644 src/python/sentryCollector/__init__.py + create mode 100644 src/python/sentryCollector/__main__.py + create mode 100644 src/python/sentryCollector/collect_config.py + create mode 100644 src/python/sentryCollector/collect_io.py + create mode 100644 src/python/sentryCollector/collect_plugin.py + create mode 100644 src/python/sentryCollector/collect_server.py + create mode 100644 src/python/sentryCollector/collectd.py + +diff --git a/config/collector.conf b/config/collector.conf +new file mode 100644 +index 0000000..9baa086 +--- /dev/null ++++ b/config/collector.conf +@@ -0,0 +1,7 @@ ++[common] ++modules=io ++ ++[io] ++period_time=1 ++max_save=10 ++disk=default +\ No newline at end of file +diff --git a/service/sentryCollector.service b/service/sentryCollector.service +new file mode 100644 +index 0000000..2e50d7a +--- /dev/null ++++ b/service/sentryCollector.service +@@ -0,0 +1,12 @@ ++[Unit] ++Description = Collection module added for sysSentry and kernel lock-free collection ++ ++[Service] ++ExecStart=/usr/bin/sentryCollector ++ExecStop=/bin/kill $MAINPID ++KillMode=process ++Restart=on-failure ++RestartSec=10s ++ ++[Install] ++WantedBy = multi-user.target +diff --git a/src/python/sentryCollector/__init__.py b/src/python/sentryCollector/__init__.py +new file mode 100644 +index 0000000..e69de29 +diff --git a/src/python/sentryCollector/__main__.py b/src/python/sentryCollector/__main__.py +new file mode 100644 +index 0000000..9c2ae50 +--- /dev/null ++++ b/src/python/sentryCollector/__main__.py +@@ -0,0 +1,17 @@ ++# coding: utf-8 ++# Copyright (c) 2023 Huawei Technologies Co., Ltd. ++# sysSentry is 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. ++ ++""" ++main ++""" ++from collectd import collectd ++ ++collectd.main() +diff --git a/src/python/sentryCollector/collect_config.py b/src/python/sentryCollector/collect_config.py +new file mode 100644 +index 0000000..b6cc75c +--- /dev/null ++++ b/src/python/sentryCollector/collect_config.py +@@ -0,0 +1,118 @@ ++# coding: utf-8 ++# Copyright (c) 2024 Huawei Technologies Co., Ltd. ++# sysSentry is 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. ++ ++""" ++Read and save collector.conf value. ++""" ++import configparser ++import logging ++import os ++import re ++ ++ ++COLLECT_CONF_PATH = "/etc/sysSentry/collector.conf" ++ ++CONF_COMMON = 'common' ++CONF_MODULES = 'modules' ++ ++# io ++CONF_IO = 'io' ++CONF_IO_PERIOD_TIME = 'period_time' ++CONF_IO_MAX_SAVE = 'max_save' ++CONF_IO_DISK = 'disk' ++CONF_IO_PERIOD_TIME_DEFAULT = 1 ++CONF_IO_MAX_SAVE_DEFAULT = 10 ++CONF_IO_DISK_DEFAULT = "default" ++ ++class CollectConfig: ++ def __init__(self, filename=COLLECT_CONF_PATH): ++ ++ self.filename = filename ++ self.modules = [] ++ self.module_count = 0 ++ self.load_config() ++ ++ def load_config(self): ++ if not os.path.exists(self.filename): ++ logging.error("%s is not exists", self.filename) ++ return ++ ++ try: ++ self.config = configparser.ConfigParser() ++ self.config.read(self.filename) ++ except configparser.Error: ++ logging.error("collectd configure file read failed") ++ return ++ ++ try: ++ common_config = self.config[CONF_COMMON] ++ modules_str = common_config[CONF_MODULES] ++ # remove space ++ modules_list = modules_str.replace(" ", "").split(',') ++ except KeyError as e: ++ logging.error("read config data failed, %s", e) ++ return ++ ++ pattern = r'^[a-zA-Z0-9-_]+$' ++ for module_name in modules_list: ++ if not re.match(pattern, module_name): ++ logging.warning("module_name: %s is invalid", module_name) ++ continue ++ if not self.config.has_section(module_name): ++ logging.warning("module_name: %s config is incorrect", module_name) ++ continue ++ self.modules.append(module_name) ++ ++ def load_module_config(self, module_name): ++ module_name = module_name.strip().lower() ++ if module_name in self.modules and self.config.has_section(module_name): ++ return {key.lower(): value for key, value in self.config[module_name].items()} ++ else: ++ raise ValueError(f"Module '{module_name}' not found in configuration") ++ ++ def get_io_config(self): ++ result_io_config = {} ++ io_map_value = self.load_module_config(CONF_IO) ++ # period_time ++ period_time = io_map_value.get(CONF_IO_PERIOD_TIME) ++ if period_time and period_time.isdigit() and int(period_time) >= 1 and int(period_time) <= 300: ++ result_io_config[CONF_IO_PERIOD_TIME] = int(period_time) ++ else: ++ logging.warning("module_name = %s section, field = %s is incorrect, use default %d", ++ CONF_IO, CONF_IO_PERIOD_TIME, CONF_IO_PERIOD_TIME_DEFAULT) ++ result_io_config[CONF_IO_PERIOD_TIME] = CONF_IO_PERIOD_TIME_DEFAULT ++ # max_save ++ max_save = io_map_value.get(CONF_IO_MAX_SAVE) ++ if max_save and max_save.isdigit() and int(max_save) >= 1 and int(max_save) <= 300: ++ result_io_config[CONF_IO_MAX_SAVE] = int(max_save) ++ else: ++ logging.warning("module_name = %s section, field = %s is incorrect, use default %d", ++ CONF_IO, CONF_IO_MAX_SAVE, CONF_IO_MAX_SAVE_DEFAULT) ++ result_io_config[CONF_IO_MAX_SAVE] = CONF_IO_MAX_SAVE_DEFAULT ++ # disk ++ disk = io_map_value.get(CONF_IO_DISK) ++ if disk: ++ disk_str = disk.replace(" ", "") ++ pattern = r'^[a-zA-Z0-9-_,]+$' ++ if not re.match(pattern, disk_str): ++ logging.warning("module_name = %s section, field = %s is incorrect, use default %s", ++ CONF_IO, CONF_IO_DISK, CONF_IO_DISK_DEFAULT) ++ disk_str = CONF_IO_DISK_DEFAULT ++ result_io_config[CONF_IO_DISK] = disk_str ++ else: ++ logging.warning("module_name = %s section, field = %s is incorrect, use default %s", ++ CONF_IO, CONF_IO_DISK, CONF_IO_DISK_DEFAULT) ++ result_io_config[CONF_IO_DISK] = CONF_IO_DISK_DEFAULT ++ logging.info("config get_io_config: %s", result_io_config) ++ return result_io_config ++ ++ def get_common_config(self): ++ return {key.lower(): value for key, value in self.config['common'].items()} +diff --git a/src/python/sentryCollector/collect_io.py b/src/python/sentryCollector/collect_io.py +new file mode 100644 +index 0000000..b826dc4 +--- /dev/null ++++ b/src/python/sentryCollector/collect_io.py +@@ -0,0 +1,239 @@ ++# coding: utf-8 ++# Copyright (c) 2024 Huawei Technologies Co., Ltd. ++# sysSentry is 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. ++ ++""" ++collect module ++""" ++import os ++import time ++import logging ++import threading ++ ++from .collect_config import CollectConfig ++ ++Io_Category = ["read", "write", "flush", "discard"] ++IO_GLOBAL_DATA = {} ++IO_CONFIG_DATA = [] ++ ++class IoStatus(): ++ TOTAL = 0 ++ FINISH = 1 ++ LATENCY = 2 ++ ++class CollectIo(): ++ ++ def __init__(self, module_config): ++ ++ io_config = module_config.get_io_config() ++ ++ self.period_time = io_config['period_time'] ++ self.max_save = io_config['max_save'] ++ disk_str = io_config['disk'] ++ ++ self.disk_map_stage = {} ++ self.window_value = {} ++ ++ self.loop_all = False ++ ++ if disk_str == "default": ++ self.loop_all = True ++ else: ++ self.disk_list = disk_str.strip().split(',') ++ ++ self.stop_event = threading.Event() ++ ++ IO_CONFIG_DATA.append(self.period_time) ++ IO_CONFIG_DATA.append(self.max_save) ++ ++ def get_blk_io_hierarchy(self, disk_name, stage_list): ++ stats_file = '/sys/kernel/debug/block/{}/blk_io_hierarchy/stats'.format(disk_name) ++ try: ++ with open(stats_file, 'r') as file: ++ lines = file.read() ++ except FileNotFoundError: ++ logging.error("The file %s does not exist", stats_file) ++ return -1 ++ except Exception as e: ++ logging.error("An error occurred3: %s", e) ++ return -1 ++ ++ curr_value = lines.strip().split('\n') ++ ++ for stage_val in curr_value: ++ stage = stage_val.split(' ')[0] ++ if (len(self.window_value[disk_name][stage])) >= 2: ++ self.window_value[disk_name][stage].pop(0) ++ ++ curr_stage_value = stage_val.split(' ')[1:-1] ++ self.window_value[disk_name][stage].append(curr_stage_value) ++ return 0 ++ ++ def append_period_lat(self, disk_name, stage_list): ++ for stage in stage_list: ++ if len(self.window_value[disk_name][stage]) < 2: ++ return ++ curr_stage_value = self.window_value[disk_name][stage][-1] ++ last_stage_value = self.window_value[disk_name][stage][-2] ++ ++ for index in range(len(Io_Category)): ++ # read=0, write=1, flush=2, discard=3 ++ if (len(IO_GLOBAL_DATA[disk_name][stage][Io_Category[index]])) >= self.max_save: ++ IO_GLOBAL_DATA[disk_name][stage][Io_Category[index]].pop() ++ ++ curr_lat = self.get_latency_value(curr_stage_value, last_stage_value, index) ++ curr_iops = self.get_iops(curr_stage_value, last_stage_value, index) ++ curr_io_length = self.get_io_length(curr_stage_value, last_stage_value, index) ++ curr_io_dump = self.get_io_dump(disk_name, stage, index) ++ ++ IO_GLOBAL_DATA[disk_name][stage][Io_Category[index]].insert(0, [curr_lat, curr_io_dump, curr_io_length, curr_iops]) ++ ++ def get_iops(self, curr_stage_value, last_stage_value, category): ++ try: ++ finish = int(curr_stage_value[category * 3 + IoStatus.FINISH]) - int(last_stage_value[category * 3 + IoStatus.FINISH]) ++ except ValueError as e: ++ logging.error("get_iops convert to int failed, %s", e) ++ return 0 ++ value = finish / self.period_time ++ if value.is_integer(): ++ return int(value) ++ else: ++ return round(value, 1) ++ ++ def get_latency_value(self, curr_stage_value, last_stage_value, category): ++ try: ++ finish = int(curr_stage_value[category * 3 + IoStatus.FINISH]) - int(last_stage_value[category * 3 + IoStatus.FINISH]) ++ lat_time = (int(curr_stage_value[category * 3 + IoStatus.LATENCY]) - int(last_stage_value[category * 3 + IoStatus.LATENCY])) ++ except ValueError as e: ++ logging.error("get_latency_value convert to int failed, %s", e) ++ return 0 ++ if finish <= 0 or lat_time <= 0: ++ return 0 ++ value = lat_time / finish / 1000 / 1000 ++ if value.is_integer(): ++ return int(value) ++ else: ++ return round(value, 1) ++ ++ def get_io_length(self, curr_stage_value, last_stage_value, category): ++ try: ++ finish = int(curr_stage_value[category * 3 + IoStatus.FINISH]) - int(last_stage_value[category * 3 + IoStatus.FINISH]) ++ except ValueError as e: ++ logging.error("get_io_length convert to int failed, %s", e) ++ return 0 ++ value = finish / self.period_time / 1000 / 1000 ++ if value.is_integer(): ++ return int(value) ++ else: ++ return round(value, 1) ++ ++ def get_io_dump(self, disk_name, stage, category): ++ io_dump_file = '/sys/kernel/debug/block/{}/blk_io_hierarchy/{}/io_dump'.format(disk_name, stage) ++ count = 0 ++ try: ++ with open(io_dump_file, 'r') as file: ++ for line in file: ++ count += line.count('.op=' + Io_Category[category]) ++ except FileNotFoundError: ++ logging.error("The file %s does not exist.", io_dump_file) ++ return count ++ except Exception as e: ++ logging.error("An error occurred1: %s", e) ++ return count ++ return count ++ ++ def extract_first_column(self, file_path): ++ column_names = [] ++ try: ++ with open(file_path, 'r') as file: ++ for line in file: ++ parts = line.strip().split() ++ if parts: ++ column_names.append(parts[0]) ++ except FileNotFoundError: ++ logging.error("The file %s does not exist.", file_path) ++ except Exception as e: ++ logging.error("An error occurred2: %s", e) ++ return column_names ++ ++ def task_loop(self): ++ if self.stop_event.is_set(): ++ logging.info("collect io thread exit") ++ return ++ ++ for disk_name, stage_list in self.disk_map_stage.items(): ++ if self.get_blk_io_hierarchy(disk_name, stage_list) < 0: ++ continue ++ self.append_period_lat(disk_name, stage_list) ++ ++ threading.Timer(self.period_time, self.task_loop).start() ++ ++ def main_loop(self): ++ logging.info("collect io thread start") ++ base_path = '/sys/kernel/debug/block' ++ for disk_name in os.listdir(base_path): ++ if not self.loop_all and disk_name not in self.disk_list: ++ continue ++ ++ disk_path = os.path.join(base_path, disk_name) ++ blk_io_hierarchy_path = os.path.join(disk_path, 'blk_io_hierarchy') ++ ++ if not os.path.exists(blk_io_hierarchy_path): ++ logging.error("no blk_io_hierarchy directory found in %s, skipping.", disk_name) ++ continue ++ ++ for file_name in os.listdir(blk_io_hierarchy_path): ++ file_path = os.path.join(blk_io_hierarchy_path, file_name) ++ ++ if file_name == 'stats': ++ stage_list = self.extract_first_column(file_path) ++ self.disk_map_stage[disk_name] = stage_list ++ self.window_value[disk_name] = {} ++ IO_GLOBAL_DATA[disk_name] = {} ++ ++ if len(self.disk_map_stage) == 0: ++ logging.warning("no disks meet the requirements. the thread exits") ++ return ++ ++ for disk_name, stage_list in self.disk_map_stage.items(): ++ for stage in stage_list: ++ self.window_value[disk_name][stage] = [] ++ IO_GLOBAL_DATA[disk_name][stage] = {} ++ for category in Io_Category: ++ IO_GLOBAL_DATA[disk_name][stage][category] = [] ++ ++ while True: ++ start_time = time.time() ++ ++ if self.stop_event.is_set(): ++ logging.info("collect io thread exit") ++ return ++ ++ for disk_name, stage_list in self.disk_map_stage.items(): ++ if self.get_blk_io_hierarchy(disk_name, stage_list) < 0: ++ continue ++ self.append_period_lat(disk_name, stage_list) ++ ++ elapsed_time = time.time() - start_time ++ sleep_time = self.period_time - elapsed_time ++ if sleep_time < 0: ++ continue ++ while sleep_time > 1: ++ if self.stop_event.is_set(): ++ logging.info("collect io thread exit") ++ return ++ time.sleep(1) ++ sleep_time -= 1 ++ time.sleep(sleep_time) ++ ++ # set stop event, notify thread exit ++ def stop_thread(self): ++ logging.info("collect io thread is preparing to exit") ++ self.stop_event.set() +diff --git a/src/python/sentryCollector/collect_plugin.py b/src/python/sentryCollector/collect_plugin.py +new file mode 100644 +index 0000000..49ce0a8 +--- /dev/null ++++ b/src/python/sentryCollector/collect_plugin.py +@@ -0,0 +1,276 @@ ++# coding: utf-8 ++# Copyright (c) 2024 Huawei Technologies Co., Ltd. ++# sysSentry is 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. ++ ++""" ++collcet plugin ++""" ++import json ++import socket ++import logging ++import re ++ ++COLLECT_SOCKET_PATH = "/var/run/sysSentry/collector.sock" ++ ++# data length param ++CLT_MSG_HEAD_LEN = 9 #3+2+4 ++CLT_MSG_PRO_LEN = 2 ++CLT_MSG_MAGIC_LEN = 3 ++CLT_MSG_LEN_LEN = 4 ++ ++CLT_MAGIC = "CLT" ++RES_MAGIC = "RES" ++ ++# disk limit ++LIMIT_DISK_CHAR_LEN = 32 ++LIMIT_DISK_LIST_LEN = 10 ++ ++# stage limit ++LIMIT_STAGE_CHAR_LEN = 20 ++LIMIT_STAGE_LIST_LEN = 15 ++ ++#iotype limit ++LIMIT_IOTYPE_CHAR_LEN = 7 ++LIMIT_IOTYPE_LIST_LEN = 4 ++ ++#period limit ++LIMIT_PERIOD_MIN_LEN = 1 ++LIMIT_PERIOD_MAX_LEN = 300 ++ ++# interface protocol ++class ClientProtocol(): ++ IS_IOCOLLECT_VALID = 0 ++ GET_IO_DATA = 1 ++ PRO_END = 3 ++ ++class ResultMessage(): ++ RESULT_SUCCEED = 0 ++ RESULT_UNKNOWN = 1 # unknown error ++ RESULT_NOT_PARAM = 2 # the parameter does not exist or the type does not match. ++ RESULT_INVALID_LENGTH = 3 # invalid parameter length. ++ RESULT_EXCEED_LIMIT = 4 # the parameter length exceeds the limit. ++ RESULT_PARSE_FAILED = 5 # parse failed ++ RESULT_INVALID_CHAR = 6 # invalid char ++ ++Result_Messages = { ++ ResultMessage.RESULT_SUCCEED: "Succeed", ++ ResultMessage.RESULT_UNKNOWN: "Unknown error", ++ ResultMessage.RESULT_NOT_PARAM: "The parameter does not exist or the type does not match", ++ ResultMessage.RESULT_INVALID_LENGTH: "Invalid parameter length", ++ ResultMessage.RESULT_EXCEED_LIMIT: "The parameter length exceeds the limit", ++ ResultMessage.RESULT_PARSE_FAILED: "Parse failed", ++ ResultMessage.RESULT_INVALID_CHAR: "Invalid char" ++} ++ ++ ++def client_send_and_recv(request_data, data_str_len, protocol): ++ """client socket send and recv message""" ++ try: ++ client_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) ++ except socket.error: ++ print("collect_plugin: client creat socket error") ++ return None ++ ++ try: ++ client_socket.connect(COLLECT_SOCKET_PATH) ++ except OSError: ++ client_socket.close() ++ print("collect_plugin: client connect error") ++ return None ++ ++ req_data_len = len(request_data) ++ request_msg = CLT_MAGIC + str(protocol).zfill(CLT_MSG_PRO_LEN) + str(req_data_len).zfill(CLT_MSG_LEN_LEN) + request_data ++ ++ try: ++ client_socket.send(request_msg.encode()) ++ res_data = client_socket.recv(len(RES_MAGIC) + CLT_MSG_PRO_LEN + data_str_len) ++ res_data = res_data.decode() ++ except (OSError, UnicodeError): ++ client_socket.close() ++ print("collect_plugin: client communicate error") ++ return None ++ ++ res_magic = res_data[:CLT_MSG_MAGIC_LEN] ++ if res_magic != "RES": ++ print("res msg format error") ++ return None ++ ++ protocol_str = res_data[CLT_MSG_MAGIC_LEN:CLT_MSG_MAGIC_LEN+CLT_MSG_PRO_LEN] ++ try: ++ protocol_id = int(protocol_str) ++ except ValueError: ++ print("recv msg protocol id is invalid %s", protocol_str) ++ return None ++ ++ if protocol_id >= ClientProtocol.PRO_END: ++ print("protocol id is invalid") ++ return None ++ ++ try: ++ res_data_len = int(res_data[CLT_MSG_MAGIC_LEN+CLT_MSG_PRO_LEN:]) ++ res_msg_data = client_socket.recv(res_data_len) ++ res_msg_data = res_msg_data.decode() ++ return res_msg_data ++ except (OSError, ValueError, UnicodeError): ++ print("collect_plugin: client recv res msg error") ++ finally: ++ client_socket.close() ++ ++ return None ++ ++def validate_parameters(param, len_limit, char_limit): ++ ret = ResultMessage.RESULT_SUCCEED ++ if not param: ++ print("parm is invalid") ++ ret = ResultMessage.RESULT_NOT_PARAM ++ return [False, ret] ++ ++ if not isinstance(param, list): ++ print(f"{param} is not list type.") ++ ret = ResultMessage.RESULT_NOT_PARAM ++ return [False, ret] ++ ++ if len(param) <= 0: ++ print(f"{param} length is 0.") ++ ret = ResultMessage.RESULT_INVALID_LENGTH ++ return [False, ret] ++ ++ if len(param) > len_limit: ++ print(f"{param} length more than {len_limit}") ++ ret = ResultMessage.RESULT_EXCEED_LIMIT ++ return [False, ret] ++ ++ pattern = r'^[a-zA-Z0-9_-]+$' ++ for info in param: ++ if len(info) > char_limit: ++ print(f"{info} length more than {char_limit}") ++ ret = ResultMessage.RESULT_EXCEED_LIMIT ++ return [False, ret] ++ if not re.match(pattern, info): ++ print(f"{info} is invalid char") ++ ret = ResultMessage.RESULT_INVALID_CHAR ++ return [False, ret] ++ ++ return [True, ret] ++ ++def is_iocollect_valid(period, disk_list=None, stage=None): ++ result = inter_is_iocollect_valid(period, disk_list, stage) ++ error_code = result['ret'] ++ if error_code != ResultMessage.RESULT_SUCCEED: ++ result['message'] = Result_Messages[error_code] ++ return result ++ ++def inter_is_iocollect_valid(period, disk_list=None, stage=None): ++ result = {} ++ result['ret'] = ResultMessage.RESULT_UNKNOWN ++ result['message'] = "" ++ ++ if not period or not isinstance(period, int): ++ result['ret'] = ResultMessage.RESULT_NOT_PARAM ++ return result ++ if period < LIMIT_PERIOD_MIN_LEN or period > LIMIT_PERIOD_MAX_LEN: ++ result['ret'] = ResultMessage.RESULT_INVALID_LENGTH ++ return result ++ ++ if not disk_list: ++ disk_list = [] ++ else: ++ res = validate_parameters(disk_list, LIMIT_DISK_LIST_LEN, LIMIT_DISK_CHAR_LEN) ++ if not res[0]: ++ result['ret'] = res[1] ++ return result ++ ++ if not stage: ++ stage = [] ++ else: ++ res = validate_parameters(stage, LIMIT_STAGE_LIST_LEN, LIMIT_STAGE_CHAR_LEN) ++ if not res[0]: ++ result['ret'] = res[1] ++ return result ++ ++ req_msg_struct = { ++ 'disk_list': json.dumps(disk_list), ++ 'period': period, ++ 'stage': json.dumps(stage) ++ } ++ request_message = json.dumps(req_msg_struct) ++ result_message = client_send_and_recv(request_message, CLT_MSG_LEN_LEN, ClientProtocol.IS_IOCOLLECT_VALID) ++ if not result_message: ++ print("collect_plugin: client_send_and_recv failed") ++ return result ++ ++ try: ++ json.loads(result_message) ++ except json.JSONDecodeError: ++ print("is_iocollect_valid: json decode error") ++ result['ret'] = ResultMessage.RESULT_PARSE_FAILED ++ return result ++ ++ result['ret'] = ResultMessage.RESULT_SUCCEED ++ result['message'] = result_message ++ return result ++ ++def get_io_data(period, disk_list, stage, iotype): ++ result = inter_get_io_data(period, disk_list, stage, iotype) ++ error_code = result['ret'] ++ if error_code != ResultMessage.RESULT_SUCCEED: ++ result['message'] = Result_Messages[error_code] ++ return result ++ ++def inter_get_io_data(period, disk_list, stage, iotype): ++ result = {} ++ result['ret'] = ResultMessage.RESULT_UNKNOWN ++ result['message'] = "" ++ ++ if not isinstance(period, int): ++ result['ret'] = ResultMessage.RESULT_NOT_PARAM ++ return result ++ if period < LIMIT_PERIOD_MIN_LEN or period > LIMIT_PERIOD_MAX_LEN: ++ result['ret'] = ResultMessage.RESULT_INVALID_LENGTH ++ return result ++ ++ res = validate_parameters(disk_list, LIMIT_DISK_LIST_LEN, LIMIT_DISK_CHAR_LEN) ++ if not res[0]: ++ result['ret'] = res[1] ++ return result ++ ++ res = validate_parameters(stage, LIMIT_STAGE_LIST_LEN, LIMIT_STAGE_CHAR_LEN) ++ if not res[0]: ++ result['ret'] = res[1] ++ return result ++ ++ res = validate_parameters(iotype, LIMIT_IOTYPE_LIST_LEN, LIMIT_IOTYPE_CHAR_LEN) ++ if not res[0]: ++ result['ret'] = res[1] ++ return result ++ ++ req_msg_struct = { ++ 'disk_list': json.dumps(disk_list), ++ 'period': period, ++ 'stage': json.dumps(stage), ++ 'iotype' : json.dumps(iotype) ++ } ++ ++ request_message = json.dumps(req_msg_struct) ++ result_message = client_send_and_recv(request_message, CLT_MSG_LEN_LEN, ClientProtocol.GET_IO_DATA) ++ if not result_message: ++ print("collect_plugin: client_send_and_recv failed") ++ return result ++ try: ++ json.loads(result_message) ++ except json.JSONDecodeError: ++ print("get_io_data: json decode error") ++ result['ret'] = ResultMessage.RESULT_PARSE_FAILED ++ return result ++ ++ result['ret'] = ResultMessage.RESULT_SUCCEED ++ result['message'] = result_message ++ return result ++ +diff --git a/src/python/sentryCollector/collect_server.py b/src/python/sentryCollector/collect_server.py +new file mode 100644 +index 0000000..fa49781 +--- /dev/null ++++ b/src/python/sentryCollector/collect_server.py +@@ -0,0 +1,285 @@ ++# coding: utf-8 ++# Copyright (c) 2024 Huawei Technologies Co., Ltd. ++# sysSentry is 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. ++ ++""" ++listen module ++""" ++import sys ++import signal ++import traceback ++import socket ++import os ++import json ++import logging ++import fcntl ++import select ++import threading ++import time ++ ++from .collect_io import IO_GLOBAL_DATA, IO_CONFIG_DATA ++from .collect_config import CollectConfig ++ ++SENTRY_RUN_DIR = "/var/run/sysSentry" ++COLLECT_SOCKET_PATH = "/var/run/sysSentry/collector.sock" ++ ++# socket param ++CLT_LISTEN_QUEUE_LEN = 5 ++SERVER_EPOLL_TIMEOUT = 0.3 ++ ++# data length param ++CLT_MSG_HEAD_LEN = 9 #3+2+4 ++CLT_MSG_PRO_LEN = 2 ++CLT_MSG_MAGIC_LEN = 3 ++CLT_MSG_LEN_LEN = 4 ++ ++# data flag param ++CLT_MAGIC = "CLT" ++RES_MAGIC = "RES" ++ ++# interface protocol ++class ServerProtocol(): ++ IS_IOCOLLECT_VALID = 0 ++ GET_IO_DATA = 1 ++ PRO_END = 3 ++ ++class CollectServer(): ++ ++ def __init__(self): ++ ++ self.io_global_data = {} ++ ++ self.stop_event = threading.Event() ++ ++ def is_iocollect_valid(self, data_struct): ++ ++ result_rev = {} ++ self.io_global_data = IO_GLOBAL_DATA ++ ++ if len(IO_CONFIG_DATA) == 0: ++ logging.error("the collect thread is not started, the data is invalid. ") ++ return json.dumps(result_rev) ++ ++ period_time = IO_CONFIG_DATA[0] ++ max_save = IO_CONFIG_DATA[1] ++ ++ disk_list = json.loads(data_struct['disk_list']) ++ period = int(data_struct['period']) ++ stage_list = json.loads(data_struct['stage']) ++ ++ if (period < period_time) or (period > period_time * max_save) or (period % period_time): ++ logging.error("is_iocollect_valid: period time: %d is invalid", period) ++ return json.dumps(result_rev) ++ ++ for disk_name, stage_info in self.io_global_data.items(): ++ if len(disk_list) > 0 and disk_name not in disk_list: ++ continue ++ result_rev[disk_name] = [] ++ if len(stage_list) == 0: ++ result_rev[disk_name] = list(stage_info.keys()) ++ continue ++ for stage_name, stage_data in stage_info.items(): ++ if stage_name in stage_list: ++ result_rev[disk_name].append(stage_name) ++ ++ return json.dumps(result_rev) ++ ++ def get_io_data(self, data_struct): ++ result_rev = {} ++ self.io_global_data = IO_GLOBAL_DATA ++ ++ if len(IO_CONFIG_DATA) == 0: ++ logging.error("the collect thread is not started, the data is invalid. ") ++ return json.dumps(result_rev) ++ period_time = IO_CONFIG_DATA[0] ++ max_save = IO_CONFIG_DATA[1] ++ ++ period = int(data_struct['period']) ++ disk_list = json.loads(data_struct['disk_list']) ++ stage_list = json.loads(data_struct['stage']) ++ iotype_list = json.loads(data_struct['iotype']) ++ ++ if (period < period_time) or (period > period_time * max_save) or (period % period_time): ++ logging.error("get_io_data: period time: %d is invalid", period) ++ return json.dumps(result_rev) ++ ++ collect_index = period // period_time - 1 ++ logging.debug("period: %d, collect_index: %d", period, collect_index) ++ ++ for disk_name, stage_info in self.io_global_data.items(): ++ if disk_name not in disk_list: ++ continue ++ result_rev[disk_name] = {} ++ for stage_name, iotype_info in stage_info.items(): ++ if len(stage_list) > 0 and stage_name not in stage_list: ++ continue ++ result_rev[disk_name][stage_name] = {} ++ for iotype_name, iotype_info in iotype_info.items(): ++ if iotype_name not in iotype_list: ++ continue ++ if len(iotype_info) < collect_index: ++ continue ++ result_rev[disk_name][stage_name][iotype_name] = iotype_info[collect_index] ++ ++ return json.dumps(result_rev) ++ ++ def msg_data_process(self, msg_data, protocal_id): ++ """message data process""" ++ logging.debug("msg_data %s", msg_data) ++ protocol_name = msg_data[0] ++ try: ++ data_struct = json.loads(msg_data) ++ except json.JSONDecodeError: ++ logging.error("msg data process: json decode error") ++ return "Request message decode failed" ++ ++ if protocal_id == ServerProtocol.IS_IOCOLLECT_VALID: ++ res_msg = self.is_iocollect_valid(data_struct) ++ elif protocal_id == ServerProtocol.GET_IO_DATA: ++ res_msg = self.get_io_data(data_struct) ++ ++ return res_msg ++ ++ def msg_head_process(self, msg_head): ++ """message head process""" ++ ctl_magic = msg_head[:CLT_MSG_MAGIC_LEN] ++ if ctl_magic != CLT_MAGIC: ++ logging.error("recv msg head magic invalid") ++ return None ++ ++ protocol_str = msg_head[CLT_MSG_MAGIC_LEN:CLT_MSG_MAGIC_LEN+CLT_MSG_PRO_LEN] ++ try: ++ protocol_id = int(protocol_str) ++ except ValueError: ++ logging.error("recv msg protocol id is invalid") ++ return None ++ ++ data_len_str = msg_head[CLT_MSG_MAGIC_LEN+CLT_MSG_PRO_LEN:CLT_MSG_HEAD_LEN] ++ try: ++ data_len = int(data_len_str) ++ except ValueError: ++ logging.error("recv msg data len is invalid %s", data_len_str) ++ return None ++ ++ return [protocol_id, data_len] ++ ++ def server_recv(self, server_socket: socket.socket): ++ """server receive""" ++ try: ++ client_socket, _ = server_socket.accept() ++ logging.debug("server_fd listen ok") ++ except socket.error: ++ logging.error("server accept failed, %s", socket.error) ++ return ++ ++ try: ++ msg_head = client_socket.recv(CLT_MSG_HEAD_LEN) ++ logging.debug("recv msg head: %s", msg_head.decode()) ++ head_info = self.msg_head_process(msg_head.decode()) ++ except (OSError, UnicodeError): ++ client_socket.close() ++ logging.error("server recv HEAD failed") ++ return ++ ++ protocol_id = head_info[0] ++ data_len = head_info[1] ++ logging.debug("msg protocol id: %d, data length: %d", protocol_id, data_len) ++ if protocol_id >= ServerProtocol.PRO_END: ++ client_socket.close() ++ logging.error("protocol id is invalid") ++ return ++ ++ if data_len < 0: ++ client_socket.close() ++ logging.error("msg head parse failed") ++ return ++ ++ try: ++ msg_data = client_socket.recv(data_len) ++ msg_data_decode = msg_data.decode() ++ logging.debug("msg data %s", msg_data_decode) ++ except (OSError, UnicodeError): ++ client_socket.close() ++ logging.error("server recv MSG failed") ++ return ++ ++ res_data = self.msg_data_process(msg_data_decode, protocol_id) ++ logging.debug("res data %s", res_data) ++ ++ # server send ++ res_head = RES_MAGIC ++ res_head += str(protocol_id).zfill(CLT_MSG_PRO_LEN) ++ res_data_len = str(len(res_data)).zfill(CLT_MSG_LEN_LEN) ++ res_head += res_data_len ++ logging.debug("res head %s", res_head) ++ ++ res_msg = res_head + res_data ++ logging.debug("res msg %s", res_msg) ++ ++ try: ++ client_socket.send(res_msg.encode()) ++ except OSError: ++ logging.error("server recv failed") ++ finally: ++ client_socket.close() ++ return ++ ++ def server_fd_create(self): ++ """create server fd""" ++ if not os.path.exists(SENTRY_RUN_DIR): ++ logging.error("%s not exist, failed", SENTRY_RUN_DIR) ++ return None ++ ++ try: ++ server_fd = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) ++ server_fd.setblocking(False) ++ if os.path.exists(COLLECT_SOCKET_PATH): ++ os.remove(COLLECT_SOCKET_PATH) ++ ++ server_fd.bind(COLLECT_SOCKET_PATH) ++ os.chmod(COLLECT_SOCKET_PATH, 0o600) ++ server_fd.listen(CLT_LISTEN_QUEUE_LEN) ++ logging.debug("%s bind and listen", COLLECT_SOCKET_PATH) ++ except socket.error: ++ logging.error("server fd create failed") ++ server_fd = None ++ ++ return server_fd ++ ++ ++ def server_loop(self): ++ """main loop""" ++ logging.info("collect server thread start") ++ server_fd = self.server_fd_create() ++ if not server_fd: ++ return ++ ++ epoll_fd = select.epoll() ++ epoll_fd.register(server_fd.fileno(), select.EPOLLIN) ++ ++ logging.debug("start server_loop loop") ++ while True: ++ if self.stop_event.is_set(): ++ logging.info("collect server thread exit") ++ server_fd = None ++ return ++ try: ++ events_list = epoll_fd.poll(SERVER_EPOLL_TIMEOUT) ++ for event_fd, _ in events_list: ++ if event_fd == server_fd.fileno(): ++ self.server_recv(server_fd) ++ else: ++ continue ++ except socket.error: ++ pass ++ ++ def stop_thread(self): ++ logging.info("collect server thread is preparing to exit") ++ self.stop_event.set() +diff --git a/src/python/sentryCollector/collectd.py b/src/python/sentryCollector/collectd.py +new file mode 100644 +index 0000000..b77c642 +--- /dev/null ++++ b/src/python/sentryCollector/collectd.py +@@ -0,0 +1,99 @@ ++# coding: utf-8 ++# Copyright (c) 2024 Huawei Technologies Co., Ltd. ++# sysSentry is 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. ++ ++""" ++main loop for collect. ++""" ++import sys ++import signal ++import traceback ++import socket ++import os ++import json ++import logging ++import fcntl ++import select ++ ++import threading ++ ++from .collect_io import CollectIo ++from .collect_server import CollectServer ++from .collect_config import CollectConfig ++ ++SENTRY_RUN_DIR = "/var/run/sysSentry" ++COLLECT_SOCKET_PATH = "/var/run/sysSentry/collector.sock" ++SENTRY_RUN_DIR_PERM = 0o750 ++ ++COLLECT_LOG_FILE = "/var/log/sysSentry/collector.log" ++Thread_List = [] ++Module_Map_Class = {"io" : CollectIo} ++ ++def remove_sock_file(): ++ try: ++ os.unlink(COLLECT_SOCKET_PATH) ++ except FileNotFoundError: ++ pass ++ ++def sig_handler(signum, _f): ++ if signum not in (signal.SIGINT, signal.SIGTERM): ++ return ++ for i in range(len(Thread_List)): ++ Thread_List[i][0].stop_thread() ++ ++ remove_sock_file() ++ sys.exit(0) ++ ++def main(): ++ """main ++ """ ++ if not os.path.exists(SENTRY_RUN_DIR): ++ os.mkdir(SENTRY_RUN_DIR) ++ os.chmod(SENTRY_RUN_DIR, mode=SENTRY_RUN_DIR_PERM) ++ ++ logging.basicConfig(filename=COLLECT_LOG_FILE, level=logging.INFO) ++ os.chmod(COLLECT_LOG_FILE, 0o600) ++ ++ try: ++ signal.signal(signal.SIGINT, sig_handler) ++ signal.signal(signal.SIGTERM, sig_handler) ++ signal.signal(signal.SIGHUP, sig_handler) ++ ++ logging.info("finish main parse_args") ++ ++ module_config = CollectConfig() ++ module_list = module_config.modules ++ ++ # listen thread ++ cs = CollectServer() ++ listen_thread = threading.Thread(target=cs.server_loop) ++ listen_thread.start() ++ Thread_List.append([cs, listen_thread]) ++ ++ # collect thread ++ for info in module_list: ++ class_name = Module_Map_Class.get(info) ++ if not class_name: ++ logging.info("%s correspond to class is not exists", info) ++ continue ++ cn = class_name(module_config) ++ collect_thread = threading.Thread(target=cn.main_loop) ++ collect_thread.start() ++ Thread_List.append([cn, collect_thread]) ++ ++ for i in range(len(Thread_List)): ++ Thread_List[i][1].join() ++ ++ except Exception: ++ logging.error('%s', traceback.format_exc()) ++ finally: ++ pass ++ ++ logging.info("All threads have finished. Main thread is exiting.") +\ No newline at end of file +diff --git a/src/python/setup.py b/src/python/setup.py +index f96a96e..369e6fc 100644 +--- a/src/python/setup.py ++++ b/src/python/setup.py +@@ -31,7 +31,9 @@ setup( + 'console_scripts': [ + 'cpu_sentry=syssentry.cpu_sentry:main', + 'syssentry=syssentry.syssentry:main', +- 'xalarmd=xalarm.xalarm_daemon:alarm_process_create' ++ 'xalarmd=xalarm.xalarm_daemon:alarm_process_create', ++ 'sentryCollector=sentryCollector.collectd:main', ++ 'avg_block_io=sentryPlugins.avg_block_io.avg_block_io:main' + ] + }, + ) +-- +2.33.0 + diff --git a/feature-add-avg_block_io-plugin.patch b/feature-add-avg_block_io-plugin.patch new file mode 100644 index 0000000000000000000000000000000000000000..ac712a4c9b488d10c61de355c1fca068c7f5907c --- /dev/null +++ b/feature-add-avg_block_io-plugin.patch @@ -0,0 +1,575 @@ +From acb77d6a69aa9269b0f691613bef53efd0c01e53 Mon Sep 17 00:00:00 2001 +From: gaoruoshu +Date: Thu, 12 Sep 2024 11:31:34 +0800 +Subject: [PATCH 2/2] add avg_block_io plugin + +--- + config/plugins/avg_block_io.ini | 21 ++ + config/tasks/avg_block_io.mod | 5 + + src/python/sentryPlugins/__init__.py | 0 + src/python/sentryPlugins/avg_block_io/__init__.py | 0 + .../sentryPlugins/avg_block_io/avg_block_io.py | 265 ++++++++++++++++++ + .../sentryPlugins/avg_block_io/module_conn.py | 81 ++++++ + .../sentryPlugins/avg_block_io/stage_window.py | 47 ++++ + src/python/sentryPlugins/avg_block_io/utils.py | 86 ++++++ + 8 files changed, 505 insertions(+) + create mode 100644 config/plugins/avg_block_io.ini + create mode 100644 config/tasks/avg_block_io.mod + create mode 100644 src/python/sentryPlugins/__init__.py + create mode 100644 src/python/sentryPlugins/avg_block_io/__init__.py + create mode 100644 src/python/sentryPlugins/avg_block_io/avg_block_io.py + create mode 100644 src/python/sentryPlugins/avg_block_io/module_conn.py + create mode 100644 src/python/sentryPlugins/avg_block_io/stage_window.py + create mode 100644 src/python/sentryPlugins/avg_block_io/utils.py + +diff --git a/config/plugins/avg_block_io.ini b/config/plugins/avg_block_io.ini +new file mode 100644 +index 0000000..bc33dde +--- /dev/null ++++ b/config/plugins/avg_block_io.ini +@@ -0,0 +1,21 @@ ++[common] ++disk=default ++stage=default ++iotype=read,write ++period_time=1 ++ ++[algorithm] ++win_size=30 ++win_threshold=6 ++ ++[latency] ++read_avg_lim=10 ++write_avg_lim=10 ++read_avg_time=3 ++write_avg_time=3 ++read_tot_lim=50 ++write_tot_lim=50 ++ ++[iodump] ++read_iodump_lim=0 ++write_iodump_lim=0 +diff --git a/config/tasks/avg_block_io.mod b/config/tasks/avg_block_io.mod +new file mode 100644 +index 0000000..75c7299 +--- /dev/null ++++ b/config/tasks/avg_block_io.mod +@@ -0,0 +1,5 @@ ++[common] ++enabled=yes ++task_start=/usr/bin/avg_block_io ++task_stop=pkill avg_block_io ++type=oneshot +\ No newline at end of file +diff --git a/src/python/sentryPlugins/__init__.py b/src/python/sentryPlugins/__init__.py +new file mode 100644 +index 0000000..e69de29 +diff --git a/src/python/sentryPlugins/avg_block_io/__init__.py b/src/python/sentryPlugins/avg_block_io/__init__.py +new file mode 100644 +index 0000000..e69de29 +diff --git a/src/python/sentryPlugins/avg_block_io/avg_block_io.py b/src/python/sentryPlugins/avg_block_io/avg_block_io.py +new file mode 100644 +index 0000000..f142550 +--- /dev/null ++++ b/src/python/sentryPlugins/avg_block_io/avg_block_io.py +@@ -0,0 +1,265 @@ ++# coding: utf-8 ++# Copyright (c) 2024 Huawei Technologies Co., Ltd. ++# sysSentry is 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 logging ++import signal ++import configparser ++import time ++ ++from .stage_window import IoWindow, IoDumpWindow ++from .module_conn import avg_is_iocollect_valid, avg_get_io_data, report_alarm_fail, process_report_data ++from .utils import update_avg_and_check_abnormal ++ ++CONFIG_FILE = "/etc/sysSentry/plugins/avg_block_io.ini" ++ ++ ++def sig_handler(signum, _f): ++ """stop avg_block_io""" ++ report_result(TASK_NAME, ResultLevel.PASS, {}) ++ logging.info("Finished avg_block_io plugin running.") ++ sys.exit(0) ++ ++ ++def log_invalid_keys(not_in_list, keys_name, config_list, default_list): ++ """print invalid log""" ++ if config_list and default_list: ++ logging.warning("{} in common.{} are not valid, set {}={}".format(not_in_list, keys_name, keys_name, default_list)) ++ elif config_list == ["default"]: ++ logging.warning("Default {} use {}".format(keys_name, default_list)) ++ ++ ++def read_config_common(config): ++ """read config file, get [common] section value""" ++ try: ++ common_sec = config['common'] ++ except configparser.NoSectionError: ++ report_alarm_fail("Cannot find common section in config file") ++ ++ try: ++ period_time = int(common_sec.get("period_time", 1)) ++ if not (1 <= period_time <= 300): ++ raise ValueError("Invalid period_time") ++ except ValueError: ++ period_time = 1 ++ logging.warning("Invalid period_time, set to 1s") ++ ++ disk = common_sec.get('disk').split(",") if common_sec.get('disk') not in [None, 'default'] else [] ++ stage = common_sec.get('stage').split(",") if common_sec.get('stage') not in [None, 'default'] else [] ++ ++ if len(disk) > 10: ++ logging.warning("Too many disks, record only max 10 disks") ++ disk = disk[:10] ++ ++ iotype = common_sec.get('iotype', 'read,write').split(",") ++ iotype_list = [rw.lower() for rw in iotype if rw.lower() in ['read', 'write', 'flush', 'discard']] ++ err_iotype = [rw for rw in iotype if rw.lower() not in ['read', 'write', 'flush', 'discard']] ++ ++ if err_iotype: ++ logging.warning("{} in common.iotype are not valid, set iotype={}".format(err_iotype, iotype_list)) ++ ++ return period_time, disk, stage, iotype_list ++ ++ ++def read_config_algorithm(config): ++ """read config file, get [algorithm] section value""" ++ if not config.has_section("algorithm"): ++ report_alarm_fail("Cannot find algorithm section in config file") ++ ++ try: ++ win_size = int(config.get("algorithm", "win_size")) ++ if not (1 <= win_size <= 300): ++ raise ValueError("Invalid win_size") ++ win_threshold = int(config.get("algorithm", "win_threshold")) ++ if win_threshold < 1 or win_threshold > 300 or win_threshold > win_size: ++ raise ValueError("Invalid win_threshold") ++ except ValueError: ++ report_alarm_fail("Invalid win_threshold or win_size") ++ ++ return win_size, win_threshold ++ ++ ++def read_config_lat_iodump(io_dic, config): ++ """read config file, get [latency] [iodump] section value""" ++ common_param = {} ++ for io_type in io_dic["iotype_list"]: ++ common_param[io_type] = {} ++ ++ latency_keys = { ++ "avg_lim": "{}_avg_lim".format(io_type), ++ "avg_time": "{}_avg_time".format(io_type), ++ "tot_lim": "{}_tot_lim".format(io_type), ++ } ++ iodump_key = "{}_iodump_lim".format(io_type) ++ ++ for key_suffix, key_template in latency_keys.items(): ++ if key_template in config["latency"] and config["latency"][key_template].isdecimal(): ++ common_param[io_type][key_template] = int(config["latency"][key_template]) ++ ++ if iodump_key in config["iodump"] and config["iodump"][iodump_key].isdecimal(): ++ common_param[io_type][iodump_key] = int(config["iodump"][iodump_key]) ++ ++ return common_param ++ ++ ++def read_config_stage(config, stage, iotype_list): ++ """read config file, get [STAGE_NAME] section value""" ++ res = {} ++ if not stage in config: ++ return res ++ ++ for key in config[stage]: ++ if config[stage][key].isdecimal(): ++ res[key] = int(config[stage][key]) ++ ++ return res ++ ++ ++def init_io_win(io_dic, config, common_param): ++ """initialize windows of latency, iodump, and dict of avg_value""" ++ iotype_list = io_dic["iotype_list"] ++ io_data = {} ++ io_avg_value = {} ++ for disk_name in io_dic["disk_list"]: ++ io_data[disk_name] = {} ++ io_avg_value[disk_name] = {} ++ for stage_name in io_dic["stage_list"]: ++ io_data[disk_name][stage_name] = {} ++ io_avg_value[disk_name][stage_name] = {} ++ # step3. 解析stage配置 ++ curr_stage_param = read_config_stage(config, stage_name, iotype_list) ++ for rw in iotype_list: ++ io_data[disk_name][stage_name][rw] = {} ++ io_avg_value[disk_name][stage_name][rw] = [0, 0] ++ ++ # 对每个rw创建latency和iodump窗口 ++ avg_lim_key = "{}_avg_lim".format(rw) ++ avg_time_key = "{}_avg_time".format(rw) ++ tot_lim_key = "{}_tot_lim".format(rw) ++ iodump_lim_key = "{}_iodump_lim".format(rw) ++ ++ # 获取值,优先从 curr_stage_param 获取,如果不存在,则从 common_param 获取 ++ avg_lim_value = curr_stage_param.get(avg_lim_key, common_param.get(rw, {}).get(avg_lim_key)) ++ avg_time_value = curr_stage_param.get(avg_time_key, common_param.get(rw, {}).get(avg_time_key)) ++ tot_lim_value = curr_stage_param.get(tot_lim_key, common_param.get(rw, {}).get(tot_lim_key)) ++ iodump_lim_value = curr_stage_param.get(iodump_lim_key, common_param.get(rw, {}).get(iodump_lim_key)) ++ ++ if avg_lim_value and avg_time_value and tot_lim_value: ++ io_data[disk_name][stage_name][rw]["latency"] = IoWindow(window_size=io_dic["win_size"], window_threshold=io_dic["win_threshold"], abnormal_multiple=avg_time_value, abnormal_multiple_lim=avg_lim_value, abnormal_time=tot_lim_value) ++ ++ if iodump_lim_value is not None: ++ io_data[disk_name][stage_name][rw]["iodump"] = IoDumpWindow(window_size=io_dic["win_size"], window_threshold=io_dic["win_threshold"], abnormal_time=iodump_lim_value) ++ return io_data, io_avg_value ++ ++ ++def get_valid_disk_stage_list(io_dic, config_disk, config_stage): ++ """get disk_list and stage_list by sentryCollector""" ++ json_data = avg_is_iocollect_valid(io_dic, config_disk, config_stage) ++ ++ all_disk_set = json_data.keys() ++ all_stage_set = set() ++ for disk_stage_list in json_data.values(): ++ all_stage_set.update(disk_stage_list) ++ ++ disk_list = [key for key in config_disk if key in all_disk_set] ++ not_in_disk_list = [key for key in config_disk if key not in all_disk_set] ++ ++ stage_list = [key for key in config_stage if key in all_stage_set] ++ not_in_stage_list = [key for key in config_stage if key not in all_stage_set] ++ ++ if not config_disk: ++ disk_list = [key for key in all_disk_set] ++ ++ if not config_stage: ++ stage_list = [key for key in all_stage_set] ++ ++ if config_disk and not disk_list: ++ logging.warning("Cannot get valid disk by disk={}, set to default".format(config_disk)) ++ disk_list, stage_list = get_valid_disk_stage_list(io_dic, [], config_stage) ++ ++ if config_stage and not stage_list: ++ logging.warning("Cannot get valid stage by stage={}, set to default".format(config_stage)) ++ disk_list, stage_list = get_valid_disk_stage_list(io_dic, config_disk, []) ++ ++ if not stage_list or not disk_list: ++ report_alarm_fail("Cannot get valid disk name or stage name.") ++ ++ log_invalid_keys(not_in_disk_list, 'disk', config_disk, disk_list) ++ log_invalid_keys(not_in_stage_list, 'stage', config_stage, stage_list) ++ ++ return disk_list, stage_list ++ ++ ++def main_loop(io_dic, io_data, io_avg_value): ++ """main loop of avg_block_io""" ++ period_time = io_dic["period_time"] ++ disk_list = io_dic["disk_list"] ++ stage_list = io_dic["stage_list"] ++ iotype_list = io_dic["iotype_list"] ++ win_size = io_dic["win_size"] ++ # 开始循环 ++ while True: ++ # 等待x秒 ++ time.sleep(period_time) ++ ++ # 采集模块对接,获取周期数据 ++ curr_period_data = avg_get_io_data(io_dic) ++ ++ # 处理周期数据 ++ reach_size = False ++ for disk_name in disk_list: ++ for stage_name in stage_list: ++ for rw in iotype_list: ++ if disk_name in curr_period_data and stage_name in curr_period_data[disk_name] and rw in curr_period_data[disk_name][stage_name]: ++ io_key = (disk_name, stage_name, rw) ++ reach_size = update_avg_and_check_abnormal(curr_period_data, io_key, win_size, io_avg_value, io_data) ++ ++ # win_size不满时不进行告警判断 ++ if not reach_size: ++ continue ++ ++ # 判断异常窗口、异常场景 ++ for disk_name in disk_list: ++ for rw in iotype_list: ++ process_report_data(disk_name, rw, io_data) ++ ++ ++def main(): ++ """main func""" ++ # 注册停止信号-2/-15 ++ signal.signal(signal.SIGINT, sig_handler) ++ signal.signal(signal.SIGTERM, sig_handler) ++ ++ # 初始化配置读取 ++ config = configparser.ConfigParser(comment_prefixes=('#', ';')) ++ try: ++ config.read(CONFIG_FILE) ++ except configparser.Error: ++ report_alarm_fail("Failed to read config file") ++ ++ io_dic = {} ++ ++ # 读取配置文件 -- common段 ++ io_dic["period_time"], disk, stage, io_dic["iotype_list"] = read_config_common(config) ++ ++ # 采集模块对接,is_iocollect_valid() ++ io_dic["disk_list"], io_dic["stage_list"] = get_valid_disk_stage_list(io_dic, disk, stage) ++ ++ if "bio" not in io_dic["stage_list"]: ++ report_alarm_fail("Cannot run avg_block_io without bio stage") ++ ++ # 初始化窗口 -- config读取,对应is_iocollect_valid返回的结果 ++ # step1. 解析公共配置 --- algorithm ++ io_dic["win_size"], io_dic["win_threshold"] = read_config_algorithm(config) ++ ++ # step2. 循环创建窗口 ++ common_param = read_config_lat_iodump(io_dic, config) ++ io_data, io_avg_value = init_io_win(io_dic, config, common_param) ++ ++ main_loop(io_dic, io_data, io_avg_value) +diff --git a/src/python/sentryPlugins/avg_block_io/module_conn.py b/src/python/sentryPlugins/avg_block_io/module_conn.py +new file mode 100644 +index 0000000..0ff982d +--- /dev/null ++++ b/src/python/sentryPlugins/avg_block_io/module_conn.py +@@ -0,0 +1,81 @@ ++# coding: utf-8 ++# Copyright (c) 2024 Huawei Technologies Co., Ltd. ++# sysSentry is 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 json ++import logging ++import sys ++import time ++ ++from .utils import is_abnormal ++from sentryCollector.collect_plugin import is_iocollect_valid, get_io_data, Result_Messages ++from syssentry.result import ResultLevel, report_result ++ ++ ++TASK_NAME = "avg_block_io" ++ ++ ++def avg_get_io_data(io_dic): ++ """get_io_data from sentryCollector""" ++ res = get_io_data(io_dic["period_time"], io_dic["disk_list"], io_dic["stage_list"], io_dic["iotype_list"]) ++ return check_result_validation(res, 'get io data') ++ ++ ++def avg_is_iocollect_valid(io_dic, config_disk, config_stage): ++ """is_iocollect_valid from sentryCollector""" ++ res = is_iocollect_valid(io_dic["period_time"], config_disk, config_stage) ++ return check_result_validation(res, 'check config validation') ++ ++ ++def check_result_validation(res, reason): ++ """check validation of result from sentryCollector""" ++ if not 'ret' in res or not 'message' in res: ++ err_msg = "Failed to {}: Cannot connect to sentryCollector.".format(reason) ++ report_alarm_fail(err_msg) ++ if res['ret'] != 0: ++ err_msg = "Failed to {}: {}".format(reason, Result_Messages[res['ret']]) ++ report_alarm_fail(err_msg) ++ ++ try: ++ json_data = json.loads(res['message']) ++ except json.JSONDecodeError: ++ err_msg = "Failed to {}: invalid return message".format(reason) ++ report_alarm_fail(err_msg) ++ ++ return json_data ++ ++ ++def report_alarm_fail(alarm_info): ++ """report result to xalarmd""" ++ report_result(TASK_NAME, ResultLevel.FAIL, {"msg": alarm_info}) ++ logging.error(alarm_info) ++ sys.exit(1) ++ ++ ++def process_report_data(disk_name, rw, io_data): ++ """check abnormal window and report to xalarm""" ++ if not is_abnormal((disk_name, 'bio', rw), io_data): ++ return ++ ++ ctrl_stage = ['throtl', 'wbt', 'iocost', 'bfq'] ++ for stage_name in ctrl_stage: ++ if is_abnormal((disk_name, stage_name, rw), io_data): ++ logging.warning("{} - {} - {} report IO press".format(time.ctime(), disk_name, rw)) ++ return ++ ++ if is_abnormal((disk_name, 'rq_driver', rw), io_data): ++ logging.warning("{} - {} - {} report driver".format(time.ctime(), disk_name, rw)) ++ return ++ ++ kernel_stage = ['gettag', 'plug', 'deadline', 'hctx', 'requeue'] ++ for stage_name in kernel_stage: ++ if is_abnormal((disk_name, stage_name, rw), io_data): ++ logging.warning("{} - {} - {} report kernel".format(time.ctime(), disk_name, rw)) ++ return ++ logging.warning("{} - {} - {} report IO press".format(time.ctime(), disk_name, rw)) +diff --git a/src/python/sentryPlugins/avg_block_io/stage_window.py b/src/python/sentryPlugins/avg_block_io/stage_window.py +new file mode 100644 +index 0000000..9b0ce79 +--- /dev/null ++++ b/src/python/sentryPlugins/avg_block_io/stage_window.py +@@ -0,0 +1,47 @@ ++# coding: utf-8 ++# Copyright (c) 2024 Huawei Technologies Co., Ltd. ++# sysSentry is 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. ++ ++class AbnormalWindowBase: ++ def __init__(self, window_size=10, window_threshold=7): ++ self.window_size = window_size ++ self.window_threshold = window_threshold ++ self.abnormal_window = [False] * window_size ++ ++ def append_new_period(self, ab_res, avg_val=0): ++ self.abnormal_window.pop(0) ++ if self.is_abnormal_period(ab_res, avg_val): ++ self.abnormal_window.append(True) ++ else: ++ self.abnormal_window.append(False) ++ ++ def is_abnormal_window(self): ++ return sum(self.abnormal_window) > self.window_threshold ++ ++ ++class IoWindow(AbnormalWindowBase): ++ def __init__(self, window_size=10, window_threshold=7, abnormal_multiple=5, abnormal_multiple_lim=30, abnormal_time=40): ++ super().__init__(window_size, window_threshold) ++ self.abnormal_multiple = abnormal_multiple ++ self.abnormal_multiple_lim = abnormal_multiple_lim ++ self.abnormal_time = abnormal_time ++ ++ def is_abnormal_period(self, value, avg_val): ++ return (value > avg_val * self.abnormal_multiple and value > self.abnormal_multiple_lim) or \ ++ (value > self.abnormal_time) ++ ++ ++class IoDumpWindow(AbnormalWindowBase): ++ def __init__(self, window_size=10, window_threshold=7, abnormal_time=40): ++ super().__init__(window_size, window_threshold) ++ self.abnormal_time = abnormal_time ++ ++ def is_abnormal_period(self, value, avg_val=0): ++ return value > self.abnormal_time +diff --git a/src/python/sentryPlugins/avg_block_io/utils.py b/src/python/sentryPlugins/avg_block_io/utils.py +new file mode 100644 +index 0000000..54ed080 +--- /dev/null ++++ b/src/python/sentryPlugins/avg_block_io/utils.py +@@ -0,0 +1,86 @@ ++# coding: utf-8 ++# Copyright (c) 2024 Huawei Technologies Co., Ltd. ++# sysSentry is 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. ++AVG_VALUE = 0 ++AVG_COUNT = 1 ++ ++ ++def get_nested_value(data, keys): ++ """get data from nested dict""" ++ for key in keys: ++ if key in data: ++ data = data[key] ++ else: ++ return None ++ return data ++ ++ ++def set_nested_value(data, keys, value): ++ """set data to nested dict""" ++ for key in keys[:-1]: ++ if key in data: ++ data = data[key] ++ else: ++ return False ++ data[keys[-1]] = value ++ return True ++ ++ ++def is_abnormal(io_key, io_data): ++ """check if latency and iodump win abnormal""" ++ for key in ['latency', 'iodump']: ++ all_keys = get_nested_value(io_data, io_key) ++ if all_keys and key in all_keys: ++ win = get_nested_value(io_data, io_key + (key,)) ++ if win and win.is_abnormal_window(): ++ return True ++ return False ++ ++ ++def update_io_avg(old_avg, period_value, win_size): ++ """update average of latency window""" ++ if old_avg[AVG_COUNT] < win_size: ++ new_avg_count = old_avg[AVG_COUNT] + 1 ++ new_avg_value = (old_avg[AVG_VALUE] * old_avg[AVG_COUNT] + period_value[0]) / new_avg_count ++ else: ++ new_avg_count = old_avg[AVG_COUNT] ++ new_avg_value = (old_avg[AVG_VALUE] * (old_avg[AVG_COUNT] - 1) + period_value[0]) / new_avg_count ++ return [new_avg_value, new_avg_count] ++ ++ ++def update_io_data(old_avg, period_value, win_size, io_data, io_key): ++ """update data of latency and iodump window""" ++ all_wins = get_nested_value(io_data, io_key) ++ if all_wins and "latency" in all_wins: ++ io_data[io_key[0]][io_key[1]][io_key[2]]["latency"].append_new_period(period_value[0], old_avg[AVG_VALUE]) ++ if all_wins and "iodump" in all_wins: ++ io_data[io_key[0]][io_key[1]][io_key[2]]["iodump"].append_new_period(period_value[1]) ++ ++ ++def update_avg_and_check_abnormal(data, io_key, win_size, io_avg_value, io_data): ++ """update avg and check abonrmal, return true if win_size full""" ++ period_value = get_nested_value(data, io_key) ++ old_avg = get_nested_value(io_avg_value, io_key) ++ ++ # 更新avg数据 ++ if old_avg[AVG_COUNT] < win_size: ++ set_nested_value(io_avg_value, io_key, update_io_avg(old_avg, period_value, win_size)) ++ return False ++ ++ # 更新win数据 -- 判断异常周期 ++ update_io_data(old_avg, period_value, win_size, io_data, io_key) ++ all_wins = get_nested_value(io_data, io_key) ++ if all_wins and 'latency' not in all_wins: ++ return True ++ period = get_nested_value(io_data, io_key + ("latency",)) ++ if period and period.is_abnormal_period(period_value[0], old_avg[AVG_VALUE]): ++ return True ++ set_nested_value(io_avg_value, io_key, update_io_avg(old_avg, period_value, win_size)) ++ return True +-- +2.33.0 + diff --git a/sysSentry.spec b/sysSentry.spec index beea5be18402a36c061ea83da48f2a1d768a00ad..5d46631177ba5fbe72835ecad9f0f25227d9f6e6 100644 --- a/sysSentry.spec +++ b/sysSentry.spec @@ -4,7 +4,7 @@ Summary: System Inspection Framework Name: sysSentry Version: 1.0.2 -Release: 11 +Release: 12 License: Mulan PSL v2 Group: System Environment/Daemons Source0: https://gitee.com/openeuler/sysSentry/releases/download/v%{version}/%{name}-%{version}.tar.gz @@ -21,6 +21,8 @@ Patch8: add-deleted-code-to-plugin-rasdaemon.patch Patch9: Remove-ANSI-escape-sequences.patch Patch10: split-cpu_sentry-and-syssentry.patch Patch11: fix-configparser.InterpolationSyntaxError.patch +Patch12: add-collect-module-to-sysSentry.patch +Patch13: feature-add-avg_block_io-plugin.patch BuildRequires: cmake gcc-c++ BuildRequires: python3 python3-setuptools @@ -57,6 +59,13 @@ Recommends: ipmitool %description -n cpu_sentry This package provides CPU fault detection +%package -n avg_block_io +Summary: Supports slow I/O detection +Requires: sysSentry = %{version}-%{release} + +%description -n avg_block_io +This package provides Supports slow I/O detection based on EBPF + %prep %autosetup -n %{name}-%{version} -p1 @@ -98,6 +107,10 @@ install -m 600 service/xalarmd.service %{buildroot}%{_unitdir} install -m 600 config/logrotate %{buildroot}%{_sysconfdir}/logrotate.d/sysSentry install -m 644 src/libso/xalarm/register_xalarm.h %{buildroot}%{_includedir}/xalarm/register_xalarm.h +# sentryCollector +install -m 600 config/collector.conf %{buildroot}%{_sysconfdir}/sysSentry +install -m 600 service/sentryCollector.service %{buildroot}%{_unitdir} + # cpu sentry install config/tasks/cpu_sentry.mod %{buildroot}/etc/sysSentry/tasks/ install config/plugins/cpu_sentry.ini %{buildroot}/etc/sysSentry/plugins/cpu_sentry.ini @@ -107,6 +120,10 @@ install src/c/catcli/catlib/build/plugin/cpu_patrol/libcpu_patrol.so %{buildroot chrpath -d %{buildroot}%{_bindir}/cat-cli chrpath -d %{buildroot}%{_libdir}/libcpu_patrol.so +# avg_block_io +install config/tasks/avg_block_io.mod %{buildroot}/etc/sysSentry/tasks/ +install config/plugins/avg_block_io.ini %{buildroot}/etc/sysSentry/plugins/avg_block_io.ini + pushd src/python python3 setup.py install -O1 --root=$RPM_BUILD_ROOT --record=SENTRY_FILES popd @@ -122,6 +139,8 @@ if [ "$1" = "0" ]; then systemctl disable xalarmd.service systemctl stop sysSentry.service systemctl disable sysSentry.service + systemctl stop sentryCollector.service + systemctl disable sentryCollector.service fi rm -rf /var/run/xalarm | : rm -rf /var/run/sysSentry | : @@ -136,6 +155,8 @@ rm -rf %{buildroot} %defattr(0550,root,root) %attr(0550,root,root) %{python3_sitelib}/xalarm %attr(0550,root,root) %{python3_sitelib}/syssentry +%attr(0550,root,root) %{python3_sitelib}/sentryCollector +%attr(0550,root,root) %{python3_sitelib}/sentryPlugins/avg_block_io # sysSentry %attr(0500,root,root) %{_bindir}/sentryctl @@ -161,6 +182,17 @@ rm -rf %{buildroot} %exclude %{python3_sitelib}/syssentry/cpu_* %exclude %{python3_sitelib}/syssentry/*/cpu_* +# avg block io +%exclude %{_sysconfdir}/sysSentry/tasks/avg_block_io.mod +%exclude %{_sysconfdir}/sysSentry/plugins/avg_block_io.ini +%exclude %{_bindir}/avg_block_io +%exclude %{python3_sitelib}/sentryPlugins/* + +# sentryCollector +%attr(0550,root,root) %{_bindir}/sentryCollector +%attr(0600,root,root) %{_sysconfdir}/sysSentry/collector.conf +%attr(0600,root,root) %{_unitdir}/sentryCollector.service + %files -n libxalarm %attr(0550,root,root) %{_libdir}/libxalarm.so @@ -177,7 +209,19 @@ rm -rf %{buildroot} %attr(0600,root,root) %{_sysconfdir}/sysSentry/plugins/cpu_sentry.ini %attr(0550,root,root) %{python3_sitelib}/syssentry/cpu_* +%files -n avg_block_io +%attr(0500,root,root) %{_bindir}/avg_block_io +%attr(0600,root,root) %config(noreplace) %{_sysconfdir}/sysSentry/tasks/avg_block_io.mod +%attr(0600,root,root) %{_sysconfdir}/sysSentry/plugins/avg_block_io.ini +%attr(0550,root,root) %{python3_sitelib}/sentryPlugins/avg_block_io + %changelog +* Thu Sep 12 2024 zhuofeng - 1.0.2-12 +- Type:requirement +- CVE:NA +- SUG:NA +- DESC:add collect module and avg_block_io plugin to sysSentry + * Wed Sep 11 2024 shixuantong - 1.0.2-11 - Type:bugfix - CVE:NA