1# 2# CDDL HEADER START 3# 4# The contents of this file are subject to the terms of the 5# Common Development and Distribution License (the "License"). 6# You may not use this file except in compliance with the License. 7# 8# See LICENSE.txt included in this distribution for the specific 9# language governing permissions and limitations under the License. 10# 11# When distributing Covered Code, include this CDDL HEADER in each 12# file and include the License file at LICENSE.txt. 13# If applicable, add the following below this CDDL HEADER, with the 14# fields enclosed by brackets "[]" replaced with your own identifying 15# information: Portions Copyright [yyyy] [name of copyright owner] 16# 17# CDDL HEADER END 18# 19 20# 21# Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved. 22# 23 24import logging 25 26from requests.exceptions import RequestException 27 28from .command import Command 29from .exitvals import ( 30 CONTINUE_EXITVAL, 31 SUCCESS_EXITVAL, 32 FAILURE_EXITVAL 33) 34from .restful import call_rest_api 35from .patterns import PROJECT_SUBST, COMMAND_PROPERTY, CALL_PROPERTY, URL_SUBST 36import re 37 38 39API_TIMEOUT_PROPERTY = "api_timeout" 40ASYNC_API_TIMEOUT_PROPERTY = "async_api_timeout" 41HEADERS_PROPERTY = "headers" 42METHOD_PROPERTY = "method" 43URI_PROPERTY = "uri" 44 45 46class CommandConfigurationException(Exception): 47 pass 48 49 50def check_call_config(call): 51 """ 52 :param call: dictionary with API call configuration 53 """ 54 if not isinstance(call, dict): 55 raise CommandConfigurationException("call value not a dictionary: {}". 56 format(call)) 57 58 uri = call.get(URI_PROPERTY) 59 if not uri: 60 raise CommandConfigurationException(f"no '{URI_PROPERTY}' key present in {call}") 61 62 method = call.get(METHOD_PROPERTY) 63 if method and method.upper() not in ['GET', 'POST', 'PUT', 'DELETE']: 64 raise CommandConfigurationException(f"invalid HTTP method: {method}") 65 66 headers = call.get(HEADERS_PROPERTY) 67 if headers and not isinstance(headers, dict): 68 raise CommandConfigurationException("headers must be a dictionary") 69 70 call_timeout = call.get(API_TIMEOUT_PROPERTY) 71 if call_timeout: 72 try: 73 int(call_timeout) 74 except ValueError as exc: 75 raise CommandConfigurationException(f"{API_TIMEOUT_PROPERTY} not an integer", exc) 76 77 call_api_timeout = call.get(ASYNC_API_TIMEOUT_PROPERTY) 78 if call_api_timeout: 79 try: 80 int(call_api_timeout) 81 except ValueError as exc: 82 raise CommandConfigurationException(f"{ASYNC_API_TIMEOUT_PROPERTY} not an integer", exc) 83 84 85def check_command_property(command): 86 """ 87 Check if the 'commands' parameter of CommandSequenceBase() has the right structure 88 w.r.t. individual commands. 89 :param command: command element 90 """ 91 92 if not isinstance(command, dict): 93 raise CommandConfigurationException("command '{}' is not a dictionary".format(command)) 94 95 command_value = command.get(COMMAND_PROPERTY) 96 call_value = command.get(CALL_PROPERTY) 97 if command_value is None and call_value is None: 98 raise CommandConfigurationException(f"command dictionary has unknown key: {command}") 99 100 if command_value and not isinstance(command_value, list): 101 raise CommandConfigurationException("command value not a list: {}". 102 format(command_value)) 103 if call_value: 104 check_call_config(call_value) 105 106 107class ApiCall: 108 """ 109 Container class to store properties of API call. 110 """ 111 def __init__(self, call_dict): 112 """ 113 Initialize the object from a dictionary. 114 :param call_dict: dictionary 115 """ 116 if not isinstance(call_dict, dict): 117 raise CommandConfigurationException(f"not a dictionary: {call_dict}") 118 119 self.uri = call_dict.get(URI_PROPERTY) 120 self.method = call_dict.get(METHOD_PROPERTY) 121 if not self.method: 122 self.method = "GET" 123 124 self.data = call_dict.get("data") 125 126 self.headers = call_dict.get(HEADERS_PROPERTY) 127 if not self.headers: 128 self.headers = {} 129 130 self.api_timeout = None 131 call_timeout = call_dict.get(API_TIMEOUT_PROPERTY) 132 if call_timeout: 133 self.api_timeout = call_timeout 134 135 self.async_api_timeout = None 136 call_api_timeout = call_dict.get(ASYNC_API_TIMEOUT_PROPERTY) 137 if call_api_timeout: 138 self.async_api_timeout = call_api_timeout 139 140 141class CommandSequenceBase: 142 """ 143 Wrap the run of a set of Command instances. 144 145 This class intentionally does not contain any logging 146 so that it can be passed through Pool.map(). 147 """ 148 149 def __init__(self, name, commands, loglevel=logging.INFO, cleanup=None, 150 driveon=False, url=None, env=None, http_headers=None, 151 api_timeout=None, async_api_timeout=None): 152 self.name = name 153 154 if commands is None: 155 raise CommandConfigurationException("commands is None") 156 if not isinstance(commands, list): 157 raise CommandConfigurationException("commands is not a list") 158 self.commands = commands 159 for command in self.commands: 160 check_command_property(command) 161 162 self.failed = False 163 self.retcodes = {} 164 self.outputs = {} 165 166 if cleanup and not isinstance(cleanup, list): 167 raise CommandConfigurationException("cleanup is not a list of commands") 168 self.cleanup = cleanup 169 if self.cleanup: 170 for command in self.cleanup: 171 check_command_property(command) 172 173 self.loglevel = loglevel 174 self.driveon = driveon 175 self.env = env 176 self.http_headers = http_headers 177 self.api_timeout = api_timeout 178 self.async_api_timeout = async_api_timeout 179 180 self.url = url 181 182 def __str__(self): 183 return str(self.name) 184 185 def get_cmd_output(self, cmd, indent=""): 186 """ 187 :param cmd: command 188 :param indent: prefix for each line 189 :return: command output as string 190 """ 191 192 str_out = "" 193 for line in self.outputs.get(cmd, []): 194 str_out += '{}{}'.format(indent, line) 195 196 return str_out 197 198 def fill(self, retcodes, outputs, failed): 199 self.retcodes = retcodes 200 self.outputs = outputs 201 self.failed = failed 202 203 204class CommandSequence(CommandSequenceBase): 205 re_program = re.compile('ERROR[:]*\\s+') 206 207 def __init__(self, base): 208 super().__init__(base.name, base.commands, loglevel=base.loglevel, 209 cleanup=base.cleanup, driveon=base.driveon, 210 url=base.url, env=base.env, 211 http_headers=base.http_headers, 212 api_timeout=base.api_timeout, 213 async_api_timeout=base.async_api_timeout) 214 215 self.logger = logging.getLogger(__name__) 216 self.logger.setLevel(base.loglevel) 217 218 def run_command(self, cmd): 219 """ 220 Execute a command and return its return code. 221 """ 222 cmd.execute() 223 self.retcodes[str(cmd)] = cmd.getretcode() 224 self.outputs[str(cmd)] = cmd.getoutput() 225 226 return cmd.getretcode() 227 228 def run(self): 229 """ 230 Run the sequence of commands and capture their output and return code. 231 First command that returns code other than 0 terminates the sequence. 232 If the command has return code 2, the sequence will be terminated 233 however it will not be treated as error (unless the 'driveon' parameter 234 is True). 235 236 If a command contains PROJECT_SUBST pattern, it will be replaced 237 by project name, otherwise project name will be appended to the 238 argument list of the command. 239 240 Any command entry that is a URI, will be used to submit REST API 241 request. 242 """ 243 244 for command in self.commands: 245 if command.get(CALL_PROPERTY): 246 try: 247 call_rest_api(ApiCall(command.get(CALL_PROPERTY)), 248 {PROJECT_SUBST: self.name, 249 URL_SUBST: self.url}, 250 self.http_headers, 251 self.api_timeout, 252 self.async_api_timeout) 253 except RequestException as e: 254 self.logger.error("REST API call {} failed: {}". 255 format(command, e)) 256 self.failed = True 257 self.retcodes[str(command)] = FAILURE_EXITVAL 258 259 break 260 elif command.get(COMMAND_PROPERTY): 261 command_args = command.get(COMMAND_PROPERTY) 262 command = Command(command_args, 263 env_vars=command.get("env"), 264 logger=self.logger, 265 resource_limits=command.get("limits"), 266 args_subst={PROJECT_SUBST: self.name, 267 URL_SUBST: self.url}, 268 args_append=[self.name], excl_subst=True) 269 ret_code = self.run_command(command) 270 271 # If a command exits with non-zero return code, 272 # terminate the sequence of commands. 273 if ret_code != SUCCESS_EXITVAL: 274 if ret_code == CONTINUE_EXITVAL: 275 if not self.driveon: 276 self.logger.debug("command '{}' for project {} " 277 "requested break". 278 format(command, self.name)) 279 self.run_cleanup() 280 else: 281 self.logger.debug("command '{}' for project {} " 282 "requested break however " 283 "the 'driveon' option is set " 284 "so driving on.". 285 format(command, self.name)) 286 continue 287 else: 288 self.logger.error("command '{}' for project {} failed " 289 "with code {}, breaking". 290 format(command, self.name, ret_code)) 291 self.failed = True 292 self.run_cleanup() 293 294 break 295 else: 296 raise Exception(f"unknown command: {command}") 297 298 def run_cleanup(self): 299 """ 300 Call cleanup sequence in case the command sequence failed 301 or termination was requested. 302 """ 303 if self.cleanup is None: 304 return 305 306 for cleanup_cmd in self.cleanup: 307 if cleanup_cmd.get(CALL_PROPERTY): 308 try: 309 call_rest_api(ApiCall(cleanup_cmd.get(CALL_PROPERTY)), 310 {PROJECT_SUBST: self.name, 311 URL_SUBST: self.url}, 312 self.http_headers, 313 self.api_timeout, 314 self.async_api_timeout) 315 except RequestException as e: 316 self.logger.error("API call {} failed: {}". 317 format(cleanup_cmd, e)) 318 elif cleanup_cmd.get(COMMAND_PROPERTY): 319 command_args = cleanup_cmd.get(COMMAND_PROPERTY) 320 self.logger.debug("Running cleanup command '{}'". 321 format(command_args)) 322 cmd = Command(command_args, 323 logger=self.logger, 324 args_subst={PROJECT_SUBST: self.name, 325 URL_SUBST: self.url}, 326 args_append=[self.name], excl_subst=True) 327 cmd.execute() 328 if cmd.getretcode() != SUCCESS_EXITVAL: 329 self.logger.error("cleanup command '{}' failed with " 330 "code {}". 331 format(cmd.cmd, cmd.getretcode())) 332 self.logger.info('output: {}'.format(cmd.getoutputstr())) 333 else: 334 raise Exception(f"unknown type of action: {cleanup_cmd}") 335 336 def print_outputs(self, logger, loglevel=logging.INFO, lines=False): 337 """ 338 Print command outputs. 339 """ 340 341 logger.debug("Output for project '{}':".format(self.name)) 342 for cmd in self.outputs.keys(): 343 if self.outputs[cmd] and len(self.outputs[cmd]) > 0: 344 if lines: 345 logger.log(loglevel, "Output from '{}':".format(cmd)) 346 logger.log(loglevel, '{}'.format(self.get_cmd_output(cmd))) 347 else: 348 logger.log(loglevel, "'{}': {}". 349 format(cmd, self.outputs[cmd])) 350 351 def check(self, ignore_errors): 352 """ 353 Check the output of the commands and perform logging. 354 355 Return SUCCESS_EXITVAL on success, 1 if error was detected. 356 """ 357 358 ret = SUCCESS_EXITVAL 359 self.print_outputs(self.logger, loglevel=logging.DEBUG) 360 361 if ignore_errors and self.name in ignore_errors: 362 self.logger.debug("errors of project '{}' ignored". 363 format(self.name)) 364 return 365 366 self.logger.debug("retcodes = {}".format(self.retcodes)) 367 if any(rv != SUCCESS_EXITVAL and rv != CONTINUE_EXITVAL 368 for rv in self.retcodes.values()): 369 ret = 1 370 self.logger.error("processing of project '{}' failed". 371 format(self)) 372 indent = " " 373 self.logger.error("{}failed commands:".format(indent)) 374 failed_cmds = {k: v for k, v in 375 self.retcodes.items() if v != SUCCESS_EXITVAL} 376 indent = " " 377 for cmd in failed_cmds.keys(): 378 self.logger.error("{}'{}': {}". 379 format(indent, cmd, failed_cmds[cmd])) 380 out = self.get_cmd_output(cmd, 381 indent=indent + " ") 382 if out: 383 self.logger.error(out) 384 self.logger.error("") 385 386 errored_cmds = {k: v for k, v in self.outputs.items() 387 if self.re_program.match(str(v))} 388 if len(errored_cmds) > 0: 389 ret = 1 390 self.logger.error("Command output in project '{}'" 391 " contains errors:".format(self.name)) 392 indent = " " 393 for cmd in errored_cmds.keys(): 394 self.logger.error("{}{}".format(indent, cmd)) 395 out = self.get_cmd_output(cmd, 396 indent=indent + " ") 397 if out: 398 self.logger.error(out) 399 self.logger.error("") 400 401 return ret 402