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
13from ssh_utility import SSHRemoteclient
14
15
16class FFDCCollector:
17
18    r"""
19    Sends commands from configuration file to the targeted system to collect log files.
20    Fetch and store generated files at the specified location.
21
22    """
23
24    def __init__(self, hostname, username, password, ffdc_config, location):
25        r"""
26        Description of argument(s):
27
28        hostname                name/ip of the targeted (remote) system
29        username                user on the targeted system with access to FFDC files
30        password                password for user on targeted system
31        ffdc_config             configuration file listing commands and files for FFDC
32        location                Where to store collected FFDC
33
34        """
35        if self.verify_script_env():
36            self.hostname = hostname
37            self.username = username
38            self.password = password
39            self.ffdc_config = ffdc_config
40            self.location = location
41            self.remote_client = None
42            self.ffdc_dir_path = ""
43            self.ffdc_prefix = ""
44            self.receive_file_list = []
45            self.target_type = ""
46        else:
47            sys.exit(-1)
48
49    def verify_script_env(self):
50
51        # Import to log version
52        import click
53        import paramiko
54
55        run_env_ok = True
56        print("\n\t---- Script host environment ----")
57        print("\t{:<10}  {:<10}".format('Script hostname', os.uname()[1]))
58        print("\t{:<10}  {:<10}".format('Script host os', platform.platform()))
59        print("\t{:<10}  {:>10}".format('Python', platform.python_version()))
60        print("\t{:<10}  {:>10}".format('PyYAML', yaml.__version__))
61        print("\t{:<10}  {:>10}".format('click', click.__version__))
62        print("\t{:<10}  {:>10}".format('paramiko', paramiko.__version__))
63
64        if eval(yaml.__version__.replace('.', ',')) < (5, 4, 1):
65            print("\n\tERROR: Python or python packages do not meet minimum version requirement.")
66            print("\tERROR: PyYAML version 5.4.1 or higher is needed.\n")
67            run_env_ok = False
68
69        print("\t---- End script host environment ----")
70        return run_env_ok
71
72    def target_is_pingable(self):
73        r"""
74        Check if target system is ping-able.
75
76        """
77        response = os.system("ping -c 1 -w 2 %s  2>&1 >/dev/null" % self.hostname)
78        if response == 0:
79            print("\n\t[Check] %s is ping-able.\t\t\t [OK]" % self.hostname)
80            return True
81        else:
82            print("\n>>>>>\tERROR: %s is not ping-able. FFDC collection aborted.\n" % self.hostname)
83            sys.exit(-1)
84
85    def set_target_machine_type(self):
86        r"""
87        Determine and set target machine type.
88
89        """
90        # Default to openbmc for first few sprints
91        self.target_type = "OPENBMC"
92
93    def collect_ffdc(self):
94        r"""
95        Initiate FFDC Collection depending on requested protocol.
96
97        """
98
99        print("\n\t---- Start communicating with %s ----" % self.hostname)
100        working_protocol_list = []
101        if self.target_is_pingable():
102            # Check supported protocol ping,ssh, redfish are working.
103            if self.ssh_to_target_system():
104                working_protocol_list.append("SSH")
105            # Verify top level directory exists for storage
106            self.validate_local_store(self.location)
107            self.set_target_machine_type()
108            print("\n\t---- Completed protocol pre-requisite check ----\n")
109            self.generate_ffdc(working_protocol_list)
110
111    def ssh_to_target_system(self):
112        r"""
113        Open a ssh connection to targeted system.
114
115        """
116
117        self.remoteclient = SSHRemoteclient(self.hostname,
118                                            self.username,
119                                            self.password)
120
121        self.remoteclient.ssh_remoteclient_login()
122        print("\n\t[Check] %s SSH connection established.\t [OK]" % self.hostname)
123
124        # Check scp connection.
125        # If scp connection fails,
126        # continue with FFDC generation but skip scp files to local host.
127        self.remoteclient.scp_connection()
128        return True
129
130    def generate_ffdc(self, working_protocol_list):
131        r"""
132        Send commands in ffdc_config file to targeted system.
133
134        """
135
136        print("\n\t---- Executing commands on " + self.hostname + " ----")
137        print("\n\tWorking protocol list: %s" % working_protocol_list)
138        with open(self.ffdc_config, 'r') as file:
139            ffdc_actions = yaml.load(file, Loader=yaml.FullLoader)
140
141        for machine_type in ffdc_actions.keys():
142            if machine_type == self.target_type:
143
144                if (ffdc_actions[machine_type]['PROTOCOL'][0] in working_protocol_list):
145
146                    print("\n\t[Run] Executing commands on %s using %s"
147                          % (self.hostname, ffdc_actions[machine_type]['PROTOCOL'][0]))
148                    list_of_commands = ffdc_actions[machine_type]['COMMANDS']
149                    progress_counter = 0
150                    for command in list_of_commands:
151                        self.remoteclient.execute_command(command)
152                        progress_counter += 1
153                        self.print_progress(progress_counter)
154                    print("\n\t[Run] Commands execution completed.\t\t [OK]")
155
156                    if self.remoteclient.scpclient:
157                        print("\n\n\tCopying FFDC files from remote system %s.\n" % self.hostname)
158                        # Get default values for scp action.
159                        # self.location == local system for now
160                        self.set_ffdc_defaults()
161                        # Retrieving files from target system
162                        list_of_files = ffdc_actions[machine_type]['FILES']
163                        self.scp_ffdc(self.ffdc_dir_path, self.ffdc_prefix, list_of_files)
164                    else:
165                        print("\n\n\tSkip copying FFDC files from remote system %s.\n" % self.hostname)
166                else:
167                    print("\n\tProtocol %s is not yet supported by this script.\n"
168                          % ffdc_actions[machine_type]['PROTOCOL'][0])
169
170    def scp_ffdc(self,
171                 targ_dir_path,
172                 targ_file_prefix="",
173                 file_list=None,
174                 quiet=None):
175        r"""
176        SCP all files in file_dict to the indicated directory on the local system.
177
178        Description of argument(s):
179        targ_dir_path                   The path of the directory to receive the files.
180        targ_file_prefix                Prefix which will be pre-pended to each
181                                        target file's name.
182        file_dict                       A dictionary of files to scp from targeted system to this system
183
184        """
185
186        self.receive_file_list = []
187        progress_counter = 0
188        for filename in file_list:
189            source_file_path = filename
190            targ_file_path = targ_dir_path + targ_file_prefix + filename.split('/')[-1]
191
192            # self.remoteclient.scp_file_from_remote() completed without exception,
193            # add file to the receiving file list.
194            scp_result = self.remoteclient.scp_file_from_remote(source_file_path, targ_file_path)
195            if scp_result:
196                self.receive_file_list.append(targ_file_path)
197
198            if not quiet:
199                if scp_result:
200                    print("\t\tSuccessfully copied from " + self.hostname + ':' + source_file_path + ".\n")
201                else:
202                    print("\t\tFail to copy from " + self.hostname + ':' + source_file_path + ".\n")
203            else:
204                progress_counter += 1
205                self.print_progress(progress_counter)
206
207        self.remoteclient.ssh_remoteclient_disconnect()
208
209    def set_ffdc_defaults(self):
210        r"""
211        Set a default value for self.ffdc_dir_path and self.ffdc_prefix.
212        Collected ffdc file will be stored in dir /self.location/hostname_timestr/.
213        Individual ffdc file will have timestr_filename.
214
215        Description of class variables:
216        self.ffdc_dir_path  The dir path where collected ffdc data files should be put.
217
218        self.ffdc_prefix    The prefix to be given to each ffdc file name.
219
220        """
221
222        timestr = time.strftime("%Y%m%d-%H%M%S")
223        self.ffdc_dir_path = self.location + "/" + self.hostname + "_" + timestr + "/"
224        self.ffdc_prefix = timestr + "_"
225        self.validate_local_store(self.ffdc_dir_path)
226
227    def validate_local_store(self, dir_path):
228        r"""
229        Ensure path exists to store FFDC files locally.
230
231        Description of variable:
232        dir_path  The dir path where collected ffdc data files will be stored.
233
234        """
235
236        if not os.path.exists(dir_path):
237            try:
238                os.mkdir(dir_path, 0o755)
239            except (IOError, OSError) as e:
240                # PermissionError
241                if e.errno == EPERM or e.errno == EACCES:
242                    print('>>>>>\tERROR: os.mkdir %s failed with PermissionError.\n' % dir_path)
243                else:
244                    print('>>>>>\tERROR: os.mkdir %s failed with %s.\n' % (dir_path, e.strerror))
245                sys.exit(-1)
246
247    def print_progress(self, progress):
248        r"""
249        Print activity progress +
250
251        Description of variable:
252        progress  Progress counter.
253
254        """
255
256        sys.stdout.write("\r\t" + "+" * progress)
257        sys.stdout.flush()
258        time.sleep(.1)
259