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        return True
124
125    def generate_ffdc(self, working_protocol_list):
126        r"""
127        Send commands in ffdc_config file to targeted system.
128
129        """
130
131        print("\n\t---- Executing commands on " + self.hostname + " ----")
132        print("\n\tWorking protocol list: %s" % working_protocol_list)
133        with open(self.ffdc_config, 'r') as file:
134            ffdc_actions = yaml.load(file, Loader=yaml.FullLoader)
135
136        for machine_type in ffdc_actions.keys():
137            if machine_type == self.target_type:
138
139                if (ffdc_actions[machine_type]['PROTOCOL'][0] in working_protocol_list):
140
141                    print("\n\t[Run] Executing commands on %s using %s"
142                          % (self.hostname, ffdc_actions[machine_type]['PROTOCOL'][0]))
143                    list_of_commands = ffdc_actions[machine_type]['COMMANDS']
144                    progress_counter = 0
145                    for command in list_of_commands:
146                        self.remoteclient.execute_command(command)
147                        progress_counter += 1
148                        self.print_progress(progress_counter)
149                    print("\n\t[Run] Commands execution completed \t\t [OK]")
150
151                    print("\n\n\tCopying FFDC files from remote system %s \n\n" % self.hostname)
152                    # Get default values for scp action.
153                    # self.location == local system for now
154                    self.set_ffdc_defaults()
155                    # Retrieving files from target system
156                    list_of_files = ffdc_actions[machine_type]['FILES']
157                    self.scp_ffdc(self.ffdc_dir_path, self.ffdc_prefix, list_of_files)
158                else:
159                    print("\n\tProtocol %s is not yet supported by this script.\n"
160                          % ffdc_actions[machine_type]['PROTOCOL'][0])
161
162    def scp_ffdc(self,
163                 targ_dir_path,
164                 targ_file_prefix="",
165                 file_list=None,
166                 quiet=None):
167        r"""
168        SCP all files in file_dict to the indicated directory on the local system.
169
170        Description of argument(s):
171        targ_dir_path                   The path of the directory to receive the files.
172        targ_file_prefix                Prefix which will be pre-pended to each
173                                        target file's name.
174        file_dict                       A dictionary of files to scp from targeted system to this system
175
176        """
177
178        self.remoteclient.scp_connection()
179
180        self.receive_file_list = []
181        progress_counter = 0
182        for filename in file_list:
183            source_file_path = filename
184            targ_file_path = targ_dir_path + targ_file_prefix + filename.split('/')[-1]
185
186            # self.remoteclient.scp_file_from_remote() completed without exception,
187            # add file to the receiving file list.
188            scp_result = self.remoteclient.scp_file_from_remote(source_file_path, targ_file_path)
189            if scp_result:
190                self.receive_file_list.append(targ_file_path)
191
192            if not quiet:
193                if scp_result:
194                    print("\t\tSuccessfully copied from " + self.hostname + ':' + source_file_path + ".\n")
195                else:
196                    print("\t\tFail to copy from " + self.hostname + ':' + source_file_path + ".\n")
197            else:
198                progress_counter += 1
199                self.print_progress(progress_counter)
200
201        self.remoteclient.ssh_remoteclient_disconnect()
202
203    def set_ffdc_defaults(self):
204        r"""
205        Set a default value for self.ffdc_dir_path and self.ffdc_prefix.
206        Collected ffdc file will be stored in dir /self.location/hostname_timestr/.
207        Individual ffdc file will have timestr_filename.
208
209        Description of class variables:
210        self.ffdc_dir_path  The dir path where collected ffdc data files should be put.
211
212        self.ffdc_prefix    The prefix to be given to each ffdc file name.
213
214        """
215
216        timestr = time.strftime("%Y%m%d-%H%M%S")
217        self.ffdc_dir_path = self.location + "/" + self.hostname + "_" + timestr + "/"
218        self.ffdc_prefix = timestr + "_"
219        self.validate_local_store(self.ffdc_dir_path)
220
221    def validate_local_store(self, dir_path):
222        r"""
223        Ensure path exists to store FFDC files locally.
224
225        Description of variable:
226        dir_path  The dir path where collected ffdc data files will be stored.
227
228        """
229
230        if not os.path.exists(dir_path):
231            try:
232                os.mkdir(dir_path, 0o755)
233            except (IOError, OSError) as e:
234                # PermissionError
235                if e.errno == EPERM or e.errno == EACCES:
236                    print('>>>>>\tERROR: os.mkdir %s failed with PermissionError.\n' % dir_path)
237                else:
238                    print('>>>>>\tERROR: os.mkdir %s failed with %s.\n' % (dir_path, e.strerror))
239                sys.exit(-1)
240
241    def print_progress(self, progress):
242        r"""
243        Print activity progress +
244
245        Description of variable:
246        progress  Progress counter.
247
248        """
249
250        sys.stdout.write("\r\t" + "+" * progress)
251        sys.stdout.flush()
252        time.sleep(.1)
253