xref: /openbmc/openbmc-test-automation/ffdc/ffdc_collector.py (revision 714ed72cf1f5fd79826ef18bff3dc57ea7766ccc)
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