xref: /openbmc/openbmc-test-automation/ffdc/ffdc_collector.py (revision 16162633c64de5f7e38824ec1350d39795e774a6)
1#!/usr/bin/env python
2
3r"""
4See class prolog below for details.
5"""
6
7import os
8import sys
9import yaml
10import time
11import platform
12from errno import EACCES, EPERM
13import subprocess
14from ssh_utility import SSHRemoteclient
15
16
17class FFDCCollector:
18
19    r"""
20    Sends commands from configuration file to the targeted system to collect log files.
21    Fetch and store generated files at the specified location.
22
23    """
24
25    # List of supported OSes.
26    supported_oses = ['OPENBMC', 'RHEL', 'AIX', 'UBUNTU']
27
28    def __init__(self,
29                 hostname,
30                 username,
31                 password,
32                 ffdc_config,
33                 location,
34                 remote_type,
35                 remote_protocol):
36        r"""
37        Description of argument(s):
38
39        hostname                name/ip of the targeted (remote) system
40        username                user on the targeted system with access to FFDC files
41        password                password for user on targeted system
42        ffdc_config             configuration file listing commands and files for FFDC
43        location                where to store collected FFDC
44        remote_type             os type of the remote host
45
46        """
47        if self.verify_script_env():
48            self.hostname = hostname
49            self.username = username
50            self.password = password
51            self.ffdc_config = ffdc_config
52            self.location = location
53            self.remote_client = None
54            self.ffdc_dir_path = ""
55            self.ffdc_prefix = ""
56            self.target_type = remote_type.upper()
57            self.remote_protocol = remote_protocol.upper()
58        else:
59            sys.exit(-1)
60
61    def verify_script_env(self):
62
63        # Import to log version
64        import click
65        import paramiko
66
67        run_env_ok = True
68
69        redfishtool_version = self.run_redfishtool('-V').split(' ')[2].strip('\n')
70        ipmitool_version = self.run_ipmitool('-V').split(' ')[2]
71
72        print("\n\t---- Script host environment ----")
73        print("\t{:<10}  {:<10}".format('Script hostname', os.uname()[1]))
74        print("\t{:<10}  {:<10}".format('Script host os', platform.platform()))
75        print("\t{:<10}  {:>10}".format('Python', platform.python_version()))
76        print("\t{:<10}  {:>10}".format('PyYAML', yaml.__version__))
77        print("\t{:<10}  {:>10}".format('click', click.__version__))
78        print("\t{:<10}  {:>10}".format('paramiko', paramiko.__version__))
79        print("\t{:<10}  {:>9}".format('redfishtool', redfishtool_version))
80        print("\t{:<10}  {:>12}".format('ipmitool', ipmitool_version))
81
82        if eval(yaml.__version__.replace('.', ',')) < (5, 4, 1):
83            print("\n\tERROR: Python or python packages do not meet minimum version requirement.")
84            print("\tERROR: PyYAML version 5.4.1 or higher is needed.\n")
85            run_env_ok = False
86
87        print("\t---- End script host environment ----")
88        return run_env_ok
89
90    def target_is_pingable(self):
91        r"""
92        Check if target system is ping-able.
93
94        """
95        response = os.system("ping -c 1 -w 2 %s  2>&1 >/dev/null" % self.hostname)
96        if response == 0:
97            print("\n\t[Check] %s is ping-able.\t\t [OK]" % self.hostname)
98            return True
99        else:
100            print("\n>>>>>\tERROR: %s is not ping-able. FFDC collection aborted.\n" % self.hostname)
101            sys.exit(-1)
102
103    def inspect_target_machine_type(self):
104        r"""
105        Inspect remote host os-release or uname.
106
107        """
108        command = "cat /etc/os-release"
109        response = self.remoteclient.execute_command(command)
110        if response:
111            print("\n\t[INFO] %s /etc/os-release\n" % self.hostname)
112            for each_info in response:
113                print("\t\t %s" % each_info)
114            identity = self.find_os_type(response, 'ID').split('=')[1].upper()
115        else:
116            response = self.remoteclient.execute_command('uname -a')
117            print("\n\t[INFO] %s uname -a\n" % self.hostname)
118            print("\t\t %s" % ' '.join(response))
119            identity = self.find_os_type(response, 'AIX').split(' ')[0].upper()
120
121            # If OS does not have /etc/os-release and is not AIX,
122            # script does not yet know what to do.
123            if not identity:
124                print(">>>>>\tERROR: Script does not yet know about %s" % ' '.join(response))
125                sys.exit(-1)
126
127        if (self.target_type not in identity):
128
129            user_target_type = self.target_type
130            self.target_type = ""
131            for each_os in FFDCCollector.supported_oses:
132                if each_os in identity:
133                    self.target_type = each_os
134                    break
135
136            # If OS in not one of ['OPENBMC', 'RHEL', 'AIX', 'UBUNTU']
137            # script does not yet know what to do.
138            if not self.target_type:
139                print(">>>>>\tERROR: Script does not yet know about %s" % identity)
140                sys.exit(-1)
141
142            print("\n\t[WARN] user request %s does not match remote host type %s.\n"
143                  % (user_target_type, self.target_type))
144            print("\t[WARN] FFDC collection continues for %s.\n" % self.target_type)
145
146    def find_os_type(self,
147                     listing_from_os,
148                     key):
149        r"""
150        Return OS information with the requested key
151
152        Description of argument(s):
153
154        listing_from_os    list of information returns from OS command
155        key                key of the desired data
156
157        """
158
159        for each_item in listing_from_os:
160            if key in each_item:
161                return each_item
162        return ''
163
164    def collect_ffdc(self):
165        r"""
166        Initiate FFDC Collection depending on requested protocol.
167
168        """
169
170        print("\n\t---- Start communicating with %s ----" % self.hostname)
171        working_protocol_list = []
172        if self.target_is_pingable():
173            # Check supported protocol ping,ssh, redfish are working.
174            if self.ssh_to_target_system():
175                working_protocol_list.append("SSH")
176                working_protocol_list.append("SCP")
177
178            # Redfish
179            if self.verify_redfish():
180                working_protocol_list.append("REDFISH")
181                print("\n\t[Check] %s Redfish Service.\t\t [OK]" % self.hostname)
182            else:
183                print("\n\t[Check] %s Redfish Service.\t\t [FAILED]" % self.hostname)
184
185            if self.verify_ipmi():
186                working_protocol_list.append("IPMI")
187                print("\n\t[Check] %s IPMI LAN Service.\t\t [OK]" % self.hostname)
188            else:
189                print("\n\t[Check] %s IPMI LAN Service.\t\t [FAILED]" % self.hostname)
190
191            # Verify top level directory exists for storage
192            self.validate_local_store(self.location)
193            self.inspect_target_machine_type()
194            print("\n\t---- Completed protocol pre-requisite check ----\n")
195
196            if ((self.remote_protocol not in working_protocol_list) and (self.remote_protocol != 'ALL')):
197                print("\n\tWorking protocol list: %s" % working_protocol_list)
198                print(
199                    '>>>>>\tERROR: Requested protocol %s is not in working protocol list.\n'
200                    % self.remote_protocol)
201                sys.exit(-1)
202            else:
203                self.generate_ffdc(working_protocol_list)
204
205    def ssh_to_target_system(self):
206        r"""
207        Open a ssh connection to targeted system.
208
209        """
210
211        self.remoteclient = SSHRemoteclient(self.hostname,
212                                            self.username,
213                                            self.password)
214
215        self.remoteclient.ssh_remoteclient_login()
216        print("\n\t[Check] %s SSH connection established.\t [OK]" % self.hostname)
217
218        # Check scp connection.
219        # If scp connection fails,
220        # continue with FFDC generation but skip scp files to local host.
221        self.remoteclient.scp_connection()
222        return True
223
224    def generate_ffdc(self, working_protocol_list):
225        r"""
226        Determine actions based on remote host type
227
228        Description of argument(s):
229        working_protocol_list    list of confirmed working protocols to connect to remote host.
230        """
231
232        print("\n\t---- Executing commands on " + self.hostname + " ----")
233        print("\n\tWorking protocol list: %s" % working_protocol_list)
234        with open(self.ffdc_config, 'r') as file:
235            ffdc_actions = yaml.load(file, Loader=yaml.FullLoader)
236
237        # Set prefix values for scp files and directory.
238        # Since the time stamp is at second granularity, these values are set here
239        # to be sure that all files for this run will have same timestamps
240        # and they will be saved in the same directory.
241        # self.location == local system for now
242        self.set_ffdc_defaults()
243
244        for machine_type in ffdc_actions.keys():
245
246            if machine_type == self.target_type:
247                if self.remote_protocol == 'SSH' or self.remote_protocol == 'ALL':
248                    self.protocol_ssh(ffdc_actions, machine_type)
249
250                if self.target_type == 'OPENBMC':
251                    if self.remote_protocol == 'REDFISH' or self.remote_protocol == 'ALL':
252                        self.protocol_redfish(ffdc_actions, 'OPENBMC_REDFISH')
253
254                    if self.remote_protocol == 'IPMI' or self.remote_protocol == 'ALL':
255                        self.protocol_ipmi(ffdc_actions, 'OPENBMC_IPMI')
256
257        # Close network connection after collecting all files
258        self.remoteclient.ssh_remoteclient_disconnect()
259
260    def protocol_ssh(self,
261                     ffdc_actions,
262                     machine_type):
263        r"""
264        Perform actions using SSH and SCP protocols.
265
266        Description of argument(s):
267        ffdc_actions        List of actions from ffdc_config.yaml.
268        machine_type        OS Type of remote host.
269        """
270
271        # For OPENBMC collect general system info.
272        if self.target_type == 'OPENBMC':
273
274            self.collect_and_copy_ffdc(ffdc_actions['GENERAL'],
275                                       form_filename=True)
276            self.group_copy(ffdc_actions['OPENBMC_DUMPS'])
277
278        # For RHEL and UBUNTU, collect common Linux OS FFDC.
279        if self.target_type == 'RHEL' \
280           or self.target_type == 'UBUNTU':
281
282            self.collect_and_copy_ffdc(ffdc_actions['LINUX'])
283
284        # Collect remote host specific FFDC.
285        self.collect_and_copy_ffdc(ffdc_actions[machine_type])
286
287    def protocol_redfish(self,
288                         ffdc_actions,
289                         machine_type):
290        r"""
291        Perform actions using Redfish protocol.
292
293        Description of argument(s):
294        ffdc_actions        List of actions from ffdc_config.yaml.
295        machine_type        OS Type of remote host.
296        """
297
298        print("\n\t[Run] Executing commands to %s using %s" % (self.hostname, 'REDFISH'))
299        redfish_files_saved = []
300        progress_counter = 0
301        list_of_URL = ffdc_actions[machine_type]['URL']
302        for index, each_url in enumerate(list_of_URL, start=0):
303            redfish_parm = '-u ' + self.username + ' -p ' + self.password + ' -r ' \
304                           + self.hostname + ' -S Always raw GET ' + each_url
305
306            result = self.run_redfishtool(redfish_parm)
307            if result:
308                try:
309                    targ_file = ffdc_actions[machine_type]['FILES'][index]
310                except IndexError:
311                    targ_file = each_url.split('/')[-1]
312                    print("\n\t[WARN] Missing filename to store data from redfish URL %s." % each_url)
313                    print("\t[WARN] Data will be stored in %s." % targ_file)
314
315                targ_file_with_path = (self.ffdc_dir_path
316                                       + self.ffdc_prefix
317                                       + targ_file)
318
319                # Creates a new file
320                with open(targ_file_with_path, 'w') as fp:
321                    fp.write(result)
322                    fp.close
323                    redfish_files_saved.append(targ_file)
324
325            progress_counter += 1
326            self.print_progress(progress_counter)
327
328        print("\n\t[Run] Commands execution completed.\t\t [OK]")
329
330        for file in redfish_files_saved:
331            print("\n\t\tSuccessfully save file " + file + ".")
332
333    def protocol_ipmi(self,
334                      ffdc_actions,
335                      machine_type):
336        r"""
337        Perform actions using ipmitool over LAN protocol.
338
339        Description of argument(s):
340        ffdc_actions        List of actions from ffdc_config.yaml.
341        machine_type        OS Type of remote host.
342        """
343
344        print("\n\t[Run] Executing commands to %s using %s" % (self.hostname, 'IPMI'))
345        ipmi_files_saved = []
346        progress_counter = 0
347        list_of_cmd = ffdc_actions[machine_type]['COMMANDS']
348        for index, each_cmd in enumerate(list_of_cmd, start=0):
349            ipmi_parm = '-U ' + self.username + ' -P ' + self.password + ' -H ' \
350                + self.hostname + ' ' + each_cmd
351
352            result = self.run_ipmitool(ipmi_parm)
353            if result:
354                try:
355                    targ_file = ffdc_actions[machine_type]['FILES'][index]
356                except IndexError:
357                    targ_file = each_url.split('/')[-1]
358                    print("\n\t[WARN] Missing filename to store data from IPMI %s." % each_cmd)
359                    print("\t[WARN] Data will be stored in %s." % targ_file)
360
361                targ_file_with_path = (self.ffdc_dir_path
362                                       + self.ffdc_prefix
363                                       + targ_file)
364
365                # Creates a new file
366                with open(targ_file_with_path, 'w') as fp:
367                    fp.write(result)
368                    fp.close
369                    ipmi_files_saved.append(targ_file)
370
371            progress_counter += 1
372            self.print_progress(progress_counter)
373
374        print("\n\t[Run] Commands execution completed.\t\t [OK]")
375
376        for file in ipmi_files_saved:
377            print("\n\t\tSuccessfully save file " + file + ".")
378
379    def collect_and_copy_ffdc(self,
380                              ffdc_actions_for_machine_type,
381                              form_filename=False):
382        r"""
383        Send commands in ffdc_config file to targeted system.
384
385        Description of argument(s):
386        ffdc_actions_for_machine_type    commands and files for the selected remote host type.
387        form_filename                    if true, pre-pend self.target_type to filename
388        """
389
390        print("\n\t[Run] Executing commands on %s using %s"
391              % (self.hostname, ffdc_actions_for_machine_type['PROTOCOL'][0]))
392        list_of_commands = ffdc_actions_for_machine_type['COMMANDS']
393        progress_counter = 0
394        for command in list_of_commands:
395            if form_filename:
396                command = str(command % self.target_type)
397            self.remoteclient.execute_command(command)
398            progress_counter += 1
399            self.print_progress(progress_counter)
400
401        print("\n\t[Run] Commands execution completed.\t\t [OK]")
402
403        if self.remoteclient.scpclient:
404            print("\n\n\tCopying FFDC files from remote system %s.\n" % self.hostname)
405
406            # Retrieving files from target system
407            list_of_files = ffdc_actions_for_machine_type['FILES']
408            self.scp_ffdc(self.ffdc_dir_path, self.ffdc_prefix, form_filename, list_of_files)
409        else:
410            print("\n\n\tSkip copying FFDC files from remote system %s.\n" % self.hostname)
411
412    def group_copy(self,
413                   ffdc_actions_for_machine_type):
414        r"""
415        scp group of files (wild card) from remote host.
416
417        Description of argument(s):
418        ffdc_actions_for_machine_type    commands and files for the selected remote host type.
419        """
420        if self.remoteclient.scpclient:
421            print("\n\tCopying DUMP files from remote system %s.\n" % self.hostname)
422
423            # Retrieving files from target system, if any
424            list_of_files = ffdc_actions_for_machine_type['FILES']
425
426            for filename in list_of_files:
427                command = 'ls -AX ' + filename
428                response = self.remoteclient.execute_command(command)
429                # self.remoteclient.scp_file_from_remote() completed without exception,
430                # if any
431                if response:
432                    scp_result = self.remoteclient.scp_file_from_remote(filename, self.ffdc_dir_path)
433                    if scp_result:
434                        print("\t\tSuccessfully copied from " + self.hostname + ':' + filename)
435                else:
436                    print("\t\tThere is no  " + filename)
437
438        else:
439            print("\n\n\tSkip copying files from remote system %s.\n" % self.hostname)
440
441    def scp_ffdc(self,
442                 targ_dir_path,
443                 targ_file_prefix,
444                 form_filename,
445                 file_list=None,
446                 quiet=None):
447        r"""
448        SCP all files in file_dict to the indicated directory on the local system.
449
450        Description of argument(s):
451        targ_dir_path                   The path of the directory to receive the files.
452        targ_file_prefix                Prefix which will be pre-pended to each
453                                        target file's name.
454        file_dict                       A dictionary of files to scp from targeted system to this system
455
456        """
457
458        progress_counter = 0
459        for filename in file_list:
460            if form_filename:
461                filename = str(filename % self.target_type)
462            source_file_path = filename
463            targ_file_path = targ_dir_path + targ_file_prefix + filename.split('/')[-1]
464
465            # self.remoteclient.scp_file_from_remote() completed without exception,
466            # add file to the receiving file list.
467            scp_result = self.remoteclient.scp_file_from_remote(source_file_path, targ_file_path)
468
469            if not quiet:
470                if scp_result:
471                    print("\t\tSuccessfully copied from " + self.hostname + ':' + source_file_path + ".\n")
472                else:
473                    print("\t\tFail to copy from " + self.hostname + ':' + source_file_path + ".\n")
474            else:
475                progress_counter += 1
476                self.print_progress(progress_counter)
477
478    def set_ffdc_defaults(self):
479        r"""
480        Set a default value for self.ffdc_dir_path and self.ffdc_prefix.
481        Collected ffdc file will be stored in dir /self.location/hostname_timestr/.
482        Individual ffdc file will have timestr_filename.
483
484        Description of class variables:
485        self.ffdc_dir_path  The dir path where collected ffdc data files should be put.
486
487        self.ffdc_prefix    The prefix to be given to each ffdc file name.
488
489        """
490
491        timestr = time.strftime("%Y%m%d-%H%M%S")
492        self.ffdc_dir_path = self.location + "/" + self.hostname + "_" + timestr + "/"
493        self.ffdc_prefix = timestr + "_"
494        self.validate_local_store(self.ffdc_dir_path)
495
496    def validate_local_store(self, dir_path):
497        r"""
498        Ensure path exists to store FFDC files locally.
499
500        Description of variable:
501        dir_path  The dir path where collected ffdc data files will be stored.
502
503        """
504
505        if not os.path.exists(dir_path):
506            try:
507                os.mkdir(dir_path, 0o755)
508            except (IOError, OSError) as e:
509                # PermissionError
510                if e.errno == EPERM or e.errno == EACCES:
511                    print('>>>>>\tERROR: os.mkdir %s failed with PermissionError.\n' % dir_path)
512                else:
513                    print('>>>>>\tERROR: os.mkdir %s failed with %s.\n' % (dir_path, e.strerror))
514                sys.exit(-1)
515
516    def print_progress(self, progress):
517        r"""
518        Print activity progress +
519
520        Description of variable:
521        progress  Progress counter.
522
523        """
524
525        sys.stdout.write("\r\t" + "+" * progress)
526        sys.stdout.flush()
527        time.sleep(.1)
528
529    def verify_redfish(self):
530        r"""
531        Verify remote host has redfish service active
532
533        """
534        redfish_parm = '-u ' + self.username + ' -p ' + self.password + ' -r ' \
535                       + self.hostname + ' -S Always raw GET /redfish/v1/'
536        return(self.run_redfishtool(redfish_parm, True))
537
538    def verify_ipmi(self):
539        r"""
540        Verify remote host has IPMI LAN service active
541
542        """
543        ipmi_parm = '-U ' + self.username + ' -P ' + self.password + ' -H ' \
544            + self.hostname + ' power status'
545        return(self.run_ipmitool(ipmi_parm, True))
546
547    def run_redfishtool(self,
548                        parms_string,
549                        quiet=False):
550        r"""
551        Run CLI redfishtool
552
553        Description of variable:
554        parms_string         redfishtool subcommand and options.
555        quiet                do not print redfishtool error message if True
556        """
557
558        result = subprocess.run(['redfishtool ' + parms_string],
559                                stdout=subprocess.PIPE,
560                                stderr=subprocess.PIPE,
561                                shell=True,
562                                universal_newlines=True)
563
564        if result.stderr and not quiet:
565            print('\n\t\tERROR with redfishtool ' + parms_string)
566            print('\t\t' + result.stderr)
567
568        return result.stdout
569
570    def run_ipmitool(self,
571                     parms_string,
572                     quiet=False):
573        r"""
574        Run CLI IPMI tool.
575
576        Description of variable:
577        parms_string         ipmitool subcommand and options.
578        quiet                do not print redfishtool error message if True
579        """
580
581        result = subprocess.run(['ipmitool -I lanplus -C 17 ' + parms_string],
582                                stdout=subprocess.PIPE,
583                                stderr=subprocess.PIPE,
584                                shell=True,
585                                universal_newlines=True)
586
587        if result.stderr and not quiet:
588            print('\n\t\tERROR with ipmitool -I lanplus -C 17 ' + parms_string)
589            print('\t\t' + result.stderr)
590
591        return result.stdout
592