1#!/usr/bin/env python3 2 3r""" 4See class prolog below for details. 5""" 6 7import json 8import logging 9import os 10import platform 11import re 12import subprocess 13import sys 14import time 15from errno import EACCES, EPERM 16 17import yaml 18 19sys.dont_write_bytecode = True 20 21 22script_dir = os.path.dirname(os.path.abspath(__file__)) 23sys.path.append(script_dir) 24# Walk path and append to sys.path 25for root, dirs, files in os.walk(script_dir): 26 for dir in dirs: 27 sys.path.append(os.path.join(root, dir)) 28 29from ssh_utility import SSHRemoteclient # NOQA 30from telnet_utility import TelnetRemoteclient # NOQA 31 32r""" 33User define plugins python functions. 34 35It will imports files from directory plugins 36 37plugins 38├── file1.py 39└── file2.py 40 41Example how to define in YAML: 42 - plugin: 43 - plugin_name: plugin.foo_func.foo_func_yaml 44 - plugin_args: 45 - arg1 46 - arg2 47""" 48plugin_dir = os.path.join(os.path.dirname(__file__), "plugins") 49sys.path.append(plugin_dir) 50 51for module in os.listdir(plugin_dir): 52 if module == "__init__.py" or not module.endswith(".py"): 53 continue 54 55 plugin_module = f"plugins.{module[:-3]}" 56 try: 57 plugin = __import__(plugin_module, globals(), locals(), [], 0) 58 except Exception as e: 59 print(f"PLUGIN: Exception: {e}") 60 print(f"PLUGIN: Module import failed: {module}") 61 continue 62 63r""" 64This is for plugin functions returning data or responses to the caller 65in YAML plugin setup. 66 67Example: 68 69 - plugin: 70 - plugin_name: version = plugin.ssh_execution.ssh_execute_cmd 71 - plugin_args: 72 - ${hostname} 73 - ${username} 74 - ${password} 75 - "cat /etc/os-release | grep VERSION_ID | awk -F'=' '{print $2}'" 76 - plugin: 77 - plugin_name: plugin.print_vars.print_vars 78 - plugin_args: 79 - version 80 81where first plugin "version" var is used by another plugin in the YAML 82block or plugin 83 84""" 85global global_log_store_path 86global global_plugin_dict 87global global_plugin_list 88 89# Hold the plugin return values in dict and plugin return vars in list. 90# Dict is to reference and update vars processing in parser where as 91# list is for current vars from the plugin block which needs processing. 92global_plugin_dict = {} 93global_plugin_list = [] 94 95# Hold the plugin return named declared if function returned values are 96# list,dict. 97# Refer this name list to look up the plugin dict for eval() args function 98# Example ['version'] 99global_plugin_type_list = [] 100 101# Path where logs are to be stored or written. 102global_log_store_path = "" 103 104# Plugin error state defaults. 105plugin_error_dict = { 106 "exit_on_error": False, 107 "continue_on_error": False, 108} 109 110 111class ffdc_collector: 112 r""" 113 Execute commands from configuration file to collect log files. 114 Fetch and store generated files at the specified location. 115 116 """ 117 118 def __init__( 119 self, 120 hostname, 121 username, 122 password, 123 port_ssh, 124 port_https, 125 port_ipmi, 126 ffdc_config, 127 location, 128 remote_type, 129 remote_protocol, 130 env_vars, 131 econfig, 132 log_level, 133 ): 134 r""" 135 Initialize the FFDCCollector object with the provided parameters. 136 137 This method initializes an FFDCCollector object with the given 138 attributes. The attributes represent the configuration for connecting 139 to a remote system, collecting log data, and storing the collected 140 data. 141 142 Parameters: 143 hostname (str): Name or IP address of the targeted 144 (remote) system. 145 username (str): User on the targeted system with access 146 to log files. 147 password (str): Password for the user on the targeted 148 system. 149 port_ssh (int, optional): SSH port value. Defaults to 22. 150 port_https (int, optional): HTTPS port value. Defaults to 443. 151 port_ipmi (int, optional): IPMI port value. Defaults to 623. 152 ffdc_config (str): Configuration file listing commands 153 and files for FFDC. 154 location (str): Where to store collected log data. 155 remote_type (str): Block YAML type name of the remote 156 host. 157 remote_protocol (str): Protocol to use to collect data. 158 env_vars (dict, optional): User-defined CLI environment variables. 159 Defaults to None. 160 econfig (str, optional): User-defined environment variables 161 YAML file. Defaults to None. 162 log_level (str, optional): Log level for the collector. 163 Defaults to "INFO". 164 """ 165 166 self.hostname = hostname 167 self.username = username 168 self.password = password 169 self.port_ssh = str(port_ssh) 170 self.port_https = str(port_https) 171 self.port_ipmi = str(port_ipmi) 172 self.ffdc_config = ffdc_config 173 self.location = location + "/" + remote_type.upper() 174 self.ssh_remoteclient = None 175 self.telnet_remoteclient = None 176 self.ffdc_dir_path = "" 177 self.ffdc_prefix = "" 178 self.target_type = remote_type.upper() 179 self.remote_protocol = remote_protocol.upper() 180 self.env_vars = env_vars if env_vars else {} 181 self.econfig = econfig if econfig else {} 182 self.start_time = 0 183 self.elapsed_time = "" 184 self.env_dict = {} 185 self.logger = None 186 187 """ 188 Set prefix values for SCP files and directories. 189 Since the time stamp is at second granularity, these values are set 190 here to be sure that all files for this run will have the same 191 timestamps and be saved in the same directory. 192 self.location == local system for now 193 """ 194 self.set_ffdc_default_store_path() 195 196 # Logger for this run. Need to be after set_ffdc_default_store_path() 197 self.script_logging(getattr(logging, log_level.upper())) 198 199 # Verify top level directory exists for storage 200 self.validate_local_store(self.location) 201 202 if self.verify_script_env(): 203 try: 204 with open(self.ffdc_config, "r") as file: 205 self.ffdc_actions = yaml.safe_load(file) 206 except yaml.YAMLError as e: 207 self.logger.error(e) 208 sys.exit(-1) 209 210 if self.target_type not in self.ffdc_actions: 211 self.logger.error( 212 "\n\tERROR: %s is not listed in %s.\n\n" 213 % (self.target_type, self.ffdc_config) 214 ) 215 sys.exit(-1) 216 217 self.logger.info("\n\tENV: User define input YAML variables") 218 self.env_dict = self.load_env() 219 else: 220 sys.exit(-1) 221 222 def verify_script_env(self): 223 r""" 224 Verify that all required environment variables are set. 225 226 This method checks if all required environment variables are set. 227 If any required variable is missing, the method returns False. 228 Otherwise, it returns True. 229 230 Returns: 231 bool: True if all required environment variables are set, 232 False otherwise. 233 """ 234 # Import to log version 235 import click 236 import paramiko 237 238 run_env_ok = True 239 240 try: 241 redfishtool_version = ( 242 self.run_tool_cmd("redfishtool -V").split(" ")[2].strip("\n") 243 ) 244 except Exception as e: 245 self.logger.error("\tEXCEPTION redfishtool: %s", e) 246 redfishtool_version = "Not Installed (optional)" 247 248 try: 249 ipmitool_version = self.run_tool_cmd("ipmitool -V").split(" ")[2] 250 except Exception as e: 251 self.logger.error("\tEXCEPTION ipmitool: %s", e) 252 ipmitool_version = "Not Installed (optional)" 253 254 self.logger.info("\n\t---- Script host environment ----") 255 self.logger.info( 256 "\t{:<10} {:<10}".format("Script hostname", os.uname()[1]) 257 ) 258 self.logger.info( 259 "\t{:<10} {:<10}".format("Script host os", platform.platform()) 260 ) 261 self.logger.info( 262 "\t{:<10} {:>10}".format("Python", platform.python_version()) 263 ) 264 self.logger.info("\t{:<10} {:>10}".format("PyYAML", yaml.__version__)) 265 self.logger.info("\t{:<10} {:>10}".format("click", click.__version__)) 266 self.logger.info( 267 "\t{:<10} {:>10}".format("paramiko", paramiko.__version__) 268 ) 269 self.logger.info( 270 "\t{:<10} {:>9}".format("redfishtool", redfishtool_version) 271 ) 272 self.logger.info( 273 "\t{:<10} {:>12}".format("ipmitool", ipmitool_version) 274 ) 275 276 if eval(yaml.__version__.replace(".", ",")) < (5, 3, 0): 277 self.logger.error( 278 "\n\tERROR: Python or python packages do not meet minimum" 279 " version requirement." 280 ) 281 self.logger.error( 282 "\tERROR: PyYAML version 5.3.0 or higher is needed.\n" 283 ) 284 run_env_ok = False 285 286 self.logger.info("\t---- End script host environment ----") 287 return run_env_ok 288 289 def script_logging(self, log_level_attr): 290 """ 291 Create a logger for the script with the specified log level. 292 293 This method creates a logger for the script with the specified 294 log level. The logger is configured to write log messages to a file 295 and the console. 296 297 self.logger = logging.getLogger(__name__) 298 299 Setting logger with __name__ will add the trace 300 Example: 301 302 INFO:ffdc_collector: System Type: OPENBMC 303 304 Currently, set to empty purposely to log as 305 System Type: OPENBMC 306 307 Parameters: 308 log_level_attr (str): The log level for the logger 309 (e.g., "DEBUG", "INFO", "WARNING", 310 "ERROR", "CRITICAL"). 311 312 Returns: 313 None 314 """ 315 self.logger = logging.getLogger() 316 self.logger.setLevel(log_level_attr) 317 318 log_file_handler = logging.FileHandler( 319 self.ffdc_dir_path + "collector.log" 320 ) 321 stdout_handler = logging.StreamHandler(sys.stdout) 322 323 self.logger.addHandler(log_file_handler) 324 self.logger.addHandler(stdout_handler) 325 326 # Turn off paramiko INFO logging 327 logging.getLogger("paramiko").setLevel(logging.WARNING) 328 329 def target_is_pingable(self): 330 r""" 331 Check if the target system is ping-able. 332 333 This method checks if the target system is reachable by sending an 334 ICMP echo request (ping). If the target system responds to the ping, 335 the method returns True. Otherwise, it returns False. 336 337 Returns: 338 bool: True if the target system is ping-able, False otherwise. 339 """ 340 response = os.system("ping -c 2 %s 2>&1 >/dev/null" % self.hostname) 341 if response == 0: 342 self.logger.info( 343 "\n\t[Check] %s is ping-able.\t\t [OK]" % self.hostname 344 ) 345 return True 346 else: 347 self.logger.error( 348 "\n\tERROR: %s is not ping-able. FFDC collection aborted.\n" 349 % self.hostname 350 ) 351 sys.exit(-1) 352 return False 353 354 def collect_ffdc(self): 355 r""" 356 Initiate FFDC collection based on the requested protocol. 357 358 This method initiates FFDC (First Failure Data Capture) collection 359 based on the requested protocol (SSH,SCP, TELNET, REDFISH, IPMI). 360 The method establishes a connection to the target system using the 361 specified protocol and collects the required FFDC data. 362 363 Returns: 364 None 365 """ 366 self.logger.info( 367 "\n\t---- Start communicating with %s ----" % self.hostname 368 ) 369 self.start_time = time.time() 370 371 # Find the list of target and protocol supported. 372 check_protocol_list = [] 373 config_dict = self.ffdc_actions 374 375 for target_type in config_dict.keys(): 376 if self.target_type != target_type: 377 continue 378 379 for k, v in config_dict[target_type].items(): 380 if v["PROTOCOL"][0] not in check_protocol_list: 381 check_protocol_list.append(v["PROTOCOL"][0]) 382 383 self.logger.info( 384 "\n\t %s protocol type: %s" 385 % (self.target_type, check_protocol_list) 386 ) 387 388 verified_working_protocol = self.verify_protocol(check_protocol_list) 389 390 if verified_working_protocol: 391 self.logger.info( 392 "\n\t---- Completed protocol pre-requisite check ----\n" 393 ) 394 395 # Verify top level directory exists for storage 396 self.validate_local_store(self.location) 397 398 if (self.remote_protocol not in verified_working_protocol) and ( 399 self.remote_protocol != "ALL" 400 ): 401 self.logger.info( 402 "\n\tWorking protocol list: %s" % verified_working_protocol 403 ) 404 self.logger.error( 405 "\tERROR: Requested protocol %s is not in working protocol" 406 " list.\n" % self.remote_protocol 407 ) 408 sys.exit(-1) 409 else: 410 self.generate_ffdc(verified_working_protocol) 411 412 def ssh_to_target_system(self): 413 r""" 414 Establish an SSH connection to the target system. 415 416 This method establishes an SSH connection to the target system using 417 the provided hostname, username, password, and SSH port. If the 418 connection is successful, the method returns True. Otherwise, it logs 419 an error message and returns False. 420 421 Returns: 422 bool: True if the connection is successful, False otherwise. 423 """ 424 425 self.ssh_remoteclient = SSHRemoteclient( 426 self.hostname, self.username, self.password, self.port_ssh 427 ) 428 429 if self.ssh_remoteclient.ssh_remoteclient_login(): 430 self.logger.info( 431 "\n\t[Check] %s SSH connection established.\t [OK]" 432 % self.hostname 433 ) 434 435 # Check scp connection. 436 # If scp connection fails, 437 # continue with FFDC generation but skip scp files to local host. 438 self.ssh_remoteclient.scp_connection() 439 return True 440 else: 441 self.logger.info( 442 "\n\t[Check] %s SSH connection.\t [NOT AVAILABLE]" 443 % self.hostname 444 ) 445 return False 446 447 def telnet_to_target_system(self): 448 r""" 449 Establish a Telnet connection to the target system. 450 451 This method establishes a Telnet connection to the target system using 452 the provided hostname, username, and Telnet port. If the connection is 453 successful, the method returns True. Otherwise, it logs an error 454 message and returns False. 455 456 Returns: 457 bool: True if the connection is successful, False otherwise. 458 """ 459 self.telnet_remoteclient = TelnetRemoteclient( 460 self.hostname, self.username, self.password 461 ) 462 if self.telnet_remoteclient.tn_remoteclient_login(): 463 self.logger.info( 464 "\n\t[Check] %s Telnet connection established.\t [OK]" 465 % self.hostname 466 ) 467 return True 468 else: 469 self.logger.info( 470 "\n\t[Check] %s Telnet connection.\t [NOT AVAILABLE]" 471 % self.hostname 472 ) 473 return False 474 475 def generate_ffdc(self, working_protocol_list): 476 r""" 477 Generate FFDC (First Failure Data Capture) based on the remote host 478 type and working protocols. 479 480 This method determines the actions to be performed for generating FFDC 481 based on the remote host type and the list of confirmed working 482 protocols. The method iterates through the available actions for the 483 remote host type and checks if any of the working protocols are 484 supported. If a supported protocol is found, the method executes the 485 corresponding FFDC generation action. 486 487 Parameters: 488 working_protocol_list (list): A list of confirmed working 489 protocols to connect to the 490 remote host. 491 492 Returns: 493 None 494 """ 495 self.logger.info( 496 "\n\t---- Executing commands on " + self.hostname + " ----" 497 ) 498 self.logger.info( 499 "\n\tWorking protocol list: %s" % working_protocol_list 500 ) 501 502 config_dict = self.ffdc_actions 503 for target_type in config_dict.keys(): 504 if self.target_type != target_type: 505 continue 506 507 self.logger.info("\n\tFFDC Path: %s " % self.ffdc_dir_path) 508 global_plugin_dict["global_log_store_path"] = self.ffdc_dir_path 509 self.logger.info("\tSystem Type: %s" % target_type) 510 for k, v in config_dict[target_type].items(): 511 protocol = v["PROTOCOL"][0] 512 513 if ( 514 self.remote_protocol not in working_protocol_list 515 and self.remote_protocol != "ALL" 516 ) or protocol not in working_protocol_list: 517 continue 518 519 if protocol in working_protocol_list: 520 if protocol in ["SSH", "SCP"]: 521 self.protocol_ssh(protocol, target_type, k) 522 elif protocol == "TELNET": 523 self.protocol_telnet(target_type, k) 524 elif protocol in ["REDFISH", "IPMI", "SHELL"]: 525 self.protocol_service_execute(protocol, target_type, k) 526 else: 527 self.logger.error( 528 "\n\tERROR: %s is not available for %s." 529 % (protocol, self.hostname) 530 ) 531 532 # Close network connection after collecting all files 533 self.elapsed_time = time.strftime( 534 "%H:%M:%S", time.gmtime(time.time() - self.start_time) 535 ) 536 self.logger.info("\n\tTotal time taken: %s" % self.elapsed_time) 537 if self.ssh_remoteclient: 538 self.ssh_remoteclient.ssh_remoteclient_disconnect() 539 if self.telnet_remoteclient: 540 self.telnet_remoteclient.tn_remoteclient_disconnect() 541 542 def protocol_ssh(self, protocol, target_type, sub_type): 543 r""" 544 Perform actions using SSH and SCP protocols. 545 546 This method executes a set of commands using the SSH protocol to 547 connect to the target system and collect FFDC data. The method takes 548 the protocol, target type, and sub-type as arguments and performs the 549 corresponding actions based on the provided parameters. 550 551 Parameters: 552 protocol (str): The protocol to execute (SSH or SCP). 553 target_type (str): The type group of the remote host. 554 sub_type (str): The group type of commands to execute. 555 556 Returns: 557 None 558 """ 559 if protocol == "SCP": 560 self.group_copy(self.ffdc_actions[target_type][sub_type]) 561 else: 562 self.collect_and_copy_ffdc( 563 self.ffdc_actions[target_type][sub_type] 564 ) 565 566 def protocol_telnet(self, target_type, sub_type): 567 r""" 568 Perform actions using the Telnet protocol. 569 570 This method executes a set of commands using the Telnet protocol to 571 connect to the target system and collect FFDC data. The method takes 572 the target type and sub-type as arguments and performs the 573 corresponding actions based on the provided parameters. 574 575 Parameters: 576 target_type (str): The type group of the remote host. 577 sub_type (str): The group type of commands to execute. 578 579 Returns: 580 None 581 """ 582 self.logger.info( 583 "\n\t[Run] Executing commands on %s using %s" 584 % (self.hostname, "TELNET") 585 ) 586 telnet_files_saved = [] 587 progress_counter = 0 588 list_of_commands = self.ffdc_actions[target_type][sub_type]["COMMANDS"] 589 for index, each_cmd in enumerate(list_of_commands, start=0): 590 command_txt, command_timeout = self.unpack_command(each_cmd) 591 result = self.telnet_remoteclient.execute_command( 592 command_txt, command_timeout 593 ) 594 if result: 595 try: 596 targ_file = self.ffdc_actions[target_type][sub_type][ 597 "FILES" 598 ][index] 599 except IndexError: 600 targ_file = command_txt 601 self.logger.warning( 602 "\n\t[WARN] Missing filename to store data from" 603 " telnet %s." % each_cmd 604 ) 605 self.logger.warning( 606 "\t[WARN] Data will be stored in %s." % targ_file 607 ) 608 targ_file_with_path = ( 609 self.ffdc_dir_path + self.ffdc_prefix + targ_file 610 ) 611 # Creates a new file 612 with open(targ_file_with_path, "w") as fp: 613 fp.write(result) 614 fp.close 615 telnet_files_saved.append(targ_file) 616 progress_counter += 1 617 self.print_progress(progress_counter) 618 self.logger.info("\n\t[Run] Commands execution completed.\t\t [OK]") 619 for file in telnet_files_saved: 620 self.logger.info("\n\t\tSuccessfully save file " + file + ".") 621 622 def protocol_service_execute(self, protocol, target_type, sub_type): 623 r""" 624 Perform actions for a given protocol. 625 626 This method executes a set of commands using the specified protocol to 627 connect to the target system and collect FFDC data. The method takes 628 the protocol, target type, and sub-type as arguments and performs the 629 corresponding actions based on the provided parameters. 630 631 Parameters: 632 protocol (str): The protocol to execute 633 (REDFISH, IPMI, or SHELL). 634 target_type (str): The type group of the remote host. 635 sub_type (str): The group type of commands to execute. 636 637 Returns: 638 None 639 """ 640 self.logger.info( 641 "\n\t[Run] Executing commands to %s using %s" 642 % (self.hostname, protocol) 643 ) 644 executed_files_saved = [] 645 progress_counter = 0 646 list_of_cmd = self.get_command_list( 647 self.ffdc_actions[target_type][sub_type] 648 ) 649 for index, each_cmd in enumerate(list_of_cmd, start=0): 650 plugin_call = False 651 if isinstance(each_cmd, dict): 652 if "plugin" in each_cmd: 653 # If the error is set and plugin explicitly 654 # requested to skip execution on error.. 655 if plugin_error_dict[ 656 "exit_on_error" 657 ] and self.plugin_error_check(each_cmd["plugin"]): 658 self.logger.info( 659 "\n\t[PLUGIN-ERROR] exit_on_error: %s" 660 % plugin_error_dict["exit_on_error"] 661 ) 662 self.logger.info( 663 "\t[PLUGIN-SKIP] %s" % each_cmd["plugin"][0] 664 ) 665 continue 666 plugin_call = True 667 # call the plugin 668 self.logger.info("\n\t[PLUGIN-START]") 669 result = self.execute_plugin_block(each_cmd["plugin"]) 670 self.logger.info("\t[PLUGIN-END]\n") 671 else: 672 each_cmd = self.yaml_env_and_plugin_vars_populate(each_cmd) 673 674 if not plugin_call: 675 result = self.run_tool_cmd(each_cmd) 676 if result: 677 try: 678 file_name = self.get_file_list( 679 self.ffdc_actions[target_type][sub_type] 680 )[index] 681 # If file is specified as None. 682 if file_name == "None": 683 continue 684 targ_file = self.yaml_env_and_plugin_vars_populate( 685 file_name 686 ) 687 except IndexError: 688 targ_file = each_cmd.split("/")[-1] 689 self.logger.warning( 690 "\n\t[WARN] Missing filename to store data from %s." 691 % each_cmd 692 ) 693 self.logger.warning( 694 "\t[WARN] Data will be stored in %s." % targ_file 695 ) 696 697 targ_file_with_path = ( 698 self.ffdc_dir_path + self.ffdc_prefix + targ_file 699 ) 700 701 # Creates a new file 702 with open(targ_file_with_path, "w") as fp: 703 if isinstance(result, dict): 704 fp.write(json.dumps(result)) 705 else: 706 fp.write(result) 707 fp.close 708 executed_files_saved.append(targ_file) 709 710 progress_counter += 1 711 self.print_progress(progress_counter) 712 713 self.logger.info("\n\t[Run] Commands execution completed.\t\t [OK]") 714 715 for file in executed_files_saved: 716 self.logger.info("\n\t\tSuccessfully save file " + file + ".") 717 718 def collect_and_copy_ffdc( 719 self, ffdc_actions_for_target_type, form_filename=False 720 ): 721 r""" 722 Send commands and collect FFDC data from the targeted system. 723 724 This method sends a set of commands and collects FFDC data from the 725 targeted system based on the provided ffdc_actions_for_target_type 726 dictionary. The method also has an optional form_filename parameter, 727 which, if set to True, prepends the target type to the output file 728 name. 729 730 Parameters: 731 ffdc_actions_for_target_type (dict): A dictionary containing 732 commands and files for the 733 selected remote host type. 734 form_filename (bool, optional): If True, prepends the target 735 type to the output file name. 736 Defaults to False. 737 738 Returns: 739 None 740 """ 741 # Executing commands, if any 742 self.ssh_execute_ffdc_commands( 743 ffdc_actions_for_target_type, form_filename 744 ) 745 746 # Copying files 747 if self.ssh_remoteclient.scpclient: 748 self.logger.info( 749 "\n\n\tCopying FFDC files from remote system %s.\n" 750 % self.hostname 751 ) 752 753 # Retrieving files from target system 754 list_of_files = self.get_file_list(ffdc_actions_for_target_type) 755 self.scp_ffdc( 756 self.ffdc_dir_path, 757 self.ffdc_prefix, 758 form_filename, 759 list_of_files, 760 ) 761 else: 762 self.logger.info( 763 "\n\n\tSkip copying FFDC files from remote system %s.\n" 764 % self.hostname 765 ) 766 767 def get_command_list(self, ffdc_actions_for_target_type): 768 r""" 769 Fetch a list of commands from the configuration file. 770 771 This method retrieves a list of commands from the 772 ffdc_actions_for_target_type dictionary, which contains commands and 773 files for the selected remote host type. The method returns the list 774 of commands. 775 776 Parameters: 777 ffdc_actions_for_target_type (dict): A dictionary containing 778 commands and files for the 779 selected remote host type. 780 781 Returns: 782 list: A list of commands. 783 """ 784 try: 785 list_of_commands = ffdc_actions_for_target_type["COMMANDS"] 786 except KeyError: 787 list_of_commands = [] 788 return list_of_commands 789 790 def get_file_list(self, ffdc_actions_for_target_type): 791 r""" 792 Fetch a list of files from the configuration file. 793 794 This method retrieves a list of files from the 795 ffdc_actions_for_target_type dictionary, which contains commands and 796 files for the selected remote host type. The method returns the list 797 of files. 798 799 Parameters: 800 ffdc_actions_for_target_type (dict): A dictionary containing 801 commands and files for the 802 selected remote host type. 803 804 Returns: 805 list: A list of files. 806 """ 807 try: 808 list_of_files = ffdc_actions_for_target_type["FILES"] 809 except KeyError: 810 list_of_files = [] 811 return list_of_files 812 813 def unpack_command(self, command): 814 r""" 815 Unpack a command from the configuration file, handling both dictionary 816 and string inputs. 817 818 This method takes a command from the configuration file, which can be 819 either a dictionary or a string. If the input is a dictionary, the 820 method extracts the command text and timeout from the dictionary. 821 If the input is a string, the method assumes a default timeout of 822 60 seconds. 823 The method returns a tuple containing the command text and timeout. 824 825 Parameters: 826 command (dict or str): A command from the configuration file, 827 which can be either a dictionary or a 828 string. 829 830 Returns: 831 tuple: A tuple containing the command text and timeout. 832 """ 833 if isinstance(command, dict): 834 command_txt = next(iter(command)) 835 command_timeout = next(iter(command.values())) 836 elif isinstance(command, str): 837 command_txt = command 838 # Default command timeout 60 seconds 839 command_timeout = 60 840 841 return command_txt, command_timeout 842 843 def ssh_execute_ffdc_commands( 844 self, ffdc_actions_for_target_type, form_filename=False 845 ): 846 r""" 847 Send commands in the ffdc_config file to the targeted system using SSH. 848 849 This method sends a set of commands and collects FFDC data from the 850 targeted system using the SSH protocol. The method takes the 851 ffdc_actions_for_target_type dictionary and an optional 852 form_filename parameter as arguments. 853 854 If form_filename is set to True, the method prepends the target type 855 to the output file name. The method returns the output of the executed 856 commands. 857 858 It also prints the progress counter string + on the console. 859 860 Parameters: 861 ffdc_actions_for_target_type (dict): A dictionary containing 862 commands and files for the 863 selected remote host type. 864 form_filename (bool, optional): If True, prepends the target 865 type to the output file name. 866 Defaults to False. 867 868 Returns: 869 None 870 """ 871 self.logger.info( 872 "\n\t[Run] Executing commands on %s using %s" 873 % (self.hostname, ffdc_actions_for_target_type["PROTOCOL"][0]) 874 ) 875 876 list_of_commands = self.get_command_list(ffdc_actions_for_target_type) 877 # If command list is empty, returns 878 if not list_of_commands: 879 return 880 881 progress_counter = 0 882 for command in list_of_commands: 883 command_txt, command_timeout = self.unpack_command(command) 884 885 if form_filename: 886 command_txt = str(command_txt % self.target_type) 887 888 ( 889 cmd_exit_code, 890 err, 891 response, 892 ) = self.ssh_remoteclient.execute_command( 893 command_txt, command_timeout 894 ) 895 896 if cmd_exit_code: 897 self.logger.warning( 898 "\n\t\t[WARN] %s exits with code %s." 899 % (command_txt, str(cmd_exit_code)) 900 ) 901 self.logger.warning("\t\t[WARN] %s " % err) 902 903 progress_counter += 1 904 self.print_progress(progress_counter) 905 906 self.logger.info("\n\t[Run] Commands execution completed.\t\t [OK]") 907 908 def group_copy(self, ffdc_actions_for_target_type): 909 r""" 910 SCP a group of files (wildcard) from the remote host. 911 912 This method copies a group of files from the remote host using the SCP 913 protocol. The method takes the fdc_actions_for_target_type dictionary 914 as an argument, which contains commands and files for the selected 915 remote host type. 916 917 Parameters: 918 fdc_actions_for_target_type (dict): A dictionary containing 919 commands and files for the 920 selected remote host type. 921 922 Returns: 923 None 924 """ 925 if self.ssh_remoteclient.scpclient: 926 self.logger.info( 927 "\n\tCopying files from remote system %s via SCP.\n" 928 % self.hostname 929 ) 930 931 list_of_commands = self.get_command_list( 932 ffdc_actions_for_target_type 933 ) 934 # If command list is empty, returns 935 if not list_of_commands: 936 return 937 938 for command in list_of_commands: 939 try: 940 command = self.yaml_env_and_plugin_vars_populate(command) 941 except IndexError: 942 self.logger.error("\t\tInvalid command %s" % command) 943 continue 944 945 ( 946 cmd_exit_code, 947 err, 948 response, 949 ) = self.ssh_remoteclient.execute_command(command) 950 951 # If file does not exist, code take no action. 952 # cmd_exit_code is ignored for this scenario. 953 if response: 954 scp_result = self.ssh_remoteclient.scp_file_from_remote( 955 response.split("\n"), self.ffdc_dir_path 956 ) 957 if scp_result: 958 self.logger.info( 959 "\t\tSuccessfully copied from " 960 + self.hostname 961 + ":" 962 + command 963 ) 964 else: 965 self.logger.info("\t\t%s has no result" % command) 966 967 else: 968 self.logger.info( 969 "\n\n\tSkip copying files from remote system %s.\n" 970 % self.hostname 971 ) 972 973 def scp_ffdc( 974 self, 975 targ_dir_path, 976 targ_file_prefix, 977 form_filename, 978 file_list=None, 979 quiet=None, 980 ): 981 r""" 982 SCP all files in the file_dict to the indicated directory on the local 983 system. 984 985 This method copies all files specified in the file_dict dictionary 986 from the targeted system to the local system using the SCP protocol. 987 The method takes the target directory path, target file prefix, and a 988 boolean flag form_filename as required arguments. 989 990 The file_dict argument is optional and contains the files to be copied. 991 The quiet argument is also optional and, if set to True, suppresses 992 the output of the SCP operation. 993 994 Parameters: 995 targ_dir_path (str): The path of the directory to receive 996 the files on the local system. 997 targ_file_prefix (str): Prefix which will be prepended to each 998 target file's name. 999 form_filename (bool): If True, prepends the target type to 1000 the file names. 1001 file_dict (dict, optional): A dictionary containing the files to 1002 be copied. Defaults to None. 1003 quiet (bool, optional): If True, suppresses the output of the 1004 SCP operation. Defaults to None. 1005 1006 Returns: 1007 None 1008 """ 1009 progress_counter = 0 1010 for filename in file_list: 1011 if form_filename: 1012 filename = str(filename % self.target_type) 1013 source_file_path = filename 1014 targ_file_path = ( 1015 targ_dir_path + targ_file_prefix + filename.split("/")[-1] 1016 ) 1017 1018 # If source file name contains wild card, copy filename as is. 1019 if "*" in source_file_path: 1020 scp_result = self.ssh_remoteclient.scp_file_from_remote( 1021 source_file_path, self.ffdc_dir_path 1022 ) 1023 else: 1024 scp_result = self.ssh_remoteclient.scp_file_from_remote( 1025 source_file_path, targ_file_path 1026 ) 1027 1028 if not quiet: 1029 if scp_result: 1030 self.logger.info( 1031 "\t\tSuccessfully copied from " 1032 + self.hostname 1033 + ":" 1034 + source_file_path 1035 + ".\n" 1036 ) 1037 else: 1038 self.logger.info( 1039 "\t\tFail to copy from " 1040 + self.hostname 1041 + ":" 1042 + source_file_path 1043 + ".\n" 1044 ) 1045 else: 1046 progress_counter += 1 1047 self.print_progress(progress_counter) 1048 1049 def set_ffdc_default_store_path(self): 1050 r""" 1051 Set default values for self.ffdc_dir_path and self.ffdc_prefix. 1052 1053 This method sets default values for the self.ffdc_dir_path and 1054 self.ffdc_prefix class variables. 1055 1056 The collected FFDC files will be stored in the directory 1057 /self.location/hostname_timestr/, with individual files having the 1058 format timestr_filename where timestr is in %Y%m%d-%H%M%S. 1059 1060 Returns: 1061 None 1062 """ 1063 timestr = time.strftime("%Y%m%d-%H%M%S") 1064 self.ffdc_dir_path = ( 1065 self.location + "/" + self.hostname + "_" + timestr + "/" 1066 ) 1067 self.ffdc_prefix = timestr + "_" 1068 self.validate_local_store(self.ffdc_dir_path) 1069 1070 # Need to verify local store path exists prior to instantiate this class. 1071 # This class method to validate log path before referencing this class. 1072 @classmethod 1073 def validate_local_store(cls, dir_path): 1074 r""" 1075 Ensure the specified directory exists to store FFDC files locally. 1076 1077 This method checks if the provided dir_path exists. If the directory 1078 does not exist, the method creates it. The method does not return any 1079 value. 1080 1081 Parameters: 1082 dir_path (str): The directory path where collected FFDC data files 1083 will be stored. 1084 1085 Returns: 1086 None 1087 """ 1088 if not os.path.exists(dir_path): 1089 try: 1090 os.makedirs(dir_path, 0o755) 1091 except (IOError, OSError) as e: 1092 # PermissionError 1093 if e.errno == EPERM or e.errno == EACCES: 1094 print( 1095 "\tERROR: os.makedirs %s failed with" 1096 " PermissionError.\n" % dir_path 1097 ) 1098 else: 1099 print( 1100 "\tERROR: os.makedirs %s failed with %s.\n" 1101 % (dir_path, e.strerror) 1102 ) 1103 sys.exit(-1) 1104 1105 def print_progress(self, progress): 1106 r""" 1107 Print the current activity progress. 1108 1109 This method prints the current activity progress using the provided 1110 progress counter. The method does not return any value. 1111 1112 Parameters: 1113 progress (int): The current activity progress counter. 1114 1115 Returns: 1116 None 1117 """ 1118 sys.stdout.write("\r\t" + "+" * progress) 1119 sys.stdout.flush() 1120 time.sleep(0.1) 1121 1122 def verify_redfish(self): 1123 r""" 1124 Verify if the remote host has the Redfish service active. 1125 1126 This method checks if the remote host has the Redfish service active 1127 by sending a GET request to the Redfish base URL /redfish/v1/. 1128 If the request is successful (status code 200), the method returns 1129 stdout output of the run else error message. 1130 1131 Returns: 1132 str: Redfish service executed output. 1133 """ 1134 redfish_parm = ( 1135 "redfishtool -r " 1136 + self.hostname 1137 + ":" 1138 + self.port_https 1139 + " -S Always raw GET /redfish/v1/" 1140 ) 1141 return self.run_tool_cmd(redfish_parm, True) 1142 1143 def verify_ipmi(self): 1144 r""" 1145 Verify if the remote host has the IPMI LAN service active. 1146 1147 This method checks if the remote host has the IPMI LAN service active 1148 by sending an IPMI "power status" command. 1149 1150 If the command is successful (returns a non-empty response), 1151 else error message. 1152 1153 Returns: 1154 str: IPMI LAN service executed output. 1155 """ 1156 if self.target_type == "OPENBMC": 1157 ipmi_parm = ( 1158 "ipmitool -I lanplus -C 17 -U " 1159 + self.username 1160 + " -P " 1161 + self.password 1162 + " -H " 1163 + self.hostname 1164 + " -p " 1165 + str(self.port_ipmi) 1166 + " power status" 1167 ) 1168 else: 1169 ipmi_parm = ( 1170 "ipmitool -I lanplus -P " 1171 + self.password 1172 + " -H " 1173 + self.hostname 1174 + " -p " 1175 + str(self.port_ipmi) 1176 + " power status" 1177 ) 1178 1179 return self.run_tool_cmd(ipmi_parm, True) 1180 1181 def run_tool_cmd(self, parms_string, quiet=False): 1182 r""" 1183 Run a CLI standard tool or script with the provided command options. 1184 1185 This method runs a CLI standard tool or script with the provided 1186 parms_string command options. If the quiet parameter is set to True, 1187 the method suppresses the output of the command. 1188 The method returns the output of the command as a string. 1189 1190 Parameters: 1191 parms_string (str): The command options for the CLI tool or 1192 script. 1193 quiet (bool, optional): If True, suppresses the output of the 1194 command. Defaults to False. 1195 1196 Returns: 1197 str: The output of the command as a string. 1198 """ 1199 1200 result = subprocess.run( 1201 [parms_string], 1202 stdout=subprocess.PIPE, 1203 stderr=subprocess.PIPE, 1204 shell=True, 1205 universal_newlines=True, 1206 ) 1207 1208 if result.stderr and not quiet: 1209 if self.password in parms_string: 1210 parms_string = parms_string.replace(self.password, "********") 1211 self.logger.error("\n\t\tERROR with %s " % parms_string) 1212 self.logger.error("\t\t" + result.stderr) 1213 1214 return result.stdout 1215 1216 def verify_protocol(self, protocol_list): 1217 r""" 1218 Perform a working check for the provided list of protocols. 1219 1220 This method checks if the specified protocols are available on the 1221 remote host. The method iterates through the protocol_list and 1222 attempts to establish a connection using each protocol. 1223 1224 If a connection is successfully established, the method append to the 1225 list and if any protocol fails to connect, the method ignores it. 1226 1227 Parameters: 1228 protocol_list (list): A list of protocols to check. 1229 1230 Returns: 1231 list: All protocols are available list. 1232 """ 1233 1234 tmp_list = [] 1235 if self.target_is_pingable(): 1236 tmp_list.append("SHELL") 1237 1238 for protocol in protocol_list: 1239 if self.remote_protocol != "ALL": 1240 if self.remote_protocol != protocol: 1241 continue 1242 1243 # Only check SSH/SCP once for both protocols 1244 if ( 1245 protocol == "SSH" 1246 or protocol == "SCP" 1247 and protocol not in tmp_list 1248 ): 1249 if self.ssh_to_target_system(): 1250 # Add only what user asked. 1251 if self.remote_protocol != "ALL": 1252 tmp_list.append(self.remote_protocol) 1253 else: 1254 tmp_list.append("SSH") 1255 tmp_list.append("SCP") 1256 1257 if protocol == "TELNET": 1258 if self.telnet_to_target_system(): 1259 tmp_list.append(protocol) 1260 1261 if protocol == "REDFISH": 1262 if self.verify_redfish(): 1263 tmp_list.append(protocol) 1264 self.logger.info( 1265 "\n\t[Check] %s Redfish Service.\t\t [OK]" 1266 % self.hostname 1267 ) 1268 else: 1269 self.logger.info( 1270 "\n\t[Check] %s Redfish Service.\t\t [NOT AVAILABLE]" 1271 % self.hostname 1272 ) 1273 1274 if protocol == "IPMI": 1275 if self.verify_ipmi(): 1276 tmp_list.append(protocol) 1277 self.logger.info( 1278 "\n\t[Check] %s IPMI LAN Service.\t\t [OK]" 1279 % self.hostname 1280 ) 1281 else: 1282 self.logger.info( 1283 "\n\t[Check] %s IPMI LAN Service.\t\t [NOT AVAILABLE]" 1284 % self.hostname 1285 ) 1286 1287 return tmp_list 1288 1289 def load_env(self): 1290 r""" 1291 Load the user environment variables from a YAML file. 1292 1293 This method reads the environment variables from a YAML file specified 1294 in the ENV_FILE environment variable. If the file is not found or 1295 there is an error reading the file, an exception is raised. 1296 1297 The YAML file should have the following format: 1298 1299 .. code-block:: yaml 1300 1301 VAR_NAME: VAR_VALUE 1302 1303 Where VAR_NAME is the name of the environment variable, and 1304 VAR_VALUE is its value. 1305 1306 After loading the environment variables, they are stored in the 1307 self.env attribute for later use. 1308 """ 1309 1310 os.environ["hostname"] = self.hostname 1311 os.environ["username"] = self.username 1312 os.environ["password"] = self.password 1313 os.environ["port_ssh"] = self.port_ssh 1314 os.environ["port_https"] = self.port_https 1315 os.environ["port_ipmi"] = self.port_ipmi 1316 1317 # Append default Env. 1318 self.env_dict["hostname"] = self.hostname 1319 self.env_dict["username"] = self.username 1320 self.env_dict["password"] = self.password 1321 self.env_dict["port_ssh"] = self.port_ssh 1322 self.env_dict["port_https"] = self.port_https 1323 self.env_dict["port_ipmi"] = self.port_ipmi 1324 1325 try: 1326 tmp_env_dict = {} 1327 if self.env_vars: 1328 tmp_env_dict = json.loads(self.env_vars) 1329 # Export ENV vars default. 1330 for key, value in tmp_env_dict.items(): 1331 os.environ[key] = value 1332 self.env_dict[key] = str(value) 1333 1334 # Load user specified ENV config YAML. 1335 if self.econfig: 1336 with open(self.econfig, "r") as file: 1337 try: 1338 tmp_env_dict = yaml.load(file, Loader=yaml.SafeLoader) 1339 except yaml.YAMLError as e: 1340 self.logger.error(e) 1341 sys.exit(-1) 1342 # Export ENV vars. 1343 for key, value in tmp_env_dict["env_params"].items(): 1344 os.environ[key] = str(value) 1345 self.env_dict[key] = str(value) 1346 except json.decoder.JSONDecodeError as e: 1347 self.logger.error("\n\tERROR: %s " % e) 1348 sys.exit(-1) 1349 except FileNotFoundError as e: 1350 self.logger.error("\n\tERROR: %s " % e) 1351 sys.exit(-1) 1352 1353 # This to mask the password from displaying on the console. 1354 mask_dict = self.env_dict.copy() 1355 for k, v in mask_dict.items(): 1356 if k.lower().find("password") != -1: 1357 hidden_text = [] 1358 hidden_text.append(v) 1359 password_regex = ( 1360 "(" + "|".join([re.escape(x) for x in hidden_text]) + ")" 1361 ) 1362 mask_dict[k] = re.sub(password_regex, "********", v) 1363 1364 self.logger.info(json.dumps(mask_dict, indent=8, sort_keys=False)) 1365 1366 def execute_python_eval(self, eval_string): 1367 r""" 1368 Execute a qualified Python function string using the eval() function. 1369 1370 This method executes a provided Python function string using the 1371 eval() function. 1372 1373 The method takes the eval_string as an argument, which is expected to 1374 be a valid Python function call. 1375 1376 The method returns the result of the executed function. 1377 1378 Example: 1379 eval(plugin.foo_func.foo_func(10)) 1380 1381 Parameters: 1382 eval_string (str): A valid Python function call string. 1383 1384 Returns: 1385 str: The result of the executed function and on failure return 1386 PLUGIN_EVAL_ERROR. 1387 """ 1388 try: 1389 self.logger.info("\tExecuting plugin func()") 1390 self.logger.debug("\tCall func: %s" % eval_string) 1391 result = eval(eval_string) 1392 self.logger.info("\treturn: %s" % str(result)) 1393 except ( 1394 ValueError, 1395 SyntaxError, 1396 NameError, 1397 AttributeError, 1398 TypeError, 1399 ) as e: 1400 self.logger.error("\tERROR: execute_python_eval: %s" % e) 1401 # Set the plugin error state. 1402 plugin_error_dict["exit_on_error"] = True 1403 self.logger.info("\treturn: PLUGIN_EVAL_ERROR") 1404 return "PLUGIN_EVAL_ERROR" 1405 1406 return result 1407 1408 def execute_plugin_block(self, plugin_cmd_list): 1409 r""" 1410 Pack the plugin commands into qualified Python string objects. 1411 1412 This method processes the plugin_cmd_list argument, which is expected 1413 to contain a list of plugin commands read from a YAML file. The method 1414 iterates through the list, constructs a qualified Python string object 1415 for each plugin command, and returns a list of these string objects. 1416 1417 Parameters: 1418 plugin_cmd_list (list): A list of plugin commands containing 1419 plugin names and arguments. 1420 Plugin block read from YAML 1421 [ 1422 {'plugin_name':'plugin.foo_func.my_func'}, 1423 {'plugin_args':[10]}, 1424 ] 1425 1426 Example: 1427 Execute and no return response 1428 - plugin: 1429 - plugin_name: plugin.foo_func.my_func 1430 - plugin_args: 1431 - arg1 1432 - arg2 1433 1434 Execute and return a response 1435 - plugin: 1436 - plugin_name: result = plugin.foo_func.my_func 1437 - plugin_args: 1438 - arg1 1439 - arg2 1440 1441 Execute and return multiple values response 1442 - plugin: 1443 - plugin_name: result1,result2 = plugin.foo_func.my_func 1444 - plugin_args: 1445 - arg1 1446 - arg2 1447 1448 Returns: 1449 str: Execute and not response or a string value(s) responses, 1450 1451 """ 1452 try: 1453 idx = self.key_index_list_dict("plugin_name", plugin_cmd_list) 1454 plugin_name = plugin_cmd_list[idx]["plugin_name"] 1455 # Equal separator means plugin function returns result. 1456 if " = " in plugin_name: 1457 # Ex. ['result', 'plugin.foo_func.my_func'] 1458 plugin_name_args = plugin_name.split(" = ") 1459 # plugin func return data. 1460 for arg in plugin_name_args: 1461 if arg == plugin_name_args[-1]: 1462 plugin_name = arg 1463 else: 1464 plugin_resp = arg.split(",") 1465 # ['result1','result2'] 1466 for x in plugin_resp: 1467 global_plugin_list.append(x) 1468 global_plugin_dict[x] = "" 1469 1470 # Walk the plugin args ['arg1,'arg2'] 1471 # If the YAML plugin statement 'plugin_args' is not declared. 1472 plugin_args = [] 1473 if any("plugin_args" in d for d in plugin_cmd_list): 1474 idx = self.key_index_list_dict("plugin_args", plugin_cmd_list) 1475 if idx is not None: 1476 plugin_args = plugin_cmd_list[idx].get("plugin_args", []) 1477 plugin_args = self.yaml_args_populate(plugin_args) 1478 else: 1479 plugin_args = self.yaml_args_populate([]) 1480 1481 # Pack the args list to string parameters for plugin function. 1482 parm_args_str = self.yaml_args_string(plugin_args) 1483 1484 """ 1485 Example of plugin_func: 1486 plugin.redfish.enumerate_request( 1487 "xx.xx.xx.xx:443", 1488 "root", 1489 "********", 1490 "/redfish/v1/", 1491 "json") 1492 """ 1493 if parm_args_str: 1494 plugin_func = f"{plugin_name}({parm_args_str})" 1495 else: 1496 plugin_func = f"{plugin_name}()" 1497 1498 # Execute plugin function. 1499 if global_plugin_dict: 1500 resp = self.execute_python_eval(plugin_func) 1501 # Update plugin vars dict if there is any. 1502 if resp != "PLUGIN_EVAL_ERROR": 1503 self.response_args_data(resp) 1504 else: 1505 resp = self.execute_python_eval(plugin_func) 1506 except Exception as e: 1507 # Set the plugin error state. 1508 plugin_error_dict["exit_on_error"] = True 1509 self.logger.error("\tERROR: execute_plugin_block: %s" % e) 1510 pass 1511 1512 # There is a real error executing the plugin function. 1513 if resp == "PLUGIN_EVAL_ERROR": 1514 return resp 1515 1516 # Check if plugin_expects_return (int, string, list,dict etc) 1517 if any("plugin_expects_return" in d for d in plugin_cmd_list): 1518 idx = self.key_index_list_dict( 1519 "plugin_expects_return", plugin_cmd_list 1520 ) 1521 plugin_expects = plugin_cmd_list[idx]["plugin_expects_return"] 1522 if plugin_expects: 1523 if resp: 1524 if ( 1525 self.plugin_expect_type(plugin_expects, resp) 1526 == "INVALID" 1527 ): 1528 self.logger.error("\tWARN: Plugin error check skipped") 1529 elif not self.plugin_expect_type(plugin_expects, resp): 1530 self.logger.error( 1531 "\tERROR: Plugin expects return data: %s" 1532 % plugin_expects 1533 ) 1534 plugin_error_dict["exit_on_error"] = True 1535 elif not resp: 1536 self.logger.error( 1537 "\tERROR: Plugin func failed to return data" 1538 ) 1539 plugin_error_dict["exit_on_error"] = True 1540 1541 return resp 1542 1543 def response_args_data(self, plugin_resp): 1544 r""" 1545 Parse the plugin function response and update plugin return variable. 1546 1547 plugin_resp Response data from plugin function. 1548 """ 1549 resp_list = [] 1550 resp_data = "" 1551 1552 # There is nothing to update the plugin response. 1553 if len(global_plugin_list) == 0 or plugin_resp == "None": 1554 return 1555 1556 if isinstance(plugin_resp, str): 1557 resp_data = plugin_resp.strip("\r\n\t") 1558 resp_list.append(resp_data) 1559 elif isinstance(plugin_resp, bytes): 1560 resp_data = str(plugin_resp, "UTF-8").strip("\r\n\t") 1561 resp_list.append(resp_data) 1562 elif isinstance(plugin_resp, tuple): 1563 if len(global_plugin_list) == 1: 1564 resp_list.append(plugin_resp) 1565 else: 1566 resp_list = list(plugin_resp) 1567 resp_list = [x.strip("\r\n\t") for x in resp_list] 1568 elif isinstance(plugin_resp, list): 1569 if len(global_plugin_list) == 1: 1570 resp_list.append([x.strip("\r\n\t") for x in plugin_resp]) 1571 else: 1572 resp_list = [x.strip("\r\n\t") for x in plugin_resp] 1573 elif isinstance(plugin_resp, int) or isinstance(plugin_resp, float): 1574 resp_list.append(plugin_resp) 1575 1576 # Iterate if there is a list of plugin return vars to update. 1577 for idx, item in enumerate(resp_list, start=0): 1578 # Exit loop, done required loop. 1579 if idx >= len(global_plugin_list): 1580 break 1581 # Find the index of the return func in the list and 1582 # update the global func return dictionary. 1583 try: 1584 dict_idx = global_plugin_list[idx] 1585 global_plugin_dict[dict_idx] = item 1586 except (IndexError, ValueError) as e: 1587 self.logger.warn("\tWARN: response_args_data: %s" % e) 1588 pass 1589 1590 # Done updating plugin dict irrespective of pass or failed, 1591 # clear all the list element for next plugin block execute. 1592 global_plugin_list.clear() 1593 1594 def yaml_args_string(self, plugin_args): 1595 r""" 1596 Pack the arguments into a string representation. 1597 1598 This method processes the plugin_arg argument, which is expected to 1599 contain a list of arguments. The method iterates through the list, 1600 converts each argument to a string, and concatenates them into a 1601 single string. Special handling is applied for integer, float, and 1602 predefined plugin variable types. 1603 1604 Ecample: 1605 From 1606 ['xx.xx.xx.xx:443', 'root', '********', '/redfish/v1/', 'json'] 1607 to 1608 "xx.xx.xx.xx:443","root","********","/redfish/v1/","json" 1609 1610 Parameters: 1611 plugin_args (list): A list of arguments to be packed into 1612 a string. 1613 1614 Returns: 1615 str: A string representation of the arguments. 1616 """ 1617 args_str = "" 1618 1619 for i, arg in enumerate(plugin_args): 1620 if arg: 1621 if isinstance(arg, (int, float)): 1622 args_str += str(arg) 1623 elif arg in global_plugin_type_list: 1624 args_str += str(global_plugin_dict[arg]) 1625 else: 1626 args_str += f'"{arg.strip("\r\n\t")}"' 1627 1628 # Skip last list element. 1629 if i != len(plugin_args) - 1: 1630 args_str += "," 1631 1632 return args_str 1633 1634 def yaml_args_populate(self, yaml_arg_list): 1635 r""" 1636 Decode environment and plugin variables and populate the argument list. 1637 1638 This method processes the yaml_arg_list argument, which is expected to 1639 contain a list of arguments read from a YAML file. The method iterates 1640 through the list, decodes environment and plugin variables, and 1641 returns a populated list of arguments. 1642 1643 .. code-block:: yaml 1644 1645 - plugin_args: 1646 - arg1 1647 - arg2 1648 1649 ['${hostname}:${port_https}', '${username}', '/redfish/v1/', 'json'] 1650 1651 Returns the populated plugin list 1652 ['xx.xx.xx.xx:443', 'root', '/redfish/v1/', 'json'] 1653 1654 Parameters: 1655 yaml_arg_list (list): A list of arguments containing environment 1656 and plugin variables. 1657 1658 Returns: 1659 list: A populated list of arguments with decoded environment and 1660 plugin variables. 1661 """ 1662 if isinstance(yaml_arg_list, list): 1663 populated_list = [] 1664 for arg in yaml_arg_list: 1665 if isinstance(arg, (int, float)): 1666 populated_list.append(arg) 1667 elif isinstance(arg, str): 1668 arg_str = self.yaml_env_and_plugin_vars_populate(str(arg)) 1669 populated_list.append(arg_str) 1670 else: 1671 populated_list.append(arg) 1672 1673 return populated_list 1674 1675 def yaml_env_and_plugin_vars_populate(self, yaml_arg_str): 1676 r""" 1677 Update environment variables and plugin variables based on the 1678 provided YAML argument string. 1679 1680 This method processes the yaml_arg_str argument, which is expected 1681 to contain a string representing environment variables and plugin 1682 variables in the format: 1683 1684 .. code-block:: yaml 1685 1686 - cat ${MY_VAR} 1687 - ls -AX my_plugin_var 1688 1689 The method parses the string, extracts the variable names, and updates 1690 the corresponding environment variables and plugin variables. 1691 1692 Parameters: 1693 yaml_arg_str (str): A string containing environment and plugin 1694 variable definitions in YAML format. 1695 1696 Returns: 1697 str: The updated YAML argument string with plugin variables 1698 replaced. 1699 """ 1700 1701 # Parse and convert the Plugin YAML vars string to python vars 1702 # Example: 1703 # ${my_hostname}:${port_https} -> ['my_hostname', 'port_https'] 1704 try: 1705 # Example, list of matching 1706 # env vars ['username', 'password', 'hostname'] 1707 # Extra escape \ for special symbols. '\$\{([^\}]+)\}' works good. 1708 env_var_regex = r"\$\{([^\}]+)\}" 1709 env_var_names_list = re.findall(env_var_regex, yaml_arg_str) 1710 1711 for var in env_var_names_list: 1712 env_var = os.environ.get(var) 1713 if env_var: 1714 env_replace = "${" + var + "}" 1715 yaml_arg_str = yaml_arg_str.replace(env_replace, env_var) 1716 except Exception as e: 1717 self.logger.error("\tERROR:yaml_env_vars_populate: %s" % e) 1718 pass 1719 1720 """ 1721 Parse the string for plugin vars. 1722 Implement the logic to update environment variables based on the 1723 extracted variable names. 1724 """ 1725 try: 1726 # Example, list of plugin vars env_var_names_list 1727 # ['my_hostname', 'port_https'] 1728 global_plugin_dict_keys = set(global_plugin_dict.keys()) 1729 # Skip env var list already populated above code block list. 1730 plugin_var_name_list = [ 1731 var 1732 for var in global_plugin_dict_keys 1733 if var not in env_var_names_list 1734 ] 1735 1736 for var in plugin_var_name_list: 1737 plugin_var_value = global_plugin_dict[var] 1738 if yaml_arg_str in global_plugin_dict: 1739 """ 1740 If this plugin var exist but empty in dict, don't replace. 1741 his is either a YAML plugin statement incorrectly used or 1742 user added a plugin var which is not going to be populated. 1743 """ 1744 if isinstance(plugin_var_value, (list, dict)): 1745 """ 1746 List data type or dict can't be replaced, use 1747 directly in eval function call. 1748 """ 1749 global_plugin_type_list.append(var) 1750 else: 1751 yaml_arg_str = yaml_arg_str.replace( 1752 str(var), str(plugin_var_value) 1753 ) 1754 except (IndexError, ValueError) as e: 1755 self.logger.error("\tERROR: yaml_plugin_vars_populate: %s" % e) 1756 pass 1757 1758 # From ${my_hostname}:${port_https} -> ['my_hostname', 'port_https'] 1759 # to populated values string as 1760 # Example: xx.xx.xx.xx:443 and return the string 1761 return yaml_arg_str 1762 1763 def plugin_error_check(self, plugin_dict): 1764 r""" 1765 Process plugin error dictionary and return the corresponding error 1766 message. 1767 1768 This method checks if any dictionary in the plugin_dict list contains 1769 a "plugin_error" key. If such a dictionary is found, it retrieves the 1770 value associated with the "plugin_error" key and returns the 1771 corresponding error message from the plugin_error_dict attribute. 1772 1773 Parameters: 1774 plugin_dict (list of dict): A list of dictionaries containing 1775 plugin error information. 1776 1777 Returns: 1778 str: The error message corresponding to the "plugin_error" value, 1779 or None if no error is found. 1780 """ 1781 if any("plugin_error" in d for d in plugin_dict): 1782 for d in plugin_dict: 1783 if "plugin_error" in d: 1784 value = d["plugin_error"] 1785 return self.plugin_error_dict.get(value, None) 1786 return None 1787 1788 def key_index_list_dict(self, key, list_dict): 1789 r""" 1790 Find the index of the first dictionary in the list that contains 1791 the specified key. 1792 1793 Parameters: 1794 key (str): The key to search for in the 1795 dictionaries. 1796 list_dict (list of dict): A list of dictionaries to search 1797 through. 1798 1799 Returns: 1800 int: The index of the first dictionary containing the key, or -1 1801 if no match is found. 1802 """ 1803 for i, d in enumerate(list_dict): 1804 if key in d: 1805 return i 1806 return -1 1807 1808 def plugin_expect_type(self, type, data): 1809 r""" 1810 Check if the provided data matches the expected type. 1811 1812 This method checks if the data argument matches the specified type. 1813 It supports the following types: "int", "float", "str", "list", "dict", 1814 and "tuple". 1815 1816 If the type is not recognized, it logs an info message and returns 1817 "INVALID". 1818 1819 Parameters: 1820 type (str): The expected data type. 1821 data: The data to check against the expected type. 1822 1823 Returns: 1824 bool or str: True if the data matches the expected type, False if 1825 not, or "INVALID" if the type is not recognized. 1826 """ 1827 if type == "int": 1828 return isinstance(data, int) 1829 elif type == "float": 1830 return isinstance(data, float) 1831 elif type == "str": 1832 return isinstance(data, str) 1833 elif type == "list": 1834 return isinstance(data, list) 1835 elif type == "dict": 1836 return isinstance(data, dict) 1837 elif type == "tuple": 1838 return isinstance(data, tuple) 1839 else: 1840 self.logger.info("\tInvalid data type requested: %s" % type) 1841 return "INVALID" 1842