1#!/usr/bin/env python
2
3r"""
4See class prolog below for details.
5"""
6
7import os
8import re
9import sys
10import yaml
11import json
12import time
13import logging
14import platform
15from errno import EACCES, EPERM
16import subprocess
17from ssh_utility import SSHRemoteclient
18from telnet_utility import TelnetRemoteclient
19
20r"""
21User define plugins python functions.
22
23It will imports files from directory plugins
24
25plugins
26├── file1.py
27└── file2.py
28
29Example how to define in YAML:
30 - plugin:
31   - plugin_name: plugin.foo_func.foo_func_yaml
32     - plugin_args:
33       - arg1
34       - arg2
35"""
36plugin_dir = 'plugins'
37try:
38    for module in os.listdir(plugin_dir):
39        if module == '__init__.py' or module[-3:] != '.py':
40            continue
41        plugin_module = "plugins." + module[:-3]
42        # To access the module plugin.<module name>.<function>
43        # Example: plugin.foo_func.foo_func_yaml()
44        try:
45            plugin = __import__(plugin_module, globals(), locals(), [], 0)
46        except Exception as e:
47            print("PLUGIN: Module import failed: %s" % module)
48            pass
49except FileNotFoundError as e:
50    print("PLUGIN: %s" % e)
51    pass
52
53r"""
54This is for plugin functions returning data or responses to the caller
55in YAML plugin setup.
56
57Example:
58
59    - plugin:
60      - plugin_name: version = plugin.ssh_execution.ssh_execute_cmd
61      - plugin_args:
62        - ${hostname}
63        - ${username}
64        - ${password}
65        - "cat /etc/os-release | grep VERSION_ID | awk -F'=' '{print $2}'"
66     - plugin:
67        - plugin_name: plugin.print_vars.print_vars
68        - plugin_args:
69          - version
70
71where first plugin "version" var is used by another plugin in the YAML
72block or plugin
73
74"""
75global global_log_store_path
76global global_plugin_dict
77global global_plugin_list
78global_plugin_dict = {}
79global_plugin_list = []
80global_log_store_path = ''
81
82
83class FFDCCollector:
84
85    r"""
86    Sends commands from configuration file to the targeted system to collect log files.
87    Fetch and store generated files at the specified location.
88
89    """
90
91    def __init__(self,
92                 hostname,
93                 username,
94                 password,
95                 ffdc_config,
96                 location,
97                 remote_type,
98                 remote_protocol,
99                 env_vars,
100                 econfig,
101                 log_level):
102        r"""
103        Description of argument(s):
104
105        hostname            name/ip of the targeted (remote) system
106        username            user on the targeted system with access to FFDC files
107        password            password for user on targeted system
108        ffdc_config         configuration file listing commands and files for FFDC
109        location            where to store collected FFDC
110        remote_type         os type of the remote host
111        remote_protocol     Protocol to use to collect data
112        env_vars            User define CLI env vars '{"key : "value"}'
113        econfig             User define env vars YAML file
114
115        """
116
117        self.hostname = hostname
118        self.username = username
119        self.password = password
120        self.ffdc_config = ffdc_config
121        self.location = location + "/" + remote_type.upper()
122        self.ssh_remoteclient = None
123        self.telnet_remoteclient = None
124        self.ffdc_dir_path = ""
125        self.ffdc_prefix = ""
126        self.target_type = remote_type.upper()
127        self.remote_protocol = remote_protocol.upper()
128        self.env_vars = env_vars
129        self.econfig = econfig
130        self.start_time = 0
131        self.elapsed_time = ''
132        self.logger = None
133
134        # Set prefix values for scp files and directory.
135        # Since the time stamp is at second granularity, these values are set here
136        # to be sure that all files for this run will have same timestamps
137        # and they will be saved in the same directory.
138        # self.location == local system for now
139        self.set_ffdc_defaults()
140
141        # Logger for this run.  Need to be after set_ffdc_defaults()
142        self.script_logging(getattr(logging, log_level.upper()))
143
144        # Verify top level directory exists for storage
145        self.validate_local_store(self.location)
146
147        if self.verify_script_env():
148            # Load default or user define YAML configuration file.
149            with open(self.ffdc_config, 'r') as file:
150                self.ffdc_actions = yaml.load(file, Loader=yaml.FullLoader)
151
152            if self.target_type not in self.ffdc_actions.keys():
153                self.logger.error(
154                    "\n\tERROR: %s is not listed in %s.\n\n" % (self.target_type, self.ffdc_config))
155                sys.exit(-1)
156        else:
157            sys.exit(-1)
158
159        # Load ENV vars from user.
160        self.logger.info("\n\tENV: User define input YAML variables")
161        self.env_dict = {}
162        self. load_env()
163
164    def verify_script_env(self):
165
166        # Import to log version
167        import click
168        import paramiko
169
170        run_env_ok = True
171
172        redfishtool_version = self.run_tool_cmd('redfishtool -V').split(' ')[2].strip('\n')
173        ipmitool_version = self.run_tool_cmd('ipmitool -V').split(' ')[2]
174
175        self.logger.info("\n\t---- Script host environment ----")
176        self.logger.info("\t{:<10}  {:<10}".format('Script hostname', os.uname()[1]))
177        self.logger.info("\t{:<10}  {:<10}".format('Script host os', platform.platform()))
178        self.logger.info("\t{:<10}  {:>10}".format('Python', platform.python_version()))
179        self.logger.info("\t{:<10}  {:>10}".format('PyYAML', yaml.__version__))
180        self.logger.info("\t{:<10}  {:>10}".format('click', click.__version__))
181        self.logger.info("\t{:<10}  {:>10}".format('paramiko', paramiko.__version__))
182        self.logger.info("\t{:<10}  {:>9}".format('redfishtool', redfishtool_version))
183        self.logger.info("\t{:<10}  {:>12}".format('ipmitool', ipmitool_version))
184
185        if eval(yaml.__version__.replace('.', ',')) < (5, 4, 1):
186            self.logger.error("\n\tERROR: Python or python packages do not meet minimum version requirement.")
187            self.logger.error("\tERROR: PyYAML version 5.4.1 or higher is needed.\n")
188            run_env_ok = False
189
190        self.logger.info("\t---- End script host environment ----")
191        return run_env_ok
192
193    def script_logging(self,
194                       log_level_attr):
195        r"""
196        Create logger
197
198        """
199        self.logger = logging.getLogger()
200        self.logger.setLevel(log_level_attr)
201        log_file_handler = logging.FileHandler(self.ffdc_dir_path + "collector.log")
202
203        stdout_handler = logging.StreamHandler(sys.stdout)
204        self.logger.addHandler(log_file_handler)
205        self.logger.addHandler(stdout_handler)
206
207        # Turn off paramiko INFO logging
208        logging.getLogger("paramiko").setLevel(logging.WARNING)
209
210    def target_is_pingable(self):
211        r"""
212        Check if target system is ping-able.
213
214        """
215        response = os.system("ping -c 1 %s  2>&1 >/dev/null" % self.hostname)
216        if response == 0:
217            self.logger.info("\n\t[Check] %s is ping-able.\t\t [OK]" % self.hostname)
218            return True
219        else:
220            self.logger.error(
221                "\n\tERROR: %s is not ping-able. FFDC collection aborted.\n" % self.hostname)
222            sys.exit(-1)
223
224    def collect_ffdc(self):
225        r"""
226        Initiate FFDC Collection depending on requested protocol.
227
228        """
229
230        self.logger.info("\n\t---- Start communicating with %s ----" % self.hostname)
231        self.start_time = time.time()
232
233        # Find the list of target and protocol supported.
234        check_protocol_list = []
235        config_dict = self.ffdc_actions
236
237        for target_type in config_dict.keys():
238            if self.target_type != target_type:
239                continue
240
241            for k, v in config_dict[target_type].items():
242                if config_dict[target_type][k]['PROTOCOL'][0] not in check_protocol_list:
243                    check_protocol_list.append(config_dict[target_type][k]['PROTOCOL'][0])
244
245        self.logger.info("\n\t %s protocol type: %s" % (self.target_type, check_protocol_list))
246
247        verified_working_protocol = self.verify_protocol(check_protocol_list)
248
249        if verified_working_protocol:
250            self.logger.info("\n\t---- Completed protocol pre-requisite check ----\n")
251
252        # Verify top level directory exists for storage
253        self.validate_local_store(self.location)
254
255        if ((self.remote_protocol not in verified_working_protocol) and (self.remote_protocol != 'ALL')):
256            self.logger.info("\n\tWorking protocol list: %s" % verified_working_protocol)
257            self.logger.error(
258                '\tERROR: Requested protocol %s is not in working protocol list.\n'
259                % self.remote_protocol)
260            sys.exit(-1)
261        else:
262            self.generate_ffdc(verified_working_protocol)
263
264    def ssh_to_target_system(self):
265        r"""
266        Open a ssh connection to targeted system.
267
268        """
269
270        self.ssh_remoteclient = SSHRemoteclient(self.hostname,
271                                                self.username,
272                                                self.password)
273
274        if self.ssh_remoteclient.ssh_remoteclient_login():
275            self.logger.info("\n\t[Check] %s SSH connection established.\t [OK]" % self.hostname)
276
277            # Check scp connection.
278            # If scp connection fails,
279            # continue with FFDC generation but skip scp files to local host.
280            self.ssh_remoteclient.scp_connection()
281            return True
282        else:
283            self.logger.info("\n\t[Check] %s SSH connection.\t [NOT AVAILABLE]" % self.hostname)
284            return False
285
286    def telnet_to_target_system(self):
287        r"""
288        Open a telnet connection to targeted system.
289        """
290        self.telnet_remoteclient = TelnetRemoteclient(self.hostname,
291                                                      self.username,
292                                                      self.password)
293        if self.telnet_remoteclient.tn_remoteclient_login():
294            self.logger.info("\n\t[Check] %s Telnet connection established.\t [OK]" % self.hostname)
295            return True
296        else:
297            self.logger.info("\n\t[Check] %s Telnet connection.\t [NOT AVAILABLE]" % self.hostname)
298            return False
299
300    def generate_ffdc(self, working_protocol_list):
301        r"""
302        Determine actions based on remote host type
303
304        Description of argument(s):
305        working_protocol_list    list of confirmed working protocols to connect to remote host.
306        """
307
308        self.logger.info("\n\t---- Executing commands on " + self.hostname + " ----")
309        self.logger.info("\n\tWorking protocol list: %s" % working_protocol_list)
310
311        config_dict = self.ffdc_actions
312        for target_type in config_dict.keys():
313            if self.target_type != target_type:
314                continue
315
316            self.logger.info("\n\tFFDC Path: %s " % self.ffdc_dir_path)
317            global_plugin_dict['global_log_store_path'] = self.ffdc_dir_path
318            self.logger.info("\tSystem Type: %s" % target_type)
319            for k, v in config_dict[target_type].items():
320
321                if self.remote_protocol not in working_protocol_list \
322                        and self.remote_protocol != 'ALL':
323                    continue
324
325                protocol = config_dict[target_type][k]['PROTOCOL'][0]
326
327                if protocol not in working_protocol_list:
328                    continue
329
330                if protocol in working_protocol_list:
331                    if protocol == 'SSH' or protocol == 'SCP':
332                        self.protocol_ssh(protocol, target_type, k)
333                    elif protocol == 'TELNET':
334                        self.protocol_telnet(target_type, k)
335                    elif protocol == 'REDFISH' or protocol == 'IPMI' or protocol == 'SHELL':
336                        self.protocol_execute(protocol, target_type, k)
337                else:
338                    self.logger.error("\n\tERROR: %s is not available for %s." % (protocol, self.hostname))
339
340        # Close network connection after collecting all files
341        self.elapsed_time = time.strftime("%H:%M:%S", time.gmtime(time.time() - self.start_time))
342        if self.ssh_remoteclient:
343            self.ssh_remoteclient.ssh_remoteclient_disconnect()
344        if self.telnet_remoteclient:
345            self.telnet_remoteclient.tn_remoteclient_disconnect()
346
347    def protocol_ssh(self,
348                     protocol,
349                     target_type,
350                     sub_type):
351        r"""
352        Perform actions using SSH and SCP protocols.
353
354        Description of argument(s):
355        protocol            Protocol to execute.
356        target_type         OS Type of remote host.
357        sub_type            Group type of commands.
358        """
359
360        if protocol == 'SCP':
361            self.group_copy(self.ffdc_actions[target_type][sub_type])
362        else:
363            self.collect_and_copy_ffdc(self.ffdc_actions[target_type][sub_type])
364
365    def protocol_telnet(self,
366                        target_type,
367                        sub_type):
368        r"""
369        Perform actions using telnet protocol.
370        Description of argument(s):
371        target_type          OS Type of remote host.
372        """
373        self.logger.info("\n\t[Run] Executing commands on %s using %s" % (self.hostname, 'TELNET'))
374        telnet_files_saved = []
375        progress_counter = 0
376        list_of_commands = self.ffdc_actions[target_type][sub_type]['COMMANDS']
377        for index, each_cmd in enumerate(list_of_commands, start=0):
378            command_txt, command_timeout = self.unpack_command(each_cmd)
379            result = self.telnet_remoteclient.execute_command(command_txt, command_timeout)
380            if result:
381                try:
382                    targ_file = self.ffdc_actions[target_type][sub_type]['FILES'][index]
383                except IndexError:
384                    targ_file = command_txt
385                    self.logger.warning(
386                        "\n\t[WARN] Missing filename to store data from telnet %s." % each_cmd)
387                    self.logger.warning("\t[WARN] Data will be stored in %s." % targ_file)
388                targ_file_with_path = (self.ffdc_dir_path
389                                       + self.ffdc_prefix
390                                       + targ_file)
391                # Creates a new file
392                with open(targ_file_with_path, 'wb') as fp:
393                    fp.write(result)
394                    fp.close
395                    telnet_files_saved.append(targ_file)
396            progress_counter += 1
397            self.print_progress(progress_counter)
398        self.logger.info("\n\t[Run] Commands execution completed.\t\t [OK]")
399        for file in telnet_files_saved:
400            self.logger.info("\n\t\tSuccessfully save file " + file + ".")
401
402    def protocol_execute(self,
403                         protocol,
404                         target_type,
405                         sub_type):
406        r"""
407        Perform actions for a given protocol.
408
409        Description of argument(s):
410        protocol            Protocol to execute.
411        target_type         OS Type of remote host.
412        sub_type            Group type of commands.
413        """
414
415        self.logger.info("\n\t[Run] Executing commands to %s using %s" % (self.hostname, protocol))
416        executed_files_saved = []
417        progress_counter = 0
418        list_of_cmd = self.get_command_list(self.ffdc_actions[target_type][sub_type])
419        for index, each_cmd in enumerate(list_of_cmd, start=0):
420            if isinstance(each_cmd, dict):
421                if 'plugin' in each_cmd:
422                    # call the plugin
423                    self.logger.info("\n\t[PLUGIN-START]")
424                    self.execute_plugin_block(each_cmd['plugin'])
425                    self.logger.info("\t[PLUGIN-END]\n")
426                    continue
427            else:
428                self.yaml_env_and_plugin_vars_populate(each_cmd)
429
430            result = self.run_tool_cmd(each_cmd)
431            if result:
432                try:
433                    targ_file = self.get_file_list(self.ffdc_actions[target_type][sub_type])[index]
434                    targ_file = self.yaml_env_and_plugin_vars_populate(targ_file)
435                    # If file is specified as None.
436                    if not targ_file:
437                        continue
438                except IndexError:
439                    targ_file = each_cmd.split('/')[-1]
440                    self.logger.warning(
441                        "\n\t[WARN] Missing filename to store data from %s." % each_cmd)
442                    self.logger.warning("\t[WARN] Data will be stored in %s." % targ_file)
443
444                targ_file_with_path = (self.ffdc_dir_path
445                                       + self.ffdc_prefix
446                                       + targ_file)
447
448                # Creates a new file
449                with open(targ_file_with_path, 'w') as fp:
450                    fp.write(result)
451                    fp.close
452                    executed_files_saved.append(targ_file)
453
454            progress_counter += 1
455            self.print_progress(progress_counter)
456
457        self.logger.info("\n\t[Run] Commands execution completed.\t\t [OK]")
458
459        for file in executed_files_saved:
460            self.logger.info("\n\t\tSuccessfully save file " + file + ".")
461
462    def collect_and_copy_ffdc(self,
463                              ffdc_actions_for_target_type,
464                              form_filename=False):
465        r"""
466        Send commands in ffdc_config file to targeted system.
467
468        Description of argument(s):
469        ffdc_actions_for_target_type     commands and files for the selected remote host type.
470        form_filename                    if true, pre-pend self.target_type to filename
471        """
472
473        # Executing commands, if any
474        self.ssh_execute_ffdc_commands(ffdc_actions_for_target_type,
475                                       form_filename)
476
477        # Copying files
478        if self.ssh_remoteclient.scpclient:
479            self.logger.info("\n\n\tCopying FFDC files from remote system %s.\n" % self.hostname)
480
481            # Retrieving files from target system
482            list_of_files = self.get_file_list(ffdc_actions_for_target_type)
483            self.scp_ffdc(self.ffdc_dir_path, self.ffdc_prefix, form_filename, list_of_files)
484        else:
485            self.logger.info("\n\n\tSkip copying FFDC files from remote system %s.\n" % self.hostname)
486
487    def get_command_list(self,
488                         ffdc_actions_for_target_type):
489        r"""
490        Fetch list of commands from configuration file
491
492        Description of argument(s):
493        ffdc_actions_for_target_type    commands and files for the selected remote host type.
494        """
495        try:
496            list_of_commands = ffdc_actions_for_target_type['COMMANDS']
497        except KeyError:
498            list_of_commands = []
499        return list_of_commands
500
501    def get_file_list(self,
502                      ffdc_actions_for_target_type):
503        r"""
504        Fetch list of commands from configuration file
505
506        Description of argument(s):
507        ffdc_actions_for_target_type    commands and files for the selected remote host type.
508        """
509        try:
510            list_of_files = ffdc_actions_for_target_type['FILES']
511        except KeyError:
512            list_of_files = []
513        return list_of_files
514
515    def unpack_command(self,
516                       command):
517        r"""
518        Unpack command from config file
519
520        Description of argument(s):
521        command    Command from config file.
522        """
523        if isinstance(command, dict):
524            command_txt = next(iter(command))
525            command_timeout = next(iter(command.values()))
526        elif isinstance(command, str):
527            command_txt = command
528            # Default command timeout 60 seconds
529            command_timeout = 60
530
531        return command_txt, command_timeout
532
533    def ssh_execute_ffdc_commands(self,
534                                  ffdc_actions_for_target_type,
535                                  form_filename=False):
536        r"""
537        Send commands in ffdc_config file to targeted system.
538
539        Description of argument(s):
540        ffdc_actions_for_target_type    commands and files for the selected remote host type.
541        form_filename                    if true, pre-pend self.target_type to filename
542        """
543        self.logger.info("\n\t[Run] Executing commands on %s using %s"
544                         % (self.hostname, ffdc_actions_for_target_type['PROTOCOL'][0]))
545
546        list_of_commands = self.get_command_list(ffdc_actions_for_target_type)
547        # If command list is empty, returns
548        if not list_of_commands:
549            return
550
551        progress_counter = 0
552        for command in list_of_commands:
553            command_txt, command_timeout = self.unpack_command(command)
554
555            if form_filename:
556                command_txt = str(command_txt % self.target_type)
557
558            cmd_exit_code, err, response = \
559                self.ssh_remoteclient.execute_command(command_txt, command_timeout)
560
561            if cmd_exit_code:
562                self.logger.warning(
563                    "\n\t\t[WARN] %s exits with code %s." % (command_txt, str(cmd_exit_code)))
564                self.logger.warning("\t\t[WARN] %s " % err)
565
566            progress_counter += 1
567            self.print_progress(progress_counter)
568
569        self.logger.info("\n\t[Run] Commands execution completed.\t\t [OK]")
570
571    def group_copy(self,
572                   ffdc_actions_for_target_type):
573        r"""
574        scp group of files (wild card) from remote host.
575
576        Description of argument(s):
577        fdc_actions_for_target_type    commands and files for the selected remote host type.
578        """
579
580        if self.ssh_remoteclient.scpclient:
581            self.logger.info("\n\tCopying files from remote system %s via SCP.\n" % self.hostname)
582
583            list_of_commands = self.get_command_list(ffdc_actions_for_target_type)
584            # If command list is empty, returns
585            if not list_of_commands:
586                return
587
588            for command in list_of_commands:
589                try:
590                    command = self.yaml_env_and_plugin_vars_populate(command)
591                    filename = command.split('ls -AX')[1]
592                    filename = self.yaml_env_and_plugin_vars_populate(filename)
593                except IndexError:
594                    self.logger.error("\t\tInvalid command %s" % command)
595                    continue
596
597                cmd_exit_code, err, response = \
598                    self.ssh_remoteclient.execute_command(command)
599
600                # If file does not exist, code take no action.
601                # cmd_exit_code is ignored for this scenario.
602                if response:
603                    scp_result = self.ssh_remoteclient.scp_file_from_remote(filename, self.ffdc_dir_path)
604                    if scp_result:
605                        self.logger.info("\t\tSuccessfully copied from " + self.hostname + ':' + filename)
606                else:
607                    self.logger.info("\t\tThere is no " + filename)
608
609        else:
610            self.logger.info("\n\n\tSkip copying files from remote system %s.\n" % self.hostname)
611
612    def scp_ffdc(self,
613                 targ_dir_path,
614                 targ_file_prefix,
615                 form_filename,
616                 file_list=None,
617                 quiet=None):
618        r"""
619        SCP all files in file_dict to the indicated directory on the local system.
620
621        Description of argument(s):
622        targ_dir_path                   The path of the directory to receive the files.
623        targ_file_prefix                Prefix which will be pre-pended to each
624                                        target file's name.
625        file_dict                       A dictionary of files to scp from targeted system to this system
626
627        """
628
629        progress_counter = 0
630        for filename in file_list:
631            if form_filename:
632                filename = str(filename % self.target_type)
633            source_file_path = filename
634            targ_file_path = targ_dir_path + targ_file_prefix + filename.split('/')[-1]
635
636            # If source file name contains wild card, copy filename as is.
637            if '*' in source_file_path:
638                scp_result = self.ssh_remoteclient.scp_file_from_remote(source_file_path, self.ffdc_dir_path)
639            else:
640                scp_result = self.ssh_remoteclient.scp_file_from_remote(source_file_path, targ_file_path)
641
642            if not quiet:
643                if scp_result:
644                    self.logger.info(
645                        "\t\tSuccessfully copied from " + self.hostname + ':' + source_file_path + ".\n")
646                else:
647                    self.logger.info(
648                        "\t\tFail to copy from " + self.hostname + ':' + source_file_path + ".\n")
649            else:
650                progress_counter += 1
651                self.print_progress(progress_counter)
652
653    def set_ffdc_defaults(self):
654        r"""
655        Set a default value for self.ffdc_dir_path and self.ffdc_prefix.
656        Collected ffdc file will be stored in dir /self.location/hostname_timestr/.
657        Individual ffdc file will have timestr_filename.
658
659        Description of class variables:
660        self.ffdc_dir_path  The dir path where collected ffdc data files should be put.
661
662        self.ffdc_prefix    The prefix to be given to each ffdc file name.
663
664        """
665
666        timestr = time.strftime("%Y%m%d-%H%M%S")
667        self.ffdc_dir_path = self.location + "/" + self.hostname + "_" + timestr + "/"
668        self.ffdc_prefix = timestr + "_"
669        self.validate_local_store(self.ffdc_dir_path)
670
671    def validate_local_store(self, dir_path):
672        r"""
673        Ensure path exists to store FFDC files locally.
674
675        Description of variable:
676        dir_path  The dir path where collected ffdc data files will be stored.
677
678        """
679
680        if not os.path.exists(dir_path):
681            try:
682                os.makedirs(dir_path, 0o755)
683            except (IOError, OSError) as e:
684                # PermissionError
685                if e.errno == EPERM or e.errno == EACCES:
686                    self.logger.error(
687                        '\tERROR: os.makedirs %s failed with PermissionError.\n' % dir_path)
688                else:
689                    self.logger.error(
690                        '\tERROR: os.makedirs %s failed with %s.\n' % (dir_path, e.strerror))
691                sys.exit(-1)
692
693    def print_progress(self, progress):
694        r"""
695        Print activity progress +
696
697        Description of variable:
698        progress  Progress counter.
699
700        """
701
702        sys.stdout.write("\r\t" + "+" * progress)
703        sys.stdout.flush()
704        time.sleep(.1)
705
706    def verify_redfish(self):
707        r"""
708        Verify remote host has redfish service active
709
710        """
711        redfish_parm = 'redfishtool -r ' \
712                       + self.hostname + ' -S Always raw GET /redfish/v1/'
713        return(self.run_tool_cmd(redfish_parm, True))
714
715    def verify_ipmi(self):
716        r"""
717        Verify remote host has IPMI LAN service active
718
719        """
720        if self.target_type == 'OPENBMC':
721            ipmi_parm = 'ipmitool -I lanplus -C 17  -U ' + self.username + ' -P ' \
722                + self.password + ' -H ' + self.hostname + ' power status'
723        else:
724            ipmi_parm = 'ipmitool -I lanplus  -P ' \
725                + self.password + ' -H ' + self.hostname + ' power status'
726
727        return(self.run_tool_cmd(ipmi_parm, True))
728
729    def run_tool_cmd(self,
730                     parms_string,
731                     quiet=False):
732        r"""
733        Run CLI standard tool or scripts.
734
735        Description of variable:
736        parms_string         tool command options.
737        quiet                do not print tool error message if True
738        """
739
740        result = subprocess.run([parms_string],
741                                stdout=subprocess.PIPE,
742                                stderr=subprocess.PIPE,
743                                shell=True,
744                                universal_newlines=True)
745
746        if result.stderr and not quiet:
747            self.logger.error('\n\t\tERROR with %s ' % parms_string)
748            self.logger.error('\t\t' + result.stderr)
749
750        return result.stdout
751
752    def verify_protocol(self, protocol_list):
753        r"""
754        Perform protocol working check.
755
756        Description of argument(s):
757        protocol_list        List of protocol.
758        """
759
760        tmp_list = []
761        if self.target_is_pingable():
762            tmp_list.append("SHELL")
763
764        for protocol in protocol_list:
765            if self.remote_protocol != 'ALL':
766                if self.remote_protocol != protocol:
767                    continue
768
769            # Only check SSH/SCP once for both protocols
770            if protocol == 'SSH' or protocol == 'SCP' and protocol not in tmp_list:
771                if self.ssh_to_target_system():
772                    # Add only what user asked.
773                    if self.remote_protocol != 'ALL':
774                        tmp_list.append(self.remote_protocol)
775                    else:
776                        tmp_list.append('SSH')
777                        tmp_list.append('SCP')
778
779            if protocol == 'TELNET':
780                if self.telnet_to_target_system():
781                    tmp_list.append(protocol)
782
783            if protocol == 'REDFISH':
784                if self.verify_redfish():
785                    tmp_list.append(protocol)
786                    self.logger.info("\n\t[Check] %s Redfish Service.\t\t [OK]" % self.hostname)
787                else:
788                    self.logger.info("\n\t[Check] %s Redfish Service.\t\t [NOT AVAILABLE]" % self.hostname)
789
790            if protocol == 'IPMI':
791                if self.verify_ipmi():
792                    tmp_list.append(protocol)
793                    self.logger.info("\n\t[Check] %s IPMI LAN Service.\t\t [OK]" % self.hostname)
794                else:
795                    self.logger.info("\n\t[Check] %s IPMI LAN Service.\t\t [NOT AVAILABLE]" % self.hostname)
796
797        return tmp_list
798
799    def load_env(self):
800        r"""
801        Perform protocol working check.
802
803        """
804        # This is for the env vars a user can use in YAML to load it at runtime.
805        # Example YAML:
806        # -COMMANDS:
807        #    - my_command ${hostname}  ${username}   ${password}
808        os.environ['hostname'] = self.hostname
809        os.environ['username'] = self.username
810        os.environ['password'] = self.password
811
812        # Append default Env.
813        self.env_dict['hostname'] = self.hostname
814        self.env_dict['username'] = self.username
815        self.env_dict['password'] = self.password
816
817        try:
818            tmp_env_dict = {}
819            if self.env_vars:
820                tmp_env_dict = json.loads(self.env_vars)
821                # Export ENV vars default.
822                for key, value in tmp_env_dict.items():
823                    os.environ[key] = value
824                    self.env_dict[key] = str(value)
825
826            if self.econfig:
827                with open(self.econfig, 'r') as file:
828                    tmp_env_dict = yaml.load(file, Loader=yaml.FullLoader)
829                # Export ENV vars.
830                for key, value in tmp_env_dict['env_params'].items():
831                    os.environ[key] = str(value)
832                    self.env_dict[key] = str(value)
833        except json.decoder.JSONDecodeError as e:
834            self.logger.error("\n\tERROR: %s " % e)
835            sys.exit(-1)
836
837        # This to mask the password from displaying on the console.
838        mask_dict = self.env_dict.copy()
839        for k, v in mask_dict.items():
840            if k.lower().find("password") != -1:
841                hidden_text = []
842                hidden_text.append(v)
843                password_regex = '(' +\
844                    '|'.join([re.escape(x) for x in hidden_text]) + ')'
845                mask_dict[k] = re.sub(password_regex, "********", v)
846
847        self.logger.info(json.dumps(mask_dict, indent=8, sort_keys=False))
848
849    def execute_python_eval(self, eval_string):
850        r"""
851        Execute qualified python function using eval.
852
853        Description of argument(s):
854        eval_string        Execute the python object.
855
856        Example:
857                eval(plugin.foo_func.foo_func(10))
858        """
859        try:
860            self.logger.info("\tCall func: %s" % eval_string)
861            result = eval(eval_string)
862            self.logger.info("\treturn: %s" % str(result))
863        except (ValueError, SyntaxError, NameError) as e:
864            self.logger.error("execute_python_eval: %s" % e)
865            pass
866
867        return result
868
869    def execute_plugin_block(self, plugin_cmd_list):
870        r"""
871        Pack the plugin command to quailifed python string object.
872
873        Description of argument(s):
874        plugin_list_dict      Plugin block read from YAML
875                              [{'plugin_name': 'plugin.foo_func.my_func'},
876                               {'plugin_args': [10]}]
877
878        Example:
879            - plugin:
880              - plugin_name: plugin.foo_func.my_func
881              - plugin_args:
882                - arg1
883                - arg2
884
885            - plugin:
886              - plugin_name: result = plugin.foo_func.my_func
887              - plugin_args:
888                - arg1
889                - arg2
890
891            - plugin:
892              - plugin_name: result1,result2 = plugin.foo_func.my_func
893              - plugin_args:
894                - arg1
895                - arg2
896        """
897        try:
898            plugin_name = plugin_cmd_list[0]['plugin_name']
899            # Equal separator means plugin function returns result.
900            if ' = ' in plugin_name:
901                # Ex. ['result', 'plugin.foo_func.my_func']
902                plugin_name_args = plugin_name.split(' = ')
903                # plugin func return data.
904                for arg in plugin_name_args:
905                    if arg == plugin_name_args[-1]:
906                        plugin_name = arg
907                    else:
908                        plugin_resp = arg.split(',')
909                        # ['result1','result2']
910                        for x in plugin_resp:
911                            global_plugin_list.append(x)
912                            global_plugin_dict[x] = ""
913
914            # Walk the plugin args ['arg1,'arg2']
915            # If the YAML plugin statement 'plugin_args' is not declared.
916            if any('plugin_args' in d for d in plugin_cmd_list):
917                plugin_args = plugin_cmd_list[1]['plugin_args']
918                if plugin_args:
919                    plugin_args = self.yaml_args_populate(plugin_args)
920                else:
921                    plugin_args = []
922            else:
923                plugin_args = self.yaml_args_populate([])
924
925            # Pack the args arg1, arg2, .... argn into
926            # "arg1","arg2","argn"  string as params for function.
927            parm_args_str = self.yaml_args_string(plugin_args)
928            if parm_args_str:
929                plugin_func = plugin_name + '(' + parm_args_str + ')'
930            else:
931                plugin_func = plugin_name + '()'
932
933            # Execute plugin function.
934            if global_plugin_dict:
935                resp = self.execute_python_eval(plugin_func)
936                self.response_args_data(resp)
937            else:
938                self.execute_python_eval(plugin_func)
939        except Exception as e:
940            self.logger.error("execute_plugin_block: %s" % e)
941            pass
942
943    def response_args_data(self, plugin_resp):
944        r"""
945        Parse the plugin function response.
946
947        plugin_resp       Response data from plugin function.
948        """
949        resp_list = []
950        resp_data = ""
951        # There is nothing to update the plugin response.
952        if len(global_plugin_list) == 0 or plugin_resp == 'None':
953            return
954
955        if isinstance(plugin_resp, str):
956            resp_data = plugin_resp.strip('\r\n\t')
957            resp_list.append(resp_data)
958        elif isinstance(plugin_resp, bytes):
959            resp_data = str(plugin_resp, 'UTF-8').strip('\r\n\t')
960            resp_list.append(resp_data)
961        elif isinstance(plugin_resp, tuple):
962            if len(global_plugin_list) == 1:
963                resp_list.append(plugin_resp)
964            else:
965                resp_list = list(plugin_resp)
966                resp_list = [x.strip('\r\n\t') for x in resp_list]
967        elif isinstance(plugin_resp, list):
968            if len(global_plugin_list) == 1:
969                resp_list.append([x.strip('\r\n\t') for x in plugin_resp])
970            else:
971                resp_list = [x.strip('\r\n\t') for x in plugin_resp]
972        elif isinstance(plugin_resp, int) or isinstance(plugin_resp, float):
973            resp_list.append(plugin_resp)
974
975        for idx, item in enumerate(resp_list, start=0):
976            # Exit loop
977            if idx >= len(global_plugin_list):
978                break
979            # Find the index of the return func in the list and
980            # update the global func return dictionary.
981            try:
982                dict_idx = global_plugin_list[idx]
983                global_plugin_dict[dict_idx] = item
984            except (IndexError, ValueError) as e:
985                self.logger.warn("\tresponse_args_data: %s" % e)
986                pass
987
988        # Done updating plugin dict irrespective of pass or failed,
989        # clear all the list element.
990        global_plugin_list.clear()
991
992    def yaml_args_string(self, plugin_args):
993        r"""
994        Pack the args into string.
995
996        plugin_args            arg list ['arg1','arg2,'argn']
997        """
998        args_str = ''
999        for args in plugin_args:
1000            if args:
1001                if isinstance(args, int):
1002                    args_str += str(args)
1003                else:
1004                    args_str += '"' + str(args.strip('\r\n\t')) + '"'
1005            # Skip last list element.
1006            if args != plugin_args[-1]:
1007                args_str += ","
1008        return args_str
1009
1010    def yaml_args_populate(self, yaml_arg_list):
1011        r"""
1012        Decode ${MY_VAR} and load env data when read from YAML.
1013
1014        Description of argument(s):
1015        yaml_arg_list         arg list read from YAML
1016
1017        Example:
1018          - plugin_args:
1019            - arg1
1020            - arg2
1021
1022                  yaml_arg_list:  [arg2, arg2]
1023        """
1024        # Get the env loaded keys as list ['hostname', 'username', 'password'].
1025        env_vars_list = list(self.env_dict)
1026
1027        if isinstance(yaml_arg_list, list):
1028            tmp_list = []
1029            for arg in yaml_arg_list:
1030                if isinstance(arg, int):
1031                    tmp_list.append(arg)
1032                    continue
1033                elif isinstance(arg, str):
1034                    arg_str = self.yaml_env_and_plugin_vars_populate(str(arg))
1035                    tmp_list.append(arg_str)
1036                else:
1037                    tmp_list.append(arg)
1038
1039            # return populated list.
1040            return tmp_list
1041
1042    def yaml_env_and_plugin_vars_populate(self, yaml_arg_str):
1043        r"""
1044        Update ${MY_VAR} and my_plugin_vars
1045
1046        Description of argument(s):
1047        yaml_arg_str         arg string read from YAML
1048
1049        Example:
1050            - cat ${MY_VAR}
1051            - ls -AX my_plugin_var
1052        """
1053        # Parse the string for env vars.
1054        try:
1055            # Example, list of matching env vars ['username', 'password', 'hostname']
1056            # Extra escape \ for special symbols. '\$\{([^\}]+)\}' works good.
1057            var_name_regex = '\\$\\{([^\\}]+)\\}'
1058            env_var_names_list = re.findall(var_name_regex, yaml_arg_str)
1059            for var in env_var_names_list:
1060                env_var = os.environ[var]
1061                env_replace = '${' + var + '}'
1062                yaml_arg_str = yaml_arg_str.replace(env_replace, env_var)
1063        except Exception as e:
1064            self.logger.error("yaml_env_vars_populate: %s" % e)
1065            pass
1066
1067        # Parse the string for plugin vars.
1068        try:
1069            # Example, list of plugin vars ['my_username', 'my_data']
1070            plugin_var_name_list = global_plugin_dict.keys()
1071            for var in plugin_var_name_list:
1072                # If this plugin var exist but empty value in dict, don't replace.
1073                # This is either a YAML plugin statement incorrecly used or
1074                # user added a plugin var which is not populated.
1075                if str(global_plugin_dict[var]):
1076                    yaml_arg_str = yaml_arg_str.replace(str(var), str(global_plugin_dict[var]))
1077        except (IndexError, ValueError) as e:
1078            self.logger.error("yaml_plugin_vars_populate: %s" % e)
1079            pass
1080
1081        return yaml_arg_str
1082