From d93ce72fed25f97f73b6ce42b4e056b0a87f9b48 Mon Sep 17 00:00:00 2001 From: TalLerner Date: Mon, 26 Aug 2024 11:30:10 +0300 Subject: [PATCH 1/6] Pdr remove dynamic1 (#239) --- .../.ci/do_add_plugin.sh | 2 +- .../.ci/do_install_plugin_server.sh | 2 +- .../.ci/do_load_plugin.sh | 2 +- .../.ci/do_remove_plugin.sh | 2 +- .../build/config/pdr_deterministic.conf | 3 - .../tests/simulation_telemetry.py | 12 +- .../ufm_sim_web_service/constants.py | 19 +-- .../ufm_sim_web_service/isolation_mgr.py | 141 ++---------------- .../ufm_communication_mgr.py | 76 +++------- 9 files changed, 54 insertions(+), 205 deletions(-) diff --git a/plugins/pdr_deterministic_plugin/.ci/do_add_plugin.sh b/plugins/pdr_deterministic_plugin/.ci/do_add_plugin.sh index 2d2b5b782..210c2ac42 100755 --- a/plugins/pdr_deterministic_plugin/.ci/do_add_plugin.sh +++ b/plugins/pdr_deterministic_plugin/.ci/do_add_plugin.sh @@ -1,7 +1,7 @@ #!/bin/bash -x export SERVER_HOST=$SERVER_HOST expect << EOF -spawn ssh admin@${SERVER_HOST} +spawn ssh -o StrictHostKeyChecking=no admin@${SERVER_HOST} expect "Password:*" send -- "admin\r" expect "> " diff --git a/plugins/pdr_deterministic_plugin/.ci/do_install_plugin_server.sh b/plugins/pdr_deterministic_plugin/.ci/do_install_plugin_server.sh index a8adc7ec9..994cfcd9e 100755 --- a/plugins/pdr_deterministic_plugin/.ci/do_install_plugin_server.sh +++ b/plugins/pdr_deterministic_plugin/.ci/do_install_plugin_server.sh @@ -4,7 +4,7 @@ namehost=$(echo $HOSTNAME) export SERVER_HOST=$SERVER_HOST export PASSWORD=$PASSWORD expect << EOF -spawn ssh admin@${SERVER_HOST} +spawn ssh -o StrictHostKeyChecking=no admin@${SERVER_HOST} expect "Password:*" send -- "admin\r" expect "> " diff --git a/plugins/pdr_deterministic_plugin/.ci/do_load_plugin.sh b/plugins/pdr_deterministic_plugin/.ci/do_load_plugin.sh index ee05057b6..ba8c9bae6 100755 --- a/plugins/pdr_deterministic_plugin/.ci/do_load_plugin.sh +++ b/plugins/pdr_deterministic_plugin/.ci/do_load_plugin.sh @@ -1,7 +1,7 @@ #!/bin/bash -x export SERVER_HOST=$SERVER_HOST expect << EOF -spawn ssh admin@${SERVER_HOST} +spawn ssh -o StrictHostKeyChecking=no admin@${SERVER_HOST} expect "Password:*" send -- "admin\r" expect "> " diff --git a/plugins/pdr_deterministic_plugin/.ci/do_remove_plugin.sh b/plugins/pdr_deterministic_plugin/.ci/do_remove_plugin.sh index ee77f586c..978445e75 100755 --- a/plugins/pdr_deterministic_plugin/.ci/do_remove_plugin.sh +++ b/plugins/pdr_deterministic_plugin/.ci/do_remove_plugin.sh @@ -1,7 +1,7 @@ #!/bin/bash -x export SERVER_HOST=$SERVER_HOST expect << EOF -spawn ssh admin@${SERVER_HOST} +spawn ssh -o StrictHostKeyChecking=no admin@${SERVER_HOST} expect "Password:*" send -- "admin\r" expect "> " diff --git a/plugins/pdr_deterministic_plugin/build/config/pdr_deterministic.conf b/plugins/pdr_deterministic_plugin/build/config/pdr_deterministic.conf index c0ca1b8ae..a4535b2ce 100644 --- a/plugins/pdr_deterministic_plugin/build/config/pdr_deterministic.conf +++ b/plugins/pdr_deterministic_plugin/build/config/pdr_deterministic.conf @@ -27,9 +27,6 @@ DEISOLATE_CONSIDER_TIME=300 AUTOMATIC_DEISOLATE=True # if set to false, the plugin will not perform deisolation DO_DEISOLATION=True -DYNAMIC_WAIT_TIME=30 -# number of times to check if a dynamic session is unresponsive before restarting it -DYNAMIC_UNRESPONSIVE_LIMIT=3 [Metrics] # in Celsius diff --git a/plugins/pdr_deterministic_plugin/tests/simulation_telemetry.py b/plugins/pdr_deterministic_plugin/tests/simulation_telemetry.py index 59023bf39..edaff7349 100755 --- a/plugins/pdr_deterministic_plugin/tests/simulation_telemetry.py +++ b/plugins/pdr_deterministic_plugin/tests/simulation_telemetry.py @@ -25,12 +25,12 @@ lock = Lock() PHY_EFF_ERROR = "phy_effective_errors" -PHY_SYMBOL_ERROR = "phy_symbol_errors" +PHY_SYMBOL_ERROR = "Symbol_Errors" RCV_PACKETS_COUNTER = "PortRcvPktsExtended" -RCV_ERRORS_COUNTER = "PortRcvErrorsExtended" -LINK_DOWN_COUNTER = "LinkDownedCounterExtended" -RCV_REMOTE_PHY_ERROR_COUNTER = "PortRcvRemotePhysicalErrorsExtended" -TEMP_COUNTER = "CableInfo.Temperature" +RCV_ERRORS_COUNTER = "PortRcvErrors" +LINK_DOWN_COUNTER = "Link_Down_IB" +RCV_REMOTE_PHY_ERROR_COUNTER = "PortRcvRemotePhysicalErrors" +TEMP_COUNTER = "Module_Temperature" FEC_MODE = "fec_mode_active" ENDPOINT_CONFIG = {} @@ -192,7 +192,7 @@ def start_server(port:str,changes_intervals:int, run_forever:bool): t.daemon = True t.start() counters_names = list(counters.keys()) - header = ['timestamp', 'source_id,tag,node_guid,port_guid,port_num'] + counters_names + header = ['timestamp', 'source_id,tag,Node_GUID,port_guid,Port_Number'] + counters_names endpoint['data'] = "" while True: # lock.acquire() diff --git a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/constants.py b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/constants.py index 87e3dc92d..31097e62e 100644 --- a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/constants.py +++ b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/constants.py @@ -43,7 +43,7 @@ class PDRConstants(object): SWITCH_TO_HOST_ISOLATION = "SWITCH_TO_HOST_ISOLATION" TEST_MODE = "TEST_MODE" TEST_MODE_PORT = 9090 - DYNAMIC_UNRESPONSIVE_LIMIT = "DYNAMIC_UNRESPONSIVE_LIMIT" + SECONDARY_TELEMETRY_PORT = 9002 GET_SESSION_DATA_REST = "/monitoring/session/0/data" POST_EVENT_REST = "/app/events/external_event" @@ -53,8 +53,7 @@ class PDRConstants(object): GET_ACTIVE_PORTS_REST = "/resources/ports?active=true" API_HEALTHY_PORTS = "healthy_ports" API_ISOLATED_PORTS = "isolated_ports" - DYNAMIC_SESSION_REST = "/app/telemetry/instances/%s" - STATUS_DYNAMIC_SESSION_REST = "/app/telemetry/instances/status" + SECONDARY_INSTANCE = "low_freq_debug" EXTERNAL_EVENT_ERROR = 554 EXTERNAL_EVENT_ALERT = 553 @@ -70,12 +69,12 @@ class PDRConstants(object): CONF_USERNAME = 'admin' CONF_PASSWORD = 'password' - TEMP_COUNTER = "CableInfo.Temperature" - ERRORS_COUNTER = "errors" + ERRORS_COUNTER = "Symbol_Errors" RCV_PACKETS_COUNTER = "PortRcvPktsExtended" - RCV_ERRORS_COUNTER = "PortRcvErrorsExtended" - RCV_REMOTE_PHY_ERROR_COUNTER = "PortRcvRemotePhysicalErrorsExtended" - LNK_DOWNED_COUNTER = "LinkDownedCounterExtended" + RCV_ERRORS_COUNTER = "PortRcvErrors" + RCV_REMOTE_PHY_ERROR_COUNTER = "PortRcvRemotePhysicalErrors" + TEMP_COUNTER = "Module_Temperature" + LNK_DOWNED_COUNTER = "Link_Down_IB" PHY_RAW_ERROR_LANE0 = "phy_raw_errors_lane0" PHY_RAW_ERROR_LANE1 = "phy_raw_errors_lane1" @@ -98,6 +97,9 @@ class PDRConstants(object): NODE_TYPE_OTHER = "other" BER_TELEMETRY = "ber_telemetry" + NODE_GUID = "Node_GUID" + PORT_NUMBER = "Port_Number" + ISSUE_PDR = "pdr" ISSUE_BER = "ber" ISSUE_PDR_BER = "pdr&ber" @@ -109,6 +111,5 @@ class PDRConstants(object): STATE_ISOLATED = "isolated" STATE_TREATED = "treated" - PDR_DYNAMIC_NAME = "pdr_dynamic" # intervals in seconds for testing ber values and corresponding thresholds BER_THRESHOLDS_INTERVALS = [(125 * 60, 3), (12 * 60, 2.88)] diff --git a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/isolation_mgr.py b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/isolation_mgr.py index c5eb8d632..20804fb00 100644 --- a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/isolation_mgr.py +++ b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/isolation_mgr.py @@ -22,17 +22,9 @@ from exclude_list import ExcludeList from constants import PDRConstants as Constants -from ufm_communication_mgr import DynamicSessionState, UFMCommunicator +from ufm_communication_mgr import UFMCommunicator # should actually be persistent and thread safe dictionary pf PortStates - -class DynamicTelemetryUnresponsive(Exception): - """ - Exception raised when the dynamic telemetry is unresponsive. - """ - pass - - class PortData(object): """ Represents the port data. @@ -194,13 +186,11 @@ def __init__(self, ufm_client: UFMCommunicator, logger): self.do_deisolate = pdr_config.getboolean(Constants.CONF_ISOLATION,Constants.DO_DEISOLATION) self.deisolate_consider_time = pdr_config.getint(Constants.CONF_ISOLATION,Constants.DEISOLATE_CONSIDER_TIME) self.automatic_deisolate = pdr_config.getboolean(Constants.CONF_ISOLATION,Constants.AUTOMATIC_DEISOLATE) - self.dynamic_wait_time = pdr_config.getint(Constants.CONF_ISOLATION,"DYNAMIC_WAIT_TIME") self.temp_check = pdr_config.getboolean(Constants.CONF_ISOLATION,Constants.CONFIGURED_TEMP_CHECK) self.link_down_isolation = pdr_config.getboolean(Constants.CONF_ISOLATION,Constants.LINK_DOWN_ISOLATION) self.switch_hca_isolation = pdr_config.getboolean(Constants.CONF_ISOLATION,Constants.SWITCH_TO_HOST_ISOLATION) self.test_mode = pdr_config.getboolean(Constants.CONF_COMMON,Constants.TEST_MODE, fallback=False) self.test_iteration = 0 - self.dynamic_unresponsive_limit = pdr_config.getint(Constants.CONF_ISOLATION,Constants.DYNAMIC_UNRESPONSIVE_LIMIT, fallback=3) # Take from Conf self.logger = logger self.ber_intervals = Constants.BER_THRESHOLDS_INTERVALS if not self.test_mode else [[0.5 * 60, 3]] @@ -227,13 +217,6 @@ def __init__(self, ufm_client: UFMCommunicator, logger): Constants.LNK_DOWNED_COUNTER, ] - # bring telemetry data on disabled ports - self.dynamic_extra_configuration = { - "plugin_env_CLX_EXPORT_API_ENABLE_DOWN_PORT_COUNTERS": "1", - "plugin_env_CLX_EXPORT_API_ENABLE_DOWN_PHY": "1", - "arg_11": "" - } - self.exclude_list = ExcludeList(self.logger) def calc_max_ber_wait_time(self, min_threshold): @@ -415,10 +398,10 @@ def find_peer_row_for_port(self, port_obj, ports_counters): return None peer_guid, peer_num = port_obj.peer.split('_') # Fix peer guid format for future search - if ports_counters['port_guid'].iloc[0].startswith('0x') and not peer_guid.startswith('0x'): + if ports_counters[Constants.NODE_GUID].iloc[0].startswith('0x') and not peer_guid.startswith('0x'): peer_guid = f'0x{peer_guid}' #TODO check for a way to save peer row in data structure for performance - peer_row_list = ports_counters.loc[(ports_counters['port_guid'] == peer_guid) & (ports_counters['port_num'] == int(peer_num))] + peer_row_list = ports_counters.loc[(ports_counters[Constants.NODE_GUID] == peer_guid) & (ports_counters[Constants.PORT_NUMBER] == int(peer_num))] if peer_row_list.empty: self.logger.warning(f"Peer port {port_obj.peer} not found in ports data") return None @@ -459,10 +442,8 @@ def check_temp_issue(self, port_obj, row, timestamp): if cable_temp is not None and not pd.isna(cable_temp): if cable_temp in ["NA", "N/A", "", "0C", "0"]: return None - # Get new and saved temperature values - cable_temp = int(cable_temp.split("C")[0]) if isinstance(cable_temp, str) else cable_temp - old_cable_temp = port_obj.counters_values.get(Constants.TEMP_COUNTER); - # Save new temperature value + cable_temp = int(cable_temp.split("C")[0]) if type(cable_temp) == str else cable_temp + old_cable_temp = port_obj.counters_values.get(Constants.TEMP_COUNTER, 0) port_obj.counters_values[Constants.TEMP_COUNTER] = cable_temp # Check temperature condition if cable_temp and (cable_temp > self.tmax): @@ -542,17 +523,17 @@ def check_ber_issue(self, port_obj, row, timestamp): return Issue(port_obj.port_name, Constants.ISSUE_BER) return None - def read_next_set_of_high_ber_or_pdr_ports(self, endpoint_port): + def read_next_set_of_high_ber_or_pdr_ports(self): """ Read the next set of ports and check if they have high BER, PDR, temperature or link downed issues """ issues = {} - ports_counters = self.ufm_client.get_telemetry(endpoint_port, Constants.PDR_DYNAMIC_NAME,self.test_mode) + ports_counters = self.ufm_client.get_telemetry(self.test_mode) if ports_counters is None: self.logger.error("Couldn't retrieve telemetry data") - raise DynamicTelemetryUnresponsive - for index, row in ports_counters.iterrows(): - port_name = f"{row.get('port_guid', '').split('x')[-1]}_{row.get('port_num', '')}" + return {} + for _, row in ports_counters.iterrows(): + port_name = f"{row.get(Constants.NODE_GUID, '').split('x')[-1]}_{row.get(Constants.PORT_NUMBER, '')}" if self.exclude_list.contains(port_name): # The port is excluded from analysis continue @@ -766,35 +747,6 @@ def get_isolation_state(self): port_state.update(Constants.STATE_ISOLATED, Constants.ISSUE_OONOC) self.ports_states[port] = port_state - def start_telemetry_session(self): - """ - Starts a telemetry session. - - Returns: - str: The port number if the dynamic session is started successfully, False otherwise. - """ - self.logger.info("Starting telemetry session") - guids = self.get_requested_guids() - response = self.ufm_client.start_dynamic_session(Constants.PDR_DYNAMIC_NAME, self.telemetry_counters, self.interval, guids, self.dynamic_extra_configuration) - if response and response.status_code == http.HTTPStatus.ACCEPTED: - port = str(int(response.content)) - else: - self.logger.error(f"Failed to start dynamic session: {response}") - return False - return port - - def update_telemetry_session(self): - """ - Updates the telemetry session by requesting and updating the dynamic session with the specified interval and guids. - - Returns: - The response from the UFM client after updating the dynamic session. - """ - self.logger.info("Updating telemetry session") - guids = self.get_requested_guids() - response = self.ufm_client.update_dynamic_session(Constants.PDR_DYNAMIC_NAME, self.interval, guids) - return response - def get_requested_guids(self): """ Get the requested GUIDs and their corresponding ports. @@ -812,63 +764,13 @@ def get_requested_guids(self): requested_guids = [{"guid": sys_guid, "ports": ports} for sys_guid, ports in guids.items()] return requested_guids - # this function create dynamic telemetry and returns the port of this telemetry - def run_telemetry_get_port(self): - """ - Runs the telemetry and returns the endpoint port. - - If the test mode is enabled, it returns the test mode port. - Otherwise, it waits for the dynamic session to start, starts the telemetry session, - and retrieves the endpoint port. - - Returns: - int: The endpoint port for the telemetry. - - Raises: - Exception: If an error occurs during the process. - """ - if self.test_mode: - return Constants.TEST_MODE_PORT - try: - while True: - session_state = self.ufm_client.get_dynamic_session_state(Constants.PDR_DYNAMIC_NAME) - if session_state == DynamicSessionState.RUNNING: - # Telemetry session is running - break - if session_state == DynamicSessionState.NONE: - # Start new session - self.logger.info("Waiting for dynamic session to start") - endpoint_port = self.start_telemetry_session() - time.sleep(self.dynamic_wait_time) - else: - # Stop inactive session - self.logger.info("Waiting for inactive dynamic session to stop") - self.ufm_client.stop_dynamic_session(Constants.PDR_DYNAMIC_NAME) - time.sleep(self.dynamic_wait_time) - except Exception as e: - self.ufm_client.stop_dynamic_session(Constants.PDR_DYNAMIC_NAME) - time.sleep(self.dynamic_wait_time) - endpoint_port = self.ufm_client.dynamic_session_get_port(Constants.PDR_DYNAMIC_NAME) - return endpoint_port - - def restart_telemetry_session(self): - """ - Restart the dynamic telemetry session and return the new endpoint port - """ - self.logger.info("Restarting telemetry session") - self.ufm_client.stop_dynamic_session(Constants.PDR_DYNAMIC_NAME) - time.sleep(self.dynamic_wait_time) - endpoint_port = self.run_telemetry_get_port() - return endpoint_port - def main_flow(self): """ Executes the main flow of the Isolation Manager. This method synchronizes with the telemetry clock, retrieves ports metadata, - starts the telemetry session, and continuously retrieves telemetry data to - determine the states of the ports. It handles dynamic telemetry unresponsiveness, - skips isolation if too many ports are detected as unhealthy, and evaluates + continuously retrieves telemetry data from secondary telemetry to + determine the states of the ports. skips isolation if too many ports are detected as unhealthy, and evaluates isolation and deisolation for reported issues and ports with specific causes. Args: @@ -880,9 +782,6 @@ def main_flow(self): self.logger.info("Isolation Manager initialized, starting isolation loop") self.get_ports_metadata() self.logger.info("Retrieved ports metadata") - endpoint_port = self.run_telemetry_get_port() - self.logger.info("telemetry session started") - dynamic_telemetry_unresponsive_count = 0 while(True): try: t_begin = time.time() @@ -894,15 +793,9 @@ def main_flow(self): self.logger.info(f"Retrieving test mode telemetry data to determine ports' states: iteration {self.test_iteration}") self.test_iteration += 1 try: - issues = self.read_next_set_of_high_ber_or_pdr_ports(endpoint_port) - except DynamicTelemetryUnresponsive: - dynamic_telemetry_unresponsive_count += 1 - if dynamic_telemetry_unresponsive_count > self.dynamic_unresponsive_limit: - self.logger.error(f"Dynamic telemetry is unresponsive for {dynamic_telemetry_unresponsive_count} times, restarting telemetry session...") - endpoint_port = self.restart_telemetry_session() - dynamic_telemetry_unresponsive_count = 0 - self.test_iteration = 0 - continue + issues = self.read_next_set_of_high_ber_or_pdr_ports() + except (KeyError,) as e: + self.logger.error(f"failed to read information with error {e}") if len(issues) > self.max_num_isolate: # UFM send external event event_msg = "got too many ports detected as unhealthy: %d, skipping isolation" % len(issues) @@ -937,7 +830,3 @@ def main_flow(self): self.logger.warning(traceback_err) t_end = time.time() time.sleep(max(1, self.interval - (t_end - t_begin))) - -# this is a callback for API exposed by this code - second phase -# def work_reportingd(port): -# PORTS_STATE[port].update(Constants.STATE_TREATED, Constants.ISSUE_INIT) diff --git a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/ufm_communication_mgr.py b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/ufm_communication_mgr.py index 1b38a0928..3f2cfe33e 100644 --- a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/ufm_communication_mgr.py +++ b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/ufm_communication_mgr.py @@ -11,21 +11,14 @@ # from enum import Enum +import urllib.error from constants import PDRConstants as Constants import requests import logging -import copy +import urllib import http import pandas as pd -class DynamicSessionState(Enum): - """ - States of telemetry session instance - """ - NONE = 0 - INACTIVE = 1 - RUNNING = 2 - class UFMCommunicator: def __init__(self, host='127.0.0.1', ufm_port=8000): @@ -40,12 +33,14 @@ def get_request(self, uri, headers=None): request = self.ufm_protocol + '://' + self._host + uri if not headers: headers = self.headers - response = requests.get(request, verify=False, headers=headers) - logging.info("UFM API Request Status: {}, URL: {}".format(response.status_code, request)) - if response.status_code == http.client.OK: - return response.json() - else: - return + try: + response = requests.get(request, verify=False, headers=headers) + logging.info("UFM API Request Status: {}, URL: {}".format(response.status_code, request)) + if response.status_code == http.client.OK: + return response.json() + except ConnectionRefusedError as e: + logging.error(f"failed to get data from {request} with error {e}") + return def send_request(self, uri, data, method=Constants.POST_METHOD, headers=None): request = self.ufm_protocol + '://' + self._host + uri @@ -59,18 +54,23 @@ def send_request(self, uri, data, method=Constants.POST_METHOD, headers=None): response = requests.delete(url=request, verify=False, headers=headers) logging.info("UFM API Request Status: {}, URL: {}".format(response.status_code, request)) return response - - def get_telemetry(self, port, instance_name,test_mode): + + def get_telemetry(self,test_mode): + """ + get the telemetry from secondary telemetry, if it in test mode it get from the simulation + return DataFrame of the telemetry + """ if test_mode: url = f"http://127.0.0.1:9090/csv/xcset/simulated_telemetry" else: - url = f"http://127.0.0.1:{port}/csv/xcset/{instance_name}" + url = f"http://127.0.0.1:{Constants.SECONDARY_TELEMETRY_PORT}/csv/xcset/{Constants.SECONDARY_INSTANCE}" try: telemetry_data = pd.read_csv(url) - except Exception as e: + except (pd.errors.ParserError, pd.errors.EmptyDataError, urllib.error.URLError) as e: logging.error(f"Failed to get telemetry data from UFM, fetched url={url}. Error: {e}") telemetry_data = None return telemetry_data + def send_event(self, message, event_id=Constants.EXTERNAL_EVENT_NOTICE, external_event_name="PDR Plugin Event", external_event_type="PDR Plugin Event"): data = { @@ -119,41 +119,3 @@ def get_ports_metadata(self): def get_port_metadata(self, port_name): return self.get_request("%s/%s" % (Constants.GET_PORTS_REST, port_name)) - - def start_dynamic_session(self, instance_name, counters, sample_rate, guids, extra_configuration=None): - data = { - "counters": counters, - "sample_rate": sample_rate, - "requested_guids": guids, - "is_registered_discovery": False - } - if extra_configuration: - data["configuration"] = extra_configuration - return self.send_request(Constants.DYNAMIC_SESSION_REST % instance_name, data, method=Constants.POST_METHOD) - - def update_dynamic_session(self, instance_name, sample_rate, guids): - data = { - "sample_rate": sample_rate, - "requested_guids": guids - } - return self.send_request(Constants.DYNAMIC_SESSION_REST % instance_name, data, method=Constants.PUT_METHOD) - - def get_dynamic_session_state(self, instance_name): - response = self.get_request(Constants.STATUS_DYNAMIC_SESSION_REST) - if response: - instance_status = response.get(instance_name) - if instance_status: - if instance_status.get("status") == "running": - return DynamicSessionState.RUNNING - else: - return DynamicSessionState.INACTIVE - return DynamicSessionState.NONE - - def stop_dynamic_session(self, instance_name): - data = {} - return self.send_request(Constants.DYNAMIC_SESSION_REST % instance_name, data, method=Constants.DELETE_METHOD) - - def dynamic_session_get_port(self, instance_name): - data = self.get_request(Constants.DYNAMIC_SESSION_REST % instance_name) - if data: - return data.get("endpoint_port") From 5c31430b01ff6156f6986b9c9fde4296ebdcedc6 Mon Sep 17 00:00:00 2001 From: ananalaghbar <79898567+ananalaghbar@users.noreply.github.com> Date: Mon, 26 Aug 2024 11:52:14 +0300 Subject: [PATCH 2/6] issue: 3966106: [UFM events Grafana Dashboard] - Build the docker & configure the required components (fluentd, Loki & Grafana) (#229) --- .../.ci/ci_matrix.yaml | 48 ++ .../build/Dockerfile | 51 ++ .../build/docker_build.sh | 126 ++++ .../conf/fluentd/fluentd.conf | 46 ++ .../conf/fluentd/parse_object_id.rb | 74 ++ .../conf/grafana/grafana.ini | 7 + .../provisioning/dashboards/dashboard.yaml | 11 + .../dashboards/json/UFM_Events_dashboard.json | 690 ++++++++++++++++++ .../datasources/loki-datasource.yaml | 11 + .../conf/loki/loki-local-config.yaml | 60 ++ .../conf/supervisord.conf | 51 ++ ...ents_grafana_dashboard_shared_volumes.conf | 2 + .../grafana_dashboard.png | Bin 0 -> 102408 bytes .../scripts/deinit.sh | 20 + .../scripts/init.sh | 48 ++ 15 files changed, 1245 insertions(+) create mode 100644 plugins/ufm_events_grafana_dashboard_plugin/.ci/ci_matrix.yaml create mode 100644 plugins/ufm_events_grafana_dashboard_plugin/build/Dockerfile create mode 100755 plugins/ufm_events_grafana_dashboard_plugin/build/docker_build.sh create mode 100644 plugins/ufm_events_grafana_dashboard_plugin/conf/fluentd/fluentd.conf create mode 100755 plugins/ufm_events_grafana_dashboard_plugin/conf/fluentd/parse_object_id.rb create mode 100644 plugins/ufm_events_grafana_dashboard_plugin/conf/grafana/grafana.ini create mode 100644 plugins/ufm_events_grafana_dashboard_plugin/conf/grafana/provisioning/dashboards/dashboard.yaml create mode 100644 plugins/ufm_events_grafana_dashboard_plugin/conf/grafana/provisioning/dashboards/json/UFM_Events_dashboard.json create mode 100644 plugins/ufm_events_grafana_dashboard_plugin/conf/grafana/provisioning/datasources/loki-datasource.yaml create mode 100644 plugins/ufm_events_grafana_dashboard_plugin/conf/loki/loki-local-config.yaml create mode 100644 plugins/ufm_events_grafana_dashboard_plugin/conf/supervisord.conf create mode 100644 plugins/ufm_events_grafana_dashboard_plugin/conf/ufm_events_grafana_dashboard_shared_volumes.conf create mode 100644 plugins/ufm_events_grafana_dashboard_plugin/grafana_dashboard.png create mode 100755 plugins/ufm_events_grafana_dashboard_plugin/scripts/deinit.sh create mode 100755 plugins/ufm_events_grafana_dashboard_plugin/scripts/init.sh diff --git a/plugins/ufm_events_grafana_dashboard_plugin/.ci/ci_matrix.yaml b/plugins/ufm_events_grafana_dashboard_plugin/.ci/ci_matrix.yaml new file mode 100644 index 000000000..c46280ca2 --- /dev/null +++ b/plugins/ufm_events_grafana_dashboard_plugin/.ci/ci_matrix.yaml @@ -0,0 +1,48 @@ +--- +job: ufm-ufm_events_grafana_dashboard-plugin + +registry_host: harbor.mellanox.com +registry_path: /swx-storage/ci-demo +registry_auth: swx-storage + +env: + plugin_dir: ufm_events_grafana_dashboard_plugin + plugin_name: ufm-plugin-ufm_events_grafana_dashboard + DOCKER_CLI_EXPERIMENTAL: enabled + +kubernetes: + cloud: swx-k8s-spray + +volumes: + - {mountPath: /var/run/docker.sock, hostPath: /var/run/docker.sock} + - {mountPath: /auto/UFM, hostPath: /auto/UFM } + + +runs_on_dockers: + - {file: '.ci/Dockerfile', arch: 'x86_64', name: 'plugin_worker', tag: 'latest'} + + +steps: + - name: Build Plugin + containerSelector: "{name: 'plugin_worker'}" + run: | + cd plugins/$plugin_dir/build + bash -x ./docker_build.sh latest / + ls -l / + cp /ufm-plugin* /auto/UFM/tmp/${JOB_NAME}/${BUILD_ID}/ + parallel: true + + +pipeline_start: + run: | + mkdir -p /auto/UFM/tmp/${JOB_NAME}/${BUILD_ID} + + +pipeline_stop: + run: | + echo 'All done'; + #sudo rm -rf /auto/UFM/tmp/${JOB_NAME}/${BUILD_ID} + + +# Fail job if one of the steps fails or continue +failFast: false diff --git a/plugins/ufm_events_grafana_dashboard_plugin/build/Dockerfile b/plugins/ufm_events_grafana_dashboard_plugin/build/Dockerfile new file mode 100644 index 000000000..47995f935 --- /dev/null +++ b/plugins/ufm_events_grafana_dashboard_plugin/build/Dockerfile @@ -0,0 +1,51 @@ +FROM ubuntu:22.04 + +LABEL maintainer="anana@nvidia.com" + +ARG PLUGIN_NAME +ARG BASE_PATH=/opt/ufm/ufm_plugin_${PLUGIN_NAME} +ARG SRC_BASE_DIR=${PLUGIN_NAME}_plugin +ARG ETC_ALTERNATIVE_PATH=/var/etc +ARG SUPERVISOR_PATH=${ETC_ALTERNATIVE_PATH}/supervisor +ENV DEBIAN_FRONTEND=noninteractive +ENV REQUIRED_UFM_VERSION=6.12.0 + +COPY ${SRC_BASE_DIR}/ ${BASE_PATH}/${SRC_BASE_DIR}/ +COPY ${SRC_BASE_DIR}/scripts/ / + +RUN apt-get update && apt-get upgrade -y && \ + # Install plugin dependacies + apt-get install -y supervisor vim tzdata wget unzip curl \ + # Install Fluentd prerequisites + gnupg build-essential ruby ruby-dev \ + # Install Grafana prerequisites + libfontconfig1 musl || apt --fix-broken install -y && \ + # Install Fluentd + gem install fluentd --no-document && \ + fluent-gem install fluent-plugin-script fluent-plugin-grafana-loki && \ + # Clean up Fluentd development packages + apt-get remove --purge -y ruby-dev build-essential && \ + apt-get autoremove -y && \ + # Install Loki + wget https://github.com/grafana/loki/releases/download/v3.1.0/loki-linux-amd64.zip && \ + unzip loki-linux-amd64.zip && \ + mv loki-linux-amd64 /usr/local/bin/loki && \ + rm loki-linux-amd64.zip && \ + # Install Grafana + wget https://dl.grafana.com/oss/release/grafana_11.1.0_amd64.deb && \ + dpkg -i grafana_11.1.0_amd64.deb && \ + rm grafana_11.1.0_amd64.deb && \ + # Final cleanup + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# move /etc/supervisor from the /etc, /etc dir will be overridden by the shared volume +RUN mkdir -p ${ETC_ALTERNATIVE_PATH} && mv /etc/supervisor ${ETC_ALTERNATIVE_PATH} + +RUN sed -i "s|/etc/supervisor/conf.d/\*.conf|${SUPERVISOR_PATH}/conf.d/\*.conf|g" ${SUPERVISOR_PATH}/supervisord.conf + +# Copy Supervisor configuration file +COPY ${SRC_BASE_DIR}/conf/supervisord.conf ${SUPERVISOR_PATH}/conf.d/ + +# Start services using supervisord +CMD ["/usr/bin/supervisord", "-c", "/var/etc/supervisor/supervisord.conf"] diff --git a/plugins/ufm_events_grafana_dashboard_plugin/build/docker_build.sh b/plugins/ufm_events_grafana_dashboard_plugin/build/docker_build.sh new file mode 100755 index 000000000..d2fa8476c --- /dev/null +++ b/plugins/ufm_events_grafana_dashboard_plugin/build/docker_build.sh @@ -0,0 +1,126 @@ +#!/bin/bash +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. + +set -eE + +if [ "${EUID}" -ne 0 ] + then echo "Please run the script as root" + exit 1 +fi + +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" +PARENT_DIR=$(realpath "${SCRIPT_DIR}/../../../") + +PLUGIN_NAME=ufm_events_grafana_dashboard +IMAGE_NAME="ufm-plugin-${PLUGIN_NAME}" +IMAGE_VERSION=$1 +OUT_DIR=$2 +RANDOM_HASH=$3 + +echo "RANDOM_HASH : [${RANDOM_HASH}]" +echo "SCRIPT_DIR : [${SCRIPT_DIR}]" +echo " " +echo "IMAGE_VERSION: [${IMAGE_VERSION}]" +echo "IMAGE_NAME : [${IMAGE_NAME}]" +echo "OUT_DIR : [${OUT_DIR}]" +echo " " + +if [ -z "${OUT_DIR}" ]; then + OUT_DIR="." +fi +if [ -z "${IMAGE_VERSION}" ]; then + IMAGE_VERSION="latest" +fi + +function create_out_dir() +{ + build_dir=$(mktemp --tmpdir -d ${IMAGE_NAME}_output_XXXXXXXX) + chmod 777 ${build_dir} + echo ${build_dir} +} + +function build_docker_image() +{ + build_dir=$1 + image_name=$2 + image_version=$3 + out_dir=$4 + random_hash=$5 + keep_image=$6 + prefix="mellanox" + + echo "build_docker_image" + echo " build_dir : [${build_dir}]" + echo " image_name : [${image_name}]" + echo " image_version : [${image_version}]" + echo " random_hash : [${random_hash}]" + echo " out_dir : [${out_dir}]" + echo " keep_image : [${keep_image}]" + echo " prefix : [${prefix}]" + echo " " + if [ "${IMAGE_VERSION}" == "0.0.00-0" ]; then + full_image_version="${image_name}_${image_version}-${random_hash}" + else + full_image_version="${image_name}_${image_version}" + fi + + echo " full_image_version : [${full_image_version}]" + + image_with_prefix="${prefix}/${image_name}" + image_with_prefix_and_version="${prefix}/${image_name}:${image_version}" + + pushd ${build_dir} + + echo "docker build --network host --no-cache --pull -t ${image_with_prefix_and_version} . --compress --build-arg PLUGIN_NAME=${PLUGIN_NAME}" + + docker build --network host --no-cache --pull -t ${image_with_prefix_and_version} . --compress --build-arg PLUGIN_NAME=${PLUGIN_NAME} + exit_code=$? + popd + if [ $exit_code -ne 0 ]; then + echo "Failed to build image" + return $exit_code + fi + + printf "\n\n\n" + echo "docker images | grep ${image_with_prefix}" + docker images | grep ${image_with_prefix} + printf "\n\n\n" + + echo "docker save ${image_with_prefix_and_version} | gzip > ${out_dir}/${full_image_version}-docker.img.gz" + docker save ${image_with_prefix_and_version} | gzip > ${out_dir}/${full_image_version}-docker.img.gz + exit_code=$? + if [ $exit_code -ne 0 ]; then + echo "Failed to save image" + return $exit_code + fi + if [ "$keep_image" != "y" -a "$keep_image" != "Y" ]; then + docker image rm -f ${image_with_prefix_and_version} + fi + return 0 +} + + +pushd ${SCRIPT_DIR} + +echo ${IMAGE_VERSION} > ../../${PLUGIN_NAME}_plugin/version + +BUILD_DIR=$(create_out_dir) +cp Dockerfile ${BUILD_DIR} +cp -r ../../${PLUGIN_NAME}_plugin ${BUILD_DIR} + +echo "BUILD_DIR : [${BUILD_DIR}]" + +build_docker_image $BUILD_DIR $IMAGE_NAME $IMAGE_VERSION $OUT_DIR ${RANDOM_HASH} +exit_code=$? +rm -rf ${BUILD_DIR} +popd +exit $exit_code diff --git a/plugins/ufm_events_grafana_dashboard_plugin/conf/fluentd/fluentd.conf b/plugins/ufm_events_grafana_dashboard_plugin/conf/fluentd/fluentd.conf new file mode 100644 index 000000000..32d052077 --- /dev/null +++ b/plugins/ufm_events_grafana_dashboard_plugin/conf/fluentd/fluentd.conf @@ -0,0 +1,46 @@ + + @type tail + path /opt/ufm/files/log/event.log + pos_file /config/event.pos + tag ufm-events + read_from_head true # set it to false if you want to skip the existing logs + +# @type regexp +# expression /^(?[\d\-]+ [\d\:\.]+) \[\d+\] \[\d+\] (?\w+) (?:Site \[[\w-]*\] )?\[(?\w+)\] (?\w+) \[(?.*)\]\: (((?[ \w\/\-]+)((\,|\:|\.) (?.*))?)|(?.*))$/ + # some logs might be multilines entry that should be handled: + @type multiline + format_firstline /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})/ # to match the first line of a log entry, which is date time format + format1 /^(?[\d\-]+ [\d\:\.]+) \[\d+\] \[\d+\] (?\w+) (?:Site \[[\w-]*\] )?\[(?\w+)\] (?\w+) \[(?.*)\]\: (((?[ \w\/\-]+)((\,|\:|\.) (?.*))?)|(?.*))$/ + time_key timestamp + time_format %Y-%m-%d %H:%M:%S.%L + + + + + @type script + path /config/fluentd/parse_object_id.rb + + + + @type loki + url "http://127.0.0.1:3101" + + @type memory + flush_interval 10s + + + extra_labels {"job":"ufm-events"} + line_format json + diff --git a/plugins/ufm_events_grafana_dashboard_plugin/conf/fluentd/parse_object_id.rb b/plugins/ufm_events_grafana_dashboard_plugin/conf/fluentd/parse_object_id.rb new file mode 100755 index 000000000..2b425eeb3 --- /dev/null +++ b/plugins/ufm_events_grafana_dashboard_plugin/conf/fluentd/parse_object_id.rb @@ -0,0 +1,74 @@ +def filter(tag, time, record) + objtype = record['object_id'] + type = record['object_type'] + case type + when 'IBPort', 'Computer' + # Match the object_id against the regex pattern + match = /(?:Computer|Switch): (?[^\/]+) \/ (?[^\]]+)\] \[dev_id: (?[a-f0-9]+)/.match(objtype) + if match + # Extract and assign the captured groups to new fields + record['device_name'] = match[:device_name] + record['port_num'] = match[:port_num] + record['node_guid'] = match[:device_guid] + # Assign the object_id to the matched part of the string + record['object_id'] = "#{match[0].split('] [').first}" + + # Determine the device_type based on the prefix + if match[0].start_with?('Computer') + record['device_type'] = 'host' + elsif match[0].start_with?('Switch') + record['device_type'] = 'switch' + end + end + when 'Site' + record['object_id'] = 'Site' +# when 'Computer' +# match = /(Computer.*)\] \[/.match(objtype) +# record['object_id'] = match ? match[1] : objtype + when 'Module' + match = /(Switch.*)\] \[/.match(objtype) + record['object_id'] = match ? match[1] : objtype + when 'Link' + match = /(?[a-f0-9]+)_(?[0-9]+)[\w ]+\: (?[a-f0-9]+)_(?[0-9]+)/.match(objtype) + # match = /([a-f0-9]+\_[0-9]+)[\w ]+\: ([a-f0-9]+_[0-9]+)/.match(objtype) + # record['object_id'] = match ? "#{match[1]}:#{match[2]}" : objtype + + if match + record['port_guid'] = match[:port_guid] + record['port_num'] = match[:port_num] + record['link_partner_port_guid'] = match[:link_partner_port_guid] + record['link_partner_port_num'] = match[:link_partner_port_num] + record['object_id'] = "#{match[:port_guid]}_#{match[:port_num]}:#{match[:link_partner_port_guid]}_#{match[:link_partner_port_num]}" + else + record['object_id'] = objtype + end + else + record['object_id'] = objtype + end + + + # Extract and set event and event_details + event = record['event'] + event_details = record['event_details'] + + if event.nil? || event.empty? + event = record['event_fallback'] + end + + # Check and remove "None" from event and event_details + if event && event.include?("None") + event = event.gsub("None", "") + end + + if event_details && event_details.include?("None") + event_details = event_details.gsub("None", "") + end + + record['event'] = event + record['event_details'] = event_details + + # Remove the event_fallback field + record.delete('event_fallback') + + record +end diff --git a/plugins/ufm_events_grafana_dashboard_plugin/conf/grafana/grafana.ini b/plugins/ufm_events_grafana_dashboard_plugin/conf/grafana/grafana.ini new file mode 100644 index 000000000..3f2631c89 --- /dev/null +++ b/plugins/ufm_events_grafana_dashboard_plugin/conf/grafana/grafana.ini @@ -0,0 +1,7 @@ +[server] +# The http port to use +http_port = 3002 +[paths] +data=/data +logs=/var/log +provisioning = /config/grafana/provisioning diff --git a/plugins/ufm_events_grafana_dashboard_plugin/conf/grafana/provisioning/dashboards/dashboard.yaml b/plugins/ufm_events_grafana_dashboard_plugin/conf/grafana/provisioning/dashboards/dashboard.yaml new file mode 100644 index 000000000..498c2264f --- /dev/null +++ b/plugins/ufm_events_grafana_dashboard_plugin/conf/grafana/provisioning/dashboards/dashboard.yaml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: 'default' + orgId: 1 + folder: '' + type: file + disableDeletion: false + updateIntervalSeconds: 10 + options: + path: /config/grafana/provisioning/dashboards/json diff --git a/plugins/ufm_events_grafana_dashboard_plugin/conf/grafana/provisioning/dashboards/json/UFM_Events_dashboard.json b/plugins/ufm_events_grafana_dashboard_plugin/conf/grafana/provisioning/dashboards/json/UFM_Events_dashboard.json new file mode 100644 index 000000000..e1211834f --- /dev/null +++ b/plugins/ufm_events_grafana_dashboard_plugin/conf/grafana/provisioning/dashboards/json/UFM_Events_dashboard.json @@ -0,0 +1,690 @@ +{ + "__inputs": [ + { + "name": "DS_LOKI", + "label": "loki", + "description": "", + "type": "datasource", + "pluginId": "loki", + "pluginName": "Loki" + } + ], + "__elements": {}, + "__requires": [ + { + "type": "panel", + "id": "gauge", + "name": "Gauge", + "version": "" + }, + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "11.1.0" + }, + { + "type": "datasource", + "id": "loki", + "name": "Loki", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "table", + "name": "Table", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "orange", + "value": 70 + }, + { + "color": "red", + "value": 85 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 8, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "11.1.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "editorMode": "builder", + "expr": "sum by (severity) (count_over_time({job=\"ufm-events\"}[$__range]))", + "legendFormat": "{{severity}}", + "queryType": "range", + "refId": "A" + } + ], + "title": "Events Stats By Severity", + "type": "gauge" + }, + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "orange", + "value": 70 + }, + { + "color": "red", + "value": 85 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 10, + "w": 16, + "x": 8, + "y": 0 + }, + "id": 3, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": false, + "sizing": "auto" + }, + "pluginVersion": "11.1.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "editorMode": "code", + "expr": "sum by (event_type) (count_over_time({job=\"ufm-events\"}[$__range]))", + "legendFormat": "{{event_type}}", + "queryType": "range", + "refId": "A" + } + ], + "title": "Events Stats By Category", + "type": "gauge" + }, + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 11, + "w": 24, + "x": 0, + "y": 10 + }, + "id": 1, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Time" + } + ] + }, + "pluginVersion": "11.1.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "editorMode": "builder", + "expr": "{job=\"ufm-events\"} | json", + "queryType": "range", + "refId": "A" + } + ], + "title": "Last 1000 Events", + "transformations": [ + { + "id": "extractFields", + "options": { + "source": "labels" + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": { + "Time": true, + "device_guid": true, + "device_name": true, + "device_type": true, + "event": true, + "event_details": true, + "event_type": true, + "labels": false, + "object_id": true, + "object_type": true, + "port_num": true, + "severity": true + }, + "indexByName": { + "Line": 12, + "Time": 0, + "detected_level": 16, + "device_guid": 5, + "device_name": 4, + "device_type": 6, + "event": 9, + "event_details": 10, + "event_type": 2, + "id": 15, + "job": 17, + "labelTypes": 14, + "labels": 11, + "object_id": 8, + "object_type": 3, + "port_num": 7, + "service_name": 18, + "severity": 1, + "timestamp": 19, + "tsNs": 13 + }, + "renameByName": { + "device_guid": "GUID", + "device_name": "Device Name", + "device_type": "Device Type", + "event": "Event", + "event_details": "Event Details", + "event_type": "Event Type", + "object_id": "Object ID", + "object_type": "Object Type", + "port_num": "Port Number", + "severity": "Severity" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 21 + }, + "id": 4, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Time" + } + ] + }, + "pluginVersion": "11.1.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "editorMode": "builder", + "expr": "{job=\"ufm-events\", object_type=\"IBPort\"} | json", + "queryType": "range", + "refId": "A" + } + ], + "title": "Ports Events", + "transformations": [ + { + "id": "extractFields", + "options": { + "source": "labels" + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": { + "Time": true, + "device_guid": true, + "device_name": true, + "device_type": true, + "event": true, + "event_details": true, + "event_type": true, + "labelTypes": false, + "object_type": true, + "port_num": true, + "severity": true + }, + "indexByName": { + "Time": 0, + "device_guid": 6, + "device_name": 4, + "device_type": 5, + "event": 8, + "event_details": 9, + "event_type": 2, + "object_type": 3, + "port_num": 7, + "severity": 1 + }, + "renameByName": { + "device_guid": "GUID", + "device_name": "Device Name", + "device_type": "Device Type", + "event": "Event", + "event_details": "Event Details", + "event_type": "Event Type", + "object_type": "Object Type", + "port_num": "Port Number", + "severity": "Severity" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 29 + }, + "id": 6, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.1.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "editorMode": "builder", + "expr": "{event_type=~\"Hardware\", job=\"ufm-events\"} | json", + "queryType": "range", + "refId": "A" + } + ], + "title": "Hardware Events", + "transformations": [ + { + "id": "extractFields", + "options": { + "source": "labels" + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": { + "Time": true, + "device_guid": true, + "device_name": true, + "device_type": true, + "event": true, + "event_details": true, + "event_type": true, + "object_type": true, + "port_num": true, + "severity": true + }, + "indexByName": { + "Time": 0, + "device_guid": 6, + "device_name": 4, + "device_type": 5, + "event": 8, + "event_details": 9, + "event_type": 2, + "object_type": 3, + "port_num": 7, + "severity": 1 + }, + "renameByName": { + "device_guid": "GUID", + "device_name": "Device Name", + "device_type": "Device Type", + "event": "Event", + "event_details": "Event Details", + "event_type": "Event Type", + "object_type": "Object Type", + "port_num": "Port Number", + "severity": "Severity" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 29 + }, + "id": 5, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "11.1.0", + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "editorMode": "builder", + "expr": "{event_type=~\"Fabric_Topology\", job=\"ufm-events\"} | json", + "queryType": "range", + "refId": "A" + } + ], + "title": "Topology Changes Events", + "transformations": [ + { + "id": "extractFields", + "options": { + "source": "labels" + } + }, + { + "id": "organize", + "options": { + "excludeByName": {}, + "includeByName": { + "Time": true, + "event": true, + "event_details": true, + "event_type": true, + "object_id": true, + "object_type": true, + "severity": true + }, + "indexByName": { + "Time": 0, + "event": 5, + "event_details": 6, + "event_type": 2, + "object_id": 4, + "object_type": 3, + "severity": 1 + }, + "renameByName": { + "event": "Event", + "event_details": "Event Details", + "event_type": "Event Type", + "labels": "", + "object_id": "Object ID", + "object_type": "Object Type", + "severity": "Severity" + } + } + } + ], + "type": "table" + } + ], + "refresh": "", + "schemaVersion": 39, + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "UFM Events", + "uid": "dds4v53h2h69sb", + "version": 8, + "weekStart": "" +} diff --git a/plugins/ufm_events_grafana_dashboard_plugin/conf/grafana/provisioning/datasources/loki-datasource.yaml b/plugins/ufm_events_grafana_dashboard_plugin/conf/grafana/provisioning/datasources/loki-datasource.yaml new file mode 100644 index 000000000..525dbb60d --- /dev/null +++ b/plugins/ufm_events_grafana_dashboard_plugin/conf/grafana/provisioning/datasources/loki-datasource.yaml @@ -0,0 +1,11 @@ +apiVersion: 1 + +datasources: + - name: Loki + type: loki + access: proxy + url: http://localhost:3101 + isDefault: true + uid: P8E80F9AEF21F6940 + jsonData: + maxLines: 1000 diff --git a/plugins/ufm_events_grafana_dashboard_plugin/conf/loki/loki-local-config.yaml b/plugins/ufm_events_grafana_dashboard_plugin/conf/loki/loki-local-config.yaml new file mode 100644 index 000000000..c775bfe53 --- /dev/null +++ b/plugins/ufm_events_grafana_dashboard_plugin/conf/loki/loki-local-config.yaml @@ -0,0 +1,60 @@ +# This is a complete configuration to deploy Loki backed by the filesystem. +# The index will be shipped to the storage via tsdb-shipper. + +auth_enabled: false + + +server: + http_listen_port: 3101 + grpc_listen_port: 0 # choose random free port, for internal usage only + grpc_server_max_recv_msg_size: 15728640 # 15MB + grpc_server_max_send_msg_size: 15728640 # 15MB + + +common: + path_prefix: /config/loki_db + storage: + filesystem: + chunks_directory: /config/loki_db/chunks + rules_directory: /config/loki_db/rules + replication_factor: 1 + ring: + kvstore: + store: inmemory + + +schema_config: + configs: + - from: 2020-05-15 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + +storage_config: + tsdb_shipper: + active_index_directory: /config/loki_db/index + cache_location: /config/loki_db/index_cache + filesystem: + directory: /config/loki_db/chunks + +compactor: + retention_enabled: true + working_directory: /config/loki_db/retention + compaction_interval: 1h + retention_delete_delay: 2h + delete_request_store: filesystem + +limits_config: + reject_old_samples: true + reject_old_samples_max_age: 168h # Accept logs up to 7 days old + retention_period: 672h # 28 days, minimum should be 24h and multiple of the index's period (currently period: 24h) + allow_structured_metadata: true # Allows the inclusion of structured metadata in logs, which can be useful for more detailed log querying and analysis. + volume_enabled: true # Enables volume-based limits, which can help in managing the amount of data ingested and stored by Loki + ingestion_rate_mb: 10 + ingestion_burst_size_mb: 20 + +pattern_ingester: + enabled: true diff --git a/plugins/ufm_events_grafana_dashboard_plugin/conf/supervisord.conf b/plugins/ufm_events_grafana_dashboard_plugin/conf/supervisord.conf new file mode 100644 index 000000000..e9c9bf528 --- /dev/null +++ b/plugins/ufm_events_grafana_dashboard_plugin/conf/supervisord.conf @@ -0,0 +1,51 @@ +[supervisord] +user=root +nodaemon = true +logfile=/opt/ufm/files/log/plugins/ufm_events_grafana_dashboard/supervisord.log +logfile_backups=5 +logfile_maxbytes=1048576 + +[program:loki] +command=/usr/local/bin/loki -config.file=/config/loki/loki-local-config.yaml +user=root +priority=150 +autostart=true +autorestart=true +startretries=1 +startsecs=1 +stdout_logfile=/opt/ufm/files/log/plugins/ufm_events_grafana_dashboard/loki_stdout.log +stderr_logfile=/opt/ufm/files/log/plugins/ufm_events_grafana_dashboard/loki_stdout.log +stdout_logfile_maxbytes=1048576 +stderr_logfile_maxbytes=1048576 +stdout_logfile_backups=5 +stderr_logfile_backups=5 + +[program:fluentd] +command=/usr/local/bin/fluentd -c /config/fluentd/fluentd.conf +user=root +priority=200 +autostart=true +autorestart=true +startretries=1 +startsecs=1 +stdout_logfile=/opt/ufm/files/log/plugins/ufm_events_grafana_dashboard/fluentd_stdout.log +stderr_logfile=/opt/ufm/files/log/plugins/ufm_events_grafana_dashboard/fluentd_stdout.log +stdout_logfile_maxbytes=1048576 +stderr_logfile_maxbytes=1048576 +stdout_logfile_backups=5 +stderr_logfile_backups=5 + +[program:grafana] +command=/usr/sbin/grafana-server --homepath=/usr/share/grafana --config=/config/grafana/grafana.ini +user=root +priority=300 +autostart=true +autorestart=true +startretries=1 +startsecs=1 +stdout_logfile=/opt/ufm/files/log/plugins/ufm_events_grafana_dashboard/grafana_stdout.log +stderr_logfile=/opt/ufm/files/log/plugins/ufm_events_grafana_dashboard/grafana_stdout.log +stdout_logfile_maxbytes=1048576 +stderr_logfile_maxbytes=1048576 +stdout_logfile_backups=5 +stderr_logfile_backups=5 diff --git a/plugins/ufm_events_grafana_dashboard_plugin/conf/ufm_events_grafana_dashboard_shared_volumes.conf b/plugins/ufm_events_grafana_dashboard_plugin/conf/ufm_events_grafana_dashboard_shared_volumes.conf new file mode 100644 index 000000000..f047b0f27 --- /dev/null +++ b/plugins/ufm_events_grafana_dashboard_plugin/conf/ufm_events_grafana_dashboard_shared_volumes.conf @@ -0,0 +1,2 @@ +/opt/ufm/files/log:/opt/ufm/files/log +/etc:/etc diff --git a/plugins/ufm_events_grafana_dashboard_plugin/grafana_dashboard.png b/plugins/ufm_events_grafana_dashboard_plugin/grafana_dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..0e9c7b4fb58b3103b332d4ba183fbf058e0e59fd GIT binary patch literal 102408 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 literal 0 HcmV?d00001 diff --git a/plugins/ufm_events_grafana_dashboard_plugin/scripts/deinit.sh b/plugins/ufm_events_grafana_dashboard_plugin/scripts/deinit.sh new file mode 100755 index 000000000..f7e892ac1 --- /dev/null +++ b/plugins/ufm_events_grafana_dashboard_plugin/scripts/deinit.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# @author: Anan Al-Aghbar +# @date: July 18, 2024 +# + +set -eE + +rm -rf /opt/ufm/files/log/event.pos + +exit 0 diff --git a/plugins/ufm_events_grafana_dashboard_plugin/scripts/init.sh b/plugins/ufm_events_grafana_dashboard_plugin/scripts/init.sh new file mode 100755 index 000000000..8a1d90449 --- /dev/null +++ b/plugins/ufm_events_grafana_dashboard_plugin/scripts/init.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# @author: Anan Al-Aghbar +# @date: July 18, 2024 +# +set -eE +PLUGIN_NAME=ufm_events_grafana_dashboard +SRC_DIR_PATH=/opt/ufm/ufm_plugin_${PLUGIN_NAME}/${PLUGIN_NAME}_plugin +CONFIG_PATH=/config + +cp -r ${SRC_DIR_PATH}/conf/* ${CONFIG_PATH} + +# UFM version test +required_ufm_version=${REQUIRED_UFM_VERSION} +# Check if the environment variable is set +if [ -z "${required_ufm_version}" ]; then + required_ufm_version=6.12.0 +fi +required_ufm_version=(${required_ufm_version//./ }) +echo "Required UFM version: ${required_ufm_version[0]}.${required_ufm_version[1]}.${required_ufm_version[2]}" + +if [ "$1" == "-ufm_version" ]; then + actual_ufm_version_string=$2 + actual_ufm_version=(${actual_ufm_version_string//./ }) + echo "Actual UFM version: ${actual_ufm_version[0]}.${actual_ufm_version[1]}.${actual_ufm_version[2]}" + if [ ${actual_ufm_version[0]} -ge ${required_ufm_version[0]} ] \ + && [ ${actual_ufm_version[1]} -ge ${required_ufm_version[1]} ] \ + && [ ${actual_ufm_version[2]} -ge ${required_ufm_version[2]} ]; then + echo "UFM version meets the requirements" + exit 0 + else + echo "UFM version is older than required" + exit 1 + fi +else + exit 1 +fi + +exit 1 \ No newline at end of file From 0d39f7b529563d9178b33850fb329d2b1bcb708e Mon Sep 17 00:00:00 2001 From: boazhaim <160493207+boazhaim@users.noreply.github.com> Date: Tue, 27 Aug 2024 12:36:06 +0300 Subject: [PATCH 3/6] issue4036264 - Adding link flapping logic (#241) * Inital commit for netfix link down logic * updage readme * Making links flapping a function call * Adding missing return * Fix e2e flow * After some cleaning * Removing the warnings * linter + ruff * Small alignments * PR Comments * Renaming to link flapping * Updated to the right usage * Making sure the CI won't trigger when utils has changes, since it does not have a job to run --- .ci/cidemo-init.sh | 2 +- utils/netfix/README.md | 19 ++ utils/netfix/__init__.py | 11 + utils/netfix/link_flapping.py | 300 +++++++++++++++++ utils/netfix/netfix_utils/__init__.py | 463 ++++++++++++++++++++++++++ utils/netfix/requirements.txt | 1 + 6 files changed, 795 insertions(+), 1 deletion(-) create mode 100644 utils/netfix/README.md create mode 100644 utils/netfix/__init__.py create mode 100644 utils/netfix/link_flapping.py create mode 100644 utils/netfix/netfix_utils/__init__.py create mode 100644 utils/netfix/requirements.txt diff --git a/.ci/cidemo-init.sh b/.ci/cidemo-init.sh index bfa2c0a20..9a6bdcfe8 100755 --- a/.ci/cidemo-init.sh +++ b/.ci/cidemo-init.sh @@ -7,7 +7,7 @@ cd .ci changed_files=$(git diff --name-only remotes/origin/$ghprbTargetBranch) # Check for changes excluding .gitmodules and root .ci directory -changes_excluding_gitmodules_and_root_ci=$(echo "$changed_files" | grep -v -e '.gitmodules' -e '.gitignore' -e '^\.ci/') +changes_excluding_gitmodules_and_root_ci=$(echo "$changed_files" | grep -v -e '.gitmodules' -e '.gitignore' -e '^\.ci/' -e '\utils') # Check if changes exist and only in a single plugin directory (including its .ci directory) if [ -n "$changes_excluding_gitmodules_and_root_ci" ] && [ $(echo "$changes_excluding_gitmodules_and_root_ci" | cut -d '/' -f1,2 | uniq | wc -l) -eq 1 ]; then diff --git a/utils/netfix/README.md b/utils/netfix/README.md new file mode 100644 index 000000000..a66f44403 --- /dev/null +++ b/utils/netfix/README.md @@ -0,0 +1,19 @@ +This path holds a copy of the logic original logic + +# This logic is still under development and should not be used + +For now, we are taking only the link down logic (Flapping links) + +The current way to run it is: +1. Install the python requirements +2. Load Python and import the function using +``` + from link_flapping import get_link_flapping +``` +3. Call the function with two second telemetry samples +``` +get_link_flapping(prev_second_telemetry_samples, older_second_telemetry_samples) + +``` + +This will output a datafram with all the flapping links info \ No newline at end of file diff --git a/utils/netfix/__init__.py b/utils/netfix/__init__.py new file mode 100644 index 000000000..a157e7482 --- /dev/null +++ b/utils/netfix/__init__.py @@ -0,0 +1,11 @@ +# +# Copyright © 2014-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# \ No newline at end of file diff --git a/utils/netfix/link_flapping.py b/utils/netfix/link_flapping.py new file mode 100644 index 000000000..eb1d362fd --- /dev/null +++ b/utils/netfix/link_flapping.py @@ -0,0 +1,300 @@ +# +# Copyright © 2014-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +""" +This is the main logic for identifying flapping links +""" + +from datetime import datetime +import traceback +import warnings + +import pandas as pd +from utils.netfix.netfix_utils import read_and_preprocessing_file, add_partner_info, add_link_hash_id + +warnings.filterwarnings("ignore", category=pd.errors.DtypeWarning) +warnings.filterwarnings("ignore", category=FutureWarning) +warnings.filterwarnings("ignore", category=pd.errors.SettingWithCopyWarning) + + +def _fill_missing_partner(df1, df2): + df2["link_partner_node_guid"] = df2["link_partner_node_guid"].replace( + "0x0000000000000000", "" + ) + df2["link_partner_port_num"] = df2["link_partner_port_num"].replace( + "0x0000000000000000", "" + ) + df2["link_partner_port_num"] = df2["link_partner_port_num"].replace("0", 0) + df2["link_partner_port_num"] = df2["link_partner_port_num"].replace("", 0) + df2["link_partner_port_num"] = df2["link_partner_port_num"].replace(0.0, 0) + df_join = pd.merge( + left=df2, + right=df1[ + [ + "Node_GUID", + "Port_Number", + "link_partner_node_guid", + "link_partner_port_num", + ] + ], + how="left", + on=["Node_GUID", "Port_Number"], + ) + df_join = df_join.rename( + columns={ + "link_partner_node_guid_x": "link_partner_node_guid", + "link_partner_port_num_x": "link_partner_port_num", + } + ) + df_join["link_partner_node_guid"] = ( + df_join["link_partner_node_guid"] + .replace("", pd.NA) + .fillna(df_join["link_partner_node_guid_y"]) + .replace(pd.NA, "") + ) + df_join["link_partner_port_num"] = ( + df_join["link_partner_port_num"] + .replace(0, pd.NA) + .fillna(df_join["link_partner_port_num_y"]) + .replace(pd.NA, 0) + ) + + df_join = df_join.drop( + columns=["link_partner_node_guid_y", "link_partner_port_num_y"] + ) + return df_join + + +def _get_suspected_real_linkdown( + df1_with_partner, df2_with_partner, min_to_filter_reboot_threshold=10 +): + func_summary_dict = {} + col4diff_calc = [ + "Effective_Errors", + "Symbol_Errors", + "PortRcvDataExtended", + "Link_Down", + "Link_Down_partner", + ] + # relevant column to present + col2_present = [ + "timestamp", + "timestamp_partner", + "Node_GUID", + "Device_ID", + "node_description", + "Port_Number", + "Device_ID_partner", + "link_partner_description", + "link_partner_node_guid", + "link_partner_port_num", + "Cable_SN", + "Time_since_last_clear", + "Time_since_last_clear_partner", + "Total_Raw_BER", + "Effective_BER", + "Symbol_BER", + "Total_Raw_BER_partner", + "Effective_BER_partner", + "Symbol_BER_partner", + "max_time_since_clear_by_switch_or_server", + "max_time_since_clear_by_switch_or_server_partner", + "Status_Message", + "local_reason_opcode", + "remote_reason_opcode", + "Status_Message_partner", + "local_reason_opcode_partner", + "remote_reason_opcode_partner", + "estimated_time", + "estimated_time_partner", + "time_to_link_up_msec", + "time_to_link_up_msec_partner", + ] + col4diff_calc + # take common columns + col2_present1 = [col for col in col2_present if col in df1_with_partner.columns] + col2_present1 = [col for col in col2_present1 if col in df2_with_partner.columns] + + join_iteration = pd.merge( + df1_with_partner[col2_present1], + df2_with_partner[col2_present1], + how="inner", + left_on=[ + "Node_GUID", + "Port_Number", + "link_partner_description", + "link_partner_node_guid", + "link_partner_port_num", + ], + right_on=[ + "Node_GUID", + "Port_Number", + "link_partner_description", + "link_partner_node_guid", + "link_partner_port_num", + ], + ) + # calculate diff columns + for col in col4diff_calc: + join_iteration["diff_" + col] = ( + join_iteration[col + "_y"] - join_iteration[col + "_x"] + ) + + # take the latest data to present + col_y = [col + "_y" for col in col2_present1] + col_latest = list(col2_present1) + + col_naming_dict = {} + col_naming_dict = dict(zip(col_y, col_latest)) + + join_iteration = join_iteration.rename(columns=col_naming_dict, errors="ignore") + + join_iteration = join_iteration.rename( + columns={ + "timestamp_x": "prev_timestamp", + "Link_Down_x": "prev_Link_Down", + "Link_Down_partner_x": "prev_Link_Down_partner", + "Time_since_last_clear_x": "prev_Time_since_last_clear", + "Time_since_last_clear_partner_x": "prev_Time_since_last_clear_partner", + }, + errors="ignore", + ) + + join_iteration = add_link_hash_id(join_iteration) + join_iteration1 = join_iteration + + # remove old link down by estimated time + join_iteration1 = join_iteration1[ + join_iteration1.estimated_time > join_iteration1.prev_timestamp + ] + + join_iteration1["estimated_time"] = join_iteration1["estimated_time"].apply( + lambda x: datetime.fromtimestamp(x).strftime("%Y-%m-%d %H:%M:%S") + ) + + # add support of linkdown_diff<0 but linkdown value is >0 + df_linkdown = join_iteration1[ + ( + abs( + join_iteration1.Time_since_last_clear + - join_iteration1.Time_since_last_clear_partner + ) + < 10 + ) + & ( + (join_iteration1.diff_Link_Down > 0) + | ((join_iteration1.diff_Link_Down < 0) & (join_iteration1.Link_Down > 0)) + ) + & ( + (join_iteration1.diff_Link_Down_partner > 0) + | ( + (join_iteration1.diff_Link_Down_partner < 0) + & (join_iteration1.Link_Down_partner > 0) + ) + ) + | ( + (join_iteration1.diff_Link_Down > 0) + & ( + abs( + join_iteration1.Time_since_last_clear_partner + - join_iteration1.max_time_since_clear_by_switch_or_server_partner + ) + > min_to_filter_reboot_threshold + ) + ) + | ( + (join_iteration1.local_reason_opcode == 25) + | (join_iteration1.remote_reason_opcode == 25) + ) + ] + + if ("Device_ID" in df_linkdown.columns) & ( + "Device_ID_partner" in df_linkdown.columns + ): + df_linkdown["link_type"] = "other" + mask = (df_linkdown.Device_ID.str.contains("Connect")) & ( + df_linkdown.Device_ID_partner.str.contains("Quantum") + ) + df_linkdown.loc[mask, "link_type"] = "switch-hca" + + mask = (df_linkdown.Device_ID.str.contains("Quantum")) & ( + df_linkdown.Device_ID_partner.str.contains("Connect") + ) + df_linkdown.loc[mask, "link_type"] = "switch-hca" + + mask = (df_linkdown.Device_ID.str.contains("Quantum")) & ( + df_linkdown.Device_ID_partner.str.contains("Quantum") + ) + df_linkdown.loc[mask, "link_type"] = "switch-switch" + + # use /2 because table have duplication A->B + B->A + func_summary_dict["Number of links - suspected linkdown switch-switch"] = ( + df_linkdown[df_linkdown["link_type"] == "switch-switch"] + .link_hash_id.drop_duplicates() + .shape[0] + ) + func_summary_dict["Number of links - suspected linkdown switch-hca"] = ( + df_linkdown[df_linkdown["link_type"] == "switch-hca"] + .link_hash_id.drop_duplicates() + .shape[0] + ) + + number_active_links = df2_with_partner[ + df2_with_partner.Phy_Manager_State == "Active" + ].shape[0] + number_link_down = df_linkdown.shape[0] + func_summary_dict["pct of link down"] = round( + 100 * number_link_down / number_active_links, 2 + ) + # set_df_col_order + col_order = ( + ["link_hash_id", "link_type"] + + list(df2_with_partner.columns) + + [ + "diff_Link_Down", + "diff_Link_Down_partner", + "prev_Link_Down", + "prev_Link_Down_partner", + "prev_Time_since_last_clear", + "prev_Time_since_last_clear_partner", + "estimated_time", + ] + ) + + col_order1 = [col for col in col_order if col in df_linkdown.columns] + df_linkdown = df_linkdown[col_order1] + + return df_linkdown, func_summary_dict + + +def get_link_flapping(prev_counters_csv, cur_counters_csv): + """ + Entry point to the flapping links logic. + Gets 2 telemetry sampling and returns a dataframe + with flapping links + """ + try: + df1 = read_and_preprocessing_file(prev_counters_csv) + df2 = read_and_preprocessing_file(cur_counters_csv) + + # complete missing partner in df2 based on df1 + df2 = _fill_missing_partner(df1, df2) + df1_with_partner = add_partner_info(df1) + df2_with_partner = add_partner_info(df2) + + # linkdown both sides + linkdown_df1, _ = _get_suspected_real_linkdown( + df1_with_partner, df2_with_partner, min_to_filter_reboot_threshold=10 + ) + return linkdown_df1 + except Exception as e: + print(e) + traceback.print_exc() + return pd.DataFrame() diff --git a/utils/netfix/netfix_utils/__init__.py b/utils/netfix/netfix_utils/__init__.py new file mode 100644 index 000000000..f5f250a16 --- /dev/null +++ b/utils/netfix/netfix_utils/__init__.py @@ -0,0 +1,463 @@ +# +# Copyright © 2014-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +""" +This module provides utility functions for the netfix_utils package. +""" + +import pandas as pd +from pandas.api.types import is_string_dtype +from pandas.api.types import is_numeric_dtype + + +Phy_Manager_State_dict_str = { + "Active_or_Linkup": "Active", + "Rx_disable": "RX_DISABLE" "", +} + +link_down_opcode_dict = { + 0: "No_link_down_indication", + 1: "Unknown_reason", + 2: "Hi_SER_or_Hi_BER", + 3: "Block_Lock_loss", + 4: "Alignment_loss", + 5: "FEC_sync_loss", + 6: "PLL_lock_loss", + 7: "FIFO_overflow", + 8: "false_SKIP_condition", + 9: "Minor_Error_threshold_exceeded", + 10: "Physical_layer_retransmission_timeout", + 11: "Heartbeat_errors", + 12: "Link_Layer_credit_monitoring_watchdog", + 13: "Link_Layer_integrity_threshold_exceeded", + 14: "Link_Layer_buffer_overrun", + 15: "Down_by_outband_command_with_healthy_link", + 16: "Down_by_outband_command_for_link_with_hi_ber", + 17: "Down_by_inband_command_with_healthy_link", + 18: "Down_by_inband_command_for_link_with_hi_ber", + 19: "Down_by_verification_GW", + 20: "Received_Remote_Fault", + 21: "Received_TS1", + 22: "Down_by_management_command", + 23: "Cable_was_unplugged", + 24: "Cable_access_issue", + 25: "Thermal_shutdown", + 26: "Current_issue", + 27: "Power_budget", + 28: "Fast_recovery_raw_ber", + 29: "Fast_recovery_effective_ber", + 30: "Fast_recovery_symbol_ber", + 31: "Fast_recovery_credit_watchdog", + 32: "Timeout", +} + + +Phy_Manager_State_dict = { + 0: "Disabled", + 2: "Polling", + 3: "Active", + 4: "Close", + 5: "ETH_PHY_UP", + 7: "RX_DISABLE", +} + +switch_dict = { + "0x0": "UNKNOWN", + "0xC738": "SwitchX and SwitchX-2", # Baz + "0x0246": "SwitchX in Flash Recovery Mode", + "0xCB20": "Switch-IB", # Pelican + "0x0247": "Switch-IB in Flash Recovery Mode", + "0xCB84": "Spectrum", # Condor + "0x0249": "Spectrum in Flash Recovery Mode", + "0xCF08": "Switch-IB 2", # Eagle + "0x024B": "Switch-IB 2 in Flash Recovery Mode", + "0xD2F0": "Quantum", # Raven + "0x024D": "Quantum in Flash Recovery Mode", + "0xCF6C": "Spectrum-2", # Phoenix + "0x024E": "Spectrum-2 in Flash Recovery Mode", + "0x024F": "Spectrum-2 in Secure Flash Recovery Mode (obsolete) [Internal]", + "0xCF70": "Spectrum-3", # Firebird + "0x0250": "Spectrum-3 in Flash Recovery Mode", + "0x0251": "Spectrum-3 in Secure Flash Recovery Mode (obsolete) [Internal]", + "0x0252": "Spectrum-3 Amos GearBox [Internal]", # Amos + "0x0253": "AGBM - Amos GearBox Manager", + "0xCF80": "Spectrum-4 Spectrum-4", # Albatross + "0x0254": "Spectrum-4 in Flash Recovery Mode", + "0x0255": "Spectrum-4 RMA [Internal]", + "0x0256": "Abir GearBox", # Abir + "0x0357": "Abir GearBox in Flash Recovery Mode", + "0x0358": "Abir GearBox in RMA", + "0xD2F2": "Quantum-2", # Blackbird + "0x0257": "Quantum-2 in Flash Recovery Mode", + "0x0258": "Quantum-2 RMA", +} + + +hca_dict = { + 4099: "ConnectX-3", + 4113: "Connect-IB", + 4115: "ConnectX-4", + 4117: "ConnectX-4 Lx", + 4119: "ConnectX-5", + 4121: "ConnectX-5 Ex", + 4123: "ConnectX-6", + 4125: "ConnectX-6 Dx", + 4129: "Connectx-7", + 41680: "BlueField with crypto enabled", + 41681: "BlueField with crypto disabled", + 41684: "BlueField-2 with crypto enabled", + 41685: "BlueField-2 with crypto disabled", + 41686: "BlueField-2 integrated ConnectX-6 Dx network controller", + 41690: "BlueField-3 SoC Crypto enabled", + 41691: "BlueField-3 SoC Crypto disabled", + 41692: "BlueField-3 integrated ConnectX-7 network controller", + 53001: "Aggregation Node", +} + + +def read_and_preprocessing_file(file_path: str): + """ + Read the file and align names + """ + df_file = pd.read_csv(file_path, encoding="ISO-8859-1") + + if df_file.shape[0] == 0: + print(" ERROR: empty file - ", file_path) + + df_file = df_file.rename(columns={"Node_Description": "node_description"}) + + df_file = df_file[ + df_file.node_description != "Mellanox Technologies Aggregation Node" + ] + + if "ts" in df_file.columns: + df_file = df_file.drop(columns=["timestamp"], errors="ignore") + df_file = df_file.rename(columns={"ts": "timestamp"}) + + # remove non numeric timestamp - remove all row + df_file.timestamp = pd.to_numeric(df_file.timestamp, errors="coerce") + df_file = df_file.dropna(subset=["timestamp"]) + # if timestamp in milisec divid by 1000 + if len(str(df_file["timestamp"].iloc[0])) > 15: + df_file["timestamp"] = df_file["timestamp"] / 1000000 + else: + df_file["timestamp"] = df_file["timestamp"] / 1000 + + df_file["timestamp"] = min(df_file.timestamp) + + # features handling + if "Temperature" in df_file.columns: + if (is_string_dtype(df_file["Temperature"])) & ( + len(pd.unique(df_file["Temperature"])) > 1 + ): + df_file.Temperature = df_file.Temperature.str.replace("C", "").astype( + "float" + ) + + # time since last clear + df_file = df_file.rename( + columns={"Time_since_last_clear_Min": "Time_since_last_clear"} + ) + df_file = df_file.rename( + columns={"Time_since_last_clear__Min_": "Time_since_last_clear"} + ) + df_file = df_file.rename( + columns={"Time_since_last_clear_[Min]": "Time_since_last_clear"} + ) + + df_file["estimated_time"] = ( + df_file["timestamp"] - 60 * df_file["Time_since_last_clear"] + ) + + # check basic columns that must exists + must_columns_in_files = [ + "Node_GUID", + "Port_Number", + "timestamp", + "Time_since_last_clear", + "link_partner_node_guid", + "link_partner_port_num", + "Link_Down", + ] + for col in must_columns_in_files: + if col not in df_file.columns: + print(col, " is missing in data !!!") + + df_file = df_file[df_file.Port_Number != 65] + + switch_dict1 = {int(k, 16): v for k, v in switch_dict.items()} + hca_dict1 = dict(hca_dict) + df_file = df_file.replace({"Device_ID": hca_dict1}) + df_file = df_file.replace({"Device_ID": switch_dict1}) + + df_file = df_file.replace({"Phy_Manager_State": Phy_Manager_State_dict}) + df_file = df_file.replace({"Phy_Manager_State": Phy_Manager_State_dict_str}) + + # replace strings in opcode by numbers + opcode_dict = {v: k for k, v in link_down_opcode_dict.items()} + df_file = df_file.replace({"local_reason_opcode": opcode_dict}) + df_file = df_file.replace({"remote_reason_opcode": opcode_dict}) + + if "time_to_link_up_ext_msec" in df_file.columns: + df_file = df_file.rename( + columns={"time_to_link_up_ext_msec": "time_to_link_up_msec"} + ) + + col_to_convert_to_float = [ + "Link_Down", + "Symbol_BER", + "Effective_BER", + "Port_Number", + "PortXmitDiscards", + "link_partner_port_num", + "max_delta_freq_0", + "max_delta_freq_1", + ] + + for col in col_to_convert_to_float: + if col in df_file.columns: + df_file[col] = pd.to_numeric(df_file[col], errors="coerce") + df_file.dropna(subset=[col], inplace=True) + else: + print(col, " not exists in dataframe") + + # add to df min /max/std of time since lase clear per switch/host name + add 'server_name' col + df_file = get_time_since_last_clear_per_groups(df_file) + + df_file = df_file.replace( + { + "Phy_Manager_State": { + "0": "Disabled", + "2": "Polling", + "3": "Active", + "4": "Close", + "5": "ETH_PHY_UP", + "7": "RX_DISABLE", + } + } + ) + + # drop columns that all is nan - relevant for meta which have 2 fw_version columns + df_file.dropna(how="all", axis=1, inplace=True) + + return df_file + + +def get_time_since_last_clear_per_groups(df): + """ + Get the last time since clear for each group""" + if "Device_ID" not in df: + return df + + df["server_name"] = "" + if is_numeric_dtype(df.Device_ID.dtype): + mask = df.Device_ID.astype(float).astype("Int64").isin(list(hca_dict.keys())) + else: + mask = df.Device_ID.astype(str).str.contains("Connect") + + if not "Time_since_last_clear" in df.columns: + print(" Time_since_last_clear not exists in df ") + return df + + df.loc[mask, "server_name"] = df.loc[mask, "node_description"].apply( + lambda x: x.split(" ")[0] + ) + + df_hca = df[mask] + df_hca_group = df_hca.groupby("server_name").agg( + {"Time_since_last_clear": ["min", "max", "std"]} + ) + df_hca_group = df_hca_group.reset_index() + df_hca_group.columns = [ + "server_name", + "min_Time_since_last_clear_server", + "max_Time_since_last_clear_server", + "std_Time_since_last_clear_server", + ] + df_hca_group.columns = [ + "server_name", + "min_Time_since_last_clear_server", + "max_Time_since_last_clear_server", + "std_Time_since_last_clear_server", + ] + + switch_dict1 = {int(k, 16): v for k, v in switch_dict.items()} + + if is_numeric_dtype(df.Device_ID.dtype): + (df.Device_ID.astype(float).astype("Int64").isin(list(switch_dict1.keys()))) + else: + mask = df.Device_ID.astype(str).str.contains("Quantum") + + df_switch = df[mask] + df_switch_group = df_switch.groupby("Node_GUID").agg( + {"Time_since_last_clear": ["min", "max", "std"]} + ) + df_switch_group = df_switch_group.reset_index() + df_switch_group.columns = [ + "Node_GUID", + "min_Time_since_last_clear_switch", + "max_Time_since_last_clear_switch", + "std_Time_since_last_clear_switch", + ] + + df_join1 = pd.merge( + left=df, + right=df_hca_group, + left_on=["server_name"], + right_on=["server_name"], + how="left", + ) + df_join2 = pd.merge( + left=df_join1, + right=df_switch_group, + left_on=["Node_GUID"], + right_on=["Node_GUID"], + how="left", + ) + + df_join2["max_time_since_clear_by_switch_or_server"] = df_join2[ + ["max_Time_since_last_clear_switch", "max_Time_since_last_clear_server"] + ].max(axis=1) + + df_join2["change_from_max_switch_or_server"] = ( + df_join2["max_time_since_clear_by_switch_or_server"] + - df_join2["Time_since_last_clear"] + ) + df_join2["guid_and_port"] = ( + df_join2["Node_GUID"] + "_" + df_join2["Port_Number"].astype(str) + ) + df_join2_group = ( + df_join2[df_join2.change_from_max_switch_or_server < 10] + .groupby(["server_name", "Node_GUID"]) + .agg({"guid_and_port": "nunique"}) + ) + df_join2_group = df_join2_group.reset_index() + df_join2_group.columns = [ + "server_name", + "Node_GUID", + "num_ports_in_last_switch_or_server_reboot", + ] + + df_join2 = pd.merge( + left=df_join2, right=df_join2_group, how="left", on=["server_name", "Node_GUID"] + ) + + df_join2 = df_join2.drop( + columns=[ + "min_Time_since_last_clear_switch", + "std_Time_since_last_clear_switch", + "min_Time_since_last_clear_server", + "std_Time_since_last_clear_server", + "guid_and_port", + ], + errors="ignore", + ) + + return df_join2 + + +def add_partner_info(df_link_down): + """ + Adding link partner ifo to the data + """ + columns_to_display = [ + "timestamp", + "layer", + "Device_ID", + "Cable_SN", + "Cable_PN", + "FW_Version", + "cable_fw_version", + "Node_GUID", + "Port_Number", + "node_description", + "layer", + "server_name", + "Link_Down", + "link_partner_node_guid", + "link_partner_port_num", + "link_partner_description", + "Total_Raw_BER", + "Effective_BER", + "Symbol_BER", + "Time_since_last_clear", + "sw_revision", + "Effective_Errors", + "Symbol_Errors", + "PortRcvDataExtended", + "max_time_since_clear_by_switch_or_server", + "Status_Message", + "local_reason_opcode", + "remote_reason_opcode", + "Phy_Manager_State", + "estimated_time", + "estimated_time_partner", + "time_to_link_up_msec", + "time_to_link_up_msec_partner", + ] + [col for col in df_link_down if "_power_lane" in col.lower()] + + columns_to_display0 = [ + col for col in columns_to_display if col in df_link_down.columns + ] + columns_to_display1 = [ + col for col in columns_to_display0 if col not in ["layer_partner"] + ] + columns_to_display1 = list(set(columns_to_display1)) + + # add number of link down to the partner to maybe the partner is problematic + df_link_down_with_partner_link_rows = pd.merge( + df_link_down[columns_to_display1], + df_link_down[columns_to_display1], + how="inner", + left_on=["link_partner_node_guid", "link_partner_port_num"], + right_on=["Node_GUID", "Port_Number"], + ) + + col_x = [col + "_x" for col in columns_to_display1] + col_local = list(columns_to_display1) + + col_y = [col + "_y" for col in columns_to_display1] + col_partner = [col + "_partner" for col in columns_to_display1] + + col_naming_dict = {} + col_naming_dict = dict(zip(col_x + col_y, col_local + col_partner)) + + df_link_down_with_partner_link_rows = df_link_down_with_partner_link_rows.rename( + columns=col_naming_dict + ) + + df_link_down_with_partner_link_rows = df_link_down_with_partner_link_rows.rename( + columns=col_naming_dict + ) + df_link_down_with_partner_link_rows1 = df_link_down_with_partner_link_rows + + col_order = list(df_link_down.columns) + [col + "_partner" for col in df_link_down] + + col_order1 = [ + col for col in col_order if col in df_link_down_with_partner_link_rows1.columns + ] + + return df_link_down_with_partner_link_rows1[col_order1] + + +def add_link_hash_id(join_iteration): + "Adding a new colum for link_had_id" + join_iteration["link_hash_id"] = join_iteration.apply( + lambda x: min(x["Node_GUID"], x["link_partner_node_guid"]) + + "_" + + max(x["Node_GUID"], x["link_partner_node_guid"]) + + "_" + + min(str(int(x["Port_Number"])), str(int(x["link_partner_port_num"]))) + + "_" + + max(str(int(x["Port_Number"])), str(int(x["link_partner_port_num"]))), + axis=1, + ) + return join_iteration diff --git a/utils/netfix/requirements.txt b/utils/netfix/requirements.txt new file mode 100644 index 000000000..1411a4a0b --- /dev/null +++ b/utils/netfix/requirements.txt @@ -0,0 +1 @@ +pandas \ No newline at end of file From 3c4d311d4db77bf64efed53f92fa2944d1b0eb07 Mon Sep 17 00:00:00 2001 From: boazhaim <160493207+boazhaim@users.noreply.github.com> Date: Wed, 28 Aug 2024 18:01:38 +0300 Subject: [PATCH 4/6] issue:4036267 - Move ufm log analyzer to sdk repo (#242) * Removing the temp venv * Fixing CI for log analyzer * Fixing ci file * removing whitespace * Fixing ci, as it should not run for ci config --- .ci/cidemo-init.sh | 2 +- .../ufm_log_analyzer_ci_workflow.yml | 24 + plugins/ufm_log_analyzer_plugin/README.md | 84 + .../img/loganalzer.png | Bin 0 -> 54762 bytes .../ufm_log_analyzer_plugin/log_analyzer.sh | 21 + .../src/loganalyze/.pylintrc | 6 + .../src/loganalyze/__init__.py | 14 + .../src/loganalyze/log_analyzer.py | 374 +++++ .../src/loganalyze/log_analyzers/__init__.py | 11 + .../loganalyze/log_analyzers/base_analyzer.py | 199 +++ .../log_analyzers/console_log_analyzer.py | 70 + .../src/loganalyze/log_analyzers/constants.py | 17 + .../log_analyzers/events_log_analyzer.py | 119 ++ .../log_analyzers/ibdiagnet_log_analyzer.py | 36 + .../log_analyzers/rest_api_log_analyzer.py | 70 + .../log_analyzers/ufm_health_analyzer.py | 75 + .../log_analyzers/ufm_log_analyzer.py | 293 ++++ .../log_analyzers/ufm_top_analyzer.py | 32 + .../src/loganalyze/log_parsing/__init__.py | 11 + .../src/loganalyze/log_parsing/base_regex.py | 31 + .../log_parsing/console_log_regex.py | 73 + .../loganalyze/log_parsing/event_log_regex.py | 97 ++ .../log_parsing/ibdiagnet_log_regex.py | 48 + .../src/loganalyze/log_parsing/log_parser.py | 66 + .../src/loganalyze/log_parsing/logs_regex.py | 32 + .../log_parsing/rest_api_log_regex.py | 59 + .../log_parsing/ufm_health_regex.py | 67 + .../src/loganalyze/log_parsing/ufm_log.py | 159 ++ .../src/loganalyze/logger.py | 34 + .../src/loganalyze/logs_csv/__init__.py | 12 + .../src/loganalyze/logs_csv/csv_handler.py | 60 + .../loganalyze/logs_extraction/__init__.py | 12 + .../logs_extraction/base_extractor.py | 26 + .../logs_extraction/directory_extractor.py | 50 + .../logs_extraction/tar_extractor.py | 110 ++ .../loganalyze/old_logic/base_log_parser.py | 60 + .../src/loganalyze/old_logic/constants.py | 21 + .../src/loganalyze/old_logic/csv_builder.py | 70 + .../old_logic/events_explorer.ipynb | 1477 +++++++++++++++++ .../src/loganalyze/old_logic/policy.csv | 296 ++++ .../old_logic/ufm_events_log_parser.py | 110 ++ .../src/loganalyze/pdf_creator.py | 84 + .../src/loganalyze/requirements.txt | 5 + .../src/loganalyze/utils/common.py | 57 + 44 files changed, 4573 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ufm_log_analyzer_ci_workflow.yml create mode 100644 plugins/ufm_log_analyzer_plugin/README.md create mode 100644 plugins/ufm_log_analyzer_plugin/img/loganalzer.png create mode 100755 plugins/ufm_log_analyzer_plugin/log_analyzer.sh create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/.pylintrc create mode 100755 plugins/ufm_log_analyzer_plugin/src/loganalyze/__init__.py create mode 100755 plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzer.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/__init__.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/base_analyzer.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/console_log_analyzer.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/constants.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/events_log_analyzer.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/ibdiagnet_log_analyzer.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/rest_api_log_analyzer.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/ufm_health_analyzer.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/ufm_log_analyzer.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/ufm_top_analyzer.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/__init__.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/base_regex.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/console_log_regex.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/event_log_regex.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/ibdiagnet_log_regex.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/log_parser.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/logs_regex.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/rest_api_log_regex.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/ufm_health_regex.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/ufm_log.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/logger.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_csv/__init__.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_csv/csv_handler.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_extraction/__init__.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_extraction/base_extractor.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_extraction/directory_extractor.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_extraction/tar_extractor.py create mode 100755 plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/base_log_parser.py create mode 100755 plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/constants.py create mode 100755 plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/csv_builder.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/events_explorer.ipynb create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/policy.csv create mode 100755 plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/ufm_events_log_parser.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/pdf_creator.py create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/requirements.txt create mode 100644 plugins/ufm_log_analyzer_plugin/src/loganalyze/utils/common.py diff --git a/.ci/cidemo-init.sh b/.ci/cidemo-init.sh index 9a6bdcfe8..dc0e31e35 100755 --- a/.ci/cidemo-init.sh +++ b/.ci/cidemo-init.sh @@ -7,7 +7,7 @@ cd .ci changed_files=$(git diff --name-only remotes/origin/$ghprbTargetBranch) # Check for changes excluding .gitmodules and root .ci directory -changes_excluding_gitmodules_and_root_ci=$(echo "$changed_files" | grep -v -e '.gitmodules' -e '.gitignore' -e '^\.ci/' -e '\utils') +changes_excluding_gitmodules_and_root_ci=$(echo "$changed_files" | grep -v -e '.gitmodules' -e '.gitignore' -e '^\.ci/' -e '^\.github/workflows' -e '\utils' -e '\plugins/ufm_log_analyzer_plugin') #Removing ufm_log_analyzer_plugin as for now it does not need a formal build # Check if changes exist and only in a single plugin directory (including its .ci directory) if [ -n "$changes_excluding_gitmodules_and_root_ci" ] && [ $(echo "$changes_excluding_gitmodules_and_root_ci" | cut -d '/' -f1,2 | uniq | wc -l) -eq 1 ]; then diff --git a/.github/workflows/ufm_log_analyzer_ci_workflow.yml b/.github/workflows/ufm_log_analyzer_ci_workflow.yml new file mode 100644 index 000000000..463d9e0a3 --- /dev/null +++ b/.github/workflows/ufm_log_analyzer_ci_workflow.yml @@ -0,0 +1,24 @@ +name: Ufm log analyzer CI Workflow + +on: + push: + paths: + - 'plugins/ufm_log_analyzer_plugin/**' +jobs: + pylint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@main + + - name: Set up Python + uses: actions/setup-python@main + with: + python-version: 3.9 # Specify the Python version you want to use + + - name: Install dependencies + run: | + pip install -r plugins/ufm_log_analyzer_plugin/src/loganalyze/requirements.txt + pip install pylint + - name: Run PyLint + run: pylint --rcfile=plugins/ufm_log_analyzer_plugin/src/loganalyze/.pylintrc plugins/ufm_log_analyzer_plugin/src/loganalyze diff --git a/plugins/ufm_log_analyzer_plugin/README.md b/plugins/ufm_log_analyzer_plugin/README.md new file mode 100644 index 000000000..f34c12072 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/README.md @@ -0,0 +1,84 @@ +# UFM LOGANALYZER + +**Warning:** This feature is still under development and right now should only be used internally + +## What +This tool should help developers find issues in a UFM sysdump or logs. + +It is meant to help find issues fast ,without the need to manually go over logs. + +## How to use +The tool is meant to run from a PC or a remote connection + +### Prerequisites +Install the needed Python dependencies +``` +python3 -m pip install -r src/loganalyze/requirements.txt +``` +Install software for PDF creation: +``` +# For Ubuntu/Debian +sudo apt-get update +sudo apt-get install -y libjpeg-dev zlib1g-dev + +# For Red Hat/CentOS/Fedora +sudo yum install -y libjpeg-devel zlib-devel +``` +Know your UFM sysdump location. + +#### Running on a remote server +Since the tool generates graphs, you will need to setup an X11 forwarding: + +1. Mac - Install and run [Quartz](https://www.xquartz.org/). Windows - Install and run [Xming](http://www.straightrunning.com) +2. On your remote server (Ubuntu/RedHat), make sure the x11 forwarding is enabled: +``` +vim /etc/ssh/sshd_config +#Enalbe x11 +X11Forwarding yes +``` +3. Restart the ssh service `systemctl restart ssh` or `systemctl restart sshd` depends on the OS. +4. Install `python3-tk` using `sudo yum install python3-tkinter` or `sudo apt-get install python3-tk` depends on the OS. +5. When you SSH to the server, use the flag `-X`, for example `ssh -X root@my-vm` + +If you would like to make sure it is working, once connection is done, do `xclock &`. This should start a clock on your machine. + +### How to run +``` +./log_analzer.sh [options] -l +``` + +While the options are: +``` +options: + -h, --help show this help message and exit + -l LOCATION, --location LOCATION + Location of dump tar file. + -d DESTINATION, --destination DESTINATION + Where should be place the extracted logs and the CSV files. + --extract-level EXTRACT_LEVEL + Depth of logs tar extraction, default is 1 + --hours HOURS How many hours to process from last logs. Default is 6 hours + -i, --interactive Should an interactive Ipython session start. Default is False + -s, --show-output Should the output charts be presented. Default is False + --skip-tar-extract If the location is to an existing extracted tar or just UFM logs directory, skip the tar extraction and only copy the needed logs. Default is False + --interval [{1min,10min,1h,24h}] + Time interval for the graphs. Choices are: '1min'- Every minute, '10min'- Every ten minutes, '1h'- Every one hour, '24h'- Every 24 hours. Default is '1H'. + ``` + +What is mandatory: +1. `--location`. + +## Which logs are taken from the dump +The following list: `event.log, ufmhealth.log, ufm.log, ibdiagnet2.log, console.log` + +Also, each log `tar` is taken, according to the `extract-level` flag. +## How it works +1. Given the list of logs to work with, they are extract from the dump to the destination directory. +2. Each log is being parsed, with his own unique set of regex's. +3. Each parsed line is saved in a CSV file that represents the parsed line from the specific log. +4. Once all logs are parsed, we use Pandas analyzer to load the CSV's and query them. +5. A pre defined set of analysis runs and outputs some plots and data to the terminal. +6. A PDF file is created with the summary of the images and the fabric size. +7. We are starting an interactive Python session, where the user can run pre-defined analysis function on the parsed data, or do personal data query/manipulation to find the needed data + +![Tool flow](img/loganalzer.png) diff --git a/plugins/ufm_log_analyzer_plugin/img/loganalzer.png b/plugins/ufm_log_analyzer_plugin/img/loganalzer.png new file mode 100644 index 0000000000000000000000000000000000000000..b11d2dca33f8fb8028b5732ede7abc4fdcfb138c GIT binary patch literal 54762 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 Nfv2mV%Q~loCII7^wTA!z literal 0 HcmV?d00001 diff --git a/plugins/ufm_log_analyzer_plugin/log_analyzer.sh b/plugins/ufm_log_analyzer_plugin/log_analyzer.sh new file mode 100755 index 000000000..c9fe9e7a7 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/log_analyzer.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# author: Samer Deeb +# date: Mar 02, 2024 +# + +SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" +src_dir=$( realpath "${SCRIPT_DIR}/src" ) +export PYTHONPATH="${src_dir}" + +python3 "${src_dir}/loganalyze/log_analyzer.py" "$@" diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/.pylintrc b/plugins/ufm_log_analyzer_plugin/src/loganalyze/.pylintrc new file mode 100644 index 000000000..2049130fe --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/.pylintrc @@ -0,0 +1,6 @@ +[MESSAGES CONTROL] +disable=missing-function-docstring, + missing-class-docstring, + missing-module-docstring, + too-few-public-methods, + logging-fstring-interpolation diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/__init__.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/__init__.py new file mode 100755 index 000000000..a388afa28 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/__init__.py @@ -0,0 +1,14 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# author: Samer Deeb +# date: Mar 02, 2024 +# diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzer.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzer.py new file mode 100755 index 000000000..f371156e2 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzer.py @@ -0,0 +1,374 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# author: Samer Deeb +# date: Mar 02, 2024 +# +# pylint: disable=broad-exception-caught +# pylint: disable=missing-module-docstring +# pylint: disable=missing-function-docstring + +import argparse +from functools import partial +import glob +from multiprocessing import Process +import os +import re +import sys +import time +from pathlib import Path +import traceback +from typing import Callable, List, Set, Tuple +import subprocess +import platform +import matplotlib.pyplot as plt + + +from loganalyze.log_analyzers.base_analyzer import BaseAnalyzer +from loganalyze.logs_extraction.directory_extractor import DirectoryExtractor +from loganalyze.log_analyzers.ufm_top_analyzer import UFMTopAnalyzer +from loganalyze.logs_extraction.tar_extractor import DumpFilesExtractor +from loganalyze.log_parsing.log_parser import LogParser +from loganalyze.log_parsing.logs_regex import logs_regex_csv_headers_list +from loganalyze.logs_csv.csv_handler import CsvHandler + +from loganalyze.log_analyzers.ufm_log_analyzer import UFMLogAnalyzer +from loganalyze.log_analyzers.ufm_health_analyzer import UFMHealthAnalyzer +from loganalyze.log_analyzers.ibdiagnet_log_analyzer import IBDIAGNETLogAnalyzer +from loganalyze.log_analyzers.events_log_analyzer import EventsLogAnalyzer +from loganalyze.log_analyzers.console_log_analyzer import ConsoleLogAnalyzer +from loganalyze.log_analyzers.rest_api_log_analyzer import RestApiAnalyzer + +from loganalyze.pdf_creator import PDFCreator +from loganalyze.utils.common import delete_files_by_types + +import loganalyze.logger as log + +LOGS_TO_EXTRACT = [ + "event.log", + "ufmhealth.log", + "ufm.log", + "ibdiagnet2.log", + "console.log", + "rest_api.log" +] + +def run_both_functions(parser_func, action_func, save_func): + parser_func(action_func) + save_func() + + +def create_parsers_processes(log_files_and_regex: List[ + Tuple[str, List[str], Callable[[Tuple], None], Callable] + ],): + """ + Per log files, creates the log parser process class that will + handle that log. + """ + processes = [] + for log_file, regex_list_and_fn, action_func, save_func in log_files_and_regex: + parser = LogParser(log_file, regex_list_and_fn) + process = Process( + target=run_both_functions, + args=( + parser.parse, + action_func, + save_func, + ), + ) + processes.append(process) + return processes + + +def run_parsers_processes(processes:List[Process]): + """ + Runs all the parsing process and waits for the to finish + """ + start_time = time.perf_counter() + for process in processes: + process.start() + for process in processes: + process.join() + end_time = time.perf_counter() + total_run_time = end_time - start_time + log.LOGGER.debug(f"It took {total_run_time:.3f} seconds to parse all the logs") + + +def create_logs_regex_csv_handler_list(logs: Set[str]): + """ + Creates a list of tuples where each is a log location, the regex we should + apply on it and a csv handler to use the matched results. + """ + result_list = [] + pattern_of_gz = r"\.\d+\.gz$" + for path in logs: + base_filename = os.path.basename(path) + base_filename = re.sub(pattern_of_gz, "", base_filename) + for filename, patterns_and_fn, csv_headers in logs_regex_csv_headers_list: + if base_filename == filename: + file_as_path = Path(path) + cur_path_suffix = file_as_path.suffix + if cur_path_suffix == ".gz": + csv_path = file_as_path.with_suffix(".csv") + else: + csv_path = file_as_path.with_suffix(cur_path_suffix + ".csv") + csv_handler = CsvHandler(csv_headers, csv_path) + result_list.append((path, patterns_and_fn, + csv_handler.add_line, csv_handler.save_file)) + return result_list + + +def sorting_logs(log_path): + """ + This function is used to sorted the logs making sure + the result order is log and after that the gz files + by order + """ + base_name = os.path.basename(log_path) + count = 0 + for c in base_name: + count += ord(c) + return count + + +def get_csvs_in_dest(location: str, base_name: str, extraction_level: int): + """ + Return a list of all the CSV files that were parsed and part of the current + extraction level requested + """ + csv_files = glob.glob(os.path.join(location, "*.csv")) + matched_files = [file for file in csv_files if base_name in os.path.basename(file)] + full_paths = [os.path.abspath(file) for file in matched_files] + sorted_csvs = sorted(full_paths, key=sorting_logs) + sliced_csvs = sorted_csvs[: (extraction_level + 1)] + return sliced_csvs + + +def parse_args(): + """parses the CLI arguments""" + parser = argparse.ArgumentParser( + description="Analyze ufm logs . If no option is passed, " + "we assume you would like to do all ops" + ) + parser.add_argument( + "-l", + "--location", + help="Location of dump tar file.", + required=True, + action="store", + ) + parser.add_argument( + "-d", + "--destination", + default="/tmp/ufm_log_analyzer", + help="Where should be place the extracted logs and the CSV files.", + ) + parser.add_argument( + "--extract-level", + default=1, + type=int, + help="Depth of logs tar extraction, default is 1", + ) + parser.add_argument( + "--hours", + help="How many hours to process from last logs. Default is 6 hours", + default=6, + type=int, + ) + parser.add_argument( + "-i", + "--interactive", + action="store_true", + help="Should an interactive Ipython session start. Default is False", + ) + parser.add_argument( + "-s", + "--show-output", + action="store_true", + help="Should the output charts be presented. Default is False", + ) + parser.add_argument( + "--skip-tar-extract", + action="store_true", + help="If the location is to an existing extracted tar, skip the " \ + "tar extraction and only copy the needed logs. Default is False" + ) + parser.add_argument( + '--interval', + type=str, + nargs='?', + default='1h', + choices=['1min', '10min', '1h', '24h'], + help="Time interval for the graphs. Choices are: '1min'- Every minute, " + "'10min'- Every ten minutes, '1h'- Every one hour, " + "'24h'- Every 24 hours. Default is '1H'." + ) + parser.add_argument( + '--log-level', + help="Tool log level, default is CRITICAL", + default='INFO', + choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] + ) + + return parser.parse_args() + + +def add_extraction_levels_to_files_set( + files_to_extract: List[str], extraction_level: int +): + """ + For a given log name e.g. ufm.log, we need to also + look for the gz logs, this function returns a list of + all the logs including the gz, according to the extraction + level. + """ + combined_logs_named = list(files_to_extract) + for file in files_to_extract: + for i in range(1, extraction_level + 1): + new_file_name = file + f".{i}.gz" + combined_logs_named.append(new_file_name) + return combined_logs_named + + +def create_analyzer(parsed_args, full_extracted_logs_list, + ufm_top_analyzer_obj, log_name, analyzer_clc): + """ + Create the analyzer based on the given inputs. + Also adds it to the top_analyzer so it can be used + in the full report. + Returns the created analyzer + """ + if log_name in full_extracted_logs_list: + log_csvs = get_csvs_in_dest(parsed_args.destination, log_name, parsed_args.extract_level) + analyzer = analyzer_clc(log_csvs, parsed_args.hours, parsed_args.destination) + ufm_top_analyzer_obj.add_analyzer(analyzer) + return analyzer + return None + + +if __name__ == "__main__": + args = parse_args() + log.setup_logger("Logs_analyzer", args.log_level) + log.LOGGER.info("Starting analysis, this might take a few minutes" + " depending on the amount of data in the logs") + if not os.path.exists(args.location): + log.LOGGER.critical(f"-E- Cannot find dump file at {args.location}") + sys.exit(1) + ufm_top_analyzer = UFMTopAnalyzer() + try: + # Extracting the files from the tar + full_logs_list = add_extraction_levels_to_files_set( + LOGS_TO_EXTRACT, args.extract_level + ) + log.LOGGER.debug(f"Going to extract {len(full_logs_list)} logs from {args.location}") + if args.skip_tar_extract: + extractor = DirectoryExtractor(args.location) + else: + extractor = DumpFilesExtractor(args.location) + + logs_to_work_with, failed_extract = extractor.extract_files( + full_logs_list, args.destination + ) + + if len(failed_extract) > 0: + log.LOGGER.warning(f"Failed to get some logs - {failed_extract}, skipping them") + logs_regex_csv_handler_list = create_logs_regex_csv_handler_list( + logs_to_work_with + ) + # Parsing the logs into CSV files + parsers_processes = create_parsers_processes(logs_regex_csv_handler_list) + run_parsers_processes(parsers_processes) + log.LOGGER.debug("Done saving all CSV files") + + + # Setting the time granularity for the graphs + BaseAnalyzer.time_interval = args.interval + + # Analyze the CSV and be able to query the data + start = time.perf_counter() + log.LOGGER.debug("Starting analyzing the data") + partial_create_analyzer = partial(create_analyzer, + parsed_args=args, + full_extracted_logs_list=full_logs_list, + ufm_top_analyzer_obj=ufm_top_analyzer) + + # Creating the analyzer for each log + # By assigning them, a user can query the data via + # the interactive session + ibdiagnet_analyzer = partial_create_analyzer(log_name="ibdiagnet2.log", + analyzer_clc=IBDIAGNETLogAnalyzer) + + event_log_analyzer = partial_create_analyzer(log_name="event.log", + analyzer_clc=EventsLogAnalyzer) + + ufm_health_analyzer = partial_create_analyzer(log_name="ufmhealth.log", + analyzer_clc=UFMHealthAnalyzer) + + ufm_log_analyzer = partial_create_analyzer(log_name="ufm.log", + analyzer_clc=UFMLogAnalyzer) + + console_log_analyzer = partial_create_analyzer(log_name="console.log", + analyzer_clc=ConsoleLogAnalyzer) + + rest_api_log_analyzer = partial_create_analyzer(log_name="rest_api.log", + analyzer_clc=RestApiAnalyzer) + end = time.perf_counter() + log.LOGGER.debug(f"Took {end-start:.3f} to load the parsed data") + + all_images_outputs_and_title = ufm_top_analyzer.full_analysis() + png_images =[] + images_and_title_to_present = [] + for image_title in all_images_outputs_and_title: + image, title = image_title + if image.endswith(".png"): + png_images.append(image) + else: + images_and_title_to_present.append((image, title)) + # Next section is to create a summary PDF + pdf_path = os.path.join(args.destination, "UFM_Dump_analysis.pdf") + pdf_header = ( + f"Dump analysis for {os.path.basename(args.location)}, hours={args.hours}" + ) + fabric_info = ibdiagnet_analyzer.get_fabric_size() \ + if ibdiagnet_analyzer else "No Fabric Info found" + # PDF creator gets all the images and to add to the report + pdf = PDFCreator(pdf_path, pdf_header, png_images, fabric_info) + pdf.created_pdf() + # Generated a report that can be located in the destination + log.LOGGER.info("Analysis is done, please see the following outputs:") + for image, title in images_and_title_to_present: + log.LOGGER.info(f"{title}: {image}") + log.LOGGER.info(f"Summary PDF was created! you can open here at {pdf_path}") + # Clean some unended files created during run + files_types_to_delete = set() + files_types_to_delete.add("png") #png images created for PDF report + files_types_to_delete.add("log") #logs taken from the logs + files_types_to_delete.add("gz") # Zipped logs taken from the logs + delete_files_by_types(args.destination, files_types_to_delete) + if args.show_output: + if platform.system() == "Windows": + os.startfile(pdf_path) # pylint: disable=no-member + elif platform.system() == "Darwin": # macOS + subprocess.call(("open", pdf_path)) # pylint: disable=no-member + else: # Linux + subprocess.call(("xdg-open", pdf_path)) # pylint: disable=no-member + + # This will show all the graphs we created + plt.show() + if args.interactive: + import IPython + + IPython.embed() + + except Exception as exc: + print("-E-", str(exc)) + traceback.print_exc() + sys.exit(1) diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/__init__.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/__init__.py new file mode 100644 index 000000000..26680606e --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/__init__.py @@ -0,0 +1,11 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/base_analyzer.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/base_analyzer.py new file mode 100644 index 000000000..2ae865eee --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/base_analyzer.py @@ -0,0 +1,199 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# pylint: disable=missing-function-docstring +# pylint: disable=missing-module-docstring + +import os +import csv +import shutil +import warnings +from datetime import timedelta +from typing import List +import pandas as pd +import matplotlib.pyplot as plt +import matplotlib.dates as mdates + +from loganalyze.log_analyzers.constants import DataConstants +import loganalyze.logger as log + +pd.set_option("display.max_colwidth", None) +warnings.filterwarnings("ignore") + + +class BaseAnalyzer: + """ + Analyzer class that gives all the logs the + ability to print/save images and filter data + """ + + # Setting the graph time interval to 1 hour + # This is out side of the constructor since + # It is defined once for all graphic options + # Per call/run. + time_interval = "h" + + + def __init__( + self, + logs_csvs: List[str], + hours: int, + dest_image_path: str, + sort_timestamp=True + + ): + self._dest_image_path = dest_image_path + dataframes = [pd.read_csv(ufm_log) for ufm_log in logs_csvs] + df = pd.concat(dataframes, ignore_index=True) + if sort_timestamp: + df[DataConstants.TIMESTAMP] = pd.to_datetime(df[DataConstants.TIMESTAMP]) + max_timestamp = df[DataConstants.TIMESTAMP].max() + start_time = max_timestamp - timedelta(hours=hours) + # Filter logs to include only those within the last 'hours' from the max timestamp + filtered_logs = df[df[DataConstants.TIMESTAMP] >= start_time] + data_sorted = filtered_logs.sort_values(by=DataConstants.TIMESTAMP) + data_sorted[DataConstants.AGGREGATIONTIME] = \ + data_sorted[DataConstants.TIMESTAMP].dt.floor(self.time_interval) + self._log_data_sorted = data_sorted + else: + self._log_data_sorted = df + self._images_type = {"svg", "png"} + + @staticmethod + def _remove_empty_lines_from_csv(input_file): + temp_file = input_file + ".temp" + + with open(input_file, "r", newline="", encoding=DataConstants.UTF8ENCODING) as infile, open( + temp_file, "w", newline="", encoding=DataConstants.UTF8ENCODING + ) as outfile: + reader = csv.reader(infile) + writer = csv.writer(outfile) + + for row in reader: + if any( + field.strip() for field in row + ): # Check if any field in the row is non-empty + writer.writerow(row) + + # Replace the original file with the modified file + shutil.move(temp_file, input_file) + + @staticmethod + def fix_lines_with_no_timestamp(csvs): + """ + If a line has no timestamp, we take the last arg data from it and append + to prev line + """ + for csv_file in csvs: + # Create a temporary file to write the modified content + temp_file = csv_file + ".temp" + BaseAnalyzer._remove_empty_lines_from_csv(csv_file) + fixed_lines = 0 + with open(csv_file, "r", newline="", encoding=DataConstants.UTF8ENCODING) \ + as infile, open( + temp_file, "w", newline="", encoding=DataConstants.UTF8ENCODING + ) as outfile: + reader = csv.reader(infile) + writer = csv.writer(outfile) + current_line = None + is_first_section = True + for row in reader: + if row[0] != "": # Lines with date + if current_line is not None: + writer.writerow(current_line) + is_first_section = False + current_line = row[:] # Copy current row to current_line + elif is_first_section: # Still starting, skip them + continue + elif row[-1].strip(): # Lines with only data + if current_line is not None: + current_line[-1] += row[ + -1 + ] # Append data to the existing data + fixed_lines += 1 + else: + raise ValueError("Unexpected line format before") + + # Write the last processed line + if current_line is not None: + writer.writerow(current_line) + + # Replace the original file with the modified file + os.replace(temp_file, csv_file) + + def _plot_and_save_data_based_on_timestamp( + self, data_to_plot, x_label, y_label, title + ): + with plt.ion(): + log.LOGGER.debug(f"saving {title}") + plt.figure(figsize=(12, 6)) + plt.plot(data_to_plot, marker="o", linestyle="-", color="b") + plt.title(title) + plt.xlabel(x_label) + plt.ylabel(y_label) + plt.grid(True) + + # Set the locator to show ticks every hour and the formatter to + # include both date and time + ax = plt.gca() + ax.xaxis.set_major_locator(mdates.HourLocator()) + ax.xaxis.set_minor_locator( + mdates.MinuteLocator(interval=15) + ) # Add minor ticks every 15 minutes + ax.xaxis.set_major_formatter( + mdates.DateFormatter("%Y-%m-%d %H:%M") + ) # Format major ticks to show date and time + + plt.xticks(rotation=45) # Rotate x-axis labels to make them readable + plt.tight_layout() + + generic_file_name = f"{title}".replace(" ", "_").replace("/", "_") + images_created = [] + for img_type in self._images_type: + cur_img = os.path.join(self._dest_image_path,f"{generic_file_name}.{img_type}") + log.LOGGER.debug(f"Saving {cur_img}") + plt.savefig(cur_img, format=img_type) + images_created.append(cur_img) + images_list_with_title = [(image, title) for image in images_created] + plt.close() + return images_list_with_title + + def _plot_and_save_pivot_data_in_bars( # pylint: disable=# pylint: disable=too-many-arguments + self, pivoted_data, x_label, y_label, title, legend_title + ): + if pivoted_data.empty: + return [] + pivoted_data.plot(kind="bar", figsize=(14, 7)) + # This allows for the image to keep open when we create another one + with plt.ion(): + log.LOGGER.debug(f"saving {title}") + plt.title(title) + plt.xlabel(x_label) + plt.ylabel(y_label) + plt.legend(title=legend_title, bbox_to_anchor=(1.05, 1), loc="upper left") + plt.xticks(rotation=45) + plt.tight_layout() + generic_file_name = f"{title}".replace(" ", "_").replace("/", "_") + images_created = [] + for img_type in self._images_type: + cur_img = os.path.join(self._dest_image_path,f"{generic_file_name}.{img_type}") + log.LOGGER.debug(f"Saving {cur_img}") + plt.savefig(cur_img, format=img_type) + images_created.append(cur_img) + images_list_with_title = [(image, title) for image in images_created] + plt.close() + return images_list_with_title + + def full_analysis(self): + """ + Returns a list of all the graphs created and their title + """ + log.LOGGER.debug("Missing implementation for full_analysis") diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/console_log_analyzer.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/console_log_analyzer.py new file mode 100644 index 000000000..947973212 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/console_log_analyzer.py @@ -0,0 +1,70 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# pylint: disable=# pylint: disable=missing-function-docstring +# pylint: disable=# pylint: disable=missing-class-docstring +# pylint: disable=# pylint: disable=missing-module-docstring +from typing import List +from loganalyze.log_analyzers.constants import DataConstants +from loganalyze.log_analyzers.base_analyzer import BaseAnalyzer +import loganalyze.logger as log + + +class ConsoleLogAnalyzer(BaseAnalyzer): + def __init__( + self, logs_csvs: List[str], hours: int, dest_image_path, sort_timestamp=True + ): + self.fix_lines_with_no_timestamp(logs_csvs) + super().__init__(logs_csvs, hours, dest_image_path, sort_timestamp) + self._log_data_sorted.dropna(subset=["data"], inplace=True) + + def _get_exceptions(self): + error_data = self._log_data_sorted[self._log_data_sorted["type"] == "Error"][ + [DataConstants.TIMESTAMP, "data"] + ] + return error_data + + def get_all_exceptions_to_print(self): + error_data = self._get_exceptions() + if error_data.empty: + return "No exceptions" + result_string = " ".join( + error_data.apply(lambda row: f"{row[0]} {row[1]}\n", axis=1) + ) + return result_string + + def print_exceptions(self): + error_data = self._get_exceptions() + if error_data.empty: + log.LOGGER.info("No Errors to print") + return + log.LOGGER.debug("The following exceptions were found in console log") + for _, row in error_data.iterrows(): + log.LOGGER.debug(f"Timestamp: {row['timestamp']}: {row['data']}") + + def print_exceptions_per_time_count(self): + error_data = self._log_data_sorted[self._log_data_sorted["type"] == "Error"] + errors_per_hour = error_data.groupby(DataConstants.AGGREGATIONTIME).size() + images_created = self._plot_and_save_data_based_on_timestamp( + errors_per_hour, + "Time", + "Amount of exceptions", + "Exceptions count", + ) + return images_created + + def full_analysis(self): + """ + Returns a list of all the graphs created and their title + """ + created_images = self.print_exceptions_per_time_count() + self.print_exceptions() + return created_images if len(created_images) > 0 else [] diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/constants.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/constants.py new file mode 100644 index 000000000..0273a66f7 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/constants.py @@ -0,0 +1,17 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# + +class DataConstants: + AGGREGATIONTIME = "aggregated_by_time" + TIMESTAMP = "timestamp" + UTF8ENCODING = "utf-8" + DATA = "data" diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/events_log_analyzer.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/events_log_analyzer.py new file mode 100644 index 000000000..c9ed17348 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/events_log_analyzer.py @@ -0,0 +1,119 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# pylint: disable=# pylint: disable=missing-function-docstring +# pylint: disable=# pylint: disable=missing-class-docstring +# pylint: disable=# pylint: disable=missing-module-docstring + +from typing import List +import pandas as pd +from loganalyze.log_analyzers.constants import DataConstants +from loganalyze.log_analyzers.base_analyzer import BaseAnalyzer +import loganalyze.logger as log + + +class EventsLogAnalyzer(BaseAnalyzer): + def __init__(self, logs_csvs: List[str], hours: int, dest_image_path): + super().__init__(logs_csvs, hours, dest_image_path) + self._supported_log_levels = ["CRITICAL", "WARNING", "INFO", "MINOR"] + + # Function to split "object_id" into "device" and "description" + def _split_switch_object_id(self, row): + if "Switch:" in row["object_id"]: + parts = row["object_id"].split("/") + return pd.Series([parts[0].strip(), parts[1].strip()]) + + return pd.Series([None, None]) + + def analyze_events(self): + grouped = self._log_data_sorted.groupby(["object_id", "event"]) + event_counts_df = grouped.size().reset_index(name="count") + event_counts_df = event_counts_df.sort_values( + ["event", "count"], ascending=False + ) + self._log_data_sorted[["device", "description"]] = self._log_data_sorted.apply( + self._split_switch_object_id, axis=1 + ) + event_counts = ( + self._log_data_sorted.groupby(["event", "device", "description"]) + .size() + .reset_index(name="count") + ) + log.LOGGER.debug(event_counts.head()) + + def get_events_by_log_level(self, log_level="CRITICAL"): + if log_level not in self._supported_log_levels: + log.LOGGER.error( + f"Requested log level {log_level} is not valid, " + f"options are {self._supported_log_levels}" + ) + return None + + return self._log_data_sorted[self._log_data_sorted["severity"] == log_level] + + def get_events_by_log_level_and_event_types_as_count(self, log_level="CRITICAL"): + if log_level not in self._supported_log_levels: + log.LOGGER.error( + f"Requested log level {log_level} is not valid, " + f"options are {self._supported_log_levels}" + ) + return None + events_by_log_level = self.get_events_by_log_level(log_level) + return events_by_log_level["event_type"].value_counts() + + def print_critical_events_per_hour(self): + critical_events = self.get_events_by_log_level("CRITICAL") + critical_events_grouped_by_hour = ( + critical_events.groupby(["hour", "event"]).size().reset_index(name="count") + ) + + pivot_critical_events_by_hour = critical_events_grouped_by_hour.pivot( + index="hour", columns="event", values="count" + ).fillna(0) + + self._plot_and_save_pivot_data_in_bars( + pivot_critical_events_by_hour, + "Hour", + "Events", + "Hourly critical events", + "Events", + ) + return critical_events_grouped_by_hour + + def print_link_up_down_count_per_hour(self): + links_events = self._log_data_sorted[ + (self._log_data_sorted["event"] == "Link is up") + | + (self._log_data_sorted["event"] == "Link went down") + ] + grouped_links_events = links_events.groupby([DataConstants.AGGREGATIONTIME, "event"]) + counted_links_events_by_time = grouped_links_events.size().reset_index( + name="count" + ) + + pivot_links_data = counted_links_events_by_time.pivot( + index=DataConstants.AGGREGATIONTIME, columns="event", values="count" + ).fillna(0) + graph_images = self._plot_and_save_pivot_data_in_bars( + pivot_links_data, + "Time", + "Number of Events", + "Link up/down events", + "Event" + ) + return graph_images + + def full_analysis(self): + """ + Returns a list of all the graphs created and their title + """ + created_images = self.print_link_up_down_count_per_hour() + return created_images if len(created_images) > 0 else [] diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/ibdiagnet_log_analyzer.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/ibdiagnet_log_analyzer.py new file mode 100644 index 000000000..b867fe569 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/ibdiagnet_log_analyzer.py @@ -0,0 +1,36 @@ +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# pylint: disable=missing-function-docstring +# pylint: disable=missing-module-docstring +# pylint: disable=missing-class-docstring + +from typing import List +from loganalyze.log_analyzers.base_analyzer import BaseAnalyzer +import loganalyze.logger as log + + +class IBDIAGNETLogAnalyzer(BaseAnalyzer): + def __init__(self, logs_csvs: List[str], hours: int, dest_image_path): + super().__init__(logs_csvs, hours, dest_image_path, sort_timestamp=False) + + def print_fabric_size(self): + fabric_info = self.get_fabric_size() + log.LOGGER.info(fabric_info) + + def get_fabric_size(self): + return self._log_data_sorted + + def full_analysis(self): + """ + Returns a list of all the graphs created and their title + """ + self.print_fabric_size() + return [] diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/rest_api_log_analyzer.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/rest_api_log_analyzer.py new file mode 100644 index 000000000..6895c89a7 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/rest_api_log_analyzer.py @@ -0,0 +1,70 @@ +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +from typing import List +from urllib.parse import urlparse +from loganalyze.log_analyzers.base_analyzer import BaseAnalyzer +import pandas as pd +import numpy as np + +class RestApiAnalyzer(BaseAnalyzer): + + def __init__(self, logs_csvs: List[str], hours: int, dest_image_path: str, sort_timestamp=True): + super().__init__(logs_csvs, hours, dest_image_path, sort_timestamp) + #Removing all the request coming from the ufm itself + self._log_data_sorted = self._log_data_sorted.loc\ + [self._log_data_sorted['user'] != 'ufmsystem'] + #Splitting the URL for better analysis + self._log_data_sorted[['uri', 'query_params']] = self._log_data_sorted['url']\ + .apply(self.split_url_to_uri_and_query_params).apply(pd.Series) + self._have_duration = self._have_data_in_column('duration') + self._have_user = self._have_data_in_column('user') + + @staticmethod + def split_url_to_uri_and_query_params(url): + """ + Parse the URL into its components + """ + if url: + parsed_url = urlparse(url) + base_uri = parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + query_params = parsed_url.query if parsed_url.query else np.nan + return base_uri, query_params + return np.nan, np.nan + + def _have_data_in_column(self, column): + """ + Returns True/False if the column has any data. + This needed since old versions of UFM do not have this data + and will result in less analysis. + """ + return self._log_data_sorted[column].notna().all() + + def analyze_endpoints_freq(self, endpoints_count_to_show=10): + by_uri_per_time = self._log_data_sorted.groupby(['uri', + 'aggregated_by_time']).size().reset_index( + name='amount_per_uri') + total_amount_per_uri = by_uri_per_time.groupby('uri')['amount_per_uri'].sum() + top_x_uris = total_amount_per_uri.nlargest(endpoints_count_to_show).index + data_to_show = by_uri_per_time.pivot(index='aggregated_by_time', + columns='uri', + values='amount_per_uri').fillna(0) + data_to_show = data_to_show[top_x_uris] + + return self._plot_and_save_pivot_data_in_bars(data_to_show, + "time", + "requests count", + f"Top {endpoints_count_to_show} "\ + "requests count over time", + "legend") + + def full_analysis(self): + created_images = self.analyze_endpoints_freq() + return created_images if len(created_images) > 0 else [] diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/ufm_health_analyzer.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/ufm_health_analyzer.py new file mode 100644 index 000000000..177d94bc1 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/ufm_health_analyzer.py @@ -0,0 +1,75 @@ +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# pylint: disable=missing-function-docstring +# pylint: disable=missing-class-docstring +# pylint: disable=missing-module-docstring +from typing import List +from loganalyze.log_analyzers.constants import DataConstants +from loganalyze.log_analyzers.base_analyzer import BaseAnalyzer +import loganalyze.logger as log + + +class UFMHealthAnalyzer(BaseAnalyzer): + def __init__(self, logs_csvs: List[str], hours: int, dest_image_path): + self.fix_lines_with_no_timestamp(logs_csvs) + super().__init__(logs_csvs, hours, dest_image_path) + self._failed_tests_data = self._log_data_sorted[ + self._log_data_sorted["test_status"] != "succeeded" + ] + + def top_failed_tests_during_dates(self): + data = ( + self._failed_tests_data.groupby(["test_class", "test_name", "reason"]) + .size() + .reset_index(name="count") + .sort_values(by="count", ascending=False) + .head() + ) + log.LOGGER.debug(data) + + def full_analyze_failed_tests(self): + data = ( + self._log_data_sorted.groupby(["test_name"]) + .size() + .reset_index(name="count") + .sort_values(by="count", ascending=False) + ) + + log.LOGGER.debug(data) + + def print_failed_tests_per_hour(self): + grouped_failed_by_time = ( + self._failed_tests_data.groupby([DataConstants.AGGREGATIONTIME, "test_name"]) + .size() + .reset_index(name="count") + ) + # Filter out the AlwaysFailTest test + grouped_failed_only_relevant_by_time = grouped_failed_by_time[ + grouped_failed_by_time["test_name"] != "AlwaysFailTest" + ] + pivot_failed_by_time = grouped_failed_only_relevant_by_time.pivot( + index=DataConstants.AGGREGATIONTIME, columns="test_name", values="count" + ).fillna(0) + graph_images = self._plot_and_save_pivot_data_in_bars( + pivot_failed_by_time, + "Time", + "Number of failures", + "UFM Health failed tests", + "test_name", + ) + return graph_images + + def full_analysis(self): + """ + Returns a list of all the graphs created and their title + """ + created_images = self.print_failed_tests_per_hour() + return created_images if len(created_images) > 0 else [] diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/ufm_log_analyzer.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/ufm_log_analyzer.py new file mode 100644 index 000000000..97b81c430 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/ufm_log_analyzer.py @@ -0,0 +1,293 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# pylint: disable=too-many-locals +# pylint: disable=missing-function-docstring +# pylint: disable=useless-parent-delegation +# pylint: disable=missing-class-docstring +# pylint: disable=missing-module-docstring + +from typing import List +import pandas as pd +from loganalyze.log_analyzers.constants import DataConstants +from loganalyze.log_analyzers.base_analyzer import BaseAnalyzer +import loganalyze.logger as log + + +class UFMLogAnalyzer(BaseAnalyzer): + def __init__(self, logs_csvs: List[str], hours: int, dest_image_path): + super().__init__(logs_csvs, hours, dest_image_path) + + def full_analyze_ufm_loading_time(self): + ufm_started_logs = self._log_data_sorted[ + self._log_data_sorted["log_type"] == "ufm_started" + ] + ufm_init_done_logs = self._log_data_sorted[ + self._log_data_sorted["log_type"] == "ufm_init_done" + ] + + max_time_window = pd.Timedelta( + minutes=30 + ) # Set maximum time window for matching + + missing_completed_logs = [] + matched_logs = [] + + for _, start_log in ufm_started_logs.iterrows(): + start_time = start_log[DataConstants.TIMESTAMP] + possible_matches = ufm_init_done_logs[ + ufm_init_done_logs[DataConstants.TIMESTAMP] > start_time + ] + if not possible_matches.empty: + end_time = possible_matches[ + DataConstants.TIMESTAMP + ].min() # Find the earliest 'ufm_init_done' log after 'start_time' + time_diff = end_time - start_time + if time_diff <= max_time_window: + matched_logs.append( + {"timestamp_start": start_time, "timestamp_end": end_time} + ) + ufm_init_done_logs = ufm_init_done_logs[ + ufm_init_done_logs[DataConstants.TIMESTAMP] > end_time + ] # Remove matched log + else: + missing_completed_logs.append(start_time) + else: + missing_completed_logs.append(start_time) + + matched_logs_df = pd.DataFrame(matched_logs) + + if not matched_logs_df.empty: + matched_logs_df["time_diff"] = ( + matched_logs_df["timestamp_end"] - matched_logs_df["timestamp_start"] + ).dt.total_seconds() + + # Calculate statistics + mean_time = matched_logs_df["time_diff"].mean() + max_time = matched_logs_df["time_diff"].max() + min_time = matched_logs_df["time_diff"].min() + + max_time_occurrence = matched_logs_df.loc[ + matched_logs_df["time_diff"] == max_time, "timestamp_end" + ].iloc[0] + min_time_occurrence = matched_logs_df.loc[ + matched_logs_df["time_diff"] == min_time, "timestamp_end" + ].iloc[0] + + # Print statistics + log.LOGGER.debug(f"Mean Time: {mean_time:.2f} seconds") + log.LOGGER.debug(f"Maximum Time: {max_time:.2f} seconds at {max_time_occurrence}") + log.LOGGER.debug(f"Minimum Time: {min_time:.2f} seconds at {min_time_occurrence}") + + # Print timestamps with missing 'Completed' logs + if len(missing_completed_logs) > 0: + log.LOGGER.debug("\nTimestamps with missing 'Completed' logs:") + for timestamp_start in missing_completed_logs: + log.LOGGER.debug(f"timestamp_start: {timestamp_start}") + # Plot the results with scatter plot + if not matched_logs_df.empty: + matched_logs_df.drop(columns=["timestamp_end"], inplace=True) + matched_logs_df.set_index("timestamp_start", inplace=True) + create_images = self._plot_and_save_data_based_on_timestamp( + matched_logs_df, + DataConstants.TIMESTAMP, + "Loading time Time (seconds)", + "UFM loading time", + ) + return create_images + return [] + + def full_analyze_fabric_analysis_time(self): + # Filter by 'fabric_analysis' log_type and the timestamp range + fabric_logs = self._log_data_sorted[ + (self._log_data_sorted["log_type"] == "fabric_analysis") + ] + fabric_logs.sort_values(by=DataConstants.TIMESTAMP, inplace=True) + fabric_logs.reset_index(drop=True, inplace=True) # Reset index after sorting + merged_logs = pd.DataFrame( + columns=[ + "timestamp_start", + "timestamp_end", + "report_id", + "original_start_time", + ] + ) + reports_with_no_ending = [] + + i = 0 + fabric_logs_size = len(fabric_logs) + to_concat = [] + while i < fabric_logs_size: + row = fabric_logs.iloc[i] + + if row[DataConstants.DATA] == "Starting": + report_id = str( + int(row["extra_info"]) + ) # This is to fix an issue where some of the reports are with .0 + start_time = row[DataConstants.TIMESTAMP] + + # Find the next log entry for the current start log + j = i + 1 + while j < fabric_logs_size: + next_row = fabric_logs.iloc[j] + next_row_report_id = str(int(next_row["extra_info"])) + if ( + next_row[DataConstants.DATA] == "Starting" + and next_row_report_id <= report_id + ): + # If the next row is another start with the same report_id, + # treat the current start as having no ending + reports_with_no_ending.append((report_id, start_time)) + elif ( + next_row[DataConstants.DATA] == "Completed" + and next_row_report_id == report_id + and next_row[DataConstants.TIMESTAMP] > start_time + ): + end_time = next_row[DataConstants.TIMESTAMP] + new_row = { + "timestamp_start": start_time, + "timestamp_end": end_time, + "report_id": report_id, + "original_start_time": start_time, # Save the original start time + } + to_concat.append(new_row) + break # Exit the loop for current start log + + j += 1 + + # If no completion found, add to reports_with_no_ending + if j == fabric_logs_size: + reports_with_no_ending.append((report_id, start_time)) + + i += 1 + merged_logs = pd.concat( + [merged_logs] + [pd.DataFrame(to_concat)], ignore_index=True + ) + + # Ensure merged_logs is not empty before further operations + if not merged_logs.empty: + # Calculate the time difference in seconds for valid merged logs + merged_logs["time_diff"] = ( + merged_logs["timestamp_end"] - merged_logs["timestamp_start"] + ).dt.total_seconds() + + # Statistics + mean_time = merged_logs["time_diff"].mean() + max_time = merged_logs["time_diff"].max() + min_time = merged_logs["time_diff"].min() + + # Use the original_start_time for finding occurrences + max_time_occurrence = merged_logs.loc[ + merged_logs["time_diff"] == max_time, "original_start_time" + ].iloc[0] + min_time_occurrence = merged_logs.loc[ + merged_logs["time_diff"] == min_time, "original_start_time" + ].iloc[0] + + log.LOGGER.debug(f"Mean Time: {mean_time:.3f} seconds") + log.LOGGER.debug( + f"Maximum Time: {max_time:.3f} seconds starting at {max_time_occurrence}" + ) + log.LOGGER.debug( + f"Minimum Time: {min_time:.3f} seconds starting at {min_time_occurrence}" + ) + + # Reports with no ending + if reports_with_no_ending: + amount = len(reports_with_no_ending) + log.LOGGER.debug( + f"There are {amount} fabric analysis reports that started but did not end" + ) + # Plotting + merged_logs.drop( + columns=["timestamp_end", "report_id", "original_start_time"], + inplace=True, + ) + merged_logs.set_index("timestamp_start", inplace=True) + title = "Fabric analysis run time" + create_images = self._plot_and_save_data_based_on_timestamp( + merged_logs, "Time", "Processing Time (s)", title + ) + return create_images + return [] + + def full_telemetry_processing_time_report(self): + # Further filter to only telemetry processing time logs + telemetry_logs = self._log_data_sorted[ + self._log_data_sorted["log_type"] == "telemetry_processing_time" + ] + + # Sort the telemetry logs by timestamp + telemetry_logs_sorted = telemetry_logs.sort_values(by=DataConstants.TIMESTAMP).copy() + + # Extract the processing time and convert to float + telemetry_logs_sorted["processing_time"] = ( + telemetry_logs_sorted[DataConstants.DATA].str.extract(r"(\d+(\.\d+)?)")[0].astype(float) + ) + + # Calculate statistics + mean_time = telemetry_logs_sorted["processing_time"].mean() + max_time = telemetry_logs_sorted["processing_time"].max() + min_time = telemetry_logs_sorted["processing_time"].min() + + # Find the occurrences of the max and min processing times + max_time_row = telemetry_logs_sorted[ + telemetry_logs_sorted["processing_time"] == max_time + ] + min_time_row = telemetry_logs_sorted[ + telemetry_logs_sorted["processing_time"] == min_time + ] + + max_time_occurrence = max_time_row[DataConstants.TIMESTAMP].iloc[0] + min_time_occurrence = min_time_row[DataConstants.TIMESTAMP].iloc[0] + + # Print the statistics + log.LOGGER.debug(f"Mean Processing Time: {mean_time}") + log.LOGGER.debug(f"Maximum Processing Time: {max_time} at {max_time_occurrence}") + log.LOGGER.debug(f"Minimum Processing Time: {min_time} at {min_time_occurrence}") + + # Set the index to timestamp for resampling, converting to DateTimeIndex + telemetry_logs_sorted[DataConstants.TIMESTAMP] = pd.to_datetime( + telemetry_logs_sorted[DataConstants.TIMESTAMP] + ) + telemetry_logs_sorted.set_index(DataConstants.TIMESTAMP, inplace=True) + + # Resample to minute-wise mean processing time + minutely_mean_processing_time = ( + telemetry_logs_sorted["processing_time"].resample("min").mean() + ) + + # Drop any NaN values that may have resulted from resampling + minutely_mean_processing_time.dropna(inplace=True) + + # Plot the data within the filtered time range + title = "Telemetry processing time" + create_images = self._plot_and_save_data_based_on_timestamp( + minutely_mean_processing_time, + "Time", + "Processing Time (s)", + title + ) + return create_images + + def full_analysis(self): + """ + Returns a list of all the graphs created and their title + """ + graphs = [] + functions_to_run = {self.full_analyze_ufm_loading_time, + self.full_telemetry_processing_time_report, + self.full_analyze_fabric_analysis_time} + for function_to_run in functions_to_run: + results_list = function_to_run() + if len(results_list) > 0: + graphs.extend(results_list) + return graphs diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/ufm_top_analyzer.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/ufm_top_analyzer.py new file mode 100644 index 000000000..066b26dc4 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_analyzers/ufm_top_analyzer.py @@ -0,0 +1,32 @@ +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# pylint: disable=missing-function-docstring +# pylint: disable=missing-class-docstring +# pylint: disable=missing-module-docstring + + +class UFMTopAnalyzer: + def __init__(self): + self._analyzers = [] + + def add_analyzer(self, analyzer): + self._analyzers.append(analyzer) + + def full_analysis(self): + """ + Returns a list of all the graphs created and their title + """ + graphs_and_titles = [] + for analyzer in self._analyzers: + tmp_images_list = analyzer.full_analysis() + if len(tmp_images_list) > 0: + graphs_and_titles.extend(tmp_images_list) + return graphs_and_titles diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/__init__.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/__init__.py new file mode 100644 index 000000000..26680606e --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/__init__.py @@ -0,0 +1,11 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/base_regex.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/base_regex.py new file mode 100644 index 000000000..218d83f57 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/base_regex.py @@ -0,0 +1,31 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# pylint: disable=missing-module-docstring +# pylint: disable=missing-class-docstring +# pylint: disable=missing-function-docstring + + +class RegexAndHandlers: + def __init__(self, log_name, csv_headers): + self._regex_and_handler_list = [] + self._log_name = log_name + self._csv_headers = csv_headers + + def add_regex(self, regex, handler): + self._regex_and_handler_list.append((regex, handler)) + + def get_all_info(self): + """ + Return a tuple where first is the log name, second is the list of regex and handlers, + third is the CSV headers + """ + return (self._log_name, self._regex_and_handler_list, self._csv_headers) diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/console_log_regex.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/console_log_regex.py new file mode 100644 index 000000000..1489f7f81 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/console_log_regex.py @@ -0,0 +1,73 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# pylint: disable=missing-function-docstring +# pylint: disable=missing-module-docstring +import re +from typing import Match + +from loganalyze.log_parsing.base_regex import RegexAndHandlers + +### +# This log is only to find general exceptions and errors, +# as each specific log is being analyzed on his own. +### + +EXPCT_REGEX = r"expc" +TRACBEBACK_REGEX = r"traceback" + +CONSOLE_LOG_EXCEPTION_REGEX = re.compile( + # Date and time NA log level The rest of the line + r"^([\d\-]+ [\d\:\.]+) \w+ +(\w+) +(.*(?:traceback|exception).*)", + re.IGNORECASE, +) + +ERROR_LINE_REGEX = re.compile( + # Does not start with date The rest + r"^(?![\d\-]+ [\d\:\.]+ \w+)(.*)$", +) +""" +fetch_telemetry_data() with param: http://0.0.0.0:9001/csv/cset/converted_enterprise, 0 +Received bytes: 5369833 +Processed counters: 690000 +fetch time: 561, parse time: 186 + +""" + +FETCH_TELEMETRY_DATA = re.compile( + r"^(?:fetch_telemetry_data|Received bytes|Processed counters|fetch time).*$" +) + +UFM_STATE_CHANGE = re.compile(r"^.*UFM daemon.*") + + +def console_log_exception(match: Match): + timestamp = match.group(1) + line = match.group(3) + return (timestamp, "Error", line) + + +def error_line(match: Match): + line = match.group(1) + return (None, None, line) + + +def do_nothing(match: Match): # pylint: disable=unused-argument + return (None, None, None) + + +CONSOLE_LOG_HEADERS = ("timestamp", "type", "data") + +console_log_regex_cls = RegexAndHandlers("console.log", CONSOLE_LOG_HEADERS) +console_log_regex_cls.add_regex(FETCH_TELEMETRY_DATA, do_nothing) +console_log_regex_cls.add_regex(UFM_STATE_CHANGE, do_nothing) +console_log_regex_cls.add_regex(CONSOLE_LOG_EXCEPTION_REGEX, console_log_exception) +console_log_regex_cls.add_regex(ERROR_LINE_REGEX, error_line) diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/event_log_regex.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/event_log_regex.py new file mode 100644 index 000000000..a039f0f7b --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/event_log_regex.py @@ -0,0 +1,97 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# pylint: disable=missing-function-docstring +# pylint: disable=missing-module-docstring +import re +from typing import Match + +from loganalyze.log_parsing.base_regex import RegexAndHandlers + +EVENT_LOG_REGEX = re.compile( + # date time na na severity Site and name type objtype objid + r"^([\d\-]+ [\d\:\.]+) \[\d+\] \[\d+\] (\w+) (?:Site \[[\w-]*\] )?\[(\w+)\] (\w+) \[(.*)\]\: " + # event event_details + r"((([ \w\/\-]+)((\,|\:|\.)(.*))?)|(.*))$" +) + +IBPORT_OBJ_REGEX = re.compile(r"((Computer|Switch).*)\] \[") +COMP_OBJ_REGEX = re.compile(r"(Computer: [\w\-\.]+)") +MODULE_OBJ_REGEX = re.compile(r"(Switch.*)\] \[") +BAD_PKEY_REGEX = re.compile(r"Switch: ([\d\w-]+)") +LINK_OBJ_REGEX = re.compile(r"([a-f0-9]+\_[0-9]+)[\w ]+\: ([a-f0-9]+_[0-9]+)") + + +def _format_object_id(object_type: str, object_id: str): # pylint: disable=too-many-branches + if object_type == "IBPort": + match = IBPORT_OBJ_REGEX.search(object_id) + if match: + object_id = match.group(1) + elif object_type == "Site": + object_id = "default" + elif object_type == "Computer": + match = COMP_OBJ_REGEX.search(object_id) + if match: + object_id = match.group(1) + elif object_type == "Module": + match = MODULE_OBJ_REGEX.search(object_id) + if match: + object_id = match.group(1) + elif object_type == "Link": + match = LINK_OBJ_REGEX.search(object_id) + if match: + src = match.group(1) + dest = match.group(2) + object_id = f"{src}:{dest}" + elif object_type == "Switch": + match = BAD_PKEY_REGEX.search(object_id) + if match: + object_id = match.group(1) + return object_id + + +def event_log_extractor(match: Match): + if not match: + return None + timestamp = match.group(1) + severity = match.group(2) + event_type = match.group(3) + object_type = match.group(4) + object_id = _format_object_id(object_type, match.group(5)) + event = match.group(12) + event_details = "" + if not event: + event = match.group(8) + event_details = match.group(11) + return ( + timestamp, + severity, + event_type, + object_type, + object_id, + event, + event_details, + ) + + +EVENT_LOG_HEADERS = ( + "timestamp", + "severity", + "event_type", + "object_type", + "object_id", + "event", + "event_details", +) + + +event_log_regex_cls = RegexAndHandlers("event.log", EVENT_LOG_HEADERS) +event_log_regex_cls.add_regex(EVENT_LOG_REGEX, event_log_extractor) diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/ibdiagnet_log_regex.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/ibdiagnet_log_regex.py new file mode 100644 index 000000000..6aeae7963 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/ibdiagnet_log_regex.py @@ -0,0 +1,48 @@ +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# pylint: disable=missing-function-docstring +# pylint: disable=missing-module-docstring + +import re +from typing import Match + +from loganalyze.log_parsing.base_regex import RegexAndHandlers + +IBDIAGNET_LOG_TOTAL_NODES = re.compile( + # Num Nodes + r"^Total Nodes +\: (\d+)$" +) + +IBDIAGNET_LOG_IB_DATA = re.compile( + # type result number + r"^IB (Switches|Channel Adapters|Aggregation Nodes|Routers) +\: (\d+)" +) + + +IBDIAGNET_LOG_HEADERS = ("type", "amount") + + +def ibdiagnet_log_total_nodes(match: Match): + num_nodes = match.group(1) + return ("total_nodes", num_nodes) + + +def ibdiagnet_log_ib_data(match: Match): + # This is to make sure we do not write white space to the CSV + ib_type = f"ib_{match.group(1).replace(' ', '_')}" + num = match.group(2) + return (ib_type, num) + + +ibdiagnet_log_regex_cls = RegexAndHandlers("ibdiagnet2.log", IBDIAGNET_LOG_HEADERS) + +ibdiagnet_log_regex_cls.add_regex(IBDIAGNET_LOG_TOTAL_NODES, ibdiagnet_log_total_nodes) +ibdiagnet_log_regex_cls.add_regex(IBDIAGNET_LOG_IB_DATA, ibdiagnet_log_ib_data) diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/log_parser.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/log_parser.py new file mode 100644 index 000000000..ee79f83fa --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/log_parser.py @@ -0,0 +1,66 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# pylint: disable=missing-module-docstring + +import os +import re +from typing import Callable, Tuple, List +import gzip + +class LogParser: # pylint: disable=too-few-public-methods + """ + Basic class for parsing logs + """ + + def __init__(self, input_log_path: str, lines_regex: List[Tuple[str, Callable]]): + self._lines_regex_and_fn = lines_regex + if not os.path.exists(input_log_path): + raise FileNotFoundError(f"File {input_log_path} does not exists") + self._input_log_path = input_log_path + if input_log_path.endswith(".gz"): + self._parser_func = self._parse_gz_log + else: + self._parser_func = self._parse_regular_log + self._success_parsed = 0 + self._failed_parsed = 0 + + def _parse_gz_log(self, callback_fn: Callable[[Tuple], None]): + with gzip.open(self._input_log_path, "rt") as log_file: + for line in log_file: + self._process_line(line, callback_fn) + + def _parse_regular_log(self, callback_fn: Callable[[Tuple], None]): + with open(self._input_log_path, "r", encoding="utf-8") as log_file: + for line in log_file: + self._process_line(line, callback_fn) + + def parse(self, callback_fn: Callable[[Tuple], None]): + """ + For the given log, parse all the lines + """ + self._parser_func(callback_fn) + + def _find_first_match(self, line: str): + for _, regex_and_align_func in enumerate(self._lines_regex_and_fn): + regex, align_func = regex_and_align_func + match = re.match(regex, line) + if match: + return align_func(match) + return None + + def _process_line(self, line: str, callback_fn: Callable[[Tuple], None]): + line_groups = self._find_first_match(line) + if not line_groups: + self._failed_parsed += 1 + return + self._success_parsed += 1 + callback_fn(line_groups) diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/logs_regex.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/logs_regex.py new file mode 100644 index 000000000..cbbaefeb3 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/logs_regex.py @@ -0,0 +1,32 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# pylint: disable=missing-module-docstring + +from loganalyze.log_parsing.event_log_regex import event_log_regex_cls +from loganalyze.log_parsing.ufm_health_regex import ufm_health_regex_cls +from loganalyze.log_parsing.ufm_log import ufm_log_regex_cls +from loganalyze.log_parsing.ibdiagnet_log_regex import ibdiagnet_log_regex_cls +from loganalyze.log_parsing.console_log_regex import console_log_regex_cls +from loganalyze.log_parsing.rest_api_log_regex import rest_api_log_regex_cls + +logs = [ + event_log_regex_cls, + ufm_health_regex_cls, + ufm_log_regex_cls, + ibdiagnet_log_regex_cls, + console_log_regex_cls, + rest_api_log_regex_cls +] + +logs_regex_csv_headers_list = [] +for log_type in logs: + logs_regex_csv_headers_list.append(log_type.get_all_info()) diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/rest_api_log_regex.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/rest_api_log_regex.py new file mode 100644 index 000000000..186625c61 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/rest_api_log_regex.py @@ -0,0 +1,59 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# + + +import re +from typing import Match + +from loganalyze.log_parsing.base_regex import RegexAndHandlers + +REST_API_LOG_REGEX = re.compile( + # Date NA Severity + r"^(?P[\d\-]+ [\d\:\.]+) \w+ (?P\w+) +(?: client: " + # client user + r"(?PNone|[\.:\w]+),)?(?: user: (?P[\w]+),)" + # url method + r"(?: url: \((?P[^\s\)]+)\)?,)(?: method:? \((?P[\w]+)\),?)?" + # Status code duration + r"(?: status_code: \((?P[\d]+)\),?)?(?: duration: (?P[\d\.]+) seconds)?$" +) + +def rest_api_log(match: Match): + """ + The rest api log line had serval changes in the last releases. + By using the matching groups here, we do not need to check if + a filed exists or not. If it does not, it will be None. + """ + timestamp = match.group("date") + severity = match.group("severity") + client_ip = match.group("client") + user = match.group("user") + url = match.group("url") + method = match.group("method") + status_code = match.group("status_code") + duration = match.group("duration") + return (timestamp, severity, client_ip, user, url, method, status_code, duration) + +REST_API_LOG_HEADERS = ( + "timestamp", + "severity", + "client_ip", + "user", + "url", + "method", + "status_code", + "duration" +) + +rest_api_log_regex_cls = RegexAndHandlers("rest_api.log", REST_API_LOG_HEADERS) + +rest_api_log_regex_cls.add_regex(REST_API_LOG_REGEX, rest_api_log) diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/ufm_health_regex.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/ufm_health_regex.py new file mode 100644 index 000000000..a112812a5 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/ufm_health_regex.py @@ -0,0 +1,67 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# pylint: disable=missing-function-docstring +# pylint: disable=missing-module-docstring + +import re +from typing import Match + +from loganalyze.log_parsing.base_regex import RegexAndHandlers + +UFM_HEALTH_LOG_REGEX = re.compile( + # Date Severity Test Test Class Status + r"^([\d\-]+ [\d\:\.]+,\d+)\.\d{3} \w+ (\w+) +Test operation (\w+) of the test (\w+) (\w+)\." + # N/A Reason + r"(?: Reason\:)?(.+)?" +) +UFM_HEALTH_LOG_FAILURE_REASON_REGEX = re.compile( + # Does not start with date Reason + r"^(?!\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})(.*)$" +) + + +def ufm_health_failure_line(match: Match): + """ + This is for the lines that comes after UFMCpuUsageTest failure + """ + reason = match.group(1) + return ("", "", "", "", "", reason) + + +def ufm_health_log(match: Match): + timestamp = match.group(1) + timestamp = timestamp.replace(",", ".", 1) + severity = match.group(2) + test_name = match.group(3) + test_class = match.group(4) + test_status = match.group(5) + reason = match.group(6) + if reason: + reason = reason.replace("but we treated it as success. Reason: ", "") + return (timestamp, severity, test_name, test_class, test_status, reason) + + +UFM_HEALTH_HEADERS = ( + "timestamp", + "severity", + "test_name", + "test_class", + "test_status", + "reason", +) + +ufm_health_regex_cls = RegexAndHandlers("ufmhealth.log", UFM_HEALTH_HEADERS) + +ufm_health_regex_cls.add_regex(UFM_HEALTH_LOG_REGEX, ufm_health_log) +ufm_health_regex_cls.add_regex( + UFM_HEALTH_LOG_FAILURE_REASON_REGEX, ufm_health_failure_line +) diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/ufm_log.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/ufm_log.py new file mode 100644 index 000000000..1a0d7ae06 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/log_parsing/ufm_log.py @@ -0,0 +1,159 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# pylint: disable=missing-function-docstring +# pylint: disable=missing-module-docstring + +import re +from typing import Match + +from loganalyze.log_parsing.base_regex import RegexAndHandlers + +UFM_LOG_BASE = r"^(?P[\d\-]+ [\d\:\.]+) \w+ +(?P\w+) +{}" + +UFM_LOG_HANDLED_TRAPS = re.compile( + # date time NA severity + UFM_LOG_BASE.format(r"UFM handled (\d+) traps$") +) + +UFM_LOG_REST_MNGMNT_ERROR = re.compile( + UFM_LOG_BASE.format( + r"Rest management error: invalid username\/password \((\w+)\/[\*]+\)$" + ) +) + +UFM_LOG_SYSINFO_FAILURE_FOR_SWITCH = re.compile( + UFM_LOG_BASE.format( + # switch failure reason + r"Sysinfo cb. Failed to get sysinfo for switch ([\w\d]+): \[(.+)$" + ) +) + +UFM_LOG_FABRIC_ANALYSIS = re.compile( + UFM_LOG_BASE.format( + # starting status repord-id completed status + r"(?PStarting)?\s?Fabric_Analysis report\s(?P\d+)(?:\scompleted)?$" + ) +) + +UFM_LOG_UFM_STARTED = re.compile(UFM_LOG_BASE.format(r"UFM Enterprise Server started$")) + +UFM_LOG_GRID_LOAD_STATUS = re.compile( + UFM_LOG_BASE.format( + # state + r"Grid Load from DB before discovery (?Pstarted|finished)" + ) +) + +UFM_LOG_INIT_IS_DONE = re.compile( + UFM_LOG_BASE.format(r"Initialization done, UFM server is ready$") +) + +UFM_LOG_TELEMETRY_PROCESSING_TIME = re.compile( + UFM_LOG_BASE.format( + # processing time + r"Prometheus Client: Total Processing time = (\d+\.\d+)" + ) +) + +UFM_LOG_TELEMETRY_DEVICE_STATS = re.compile( + UFM_LOG_BASE.format( + # num devices devices rate + r"handled device stats. num_devices=(\d+) rate=(\d+\.\d+) devices\/sec." + # num ports ports rate + r" num_ports=(\d+) rate=(\d+\.\d+) ports\/sec." + ) +) + +UFM_LOG_HEADERS = ("timestamp", "severity", "log_type", "data", "extra_info") + + +def ufm_log_telemetry_device_stats(match: Match): + timestamp = match.group(1) + severity = match.group(2) + num_devices = match.group(3) + num_ports = match.group(5) + return (timestamp, severity, "telemetry_device_stats", num_devices, num_ports) + + +def ufm_log_telemetry_processing_time(match: Match): + timestamp = match.group(1) + severity = match.group(2) + processing_time = match.group(3) + return (timestamp, severity, "telemetry_processing_time", processing_time, None) + + +def ufm_log_init_is_done(match: Match): + timestamp = match.group(1) + severity = match.group(2) + return (timestamp, severity, "ufm_init_done", None, None) + + +def ufm_log_ufm_grid_load_status(match: Match): + timestamp = match.group(1) + severity = match.group(2) + status = match.group("state") + return (timestamp, severity, "grid_load_status", status, None) + + +def ufm_log_fabric_analysis(match: Match): + timestamp = match.group("timestamp") + severity = match.group("severity") + status = match.group("status") or "Completed" + report_id = match.group("id") + return (timestamp, severity, "fabric_analysis", status, report_id) + + +def ufm_log_sysinfo_failure_for_switch(match: Match): + timestamp = match.group(1) + severity = match.group(2) + switch = match.group(3) + reason = match.group(4) # not used for now + return (timestamp, severity, "failed_to_get_sysinfo", switch, reason) + + +def ufm_log_rest_mngmnt_error(match: Match): + timestamp = match.group(1) + severity = match.group(2) + user = match.group(3) + return (timestamp, severity, "mngmnt_bad_cookie", user, None) + + +def ufm_log_ufm_started(match: Match): + timestamp = match.group(1) + severity = match.group(2) + return (timestamp, severity, "ufm_started", None, None) + + +def ufm_log_trap_handler(match: Match): + timestamp = match.group(1) + severity = match.group(2) + number_of_traps = match.group(3) + return (timestamp, severity, "trap_handled", number_of_traps, None) + + +ufm_log_regex_cls = RegexAndHandlers("ufm.log", UFM_LOG_HEADERS) + +ufm_log_regex_cls.add_regex(UFM_LOG_HANDLED_TRAPS, ufm_log_trap_handler) +ufm_log_regex_cls.add_regex(UFM_LOG_FABRIC_ANALYSIS, ufm_log_fabric_analysis) +ufm_log_regex_cls.add_regex(UFM_LOG_REST_MNGMNT_ERROR, ufm_log_rest_mngmnt_error) +ufm_log_regex_cls.add_regex( + UFM_LOG_SYSINFO_FAILURE_FOR_SWITCH, ufm_log_sysinfo_failure_for_switch +) +ufm_log_regex_cls.add_regex(UFM_LOG_GRID_LOAD_STATUS, ufm_log_ufm_grid_load_status) +ufm_log_regex_cls.add_regex( + UFM_LOG_TELEMETRY_PROCESSING_TIME, ufm_log_telemetry_processing_time +) +ufm_log_regex_cls.add_regex( + UFM_LOG_TELEMETRY_DEVICE_STATS, ufm_log_telemetry_device_stats +) +ufm_log_regex_cls.add_regex(UFM_LOG_UFM_STARTED, ufm_log_ufm_started) +ufm_log_regex_cls.add_regex(UFM_LOG_INIT_IS_DONE, ufm_log_init_is_done) diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/logger.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/logger.py new file mode 100644 index 000000000..c26cd115f --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/logger.py @@ -0,0 +1,34 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +import logging + +LOGGER = None + +def setup_logger(name, level=logging.INFO): + """ + Function to set up a logger that outputs to the console. + :param name: Name of the logger. + :param level: Logging level (default is INFO). + :return: Configured logger instance. + """ + global LOGGER # pylint: disable=global-statement + + formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s') + + # Create console handler + console_handler = logging.StreamHandler() + console_handler.setFormatter(formatter) + + LOGGER = logging.getLogger(name) + LOGGER.setLevel(level) + LOGGER.addHandler(console_handler) + return LOGGER diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_csv/__init__.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_csv/__init__.py new file mode 100644 index 000000000..8ff4dcb4a --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_csv/__init__.py @@ -0,0 +1,12 @@ +#!/bin/bash +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_csv/csv_handler.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_csv/csv_handler.py new file mode 100644 index 000000000..d265ad77f --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_csv/csv_handler.py @@ -0,0 +1,60 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# pylint: disable=broad-exception-caught +# pylint: disable=missing-function-docstring +# pylint: disable=missing-module-docstring +# pylint: disable=missing-class-docstring +import os +import csv +from typing import List, Tuple +import loganalyze.logger as log + +class CsvHandler: + def __init__( + self, + csv_headers: List[str], + dest_csv_path: str, + num_of_line_to_keep_in_mem=20000, + ): + self._num_of_items_per_row = len(csv_headers) + self._csv_headers = csv_headers + self._dest_csv_path = dest_csv_path + self._lines_to_add_in_csv = [] + self._num_of_line_to_keep_in_mem = num_of_line_to_keep_in_mem + # Creating the dest CSV + csv_directory = os.path.dirname(self._dest_csv_path) + os.makedirs(csv_directory, exist_ok=True) + # If file exists, delete it and replace with empty one that has only headers + with open(self._dest_csv_path, "w", newline="", encoding="utf-8") as csv_file: + csv_writer = csv.writer(csv_file) + csv_writer.writerow(self._csv_headers) + + def add_line(self, line: Tuple): + if len(line) != self._num_of_items_per_row: + raise AttributeError( + f"Wrong number of items passed to add_line, " + f"got {len(line)} vs {self._num_of_items_per_row}" + ) + self._lines_to_add_in_csv.append(line) + if len(self._lines_to_add_in_csv) > self._num_of_line_to_keep_in_mem: + self.save_file() + + def save_file(self): + try: + with open( + self._dest_csv_path, "a", newline="", encoding="utf-8" + ) as csv_file: + csv_writer = csv.writer(csv_file) + csv_writer.writerows(self._lines_to_add_in_csv) + self._lines_to_add_in_csv = [] + except Exception as e: + log.LOGGER.error(f"Error when trying to save to {self._dest_csv_path}, {e}") diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_extraction/__init__.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_extraction/__init__.py new file mode 100644 index 000000000..8ff4dcb4a --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_extraction/__init__.py @@ -0,0 +1,12 @@ +#!/bin/bash +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_extraction/base_extractor.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_extraction/base_extractor.py new file mode 100644 index 000000000..e2f539591 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_extraction/base_extractor.py @@ -0,0 +1,26 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +from pathlib import Path + + +class BaseExtractor: + def is_exists_get_as_path(self, location) -> Path: + """ + Checks if location exists and is from the right type + Returns the location is path object and it's father, else + returns None + """ + if isinstance(location, str): + location = Path(location) + if location.exists(): + return location + return None diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_extraction/directory_extractor.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_extraction/directory_extractor.py new file mode 100644 index 000000000..a7aca2e8e --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_extraction/directory_extractor.py @@ -0,0 +1,50 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +import os +from pathlib import Path +import shutil +from typing import List +from loganalyze.logs_extraction.base_extractor import BaseExtractor + +class DirectoryExtractor(BaseExtractor): + def __init__(self, dir_path:Path): + dir_path = self.is_exists_get_as_path(dir_path) + if dir_path and dir_path.is_dir(): + self.dir_path = dir_path + else: + raise FileNotFoundError(f"Could not use {dir_path}, " + "make sure it exists and is a directory") + + def extract_files(self, files_to_extract: List[str], destination: str): + if not os.path.exists(destination): + os.makedirs(destination) + + # Convert the list to a set for faster lookup + files_to_extract = set(files_to_extract) + found_files = set() + not_found_files = set(files_to_extract) + + # Traverse the source directory and its subdirectories + for root, _, files in os.walk(self.dir_path): + for file_name in files: + if file_name in files_to_extract: + src_file_path = os.path.join(root, file_name) + dest_file_path = os.path.join(destination, file_name) + shutil.copy2(src_file_path, dest_file_path) + found_files.add(dest_file_path) + not_found_files.discard(file_name) + + # Stop if all files have been found + if not not_found_files: + return found_files, not_found_files + + return found_files, not_found_files diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_extraction/tar_extractor.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_extraction/tar_extractor.py new file mode 100644 index 000000000..22942c6ff --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/logs_extraction/tar_extractor.py @@ -0,0 +1,110 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# pylint: disable=broad-exception-caught +# pylint: disable=missing-class-docstring +# pylint: disable=missing-module-docstring +from pathlib import Path +from tarfile import TarFile +import tarfile +import os +from typing import List, Set + +from loganalyze.logs_extraction.base_extractor import BaseExtractor +from loganalyze.utils.common import delete_folders +import loganalyze.logger as log + +LOGS_GZ_POSTFIX = ".gz" +GZIP_MAGIC_NUMBER = b"\x1f\x8b" # Magic number to understand if a file is really a gzip + + +class DumpFilesExtractor(BaseExtractor): + def __init__(self, dump_path: Path) -> None: + dump_path = self.is_exists_get_as_path(dump_path) + if dump_path and dump_path.is_file(): + self.dump_path = dump_path + self.directory = dump_path.parent + else: + raise FileNotFoundError(f"Could not use {dump_path}, make sure it exists and a tar") + + def _get_files_from_tar( + self, opened_file: TarFile, files_to_extract: Set[str], destination: str + ): + files_went_over = set() + failed_extract = set() + folders_to_remove = set() + for member in opened_file: + base_name = os.path.basename(member.name) + if base_name in files_to_extract: + try: + opened_file.extract(member, path=destination) + extracted_file_path = os.path.join(destination, str(member.path)) + log.LOGGER.debug(f"Extracted {base_name}") + os.rename(extracted_file_path, os.path.join(destination, base_name)) + folder_to_remove = os.path.dirname(extracted_file_path) + folders_to_remove.add(folder_to_remove) + except Exception as e: + log.LOGGER.debug(f"Failed to extract {base_name}, {e}") + failed_extract.add(base_name) + finally: + files_went_over.add(base_name) + files_to_extract.remove(base_name) + if not files_to_extract: + break + files_extracted = files_went_over.difference(failed_extract) + # When extracting the files from the tar, they are also taken with their + # directories from inside the tar, there is no way to only take the file + # This function will remove all this nested directors + delete_folders(folders_to_remove, destination) + files_extracted = {os.path.join(destination, file) for file in files_extracted} + return files_extracted, failed_extract + + @staticmethod + def is_gzip_file(file_path: str) -> bool: + """Check if the file is a gzip-compressed file by reading its magic number.""" + with open(file_path, "rb") as file: + magic_number = file.read(2) + return magic_number == GZIP_MAGIC_NUMBER + + @staticmethod + def is_gzip_file_obj(file_obj) -> bool: + """Check if the file-like object is a gzip-compressed file.""" + position = file_obj.tell() + magic_number = file_obj.read(2) + file_obj.seek(position) # Reset the stream position + return magic_number == GZIP_MAGIC_NUMBER + + def extract_files(self, files_to_extract: List[str], destination: str): + """Since we do not know the type of dump, we search the files in the nested tars""" + os.makedirs(destination, exist_ok=True) + files_to_extract = set(files_to_extract) + open_file_mode = "r:gz" if self.is_gzip_file(self.dump_path) else "r:" + with tarfile.open(self.dump_path, open_file_mode) as outer_tar: + # Checking if we have a dump from a complex env + # The first tar that has some of the files, "wins" + inner_tar_files = [ + name for name in outer_tar.getnames() if name.endswith(".tar.gz") + ] + for inner_tar_name in inner_tar_files: + with outer_tar.extractfile(inner_tar_name) as inner_tar_stream: + inner_file_open_mode = ( + "r:gz" if self.is_gzip_file_obj(inner_tar_stream) else "r:" + ) + with tarfile.open( + fileobj=inner_tar_stream, mode=inner_file_open_mode + ) as inner_tar: + extracted_files, failed_files = self._get_files_from_tar( + inner_tar, files_to_extract, destination + ) + if len(extracted_files) > 0: + return extracted_files, failed_files + # If we got to this point, we might have a simple tar, try to extract from it + return self._get_files_from_tar(outer_tar, files_to_extract, destination) diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/base_log_parser.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/base_log_parser.py new file mode 100755 index 000000000..f7c244f44 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/base_log_parser.py @@ -0,0 +1,60 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# author: Samer Deeb +# date: Mar 02, 2024 +# +from abc import ABC, abstractmethod +from enum import Enum, auto +from typing import Iterable, Dict + + +class LogAnalysisTypes(Enum): + UFMEventsLogEntry = auto() + UFMEventsTopTalkers = auto() + SMEventsLogEntry = auto() + SMEventsTopTalkers = auto() + + +class LogSubscriber(ABC): + @abstractmethod + def update(self, analysis_type: LogAnalysisTypes, analysis_record: Iterable): + pass + + +class BaseLogParser(ABC): + def __init__(self, file_name): + self.file_name = file_name + self.subscribers: Dict[LogAnalysisTypes, LogSubscriber] = {} + + def parse(self): + lines_counter = 0 + lines_failed = 0 + with open(self.file_name, "r") as fp: + for line in fp: + lines_counter += 1 + if not self._parse_line(line): + lines_failed += 1 + if lines_counter % 1000 == 0: + print(".", end="") + print() + print(f"Parsed total {lines_counter} lines, failed {lines_failed} lines") + + @abstractmethod + def _parse_line(self, line): + pass + + def subscribe(self, analysis_type: LogAnalysisTypes, subscriber: LogSubscriber): + self.subscribers.setdefault(analysis_type, []).append(subscriber) + + def notify_sunscribers(self, analysis_type, analysis_record): + for subscriber in self.subscribers.get(analysis_type, []): + subscriber.update(analysis_type, analysis_record) diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/constants.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/constants.py new file mode 100755 index 000000000..d599d0bf7 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/constants.py @@ -0,0 +1,21 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# author: Samer Deeb +# date: Mar 05, 2024 +# + + +class Files: + UFM_EVENTS_CSV_FILE = "ufm_events_log.csv" + EVENTS_LOG_FILES_GLOB = "event.log*" + FIRST_LAST_EVENT_FILE = "first_last_event.csv" + UFM_EVENTS_STATS_FILE = "ufm_events_stats.csv" diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/csv_builder.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/csv_builder.py new file mode 100755 index 000000000..8bc9da01a --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/csv_builder.py @@ -0,0 +1,70 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# author: Samer Deeb +# date: Mar 02, 2024 +# + +import csv +import os +from typing import Iterable, List + +from log_analyzer.src.loganalyze.old_logic.base_log_parser import ( + LogSubscriber, + LogAnalysisTypes, +) +from log_analyzer.src.loganalyze.old_logic.constants import Files + + +class CsvBuilder(LogSubscriber): + UFM_EVENTS_LOG_HEADER = ( + "timestamp", + "severity", + "event_type", + "object_type", + "object_id", + "event", + "event_details", + ) + UFM_EVENTS_STATS_HEADER = ("event", "object_id", "count") + + def __init__(self): + self.ufm_events_log_entries = [] + self.ufm_events_top_talkers = [] + + def update(self, analysis_type: LogAnalysisTypes, analysis_record: Iterable): + if analysis_type == LogAnalysisTypes.UFMEventsLogEntry: + self.ufm_events_log_entries.append(analysis_record) + elif analysis_type == LogAnalysisTypes.UFMEventsTopTalkers: + self.ufm_events_top_talkers.append(analysis_record) + + def _write_csv_file(self, csv_path: str, headers: str, records: List[str]): + with open(csv_path, "w") as csvfile: + writer = csv.writer(csvfile, quoting=csv.QUOTE_MINIMAL) + writer.writerow(headers) + for index, entry in enumerate(records): + writer.writerow(entry) + if index % 1000 == 0: + print(".", end="") + print() + + def save(self, out_dir: str): + csv_path = os.path.join(out_dir, Files.UFM_EVENTS_CSV_FILE) + print("-I- Generating ufm events file:", csv_path, "...") + self._write_csv_file( + csv_path, self.UFM_EVENTS_LOG_HEADER, self.ufm_events_log_entries + ) + + csv_path = os.path.join(out_dir, "ufm_events_stats.csv") + print("-I- Generating ufm events stats file:", csv_path, "...") + self._write_csv_file( + csv_path, self.UFM_EVENTS_STATS_HEADER, self.ufm_events_top_talkers + ) diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/events_explorer.ipynb b/plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/events_explorer.ipynb new file mode 100644 index 000000000..8e95d0292 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/events_explorer.ipynb @@ -0,0 +1,1477 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#!/bin/bash\n", + "#\n", + "# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.\n", + "#\n", + "# This software product is a proprietary product of Nvidia Corporation and its affiliates\n", + "# (the \"Company\") and all right, title, and interest in and to the software\n", + "# product, including all associated intellectual property rights, are and\n", + "# shall remain exclusively with the Company.\n", + "#\n", + "# This software product is governed by the End User License Agreement\n", + "# provided with the software product.\n", + "#\n", + "# author: Alex Fok\n", + "# date: Mar 03, 2024\n", + "#" + ] + }, + { + "cell_type": "code", + "execution_count": 120, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The autoreload extension is already loaded. To reload it, use:\n", + " %reload_ext autoreload\n", + "Requirement already satisfied: pandas in c:\\users\\afok\\appdata\\local\\packages\\pythonsoftwarefoundation.python.3.10_qbz5n2kfra8p0\\localcache\\local-packages\\python310\\site-packages (from -r requirements.txt (line 1)) (1.5.3)Note: you may need to restart the kernel to use updated packages.\n", + "\n", + "Requirement already satisfied: numpy in c:\\users\\afok\\appdata\\local\\packages\\pythonsoftwarefoundation.python.3.10_qbz5n2kfra8p0\\localcache\\local-packages\\python310\\site-packages (from -r requirements.txt (line 2)) (1.24.1)\n", + "Requirement already satisfied: matplotlib in c:\\users\\afok\\appdata\\local\\packages\\pythonsoftwarefoundation.python.3.10_qbz5n2kfra8p0\\localcache\\local-packages\\python310\\site-packages (from -r requirements.txt (line 3)) (3.6.3)\n", + "Requirement already satisfied: pytz>=2020.1 in c:\\users\\afok\\appdata\\local\\packages\\pythonsoftwarefoundation.python.3.10_qbz5n2kfra8p0\\localcache\\local-packages\\python310\\site-packages (from pandas->-r requirements.txt (line 1)) (2022.7.1)\n", + "Requirement already satisfied: python-dateutil>=2.8.1 in c:\\users\\afok\\appdata\\local\\packages\\pythonsoftwarefoundation.python.3.10_qbz5n2kfra8p0\\localcache\\local-packages\\python310\\site-packages (from pandas->-r requirements.txt (line 1)) (2.8.2)\n", + "Requirement already satisfied: pillow>=6.2.0 in c:\\users\\afok\\appdata\\local\\packages\\pythonsoftwarefoundation.python.3.10_qbz5n2kfra8p0\\localcache\\local-packages\\python310\\site-packages (from matplotlib->-r requirements.txt (line 3)) (9.4.0)\n", + "Requirement already satisfied: fonttools>=4.22.0 in c:\\users\\afok\\appdata\\local\\packages\\pythonsoftwarefoundation.python.3.10_qbz5n2kfra8p0\\localcache\\local-packages\\python310\\site-packages (from matplotlib->-r requirements.txt (line 3)) (4.38.0)\n", + "Requirement already satisfied: contourpy>=1.0.1 in c:\\users\\afok\\appdata\\local\\packages\\pythonsoftwarefoundation.python.3.10_qbz5n2kfra8p0\\localcache\\local-packages\\python310\\site-packages (from matplotlib->-r requirements.txt (line 3)) (1.0.7)\n", + "Requirement already satisfied: cycler>=0.10 in c:\\users\\afok\\appdata\\local\\packages\\pythonsoftwarefoundation.python.3.10_qbz5n2kfra8p0\\localcache\\local-packages\\python310\\site-packages (from matplotlib->-r requirements.txt (line 3)) (0.11.0)\n", + "Requirement already satisfied: packaging>=20.0 in c:\\users\\afok\\appdata\\local\\packages\\pythonsoftwarefoundation.python.3.10_qbz5n2kfra8p0\\localcache\\local-packages\\python310\\site-packages (from matplotlib->-r requirements.txt (line 3)) (22.0)\n", + "Requirement already satisfied: pyparsing>=2.2.1 in c:\\users\\afok\\appdata\\local\\packages\\pythonsoftwarefoundation.python.3.10_qbz5n2kfra8p0\\localcache\\local-packages\\python310\\site-packages (from matplotlib->-r requirements.txt (line 3)) (3.0.9)\n", + "Requirement already satisfied: kiwisolver>=1.0.1 in c:\\users\\afok\\appdata\\local\\packages\\pythonsoftwarefoundation.python.3.10_qbz5n2kfra8p0\\localcache\\local-packages\\python310\\site-packages (from matplotlib->-r requirements.txt (line 3)) (1.4.4)\n", + "Requirement already satisfied: six>=1.5 in c:\\users\\afok\\appdata\\local\\packages\\pythonsoftwarefoundation.python.3.10_qbz5n2kfra8p0\\localcache\\local-packages\\python310\\site-packages (from python-dateutil>=2.8.1->pandas->-r requirements.txt (line 1)) (1.16.0)\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + "[notice] A new release of pip available: 22.3.1 -> 23.0.1\n", + "[notice] To update, run: C:\\Users\\afok\\AppData\\Local\\Microsoft\\WindowsApps\\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\\python.exe -m pip install --upgrade pip\n" + ] + }, + { + "data": { + "text/plain": [ + "{'divide': 'ignore', 'over': 'warn', 'under': 'ignore', 'invalid': 'warn'}" + ] + }, + "execution_count": 120, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%matplotlib inline\n", + "%load_ext autoreload\n", + "%autoreload 2\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd # data processing\n", + "import csv\n", + "import tarfile\n", + "import os\n", + "import re\n", + "import argparse\n", + "#import utils\n", + "from tqdm import tqdm\n", + "\n", + "\n", + "# Environment setup\n", + "%pip install -r requirements.txt\n", + "np.seterr(divide = 'ignore') " + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Parse raw events file and generate csv file" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Can be taken from UFM:\n", + "- `def get_events_by_ids(self, event_ids):`\n", + "- `def raw_data_csv_parser(self, event):`\n", + "- `def parse_logs_blob_into_csv(self, logs_blob_path, csv_path):`\n", + "- `def analyze(self, action, param, _start_time, _end_time, _timezone, is_async, offline_analysis_path):`\n", + "- `def extract_log_files_from_sysdump(sysdump_path, output_path):`\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 121, + "metadata": {}, + "outputs": [], + "source": [ + "def parse_logs_blob_into_csv(logs_blob_path, csv_path):\n", + " # Event fields indexes\n", + " DATE = 0\n", + " TIME = 1\n", + " TRAP_ID = 2\n", + " EVENT_ID = 3\n", + " TRAP_LEVEL = 4\n", + " TRAP_CATEGORY = 5\n", + " TRAP_SOURCE = 7\n", + " OBJ_TYPE = 6\n", + "\n", + " #CSV Headers\n", + " HEADER_TIMESTAMP = 'TimeStamp'\n", + " HEADER_TRAPID = 'TrapID'\n", + " HEADER_EVENTID = 'EventID'\n", + " HEADER_SEVERITY = 'Severity'\n", + " HEADER_CATEGORY = 'Category'\n", + " HEADER_OBJECTTYPE = 'ObjectType'\n", + " HEADER_SOURCE = 'Source'\n", + " HEADER_OBJECTID = 'ObjectID'\n", + " HEADER_PAYLOAD = 'Payload'\n", + " HEADER_DESCRIPTION = 'Description'\n", + " HEADER_EVENTS_COUNT = 'Events Count'\n", + " ID_REG = re.compile('\\[.*\\] ')\n", + " _DEF_ID_REG = re.compile('\\[.*\\]:')\n", + "\n", + " file_size = os.path.getsize(logs_blob_path)\n", + " file_pos = 0\n", + " pbar = tqdm(total=file_size, unit=\"MB\")\n", + " with open(logs_blob_path, 'r') as src:\n", + " with open(csv_path, 'w') as csv_dst:\n", + " dst_writer = csv.writer(csv_dst)\n", + " header = ['TimeStamp', 'TrapID', 'EventID', 'Severity', 'Category', 'ObjectType', 'Source', 'ObjectID', 'Payload']\n", + " dst_writer.writerow(header)\n", + " for i, event in enumerate(src):\n", + " file_pos += len(event)\n", + " if not i % 10:\n", + " pbar.update(file_pos - pbar.n)\n", + " row = []\n", + " splitted_eve = event.split()\n", + " #TimeStamp\n", + " row.append(' '.join((splitted_eve[DATE],splitted_eve[TIME])))\n", + " #TrapID\n", + " row.append(re.sub('[\\[|\\]]', '', splitted_eve[TRAP_ID]))\n", + " #EventID\n", + " row.append(re.sub('[\\[|\\]]', '', splitted_eve[EVENT_ID]))\n", + " #Severity\n", + " row.append(splitted_eve[TRAP_LEVEL])\n", + " #Category\n", + " row.append(re.sub('[\\[|\\]]', '', splitted_eve[TRAP_CATEGORY]))\n", + " #Object Type\n", + " row.append(re.sub('[\\[|\\]]', '', splitted_eve[OBJ_TYPE]))\n", + " #Source\n", + " row.append(re.sub('[\\[|\\]]', '', splitted_eve[TRAP_SOURCE]))\n", + " #ID\n", + " suffix = ' '.join(splitted_eve[TRAP_SOURCE:])\n", + " id_match = ID_REG.match(suffix)\n", + " if id_match is None:\n", + " id_match = _DEF_ID_REG.match(suffix)\n", + " row.append(re.sub('[\\[|\\]|:]', '', id_match.group()))\n", + " #Payload\n", + " a = ' '.join(splitted_eve[TRAP_SOURCE:])[id_match.end():]\n", + " row.append(a)\n", + " dst_writer.writerow(row)\n", + " pbar.close()\n", + " return csv_path\n" + ] + }, + { + "cell_type": "code", + "execution_count": 122, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|█████████▉| 249437960/249438382 [01:41<00:00, 2466252.65MB/s]\n" + ] + } + ], + "source": [ + "# Parse raw events file and generate csv file\n", + "logs_blob_path = \".\\sysdump_data\\sysdump\\sysdump-dsm07-0101-0912-01ufm-20230110-191826_events_in_range.log\"\n", + "csv_path = \".\\sysdump_data\\sysdump\\sysdump-dsm07-0101-0912-01ufm-20230110-191826_parsed_events.csv\"\n", + "#%ls \"./sysdump_data/sysdump\"\n", + "csv_file = parse_logs_blob_into_csv(logs_blob_path, csv_path)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 123, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "number of events: 1018264\n", + "attributes: Index(['TimeStamp', 'TrapID', 'EventID', 'Severity', 'Category', 'ObjectType',\n", + " 'Source', 'ObjectID', 'Payload'],\n", + " dtype='object')\n", + "attributes types:\n", + "TimeStamp datetime64[ns]\n", + "TrapID int64\n", + "EventID int64\n", + "Severity object\n", + "Category object\n", + "ObjectType object\n", + "Source object\n", + "ObjectID object\n", + "Payload object\n", + "dtype: object\n", + "Show some data:\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
TimeStampTrapIDEventIDSeverityCategoryObjectTypeSourceObjectIDPayload
02021-08-10 00:01:01.596660261110WARNINGHardwareIBPortdefault(250)default(250) / Switch dsm07-0101-0913-16ib0 / 39[dev_id: b8cef60300581d04]: Symbol-Error count...
12021-08-10 00:01:01.621660265110WARNINGHardwareIBPortdefault(249)default(249) / Switch dsm07-0101-0914-12ib0 / 37[dev_id: 043f720300c44418]: Symbol-Error count...
22021-08-10 00:01:31.996660276110WARNINGHardwareIBPortdefault(250)default(250) / Switch dsm07-0101-0911-11ib0 / 26[dev_id: b8cef6030054dec8]: Symbol-Error count...
32021-08-10 00:02:02.401660284110WARNINGHardwareIBPortdefault(251)default(251) / Switch dsm07-0101-0914-08ib0 / 36[dev_id: b8cef6030054d728]: Symbol-Error count...
42021-08-10 00:02:53.440660301392MINORHardwareModuledefault(250)default(250) / Switch dsm07-0101-0912-10ib0 / ...[dev_id: b8cef6030054da08]: Module Temperature...
\n", + "
" + ], + "text/plain": [ + " TimeStamp TrapID EventID Severity Category ObjectType \\\n", + "0 2021-08-10 00:01:01.596 660261 110 WARNING Hardware IBPort \n", + "1 2021-08-10 00:01:01.621 660265 110 WARNING Hardware IBPort \n", + "2 2021-08-10 00:01:31.996 660276 110 WARNING Hardware IBPort \n", + "3 2021-08-10 00:02:02.401 660284 110 WARNING Hardware IBPort \n", + "4 2021-08-10 00:02:53.440 660301 392 MINOR Hardware Module \n", + "\n", + " Source ObjectID \\\n", + "0 default(250) default(250) / Switch dsm07-0101-0913-16ib0 / 39 \n", + "1 default(249) default(249) / Switch dsm07-0101-0914-12ib0 / 37 \n", + "2 default(250) default(250) / Switch dsm07-0101-0911-11ib0 / 26 \n", + "3 default(251) default(251) / Switch dsm07-0101-0914-08ib0 / 36 \n", + "4 default(250) default(250) / Switch dsm07-0101-0912-10ib0 / ... \n", + "\n", + " Payload \n", + "0 [dev_id: b8cef60300581d04]: Symbol-Error count... \n", + "1 [dev_id: 043f720300c44418]: Symbol-Error count... \n", + "2 [dev_id: b8cef6030054dec8]: Symbol-Error count... \n", + "3 [dev_id: b8cef6030054d728]: Symbol-Error count... \n", + "4 [dev_id: b8cef6030054da08]: Module Temperature... " + ] + }, + "execution_count": 123, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "in_file = csv_path\n", + "header = ['TimeStamp', 'TrapID', 'EventID', 'Severity', 'Category', 'ObjectType', 'Source', 'ObjectID', 'Payload']\n", + "dtypes = {\n", + " 'Device ID':object,\n", + "# 'Event ID': int64,\n", + " 'Timestamps': object\n", + "}\n", + "dtypes = {\n", + " 'Device ID':object,\n", + "# 'Event ID': int64,\n", + " 'Timestamp': object\n", + "}\n", + "#events = pd.read_csv('sysdump_data/analyzer_output_object_sysdump.csv', dtype = dtypes, parse_dates=['Timestamps'])\n", + "events = pd.read_csv(in_file, parse_dates=['TimeStamp'])\n", + "print(f'number of events: {events.shape[0]}')\n", + "print(f'attributes: {events.columns}')\n", + "print(f'attributes types:\\n{events.dtypes}')\n", + "\n", + "print(f'Show some data:')\n", + "events.head(5)\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Load policy file" + ] + }, + { + "cell_type": "code", + "execution_count": 124, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "number of policies: 296\n", + "attributes: Index(['EventID', 'ShortDesc', 'Abbreviation', 'c1', 'c2', 'c3', 'c4', 'c5',\n", + " 'c6', 'c7', 'c8', 'c9', 'c10', 'c11', 'SEVERITY', 'c12', 'c13', 'c14',\n", + " 'ObjectType', 'FormatStr', 'Scope', 'c15', 'Description', 'c16'],\n", + " dtype='object')\n", + "attributes types:\n", + "EventID int64\n", + "ShortDesc object\n", + "Abbreviation object\n", + "c1 int64\n", + "c2 int64\n", + "c3 int64\n", + "c4 int64\n", + "c5 int64\n", + "c6 int64\n", + "c7 int64\n", + "c8 int64\n", + "c9 int64\n", + "c10 int64\n", + "c11 int64\n", + "SEVERITY object\n", + "c12 int64\n", + "c13 int64\n", + "c14 int64\n", + "ObjectType object\n", + "FormatStr object\n", + "Scope object\n", + "c15 int64\n", + "Description object\n", + "c16 float64\n", + "dtype: object\n", + "Show some data:\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
EventIDShortDescAbbreviationc1c2c3c4c5c6c7...SEVERITYc12c13c14ObjectTypeFormatStrScopec15Descriptionc16
064GID Address In ServiceSM_GID_IN_SERVICE0111001...Info13000PortGID Address In Service: prefix %(prefix)016x,g...Fabric Notification0New GID is connected to the FabricNaN
165GID Address Out of ServiceSM_GID_OUT_OF_SERVICE0111001...Warning13000PortGID Address Out of Service: prefix %(prefix)01...Fabric Notification0Existing GID is disconnected from the FabricNaN
266New MCast Group CreatedSM_MCAST_GROUP_CREATED0111001...Info13000PortNew MCast group is created: %(prefix)016x, %(p...Fabric Notification0New Multicast Group is created in SMNaN
367MCast Group DeletedSM_MCAST_GROUP_DELETED0111001...Info13000PortMcast group is deleted: %(prefix)016x, %(pkey)08xFabric Notification0Multicast Group is removed from SMNaN
4110Symbol ErrorPM_SYMBOLERROR1111001...Warning2003000PortSymbol-Error counter rate threshold exceeded. ...Hardware0Total number of minor link errors detected on ...NaN
\n", + "

5 rows × 24 columns

\n", + "
" + ], + "text/plain": [ + " EventID ShortDesc Abbreviation c1 c2 c3 \\\n", + "0 64 GID Address In Service SM_GID_IN_SERVICE 0 1 1 \n", + "1 65 GID Address Out of Service SM_GID_OUT_OF_SERVICE 0 1 1 \n", + "2 66 New MCast Group Created SM_MCAST_GROUP_CREATED 0 1 1 \n", + "3 67 MCast Group Deleted SM_MCAST_GROUP_DELETED 0 1 1 \n", + "4 110 Symbol Error PM_SYMBOLERROR 1 1 1 \n", + "\n", + " c4 c5 c6 c7 ... SEVERITY c12 c13 c14 ObjectType \\\n", + "0 1 0 0 1 ... Info 1 300 0 Port \n", + "1 1 0 0 1 ... Warning 1 300 0 Port \n", + "2 1 0 0 1 ... Info 1 300 0 Port \n", + "3 1 0 0 1 ... Info 1 300 0 Port \n", + "4 1 0 0 1 ... Warning 200 300 0 Port \n", + "\n", + " FormatStr Scope \\\n", + "0 GID Address In Service: prefix %(prefix)016x,g... Fabric Notification \n", + "1 GID Address Out of Service: prefix %(prefix)01... Fabric Notification \n", + "2 New MCast group is created: %(prefix)016x, %(p... Fabric Notification \n", + "3 Mcast group is deleted: %(prefix)016x, %(pkey)08x Fabric Notification \n", + "4 Symbol-Error counter rate threshold exceeded. ... Hardware \n", + "\n", + " c15 Description c16 \n", + "0 0 New GID is connected to the Fabric NaN \n", + "1 0 Existing GID is disconnected from the Fabric NaN \n", + "2 0 New Multicast Group is created in SM NaN \n", + "3 0 Multicast Group is removed from SM NaN \n", + "4 0 Total number of minor link errors detected on ... NaN \n", + "\n", + "[5 rows x 24 columns]" + ] + }, + "execution_count": 124, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Load policy file\n", + "col_Names=[\"EventID\", \"ShortDesc\", \"Abbreviation\", \"c1\", \"c2\", \"c3\", \"c4\", \"c5\", \"c6\", \"c7\", \"c8\", \"c9\", \"c10\", \"c11\", \"SEVERITY\", \"c12\", \"c13\", \"c14\", \"ObjectType\", \"FormatStr\", \"Scope\", \"c15\", \"Description\", \"c16\"]\n", + "policy = pd.read_csv('policy.csv', names=col_Names)\n", + "print(f'number of policies: {policy.shape[0]}')\n", + "print(f'attributes: {policy.columns}')\n", + "print(f'attributes types:\\n{policy.dtypes}')\n", + "\n", + "print(f'Show some data:')\n", + "policy.head(5)\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Data Statistics" + ] + }, + { + "cell_type": "code", + "execution_count": 125, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "328 158241\n", + "329 156219\n", + "1500 139948\n", + "65 136132\n", + "122 68089\n", + "67 51140\n", + "66 48771\n", + "392 39596\n", + "110 36288\n", + "123 30727\n", + "604 27977\n", + "112 26071\n", + "331 21525\n", + "116 18523\n", + "64 15061\n", + "917 13900\n", + "113 12578\n", + "332 11872\n", + "915 1068\n", + "393 1031\n", + "114 513\n", + "394 473\n", + "544 445\n", + "540 445\n", + "517 383\n", + "908 191\n", + "391 188\n", + "603 180\n", + "527 143\n", + "1502 133\n", + "702 114\n", + "134 82\n", + "516 62\n", + "907 56\n", + "403 20\n", + "145 19\n", + "539 11\n", + "529 5\n", + "605 5\n", + "115 5\n", + "352 5\n", + "1420 4\n", + "910 4\n", + "602 3\n", + "546 3\n", + "531 3\n", + "133 2\n", + "909 2\n", + "528 2\n", + "135 2\n", + "1503 1\n", + "1415 1\n", + "518 1\n", + "121 1\n", + "Name: EventID, dtype: int64" + ] + }, + "execution_count": 125, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "events_counts = events['EventID'].value_counts()\n", + "events_counts\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Add additional attributes from policy to events\n", + "## Find top events" + ] + }, + { + "cell_type": "code", + "execution_count": 99, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "EventID ShortDesc \n", + "328 Link is Up 158241\n", + "329 Link is Down 156219\n", + "1500 New Cable Detected 139948\n", + "65 GID Address Out of Service 136132\n", + "122 Congested Bandwidth (%) Threshold Reached 68089\n", + "67 MCast Group Deleted 51140\n", + "66 New MCast Group Created 48771\n", + "392 Module Temperature Threshold Reached 39596\n", + "110 Symbol Error 36288\n", + "123 Port Bandwidth (%) Threshold Reached 30727\n", + "604 Report Succeeded 27977\n", + "112 Link Downed 26071\n", + "331 Node is Down 21525\n", + "116 Port Xmit Discards 18523\n", + "64 GID Address In Service 15061\n", + "917 Critical Symbol BER reported 13900\n", + "113 Port Receive Errors 12578\n", + "332 Node is Up 11872\n", + "915 Critical BER reported 1068\n", + "393 Switch Module Added 1031\n", + "114 Port Receive Remote Physical Errors 513\n", + "394 Module status FAULT 473\n", + "544 Daily Report Mail Sent Failed 445\n", + "540 Daily Report Completed successfully 445\n", + "517 Fabric Health Report Error 383\n", + "908 Switch is Up 191\n", + "391 Switch Module Removed 188\n", + "603 Events Suppression 180\n", + "527 CPU utilization threshold reached 143\n", + "1502 Cable detected in a new location 133\n", + "702 Unhealthy IB Port 114\n", + "134 T4 Port Congested Bandwidth 82\n", + "516 Fabric Health Report Warning 62\n", + "907 Switch is Down 56\n", + "403 Device Pending Reboot 20\n", + "145 System Image GUID changed 19\n", + "539 DRBD TCP Connection Performance 11\n", + "352 Network Added 5\n", + "605 Report Failed 5\n", + "529 UFM standby server problem 5\n", + "115 Port Receive Switch Relay Errors 5\n", + "910 Director Switch is Up 4\n", + "1420 Cooling Device Communication Error 4\n", + "602 UFM Server Failover 3\n", + "546 Management interface is down 3\n", + "531 DRBD Bad Condition 3\n", + "909 Director Switch is Down 2\n", + "528 Fabric interface is down 2\n", + "121 VL15 Dropped 1\n", + "1415 Fault Primary DC 1\n", + "518 UFM-related process is down 1\n", + "1503 Duplicate Cable Detected 1\n", + "Name: ShortDesc, dtype: int64\n", + "EventID ShortDesc \n", + "328 Link is Up 158241\n", + "329 Link is Down 156219\n", + "1500 New Cable Detected 139948\n", + "Name: ShortDesc, dtype: int64\n", + "['Link is Up', 'Link is Down', 'New Cable Detected']\n" + ] + } + ], + "source": [ + "## Add additional attributes from policy to events\n", + "#events.Timestamps.value_counts()\n", + "columns = ['DeviceID', 'EventID', 'Timestamp']\n", + "events2 = events.reindex(columns=columns)\n", + "events1 = events2.loc[:,columns]\n", + "events1 = pd.merge(events, policy, on=\"EventID\")\n", + "events_ext = events1[['DeviceID', 'EventID', 'Timestamp', 'ShortDesc']]\n", + "#events_ext['Event ID'].value_counts()\n", + "\n", + "#events_ext.sort_values(by='Event ID', ascending=False).head(10)[['Device ID', 'Event ID', 'DESCRIPTION']]\n", + "#events_counts = events_ext['Event ID'].value_counts()\n", + "# Count and sort events by Event ID\n", + "#events_counts = events_ext.groupby('Event ID')['SHORT_DESC'].value_counts().sort_values(ascending=False)\n", + "events_counts = events_ext.groupby('EventID')['ShortDesc'].value_counts().sort_values(ascending=False)\n", + "#events_counts = events_counts.head(3).unstack(fill_value=0)\n", + "print(events_counts)\n", + "top_events = events_counts.head(3)\n", + "print(top_events)\n", + "top_events_list = top_events.index.get_level_values(1).tolist()\n", + "print(top_events_list)\n", + "\n", + "\n", + "# EventID ShortDesc \n", + "# 328 Link is Up 158241\n", + "# 329 Link is Down 156219\n", + "# 1500 New Cable Detected 139948\n", + "# 65 GID Address Out of Service 136132\n", + "# 122 Congested Bandwidth (%) Threshold Reached 68089\n", + "# 67 MCast Group Deleted 51140\n", + "# 66 New MCast Group Created 48771\n", + "# 392 Module Temperature Threshold Reached 39596\n", + "# 110 Symbol Error 36288\n", + "# 123 Port Bandwidth (%) Threshold Reached 30727\n", + "# 604 Report Succeeded 27977\n", + "# 112 Link Downed 26071\n", + "# 331 Node is Down 21525\n", + "# 116 Port Xmit Discards 18523\n", + "# 64 GID Address In Service 15061\n", + "# 917 Critical Symbol BER reported 13900\n", + "# 113 Port Receive Errors 12578\n", + "# 332 Node is Up 11872\n", + "# 915 Critical BER reported 1068\n", + "# 393 Switch Module Added 1031\n", + "# 114 Port Receive Remote Physical Errors 513\n", + "# 394 Module status FAULT 473\n", + "# 544 Daily Report Mail Sent Failed 445\n", + "# 540 Daily Report Completed successfully 445\n", + "# ...\n", + "# 329 Link is Down 156219\n", + "\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plot top events by timestamp" + ] + }, + { + "cell_type": "code", + "execution_count": 129, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "attributes: Index(['TimeStamp', 'TrapID', 'EventID', 'Severity', 'Category', 'ObjectType',\n", + " 'Source', 'ObjectID', 'Payload'],\n", + " dtype='object')\n", + "attributes types:\n", + "TimeStamp datetime64[ns]\n", + "TrapID int64\n", + "EventID int64\n", + "Severity object\n", + "Category object\n", + "ObjectType object\n", + "Source object\n", + "ObjectID object\n", + "Payload object\n", + "dtype: object\n", + "ts_max: 2022-10-29 14:32:14.401000, ts_min: 2021-08-10 00:01:01.596000\n" + ] + }, + { + "ename": "KeyError", + "evalue": "'TimeSamp'", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mKeyError\u001b[0m Traceback (most recent call last)", + "File \u001b[1;32m~\\AppData\\Local\\Packages\\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\\LocalCache\\local-packages\\Python310\\site-packages\\pandas\\core\\indexes\\base.py:3802\u001b[0m, in \u001b[0;36mIndex.get_loc\u001b[1;34m(self, key, method, tolerance)\u001b[0m\n\u001b[0;32m 3801\u001b[0m \u001b[39mtry\u001b[39;00m:\n\u001b[1;32m-> 3802\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49m_engine\u001b[39m.\u001b[39;49mget_loc(casted_key)\n\u001b[0;32m 3803\u001b[0m \u001b[39mexcept\u001b[39;00m \u001b[39mKeyError\u001b[39;00m \u001b[39mas\u001b[39;00m err:\n", + "File \u001b[1;32m~\\AppData\\Local\\Packages\\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\\LocalCache\\local-packages\\Python310\\site-packages\\pandas\\_libs\\index.pyx:138\u001b[0m, in \u001b[0;36mpandas._libs.index.IndexEngine.get_loc\u001b[1;34m()\u001b[0m\n", + "File \u001b[1;32m~\\AppData\\Local\\Packages\\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\\LocalCache\\local-packages\\Python310\\site-packages\\pandas\\_libs\\index.pyx:165\u001b[0m, in \u001b[0;36mpandas._libs.index.IndexEngine.get_loc\u001b[1;34m()\u001b[0m\n", + "File \u001b[1;32mpandas\\_libs\\hashtable_class_helper.pxi:5745\u001b[0m, in \u001b[0;36mpandas._libs.hashtable.PyObjectHashTable.get_item\u001b[1;34m()\u001b[0m\n", + "File \u001b[1;32mpandas\\_libs\\hashtable_class_helper.pxi:5753\u001b[0m, in \u001b[0;36mpandas._libs.hashtable.PyObjectHashTable.get_item\u001b[1;34m()\u001b[0m\n", + "\u001b[1;31mKeyError\u001b[0m: 'TimeSamp'", + "\nThe above exception was the direct cause of the following exception:\n", + "\u001b[1;31mKeyError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[129], line 9\u001b[0m\n\u001b[0;32m 5\u001b[0m \u001b[39mprint\u001b[39m(\u001b[39mf\u001b[39m\u001b[39m'\u001b[39m\u001b[39mts_max: \u001b[39m\u001b[39m{\u001b[39;00mts_max\u001b[39m}\u001b[39;00m\u001b[39m, ts_min: \u001b[39m\u001b[39m{\u001b[39;00mts_min\u001b[39m}\u001b[39;00m\u001b[39m'\u001b[39m)\n\u001b[0;32m 7\u001b[0m \u001b[39m#ts = events['EventID']\u001b[39;00m\n\u001b[0;32m 8\u001b[0m \u001b[39m#ts = events.index['Timestamp']\u001b[39;00m\n\u001b[1;32m----> 9\u001b[0m events[\u001b[39m'\u001b[39m\u001b[39mdate\u001b[39m\u001b[39m'\u001b[39m] \u001b[39m=\u001b[39m pd\u001b[39m.\u001b[39mto_datetime(events[\u001b[39m'\u001b[39;49m\u001b[39mTimeSamp\u001b[39;49m\u001b[39m'\u001b[39;49m])\n\u001b[0;32m 10\u001b[0m events11 \u001b[39m=\u001b[39m events\u001b[39m.\u001b[39mresample(pd\u001b[39m.\u001b[39mTimedelta(hours\u001b[39m=\u001b[39m\u001b[39m12\u001b[39m), on\u001b[39m=\u001b[39m\u001b[39m'\u001b[39m\u001b[39mdate\u001b[39m\u001b[39m'\u001b[39m)\u001b[39m.\u001b[39msum()\n", + "File \u001b[1;32m~\\AppData\\Local\\Packages\\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\\LocalCache\\local-packages\\Python310\\site-packages\\pandas\\core\\frame.py:3807\u001b[0m, in \u001b[0;36mDataFrame.__getitem__\u001b[1;34m(self, key)\u001b[0m\n\u001b[0;32m 3805\u001b[0m \u001b[39mif\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mcolumns\u001b[39m.\u001b[39mnlevels \u001b[39m>\u001b[39m \u001b[39m1\u001b[39m:\n\u001b[0;32m 3806\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_getitem_multilevel(key)\n\u001b[1;32m-> 3807\u001b[0m indexer \u001b[39m=\u001b[39m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49mcolumns\u001b[39m.\u001b[39;49mget_loc(key)\n\u001b[0;32m 3808\u001b[0m \u001b[39mif\u001b[39;00m is_integer(indexer):\n\u001b[0;32m 3809\u001b[0m indexer \u001b[39m=\u001b[39m [indexer]\n", + "File \u001b[1;32m~\\AppData\\Local\\Packages\\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\\LocalCache\\local-packages\\Python310\\site-packages\\pandas\\core\\indexes\\base.py:3804\u001b[0m, in \u001b[0;36mIndex.get_loc\u001b[1;34m(self, key, method, tolerance)\u001b[0m\n\u001b[0;32m 3802\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_engine\u001b[39m.\u001b[39mget_loc(casted_key)\n\u001b[0;32m 3803\u001b[0m \u001b[39mexcept\u001b[39;00m \u001b[39mKeyError\u001b[39;00m \u001b[39mas\u001b[39;00m err:\n\u001b[1;32m-> 3804\u001b[0m \u001b[39mraise\u001b[39;00m \u001b[39mKeyError\u001b[39;00m(key) \u001b[39mfrom\u001b[39;00m \u001b[39merr\u001b[39;00m\n\u001b[0;32m 3805\u001b[0m \u001b[39mexcept\u001b[39;00m \u001b[39mTypeError\u001b[39;00m:\n\u001b[0;32m 3806\u001b[0m \u001b[39m# If we have a listlike key, _check_indexing_error will raise\u001b[39;00m\n\u001b[0;32m 3807\u001b[0m \u001b[39m# InvalidIndexError. Otherwise we fall through and re-raise\u001b[39;00m\n\u001b[0;32m 3808\u001b[0m \u001b[39m# the TypeError.\u001b[39;00m\n\u001b[0;32m 3809\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_check_indexing_error(key)\n", + "\u001b[1;31mKeyError\u001b[0m: 'TimeSamp'" + ] + } + ], + "source": [ + "print(f'attributes: {events.columns}')\n", + "print(f'attributes types:\\n{events.dtypes}')\n", + "ts_max = events['TimeStamp'].max()\n", + "ts_min = events['TimeStamp'].min()\n", + "print(f'ts_max: {ts_max}, ts_min: {ts_min}')\n", + "\n", + "#ts = events['EventID']\n", + "#ts = events.index['Timestamp']\n", + "events['date'] = pd.to_datetime(events['TimeStamp'])\n", + "events11 = events.resample(pd.Timedelta(hours=12), on='date').sum()" + ] + }, + { + "cell_type": "code", + "execution_count": 114, + "metadata": {}, + "outputs": [ + { + "ename": "KeyError", + "evalue": "\"None of ['Timestamp'] are in the columns\"", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mKeyError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[114], line 6\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[39m## Plot top events by timestamp\u001b[39;00m\n\u001b[0;32m 2\u001b[0m \n\u001b[0;32m 3\u001b[0m \u001b[39m# plot the results with headers for specific events\u001b[39;00m\n\u001b[0;32m 4\u001b[0m \u001b[39m#print(f'columns: {events.columns}')\u001b[39;00m\n\u001b[0;32m 5\u001b[0m \u001b[39m#print(f'index: {events.index}')\u001b[39;00m\n\u001b[1;32m----> 6\u001b[0m events_ext_ts_idx \u001b[39m=\u001b[39m events\u001b[39m.\u001b[39;49mset_index(\u001b[39m'\u001b[39;49m\u001b[39mTimestamp\u001b[39;49m\u001b[39m'\u001b[39;49m, inplace\u001b[39m=\u001b[39;49m\u001b[39mFalse\u001b[39;49;00m)\n\u001b[0;32m 7\u001b[0m events_ext_ts_idx \u001b[39m=\u001b[39m events\n\u001b[0;32m 8\u001b[0m \u001b[39m#events_ext_ts_idx = events_ext.set_index('Timestamp', inplace=False)\u001b[39;00m\n", + "File \u001b[1;32m~\\AppData\\Local\\Packages\\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\\LocalCache\\local-packages\\Python310\\site-packages\\pandas\\util\\_decorators.py:331\u001b[0m, in \u001b[0;36mdeprecate_nonkeyword_arguments..decorate..wrapper\u001b[1;34m(*args, **kwargs)\u001b[0m\n\u001b[0;32m 325\u001b[0m \u001b[39mif\u001b[39;00m \u001b[39mlen\u001b[39m(args) \u001b[39m>\u001b[39m num_allow_args:\n\u001b[0;32m 326\u001b[0m warnings\u001b[39m.\u001b[39mwarn(\n\u001b[0;32m 327\u001b[0m msg\u001b[39m.\u001b[39mformat(arguments\u001b[39m=\u001b[39m_format_argument_list(allow_args)),\n\u001b[0;32m 328\u001b[0m \u001b[39mFutureWarning\u001b[39;00m,\n\u001b[0;32m 329\u001b[0m stacklevel\u001b[39m=\u001b[39mfind_stack_level(),\n\u001b[0;32m 330\u001b[0m )\n\u001b[1;32m--> 331\u001b[0m \u001b[39mreturn\u001b[39;00m func(\u001b[39m*\u001b[39margs, \u001b[39m*\u001b[39m\u001b[39m*\u001b[39mkwargs)\n", + "File \u001b[1;32m~\\AppData\\Local\\Packages\\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\\LocalCache\\local-packages\\Python310\\site-packages\\pandas\\core\\frame.py:6012\u001b[0m, in \u001b[0;36mDataFrame.set_index\u001b[1;34m(self, keys, drop, append, inplace, verify_integrity)\u001b[0m\n\u001b[0;32m 6009\u001b[0m missing\u001b[39m.\u001b[39mappend(col)\n\u001b[0;32m 6011\u001b[0m \u001b[39mif\u001b[39;00m missing:\n\u001b[1;32m-> 6012\u001b[0m \u001b[39mraise\u001b[39;00m \u001b[39mKeyError\u001b[39;00m(\u001b[39mf\u001b[39m\u001b[39m\"\u001b[39m\u001b[39mNone of \u001b[39m\u001b[39m{\u001b[39;00mmissing\u001b[39m}\u001b[39;00m\u001b[39m are in the columns\u001b[39m\u001b[39m\"\u001b[39m)\n\u001b[0;32m 6014\u001b[0m \u001b[39mif\u001b[39;00m inplace:\n\u001b[0;32m 6015\u001b[0m frame \u001b[39m=\u001b[39m \u001b[39mself\u001b[39m\n", + "\u001b[1;31mKeyError\u001b[0m: \"None of ['Timestamp'] are in the columns\"" + ] + } + ], + "source": [ + "## Plot top events by timestamp\n", + "\n", + "# plot the results with headers for specific events\n", + "#print(f'columns: {events.columns}')\n", + "#print(f'index: {events.index}')\n", + "#events_ext_ts_idx = events.set_index('Timestamp', inplace=False)\n", + "events_ext_ts_idx = events\n", + "#events_ext_ts_idx = events_ext.set_index('Timestamp', inplace=False)\n", + "fig, ax = plt.subplots()\n", + "#top_events_list = [\"Port Xmit Constraint Errors\", \"Link is Down\", \"Link is Up\"]\n", + "for event in top_events_list:\n", + "#for event in [117, 328, 329]:\n", + "# filtered_events = events_ext_ts_idx.loc[events_ext_ts_idx['Event ID'] == event]\n", + " filtered_events = events_ext_ts_idx.loc[events_ext_ts_idx['EventID'] == event]\n", + "# print(f'attributes: {events_328.columns}')\n", + "# print(f'attributes types:\\n{events_328.dtypes}')\n", + "# print(events_328.loc[events_328['Event ID'] == event])\n", + " grouped_events = filtered_events.groupby(['Timestamp', 'EventID']).size().unstack(fill_value=0)\n", + " print(f'attributes: {grouped_events.columns}')\n", + " print(f'attributes: {grouped_events.index}')\n", + "# grouped_events = grouped_events.resample(pd.Timedelta(hours=48)).sum()\n", + "# grouped_events.plot(ax=ax,style=\"-o\") parse_dates=['TimeStamp']\n", + "# grouped_events = grouped_events.set_index('Timestamp', inplace=False)\n", + " \n", + " #grouped_events['date'] = pd.to_datetime(grouped_events.index)\n", + " #grouped_events = grouped_events.set_index('date')\n", + " grouped_events = grouped_events.resample(pd.Timedelta(hours=12)).sum()\n", + " grouped_events.plot(ax=ax)\n", + " plt.xlabel(\"Hours of Day\")\n", + " plt.ylabel(\"Events Counter\")\n", + "plt.legend()\n", + "plt.show()\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# TODO: find noisy devices from top events" + ] + }, + { + "cell_type": "code", + "execution_count": 191, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Device ID SHORT_DESC \n", + "DSM082031002003:dsm08-0102-0310-03ib0 Link is Down 590\n", + " Link is Up 590\n", + "dsm08-0102-0310-03ib0 / 4 Port Xmit Constraint Errors 114\n", + "dsm08-0102-0310-03ib0 / 1 Port Xmit Constraint Errors 114\n", + "dsm08-0102-0310-03ib0 / 8 Port Xmit Constraint Errors 114\n", + " ... \n", + "dsm08-0102-0310-03ib0 / 26 Unhealthy IB Port 1\n", + "dsm08-0102-0310-03ib0 / 27 Unhealthy IB Port 1\n", + "dsm08-0102-0310-03ib0 / 38 Symbol Error 1\n", + "dsm08-0102-0310-03ib0 / 32 Port Bandwidth (%) Threshold Reached 1\n", + "dsm08-0102-0310-03ib0 / 35 Symbol Error 1\n", + "Name: SHORT_DESC, Length: 97, dtype: int64\n", + "Device ID SHORT_DESC \n", + "DSM082031002003:dsm08-0102-0310-03ib0 Link is Down 590\n", + " Link is Up 590\n", + "dsm08-0102-0310-03ib0 / 4 Port Xmit Constraint Errors 114\n", + "Name: SHORT_DESC, dtype: int64\n", + "['DSM082031002003:dsm08-0102-0310-03ib0', 'DSM082031002003:dsm08-0102-0310-03ib0', 'dsm08-0102-0310-03ib0 / 4']\n" + ] + } + ], + "source": [ + "device_counts = events_ext.groupby('Device ID')['SHORT_DESC'].value_counts().sort_values(ascending=False)\n", + "#events_counts = events_counts.head(3).unstack(fill_value=0)\n", + "print(device_counts)\n", + "top_devices = device_counts.head(3)\n", + "print(top_devices)\n", + "top_devices_list = top_devices.index.get_level_values(0).tolist()\n", + "print(top_devices_list)" + ] + }, + { + "cell_type": "code", + "execution_count": 194, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjsAAAHBCAYAAACPN3q5AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/P9b71AAAACXBIWXMAAA9hAAAPYQGoP6dpAAC/R0lEQVR4nOydd5wUhfn/PzPbbq/sVa4AR+9IEwOeBTEgRYONGFQUFYI/EjQKEexoYhSiJBYCtq9iCYolYkFEkSKigLQDBDy6tLsDrtctM8/vjym7s+VuD/b68369VtmZ2Zlnd+ZmP/tUgYgIDMMwDMMwLRSxsQ1gGIZhGIapT1jsMAzDMAzTomGxwzAMwzBMi4bFDsMwDMMwLRoWOwzDMAzDtGhY7DAMwzAM06JhscMwDMMwTIuGxQ7DMAzDMC0ac2Mb0BSQZRmnTp1CXFwcBEFobHMYhmEYhgkDIkJZWRnatm0LUQztv2GxA+DUqVPIzMxsbDMYhmEYhjkHjh8/jvbt24dcz2IHQFxcHADlw3I4HI1sDcMwDMMw4VBaWorMzEz9ezwULHYAPXTlcDhY7DAMwzBMM6O2FBROUGYYhmEYpkXDYodhGIZhmBYNix2GYRiGYVo0nLPDMAAkSYLb7W5sMxiGYRgfLBYLTCbTee+HxQ7TqiEi5OXlobi4uLFNYRiGYYKQkJCA9PT08+qDx2KHadVoQic1NRXR0dHcVJJhGKaJQESorKzE6dOnAQAZGRnnvC8WO0yrRZIkXegkJyc3tjkMwzCMH3a7HQBw+vRppKamnnNIixOUmVaLlqMTHR3dyJYwDMMwodDu0eeTV8lih2n1cOiKYRim6RKJezSLHYZhGIZhWjQsdhiGYVohb731FhISEhrbDKYZ06lTJ7zwwgv6c0EQ8OmnnzaaPTXBYodhmiF33nknBEGAIAiwWCxIS0vDVVddhTfffBOyLOvb7dy5E9deey1SU1MRFRWFTp06YcKECXp1w9GjRyEIAkwmE06ePGk4Rm5uLsxmMwRBwNGjR/Xlx44dwzXXXIPo6GikpqZi1qxZ8Hg8htcuWbIEAwYMQHR0NDIyMjB58mQUFBTo619//XVcfvnlSExMRGJiIkaOHImffvrJsA8iwpw5c5CRkQG73Y6RI0fiwIEDhm2uvfZadOjQAVFRUcjIyMDtt9+OU6dO6eurq6tx5513ol+/fjCbzbj++uuDfp7r1q3DhRdeCJvNhm7duuGtt94K2GbhwoXo1KkToqKiMHToUIO9hYWFuPfee9GzZ0/Y7XZ06NABf/nLX1BSUmLYRzifXW22vPzyy+jfv78+yy8rKwtfffVV0PfV2IRzDvfv34/rrrsOKSkpcDgcuOyyy7B27dpa971r1y5cfvnliIqKQmZmJp599lnD+j179mD8+PHo1KkTBEEwfCn7Eonzei72ffLJJ7jooouQkJCAmJgYDBw4EO+++27ANqNGjUJycjIEQUB2dnbAcaqrqzF9+nQkJycjNjYW48ePR35+foPYt2XLFtx99921HsuXwsJCTJw4EQ6HAwkJCZgyZQrKy8vrtI9zghgqKSkhAFRSUtLYpjANSFVVFe3du5eqqqoa25Q6c8cdd9CYMWMoNzeXTpw4Qdu2baOnn36aYmNjaezYseR2u+n06dOUnJxMd9xxB23fvp0OHz5Ma9asofvvv58OHz5MRERHjhwhAJSZmUnPPPOM4Rhz586lDh06EAA6cuQIERF5PB664IILaOTIkbRjxw5asWIFpaSk0MMPP6y/bsOGDSSKIr344ot0+PBh+v7776lv3750ww036NvceuuttHDhQtqxYwft27eP7rzzToqPj6cTJ07o28ybN4/i4+Pp008/pZ07d9K1115LnTt3Npyvf//737Rx40Y6evQo/fDDD5SVlUVZWVn6+vLycpo2bRq99tprNHr0aLruuusCPsvDhw9TdHQ0zZw5k/bu3UsLFiwgk8lEK1eu1LdZunQpWa1WevPNN2nPnj00depUSkhIoPz8fCIi2r17N9144430+eef08GDB2n16tXUvXt3Gj9+vL6PUJ/drAf+WidbPv/8c/ryyy9p//79lJOTQ4888ghZLBb6+eefa75o/Fi8eDHFx8fX6TV1JZxz2L17d7r66qtp586dtH//fvrzn/9M0dHRlJubG3K/JSUllJaWRhMnTqSff/6Z3n//fbLb7fTqq6/q2/z000/0wAMP0Pvvv0/p6en0/PPPB+wnEuf1XO1bu3YtffLJJ7R37146ePAgvfDCCwHn+p133qG//e1v9PrrrxMA2rFjR8Cxpk2bRpmZmbR69WraunUrXXzxxXTJJZc0iH3+AKBly5bVeOwxY8bQgAEDaNOmTfT9999Tt27d6JZbbqnxNTXdq8P9/maxQyx2WivNXewE++JevXo1AaDXX3+dli1bRmazmdxud8j9aGLnscceo+7duxvW9ejRgx5//HGD2FmxYgWJokh5eXn6di+//DI5HA5yOp1ERPTcc89Rly5dDPt66aWXqF27diHt8Hg8FBcXR2+//TYREcmyTOnp6fTcc8/p2xQXF5PNZqP3338/5H4+++wzEgSBXC5XwLpQn9ns2bOpb9++hmUTJkyg0aNH68+HDBlC06dP159LkkRt27aluXPnhrTlww8/JKvVqn/+wT67F+c9SY64WCopKgjblmAkJibS//3f/9W4zeLFiykzM5Psdjtdf/31NH/+fIPYyc7OpuHDh1NsbCzFxcXRhRdeSFu2bNFfGx8fT1988QX16NGD7HY7jR8/nioqKuitt96ijh07UkJCAt17773k8XiIKLxzeObMGQJA69ev17cpLS0lALRq1aqQ72XRokWUmJioX3NERA8++CD17Nkz6PYdO3YMKnYicV4jYZ/GoEGD6LHHHgtYrv2d+oud4uJislgs9NFHH+nL9u3bRwBo48aN9W6f/+cKgBYtWkRjxoyhqKgo6ty5s8G2vXv3EgD9uiIi+uqrr0gQBDp58mTI40ZC7HAYi2F8ICJUujyN8lDuFefHb3/7WwwYMACffPIJ0tPT4fF4sGzZslr3fe2116KoqAgbNmwAAGzYsAFFRUUYN26cYbuNGzeiX79+SEtL05eNHj0apaWl2LNnDwAgKysLx48fx4oVK0BEyM/Px8cff4yrr7465PErKyvhdruRlJQEADhy5Ajy8vIwcuRIfZv4+HgMHToUGzduDLqPwsJCLFmyBJdccgksFkuN79f/PfkeR3tP2nFcLhe2bdtm2EYURYwcOTKkLQBQUlICh8MBs9msH8f/sxt1RRZKy8rx867ssGzxR5IkLF26FBUVFcjKytKX33nnnRg+fLj+fPPmzZgyZQruueceZGdn48orr8Q//vEPw74mTpyI9u3bY8uWLdi2bRseeughw+dYWVmJl156CUuXLsXKlSuxbt063HDDDVixYgVWrFiBd999F6+++io+/vhjAOGdw+TkZPTs2RPvvPMOKioq4PF48OqrryI1NRWDBw8O+dlu3LgRw4YNg9VqNXxOOTk5KCoqCvk6XyJ1XiNhHxFh9erVyMnJwbBhw8KyHwC2bdsGt9tteA+9evVChw4danwP9Wnf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cpo2bRq99tprNHr0aMPcJg1t+ObMmTNp7969tGDBgqCDQK1WK7355pu0Z88emjp1KiUkJFB+fj4REe3evZtuvPFG+vzzz+ngwYO0evVq6t69O40fP17fh/9n98UXn1NKUgI9fM9dRCe3U2VZUVi2fP755/Tll1/S/v37KScnhx555BGyWCz0888/h3XtaASbdxVpwjmH3bt3p6uvvpp27txJ+/fvpz//+c8UHR1d43ynkpISSktLo4kTJ9LPP/9M77//PtntdsMgy59++okeeOABev/99yk9Pd0ww0kjEuf1XO1bu3YtffLJJ7R37146ePAgvfDCCwHn+p133qG//e1v9Prrr4ecYTdt2jTKzMyk1atX09atW+niiy+mSy65pEHs8wcALVu2rMZjjxkzhgYMGECbNm2i77//nrp168aDQBsKFjutk+YudoJ9ca9evZoA0Ouvv07Lli0js9kcMKTTF03sPPbYY9S9e3fDuh49etDjjz9uEDsrVqwgURT1qdBERC+//DI5HA5yOp1ERPTcc89Rly5dDPt66aWXqF27diHt8Hg8FBcXR2+//TYRKQMk09PT6bnnntO3KS4uJpvNRu+//37I/Xz22WckCAK5XK6AdaE+s9mzZ1Pfvn0NyyZMmECjR4/Wnw8ZMoSmT5+uP5ckidq2bUtz584NacuHH35IVqtV//z9Pzu3y0kvz32EHHGx5DyymSpKzoZlSzASExPp//7v/2rcZvHixZSZmUl2u52uv/56wyBQIqLs7GwaPnw4xcbGUlxcHF144YW0ZcsW/bXx8fH0xRdf6INlx48fTxUVFfTWW29Rx44dKSEhge699159Gng45/DMmTMEgNavX69vU1paSgBo1apVId/LokWLKDExUb/miIgefPBB6tmzZ9Dt/QdWakTivEbCPo1BgwbRY489FrA81MDe4uJislgs9NFHH+nL9u3bRwBo48aN9W5fsEGgixYtojFjxlBUVBR17tzZYNvevXsJgH5dERF99dVXJAgCnTx5MuRxIyF2OIzFMC2I3/72txgwYAA++eQTpKenw+PxYNmyZbWWNV977bUoKirChg0bAAAbNmxAUVERxo0bZ9hu48aN6NevH9LS0vRlo0ePRmlpKfbs2QMAyMrKwvHjx7FixQoQEfLz8/Hxxx/j6quvDnn8yspKuN1uJCUlAQCOHDmCvLw8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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "## Plot top devices by timestamp\n", + "\n", + "# plot the results with headers for specific events\n", + "events_ext_ts_idx = events_ext.set_index('Timestamps', inplace=False)\n", + "fig, ax = plt.subplots()\n", + "#top_events_list = [\"Port Xmit Constraint Errors\", \"Link is Down\", \"Link is Up\"]\n", + "for device in top_devices_list:\n", + "#for event in [117, 328, 329]:\n", + "# filtered_events = events_ext_ts_idx.loc[events_ext_ts_idx['Event ID'] == event]\n", + " filtered_events = events_ext_ts_idx.loc[events_ext_ts_idx['Device ID'] == device]\n", + "# print(f'attributes: {events_328.columns}')\n", + "# print(f'attributes types:\\n{events_328.dtypes}')\n", + "# print(events_328.loc[events_328['Event ID'] == event])\n", + "\n", + "# grouped_events = filtered_events.groupby(['Timestamps', 'SHORT_DESC', 'Device ID']).size().unstack(fill_value=0)\n", + " grouped_events = filtered_events.groupby(['Timestamps', 'Device ID']).size().unstack(fill_value=0)\n", + "# print(f'attributes: {grouped_events.columns}')\n", + "# grouped_events = grouped_events.resample(pd.Timedelta(hours=48)).sum()\n", + "# grouped_events.plot(ax=ax,style=\"-o\")\n", + " grouped_events = grouped_events.resample(pd.Timedelta(hours=24)).sum()\n", + " grouped_events.plot(ax=ax)\n", + " plt.xlabel(\"Hours of Day\")\n", + " plt.ylabel(\"Events Counter\")\n", + "plt.legend()\n", + "plt.show()\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Top 10" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "## Groupby timestamp\n", + "#events_byevents = exents_ext.groupby('Timestamps')['Event ID'].sum()\n", + "# 329, 328\n", + "#events_byevents = events_ext.groupby(events_ext['Timestamps'].dt.strftime('%Y-%m-%d'))['Event ID'].size().unstack(fill_value=0)\n", + "#df_resampled = events.resample('D').sum()\n", + "#daily_quality.plot(kind='bar')\n", + "#print(events_byevents.head(5))\n", + "#events_byevents.plot.hist(bins=10)\n", + "#plt.show()\n", + "#events_byevents.plot()\n", + "\n", + "# Create DF with specific events\n", + "events_328 = events_ext.loc[events_ext['Event ID'] == 328]\n", + "events_329 = events_ext.loc[events_ext['Event ID'] == 329]\n", + "grouped = events_328.groupby(['Timestamps', 'Event ID']).size().unstack(fill_value=0)\n", + "#events_328.plot()\n", + "## Groupby timestamp\n", + "#events_byevents = exents_ext.groupby('Timestamps')['Event ID'].sum()\n", + "# 329, 328\n", + "#events_byevents = events_ext.groupby(events_ext['Timestamps'].dt.strftime('%Y-%m-%d'))['Event ID'].size().unstack(fill_value=0)\n", + "#df_resampled = events.resample('D').sum()\n", + "#daily_quality.plot(kind='bar')\n", + "#print(events_byevents.head(5))\n", + "#events_byevents.plot.hist(bins=10)\n", + "#plt.show()\n", + "#events_byevents.plot()\n", + "\n", + "# Create DF with specific events\n", + "events_328 = events_ext.loc[events_ext['Event ID'] == 328]\n", + "events_329 = events_ext.loc[events_ext['Event ID'] == 329]\n", + "print(f'attributes: {events_328.columns}')\n", + "print(f'attributes types:\\n{events_328.dtypes}')\n", + "\n", + "#grouped = events_328.groupby(['Timestamps', 'Event ID']).size().unstack(fill_value=0)\n", + "#events_328.plot()\n", + "#grouped = events_ext.groupby(['Timestamps', 'Event ID']).size().unstack(fill_value=0)\n", + "\n", + "# plot the results with headers for specific events\n", + "events_ext_ts_idx = events_ext.set_index('Timestamps', inplace=False)\n", + "#events_328 = events_ext_ts_idx.loc[events_ext_ts_idx['Event ID'] == 328]\n", + "#print(f'attributes: {events_ext_ts_idx.columns}')\n", + "#print(f'attributes types:\\n{events_ext_ts_idx.dtypes}')\n", + "fig, ax = plt.subplots()\n", + "#for event in [\"Link is Down\", \"Link is Up\"]:\n", + "for event in [328, 329]:\n", + " events_328 = events_ext_ts_idx.loc[events_ext_ts_idx['Event ID'] == event]\n", + " print(f'attributes: {events_328.columns}')\n", + " print(f'attributes types:\\n{events_328.dtypes}')\n", + "# print(events_328.loc[events_328['Event ID'] == event])\n", + " grouped = events_328.groupby(['Timestamps', 'Event ID']).size().unstack(fill_value=0)\n", + "# grouped = grouped.resample(pd.Timedelta(hours=2)).mean()\n", + " grouped = grouped.resample(pd.Timedelta(days=1)).max()\n", + " print(grouped)\n", + " grouped.plot(ax=ax)\n", + "# events_328.loc[events_328['Event ID'] == event].plot(ax=ax, x = events_328.index[-1], label=str(event))\n", + "# ax.text(events_328.index[-1], events_328.loc[events_328['Event ID'] == event], str(event), ha='left', va='center')\n", + "# ax.text(events_328.index[-1], events_328[event].iloc[-1], event, ha='left', va='center')\n", + "plt.legend()\n", + "plt.show()\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "#print(events_328)\n", + "\n", + "# set the index to the timestamp column\n", + "#events_ext.set_index('Timestamps', inplace=True)\n", + "events_ext_ts_idx = events_ext.set_index('Timestamps', inplace=False)\n", + "\n", + "# group the dataframe by 15 minute intervals and calculate the mean\n", + "print(f'attributes: {events_ext_ts_idx.columns}')\n", + "print(f'attributes types:\\n{events_ext_ts_idx.dtypes}')\n", + "events_byevents = events_ext_ts_idx.resample(pd.Timedelta(hours=2)).mean()\n", + "print(f'attributes: {events_byevents.columns}')\n", + "print(f'attributes types:\\n{events_byevents.dtypes}')\n", + "# group the dataframe by timestamp and event\n", + "#grouped = events_ext.groupby(['Timestamps', 'SHORT_DESC']).size().unstack(fill_value=0)\n", + "\n", + "#grouped = events_byevents.groupby(['Timestamps', 'SHORT_DESC']).size().unstack(fill_value=0)\n", + "grouped = events_byevents.groupby(['Timestamps', 'Event ID']).size().unstack(fill_value=0)\n", + "\n", + "#print(f'attributes: {grouped.columns}')\n", + "#print(f'attributes types:\\n{grouped.dtypes}')\n", + "#print(grouped)\n", + "# plot the results with headers for specific events\n", + "fig, ax = plt.subplots()\n", + "#for event in [\"Link is Down\", \"Link is Up\"]:\n", + "for event in ['328', '329']:\n", + " grouped[event].plot(ax=ax, label=event)\n", + " ax.text(grouped.index[-1], grouped[event].iloc[-1], event, ha='left', va='center')\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Device IDEvent IDTimestamps
1272dsm08-0102-0310-03ib0 / 57022022-09-25 05:27:50.577
4020dsm08-0102-0310-03ib0 / 287022022-10-10 22:07:49.973
246dsm08-0102-0310-03ib0 / 17022022-11-05 03:59:47.514
504dsm08-0102-0310-03ib0 / 27022022-11-05 04:00:15.854
505dsm08-0102-0310-03ib0 / 27022022-09-28 02:38:11.455
506dsm08-0102-0310-03ib0 / 27022022-09-25 17:40:16.021
4012dsm08-0102-0310-03ib0 / 277022022-10-10 22:07:49.972
761dsm08-0102-0310-03ib0 / 37022022-11-05 04:00:15.855
762dsm08-0102-0310-03ib0 / 37022022-09-25 05:27:40.915
1522dsm08-0102-0310-03ib0 / 67022022-11-05 04:00:15.861
\n", + "
" + ], + "text/plain": [ + " Device ID Event ID Timestamps\n", + "1272 dsm08-0102-0310-03ib0 / 5 702 2022-09-25 05:27:50.577\n", + "4020 dsm08-0102-0310-03ib0 / 28 702 2022-10-10 22:07:49.973\n", + "246 dsm08-0102-0310-03ib0 / 1 702 2022-11-05 03:59:47.514\n", + "504 dsm08-0102-0310-03ib0 / 2 702 2022-11-05 04:00:15.854\n", + "505 dsm08-0102-0310-03ib0 / 2 702 2022-09-28 02:38:11.455\n", + "506 dsm08-0102-0310-03ib0 / 2 702 2022-09-25 17:40:16.021\n", + "4012 dsm08-0102-0310-03ib0 / 27 702 2022-10-10 22:07:49.972\n", + "761 dsm08-0102-0310-03ib0 / 3 702 2022-11-05 04:00:15.855\n", + "762 dsm08-0102-0310-03ib0 / 3 702 2022-09-25 05:27:40.915\n", + "1522 dsm08-0102-0310-03ib0 / 6 702 2022-11-05 04:00:15.861" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "top_10 = events.sort_values(by='Event ID', ascending=False).head(10)\n", + "top_10" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "DSM082031002003:dsm08-0102-0310-03ib0 1180\n", + "dsm08-0102-0310-03ib0 / 2 260\n", + "dsm08-0102-0310-03ib0 / 1 257\n", + "dsm08-0102-0310-03ib0 / 4 256\n", + "dsm08-0102-0310-03ib0 / 8 256\n", + "dsm08-0102-0310-03ib0 / 5 255\n", + "dsm08-0102-0310-03ib0 / 3 254\n", + "dsm08-0102-0310-03ib0 / 6 251\n", + "dsm08-0102-0310-03ib0 / 7 249\n", + "MT4123:dsm08-0102-0310-03ib0 208\n", + "dsm08-0102-0310-03ib1:dsm08-0102-0310-03ib0 66\n", + "dsm08-0102-0310-07ib1:dsm08-0102-0310-03ib0 66\n", + "dsm08-0102-0310-05ib1:dsm08-0102-0310-03ib0 66\n", + "dsm08-0102-0310-01ib1:dsm08-0102-0310-03ib0 66\n", + "dsm08-0102-0310-08ib1:dsm08-0102-0310-03ib0 48\n", + "dsm08-0102-0310-04ib1:dsm08-0102-0310-03ib0 44\n", + "dsm08-0102-0310-06ib1:dsm08-0102-0310-03ib0 44\n", + "dsm08-0102-0310-02ib1:dsm08-0102-0310-03ib0 44\n", + "dsm08-0102-0310-04ufm:dsm08-0102-0310-03ib0 22\n", + "dsm08-0102-0310-03ib0 / 40 12\n", + "dsm08-0102-0310-03ib0 / 0 11\n", + "dsm08-0102-0310-03ib0 / 39 11\n", + "dsm08-0102-0310-03ib0 / 35 9\n", + "dsm08-0102-0310-03ib0 / 28 8\n", + "dsm08-0102-0310-03ib0 / 27 8\n", + "dsm08-0102-0310-03ib0 / 26 8\n", + "dsm08-0102-0310-03ib0 / 34 7\n", + "dsm08-0102-0310-03ib0 / 21 7\n", + "dsm08-0102-0310-03ib0 / 22 7\n", + "dsm08-0102-0310-03ib0 / 23 7\n", + "dsm08-0102-0310-03ib0 / 36 6\n", + "dsm08-0102-0310-03ib0 / 38 6\n", + "dsm08-0102-0310-03ib0 / 20 6\n", + "dsm08-0102-0310-03ib0 / 37 5\n", + "dsm08-0102-0310-03ib0 / 25 5\n", + "dsm08-0102-0310-03ib0 / 24 5\n", + "dsm08-0102-0310-03ib0 / 29 4\n", + "dsm08-0102-0310-03ib0 / 30 4\n", + "dsm08-0102-0310-03ib0 / 33 3\n", + "dsm08-0102-0310-03ib0 / 31 3\n", + "dsm08-0102-0310-03ib0 / 32 1\n", + "Name: Device ID, dtype: int64" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "events['Device ID'].value_counts()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Timestamps\n", + "2022-07-20 00:22:21.553 112\n", + "2022-07-20 00:22:21.554 112\n", + "2022-07-20 00:22:21.555 112\n", + "2022-07-20 00:22:21.556 112\n", + "2022-07-20 00:22:21.560 112\n", + "Name: Event ID, dtype: int64\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkQAAAGdCAYAAADzOWwgAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/P9b71AAAACXBIWXMAAA9hAAAPYQGoP6dpAAApYUlEQVR4nO3de1SVdb7H8c9GBDEFvMGWRKW8XzOdkEk7p5EjKmOmnnNGIzXj1LHBSaPMXJU1UxNeTqY1pdOZUXONpbmONTM6WYimXVBHEk0r1FLJuOhksMUSEX7nj4antpSX7YYN/t6vtZ612M/vx8P3911cPuvZz/PgMsYYAQAAWCwo0AUAAAAEGoEIAABYj0AEAACsRyACAADWIxABAADrEYgAAID1CEQAAMB6BCIAAGC94EAX0BBUVVWpoKBAzZs3l8vlCnQ5AADgIhhjdPLkScXExCgo6PzngAhEF6GgoECxsbGBLgMAAPjg888/V7t27c47h0B0EZo3by7p24aGh4cHuBoAAHAxPB6PYmNjnb/j50MgugjVb5OFh4cTiAAAaGAu5nIXLqoGAADWIxABAADrEYgAAID1CEQAAMB6BCIAAGA9AhEAALAegQgAAFiPQAQAAKxHIAIAANYjEAEAAOsRiAAAgPUIRAAAwHoEIgAAYD0CEQAAsF5woAtAw9TxofWBLuGSHZ6THOgSAAD1FGeIAACA9QhEAADAegQiAABgPQIRAACwHoEIAABYj0AEAACsRyACAADWIxABAADrEYgAAID1CEQAAMB6BCIAAGA9AhEAALAegQgAAFiPQAQAAKxHIAIAANYjEAEAAOsRiAAAgPUIRAAAwHoEIgAAYD0CEQAAsB6BCAAAWI9ABAAArEcgAgAA1iMQAQAA6wU0EGVkZOgnP/mJmjdvrqioKN16663Ky8vzmnP69GmlpaWpVatWatasmcaOHavi4mKvOfn5+UpOTlbTpk0VFRWlGTNm6OzZs15z3n77bV1//fUKDQ1Vp06dtHz58tpeHgAAaCACGoi2bNmitLQ0bdu2TZmZmaqoqNDQoUN16tQpZ859992nv/71r1qzZo22bNmigoICjRkzxhmvrKxUcnKyzpw5o/fff18vvfSSli9frtmzZztzDh06pOTkZN18883Kzc3V9OnT9V//9V96880363S9AACgfnIZY0ygi6h2/PhxRUVFacuWLbrppptUWlqqNm3a6OWXX9a///u/S5I++eQTde/eXdnZ2Ro4cKDeeOMN/fznP1dBQYGio6MlSUuWLNHMmTN1/PhxhYSEaObMmVq/fr327t3rfK1x48appKREGzZsuGBdHo9HERERKi0tVXh4eO0svoHp+ND6QJdwyQ7PSQ50CQCAOnQpf7/r1TVEpaWlkqSWLVtKknJyclRRUaHExERnTrdu3dS+fXtlZ2dLkrKzs9W7d28nDElSUlKSPB6P9u3b58z5/jGq51Qf41zl5eXyeDxeGwAAuHLVm0BUVVWl6dOn68Ybb1SvXr0kSUVFRQoJCVFkZKTX3OjoaBUVFTlzvh+Gqserx843x+Px6JtvvqlRS0ZGhiIiIpwtNjbWL2sEAAD1U70JRGlpadq7d69WrVoV6FI0a9YslZaWOtvnn38e6JIAAEAtCg50AZI0depUrVu3Tlu3blW7du2c/W63W2fOnFFJSYnXWaLi4mK53W5nzo4dO7yOV30X2vfnnHtnWnFxscLDwxUWFlajntDQUIWGhvplbQAAoP4L6BkiY4ymTp2q1157TZs2bVJcXJzXeP/+/dW4cWNlZWU5+/Ly8pSfn6+EhARJUkJCgj788EMdO3bMmZOZmanw8HD16NHDmfP9Y1TPqT4GAACwW0DPEKWlpenll1/Wn//8ZzVv3ty55iciIkJhYWGKiIhQamqq0tPT1bJlS4WHh+tXv/qVEhISNHDgQEnS0KFD1aNHD02YMEHz5s1TUVGRHnnkEaWlpTlneaZMmaLf/e53evDBB3XnnXdq06ZNevXVV7V+fcO7UwoAAPhfQM8QLV68WKWlpfrXf/1XtW3b1tlWr17tzHnmmWf085//XGPHjtVNN90kt9uttWvXOuONGjXSunXr1KhRIyUkJOj222/XxIkT9Zvf/MaZExcXp/Xr1yszM1N9+/bV008/rT/84Q9KSkqq0/UCAID6qV49h6i+4jlENfEcIgBAfddgn0MEAAAQCAQiAABgPQIRAACwHoEIAABYj0AEAACsRyACAADWIxABAADrEYgAAID1CEQAAMB6BCIAAGA9AhEAALAegQgAAFiPQAQAAKxHIAIAANYjEAEAAOsRiAAAgPUIRAAAwHoEIgAAYD0CEQAAsB6BCAAAWI9ABAAArEcgAgAA1iMQAQAA6xGIAACA9QhEAADAegQiAABgPQIRAACwHoEIAABYj0AEAACsRyACAADWIxABAADrEYgAAID1CEQAAMB6BCIAAGA9AhEAALAegQgAAFiPQAQAAKxHIAIAANYjEAEAAOsRiAAAgPUIRAAAwHoEIgAAYD0CEQAAsB6BCAAAWI9ABAAArEcgAgAA1iMQAQAA6xGIAACA9QhEAADAegQiAABgPQIRAACwHoEIAABYj0AEAACsRyACAADWIxABAADrEYgAAID1CEQAAMB6BCIAAGA9AhEAALAegQgAAFiPQAQAAKxHIAIAANYjEAEAAOsRiAAAgPUIRAAAwHoEIgAAYD0CEQAAsB6BCAAAWI9ABAAArEcgAgAA1gtoINq6datGjhypmJgYuVwuvf76617jd9xxh1wul9c2bNgwrzknTpxQSkqKwsPDFRkZqdTUVJWVlXnN2bNnjwYPHqwmTZooNjZW8+bNq+2lAQCABiSggejUqVPq27evnn/++R+dM2zYMBUWFjrbK6+84jWekpKiffv2KTMzU+vWrdPWrVt19913O+Mej0dDhw5Vhw4dlJOTo/nz5+vxxx/Xiy++WGvrAgAADUtwIL/48OHDNXz48PPOCQ0Nldvt/sGxjz/+WBs2bNDf//53DRgwQJL03HPPacSIEfqf//kfxcTEaOXKlTpz5oyWLl2qkJAQ9ezZU7m5uVqwYIFXcAIAAPaq99cQvf3224qKilLXrl11zz336Msvv3TGsrOzFRkZ6YQhSUpMTFRQUJC2b9/uzLnpppsUEhLizElKSlJeXp6++uqrH/ya5eXl8ng8XhsAALhy1etANGzYMK1YsUJZWVmaO3eutmzZouHDh6uyslKSVFRUpKioKK/PCQ4OVsuWLVVUVOTMiY6O9ppT/bp6zrkyMjIUERHhbLGxsf5eGgAAqEcC+pbZhYwbN875uHfv3urTp4+uvfZavf322xoyZEitfd1Zs2YpPT3dee3xeAhFAABcwer1GaJzXXPNNWrdurUOHjwoSXK73Tp27JjXnLNnz+rEiRPOdUdut1vFxcVec6pf/9i1SaGhoQoPD/faAADAlatBBaKjR4/qyy+/VNu2bSVJCQkJKikpUU5OjjNn06ZNqqqqUnx8vDNn69atqqiocOZkZmaqa9euatGiRd0uAAAA1EsBDURlZWXKzc1Vbm6uJOnQoUPKzc1Vfn6+ysrKNGPGDG3btk2HDx9WVlaWRo0apU6dOikpKUmS1L17dw0bNkx33XWXduzYoffee09Tp07VuHHjFBMTI0m67bbbFBISotTUVO3bt0+rV6/WokWLvN4SAwAAdgtoINq5c6f69eunfv36SZLS09PVr18/zZ49W40aNdKePXt0yy23qEuXLkpNTVX//v31zjvvKDQ01DnGypUr1a1bNw0ZMkQjRozQoEGDvJ4xFBERobfeekuHDh1S//79df/992v27Nnccg8AABwuY4wJdBH1ncfjUUREhEpLS7me6J86PrQ+0CVcssNzkgNdAgCgDl3K3+8GdQ0RAABAbSAQAQAA6xGIAACA9QhEAADAegQiAABgPQIRAACwHoEIAABYj0AEAACsRyACAADWIxABAADrEYgAAID1CEQAAMB6BCIAAGA9AhEAALAegQgAAFiPQAQAAKxHIAIAANYjEAEAAOv5FIg+++wzf9cBAAAQMD4Fok6dOunmm2/Wn/70J50+fdrfNQEAANQpnwLRBx98oD59+ig9PV1ut1v//d//rR07dvi7NgAAgDrhUyC67rrrtGjRIhUUFGjp0qUqLCzUoEGD1KtXLy1YsEDHjx/3d50AAAC15rIuqg4ODtaYMWO0Zs0azZ07VwcPHtQDDzyg2NhYTZw4UYWFhf6qEwAAoNZcViDauXOnfvnLX6pt27ZasGCBHnjgAX366afKzMxUQUGBRo0a5a86AQAAak2wL5+0YMECLVu2THl5eRoxYoRWrFihESNGKCjo23wVFxen5cuXq2PHjv6sFQAAoFb4FIgWL16sO++8U3fccYfatm37g3OioqL0xz/+8bKKAwAAqAs+BaIDBw5ccE5ISIgmTZrky+Gt0/Gh9YEuAQAAq/l0DdGyZcu0Zs2aGvvXrFmjl1566bKLAgAAqEs+BaKMjAy1bt26xv6oqCg99dRTl10UAABAXfIpEOXn5ysuLq7G/g4dOig/P/+yiwIAAKhLPgWiqKgo7dmzp8b+3bt3q1WrVpddFAAAQF3yKRCNHz9e9957rzZv3qzKykpVVlZq06ZNmjZtmsaNG+fvGgEAAGqVT3eZPfHEEzp8+LCGDBmi4OBvD1FVVaWJEydyDREAAGhwfApEISEhWr16tZ544gnt3r1bYWFh6t27tzp06ODv+gAAAGqdT4GoWpcuXdSlSxd/1QIAABAQPgWiyspKLV++XFlZWTp27Jiqqqq8xjdt2uSX4gAAAOqCT4Fo2rRpWr58uZKTk9WrVy+5XC5/1wUAAFBnfApEq1at0quvvqoRI0b4ux4AAIA659Nt9yEhIerUqZO/awEAAAgInwLR/fffr0WLFskY4+96AAAA6pxPb5m9++672rx5s9544w317NlTjRs39hpfu3atX4oDAACoCz4FosjISI0ePdrftQAAAASET4Fo2bJl/q4DAAAgYHy6hkiSzp49q40bN+r3v/+9Tp48KUkqKChQWVmZ34oDAACoCz6dITpy5IiGDRum/Px8lZeX69/+7d/UvHlzzZ07V+Xl5VqyZIm/6wQAAKg1Pp0hmjZtmgYMGKCvvvpKYWFhzv7Ro0crKyvLb8UBAADUBZ/OEL3zzjt6//33FRIS4rW/Y8eO+uKLL/xSGAAAQF3x6QxRVVWVKisra+w/evSomjdvftlFAQAA1CWfAtHQoUO1cOFC57XL5VJZWZkee+wx/p0HAABocHx6y+zpp59WUlKSevToodOnT+u2227TgQMH1Lp1a73yyiv+rhEAAKBW+RSI2rVrp927d2vVqlXas2ePysrKlJqaqpSUFK+LrAEAABoCnwKRJAUHB+v222/3Zy0AAAAB4VMgWrFixXnHJ06c6FMxAAAAgeBTIJo2bZrX64qKCn399dcKCQlR06ZNCUQAAKBB8ekus6+++sprKysrU15engYNGsRF1QAAoMHx+X+Znatz586aM2dOjbNHAAAA9Z3fApH07YXWBQUF/jwkAABArfPpGqK//OUvXq+NMSosLNTvfvc73XjjjX4pDAAAoK74FIhuvfVWr9cul0tt2rTRz372Mz399NP+qAsAAKDO+BSIqqqq/F0HAABAwPj1GiIAAICGyKczROnp6Rc9d8GCBb58CQAAgDrjUyDatWuXdu3apYqKCnXt2lWStH//fjVq1EjXX3+9M8/lcvmnSgAAgFrkUyAaOXKkmjdvrpdeekktWrSQ9O3DGidPnqzBgwfr/vvv92uRAAAAtcmna4iefvppZWRkOGFIklq0aKEnn3ySu8wAAECD41Mg8ng8On78eI39x48f18mTJy+7KAAAgLrkUyAaPXq0Jk+erLVr1+ro0aM6evSo/u///k+pqakaM2aMv2sEAACoVT5dQ7RkyRI98MADuu2221RRUfHtgYKDlZqaqvnz5/u1QAAAgNrmUyBq2rSpXnjhBc2fP1+ffvqpJOnaa6/VVVdd5dfiAAAA6sJlPZixsLBQhYWF6ty5s6666ioZYy7p87du3aqRI0cqJiZGLpdLr7/+ute4MUazZ89W27ZtFRYWpsTERB04cMBrzokTJ5SSkqLw8HBFRkYqNTVVZWVlXnP27NmjwYMHq0mTJoqNjdW8efN8Wi8AALgy+RSIvvzySw0ZMkRdunTRiBEjVFhYKElKTU29pFvuT506pb59++r555//wfF58+bp2Wef1ZIlS7R9+3ZdddVVSkpK0unTp505KSkp2rdvnzIzM7Vu3Tpt3bpVd999tzPu8Xg0dOhQdejQQTk5OZo/f74ef/xxvfjii74sHQAAXIF8CkT33XefGjdurPz8fDVt2tTZ/4tf/EIbNmy46OMMHz5cTz75pEaPHl1jzBijhQsX6pFHHtGoUaPUp08frVixQgUFBc6ZpI8//lgbNmzQH/7wB8XHx2vQoEF67rnntGrVKhUUFEiSVq5cqTNnzmjp0qXq2bOnxo0bp3vvvZcnaAMAAIdPgeitt97S3Llz1a5dO6/9nTt31pEjR/xS2KFDh1RUVKTExERnX0REhOLj45WdnS1Jys7OVmRkpAYMGODMSUxMVFBQkLZv3+7MuemmmxQSEuLMSUpKUl5enr766iu/1AoAABo2ny6qPnXqlNeZoWonTpxQaGjoZRclSUVFRZKk6Ohor/3R0dHOWFFRkaKiorzGg4OD1bJlS685cXFxNY5RPfb9h0tWKy8vV3l5ufPa4/Fc5moAAEB95tMZosGDB2vFihXOa5fLpaqqKs2bN08333yz34oLlIyMDEVERDhbbGxsoEsCAAC1yKczRPPmzdOQIUO0c+dOnTlzRg8++KD27dunEydO6L333vNLYW63W5JUXFystm3bOvuLi4t13XXXOXOOHTvm9Xlnz57ViRMnnM93u90qLi72mlP9unrOuWbNmqX09HTntcfjIRQBAHAF8+kMUa9evbR//34NGjRIo0aN0qlTpzRmzBjt2rVL1157rV8Ki4uLk9vtVlZWlrPP4/Fo+/btSkhIkCQlJCSopKREOTk5zpxNmzapqqpK8fHxzpytW7c6D5CUpMzMTHXt2vUH3y6TpNDQUIWHh3ttAADgynXJZ4gqKio0bNgwLVmyRA8//PBlffGysjIdPHjQeX3o0CHl5uaqZcuWat++vaZPn64nn3xSnTt3VlxcnB599FHFxMTo1ltvlSR1795dw4YN01133aUlS5aooqJCU6dO1bhx4xQTEyNJuu222/TrX/9aqampmjlzpvbu3atFixbpmWeeuazaAQDAleOSA1Hjxo21Z88ev3zxnTt3el1zVP021aRJk7R8+XI9+OCDOnXqlO6++26VlJRo0KBB2rBhg5o0aeJ8zsqVKzV16lQNGTJEQUFBGjt2rJ599llnPCIiQm+99ZbS0tLUv39/tW7dWrNnz/Z6VhEAALCby1zq46X17XOIQkNDNWfOnNqoqd7xeDyKiIhQaWlprbx91vGh9X4/Jmo6PCc50CUAAOrQpfz99umi6rNnz2rp0qXauHGj+vfvX+N/mPHQQwAA0JBcUiD67LPP1LFjR+3du1fXX3+9JGn//v1ec1wul/+qAwAAqAOXFIg6d+6swsJCbd68WdK3/6rj2WefrfHwRAAAgIbkkm67P/dyozfeeEOnTp3ya0EAAAB1zafnEFXz4XpsAACAeueSApHL5apxjRDXDAEAgIbukq4hMsbojjvucP6B6+nTpzVlypQad5mtXbvWfxUCAADUsksKRJMmTfJ6ffvtt/u1GAAAgEC4pEC0bNmy2qoDAAAgYC7romoAAIArAYEIAABYj0AEAACsRyACAADWIxABAADrEYgAAID1CEQAAMB6BCIAAGA9AhEAALAegQgAAFiPQAQAAKxHIAIAANYjEAEAAOsRiAAAgPUIRAAAwHoEIgAAYD0CEQAAsB6BCAAAWI9ABAAArEcgAgAA1iMQAQAA6xGIAACA9QhEAADAegQiAABgPQIRAACwHoEIAABYj0AEAACsRyACAADWIxABAADrEYgAAID1CEQAAMB6BCIAAGA9AhEAALAegQgAAFiPQAQAAKxHIAIAANYjEAEAAOsRiAAAgPUIRAAAwHoEIgAAYD0CEQAAsB6BCAAAWI9ABAAArEcgAgAA1iMQAQAA6xGIAACA9QhEAADAegQiAABgPQIRAACwHoEIAABYj0AEAACsRyACAADWIxABAADrEYgAAID1CEQAAMB6BCIAAGA9AhEAALAegQgAAFiPQAQAAKxHIAIAANYjEAEAAOvV60D0+OOPy+VyeW3dunVzxk+fPq20tDS1atVKzZo109ixY1VcXOx1jPz8fCUnJ6tp06aKiorSjBkzdPbs2bpeCgAAqMeCA13AhfTs2VMbN250XgcHf1fyfffdp/Xr12vNmjWKiIjQ1KlTNWbMGL333nuSpMrKSiUnJ8vtduv9999XYWGhJk6cqMaNG+upp56q87UAAID6qd4HouDgYLnd7hr7S0tL9cc//lEvv/yyfvazn0mSli1bpu7du2vbtm0aOHCg3nrrLX300UfauHGjoqOjdd111+mJJ57QzJkz9fjjjyskJKSulwMAAOqhev2WmSQdOHBAMTExuuaaa5SSkqL8/HxJUk5OjioqKpSYmOjM7datm9q3b6/s7GxJUnZ2tnr37q3o6GhnTlJSkjwej/bt2/ejX7O8vFwej8drAwAAV656HYji4+O1fPlybdiwQYsXL9ahQ4c0ePBgnTx5UkVFRQoJCVFkZKTX50RHR6uoqEiSVFRU5BWGqserx35MRkaGIiIinC02Nta/CwMAAPVKvX7LbPjw4c7Hffr0UXx8vDp06KBXX31VYWFhtfZ1Z82apfT0dOe1x+MhFAEAcAWr12eIzhUZGakuXbro4MGDcrvdOnPmjEpKSrzmFBcXO9ccud3uGnedVb/+oeuSqoWGhio8PNxrAwAAV64GFYjKysr06aefqm3bturfv78aN26srKwsZzwvL0/5+flKSEiQJCUkJOjDDz/UsWPHnDmZmZkKDw9Xjx496rx+AABQP9Xrt8weeOABjRw5Uh06dFBBQYEee+wxNWrUSOPHj1dERIRSU1OVnp6uli1bKjw8XL/61a+UkJCggQMHSpKGDh2qHj16aMKECZo3b56Kior0yCOPKC0tTaGhoQFeHQAAqC/qdSA6evSoxo8fry+//FJt2rTRoEGDtG3bNrVp00aS9MwzzygoKEhjx45VeXm5kpKS9MILLzif36hRI61bt0733HOPEhISdNVVV2nSpEn6zW9+E6glAQCAeshljDGBLqK+83g8ioiIUGlpaa1cT9TxofV+PyZqOjwnOdAlAADq0KX8/W5Q1xABAADUBgIRAACwHoEIAABYj0AEAACsRyACAADWIxABAADrEYgAAID1CEQAAMB6BCIAAGA9AhEAALAegQgAAFiPQAQAAKxHIAIAANYjEAEAAOsRiAAAgPUIRAAAwHoEIgAAYD0CEQAAsB6BCAAAWI9ABAAArEcgAgAA1iMQAQAA6xGIAACA9QhEAADAegQiAABgPQIRAACwHoEIAABYj0AEAACsRyACAADWIxABAADrEYgAAID1CEQAAMB6BCIAAGA9AhEAALAegQgAAFiPQAQAAKxHIAIAANYjEAEAAOsRiAAAgPUIRAAAwHoEIgAAYD0CEQAAsB6BCAAAWI9ABAAArEcgAgAA1iMQAQAA6xGIAACA9QhEAADAegQiAABgPQIRAACwHoEIAABYj0AEAACsRyACAADWIxABAADrEYgAAID1CEQAAMB6BCIAAGA9AhEAALAegQgAAFiPQAQAAKxHIAIAANYjEAEAAOsRiAAAgPWCA10AUFc6PrQ+0CVcssNzkgNdAgBYgTNEAADAegQiAABgPQIRAACwHoEIAABYj0AEAACsRyACAADWsyoQPf/88+rYsaOaNGmi+Ph47dixI9AlAQCAesCa5xCtXr1a6enpWrJkieLj47Vw4UIlJSUpLy9PUVFRgS4PuGLwvCcADZE1Z4gWLFigu+66S5MnT1aPHj20ZMkSNW3aVEuXLg10aQAAIMCsOEN05swZ5eTkaNasWc6+oKAgJSYmKjs7u8b88vJylZeXO69LS0slSR6Pp1bqqyr/ulaOi4avtr7nalND/H5uiH0GcGHVP9vGmAvOtSIQ/eMf/1BlZaWio6O99kdHR+uTTz6pMT8jI0O//vWva+yPjY2ttRqBHxKxMNAV2IE+A1e2kydPKiIi4rxzrAhEl2rWrFlKT093XldVVenIkSO67rrr9Pnnnys8PDyA1QWex+NRbGwsvfgn+vEdevEdeuGNfnyHXnirzX4YY3Ty5EnFxMRccK4Vgah169Zq1KiRiouLvfYXFxfL7XbXmB8aGqrQ0FCvfUFB315uFR4ezjfwP9ELb/TjO/TiO/TCG/34Dr3wVlv9uNCZoWpWXFQdEhKi/v37Kysry9lXVVWlrKwsJSQkBLAyAABQH1hxhkiS0tPTNWnSJA0YMEA33HCDFi5cqFOnTmny5MmBLg0AAASYNYHoF7/4hY4fP67Zs2erqKhI1113nTZs2FDjQusfExoaqscee6zGW2k2ohfe6Md36MV36IU3+vEdeuGtvvTDZS7mXjQAAIArmBXXEAEAAJwPgQgAAFiPQAQAAKxHIAIAANYjEF2k559/Xh07dlSTJk0UHx+vHTt2BLokv8rIyNBPfvITNW/eXFFRUbr11luVl5fnNef06dNKS0tTq1at1KxZM40dO7bGwy7z8/OVnJyspk2bKioqSjNmzNDZs2frcil+N2fOHLlcLk2fPt3ZZ1svvvjiC91+++1q1aqVwsLC1Lt3b+3cudMZN8Zo9uzZatu2rcLCwpSYmKgDBw54HePEiRNKSUlReHi4IiMjlZqaqrKysrpeymWprKzUo48+qri4OIWFhenaa6/VE0884fV/kq7kXmzdulUjR45UTEyMXC6XXn/9da9xf619z549Gjx4sJo0aaLY2FjNmzevtpd2yc7Xi4qKCs2cOVO9e/fWVVddpZiYGE2cOFEFBQVex7hSeiFd+Hvj+6ZMmSKXy6WFCxd67Q94PwwuaNWqVSYkJMQsXbrU7Nu3z9x1110mMjLSFBcXB7o0v0lKSjLLli0ze/fuNbm5uWbEiBGmffv2pqyszJkzZcoUExsba7KysszOnTvNwIEDzU9/+lNn/OzZs6ZXr14mMTHR7Nq1y/ztb38zrVu3NrNmzQrEkvxix44dpmPHjqZPnz5m2rRpzn6benHixAnToUMHc8cdd5jt27ebzz77zLz55pvm4MGDzpw5c+aYiIgI8/rrr5vdu3ebW265xcTFxZlvvvnGmTNs2DDTt29fs23bNvPOO++YTp06mfHjxwdiST777W9/a1q1amXWrVtnDh06ZNasWWOaNWtmFi1a5My5knvxt7/9zTz88MNm7dq1RpJ57bXXvMb9sfbS0lITHR1tUlJSzN69e80rr7xiwsLCzO9///u6WuZFOV8vSkpKTGJiolm9erX55JNPTHZ2trnhhhtM//79vY5xpfTCmAt/b1Rbu3at6du3r4mJiTHPPPOM11ig+0Egugg33HCDSUtLc15XVlaamJgYk5GREcCqatexY8eMJLNlyxZjzLc/4I0bNzZr1qxx5nz88cdGksnOzjbGfPsDERQUZIqKipw5ixcvNuHh4aa8vLxuF+AHJ0+eNJ07dzaZmZnmX/7lX5xAZFsvZs6caQYNGvSj41VVVcbtdpv58+c7+0pKSkxoaKh55ZVXjDHGfPTRR0aS+fvf/+7MeeONN4zL5TJffPFF7RXvZ8nJyebOO+/02jdmzBiTkpJijLGrF+f+0fPX2l944QXTokULr5+TmTNnmq5du9byinx3vgBQbceOHUaSOXLkiDHmyu2FMT/ej6NHj5qrr77a7N2713To0MErENWHfvCW2QWcOXNGOTk5SkxMdPYFBQUpMTFR2dnZAaysdpWWlkqSWrZsKUnKyclRRUWFVx+6deum9u3bO33Izs5W7969vR52mZSUJI/Ho3379tVh9f6Rlpam5ORkrzVL9vXiL3/5iwYMGKD/+I//UFRUlPr166f//d//dcYPHTqkoqIir35EREQoPj7eqx+RkZEaMGCAMycxMVFBQUHavn173S3mMv30pz9VVlaW9u/fL0navXu33n33XQ0fPlySXb04l7/Wnp2drZtuukkhISHOnKSkJOXl5emrr76qo9X4X2lpqVwulyIjIyXZ14uqqipNmDBBM2bMUM+ePWuM14d+EIgu4B//+IcqKytrPNE6OjpaRUVFAaqqdlVVVWn69Om68cYb1atXL0lSUVGRQkJCnB/mat/vQ1FR0Q/2qXqsIVm1apU++OADZWRk1BizrRefffaZFi9erM6dO+vNN9/UPffco3vvvVcvvfSSpO/Wc76fkaKiIkVFRXmNBwcHq2XLlg2qHw899JDGjRunbt26qXHjxurXr5+mT5+ulJQUSXb14lz+WvuV9LNT7fTp05o5c6bGjx/v/PNS23oxd+5cBQcH69577/3B8frQD2v+dQcuXlpamvbu3at333030KUExOeff65p06YpMzNTTZo0CXQ5AVdVVaUBAwboqaeekiT169dPe/fu1ZIlSzRp0qQAV1e3Xn31Va1cuVIvv/yyevbsqdzcXE2fPl0xMTHW9QIXp6KiQv/5n/8pY4wWL14c6HICIicnR4sWLdIHH3wgl8sV6HJ+FGeILqB169Zq1KhRjTuIiouL5Xa7A1RV7Zk6darWrVunzZs3q127ds5+t9utM2fOqKSkxGv+9/vgdrt/sE/VYw1FTk6Ojh07puuvv17BwcEKDg7Wli1b9Oyzzyo4OFjR0dHW9EKS2rZtqx49enjt6969u/Lz8yV9t57z/Yy43W4dO3bMa/zs2bM6ceJEg+rHjBkznLNEvXv31oQJE3Tfffc5ZxJt6sW5/LX2K+lnpzoMHTlyRJmZmc7ZIcmuXrzzzjs6duyY2rdv7/xOPXLkiO6//3517NhRUv3oB4HoAkJCQtS/f39lZWU5+6qqqpSVlaWEhIQAVuZfxhhNnTpVr732mjZt2qS4uDiv8f79+6tx48ZefcjLy1N+fr7Th4SEBH344Yde39TVvwTO/YNanw0ZMkQffvihcnNznW3AgAFKSUlxPralF5J044031ngEw/79+9WhQwdJUlxcnNxut1c/PB6Ptm/f7tWPkpIS5eTkOHM2bdqkqqoqxcfH18Eq/OPrr79WUJD3r81GjRqpqqpKkl29OJe/1p6QkKCtW7eqoqLCmZOZmamuXbuqRYsWdbSay1cdhg4cOKCNGzeqVatWXuM29WLChAnas2eP1+/UmJgYzZgxQ2+++aaketIPv1yafYVbtWqVCQ0NNcuXLzcfffSRufvuu01kZKTXHUQN3T333GMiIiLM22+/bQoLC53t66+/duZMmTLFtG/f3mzatMns3LnTJCQkmISEBGe8+lbzoUOHmtzcXLNhwwbTpk2bBnmr+bm+f5eZMXb1YseOHSY4ONj89re/NQcOHDArV640TZs2NX/605+cOXPmzDGRkZHmz3/+s9mzZ48ZNWrUD95u3a9fP7N9+3bz7rvvms6dOzeIW82/b9KkSebqq692brtfu3atad26tXnwwQedOVdyL06ePGl27dpldu3aZSSZBQsWmF27djl3Tvlj7SUlJSY6OtpMmDDB7N2716xatco0bdq03t1qfr5enDlzxtxyyy2mXbt2Jjc31+t36vfvkLpSemHMhb83znXuXWbGBL4fBKKL9Nxzz5n27dubkJAQc8MNN5ht27YFuiS/kvSD27Jly5w533zzjfnlL39pWrRoYZo2bWpGjx5tCgsLvY5z+PBhM3z4cBMWFmZat25t7r//flNRUVHHq/G/cwORbb3461//anr16mVCQ0NNt27dzIsvvug1XlVVZR599FETHR1tQkNDzZAhQ0xeXp7XnC+//NKMHz/eNGvWzISHh5vJkyebkydP1uUyLpvH4zHTpk0z7du3N02aNDHXXHONefjhh73+yF3Jvdi8efMP/p6YNGmSMcZ/a9+9e7cZNGiQCQ0NNVdffbWZM2dOXS3xop2vF4cOHfrR36mbN292jnGl9MKYC39vnOuHAlGg++Ey5nuPWAUAALAQ1xABAADrEYgAAID1CEQAAMB6BCIAAGA9AhEAALAegQgAAFiPQAQAAKxHIAIAANYjEAEAAOsRiAAAgPUIRAAAwHoEIgAAYL3/B2mG9SMSqdMBAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "#n_by_event = events.groupby('Device ID')[\"Event ID\"].count().sort_values()\n", + "#n_by_event.head(10).plot(kind='bar') \n", + "#events['alt_date'] = events.to_datetime(df['Timestamps'], unit='s')\n", + "events_byevents = events.groupby('Timestamps')['Event ID'].sum()\n", + "#df_resampled = events.resample('D').sum()\n", + "#daily_quality.plot(kind='bar')\n", + "print(events_byevents.head(5))\n", + "events_byevents.plot.hist(bins=10)\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkQAAAGdCAYAAADzOWwgAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/P9b71AAAACXBIWXMAAA9hAAAPYQGoP6dpAAAsd0lEQVR4nO3de1xVdb7/8ffmshGUixdgw4hISRreMp1DlNY0MiI63fQ0mphaTB0bbCy6Wh2z6ULZ5FgzpdOZ1DpZmnPMGk0M8VZGmo7kZQq1MDRFPBoglojs9fvD4/q1Q0u3Gzb4fT0fj/V4sL/fL2t/1pft9v1Y+7vWdliWZQkAAMBgAf4uAAAAwN8IRAAAwHgEIgAAYDwCEQAAMB6BCAAAGI9ABAAAjEcgAgAAxiMQAQAA4wX5u4CWwO12a+/evQoPD5fD4fB3OQAA4AxYlqXDhw8rPj5eAQE/fg6IQHQG9u7dq4SEBH+XAQAAvLB792517NjxR8cQiM5AeHi4pBMTGhER4edqAADAmaiurlZCQoL9//iPIRCdgZMfk0VERBCIAABoYc5kuQuLqgEAgPEIRAAAwHgEIgAAYDzWEAEAcAr19fWqq6vzdxn4CcHBwQoMDDzn/RCIAAD4gZqaGu3Zs0eWZfm7FPwEh8Ohjh07qk2bNue0HwIRAADfU19frz179igsLEzR0dHckLcZsyxLBw4c0J49e5ScnHxOZ4oIRAAAfE9dXZ0sy1J0dLRCQ0P9XQ5+QnR0tHbt2qW6urpzCkQsqgYA4BQ4M9Qy+OrvRCACAADGIxABAADjsYYIAIAz0PnBJU36fLueHtqkz2c6zhABAHAeGDdunBwOR4Nt8ODBTVrHlClTdMkll5z1uClTptg1BwUFqUOHDrryyis1ffp01dbWNl7B/4czRAAAnCcGDx6s2bNne7SFhIT4qZqz1717dy1fvlxut1sHDx7UqlWr9MQTT+i///u/tWrVqjP61npvcYYIAIDzREhIiFwul8fWtm1bSdKoUaM0YsQIj/F1dXXq0KGDXnvtNUmS2+1WXl6ekpKSFBoaqt69e+vvf/+7PX7VqlVyOBwqLCxUv379FBYWpssvv1wlJSWSpDlz5uixxx7Tp59+ap/tmTNnzhnXHxQUJJfLpfj4ePXs2VN33nmnVq9era1bt+qZZ545x9n5iedu1L3jjDT159K+wGfbANCyZGVl6cYbb1RNTY19V+dly5bp22+/1Q033CBJysvL0+uvv66ZM2cqOTlZa9as0ejRoxUdHa2rrrrK3tfDDz+s5557TtHR0Ro/frxuvfVWrV27ViNGjNDWrVuVn5+v5cuXS5IiIyPPqe5u3bopMzNTCxcu1BNPPHFO+/oxnCECAOA8sXjxYrVp08Zje+qppyRJGRkZat26td5++217/BtvvKFrr71W4eHhqq2t1VNPPaVZs2YpIyNDF1xwgcaNG6fRo0frr3/9q8fzPPnkk7rqqquUkpKiBx98UB999JGOHj2q0NBQtWnTxj7T43K5fHJzy27dumnXrl3nvJ8fwxkiAADOE1dffbVmzJjh0dauXTtJJz6O+s1vfqO5c+fq5ptv1pEjR/TOO+9o3rx5kqSdO3fq22+/1a9+9SuP3z927Jj69Onj0darVy/757i4OElSRUWFOnXq5PNjkk58RUdj3yiTQAQAwHmidevW6tKly2n7s7KydNVVV6miokIFBQUKDQ21r0KrqamRJC1ZskQ/+9nPPH7vhwuzg4OD7Z9PBhW32+2TYziVzz77TElJSY22f4lABACAMS6//HIlJCRo/vz5Wrp0qW688UY73KSkpCgkJERlZWUe64XOltPpVH19va9K1ueff678/HxNmjTJZ/s8FQIRAADnidraWpWXl3u0nbynz0mjRo3SzJkztX37dq1cudJuDw8P17333qu7775bbrdb/fv3V1VVldauXauIiAiNHTv2jGro3LmzSktLVVxcrI4dOyo8PPyML/0/fvy4ysvLG1x2f8kll+i+++47o314i0AEAMAZaAlX1+bn59trek7q2rWrPv/8c/txVlaWnnzySSUmJuqKK67wGPv4448rOjpaeXl5+vLLLxUVFaVLL71UDz300BnXMHz4cC1cuFBXX321KisrNXv2bI0bN+6Mfnfbtm2Ki4tTYGCgIiMjlZKSokmTJumOO+5o9PspOSzLshr1Gc4D1dXVioyMVFVVlSIiIny+fy67B4Dm4+jRoyotLVVSUpJatWrl73LwE37s73U2/39z2T0AADAegQgAABiPQAQAAIxHIAIAAMYjEAEAcApcc9Qy+OrvRCACAOB7AgMDJZ34ygo0fyf/Tif/bt7iPkQAAHxPUFCQwsLCdODAAQUHBysggHMHzZXb7daBAwcUFhamoKBzizQE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/F1Os8LcnBrzcnrMzekxN6fH3JxaY8+LZVk6fPiw4uPjf3KsEYGoQ4cOCgwM1P79+z3a9+/fL5fL1WB8SEiIQkJCPNqioqIas8QmERERwT/E02BuTo15OT3m5vSYm9Njbk6tMeflp84MnWTEomqn06m+ffuqsLDQbnO73SosLFRaWpofKwMAAM2BEWeIJCk3N1djx45Vv3799G//9m+aPn26jhw5oltuucXfpQEAAD8zJhCNGDFCBw4c0OTJk1VeXq5LLrlE+fn5DRZan49CQkL06KOPNvgYEMzN6TAvp8fcnB5zc3rMzak1p3lxWGdyLRoAAMB5zIg1RAAAAD+GQAQAAIxHIAIAAMYjEAEAAOMRiFqoNWvW6JprrlF8fLwcDocWLVrk0W9ZliZPnqy4uDiFhoYqPT1dO3bs8Bhz6NAhZWVlKSIiQlFRUcrOzlZNTU0THoXv5eXl6ec//7nCw8MVExOj66+/XiUlJR5jjh49qpycHLVv315t2rTR8OHDG9y0s6ysTEOHDlVYWJhiYmJ033336fjx4015KD43Y8YM9erVy74BWlpampYuXWr3mzovp/L000/L4XDorrvusttMnZ8pU6bI4XB4bN26dbP7TZ0XSfr66681evRotW/fXqGhoerZs6c2bNhg95v6Pty5c+cGrxmHw6GcnBxJzfg1Y6FFeu+996yHH37YWrhwoSXJevvttz36n376aSsyMtJatGiR9emnn1rXXnutlZSUZH333Xf2mMGDB1u9e/e2Pv74Y+uDDz6wunTpYt10001NfCS+lZGRYc2ePdvaunWrVVxcbA0ZMsTq1KmTVVNTY48ZP368lZCQYBUWFlobNmywLrvsMuvyyy+3+48fP2716NHDSk9PtzZt2mS99957VocOHaxJkyb545B85t1337WWLFlibd++3SopKbEeeughKzg42Nq6datlWebOyw+tX7/e6ty5s9WrVy9r4sSJdrup8/Poo49a3bt3t/bt22dvBw4csPtNnZdDhw5ZiYmJ1rhx46x169ZZX375pbVs2TJr586d9hhT34crKio8Xi8FBQWWJGvlypWWZTXf1wyB6Dzww0Dkdrstl8tlPfvss3ZbZWWlFRISYr355puWZVnWv/71L0uS9cknn9hjli5dajkcDuvrr79ustobW0VFhSXJWr16tWVZJ+YhODjYWrBggT3ms88+syRZRUVFlmWdCJsBAQFWeXm5PWbGjBlWRESEVVtb27QH0Mjatm1r/e1vf2Ne/s/hw4et5ORkq6CgwLrqqqvsQGTy/Dz66KNW7969T9ln8rw88MADVv/+/U/bz/vw/zdx4kTrwgsvtNxud7N+zfCR2XmotLRU5eXlSk9Pt9siIyOVmpqqoqIiSVJRUZGioqLUr18/e0x6eroCAgK0bt26Jq+5sVRVVUmS2rVrJ0nauHGj6urqPOamW7du6tSpk8fc9OzZ0+OmnRkZGaqurta2bduasPrGU19fr3nz5unIkSNKS0tjXv5PTk6Ohg4d6jEPEq+bHTt2KD4+XhdccIGysrJUVlYmyex5effdd9WvXz/deOONiomJUZ8+ffRf//Vfdj/vwyccO3ZMr7/+um699VY5HI5m/ZohEJ2HysvLJanBXbhjY2PtvvLycsXExHj0BwUFqV27dvaYls7tduuuu+7SFVdcoR49ekg6cdxOp7PBl/X+cG5ONXcn+1qyLVu2qE2bNgoJCdH48eP19ttvKyUlxfh5kaR58+bpn//8p/Ly8hr0mTw/qampmjNnjvLz8zVjxgyVlpZqwIABOnz4sNHz8uWXX2rGjBlKTk7WsmXLdMcdd+j3v/+9Xn31VUm8D5+0aNEiVVZWaty4cZKa978lY766A+bJycnR1q1b9eGHH/q7lGaja9euKi4uVlVVlf7+979r7NixWr16tb/L8rvdu3dr4sSJKigoUKtWrfxdTrOSmZlp/9yrVy+lpqYqMTFRb731lkJDQ/1YmX+53W7169dPTz31lCSpT58+2rp1q2bOnKmxY8f6ubrm45VXXlFmZqbi4+P9XcpP4gzRecjlcklSg1X7+/fvt/tcLpcqKio8+o8fP65Dhw7ZY1qyCRMmaPHixVq5cqU6duxot7tcLh07dkyVlZUe4384N6eau5N9LZnT6VSXLl3Ut29f5eXlqXfv3nr++eeNn5eNGzeqoqJCl156qYKCghQUFKTVq1frhRdeUFBQkGJjY42en++LiorSRRddpJ07dxr9uomLi1NKSopH28UXX2x/nMj7sPTVV19p+fLl+u1vf2u3NefXDIHoPJSUlCSXy6XCwkK7rbq6WuvWrVNaWpokKS0tTZWVldq4caM9ZsWKFXK73UpNTW3ymn3FsixNmDBBb7/9tlasWKGkpCSP/r59+yo4ONhjbkpKSlRWVuYxN1u2bPF4oyooKFBERESDN8CWzu12q7a21vh5GThwoLZs2aLi4mJ769evn7KysuyfTZ6f76upqdEXX3yhuLg4o183V1xxRYNbemzfvl2JiYmSzH4fPmn27NmKiYnR0KFD7bZm/ZpptOXaaFSHDx+2Nm3aZG3atMmSZE2bNs3atGmT9dVXX1mWdeJyz6ioKOudd96xNm/ebF133XWnvNyzT58+1rp166wPP/zQSk5ObvGXe95xxx1WZGSktWrVKo/LPr/99lt7zPjx461OnTpZK1assDZs2GClpaVZaWlpdv/JSz4HDRpkFRcXW/n5+VZ0dHSLv0z4wQcftFavXm2VlpZamzdvth588EHL4XBY77//vmVZ5s7L6Xz/KjPLMnd+7rnnHmvVqlVWaWmptXbtWis9Pd3q0KGDVVFRYVmWufOyfv16KygoyHryySetHTt2WHPnzrXCwsKs119/3R5j6vuwZVlWfX291alTJ+uBBx5o0NdcXzMEohZq5cqVlqQG29ixYy3LOnHJ53/+539asbGxVkhIiDVw4ECrpKTEYx8HDx60brrpJqtNmzZWRESEdcstt1iHDx/2w9H4zqnmRJI1e/Zse8x3331n/e53v7Patm1rhYWFWTfccIO1b98+j/3s2rXLyszMtEJDQ60OHTpY99xzj1VXV9fER+Nbt956q5WYmGg5nU4rOjraGjhwoB2GLMvceTmdHwYiU+dnxIgRVlxcnOV0Oq2f/exn1ogRIzzutWPqvFiWZf3jH/+wevToYYWEhFjdunWzXn75ZY9+U9+HLcuyli1bZklqcLyW1XxfMw7LsqzGO/8EAADQ/LGGCAAAGI9ABAAAjEcgAgAAxiMQAQAA4xGIAACA8QhEAADAeAQiAABgPAIRAAAwHoEIAAAYj0AEAACMRyACAADGIxABAADj/T9v3MiirFLGPAAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "events.plot(x=\"Timestamps\", y=[\"Event ID\"], kind=\"hist\")\n", + "#events.plot(x=\"Timestamps\", y=[\"Event ID\", \"Device ID\"], kind=\"hist\")\n", + "plt.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.10" + }, + "metadata": { + "interpreter": { + "hash": "93e542f224a5f7d967126a31b609f40ad7c4fe0221d1cb37d25653d511875e23" + } + }, + "orig_nbformat": 2, + "vscode": { + "interpreter": { + "hash": "97bb7c4ceea988ef170aaf0813e87b9ab5db53db24aad0560f033fff26edc589" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/policy.csv b/plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/policy.csv new file mode 100644 index 000000000..60d5149cd --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/policy.csv @@ -0,0 +1,296 @@ +64,"GID Address In Service",SM_GID_IN_SERVICE,0,1,1,1,0,0,1,0,10,0,0,Info,1,300,0,Port,"GID Address In Service: prefix %(prefix)016x,guid %(guid)016x",Fabric Notification,0,"New GID is connected to the Fabric", +65,"GID Address Out of Service",SM_GID_OUT_OF_SERVICE,0,1,1,1,0,0,1,0,10,1,1,Warning,1,300,0,Port,"GID Address Out of Service: prefix %(prefix)016x,guid %(guid)016x",Fabric Notification,0,"Existing GID is disconnected from the Fabric", +66,"New MCast Group Created",SM_MCAST_GROUP_CREATED,0,1,1,1,0,0,1,0,11,0,0,Info,1,300,0,Port,"New MCast group is created: %(prefix)016x, %(pkey)08x",Fabric Notification,0,"New Multicast Group is created in SM", +67,"MCast Group Deleted",SM_MCAST_GROUP_DELETED,0,1,1,1,0,0,1,0,11,0,0,Info,1,300,0,Port,"Mcast group is deleted: %(prefix)016x, %(pkey)08x",Fabric Notification,0,"Multicast Group is removed from SM", +110,"Symbol Error",PM_SYMBOLERROR,1,1,1,1,0,0,1,0,4,1,1,Warning,200,300,0,Port,"Symbol-Error counter rate threshold exceeded. Threshold is %d, received value is %d. Peer info: %s.",Hardware,0,"Total number of minor link errors detected on one or more physical lanes", +111,"Link Error Recovery",PM_LINKERRORRECOVERY,1,1,1,1,0,0,1,0,4,1,1,Minor,1,300,0,Port,"Link-Error-Recovery counter rate threshold exceeded. Threshold is %d, received value is %d. Peer info: %s.",Hardware,0,"Total number of times the Port Training state machine has successfully completed the link error recovery process", +112,"Link Downed",PM_LINKDOWNEDCOUNTER,1,1,1,1,0,0,1,0,5,1,1,Warning,0,300,0,Port,"Link-Downed counter delta threshold exceeded. Threshold is %d, calculated delta is %d. Peer info: %s.",Hardware,0,"Total number of times the Port Training state machine has failed the link error recovery process and downed the link", +113,"Port Receive Errors",PM_PORTRCVERRORS,1,1,1,1,0,0,1,0,4,1,1,Warning,5,300,0,Port,"PortRcvErrors counter rate threshold exceeded. Threshold is %d, received value is %d. Peer info: %s.",Hardware,0,"Total number of packets containing an error that were received on a port", +114,"Port Receive Remote Physical Errors",PM_PORTRCVREMOTEPHYSICALERRORS,1,1,1,1,0,0,1,0,4,1,0,Minor,5,300,0,Port,"PortRcvRemotePhysicalErrors counter rate threshold exceeded. Threshold is %d, received value is %d. Peer info: %s.",Hardware,0,"Total number of packets marked with the EBP delimiter received on the port", +115,"Port Receive Switch Relay Errors",PM_PORTRCVSWITCHRELAYERRORS,1,1,1,1,0,0,1,0,7,1,1,Minor,9999,300,0,Port,"PortRcvSwitchRelayErrors counter rate threshold exceeded. Threshold is %d, received value is %d. Peer info: %s.",Fabric Configuration,0,"Total number of packets received on the port that were discarded because they could not be forwarded by the switch relay", +116,"Port Xmit Discards",PM_PORTXMITDISCARDS,1,1,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"PortXmitDiscards counter rate threshold exceeded. Threshold is %s received value is %d. Peer info: %s.",Communication Error,0,"Total number of outbound packets discarded by the port when the port is down or congested", +117,"Port Xmit Constraint Errors",PM_PORTXMITCONSTRAINTERRORS,1,1,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"PortXmitConstraintErrors counter rate threshold exceeded. Threshold is %d, received value is %d. Peer info: %s.",Communication Error,0,"Total number of packets not transmitted from the switch physical port", +118,"Port Receive Constraint Errors",PM_PORTRCVCONSTRAINTERRORS,1,1,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"PortRcvConstraintErrors counter rate threshold exceeded. Threshold is %d, received value is %d. Peer info: %s.",Communication Error,0,"Port Receive Constraint Errors", +119,"Local Link Integrity Errors",PM_LOCALLINKINTEGRITYERRORS,1,1,1,1,0,0,1,0,4,1,1,Minor,5,300,0,Port,"LocalLinkIntegrityErrors counter rate threshold exceeded. Threshold is %d, received value is %d. Peer info: %s.",Hardware,0,"The number of times that the frequency of packets containing local physical errors has exceeded LocalPhyErrors", +120,"Excessive Buffer Overrun Errors",PM_EXCESSIVEBUFFEROVERRUNERRORS,1,1,1,1,0,1,0,0,3,1,1,Warning,1,300,0,Port,"ExcessiveBufferOverrunErrors counter rate threshold exceeded. Threshold is %d, received value is %d. Peer info: %s.",Communication Error,0,"The number of times that OverrunErrors consecutive flow control update periods occurred, each having at least one overrun error", +121,"VL15 Dropped",PM_VL15DROPPED,1,1,1,1,0,0,1,0,1,1,1,Minor,50,300,0,Port,"VL15Dropped counter rate threshold exceeded. Threshold is %d, received value is %d. Peer info: %s.",Communication Error,0,"Number of incoming VL15 packets dropped because of resource limitations in the port", +122,"Congested Bandwidth (%) Threshold Reached",PM_XMITWAITERROR,1,1,1,1,0,0,1,0,4,1,1,Minor,10,300,0,Port,"Congested Bandwidth (in percents) threshold exceeded. Threshold is %d, received value is %d. Peer info: %s.",Communication Error,0,"Percent of Congested Bandwidth has exceeded defined threshold", +123,"Port Bandwidth (%) Threshold Reached",PM_BWXMITTHR,1,1,1,1,0,0,1,0,4,1,1,Minor,95,300,0,Port,"Port Normalized Transmit BW counter threshold exceeded. Threshold is %d, received value is %d. Peer info: %s.",Communication Error,0,"Percent of Port Transmit Bandwidth has exceeded defined threshold", +130,"Non-optimal link width",PHY_NON_OPTIMAL_WIDTH,0,1,1,1,0,0,1,0,5,1,1,Minor,1,0,0,Port,"Found a %s link that operates in %s width mode.",Hardware,0,"Non-optimal link width", +134,"T4 Port Congested Bandwidth",PM_T4XMITWAITERROR,1,1,1,1,0,0,1,0,12,1,1,Warning,10,300,0,Port,"T4 Congested Bandwidth (in percents) threshold exceeded. Threshold is %s received value is %s. Peer info: %s.",Communication Error,0,"Percent of T4 Port Congested Bandwidth has exceeded defined threshold", +141,"Flow Control Update Watchdog Timer Expired",SM_WATCHDOG_TIMER_EXPIRED,0,1,1,1,0,1,0,0,10,1,1,Warning,1,300,0,Port,"Flow Control Update watchdog timer has expired: lid %(lid)d, port #%(portn)d",Hardware,0,"SM Trap. The error indicates a failure of the flow control machine at the other end of the link. If the timer expires without receiving an update, a flow control update error has occurred", +144,"Capability Mask Modified",SM_CAPABILITY_MASK_CHANGED,0,1,1,0,0,0,1,0,10,0,0,Info,1,300,0,Port,"Capability Mask Modified: lid %(lid)d, mask 0x%(mask)08x",Fabric Notification,0,"Capability Mask of the specific LID is modified", +145,"System Image GUID changed",SM_SYS_IMAGE_GUID_CHANGED,0,1,1,1,0,0,1,0,10,1,1,Info,1,300,0,Port,"System Image GUID is changed: %(lid)d, %(systemguid)016x",Communication Error,0,"System GUID is changed for the specific LID", +156,"Link Speed Enforcement Disabled",LINK_SPEED_ENFORCEMENT_DISABLED,0,0,1,1,0,0,1,0,10,0,0,Critical,0,300,0,Site,"Ethernet Gateway(s) identified - Link speed enforcement is disabled in the fabric. Restart the Ethernet Gateway(s) for change to take effect.",Fabric Notification,0,"Link speed enforcement is disabled in the fabric", +250,"Running in Limited Mode",RUNNING_IN_LIMITED_MODE,0,0,1,1,0,0,1,0,75,1,1,Critical,1,0,0,Grid,"%s License has expired %s days ago! UFM is now running in Limited Mode!",Maintenance,0,"Running in Limited Mode", +251,"Switching to Limited Mode",SWITCHING_LIMITED_MODE,0,0,1,1,0,0,1,0,75,1,1,Critical,1,0,0,Grid,"%s License has expired %s days ago! UFM will switch to limited mode in %s days!",Maintenance,0,"Switching to Limited Mode", +252,"License Expired",UFM_LICENSE_EXPIRED,0,0,1,1,0,0,1,0,75,1,1,Warning,1,0,0,Grid,"%s License has expired. Please restart UFM server",Maintenance,0,"License has expired", +253,"Duplicated licenses",UFM_LICENSE_DUPLICATED,0,0,1,1,0,0,1,0,75,1,0,Critical,1,0,0,Grid,"Duplicate Serial Number %s detected on installed licenses. Please refer to your system vendor representative to update your license",Maintenance,0,"Duplicate Serial Number is detected on installed licenses", +254,"License Limit Exceeded",UFM_LICENSE_LIMIT,0,0,1,1,0,0,1,0,75,1,0,Critical,1,0,0,Grid,"Managed fabric size %s. Please refer to your system vendor representative to update your license.",Maintenance,0,"Managed fabric size has exceeded size permitted by license", +255,"License is About to Expire",UFM_LICENSE_EXPIRATION_DATE,0,0,1,1,0,0,1,0,75,1,0,Warning,1,0,0,Grid,"UFM license subscription is going to expire in %s days",Maintenance,0,"License has expired", +256,"Bad M_Key",SM_BAD_MKEY,0,1,1,1,0,1,0,0,10,0,0,Minor,1,300,0,Port,"Bad M_Key: mkey %(mkey)08x from lid %(slid)d trap %(trap_id)d",Security,0,"Found bad Management key. Check your HCA driver or partition settings. Management Key: Enforces the control of a master subnet manager", +257,"Bad P_Key",SM_BAD_PKEY,0,1,1,1,0,1,0,0,10,0,0,Minor,1,300,0,Port,"Bad P_Key: pkey %(pkey)08x from port1(lid %(lid1)d, GID %(prefix_guid1)x%(guid1)x) to port2(lid %(lid2)d GID %(prefix_guid2)x%(guid2)x)",Security,0,"Found a bad Partition key. Check your partitioning settings. Partition Key: Enforces membership. Administered through the subnet manager by the partition manager (PM).", +258,"Bad Q_Key",SM_BAD_QKEY,0,1,1,1,0,1,0,0,10,0,0,Minor,1,300,0,Port,"Bad Q_Key: qkey %(qkey)08x from port1(lid %(lid1)d, GID %(prefix_guid1)x%(guid1)x) to port2(lid %(lid2)d GID %(prefix_guid2)x%(guid2)x)",Security,0,"Found bad Queue key. Security error. Queue Key: Enforces access rights for reliable and unreliable datagram service (RAW datagram service type not included)", +259,"Bad P_Key Switch External Port",SM_BAD_PKEY_EXT,0,1,1,1,0,1,0,0,7,0,0,Minor,1,300,0,Port,"Bad P_Key switch external port: pkey %(pkey)08x from lid %(lid1)d GID %(prefix_guid1)x%(guid1)x to lid %(lid2)d GID %(prefix_guid2)x%(guid2)x at switch lid %(switchlid)d on external port %(portn)d",Security,0,"Found a bad Partition key. Check your partitioning settings. Partition Key: Enforces membership. Administered through the subnet manager by the partition manager (PM)", +271,"ISBL LAG Port Up",ISBL_LAG_PORT_UP,0,2,1,1,0,0,1,0,10,0,0,Info,1,0,0,Port,"ISBL %s LAG port up",Fabric Notification,0,"ISBL LAG Port Up", +272,"ISBL LAG Port Down",ISBL_LAG_PORT_DOWN,0,2,0,1,0,0,1,0,10,0,0,Critical,1,0,0,Port,"ISBL %s LAG port down",Fabric Notification,0,"ISBL LAG Port Down", +273,"LAG Port Up",LAG_PORT_UP,0,2,1,1,0,0,1,0,10,0,0,Info,1,0,0,Port,"LAG %s port up",Fabric Notification,0,"LAG Port Up", +274,"LAG Port Down",LAG_PORT_DOWN,0,2,0,1,0,0,1,0,10,0,0,Warning,1,0,0,Port,"LAG %s port down",Fabric Notification,0,"LAG Port Down", +275,"Port Up",PORT_UP,0,2,1,1,0,0,1,0,10,0,0,Info,1,0,0,Port,"Port %s up",Fabric Notification,0,"Port Up", +276,"Port Down",PORT_DOWN,0,2,0,1,0,0,1,0,10,0,0,Warning,1,0,0,Port,"Port %s down",Fabric Notification,0,"Port Down", +277,"Port of LAG Up",PORT_OF_LAG_UP,0,2,1,1,0,0,1,0,10,0,0,Info,1,0,0,Port,"Port %s of LAG up",Fabric Notification,0,"Port of LAG Up", +278,"Port of LAG Down",PORT_OF_LAG_DOWN,0,2,0,1,0,0,1,0,10,0,0,Warning,1,0,0,Port,"Port %s of LAG down",Fabric Notification,0,"Port of LAG Down", +279,"Port of ISBL Up",PORT_OF_ISBL_UP,0,2,1,1,0,0,1,0,10,0,0,Info,1,0,0,Port,"Port %s of ISBL up",Fabric Notification,0,"Port of ISBL Up", +280,"Port of ISBL Down",PORT_OF_ISBL_DOWN,0,2,0,1,0,0,1,0,10,0,0,Warning,1,0,0,Port,"Port %s of ISBL down",Fabric Notification,0,"Port of ISBL Down", +301,"Logical Server State Changed",STATE_CHANGED,0,0,1,1,0,0,1,0,11,0,0,Info,1,0,0,LogicalServer,"Logical %s changed state from %s to %s",Logical Model,0,"Logical Server state is changed", +302,"Logical Server State Change Failed",WORKFLOW_FAILED,0,0,1,1,0,0,1,0,10,0,0,Minor,1,0,0,LogicalServer,"Logical %s failed to changed state from %s to %s",Logical Model,0,"Indicates error in Logical Server state change. This error might be caused by any error condition related to the Logical Server resources allocation", +306,"Logical Server Added",LS_ADDED,0,0,1,1,0,0,1,0,10,0,0,Info,1,0,0,LogicalServer,"Logical %s %s is added",Logical Model,0,"New Logical Server or Logical Servers Group is created", +307,"Logical Server Removed",LS_REMOVED,0,0,1,1,0,0,1,0,10,0,0,Info,1,0,0,LogicalServer,"Logical %s %s is removed",Logical Model,0,"Logical Server or Logical Servers Group is deleted", +308,"Logical Server Resources Allocated",LS_RES_ALLOC,0,0,1,1,0,0,1,0,10,0,0,Info,1,0,0,LogicalServer,"Logical %s allocated %d Resources",Logical Model,0,"New resources are allocated to the Logical Server", +312,"Compute Resource Released",COMPUTE_FREE,0,0,1,1,0,0,1,0,10,0,0,Info,1,0,0,LogicalServer,"Compute Resource %s is released",Logical Model,0,"A resource is released from the Logical Server", +313,"Compute Resource Allocated",COMPUTE_ALLOC,0,0,1,1,0,0,1,0,11,0,0,Info,1,0,0,LogicalServer,"Compute Resource %s is allocated",Logical Model,0,"A resource is allocated to the Logical Server", +314,"Logical Server Additional Resources Allocated",LS_MORE_RES_ALLOC,0,0,1,1,0,0,1,0,11,0,0,Info,1,0,0,LogicalServer,"Logical %s allocated %d Additional Resources",Logical Model,0,"Additional resources are allocated to the Logical Server", +315,"Logical Server Resources Released",LS_RES_RELEASE,0,0,1,1,0,0,1,0,11,0,0,Info,1,0,0,LogicalServer,"Logical %s released %d Resources",Logical Model,0,"Resources were released from the Logical Server", +316,"Logical Server Compute Resource is Down",COMPUTE_FAILED,0,0,0,1,0,0,1,0,57,1,1,Critical,1,0,0,LogicalServer,"Logical %s Compute Resource %s is Down",Logical Model,0,"An allocated resource is Down or Disconnected", +317,"Logical Server Compute Resource is Up",COMPUTE_OK,0,0,0,1,0,0,1,0,58,1,1,Info,1,0,0,LogicalServer,"Logical %s Compute Resource %s is Up",Logical Model,0,"An allocated resources is Up or Connected back", +328,"Link is Up",LINK_UP,0,0,1,1,0,0,1,0,10,0,0,Info,1,0,0,Site,"Link is up: %s ",Fabric Topology,0,"Event is sent upon discovery of a new link", +329,"Link is Down",LINK_DOWN,0,0,1,1,0,0,1,0,10,0,0,Warning,1,0,0,Site,"Link went down: %s ",Fabric Topology,0,"Event is sent when exiting link is removed", +331,"Node is Down",NODE_FAILURE,0,0,1,1,0,0,1,0,10,0,0,Warning,1,0,0,Site,"Site configuration changes: %s node is Down",Fabric Topology,0,"Node is disconnected or down", +332,"Node is Up",NODE_OK,0,0,1,1,0,0,1,0,10,0,0,Info,1,300,0,Site,"Site configuration changes: %s node is Up",Fabric Topology,0,"Node is connected or up", +336,"Port Action Succeeded",NODE_PORT_ACTION_OK,0,0,1,1,0,0,1,0,10,0,0,Info,1,0,0,Port,"Port %s action %s succeeded",Maintenance,0,"Port Management Action (reset, disable) succeeded", +337,"Port Action Failed",NODE_PORT_ACTION_FAIL,0,0,0,1,0,0,1,0,10,0,0,Minor,1,0,0,Port,"Port %s action %s failed",Maintenance,0,"Port Management Action (reset, disable) failed", +338,"Device Action Succeeded",DEVICE_ACTION_OK,0,0,1,1,0,0,1,0,10,0,0,Info,1,0,0,Port,"Action %s on device %s(%s) succeeded",Maintenance,0,"Device Management Action succeeded", +339,"Device Action Failed",DEVICE_ACTION_FAIL,0,0,0,1,0,0,1,0,10,0,0,Minor,1,0,0,Port,"Action %s on device %s(%s) failed",Maintenance,0,"Device Management Action failed", +340,"Network Interface Added",UFM_IFC_ADDED,0,0,1,1,0,0,1,0,10,0,0,Info,1,0,0,LogicalServer,"Network Interface %s is added",Logical Model,0,"New Network Interface is created", +341,"Network Interface Removed",UFM_IFC_REMOVED,0,0,1,1,0,0,1,0,10,0,0,Info,1,0,0,LogicalServer,"Network Interface %s is removed",Logical Model,0,"Network Interface is deleted", +344,"Partial Switch ASIC Failure",PARTIAL_SWITCH_ASIC_FAILURE,0,1,1,1,0,0,1,0,10,1,1,Critical,1,0,0,Switch,"Number of switch unhealthy ports exceeded the defined threshold which is (%d) percent of the total switch ports.",Maintenance,0,"Number of unhealthy ports threshold of total number of switch ports was exceeded.", +350,"Environment Added",ENV_ADDED,0,0,0,1,0,0,1,0,10,0,0,Info,1,0,0,Environment,"Environment %s is added",Logical Model,0,"New Logical Environment is created", +351,"Environment Removed",ENV_REMOVED,0,0,0,1,0,0,1,0,10,0,0,Info,1,0,0,Environment,"Environment %s is removed",Logical Model,0,"Logical Environment is deleted", +352,"Network Added",NET_ADDED,0,0,0,1,0,0,1,0,10,0,0,Info,1,0,0,Network,"Network %s is added",Logical Model,0,"New Network is created", +353,"Network Removed",NET_REMOVED,0,0,0,1,0,0,1,0,10,0,0,Info,1,0,0,Network,"Network %s is removed",Logical Model,0,"Network is deleted", +358,"VMM bad access credentials",VMM_BAD_AC,0,2,1,1,0,0,1,0,9,1,1,Warning,1,300,0,Site,"VMM %s bad access credentials",Communication Error,0,"VMM bad access credentials", +359,"VM Created",VM_CREATED,0,2,1,1,0,0,1,0,10,0,0,Info,1,300,0,Site,"Site configuration changes: VM %s Created",Fabric Topology,0,"VM Created", +360,"VM Removed",VM_REMOVED,0,2,1,1,0,0,1,0,10,0,0,Info,1,300,0,Site,"Site configuration changes: VM %s Removed",Fabric Topology,0,"VM Removed", +361,"VMM is Down",VMM_IS_DOWN,0,2,1,1,0,0,1,0,10,0,0,Warning,1,0,0,Site,"Site configuration changes: VMM %s is Down",Fabric Topology,0,"VMM is Down", +362,"VMM is Up",VMM_IS_UP,0,2,1,1,0,0,1,0,10,0,0,Info,1,300,0,Site,"Site configuration changes: VMM %s is Up",Fabric Topology,0,"VMM is Up", +364,"VM state changed",VM_STATE_CHANGED,0,2,1,1,0,0,1,0,10,0,0,Info,1,300,0,Site,"Site configuration changes: VM %s state changed",Fabric Topology,0,"VM state changed", +365,"VM is Down",VM_IS_DOWN,0,2,1,1,0,0,1,0,10,0,0,Info,1,0,0,Site,"Site configuration changes: VM %s is Down",Fabric Topology,0,"VM is Down", +366,"VM is Up",VM_IS_UP,0,2,1,1,0,0,1,0,10,0,0,Info,1,300,0,Site,"Site configuration changes: VM %s is Up",Fabric Topology,0,"VM is Up", +367,"VM migration failed",VM_MIGRATION_FAILED,0,2,1,1,0,0,1,0,10,0,0,Info,1,300,0,Site,"Site configuration changes: VM %s migration failed",Fabric Topology,0,"VM migration failed", +368,"VM has been migrated",VM_HAS_MIGRATED,0,2,1,1,0,0,1,0,10,0,0,Info,1,300,0,Site,"Site configuration changes: VM %s has been migrated",Fabric Topology,0,"VM has been migrated", +369,"VM alarm status has changed",VM_ALARM_STATUS_CHANGED,0,2,1,1,0,0,1,0,10,0,0,Warning,1,300,0,Site,"Site configuration changes: VM %s alarm status changed",Fabric Topology,0,"VM alarm status has changed", +370,"Gateway Ethernet Link State Changed",GW_VOL10G_LINK_ETH_STATE,0,1,1,1,0,0,1,0,10,0,0,Warning,1,0,0,Gateway,"Ethernet port %d: link has changed its state to %s",Gateway,0,"Gateway Ethernet Physical link has changed state", +371,"Gateway Re-register Event Received",GW_VOL10G_IPR_SMREGISTER_TO_EVENTS,0,1,1,1,0,0,1,0,10,0,0,Warning,1,0,0,Gateway,"Gateway received a re-register event from the SM. Total number of re-register events during uptime: %d",Gateway,0,"10GbE Gateway received a re-register event from the SM.", +372,"Number of Gateways is Changed",GW_VOL10G_NUM_ROUTERS_CHANGE,0,1,1,1,0,0,1,0,10,0,0,Warning,1,0,0,Gateway,"Change in the number of 10GbE Gateways has been detected in interface %s new number is %s",Gateway,0,"Change in the number of 10GbE Gateways has been detected", +373,"Gateway will be Rebooted",GW_VOL10G_IPR_GOING_TO_REBOOT,0,1,1,1,0,0,1,0,10,0,0,Warning,1,0,0,Gateway,"Gateway is about to reboot. Reason is %s",Gateway,0,"10GbE Gateway is about to reboot", +374,"Gateway Reloading Finished",GW_VOL10G_IPR_FINISHED_TO_LOAD,0,1,1,1,0,0,1,0,10,0,0,Info,1,0,0,Gateway,"10GbE Gateway %s has finished reloading",Gateway,0,"10GbE Gateway has finished reloading", +375,"Vcenter is Down ",VCENTER_IS_DOWN,0,2,1,1,0,0,1,0,9,1,1,Warning,1,300,0,Site,"Vcenter %s is down",Communication Error,0,"Vcenter is Down ", +376,"Vcenter bad access credentials ",VCENTER_BAD_AC,0,2,1,1,0,0,1,0,9,1,1,Warning,1,300,0,Site,"Vcenter %s bad access credentials",Communication Error,0,"Vcenter bad access credentials ", +380,"Switch Upgrade Error",FW_UPGRADE_FAILED,0,1,1,1,0,0,1,0,10,1,1,Critical,1,0,0,Switch,"Firmware upgrade on switch %s (%s) failed",Maintenance,0,"Firmware upgrade on switch has failed", +381,"Switch Upgrade Error",SW_UPGRADE_FAILED,0,1,1,1,0,0,1,0,10,1,1,Critical,1,0,0,Switch,"Software upgrade on switch %s (%s) failed",Maintenance,0,"Software upgrade on switch has failed", +383,"Host Upgrade Failed",HOST_UPGRADE_FAILED,0,1,1,1,0,0,1,0,10,1,1,Warning,1,0,0,Computer,"Host Upgrade for %s failed - See UFM and host (/var/log/messages) logs",Maintenance,0,"Host Upgrade has failed", +384,"Switch Module Powered Off",MODULE_STATUS_POWERED_OFF,0,0,1,1,0,0,1,0,55,1,1,Info,1,420,0,Switch,"Module %s %s on %s(%s) status is %s",Module Status,0,"Module status Powered Off", +385,"Switch FW Upgrade Started",SW_FW_UPGRADE_STARTED,0,1,1,1,0,0,1,0,10,0,0,Info,1,0,0,Switch,"Switch FW upgrade for %s has started",Maintenance,0,"Switch FW Upgrade process has started", +386,"Switch SW Upgrade Started",SW_SW_UPGRADE_STARTED,0,1,1,1,0,0,1,0,10,0,0,Info,1,0,0,Switch,"Switch SW upgrade for %s has started",Maintenance,0,"Switch SW Upgrade process has started", +387,"Switch Upgrade Finished",SW_UPGRADE_FINISHED,0,1,1,1,0,0,1,0,10,0,0,Info,1,0,0,Switch,"Switch upgrade for %s finished",Maintenance,0,"Switch Upgrade Finished", +388,"Host FW Upgrade Started",HOST_FW_UPGRADE_STARTED,0,1,1,1,0,0,1,0,10,0,0,Info,1,0,0,Computer,"Host FW upgrade for %s has started",Maintenance,0,"Host FW Upgrade process has started", +389,"Host OFED Upgrade Started",HOST_SW_UPGRADE_STARTED,0,1,1,1,0,0,1,0,10,0,0,Info,1,0,0,Computer,"Host OFED upgrade for %s has started",Maintenance,0,"Host SW Upgrade process has started", +382,"Module status NOT PRESENT",MODULE_STATUS_NOT_PRESENT,0,0,1,1,0,0,1,0,53,1,1,Warning,1,420,0,Switch,"Module %s %s on %s(%s) status is %s",Module Status,0,"Module status NOT PRESENT", +391,"Switch Module Removed",SWITCH_MODULE_REMOVED,0,0,1,1,0,0,1,0,10,1,0,Info,1,0,0,Switch,"Module %s %s was removed from switch %s",Fabric Notification,0,"Module (line card, FAN or PS) is removed from the switch", +392,"Module Temperature Threshold Reached",MODULE_TEMPERATURE_EXCESS,1,0,1,1,0,1,0,0,10,1,1,Minor,60,300,0,Module,"Module Temperature threshold was exceeded. Threshold is %d, received value is %d.",Hardware,0,"Temperature detected by module sensor is too high, has exceeded the defined threshold", +393,"Switch Module Added",SWITCH_MODULE_ADDED,0,0,1,1,0,0,1,0,10,1,0,Info,1,0,0,Switch,"Module %s %s was added to switch %s",Fabric Notification,0,"Switch Module Added", +394,"Module status FAULT",MODULE_STATUS_FAULT,0,0,1,1,0,0,1,0,55,1,1,Critical,1,420,0,Switch,"Module %s %s on %s(%s) status is %s",Module Status,0,"Module status FAULT", +395,"Device Action Started",DEVICE_ACTION_STARTED,0,0,1,1,0,0,1,0,10,0,0,Info,1,0,0,Port,"Action %s started on device %s(%s)",Maintenance,0,"Device Action Started", +396,"Site Action Started",SITE_ACTION_STARTED,0,0,1,1,0,0,1,0,10,0,0,Info,1,0,0,Port,"Action %s started on site %s",Maintenance,0,"Site Action Started", +397,"Site Action Failed",SITE_ACTION_FAIL,0,0,0,1,0,0,1,0,10,0,0,Minor,1,0,0,Port,"Action %s on site %s failed",Maintenance,0,"Site Action Failed", +398,"Switch Chip Added",SWITCH_CHIP_ADDED,0,0,1,1,0,0,1,0,10,1,0,Info,1,0,0,Switch,"Chip %s on module %s %s was added to switch %s",Fabric Notification,0,"Switch Chip Added", +399,"Switch Chip Removed",SWITCH_CHIP_REMOVED,0,0,1,1,0,0,1,0,10,1,0,Critical,1,0,0,Switch,"Chip %s on module %s %s was removed from switch %s",Fabric Notification,0,"Switch Chip Removed", +403,"Device Pending Reboot",DEVICE_PENDING_REBOOT,0,0,1,1,0,1,0,0,10,1,1,Warning,0,300,0,Device,"The device software has been updated. Please reboot the device for changes to take effect. The current version is %s, upon next boot, the version will be %s.",Maintenance,0,"Reboot device after software updated", +404,"System Information is missing",SYS_INFO_MISSING,0,1,1,1,0,0,1,0,72,1,1,Warning,1,300,0,Switch,"Could not retrieve system information for switch %s: %s.",Communication Error,0,"System Information for switch is missing", +405,"Switch Identity Validation Failed",SWITCH_IDENTITY_VALIDATION_FAILED,0,1,1,1,0,0,1,0,77,1,1,Warning,1,300,0,Switch,"Identity Validation failed for switch %s: Identity found (%s) does not match device ID (%s).",Communication Error,0,"System Information for switch is missing", +406,"Switch System Information is missing",SWITCH_SYS_INFO_MISSING_EXT,0,1,1,1,0,0,1,0,72,1,1,Warning,1,300,0,Switch,"Could not retrieve system information for switch %s: %s.",Communication Error,0,"System Information for switch is missing", +407,"COMEX Ambient Temperature Threshold Reached",COMEX_TEMPERATURE_EXCESS,1,0,1,1,0,1,0,0,10,1,1,Minor,60,300,0,Switch,"COMEX Ambient Temperature threshold was exceeded. Threshold is %d, received value is %d.",Hardware,0,"Temperature detected by COMEX ambient sensor is too high, has exceeded the defined threshold", +502,"Device Upgrade Finished",PHY_UPG_FINISHED,0,1,1,1,0,0,1,0,10,0,0,Info,1,300,0,Device,"Device Upgrade has been completed successfully.",Maintenance,0,"Device SW or FW Upgrade has finished", +506,"Device Upgrade Finished",PHY_UPG_FINISHED_ON_SWITCH,0,1,1,1,0,0,1,0,10,0,0,Info,1,300,0,Device,"Switch Upgrade has been completed successfully. Please reboot the switch for changes to take effect.",Maintenance,0,"Device SW or FW Upgrade has finished", +508,"Core Dump Created",CORE_DUMP_CREATED,0,1,1,1,0,0,1,0,25,1,1,Info,1,300,0,Grid,"New core dump %s was just created.",Maintenance,0,"Core dump was created", +510,"SM Failover",SM_FAILOVER,1,1,1,0,0,0,1,0,22,1,1,Critical,1,300,0,Grid,"SM Failover. New SM is running on %s, GUID %s",Fabric Notification,0,"SM Failover", +511,"SM State Change",SM_STATE_CHANGE,0,1,1,0,0,0,1,0,24,1,1,Info,1,300,0,Grid,"The state of SM is changed to %s",Fabric Notification,0,"SM state is changed", +512,"SM UP",SM_Up,1,1,1,0,0,0,1,0,20,1,1,Info,1,300,0,Grid,"SM is UP on device %s, guid %s",Fabric Notification,0,"SM is UP", +513,"SM System Log Message",SM_SYS_LOG_MSG,1,1,1,0,0,0,1,0,23,1,1,Minor,1,300,0,Grid,"SM: %s",Fabric Notification,0,"SM System Log Message", +514,"SM LID Change",SM_LID_CHANGE,0,1,1,0,0,0,1,0,24,1,1,Warning,1,300,0,Grid,"SM lid of port %(guid)016x is changed",Fabric Notification,0,"SM lid is changed", +515,"Fabric Health Report Info",FABRIC_HEALTH_REPORT_INFO,0,1,1,1,0,0,1,0,26,1,1,Info,1,300,0,Grid,"FabricHealth Report completed without Errors or Warnings",Fabric Notification,0,"Fabric Health Report Info", +516,"Fabric Health Report Warning",FABRIC_HEALTH_REPORT_WARNING,0,1,1,1,0,0,1,0,27,1,1,Warning,1,300,0,Grid,"FabricHealth Report completed with %s Warnings",Fabric Notification,0,"Fabric Health Report Warning", +517,"Fabric Health Report Error",FABRIC_HEALTH_REPORT_ERROR,0,1,1,1,0,0,1,0,28,1,1,Critical,1,300,0,Grid,"FabricHealth Report completed with %s Errors and %s Warnings",Fabric Notification,0,"Fabric Health Report Error", +518,"UFM-related process is down",UFM_PROCESS_DOWN,0,0,1,1,0,0,1,0,29,1,1,Critical,1,300,0,Grid,"Process %s is down.",Maintenance,0,"UFM related process is down", +519,"Logs purge failure",LOGS_PURGE_FAILURE,0,0,1,1,0,0,1,0,30,1,1,Minor,1,300,0,Grid,"Failed purging database logs.",Maintenance,0,"Failed purging database logs", +520,"Restart of UFM-related process succeeded",RESTART_PROCESS_SUCCESS,0,0,1,1,0,0,1,0,31,1,1,Info,1,300,0,Grid,"Process %s was restarted successfully.",Maintenance,0,"UFM-related process was restarted successfully", +521,"UFM is being stopped",STOPPING_UFM,0,0,1,1,0,0,1,0,32,1,1,Critical,1,300,0,Grid,"Stopping UFM server now...",Maintenance,0,"Stopping UFM server", +522,"UFM is being restarted",RESTARTING_UFM,0,0,1,1,0,0,1,0,33,1,1,Critical,1,300,0,Grid,"Restarting UFM server now...",Maintenance,0,"Restarting UFM server", +523,"UFM failover is being attempted",ATTEMPTING_UFM_FAILOVER,0,0,1,1,0,0,1,0,34,1,1,Info,1,300,0,Grid,"Attempting UFM failover...",Maintenance,0,"Attempting UFM failover", +524,"UFM cannot connect to DB",CANNOT_CONNECT_TO_DB,0,0,1,1,0,0,1,0,35,1,1,Critical,1,300,0,Grid,"Connection to the database failed.",Maintenance,0,"Connection to the database failed", +525,"Disk utilization threshold reached",DISK_THRESHOLD_REACHED,0,0,1,1,0,0,1,0,36,1,1,Critical,1,300,0,Grid,"Disk space usage in %s is above the threshold of %d",Maintenance,0,"Disk utilization threshold reached", +526,"Memory utilization threshold reached",MEMORY_THRESHOLD_REACHED,0,0,1,1,0,0,1,0,37,1,1,Critical,1,300,0,Grid,"Memory usage is above the threshold of %d",Maintenance,0,"Memory utilization threshold reached", +527,"CPU utilization threshold reached",CPU_THRESHOLD_REACHED,0,0,1,1,0,0,1,0,38,1,1,Critical,1,300,0,Grid,"CPU usage is above the threshold of %d",Maintenance,0,"CPU utilization threshold reached", +528,"Fabric interface is down",FABRIC_IFACE_DOWN,0,0,1,1,0,0,1,0,39,1,1,Critical,1,300,0,Grid,"Fabric interface %s is down.",Maintenance,0,"Fabric interface is down", +529,"UFM standby server problem",UFM_STANDBY_PROBLEM,0,0,1,1,0,0,1,0,40,1,1,Critical,1,300,0,Grid,"Problem with UFM standby server: %s.",Maintenance,0,"UFM standby server problem", +530,"SM is down",SM_IS_DOWN,0,0,1,1,0,0,1,0,21,1,1,Critical,1,300,0,Grid,"SM is down (%s).",Maintenance,0,"SM is down", +531,"DRBD Bad Condition",DRBD_BAD_COND,0,0,1,1,0,0,1,0,41,1,1,Critical,1,300,0,Grid,"Drbd bad condition detected, failover or takeover will fail.",Maintenance,0,"DRBD Bad Condition", +532,"Remote UFM-SM Sync",EXTR_UFM_SM_SYNC,0,0,1,1,0,0,1,0,0,0,1,Info,1,0,0,Grid,"Remote UFM-SM %s sync succeeded",Maintenance,0,"Remote UFM-SM Sync", +533,"Remote UFM-SM problem",EXTR_UFM_SM_PROBLEM,0,0,1,1,0,0,1,0,59,1,1,Critical,1,0,0,Site,"%s",Maintenance,0,"Remote UFM-SM problem", +535,"MH Purge Failed",MH_PURGE_FAILED,0,0,1,1,0,0,1,0,42,1,1,Warning,1,300,0,Grid,"Monitoring history DB purge failed (%s)",Maintenance,0,"MH Purge Failed", +536,"UFM Health Watchdog Info",UFM_HEALTH_WATCHDOG_INFO,0,0,1,1,0,0,1,0,43,1,1,Info,1,300,0,Grid,"Message",Maintenance,0,"UFM Health Watchdog Info", +537,"UFM Health Watchdog Critical",UFM_HEALTH_WATCHDOG_CRITICAL,0,0,1,1,0,0,1,0,44,1,1,Critical,1,300,0,Grid,"Message",Maintenance,0,"UFM Health Watchdog Critical", +538,"Time Diff Between HA Servers",HA_TIME_DIFF,0,0,1,1,0,0,1,0,41,1,1,Warning,1,300,0,Grid,"Time difference between master and standby machines is above the threshold of %d seconds. Master time is: %s, standby time is: %s.",Maintenance,0,"Time Diff Between HA Servers", +539,"DRBD TCP Connection Performance",DRBD_BAD_CONNECTION_PERFORMANCE,0,0,1,1,0,0,1,0,74,1,1,Warning,1,900,0,Grid,"Message",Maintenance,0,"DRBD TCP Connection Performance", +540,"Daily Report Completed successfully",DAILY_REPORT_COMPLETED_SUCCESSFULLY,0,0,1,1,0,0,1,0,0,0,0,Info,1,300,0,Grid,"%s",Maintenance,0,"Daily Report Completed successfully", +541,"Daily Report Completed with Error",DAILY_REPORT_COMPLETED_WITH_ERROR,0,0,1,1,0,0,1,0,0,0,0,Minor,1,300,0,Grid,"%s",Maintenance,0,"Daily Report Completed with Error", +542,"Daily Report Failed",DAILY_REPORT_FAILED,0,0,1,1,0,0,1,0,0,0,0,Critical,1,300,0,Grid,"%s",Maintenance,0,"Daily Report Failed", +543,"Daily Report Mail Sent successfully",DAILY_REPORT_MAIL_SENT_SUCCESSFULLY,0,0,1,1,0,0,1,0,0,0,0,Info,1,300,0,Grid,"%s",Maintenance,0,"Daily Report Mail Sent successfully", +544,"Daily Report Mail Sent Failed",DAILY_REPORT_MAIL_SENT_FAIL,0,0,1,1,0,0,1,0,0,0,0,Minor,1,300,0,Grid,"%s",Maintenance,0,"Daily Report Mail Sent Failed", +545,"SM is not responding",SM_NOT_RESPONDING,0,0,1,1,0,0,1,0,78,1,1,Critical,1,300,0,Grid,"SM is not responding (%s).",Maintenance,0,"SM is not responding", +546,"Management interface is down",MGMT_IFC_DOWN,0,0,1,1,0,0,1,0,46,1,1,Critical,1,300,0,Grid,"Management interface %s is down.",Maintenance,0,"Management interface is down", +547,"UFM stopped polling SM for updates",STOP_POLLING_SM,0,0,1,1,0,0,1,0,78,1,1,Critical,1,300,0,Grid,"UFM stopped polling SM for updates for more than (%s) seconds.",Maintenance,0,"UFM stopped polling SM for updates", +548,"Failed to run generic script test",FAILED_TO_RUN_GENERIC_SCRIPT_TEST,0,0,1,1,0,0,1,0,29,1,1,Critical,1,300,0,Grid,"Failed to run generic script with name %s. Error: %s.",Maintenance,0,"Failed to run generic script test", +551,"General External Event Notification",GENERAL_EXTERNAL_EVENT_NOTIFICATION,0,1,1,1,0,0,1,0,75,1,1,Info,0,1,0,Grid,"%s",Maintenance,0,"General External Event Notification", +552,"General External Event Notice",GENERAL_EXTERNAL_EVENT_NOTICE,0,1,1,1,0,0,1,0,75,1,1,Minor,0,1,0,Grid,"%s",Maintenance,0,"General External Event Notice", +553,"General External Event Alert",GENERAL_EXTERNAL_EVENT_ALERT,0,1,1,1,0,0,1,0,75,1,1,Warning,0,1,0,Grid,"%s",Maintenance,0,"General External Event Alert", +554,"General External Event Error",GENERAL_EXTERNAL_EVENT_ERROR,0,1,1,1,0,0,1,0,75,1,1,Critical,0,1,0,Grid,"%s",Maintenance,0,"General External Event Error", +560,"User Connected", USER_CONNECTED, 0,0,1,1,0,0,1,0,0,0,0,Info,1,0,0,Grid,"User %s connected",Security,0,"User Connected", +561,"User Disconnected", USER_DISCONNECTED, 0,0,1,1,0,0,1,0,0,0,0,Info,1,0,0,Grid,"User %s disconnected",Security,0,"User Disconnected", +602,"UFM Server Failover",UFM_FAIL_OVER,0,0,1,1,0,0,1,0,10,1,1,Critical,1,0,0,Site,"Server %s failed, server %s took ownership",Fabric Notification,0,"UFM Server Failover", +603,"Events Suppression",EVENTS_SUPPRESSION,0,0,1,1,0,0,1,0,10,0,0,Critical,0,300,0,Site,"Due to high CPU utilization detected, %s events will be suppressed",Maintenance,0,"Events Suppression", +604,"Report Succeeded",REPORT_SUCCEEDED,0,0,1,1,0,0,0,0,0,0,1,Info,1,300,0,Grid,"%s Report succeeded",Maintenance,0,"Report Succeeded", +605,"Report Failed",REPORT_FAILED,0,0,1,1,0,0,1,0,45,1,1,Critical,1,300,0,Grid,"%s Report failed, %s",Maintenance,0,"Report Failed", +606,"Correction Attempts Paused",CORRECTION_ATTEMPTS_PAUSED,0,0,1,1,0,0,1,1,0,0,0,Warning,1,0,0,Site,"Correction attempts for a severe UFM health condition were unsuccessful and therefore are temporarily paused. UFM will resume the attempts later. Please see collected system state at: %s.",Fabric Notification,0,"Correction Attempts Paused", +610,"Monitoring History Enabled",MH_ENABLED,0,0,1,1,0,0,1,0,0,0,1,Info,1,300,0,Grid,"Monitoring History Enabled",Maintenance,0,"Monitoring History Enabled", +611,"Monitoring History Disabled",MH_DISABLED,0,0,1,1,0,0,1,0,0,0,1,Info,1,300,0,Grid,"Monitoring History Disabled",Maintenance,0,"Monitoring History Disabled", +612,"Monitoring History Bad Connection",MH_BAD_CONNECTION, 0,0,1,1,0,0,1,0,60,1,1,Critical,1,30,0,Grid,"Monitoring History failed connect to History DB",Maintenance,0,"Monitoring History Bad Connection", +613,"Monitoring History Connection Established",MH_CONNECTION_ESTABLISHED, 0,0,1,1,0,0,1,0,0,0,1,Info,1,30,0,Grid,"Monitoring History connection established",Maintenance,0,"Monitoring History Connection Established", +614,"Monitoring History Inconsistent Version",MH_BAD_VERSION,0,0,1,1,0,1,0,0,61,1,1,Critical,1,300,0,Grid,"Monitoring History inconsistent version, UFM version %s, MH version %s", Maintenance,0,"Monitoring History Inconsistent Version", +615,"Monitoring History Failed save metadata",MH_SAVE_METADATA_ERROR,0,0,1,1,0,0,1,0,62,1,1,Critical,1,300,0,Grid,"Monitoring History failed save %s session metadata, %s", Maintenance,0,"Monitoring History Failed save metadata", +616,"Monitoring History Failed update metadata",MH_UPDATE_METADATA_ERROR,0,0,1,1,0,0,1,0,63,1,1,Critical,1,300,0,Grid,"Monitoring History failed update %s session metadata, %s", Maintenance,0,"Monitoring History Failed update metadata", +617,"Monitoring History Failed save data",MH_SAVE_DATA_ERROR,0,0,1,1,0,0,1,0,64,1,1,Critical,1,300,0,Grid,"Monitoring History failed save %s session data, %s", Maintenance,0,"Monitoring History Failed save data", +618,"Monitoring History Failed get metadata",MH_GET_METADATA_ERROR,0,0,1,1,0,0,1,0,65,1,1,Critical,1,300,0,Grid,"Monitoring History failed get history metadata, %s", Maintenance,0,"Monitoring History Failed get metadata", +619,"Monitoring History Failed get data",MH_GET_DATA_ERROR,0,0,1,1,0,0,1,0,66,1,1,Critical,1,300,0,Grid,"Monitoring History failed get %s session history data, %s", Maintenance,0,"Monitoring History Failed get data", +620,"Monitoring History Failed remove file",MH_REMOVE_FILE_ERROR,0,0,1,1,0,0,1,0,67,1,1,Critical,1,300,0,Grid,"Monitoring History failed remove file %s, %s", Maintenance,0,"Monitoring History Failed remove file", +621,"Monitoring History version not matches UFM version",MH_MATCH_VERION_ERROR,0,0,1,1,0,0,1,0,68,1,1,Critical,1,300,0,Grid,"Monitoring History version %s not matches UFM version %s", Maintenance,0,"Monitoring History version not matches UFM version", +622,"Monitoring History Purge DB Occurred",MH_PURGE_DB_OCCURRED,1,1,1,1,0,0,1,0,69,1,1,Warning,1,5,0,Grid,"Purged monitoring history database due to lack of space on partition.",Fabric Notification,0,"Monitoring History Purge DB Occurred", +623,"Monitoring History Migration is not completed",MH_MIGRATION_NOT_COMPLETED,1,1,1,1,0,0,1,0,70,1,1,Warning,1,300,0,Grid,"Monitoring History Migration is not completed.",Maintenance,0,"Monitoring History Migration is not completed", +624,"Monitoring History partition utilization threshold reached",MH_DISK_THRESHOLD_REACHED,0,0,1,1,0,0,1,0,36,1,1,Critical,1,300,0,Grid,"Monitoring history local disk utilization is %d, the threshold is %d.",Maintenance ,0,"Monitoring History partition utilization threshold reached", +625,"Monitoring History local report files are about to be cleaned",MH_LOCAL_REPORTS_CLEAN,0,0,1,1,0,0,1,0,0,0,1,Info,1,300,0,Grid,"About to remove local reports of Monitoring history that were not accessed recently",Maintenance ,0,"Monitoring History local report files are about to be cleaned", +626,"Monitoring History oldest DB table is about to be removed",MH_OLDEST_TABLE_DROP,0,0,1,1,0,0,1,0,0,0,1,Info,1,300,0,Grid,"About to drop the oldest DB table of Monitoring history",Maintenance,0,"Monitoring History oldest DB table is about to be removed", +627,"Monitoring History partition is not mounted",MH_PART_NOT_MOUNTED,0,0,1,1,0,0,1,0,0,0,1,Critical,1,300,0,Grid,"Monitoring history partition %s is not mounted",Maintenance,0,"Monitoring History partition is not mounted", +701,"Non-optimal Link Speed",PHY_NON_OPTIMAL_SPEED,0,1,1,1,0,0,1,0,5,1,1,Minor,1,0,0,Port,"Found a %s link that operates in %s speed mode.",Hardware,0,"Non-optimal Link Speed", +702,"Unhealthy IB Port",UNHEALTHY_IB_PORT,0,1,1,1,0,0,1,0,71,1,1,Warning,1,0,0,Port,"Peer Port %s is considered by SM as unhealthy due to %s.",Hardware,0,"Unhealthy IB Port", +703,"Fabric Collector Connected",FC_CONNECTED,0,0,1,1,0,0,1,0, 0,0,0,Info,1,0,0,Grid,"Fabric collector on host '%s' has been connected",Maintenance,0,"Fabric Collector Connected", +704,"Fabric Collector Disconnected",FC_DISCONNECTED,0,0,1,1,0,0,1,0,73,1,1,Critical,1,0,0,Grid,"Fabric collector on host '%s' has been disconnected",Maintenance,0,"Fabric Collector Disconnected", +750,"High data retransmission count on port",HIGH_DATA_RETRANSMISSION,0,1,1,1,0,0,1,0,76,1,1,Warning,500,0,0,Port,"High number of data retransmissions was detected on port %s. Cable might need to be replaced.",Hardware,0,"High number of data retransmissions", +801,"Drop Events",SNMP_DROP_EVENTS,1,2,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"Drop Events counter rate threshold was exceeded. Threshold is %d, received value is %d.",Communication Error,0,"Drop Events", +802,"CRC Align Errors",SNMP_CRC_ALIGN_ERRORS,1,2,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"CRC Align Errors counter rate threshold was exceeded. Threshold is %d, received value is %d.",Communication Error,0,"CRC Align Errors", +803,"Undersize Pkts",SNMP_UNDERSIZE_PKTS,1,2,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"Undersize Packets counter rate threshold was exceeded. Threshold is %d, received value is %d.",Communication Error,0,"Undersize Pkts", +804,"Oversize Pkts",SNMP_OVERSIZE_PKTS,1,2,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"Oversize Packets counter rate threshold was exceeded. Threshold is %d, received value is %d.",Communication Error,0,"Oversize Pkts", +805,"Fragments",SNMP_FRAGMENTS,1,2,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"Fragments counter rate threshold was exceeded. Threshold is %d, received value is %d.",Communication Error,0,"Fragments", +806,"Jabbers",SNMP_JABBERS,1,2,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"Jabbers counter rate threshold was exceeded. Threshold is %d, received value is %d.",Communication Error,0,"Jabbers", +807,"Collisions",SNMP_COLLISIONS,1,2,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"Collisions counter rate threshold was exceeded. Threshold is %d, received value is %d.",Communication Error,0,"Collisions", +808,"Alignment Errors",SNMP_HCALIGNMENT_ERRORS,1,2,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"Alignment Errors counter rate threshold was exceeded. Threshold is %d, received value is %d.",Communication Error,0,"Alignment Errors", +809,"FCS Errors",SNMP_HCFCSERRORS,1,2,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"FCS Errors counter rate threshold was exceeded. Threshold is %d, received value is %d.",Communication Error,0,"FCS Errors", +810,"Single Collision Frames",SNMP_SINGLE_COLLISION_FRAMES,1,2,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"Single Collision Frames counter rate threshold was exceeded. Threshold is %d, received value is %d.",Communication Error,0,"Single Collision Frames", +811,"Multiple Collision Frames",SNMP_MULTIPLE_COLLISION_FRAMES,1,2,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"Multiple Collision Frames counter rate threshold was exceeded. Threshold is %d, received value is %d.",Communication Error,0,"Multiple Collision Frames", +812,"SQE Test Errors",SNMP_SQETEST_ERRORS,1,2,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"SQE Test Errors counter rate threshold was exceeded. Threshold is %d, received value is %d.",Communication Error,0,"SQE Test Errors", +813,"Deferred Transmissions",SNMP_DEFERRED_TRANSMISSIONS,1,2,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"Deferred Transmissions counter rate threshold was exceeded. Threshold is %d, received value is %d.",Communication Error,0,"Deferred Transmissions", +814,"Late Collisions",SNMP_LATE_COLLISIONS,1,2,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"Late Collisions counter rate threshold was exceeded. Threshold is %d, received value is %d.",Communication Error,0,"Late Collisions", +815,"Excessive Collisions",SNMP_EXCESSIVE_COLLISIONS,1,2,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"Excessive Collisions counter rate threshold was exceeded. Threshold is %d, received value is %d.",Communication Error,0,"Excessive Collisions", +816,"Internal Mac Transmit Errors",SNMP_HCINTERNAL_MAC_TRANSMIT_ERRORS,1,2,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"Internal MAC Transmit Error counter rate threshold was exceeded. Threshold is %d, received value is %d.",Communication Error,0,"Internal Mac Transmit Errors", +817,"Internal Mac Receive Errors",SNMP_HCINTERNALMACRECEIVEERRORS,1,2,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"Internal MAC Receive Error counter rate threshold was exceeded. Threshold is %d, received value is %d.",Communication Error,0,"Internal Mac Receive Errors", +818,"Carrier Sense Errors",SNMP_CARRIER_SENSE_ERRORS,1,2,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"Carrier Sense Error counter rate threshold was exceeded. Threshold is %d, received value is %d.",Communication Error,0,"Carrier Sense Errors", +819,"Frame Too Longs",SNMP_HCFRAME_TOO_LONGS,1,2,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"Too Long Frames counter rate threshold was exceeded. Threshold is %d, received value is %d.",Communication Error,0,"Frame Too Longs", +820,"Symbol Errors",SNMP_HCSYMBOL_ERRORS,1,2,1,1,0,0,1,0,3,1,1,Minor,200,300,0,Port,"Symbol Errors counter rate threshold was exceeded. Threshold is %d, received value is %d.",Communication Error,0,"Symbol Errors", +901,"Fabric Configuration Started",FABRIC_CONFIG_STARTED,0,0,1,0,0,0,1,0,0,0,1,Info,1,0,0,Grid,"Fabric Configuration started.",Fabric Notification,0,"Fabric Configuration Started", +902,"Fabric Configuration Completed",FABRIC_CONFIG_COMPLETED,0,0,1,0,0,0,1,0,0,0,1,Info,1,0,0,Grid,"Fabric Configuration completed.",Fabric Notification,0,"Fabric Configuration Completed", +903,"Fabric Configuration Failed",FABRIC_CONFIG_FAILED,0,0,1,0,0,0,1,0,0,0,1,Critical,1,0,0,Grid,"Fabric Configuration failed. (Please see log for more details)",Fabric Notification,0,"Fabric Configuration Failed", +904,"Device Configuration Failure",DEVICE_CONFIG_ACTION_FAILED,0,0,1,0,0,0,1,0,19,1,1,Critical,1,0,0,Device,"Configuration action on device - %s (%s) failed. (Please see log for more details)",Fabric Notification,0,"Device Configuration Failure", +905,"Device Configuration Timeout",DEVICE_CONFIG_ACTION_TIMEOUT,0,0,1,0,0,0,1,0,19,1,1,Critical,1,0,0,Device,"Configuration action on device - %s (%s) Got timeout. (Please see log for more details)",Fabric Notification,0,"Device Configuration Timeout", +906,"Provisioning Validation Failure",PROVISIONING_VALIDATION_FAILURE,0,0,1,0,0,0,1,0,0,0,1,Critical,1,0,0,Grid,"Provisioning validation of fabric failed. (Please see log for more details)",Fabric Notification,0,"Provisioning Validation Failure", +907,"Switch is Down",SWITCH_FAILURE,0,0,1,1,0,0,1,0,10,1,1,Critical,1,0,0,Site,"Site configuration changes: %s Switch is Down",Fabric Topology,0,"Switch is disconnected or down", +908,"Switch is Up",SWITCH_OK,0,0,1,1,0,0,1,0,10,1,1,Info,1,300,0,Site,"Site configuration changes: %s Switch is Up",Fabric Topology,0,"Switch is connected or up", +909,"Director Switch is Down",DIRECTOR_SWITCH_DOWN,0,0,1,1,0,0,1,0,10,1,1,Critical,1,0,0,Site,"Site configuration changes: %s Director Switch is Down",Fabric Topology,0,"Director Switch is disconnected or down", +910,"Director Switch is Up",DIRECTOR_SWITCH_UP,0,0,1,1,0,0,1,0,10,1,1,Info,1,300,0,Site,"Site configuration changes: %s Director Switch is Up",Fabric Topology,0,"Director Switch is connected or up", +911,"Module Temperature Low Threshold Reached",MODULE_TEMPERATURE_EXCESS_LOW_THRESHOLD,1,0,1,1,0,1,0,0,10,1,1,Warning,60,300,0,Module,"Module Temperature threshold was exceeded. Sensor id is %d Threshold is %d, received value is %d.",Hardware,0,"Temperature detected by module sensor is too high, has exceeded the low threshold", +912,"Module Temperature High Threshold Reached",MODULE_TEMPERATURE_EXCESS_HIGH_THRESHOLD,1,0,1,1,0,1,0,0,10,1,1,Critical,60,300,0,Module,"Module Temperature threshold was exceeded. Sensor id is %d Threshold is %d, received value is %d.",Hardware,0,"Temperature detected by module sensor is too high, has exceeded the high threshold", +913,"Module High Voltage","MODULE_HIGH_VOLTAGE",0,0,1,1,0,0,1,0,79,1,1,"Warning",10,420,0,"Switch","Voltage of Sensor %s on %s(%s) value %s exceeded the threshold %s","Module Status",0,"Sensor Voltage Threshold Exceeded", +914,"Module High Current","MODULE_HIGH_CURRENT",0,0,1,1,0,0,1,0,80,1,1,"Warning",10,420,0,"Switch","Current of Sensor %s on %s(%s) exceeded the threshold %s","Module Status",0,"Sensor Current Threshold Exceeded", +915,"Critical BER reported","BER_ERROR",0,0,1,1,0,0,1,0,80,0,1,"Critical",10,420,0,"PORT","Effective BER Error on Port (%s) exceeded critical threshold (%s)","Hardware",0,"Effective BER Error on Port exceeded critical threshold", +916,"High BER reported","BER_WARNING",0,0,1,1,1,0,0,0,80,0,1,"Warning",10,420,0,"PORT","Effective BER Warning on Port (%s) exceeded warning threshold (%s)","Hardware",0,"Effective BER Warning on Port exceeded warning threshold", +917,"Critical Symbol BER reported","SYMBOL_BER_ERROR",0,0,1,1,0,0,1,0,80,0,1,"Critical",10,420,0,"PORT","Symbol BER Error on Port (%s) exceeded critical threshold (%s)","Hardware",0,"Symbol BER Error on Port exceeded critical threshold", +918,"High Symbol BER reported","SYMBOL_BER_WARNING",0,0,1,1,1,0,0,0,80,0,1,"Warning",10,420,0,"PORT","Symbol BER Warning on Port (%s) exceeded warning threshold (%s)","Hardware",0,"Symbol BER Warning on Port exceeded warning threshold", +1300,"SA Key violation", SM_SAKEY_VIOLATION,1,1,1,1,0,0,1,0,4,1,1,Warning,5,300,0,Port,"SA Key violation committed by, SLID: %(slid)d, Attribute:%(attribute)d, Method:%(method)d, SGID_PREFIX:%(sgid_prefix)s, SGID_INTERFACE:%(sgid_interface)s, PORT:%(port)d, SA_KEY: %(sa_key)d","Security",0,"SA Key Volation Committed", +1301,"SGID Spoofed",SM_SGID_SPOOFED,1,1,1,1,0,0,1,0,4,1,1,Warning,5,300,0,"Port","SGID spoofed by, LID:%(slid)d, Attribute:%(attribute)d, Method:%(method)d, Spoofed_SGID_Prefix:%(spoofed_sgid_prefix)d, Port:%(port)d","Security",0,"SGID spoofed by VPort/port", +1302,"SA High Rate detected",SM_SA_RATE_LIMIT_EXCEEDED,1,1,1,1,0,0,1,0,4,1,1,Warning,5,300,0,"Port","SA Rate Limit Exceeded by , SGID_PREFIX: %(sgid_prefix)s, SGID_INTERFACE: %(sgid_interface)s, Port:%(port)d","Security",0,"Rate Limit Exceeded", +1303,"Multicast subscriptions over limit",SM_MULTICAST_GROUPS_LIMIT_EXCEEDED,1,1,1,1,0,0,1,0,4,1,1,Warning,5,300,0,"Port","Number of Multicast subscriptions over limit for, SGID_PREFIX: %(sgid_prefix)s, SGID_INTERFACE: %(sgid_interface)s, Port:%(port)d, Target_GUID: %(target_guid)d, PHYSP_TARGET_GUID:%(physp_target_guid)d","Security",0,"Multicast Groups Limit Exceeded", +1304,"Service Record subscriptions over limit",SM_SERVICES_LIMIT_EXCEEDED,1,1,1,1,0,0,1,0,4,1,1,Warning,5,300,0,"Port","Number of Services Records over limit for, SGID_PREFIX: %(sgid_prefix)s, SGID_INTERFACE: %(sgid_interface)s, Port:%(port)d, Target_GUID: %(target_guid)d, PHYSP_TARGET_GUID:%(physp_target_guid)d","Security",0,"Services Limit Exceeded", +1305,"Event subscriptions over limit",SM_EVENT_SUBSCRIPTION_LIMIT_EXCEEDED,1,1,1,1,0,0,1,0,4,1,1,Warning,5,300,0,"Port","Number of event subscriptions over limit for, SGID_PREFIX: %(sgid_prefix)s, SGID_INTERFACE: %(sgid_interface)s, Port:%(port)d, Target_GUID: %(target_guid)d, PHYSP_TARGET_GUID:%(physp_target_guid)d","Security",0,"Event Subscription Limit Exceeded", +1306,"Unallowed SM was detected in the fabric",UNALLOWED_SM_WAS_DETECTED,1,1,1,1,0,0,1,0,4,1,1,Warning,0,300,0,"Port","Unallowed SM was detected in the fabric: Lid = %(LID)d, Guid = %(UNAUTHORIZED_SM_GUID)s","Fabric Notification",0,"Unallowed SM was detected in the fabric", +1307,"SMInfo SET request was received from unallowed SM",SMINFO_SET_REQUEST_WAS_RECEIVED_FROM_UNALLOWED_SM,1,1,1,1,0,0,1,0,4,1,1,Warning,0,300,0,"Port","SMInfo SET request was received from unallowed SM, detected in the fabric: Lid = %(LID)d, Guid = %(UNAUTHORIZED_SM_GUID)s","Fabric Notification",0,"SMInfo SET request was received from unallowed SM", +1309,"SM was detected with non-matching SMKey",SM_WAS_DETECTED_WITH_NON_MATCHING_SMKEY,1,1,1,1,0,0,1,0,4,1,1,Warning,0,300,0,"Port","SM was detected with non-matching SMKey: Lid = %(LID)d, Source-GUID = %(PHYSP_SGUID)s","Fabric Notification",0,"SM was detected with non-matching SMKey", +1310,"Duplicated node GUID was detected",DUPLICATED_NODE_GUID_DETECTED,1,1,1,1,0,0,1,0,4,1,1,Critical,1,0,0,"Device","Duplicated node GUID was detected: NODE_GUID = %(node_guid)s, PEER_NODE_GUID_1 = %(peer_node_guid_1)s, PEER_PORT_1 = %(peer_port_1)d, PEER_NODE_GUID_2 = %(peer_node_guid_2)s, PEER_PORT_2 = %(peer_port_2)d.","Fabric Notification",0,"Duplicated node GUID was detected", +1311,"Duplicated port GUID was detected",DUPLICATED_PORT_GUID_DETECTED,1,1,1,1,0,0,1,0,4,1,1,Critical,1,0,0,"Port","Duplicated port GUID was detected: PORT_GUID = %(port_guid)s, NODE_GUID_1 = %(node_guid_1)s, PORT_1 = %(port_1)d, NODE_GUID_2 = %(node_guid_2)s, PORT_2 = %(port_2)d","Fabric Notification",0,"Duplicated port GUID was detected", +1312,"Switch was Rebooted",SWITCH_WAS_REBOOTED,1,1,1,1,0,0,1,0,4,1,1,Info,1,0,0,"Device","Switch was rebooted: NODE_GUID = %(node_guid)s.","Fabric Notification",0,"Switch was Rebooted", +1400,"High Ambient Temperature",COOLING_DEV_HIGH_AMBIENT_TEMP,0,1,1,1,0,0,1,0,81,1,1,Warning,0,86400,0,Switch,"%s",Hardware,0,"High Ambient Temperature", +1401,"High Fluid Temperature",COOLING_DEV_HIGH_FLUID_TEMP,0,1,1,1,0,0,1,0,81,1,1,Warning,0,86400,0,Switch,"%s",Hardware,0,"High Fluid Temperature", +1402,"Low Fluid Level",COOLING_DEV_LOW_FLUID_LEVEL,0,1,1,1,0,0,1,0,81,1,1,Warning,0,86400,0,Switch,"%s",Hardware,0,"Low Fluid Level", +1403,"Low Supply Pressure",COOLING_DEV_LOW_SUPPLY_PRESS,0,1,1,1,0,0,1,0,81,1,1,Warning,0,86400,0,Switch,"%s",Hardware,0,"Low Supply Pressure", +1404,"High Supply Pressure",COOLING_DEV_HIGH_SUPPLY_PRESS,0,1,1,1,0,0,1,0,81,1,1,Warning,0,86400,0,Switch,"%s",Hardware,0,"High Supply Pressure", +1405,"Low Return Pressure",COOLING_DEV_LOW_RETURN_PRESS,0,1,1,1,0,0,1,0,81,1,1,Warning,0,86400,0,Switch,"%s",Hardware,0,"Low Return Pressure", +1406,"High Return Pressure",COOLING_DEV_HIGH_RETURN_PRESS,0,1,1,1,0,0,1,0,81,1,1,Warning,0,86400,0,Switch,"%s",Hardware,0,"High Return Pressure", +1407,"High Differential Pressure",COOLING_DEV_HIGH_DIFF_PRESS,0,1,1,1,0,0,1,0,81,1,1,Warning,0,86400,0,Switch,"%s",Hardware,0,"High Differential Pressure", +1408,"Low Differential Pressure",COOLING_DEV_LOW_DIFF_PRESS,0,1,1,1,0,0,1,0,81,1,1,Warning,0,86400,0,Switch,"%s",Hardware,0,"Low Differential Pressure", +1409,"System Fail Safe",COOLING_DEV_SYSTEM_FAIL_SAFE,0,1,1,1,0,0,1,0,81,1,1,Warning,0,86400,0,Switch,"%s",Hardware,0,"System Fail Safe", +1410,"Fault Critical",COOLING_DEV_FAULT_CRITICAL,0,1,1,1,0,0,1,0,81,1,1,Critical,0,86400,0,Switch,"%s",Hardware,0,"Fault Critical", +1411,"Fault Pump1",COOLING_DEV_FAULT_PUMP1,0,1,1,1,0,0,1,0,81,1,1,Critical,0,86400,0,Switch,"%s",Hardware,0,"Fault Pump1", +1412,"Fault Pump2",COOLING_DEV_FAULT_PUMP2,0,1,1,1,0,0,1,0,81,1,1,Critical,0,86400,0,Switch,"%s",Hardware,0,"Fault Pump2", +1413,"Fault Fluid Level Critical",COOLING_DEV_FLUID_LEVEL_CRIT,0,1,1,1,0,0,1,0,81,1,1,Critical,0,86400,0,Switch,"%s",Hardware,0,"Fault Fluid Level Critical", +1414,"Fault Fluid Over Temperature",COOLING_DEV_FLUID_OVERTEMP,0,1,1,1,0,0,1,0,81,1,1,Critical,0,86400,0,Switch,"%s",Hardware,0,"Fault Fluid Over Temperature", +1415,"Fault Primary DC",COOLING_DEV_FAULT_PRIMARY_DC,0,1,1,1,0,0,1,0,81,1,1,Critical,0,86400,0,Switch,"%s",Hardware,0,"Fault Primary DC", +1416,"Fault Redundant DC",COOLING_DEV_FAULT_REDUND_DC,0,1,1,1,0,0,1,0,81,1,1,Critical,0,86400,0,Switch,"%s",Hardware,0,"Fault Redundant DC", +1417,"Fault Fluid Leak",COOLING_DEV_FAULT_FLUID_LEAK,0,1,1,1,0,0,1,0,81,1,1,Critical,0,86400,0,Switch,"%s",Hardware,0,"Fault Fluid Leak", +1418,"Fault Sensor Failure",COOLING_DEV_SENSOR_FAILURE,0,1,1,1,0,0,1,0,81,1,1,Critical,0,86400,0,Switch,"%s",Hardware,0,"Fault Sensor Failure", +1419,"Cooling Device Monitoring Error",COOLING_DEV_MONITOR_ERROR,0,1,1,1,0,0,1,0,0,0,0,Critical,0,1,0,Grid,"%s",Hardware,0,"Cooling Device Monitoring Error", +1420,"Cooling Device Communication Error",COOLING_DEV_COMM_ERROR,0,1,1,1,0,0,1,0,81,1,1,Critical,0,86400,0,Switch,"%s",Hardware,0,"Cooling Device Communication Error", +1500,"New Cable Detected",CABLE_ADDED,0,0,1,1,0,0,1,0,10,0,0,Info,1,0,0,Site,"New cable S/N: %s is detected",Security,0,"New cable was detected", +1502,"Cable detected in a new location",CABLE_NEW_LOCATION,0,0,1,1,0,0,1,0,10,0,0,Warning,1,0,0,Site,"Cable S/N: %s detected in a new location",Security,0,"Cable detected in a new location", +1503,"Duplicate Cable Detected",CABLE_DUP_SERIAL,0,0,1,1,0,0,1,0,10,0,0,Critical,1,0,0,Site,"Duplicate cable S/N: %s detected",Security,0,"Duplicate Cable S/N", +1504,"SHARP Allocation Succeeded",SHARP_ALLOCATION_SUCCEED,0,0,0,1,0,0,1,0,10,0,0,Info,1,0,0,Grid,"Create SHARP reservation for app_id = %s completed successfully.",SHARP,0,"Create SHARP allocation has succeeded", +1505,"SHARP Allocation Failed",SHARP_ALLOCATION_FAILED,0,0,0,1,0,0,1,0,10,0,0,Warning,1,0,0,Grid,"Create SHARP reservation for app_id = %s Failed, Reason: %s",SHARP,0,"Create SHARP allocation has failed", +1506,"SHARP Deallocation Succeeded",SHARP_DEALLOCATION_SUCCEED,0,0,0,1,0,0,1,0,10,0,0,Info,1,0,0,Grid,"Delete SHARP reservation for app_id = %s completed successfully.",SHARP,0,"Delete SHARP allocation has succeeded", +1507,"SHARP Deallocation Failed",SHARP_DEALLOCATION_FAILED,0,0,0,1,0,0,1,0,10,0,0,Warning,1,0,0,Grid,"Delete SHARP reservation for app_id = %s Failed, Reason: %s",SHARP,0,"Delete SHARP allocation has failed", +1508,"Device Collect System Dump Started",COLLECT_SYSTEM_DUMP_STARTED_ON_DEVICE,0,1,1,1,0,0,1,0,10,0,0,Info,1,300,0,Device,"Device collect system dump for %s has started",Maintenance,0,"Device collect system dump process has started", +1509,"Device Collect System Dump Finished",COLLECT_SYSTEM_DUMP_FINISHED_ON_DEVICE,0,1,1,1,0,0,1,0,10,0,0,Info,1,300,0,Device,"Device collect system dump on device %s has been completed successfully.",Maintenance,0,"Device collect system dump has finished", +1510,"Device Collect System Dump Error",COLLECT_SYSTEM_DUMP_FAILED_ON_DEVICE,0,1,1,1,0,0,1,0,10,0,0,Critical,1,300,0,Device,"Collect System Dump on Device %s (%s) failed",Maintenance,0,"Device collect system dump failed", +1511,"Virtual Port Added",VIRTUAL_PORT_ADDED,0,1,1,1,0,0,1,0,10,0,0,Info,1,0,0,Port,"Virtual Port %s was added to %s port_guid=%s",Fabric Notification,0,"Virtual Port Added", +1512,"Virtual Port Removed",VIRTUAL_PORT_REMOVED,0,1,0,1,0,0,1,0,10,0,0,Warning,1,0,0,Port,"Virtual Port %s was removed from %s port_guid=%s",Fabric Notification,0,"Virtual Port Removed", +1513,"Burn Cables Transceivers Started",BURN_CABLES_TRANSCEIVERS_STARTED,0,0,0,1,0,0,1,0,10,0,0,Info,1,0,0,Device,"Burn cables transceivers for %s lid-%s has been started.",Maintenance,0,"Burn cables transceivers process has been started", +1514,"Burn Cables Transceivers Finished",BURN_CABLES_TRANSCEIVERS_FINISHED,0,0,0,1,0,0,1,0,10,0,0,Info,1,0,0,Device,"Burn cables transceivers on device %s lid-%s has been completed successfully.",Maintenance,0,"Burn cables transceivers has been finished", +1515,"Burn Cables Transceivers Failed",BURN_CABLES_TRANSCEIVERS_ERROR,0,0,0,1,0,0,1,0,10,0,0,Warning,1,0,0,Device,"Burn cables transceivers on Device %s (%s) lid-%s has been failed.",Maintenance,0,"Burn cables transceivers has been failed", +1516,"Activate Cables Transceivers FW Finished",ACTIVATE_CABLES_TRANSCEIVERS_FW_FINISHED,0,0,0,1,0,0,1,0,10,0,0,Info,1,0,0,Device,"Activate cables transceivers FW on device %s lid-%s has been completed successfully.",Maintenance,0,"Activate cables transceivers FW has been finished", +1517,"Activate Cables Transceivers FW Failed",ACTIVATE_CABLES_TRANSCEIVERS_FW_ERROR,0,0,0,1,0,0,1,0,10,0,0,Warning,1,0,0,Device,"Activate cables transceivers FW on Device %s (%s) lid-%s has been failed.",Maintenance,0,"Activate cables transceivers FW has been failed", +1520,"Aggregation Node Discovery Failed",AGGREGATION_NODE_DISCOVERY_FAILED,0,0,0,1,0,0,1,0,10,0,0,Critical,1,0,0,SHARP AM,"",SHARP,0,"", +1521,"Job Started",JOB_STARTED,0,0,0,1,0,0,1,0,10,0,0,Info,1,0,0,SHARP AM,"",SHARP,0,"", +1522,"Job Ended",JOB_ENDED,0,0,0,1,0,0,1,0,10,0,0,Info,1,0,0,SHARP AM,"",SHARP,0,"", +1523,"Job Start Failed",JOB_START_FAILED,0,0,0,1,0,0,1,0,10,0,0,Critical,1,0,0,SHARP AM,"",SHARP,0,"", +1524,"Job Error",JOB_ERROR,0,0,0,1,0,0,1,0,10,0,0,Critical,1,0,0,SHARP AM,"",SHARP,0,"", +1525,"Trap QP Error",TRAP_QP_ERROR,0,0,0,1,0,0,1,0,10,0,0,Critical,1,0,0,SHARP AM,"",SHARP,0,"", +1526,"Trap Invalid Request",TRAP_INVALID_REQUEST,0,0,0,1,0,0,1,0,10,0,0,Critical,1,0,0,SHARP AM,"",SHARP,0,"", +1527,"Trap Sharp Error",TRAP_SHARP_ERROR,0,0,0,1,0,0,1,0,10,0,0,Critical,1,0,0,SHARP AM,"",SHARP,0,"", +1528,"Trap QP Alloc timeout",TRAP_QP_ALLOC_TIMEOUT,0,0,0,1,0,0,1,0,10,0,0,Critical,1,0,0,SHARP AM,"",SHARP,0,"", +1529,"Trap AMKey Violation",TRAP_AMKEY_VIOLATION,0,0,0,1,0,0,1,0,10,0,0,Critical,1,0,0,SHARP AM,"",SHARP,0,"", +1530,"Unsupported Trap",UNSUPPORTED_TRAP,0,0,0,1,0,0,1,0,10,0,0,Critical,1,0,0,SHARP AM,"",SHARP,0,"", +1531,"Reservation Updated",RESERVATION_UPDATED,0,0,0,1,0,0,1,0,10,0,0,Info,1,0,0,SHARP AM,"",SHARP,0,"", +1532,"Sharp is not Responding",SHARP_NOT_RESPONDING,0,0,0,1,0,0,1,0,10,1,1,Critical,1,300,0,SHARP AM,"SHARP Aggregation Manager is not responsive",SHARP,0,"SHARP Aggregation Manager is not responsive", +1533,"Agg Node Active",AGG_NODE_ACTIVE,0,0,0,1,0,0,1,0,10,0,0,Info,1,0,0,SHARP AM,"",SHARP,0,"", +1534,"Agg Node Inactive",AGG_NODE_INACTIVE,0,0,0,1,0,0,1,0,10,0,0,Warning,1,0,0,SHARP AM,"",SHARP,0,"", +1535,"Trap AMKey Violation Triggered by AM",TRAP_AMKEY_VIOLATION_TRIGGERED_BY_AM,0,0,0,1,0,0,1,0,10,0,0,Warning,1,0,0,SHARP AM,"",SHARP,0,"", +1550,"Guids Were Added to Pkey",GUIDS_ADDED_TO_PKEY,0,0,0,1,0,0,1,0,10,0,0,Info,1,0,0,Port,"%d Guids were added to Pkey %s",Fabric Configuration,0,"Guids Were Added to Pkey", +1551,"Guids Were Removed from Pkey",GUIDS_REMOVED_FROM_PKEY,0,0,0,1,0,0,1,0,10,0,0,Info,1,0,0,Port,"%d Guids were removed from pkey %s",Fabric Configuration,0,"Guids Were Removed from Pkey", +1600,"VS/CC Classes Key Violation", VS_CC_CLASSES_KEY_VIOLATON,0,0,0,1,0,0,1,0,10,0,0,Warning,1,0,0,Port,"Bad %(mclass)s key committed by SLID: %(slid)d, Attribute_ID: %(attribute_id)d, Method: %(method)d, SGID_PREFIX: %(sgid_prefix)s, SGID_INTERFACE: %(sgid_interface)s, PHYSP_SGUID: %(physp_sguid)s","Security",0,"VS/CC Classes Key Violation Committed", +919,"Cable Temperature High",CABLE_HIGH_TEMPERATURE,1,0,1,1,1,1,1,0,10,1,1,Critical,0,0,0,Port,"%s",Hardware,0,"Cable Temperature is High", +920,"Cable Temperature Low",CABLE_LOW_TEMPERATURE,1,0,1,1,1,1,1,0,10,1,1,Critical,0,0,0,Port,"%s",Hardware,0,"Cable Temperature is Low", diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/ufm_events_log_parser.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/ufm_events_log_parser.py new file mode 100755 index 000000000..5645785f3 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/old_logic/ufm_events_log_parser.py @@ -0,0 +1,110 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# author: Samer Deeb +# date: Mar 02, 2024 +# +import re +from collections import Counter + +from log_analyzer.src.loganalyze.old_logic.base_log_parser import ( + BaseLogParser, + LogAnalysisTypes, +) + + +class UFMEventsLogParser(BaseLogParser): + EVENT_REGEX = re.compile( + # date time na na severity Site and name type objtype objid event event_details + r"^([\d\-]+ [\d\:\.]+) \[\d+\] \[\d+\] (\w+) (?:Site \[[\w-]*\] )?\[(\w+)\] (\w+) \[(.*)\]\: ((([ \w\/\-]+)((\,|\:|\.)(.*))?)|(.*))$" + ) + IBPORT_OBJ_REGEX = re.compile(r"((Computer|Switch).*)\] \[") + COMP_OBJ_REGEX = re.compile(r"(Computer: [\w\-\.]+)") + MODULE_OBJ_REGEX = re.compile(r"(Switch.*)\] \[") + LINK_OBJ_REGEX = re.compile(r"([a-f0-9]+\_[0-9]+)[\w ]+\: ([a-f0-9]+_[0-9]+)") + + def __init__(self, file_name): + super().__init__(file_name) + self.stats = {} + + def _format_object_id(self, object_type: str, object_id: str): + if object_type == "IBPort": + match = self.IBPORT_OBJ_REGEX.search(object_id) + if match: + object_id = match.group(1) + else: + print(f"-W- NO IBPort match for: {object_id}") + elif object_type == "Site": + object_id = "default" + elif object_type == "Computer": + match = self.COMP_OBJ_REGEX.search(object_id) + if match: + object_id = match.group(1) + else: + print(f"-W- NO Computer match for: {object_id}") + elif object_type == "Module": + match = self.MODULE_OBJ_REGEX.search(object_id) + if match: + object_id = match.group(1) + else: + print(f"-W- NO Module match for: {object_id}") + elif object_type == "Link": + match = self.LINK_OBJ_REGEX.search(object_id) + if match: + src = match.group(1) + dest = match.group(2) + object_id = f"{src}:{dest}" + else: + print(f"-W- NO Link match for: {object_id}") + return object_id + + def _parse_line(self, line): + line = line.strip() + if not line: + return False + match = self.EVENT_REGEX.match(line) + if not match: + print(f"-W- No match for line: {line}") + return False + timestamp = match.group(1) + severity = match.group(2) + event_type = match.group(3) + object_type = match.group(4) + object_id = self._format_object_id(object_type, match.group(5)) + event = match.group(12) + event_details = "" + if not event: + event = match.group(8) + event_details = match.group(11) + else: + print(f"-W- No event match for: {event}") + log_entry = ( + timestamp, + severity, + event_type, + object_type, + object_id, + event, + event_details, + ) + self.notify_sunscribers(LogAnalysisTypes.UFMEventsLogEntry, log_entry) + self.stats.setdefault(event, Counter())[object_id] += 1 + return True + + def parse(self): + super().parse() + for event, ev_counter in self.stats.items(): + sorted_counters = sorted( + ev_counter.items(), key=lambda x: x[1], reverse=True + ) + for object_id, count in sorted_counters: + entry = (event, object_id, count) + self.notify_sunscribers(LogAnalysisTypes.UFMEventsTopTalkers, entry) diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/pdf_creator.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/pdf_creator.py new file mode 100644 index 000000000..071487c1c --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/pdf_creator.py @@ -0,0 +1,84 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# +# pylint: disable=missing-module-docstring +# pylint: disable=missing-class-docstring +# pylint: disable=missing-module-docstring +# pylint: disable=missing-function-docstring + +import os +from io import StringIO +from pprint import pprint +from fpdf import FPDF # Import FPDF from fpdf module + + +class PDFCreator(FPDF): + def __init__(self, pdf_path, pdf_header, images_path, fabric_stats_list): + super().__init__() + self._pdf_path = pdf_path + self._pdf_header = pdf_header + self._images_path = images_path + self._fabric_stats_list = fabric_stats_list + + def header(self): + self.set_font("Arial", "B", 12) + self.cell(0, 10, self._pdf_header, 0, 1, "C") + + def footer(self): + self.set_y(-15) + self.set_font("Arial", "I", 8) + self.cell(0, 10, f"Page {self.page_no()}", 0, 0, "C") + + def created_pdf(self): + self.set_display_mode("fullpage") + self.add_page() + + # Initial coordinates for images + x_start = 10 + y_start = 20 + image_width = 180 + image_height = 100 + spacing = 10 + + # Add each image + x = x_start + y = y_start + for image_path in self._images_path: + if os.path.exists(image_path): + self.image( + image_path, x=x, y=y, w=image_width, h=image_height, type="PNG" + ) + + # Update coordinates for the next image + y += image_height + spacing + + # Check if next image exceeds page height + if y > self.h - image_height - 20: + self.add_page() # Add a new page if needed + y = y_start + + # Add text on a new page + self.add_page() # Add a new page for the text + self.set_font("Arial", "", 12) + + # First block of text + fabric_size = """Fabric size: + """ + output = StringIO() + pprint(self._fabric_stats_list, stream=output) + fabric_size += ( + output.getvalue().strip() + ) # .strip() to remove any leading/trailing whitespace + + self.multi_cell(0, 10, fabric_size) + + # Output PDF + self.output(self._pdf_path) diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/requirements.txt b/plugins/ufm_log_analyzer_plugin/src/loganalyze/requirements.txt new file mode 100644 index 000000000..916868a50 --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/requirements.txt @@ -0,0 +1,5 @@ +pandas +numpy +matplotlib +IPython +fpdf2 \ No newline at end of file diff --git a/plugins/ufm_log_analyzer_plugin/src/loganalyze/utils/common.py b/plugins/ufm_log_analyzer_plugin/src/loganalyze/utils/common.py new file mode 100644 index 000000000..318d24b6e --- /dev/null +++ b/plugins/ufm_log_analyzer_plugin/src/loganalyze/utils/common.py @@ -0,0 +1,57 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# + + +import glob +import os +import shutil +from typing import Set +import loganalyze.logger as log + +def delete_folders(folders: Set[str], until_folder:str): + """ + Delete all the folders in the folders param, and their parents until reaching the + until_folder folder + """ + for folder in folders: + cur_dir = folder + while cur_dir != until_folder: + try: + shutil.rmtree(cur_dir) + log.LOGGER.debug(f"Removed {cur_dir}") + except FileNotFoundError: + log.LOGGER.debug(f"Error: {cur_dir} does not exist.") + except PermissionError: + log.LOGGER.debug(f"Error: Insufficient permissions to delete {cur_dir}.") + except NotADirectoryError: + log.LOGGER.debug(f"Error: {cur_dir} is not a directory.") + except OSError as e: + log.LOGGER.debug(f"Error: {e.strerror} - {e.filename}") + except ValueError as e: + log.LOGGER.debug(f"Error: Invalid parameters passed to rmtree - {e}") + cur_dir = os.path.dirname(cur_dir) + + +def delete_files_by_types(folder:str, file_types:set[str]): + """ + Given a folder and file types, for example svg and png, it will delete all + files that end with .svg and .png in the given folder. + """ + + for file_type in file_types: + search_pattern = f"*.{file_type}" + all_files_in_dir_by_type = glob.glob(os.path.join(folder, search_pattern)) + for cur_file in all_files_in_dir_by_type: + try: + os.remove(cur_file) + except Exception as e: + log.LOGGER.debug(f"Error deleting SVG files: {e}") From 276eec0bc35e10942edbfb0e08e89d0a723f182d Mon Sep 17 00:00:00 2001 From: vg12345 <77556137+vg12345@users.noreply.github.com> Date: Thu, 29 Aug 2024 12:30:50 +0300 Subject: [PATCH 5/6] Issue:4044453:Replace the flask framework with aiohttp in PDR plugin (#243) --- plugins/pdr_deterministic_plugin/README.md | 2 +- .../pdr_deterministic_plugin/build/Dockerfile | 2 +- .../pdr_deterministic_plugin/requirements.txt | 4 +- .../api/base_aiohttp_api.py | 81 +++++++++++++++++++ .../ufm_sim_web_service/api/pdr_plugin_api.py | 68 ++++++++-------- .../ufm_sim_web_service/isolation_algo.py | 18 ++--- 6 files changed, 124 insertions(+), 51 deletions(-) create mode 100644 plugins/pdr_deterministic_plugin/ufm_sim_web_service/api/base_aiohttp_api.py diff --git a/plugins/pdr_deterministic_plugin/README.md b/plugins/pdr_deterministic_plugin/README.md index 938cb7627..3cbe60da0 100644 --- a/plugins/pdr_deterministic_plugin/README.md +++ b/plugins/pdr_deterministic_plugin/README.md @@ -90,7 +90,7 @@ The ports are added or removed to blacklist via PDR plugin Rest API. Add ports to exclude list (to be excluded from the analysis): curl -k -i -X PUT 'http:///excluded' -d '[]' TTL (time to live in blacklist) can optionally follow the port after the comma (if zero or not specified, then port is excluded forever) - Example: curl -k -i -X PUT 'http://127.0.0.1:8977/excluded' -d '[["9c0591030085ac80_45"],["9c0591030085ac80_46",300]' (first port is added forever, second - just for 300 seconds) + Example: curl -k -i -X PUT 'http://127.0.0.1:8977/excluded' -d '[["9c0591030085ac80_45"],["9c0591030085ac80_46",300]]' (first port is added forever, second - just for 300 seconds) Remove ports from exclude list curl -k -i -X DELETE 'http:///excluded' -d '[]' diff --git a/plugins/pdr_deterministic_plugin/build/Dockerfile b/plugins/pdr_deterministic_plugin/build/Dockerfile index f1e8aaf9c..dbe85bdcc 100644 --- a/plugins/pdr_deterministic_plugin/build/Dockerfile +++ b/plugins/pdr_deterministic_plugin/build/Dockerfile @@ -21,7 +21,7 @@ EXPOSE 9007 RUN apt-get update && apt-get -y install supervisor python3 python3-pip rsyslog vim curl sudo -RUN python3 -m pip install flask flask_restful requests twisted jsonschema pandas numpy +RUN python3 -m pip install requests jsonschema pandas numpy aiohttp # remove an unused library that caused a high CVE vulnerability issue https://redmine.mellanox.com/issues/3837452 RUN apt-get remove -y linux-libc-dev diff --git a/plugins/pdr_deterministic_plugin/requirements.txt b/plugins/pdr_deterministic_plugin/requirements.txt index cf712dea9..197d89272 100644 --- a/plugins/pdr_deterministic_plugin/requirements.txt +++ b/plugins/pdr_deterministic_plugin/requirements.txt @@ -1,9 +1,7 @@ -flask<=3.0.3 numpy<=1.26.4 pandas<=2.2.2 pytest<=8.2.0 requests<=2.31.0 -twisted<=22.1.0 -flask_restful<=0.3.10 tzlocal<=4.2 jsonschema<=4.5.1 +aiohttp<=3.9.1 diff --git a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/api/base_aiohttp_api.py b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/api/base_aiohttp_api.py new file mode 100644 index 000000000..ebc404b39 --- /dev/null +++ b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/api/base_aiohttp_api.py @@ -0,0 +1,81 @@ +# +# Copyright © 2013-2024 NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED. +# +# This software product is a proprietary product of Nvidia Corporation and its affiliates +# (the "Company") and all right, title, and interest in and to the software +# product, including all associated intellectual property rights, are and +# shall remain exclusively with the Company. +# +# This software product is governed by the End User License Agreement +# provided with the software product. +# + +import asyncio +from aiohttp import web + +class BaseAiohttpAPI: + """ + Base class for API implemented with aiohttp + """ + def __init__(self): + """ + Initialize a new instance of the BaseAiohttpAPI class. + """ + self.app = web.Application() + + @property + def application(self): + """ + Read-only property for the application instance. + """ + return self.app + + def add_route(self, method, path, handler): + """ + Add route to API. + """ + self.app.router.add_route(method, path, handler) + + def web_response(self, text, status): + """ + Create response object. + """ + return web.json_response(text=text, status=status) + + +class BaseAiohttpServer: + """ + Base class for HTTP server implemented with aiohttp + """ + def __init__(self, logger): + """ + Initialize a new instance of the BaseAiohttpAPI class. + """ + self.logger = logger + + def run(self, app, host, port): + """ + Run the server on the specified host and port. + """ + loop = asyncio.get_event_loop() + loop.run_until_complete(self._run_server(app, host, port)) + + async def _run_server(self, app, host, port): + """ + Asynchronously run the server and handle shutdown. + """ + # Run server + runner = web.AppRunner(app) + await runner.setup() + site = web.TCPSite(runner, host, port) + await site.start() + self.logger.info(f"Server started at {host}:{port}") + + # Wait for shutdown signal + shutdown_event = asyncio.Event() + try: + await shutdown_event.wait() + except KeyboardInterrupt: + self.logger.info(f"Shutting down server {host}:{port}...") + finally: + await runner.cleanup() diff --git a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/api/pdr_plugin_api.py b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/api/pdr_plugin_api.py index c8a0c22e8..ae25e9fbc 100644 --- a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/api/pdr_plugin_api.py +++ b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/api/pdr_plugin_api.py @@ -10,16 +10,16 @@ # provided with the software product. # +import json import time from http import HTTPStatus from json import JSONDecodeError -from flask import json, request -from utils.flask_server.base_flask_api_server import BaseAPIApplication +from api.base_aiohttp_api import BaseAiohttpAPI ERROR_INCORRECT_INPUT_FORMAT = "Incorrect input format" EOL = '\n' -class PDRPluginAPI(BaseAPIApplication): +class PDRPluginAPI(BaseAiohttpAPI): ''' class PDRPluginAPI ''' @@ -31,29 +31,23 @@ def __init__(self, isolation_mgr): super(PDRPluginAPI, self).__init__() self.isolation_mgr = isolation_mgr - - def _get_routes(self): - """ - Map URLs to function calls - """ - return { - self.get_excluded_ports: dict(urls=["/excluded"], methods=["GET"]), - self.exclude_ports: dict(urls=["/excluded"], methods=["PUT"]), - self.include_ports: dict(urls=["/excluded"], methods=["DELETE"]) - } + # Define routes using the base class's method + self.add_route("GET", "/excluded", self.get_excluded_ports) + self.add_route("PUT", "/excluded", self.exclude_ports) + self.add_route("DELETE", "/excluded", self.include_ports) - def get_excluded_ports(self): + async def get_excluded_ports(self, request): """ Return ports from exclude list as comma separated port names """ items = self.isolation_mgr.exclude_list.items() formatted_items = [f"{item.port_name}: {'infinite' if item.ttl_seconds == 0 else int(max(0, item.remove_time - time.time()))}" for item in items] response = EOL.join(formatted_items) + ('' if not formatted_items else EOL) - return response, HTTPStatus.OK + return self.web_response(response, HTTPStatus.OK) - def exclude_ports(self): + async def exclude_ports(self, request): """ Parse input ports and add them to exclude list (or just update TTL) Input string example: [["0c42a10300756a04_1"],["98039b03006c73ba_2",300]] @@ -61,12 +55,12 @@ def exclude_ports(self): """ try: - pairs = self.get_request_data() + pairs = await self.get_request_data(request) except (JSONDecodeError, ValueError): - return ERROR_INCORRECT_INPUT_FORMAT + EOL, HTTPStatus.BAD_REQUEST + return self.web_response(ERROR_INCORRECT_INPUT_FORMAT + EOL, HTTPStatus.BAD_REQUEST) if not isinstance(pairs, list) or not all(isinstance(pair, list) for pair in pairs): - return ERROR_INCORRECT_INPUT_FORMAT + EOL, HTTPStatus.BAD_REQUEST + return self.web_response(ERROR_INCORRECT_INPUT_FORMAT + EOL, HTTPStatus.BAD_REQUEST) response = "" for pair in pairs: @@ -80,23 +74,23 @@ def exclude_ports(self): response += f"Port {port_name} added to exclude list for {ttl} seconds" response += self.get_port_warning(port_name) + EOL - - return response, HTTPStatus.OK + return self.web_response(response, HTTPStatus.OK) - def include_ports(self): + + async def include_ports(self, request): """ Remove ports from exclude list Input string: comma separated port names list Example: ["0c42a10300756a04_1","98039b03006c73ba_2"] """ try: - port_names = self.get_request_data() + port_names = await self.get_request_data(request) except (JSONDecodeError, ValueError): - return ERROR_INCORRECT_INPUT_FORMAT + EOL, HTTPStatus.BAD_REQUEST + return self.web_response(ERROR_INCORRECT_INPUT_FORMAT + EOL, HTTPStatus.BAD_REQUEST) if not isinstance(port_names, list): - return ERROR_INCORRECT_INPUT_FORMAT + EOL, HTTPStatus.BAD_REQUEST + return self.web_response(ERROR_INCORRECT_INPUT_FORMAT + EOL, HTTPStatus.BAD_REQUEST) response = "" for port_name in port_names: @@ -108,19 +102,25 @@ def include_ports(self): response += self.get_port_warning(port_name) + EOL - return response, HTTPStatus.OK + return self.web_response(response, HTTPStatus.OK) - def get_request_data(self): + async def get_request_data(self, request): """ - Deserialize request json data into object + Deserialize request data into object for aiohttp """ - if request.is_json: - # Directly convert JSON data into Python object - return request.get_json() - else: - # Attempt to load plain data text as JSON - return json.loads(request.get_data(as_text=True)) + try: + # Try to get JSON data + return await request.json() + except json.JSONDecodeError: + # Try to get plain text data + text = await request.text() + try: + # Try to parse the text as JSON + return json.loads(text) + except json.JSONDecodeError: + # Return the raw text data + return text def fix_port_name(self, port_name): diff --git a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/isolation_algo.py b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/isolation_algo.py index b5272c5d5..fe99c5f0d 100644 --- a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/isolation_algo.py +++ b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/isolation_algo.py @@ -14,15 +14,12 @@ import os import logging from logging.handlers import RotatingFileHandler +import threading from constants import PDRConstants as Constants from isolation_mgr import IsolationMgr +from api.base_aiohttp_api import BaseAiohttpServer from ufm_communication_mgr import UFMCommunicator from api.pdr_plugin_api import PDRPluginAPI -from twisted.web.wsgi import WSGIResource -from twisted.internet import reactor -from twisted.web import server -from utils.flask_server import run_api -from utils.flask_server.base_flask_api_app import BaseFlaskAPIApp from utils.utils import Utils @@ -81,19 +78,16 @@ def main(): logger = create_logger(Constants.LOG_FILE) algo_loop = IsolationMgr(ufm_client, logger) - reactor.callInThread(algo_loop.main_flow) + threading.Thread(target=algo_loop.main_flow).start() try: plugin_port = Utils.get_plugin_port( port_conf_file='/config/pdr_deterministic_httpd_proxy.conf', default_port_value=8977) - routes = { - "": PDRPluginAPI(algo_loop).application - } - - app = BaseFlaskAPIApp(routes) - run_api(app=app, port_number=int(plugin_port)) + api = PDRPluginAPI(algo_loop) + server = BaseAiohttpServer(logger) + server.run(api.application, "127.0.0.1", int(plugin_port)) except Exception as ex: print(f'Failed to run the app: {str(ex)}') From dac5cd4b961f08f334b5dcf278a561eb995d15fc Mon Sep 17 00:00:00 2001 From: egershonNvidia <108881287+egershonNvidia@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:45:36 +0300 Subject: [PATCH 6/6] issue:#4036243: Add pylint to PDR (#236) --- .github/workflows/pdr_plugin_ci_workflow.yml | 2 +- .../.pytest/run_pdr_standalone_pytest.sh | 0 .../terminate_pdr_standalone_pytest.sh | 0 .../ufm_sim_web_service/api/__init__.py | 5 +- .../ufm_sim_web_service/api/pdr_plugin_api.py | 11 +- .../ufm_sim_web_service/constants.py | 7 +- .../ufm_sim_web_service/exclude_list.py | 17 ++- .../ufm_sim_web_service/isolation_algo.py | 10 +- .../ufm_sim_web_service/isolation_mgr.py | 116 ++++++++++-------- .../ufm_communication_mgr.py | 56 +++++---- 10 files changed, 124 insertions(+), 100 deletions(-) mode change 100644 => 100755 plugins/pdr_deterministic_plugin/.pytest/run_pdr_standalone_pytest.sh mode change 100644 => 100755 plugins/pdr_deterministic_plugin/.pytest/terminate_pdr_standalone_pytest.sh diff --git a/.github/workflows/pdr_plugin_ci_workflow.yml b/.github/workflows/pdr_plugin_ci_workflow.yml index 1b1356c71..e4a9880e8 100644 --- a/.github/workflows/pdr_plugin_ci_workflow.yml +++ b/.github/workflows/pdr_plugin_ci_workflow.yml @@ -28,7 +28,7 @@ jobs: pip install pytest-cov - name: Run PyLint - run: pylint --rcfile=$PDRPATH/.pylintrc $PDRPATH + run: pylint --rcfile=$PDRPATH/.pylintrc $PDRPATH/ufm_sim_web_service/ - name: Run exclusion list class test timeout-minutes: 5 diff --git a/plugins/pdr_deterministic_plugin/.pytest/run_pdr_standalone_pytest.sh b/plugins/pdr_deterministic_plugin/.pytest/run_pdr_standalone_pytest.sh old mode 100644 new mode 100755 diff --git a/plugins/pdr_deterministic_plugin/.pytest/terminate_pdr_standalone_pytest.sh b/plugins/pdr_deterministic_plugin/.pytest/terminate_pdr_standalone_pytest.sh old mode 100644 new mode 100755 diff --git a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/api/__init__.py b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/api/__init__.py index 7285e76fb..46355e2cf 100644 --- a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/api/__init__.py +++ b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/api/__init__.py @@ -11,6 +11,9 @@ # class InvalidRequest(Exception): + """ + exception of invalid request + """ def __init__(self, message): - Exception.__init__(self,message) \ No newline at end of file + Exception.__init__(self,message) diff --git a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/api/pdr_plugin_api.py b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/api/pdr_plugin_api.py index ae25e9fbc..47428fcc9 100644 --- a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/api/pdr_plugin_api.py +++ b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/api/pdr_plugin_api.py @@ -28,7 +28,7 @@ def __init__(self, isolation_mgr): """ Initialize a new instance of the PDRPluginAPI class. """ - super(PDRPluginAPI, self).__init__() + super().__init__() self.isolation_mgr = isolation_mgr # Define routes using the base class's method @@ -37,12 +37,13 @@ def __init__(self, isolation_mgr): self.add_route("DELETE", "/excluded", self.include_ports) - async def get_excluded_ports(self, request): + async def get_excluded_ports(self, request): # pylint: disable=unused-argument """ Return ports from exclude list as comma separated port names """ items = self.isolation_mgr.exclude_list.items() - formatted_items = [f"{item.port_name}: {'infinite' if item.ttl_seconds == 0 else int(max(0, item.remove_time - time.time()))}" for item in items] + formatted_items = [f"{item.port_name}: {'infinite' if item.ttl_seconds == 0 else int(max(0, item.remove_time - time.time()))}" + for item in items] response = EOL.join(formatted_items) + ('' if not formatted_items else EOL) return self.web_response(response, HTTPStatus.OK) @@ -104,7 +105,6 @@ async def include_ports(self, request): return self.web_response(response, HTTPStatus.OK) - async def get_request_data(self, request): """ Deserialize request data into object for aiohttp @@ -122,8 +122,7 @@ async def get_request_data(self, request): # Return the raw text data return text - - def fix_port_name(self, port_name): + def fix_port_name(self,port_name): """ Try to fix common user mistakes for input port names Return fixed port name diff --git a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/constants.py b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/constants.py index 31097e62e..06e1019c4 100644 --- a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/constants.py +++ b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/constants.py @@ -12,7 +12,10 @@ import logging -class PDRConstants(object): +class PDRConstants(): + """ + The constants of the PDR plugin. + """ CONF_FILE = "/config/pdr_deterministic.conf" LOG_FILE = '/log/pdr_deterministic_plugin.log' @@ -55,6 +58,8 @@ class PDRConstants(object): API_ISOLATED_PORTS = "isolated_ports" SECONDARY_INSTANCE = "low_freq_debug" + TIMEOUT = 60 + EXTERNAL_EVENT_ERROR = 554 EXTERNAL_EVENT_ALERT = 553 EXTERNAL_EVENT_NOTICE = 552 diff --git a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/exclude_list.py b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/exclude_list.py index 24129abc8..50f75b916 100644 --- a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/exclude_list.py +++ b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/exclude_list.py @@ -13,7 +13,7 @@ import time import threading -class ExcludeListItem(object): +class ExcludeListItem(): """ Represents details of excluded port. @@ -33,7 +33,7 @@ def __init__(self, port_name, ttl_seconds): self.remove_time = 0 if ttl_seconds == 0 else time.time() + ttl_seconds -class ExcludeList(object): +class ExcludeList(): """ Implements list for excluded ports. @@ -71,7 +71,7 @@ def add(self, port_name, ttl_seconds = 0): def contains(self, port_name): """ Check if port exists. - Remove the port if its remove time is reached. + Remove the port if its remove time is reached. :param port_name: The name of the port. :return: True if the port still exists, False otherwise. """ @@ -81,10 +81,10 @@ def contains(self, port_name): if data.remove_time == 0 or time.time() < data.remove_time: # Excluded port return True - else: - # The time is expired, so remove port from the list - self.__dict.pop(port_name) - self.__logger.info(f"Port {port_name} automatically removed from exclude list after {data.ttl_seconds} seconds") + + # The time is expired, so remove port from the list + self.__dict.pop(port_name) + self.__logger.info(f"Port {port_name} automatically removed from exclude list after {data.ttl_seconds} seconds") return False @@ -98,8 +98,7 @@ def remove(self, port_name): self.__dict.pop(port_name) self.__logger.info(f"Port {port_name} removed from exclude list") return True - else: - return False + return False def refresh(self): diff --git a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/isolation_algo.py b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/isolation_algo.py index fe99c5f0d..74ec83ec2 100644 --- a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/isolation_algo.py +++ b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/isolation_algo.py @@ -18,8 +18,8 @@ from constants import PDRConstants as Constants from isolation_mgr import IsolationMgr from api.base_aiohttp_api import BaseAiohttpServer -from ufm_communication_mgr import UFMCommunicator from api.pdr_plugin_api import PDRPluginAPI +from ufm_communication_mgr import UFMCommunicator from utils.utils import Utils @@ -29,7 +29,7 @@ def create_logger(log_file): :param file: name of the file :return: """ - format_str = "%(asctime)-15s UFM-PDR_deterministic-plugin-{0} Machine: {1} %(levelname)-7s: %(message)s".format(log_file,'localhost') + format_str = f"%(asctime)-15s UFM-PDR_deterministic-plugin-{log_file} Machine: localhost %(levelname)-7s: %(message)s" if not os.path.exists(log_file): os.makedirs('/'.join(log_file.split('/')[:-1]), exist_ok=True) logger = logging.getLogger(log_file) @@ -76,7 +76,7 @@ def main(): ufm_port = config_parser.getint(Constants.CONF_LOGGING, Constants.CONF_INTERNAL_PORT) ufm_client = UFMCommunicator("127.0.0.1", ufm_port) logger = create_logger(Constants.LOG_FILE) - + algo_loop = IsolationMgr(ufm_client, logger) threading.Thread(target=algo_loop.main_flow).start() @@ -89,10 +89,10 @@ def main(): server = BaseAiohttpServer(logger) server.run(api.application, "127.0.0.1", int(plugin_port)) - except Exception as ex: + except Exception as ex: # pylint: disable=broad-except print(f'Failed to run the app: {str(ex)}') - + #optional second phase # rest_server = RESTserver() # rest_server.serve() diff --git a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/isolation_mgr.py b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/isolation_mgr.py index 20804fb00..fb335084a 100644 --- a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/isolation_mgr.py +++ b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/isolation_mgr.py @@ -16,7 +16,6 @@ import http import configparser import math -import json import pandas as pd import numpy from exclude_list import ExcludeList @@ -25,10 +24,13 @@ from ufm_communication_mgr import UFMCommunicator # should actually be persistent and thread safe dictionary pf PortStates -class PortData(object): +#pylint: disable=too-many-instance-attributes +class PortData(): + """ Represents the port data. """ + #pylint: disable=too-many-arguments def __init__(self, port_name=None, port_num=None, peer=None, node_type=None, active_speed=None, port_width=None, port_guid=None): """ Initialize a new instance of the PortData class. @@ -57,7 +59,7 @@ def __init__(self, port_name=None, port_num=None, peer=None, node_type=None, act -class PortState(object): +class PortState(): """ Represents the state of a port. @@ -117,7 +119,7 @@ def get_change_time(self): return self.change_time -class Issue(object): +class Issue(): """ Represents an issue that occurred on a specific port. @@ -138,7 +140,7 @@ def __init__(self, port, cause): def get_counter(counter_name, row, default=0): """ - Get the value of a specific counter from a row of data. If the counter is not present + Get the value of a specific counter from a row of data. If the counter is not present or its value is NaN, return a default value. :param counter_name: The name of the counter to get. @@ -149,7 +151,7 @@ def get_counter(counter_name, row, default=0): """ try: val = row.get(counter_name) if (row.get(counter_name) is not None and not pd.isna(row.get(counter_name))) else default - except Exception as e: + except (KeyError,ValueError,TypeError): return default return val @@ -159,6 +161,7 @@ def get_timestamp_seconds(row): ''' return row.get(Constants.TIMESTAMP) / 1000.0 / 1000.0 +#pylint: disable=too-many-instance-attributes,too-many-public-methods class IsolationMgr: ''' This class is responsible for managing the isolation of ports based on the telemetry data @@ -167,9 +170,9 @@ class IsolationMgr: def __init__(self, ufm_client: UFMCommunicator, logger): self.ufm_client = ufm_client # {port_name: PortState} - self.ports_states = dict() + self.ports_states = {} # {port_name: telemetry_data} - self.ports_data = dict() + self.ports_data = {} self.ufm_latest_isolation_state = [] pdr_config = configparser.ConfigParser() @@ -197,7 +200,7 @@ def __init__(self, ufm_client: UFMCommunicator, logger): intervals = [x[0] for x in self.ber_intervals] self.min_ber_wait_time = min(intervals) self.max_ber_wait_time = max(intervals) - self.max_ber_threshold = max([x[1] for x in self.ber_intervals]) + self.max_ber_threshold = max(x[1] for x in self.ber_intervals) self.start_time = time.time() self.max_time = self.start_time @@ -219,22 +222,23 @@ def __init__(self, ufm_client: UFMCommunicator, logger): self.exclude_list = ExcludeList(self.logger) - def calc_max_ber_wait_time(self, min_threshold): - """ - Calculates the maximum wait time for Bit Error Rate (BER) based on the given minimum threshold. + @staticmethod + def calc_max_ber_wait_time(min_threshold): + """ + Calculates the maximum wait time for Bit Error Rate (BER) based on the given minimum threshold. - Args: - min_threshold (float): The minimum threshold for BER. + Args: + min_threshold (float): The minimum threshold for BER. - Returns: - float: The maximum wait time in seconds. - """ - # min speed EDR = 32 Gb/s - min_speed, min_width = 32 * 1024 * 1024 * 1024, 1 - min_port_rate = min_speed * min_width - min_bits = float(format(float(min_threshold), '.0e').replace('-', '')) - min_sec_to_wait = min_bits / min_port_rate - return min_sec_to_wait + Returns: + float: The maximum wait time in seconds. + """ + # min speed EDR = 32 Gb/s + min_speed, min_width = 32 * 1024 * 1024 * 1024, 1 + min_port_rate = min_speed * min_width + min_bits = float(format(float(min_threshold), '.0e').replace('-', '')) + min_sec_to_wait = min_bits / min_port_rate + return min_sec_to_wait def is_out_of_operating_conf(self, port_name): """ @@ -249,7 +253,7 @@ def is_out_of_operating_conf(self, port_name): port_obj = self.ports_data.get(port_name) if not port_obj: self.logger.warning(f"Port {port_name} not found in ports data in calculation of oonoc port") - return + return False temp = port_obj.counters_values.get(Constants.TEMP_COUNTER) if temp and temp > self.tmax: return True @@ -327,7 +331,7 @@ def eval_deisolate(self, port_name): self.ports_states[port_name].update(Constants.STATE_ISOLATED, cause) return # we need some time after the change in state - elif datetime.now() >= self.ports_states[port_name].get_change_time() + timedelta(seconds=self.deisolate_consider_time): + if datetime.now() >= self.ports_states[port_name].get_change_time() + timedelta(seconds=self.deisolate_consider_time): port_obj = self.ports_data.get(port_name) port_state = self.ports_states.get(port_name) if port_state.cause == Constants.ISSUE_BER: @@ -342,7 +346,7 @@ def eval_deisolate(self, port_name): return # port is clean now - de-isolate it - # using UFM "mark as healthy" API - PUT /ufmRestV2/app/unhealthy_ports + # using UFM "mark as healthy" API - PUT /ufmRestV2/app/unhealthy_ports # { # "ports": [ # "e41d2d0300062380_3" @@ -352,7 +356,8 @@ def eval_deisolate(self, port_name): if not self.dry_run: ret = self.ufm_client.deisolate_port(port_name) if not ret or ret.status_code != http.HTTPStatus.OK: - self.logger.warning("Failed deisolating port: %s with cause: %s... status_code= %s", port_name, self.ports_states[port_name].cause, ret.status_code) + self.logger.warning("Failed deisolating port: %s with cause: %s... status_code= %s",\ + port_name, self.ports_states[port_name].cause, ret.status_code) return self.ports_states.pop(port_name) log_message = f"Deisolated port: {port_name}. dry_run: {self.dry_run}" @@ -360,7 +365,7 @@ def eval_deisolate(self, port_name): if not self.test_mode: self.ufm_client.send_event(log_message, event_id=Constants.EXTERNAL_EVENT_NOTICE, external_event_name="Deisolating Port") - def get_rate(self, port_obj, counter_name, new_val, timestamp): + def get_rate(self,port_obj, counter_name, new_val, timestamp): """ Calculate the rate of the counter """ @@ -401,7 +406,8 @@ def find_peer_row_for_port(self, port_obj, ports_counters): if ports_counters[Constants.NODE_GUID].iloc[0].startswith('0x') and not peer_guid.startswith('0x'): peer_guid = f'0x{peer_guid}' #TODO check for a way to save peer row in data structure for performance - peer_row_list = ports_counters.loc[(ports_counters[Constants.NODE_GUID] == peer_guid) & (ports_counters[Constants.PORT_NUMBER] == int(peer_num))] + peer_row_list = ports_counters.loc[(ports_counters[Constants.NODE_GUID] == peer_guid) &\ + (ports_counters[Constants.PORT_NUMBER] == int(peer_num))] if peer_row_list.empty: self.logger.warning(f"Peer port {port_obj.peer} not found in ports data") return None @@ -416,7 +422,7 @@ def calc_error_rate(self, port_obj, row, timestamp): rcv_remote_phy_error = get_counter(Constants.RCV_REMOTE_PHY_ERROR_COUNTER, row) errors = rcv_error + rcv_remote_phy_error error_rate = self.get_rate_and_update(port_obj, Constants.ERRORS_COUNTER, errors, timestamp) - return error_rate + return error_rate def check_pdr_issue(self, port_obj, row, timestamp): """ @@ -432,7 +438,7 @@ def check_pdr_issue(self, port_obj, row, timestamp): return Issue(port_obj.port_name, Constants.ISSUE_PDR) return None - def check_temp_issue(self, port_obj, row, timestamp): + def check_temp_issue(self, port_obj, row): """ Check if the port passed the temperature threshold and return an issue """ @@ -442,7 +448,7 @@ def check_temp_issue(self, port_obj, row, timestamp): if cable_temp is not None and not pd.isna(cable_temp): if cable_temp in ["NA", "N/A", "", "0C", "0"]: return None - cable_temp = int(cable_temp.split("C")[0]) if type(cable_temp) == str else cable_temp + cable_temp = int(cable_temp.split("C")[0]) if isinstance(cable_temp,str) else cable_temp old_cable_temp = port_obj.counters_values.get(Constants.TEMP_COUNTER, 0) port_obj.counters_values[Constants.TEMP_COUNTER] = cable_temp # Check temperature condition @@ -505,7 +511,7 @@ def check_ber_issue(self, port_obj, row, timestamp): if symbol_ber_val is not None: ber_data = { Constants.TIMESTAMP : timestamp, - Constants.SYMBOL_BER : symbol_ber_val, + Constants.SYMBOL_BER : symbol_ber_val, } port_obj.ber_tele_data.loc[len(port_obj.ber_tele_data)] = ber_data port_obj.last_symbol_ber_timestamp = timestamp @@ -518,7 +524,8 @@ def check_ber_issue(self, port_obj, row, timestamp): for (interval, threshold) in self.ber_intervals: symbol_ber_rate = self.calc_ber_rates(port_obj.port_name, port_obj.active_speed, port_obj.port_width, interval) if symbol_ber_rate and symbol_ber_rate > threshold: - self.logger.info(f"Isolation issue ({Constants.ISSUE_BER}) detected for port {port_obj.port_name} (speed: {port_obj.active_speed}, width: {port_obj.port_width}): " + self.logger.info(f"Isolation issue ({Constants.ISSUE_BER}) detected for port {port_obj.port_name}" + f"(speed: {port_obj.active_speed}, width: {port_obj.port_width}): " f"symbol ber rate ({symbol_ber_rate}) is higher than threshold ({threshold})") return Issue(port_obj.port_name, Constants.ISSUE_BER) return None @@ -545,13 +552,13 @@ def read_next_set_of_high_ber_or_pdr_ports(self): if not port_obj: if get_counter(Constants.RCV_PACKETS_COUNTER,row,0) == 0: # meaning it is down port continue - self.logger.warning("Port {0} not found in ports data".format(port_name)) + self.logger.warning("Port %s not found in ports data",port_name) continue # Converting from micro seconds to seconds. timestamp = get_timestamp_seconds(row) #TODO add logs regarding the exact telemetry value leading to the decision pdr_issue = self.check_pdr_issue(port_obj, row, timestamp) - temp_issue = self.check_temp_issue(port_obj, row, timestamp) + temp_issue = self.check_temp_issue(port_obj, row) link_downed_issue = self.check_link_down_issue(port_obj, row, timestamp, ports_counters) ber_issue = self.check_ber_issue(port_obj, row, timestamp) port_obj.last_timestamp = timestamp @@ -565,6 +572,7 @@ def read_next_set_of_high_ber_or_pdr_ports(self): issues[port_name] = ber_issue return issues + #pylint: disable=too-many-arguments,too-many-locals def calc_symbol_ber_rate(self, port_name, port_speed, port_width, col_name, time_delta): """ calculate the symbol BER rate for a given port given the time delta @@ -592,10 +600,12 @@ def calc_symbol_ber_rate(self, port_name, port_speed, port_width, col_name, time # Calculate the delta of 'symbol_ber' delta = port_obj.last_symbol_ber_val - comparison_sample[Constants.SYMBOL_BER] actual_speed = self.speed_types.get(port_speed, 100000) - return delta / ((port_obj.last_symbol_ber_timestamp - comparison_df.loc[comparison_idx][Constants.TIMESTAMP]) * actual_speed * port_width * 1024 * 1024 * 1024) + return delta / ((port_obj.last_symbol_ber_timestamp - + comparison_df.loc[comparison_idx][Constants.TIMESTAMP]) * + actual_speed * port_width * 1024 * 1024 * 1024) - except Exception as e: - self.logger.error(f"Error calculating {col_name}, error: {e}") + except (KeyError,ValueError,TypeError) as exception_error: + self.logger.error(f"Error calculating {col_name}, error: {exception_error}") return 0 def calc_ber_rates(self, port_name, port_speed, port_width, time_delta): @@ -666,7 +676,7 @@ def update_port_metadata(self, port_name, port): def update_ports_data(self): """ Updates the ports data by retrieving metadata from the UFM client. - + Returns: bool: True if ports data is updated, False otherwise. """ @@ -707,6 +717,7 @@ def get_port_metadata(self, port_name): if port_width: port_width = int(port_width.strip('x')) return port_speed, port_width + return None, None def set_ports_as_treated(self, ports_dict): @@ -723,7 +734,7 @@ def set_ports_as_treated(self, ports_dict): port_state = self.ports_states.get(port) if port_state and state == Constants.STATE_TREATED: port_state.state = state - + def get_isolation_state(self): """ Retrieves the isolation state of the ports. @@ -732,7 +743,7 @@ def get_isolation_state(self): None: If the test mode is enabled. List[str]: A list of isolated ports if available. """ - + if self.test_mode: # I don't want to get to the isolated ports because we simulating everything.. return @@ -764,6 +775,7 @@ def get_requested_guids(self): requested_guids = [{"guid": sys_guid, "ports": ports} for sys_guid, ports in guids.items()] return requested_guids + #pylint: disable=too-many-branches def main_flow(self): """ Executes the main flow of the Isolation Manager. @@ -782,7 +794,7 @@ def main_flow(self): self.logger.info("Isolation Manager initialized, starting isolation loop") self.get_ports_metadata() self.logger.info("Retrieved ports metadata") - while(True): + while True: try: t_begin = time.time() self.exclude_list.refresh() @@ -794,14 +806,15 @@ def main_flow(self): self.test_iteration += 1 try: issues = self.read_next_set_of_high_ber_or_pdr_ports() - except (KeyError,) as e: - self.logger.error(f"failed to read information with error {e}") + except (KeyError,TypeError,ValueError) as exception_error: + self.logger.error(f"failed to read information with error {exception_error}") if len(issues) > self.max_num_isolate: # UFM send external event - event_msg = "got too many ports detected as unhealthy: %d, skipping isolation" % len(issues) + event_msg = f"got too many ports detected as unhealthy: {len(issues)}, skipping isolation" self.logger.warning(event_msg) if not self.test_mode: - self.ufm_client.send_event(event_msg, event_id=Constants.EXTERNAL_EVENT_ALERT, external_event_name="Skipping isolation") + self.ufm_client.send_event(event_msg, event_id=Constants.EXTERNAL_EVENT_ALERT, + external_event_name="Skipping isolation") # deal with reported new issues else: @@ -815,7 +828,7 @@ def main_flow(self): for port_state in list(self.ports_states.values()): state = port_state.get_state() cause = port_state.get_cause() - # EZ: it is a state that say that some maintenance was done to the link + # EZ: it is a state that say that some maintenance was done to the link # so need to re-evaluate if to return it to service if self.automatic_deisolate or cause == Constants.ISSUE_OONOC or state == Constants.STATE_TREATED: self.eval_deisolate(port_state.name) @@ -823,10 +836,11 @@ def main_flow(self): if ports_updated: self.update_telemetry_session() t_end = time.time() - except Exception as e: + #pylint: disable=broad-except + except Exception as exception: self.logger.warning("Error in main loop") - self.logger.warning(e) + self.logger.warning(exception) traceback_err = traceback.format_exc() self.logger.warning(traceback_err) - t_end = time.time() + t_end = time.time() time.sleep(max(1, self.interval - (t_end - t_begin))) diff --git a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/ufm_communication_mgr.py b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/ufm_communication_mgr.py index 3f2cfe33e..1abe370da 100644 --- a/plugins/pdr_deterministic_plugin/ufm_sim_web_service/ufm_communication_mgr.py +++ b/plugins/pdr_deterministic_plugin/ufm_sim_web_service/ufm_communication_mgr.py @@ -10,16 +10,18 @@ # provided with the software product. # -from enum import Enum import urllib.error -from constants import PDRConstants as Constants -import requests -import logging import urllib +import logging import http +from constants import PDRConstants as Constants +import requests import pandas as pd class UFMCommunicator: + """ + communicate with the UFM, send actions to the UFM, see that ports isolated. + """ def __init__(self, host='127.0.0.1', ufm_port=8000): #TODO: read from conf @@ -27,52 +29,54 @@ def __init__(self, host='127.0.0.1', ufm_port=8000): self.ufm_protocol = "http" self.headers = {"X-Remote-User": "ufmsystem"} #self.suffix = None - self._host = "{0}:{1}".format(host, self.internal_port) - + self._host = f"{host}:{self.internal_port}" + def get_request(self, uri, headers=None): request = self.ufm_protocol + '://' + self._host + uri if not headers: headers = self.headers try: - response = requests.get(request, verify=False, headers=headers) - logging.info("UFM API Request Status: {}, URL: {}".format(response.status_code, request)) + response = requests.get(request, verify=False, headers=headers,timeout=Constants.TIMEOUT) + logging.info("UFM API Request Status: %s, URL: %s",response.status_code, request) if response.status_code == http.client.OK: return response.json() - except ConnectionRefusedError as e: - logging.error(f"failed to get data from {request} with error {e}") - return - + except ConnectionRefusedError as connection_error: + logging.error("failed to get data from %s with error %s",request,connection_error) + return None + def send_request(self, uri, data, method=Constants.POST_METHOD, headers=None): request = self.ufm_protocol + '://' + self._host + uri if not headers: headers = self.headers if method == Constants.POST_METHOD: - response = requests.post(url=request, json=data, verify=False, headers=headers) + response = requests.post(url=request, json=data, verify=False, headers=headers,timeout=Constants.TIMEOUT) elif method == Constants.PUT_METHOD: - response = requests.put(url=request, json=data, verify=False, headers=headers) + response = requests.put(url=request, json=data, verify=False, headers=headers,timeout=Constants.TIMEOUT) elif method == Constants.DELETE_METHOD: - response = requests.delete(url=request, verify=False, headers=headers) - logging.info("UFM API Request Status: {}, URL: {}".format(response.status_code, request)) + response = requests.delete(url=request, verify=False, headers=headers,timeout=Constants.TIMEOUT) + else: + return None + logging.info("UFM API Request Status: %s, URL: %s",response.status_code, request) return response - + def get_telemetry(self,test_mode): """ get the telemetry from secondary telemetry, if it in test mode it get from the simulation return DataFrame of the telemetry """ if test_mode: - url = f"http://127.0.0.1:9090/csv/xcset/simulated_telemetry" + url = "http://127.0.0.1:9090/csv/xcset/simulated_telemetry" else: url = f"http://127.0.0.1:{Constants.SECONDARY_TELEMETRY_PORT}/csv/xcset/{Constants.SECONDARY_INSTANCE}" try: telemetry_data = pd.read_csv(url) - except (pd.errors.ParserError, pd.errors.EmptyDataError, urllib.error.URLError) as e: - logging.error(f"Failed to get telemetry data from UFM, fetched url={url}. Error: {e}") + except (pd.errors.ParserError, pd.errors.EmptyDataError, urllib.error.URLError) as error: + logging.error("Failed to get telemetry data from UFM, fetched url=%s. Error: %s",url,error) telemetry_data = None return telemetry_data - - def send_event(self, message, event_id=Constants.EXTERNAL_EVENT_NOTICE, external_event_name="PDR Plugin Event", external_event_type="PDR Plugin Event"): + def send_event(self, message, event_id=Constants.EXTERNAL_EVENT_NOTICE, + external_event_name="PDR Plugin Event", external_event_type="PDR Plugin Event"): data = { "event_id": event_id, "description": message, @@ -82,8 +86,8 @@ def send_event(self, message, event_id=Constants.EXTERNAL_EVENT_NOTICE, external } ret = self.send_request(Constants.POST_EVENT_REST, data) - if ret: - return True + if ret: + return True return False def get_isolated_ports(self): @@ -113,9 +117,9 @@ def deisolate_port(self, port_name): "ports_policy": "HEALTHY", } return self.send_request(Constants.ISOLATION_REST, data, method=Constants.PUT_METHOD) - + def get_ports_metadata(self): return self.get_request(Constants.GET_ACTIVE_PORTS_REST) def get_port_metadata(self, port_name): - return self.get_request("%s/%s" % (Constants.GET_PORTS_REST, port_name)) + return self.get_request(f"{Constants.GET_PORTS_REST}/ {port_name}")