github.com/SUSE/skuba@v1.4.17/ci/infra/testrunner/utils/utils.py (about) 1 import glob 2 import logging 3 import os 4 import shutil 5 import subprocess 6 from functools import wraps 7 from threading import Thread 8 9 import requests 10 from timeout_decorator import timeout 11 12 from utils.config import Constant 13 from utils.format import Format 14 15 logger = logging.getLogger('testrunner') 16 17 _stepdepth = 0 18 19 20 def step(f): 21 @wraps(f) 22 def wrapped(*args, **kwargs): 23 global _stepdepth 24 _stepdepth += 1 25 logger.debug("{} entering {} {}".format(Format.DOT * _stepdepth, f.__name__, 26 f.__doc__ or "")) 27 r = f(*args, **kwargs) 28 logger.debug("{} exiting {}".format( 29 Format.DOT_EXIT * _stepdepth, f.__name__)) 30 _stepdepth -= 1 31 return r 32 33 return wrapped 34 35 36 class Utils: 37 38 def __init__(self, conf): 39 self.conf = conf 40 41 @staticmethod 42 def chmod_recursive(directory, permissions): 43 os.chmod(directory, permissions) 44 45 for file in glob.glob(os.path.join(directory, "**/*"), recursive=True): 46 try: 47 os.chmod(file, permissions) 48 except Exception as ex: 49 logger.debug(ex) 50 51 @staticmethod 52 def cleanup_file(file): 53 if os.path.exists(file): 54 logger.debug(f"Cleaning up {file}") 55 try: 56 try: 57 # Attempt to remove the file first, because a socket (e.g. 58 # ssh-agent) is not a file but has to be removed like one. 59 os.remove(file) 60 except IsADirectoryError: 61 shutil.rmtree(file) 62 except Exception as ex: 63 logger.debug(ex) 64 else: 65 logger.debug(f"Nothing to clean up for {file}") 66 67 @staticmethod 68 def cleanup_files(files): 69 """Remove any files or dirs in a list if they exist""" 70 for file in files: 71 Utils.cleanup_file(file) 72 73 def ssh_cleanup(self): 74 """Remove ssh sock files""" 75 # TODO: also kill ssh agent here? maybe move pkill to kill_ssh_agent()? 76 sock_file = self.conf.utils.ssh_sock 77 sock_dir = os.path.dirname(sock_file) 78 try: 79 Utils.cleanup_file(sock_file) 80 # also remove tempdir if it's empty afterwards 81 if 0 == len(os.listdir(sock_dir)): 82 os.rmdir(sock_dir) 83 else: 84 logger.warning(f"Dir {sock_dir} not empty; leaving it") 85 except FileNotFoundError: 86 pass 87 except OSError as ex: 88 logger.debug(ex) 89 90 def collect_remote_logs(self, ip_address, logs, store_path): 91 """ 92 Collect logs from a remote machine 93 :param ip_address: (str) IP of the machine to collect the logs from 94 :param logs: (dict: list) The different logs to collect {"files": [], "dirs": [], ""services": []} 95 :param store_path: (str) Path to copy the logs to 96 :return: (bool) True if there was an error while collecting the logs 97 """ 98 logging_errors = False 99 100 for log in logs.get("files", []): 101 try: 102 self.scp_file(ip_address, log, store_path) 103 except Exception as ex: 104 logger.debug( 105 f"Error while collecting {log} from {ip_address}\n {ex}") 106 logging_errors = True 107 108 for log in logs.get("dirs", []): 109 try: 110 self.rsync(ip_address, log, store_path) 111 except Exception as ex: 112 logger.debug( 113 f"Error while collecting {log} from {ip_address}\n {ex}") 114 logging_errors = True 115 116 for service in logs.get("services", []): 117 try: 118 self.ssh_run( 119 ip_address, f"sudo journalctl -xeu {service} > {service}.log") 120 self.scp_file(ip_address, f"{service}.log", store_path) 121 except Exception as ex: 122 logger.debug( 123 f"Error while collecting {service}.log from {ip_address}\n {ex}") 124 logging_errors = True 125 126 return logging_errors 127 128 def ssh_user(self): 129 return self.conf.utils.ssh_user 130 131 def authorized_keys(self): 132 public_key_path = self.conf.utils.ssh_key + ".pub" 133 os.chmod(self.conf.utils.ssh_key, 0o400) 134 135 with open(public_key_path) as f: 136 pubkey = f.read().strip() 137 return pubkey 138 139 def ssh_run(self, ipaddr, cmd): 140 key_fn = self.conf.utils.ssh_key 141 cmd = "ssh " + Constant.SSH_OPTS + " -i {key_fn} {username}@{ip} -- '{cmd}'".format( 142 key_fn=key_fn, ip=ipaddr, cmd=cmd, username=self.ssh_user()) 143 return self.runshellcommand(cmd) 144 145 def scp_file(self, ip_address, remote_file_path, local_file_path): 146 """ 147 Copies a remote file from the given ip to the give path 148 :param ip_address: (str) IP address of the node to copy from 149 :param remote_file_path: (str) Path of the file to be copied 150 :param local_file_path: (str) Path where to store the log 151 :return: 152 """ 153 cmd = (f"scp {Constant.SSH_OPTS} -i {self.conf.utils.ssh_key}" 154 f" {self.ssh_user()}@{ip_address}:{remote_file_path} {local_file_path}") 155 self.runshellcommand(cmd) 156 157 def rsync(self, ip_address, remote_dir_path, local_dir_path): 158 """ 159 Copies a remote dir from the given ip to the give path 160 :param ip_address: (str) IP address of the node to copy from 161 :param remote_dir_path: (str) Path of the dir to be copied 162 :param local_dir_path: (str) Path where to store the dir 163 :return: 164 """ 165 cmd = (f'rsync -avz --no-owner --no-perms -e "ssh {Constant.SSH_OPTS} -i {self.conf.utils.ssh_key}" ' 166 f'--rsync-path="sudo rsync" --ignore-missing-args {self.ssh_user()}@{ip_address}:{remote_dir_path} ' 167 f'{local_dir_path}') 168 self.runshellcommand(cmd) 169 170 def runshellcommand(self, cmd, cwd=None, env={}, ignore_errors=False, stdin=None): 171 """Running shell command 172 Keyword arguments: 173 cmd -- command to run 174 cwd -- dir to run the cmd 175 env -- environment variables 176 ignore_errors -- don't raise exception if command fails 177 stdin -- standard input for the command in bytes 178 """ 179 180 cmd_env = { 181 "SSH_AUTH_SOCK": self.conf.utils.ssh_sock, 182 "PATH": os.environ['PATH'], 183 **env 184 } 185 186 if cwd and not os.path.exists(cwd): 187 raise FileNotFoundError(Format.alert("Directory {} does not exists".format(cwd))) 188 189 if logging.DEBUG >= logger.level: 190 logger.debug("Executing command\n" 191 " cwd: {} \n" 192 " env: {}\n" 193 " cmd: {}".format(cwd, str(cmd_env) if cmd_env else "{}", cmd)) 194 else: 195 logger.info("Executing command {}".format(cmd)) 196 197 stdout, stderr = [], [] 198 p = subprocess.Popen( 199 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd, 200 stdin=subprocess.PIPE if stdin else None, shell=True, env=cmd_env 201 ) 202 if stdin: 203 p.stdin.write(stdin) 204 p.stdin.close() 205 stdoutStreamer = Thread(target = self.read_fd, args = (p, p.stdout, logger.debug, stdout)) 206 stderrStreamer = Thread(target = self.read_fd, args = (p, p.stderr, logger.error, stderr)) 207 stdoutStreamer.start() 208 stderrStreamer.start() 209 stdoutStreamer.join() 210 stderrStreamer.join() 211 # this is redundant, at this point threads were joined and they waited for the subprocess 212 # to exit, however it should not hurt to explicitly wait for it again (no-op). 213 p.wait() 214 stdout, stderr = "".join(stdout), "".join(stderr) 215 216 if p.returncode != 0: 217 if not ignore_errors: 218 raise RuntimeError("Error executing command {}".format(cmd)) 219 else: 220 return stderr 221 return stdout 222 223 def read_fd(self, proc, fd, logger_func, output): 224 """Read from fd, logging using logger_func 225 226 Read from fd, until proc is finished. All contents will 227 also be appended onto output.""" 228 while True: 229 contents = fd.readline().decode() 230 if contents == '' and proc.poll() is not None: 231 return 232 if contents: 233 output.append(contents) 234 logger_func(contents.strip()) 235 236 @timeout(60) 237 @step 238 def setup_ssh(self): 239 os.chmod(self.conf.utils.ssh_key, 0o400) 240 241 # use a dedicated agent to minimize stateful components 242 sock_fn = self.conf.utils.ssh_sock 243 # be sure directory containing socket exists and socket doesn't exist 244 if os.path.exists(sock_fn): 245 try: 246 if os.path.isdir(sock_fn): 247 os.path.rmdir(sock_fn) # rmdir only removes an empty dir 248 else: 249 os.remove(sock_fn) 250 except FileNotFoundError: 251 pass 252 try: 253 os.mkdir(os.path.dirname(sock_fn), mode=0o700) 254 except FileExistsError: 255 if os.path.isdir(os.path.dirname(sock_fn)): 256 pass 257 else: 258 raise 259 # clean up old ssh agent process(es) 260 try: 261 self.runshellcommand("pkill -f 'ssh-agent -a {}'".format(sock_fn)) 262 logger.warning("Killed previous instance of ssh-agent") 263 except: 264 pass 265 self.runshellcommand("ssh-agent -a {}".format(sock_fn)) 266 self.runshellcommand( 267 "ssh-add " + self.conf.utils.ssh_key, env={"SSH_AUTH_SOCK": sock_fn}) 268 269 @timeout(30) 270 @step 271 def info(self): 272 """Node info""" 273 info_lines = "Env vars: {}\n".format(sorted(os.environ)) 274 info_lines += self.runshellcommand('ip a') 275 info_lines += self.runshellcommand('ip r') 276 info_lines += self.runshellcommand('cat /etc/resolv.conf') 277 278 # TODO: the logic for retrieving external is platform depedant and should be 279 # moved to the corresponding platform 280 try: 281 r = requests.get( 282 'http://169.254.169.254/2009-04-04/meta-data/public-ipv4', timeout=2) 283 r.raise_for_status() 284 except (requests.HTTPError, requests.Timeout) as err: 285 logger.warning( 286 f'Meta Data service unavailable could not get external IP addr{err}') 287 else: 288 info_lines += 'External IP addr: {}'.format(r.text) 289 290 return info_lines