12d57dc69SVladimir Kotal# 22d57dc69SVladimir Kotal# CDDL HEADER START 32d57dc69SVladimir Kotal# 42d57dc69SVladimir Kotal# The contents of this file are subject to the terms of the 52d57dc69SVladimir Kotal# Common Development and Distribution License (the "License"). 62d57dc69SVladimir Kotal# You may not use this file except in compliance with the License. 72d57dc69SVladimir Kotal# 82d57dc69SVladimir Kotal# See LICENSE.txt included in this distribution for the specific 92d57dc69SVladimir Kotal# language governing permissions and limitations under the License. 102d57dc69SVladimir Kotal# 112d57dc69SVladimir Kotal# When distributing Covered Code, include this CDDL HEADER in each 122d57dc69SVladimir Kotal# file and include the License file at LICENSE.txt. 132d57dc69SVladimir Kotal# If applicable, add the following below this CDDL HEADER, with the 142d57dc69SVladimir Kotal# fields enclosed by brackets "[]" replaced with your own identifying 152d57dc69SVladimir Kotal# information: Portions Copyright [yyyy] [name of copyright owner] 162d57dc69SVladimir Kotal# 172d57dc69SVladimir Kotal# CDDL HEADER END 182d57dc69SVladimir Kotal# 192d57dc69SVladimir Kotal 202d57dc69SVladimir Kotal# 2180df37bbSVladimir Kotal# Copyright (c) 2017, 2022, Oracle and/or its affiliates. All rights reserved. 222d57dc69SVladimir Kotal# 232d57dc69SVladimir Kotal 242d57dc69SVladimir Kotalimport logging 252d57dc69SVladimir Kotal 2696aeefc4SVladimir Kotalfrom requests.exceptions import RequestException 272d57dc69SVladimir Kotal 282d57dc69SVladimir Kotalfrom .command import Command 292d57dc69SVladimir Kotalfrom .exitvals import ( 302d57dc69SVladimir Kotal CONTINUE_EXITVAL, 312d57dc69SVladimir Kotal SUCCESS_EXITVAL, 322d57dc69SVladimir Kotal FAILURE_EXITVAL 332d57dc69SVladimir Kotal) 342d57dc69SVladimir Kotalfrom .restful import call_rest_api 35c41895f8SVladimir Kotalfrom .patterns import PROJECT_SUBST, COMMAND_PROPERTY, CALL_PROPERTY, URL_SUBST 362d57dc69SVladimir Kotalimport re 372d57dc69SVladimir Kotal 382d57dc69SVladimir Kotal 3980df37bbSVladimir Kotalclass CommandConfigurationException(Exception): 4080df37bbSVladimir Kotal pass 4180df37bbSVladimir Kotal 4280df37bbSVladimir Kotal 4380df37bbSVladimir Kotaldef check_command_property(command): 4480df37bbSVladimir Kotal """ 4580df37bbSVladimir Kotal Check if the 'commands' parameter of CommandSequenceBase() has the right structure 4680df37bbSVladimir Kotal w.r.t. individual commands. 4780df37bbSVladimir Kotal :param command: command element 4880df37bbSVladimir Kotal """ 4980df37bbSVladimir Kotal if not isinstance(command, dict): 5077cacb4dSVladimir Kotal raise CommandConfigurationException("command '{}' is not a dictionary".format(command)) 5180df37bbSVladimir Kotal 5280df37bbSVladimir Kotal command_value = command.get(COMMAND_PROPERTY) 53c41895f8SVladimir Kotal call_value = command.get(CALL_PROPERTY) 54c41895f8SVladimir Kotal if command_value is None and call_value is None: 55c41895f8SVladimir Kotal raise CommandConfigurationException(f"command dictionary has unknown key: {command}") 5680df37bbSVladimir Kotal 57c41895f8SVladimir Kotal if command_value and not isinstance(command_value, list): 5880df37bbSVladimir Kotal raise CommandConfigurationException("command value not a list: {}". 5980df37bbSVladimir Kotal format(command_value)) 60c41895f8SVladimir Kotal if call_value and not isinstance(call_value, dict): 61c41895f8SVladimir Kotal raise CommandConfigurationException("call value not a dictionary: {}". 62c41895f8SVladimir Kotal format(call_value)) 6380df37bbSVladimir Kotal 6480df37bbSVladimir Kotal 652d57dc69SVladimir Kotalclass CommandSequenceBase: 662d57dc69SVladimir Kotal """ 672d57dc69SVladimir Kotal Wrap the run of a set of Command instances. 682d57dc69SVladimir Kotal 692d57dc69SVladimir Kotal This class intentionally does not contain any logging 702d57dc69SVladimir Kotal so that it can be passed through Pool.map(). 712d57dc69SVladimir Kotal """ 722d57dc69SVladimir Kotal 732d57dc69SVladimir Kotal def __init__(self, name, commands, loglevel=logging.INFO, cleanup=None, 74732be1c2SVladimir Kotal driveon=False, url=None, env=None, http_headers=None, 75732be1c2SVladimir Kotal api_timeout=None): 762d57dc69SVladimir Kotal self.name = name 7780df37bbSVladimir Kotal 7880df37bbSVladimir Kotal if commands is None: 7980df37bbSVladimir Kotal raise CommandConfigurationException("commands is None") 8080df37bbSVladimir Kotal if not isinstance(commands, list): 8180df37bbSVladimir Kotal raise CommandConfigurationException("commands is not a list") 822d57dc69SVladimir Kotal self.commands = commands 8380df37bbSVladimir Kotal for command in self.commands: 8480df37bbSVladimir Kotal check_command_property(command) 8580df37bbSVladimir Kotal 862d57dc69SVladimir Kotal self.failed = False 872d57dc69SVladimir Kotal self.retcodes = {} 882d57dc69SVladimir Kotal self.outputs = {} 892d57dc69SVladimir Kotal 9080df37bbSVladimir Kotal if cleanup and not isinstance(cleanup, list): 9180df37bbSVladimir Kotal raise CommandConfigurationException("cleanup is not a list of commands") 922d57dc69SVladimir Kotal self.cleanup = cleanup 9380df37bbSVladimir Kotal if self.cleanup: 9480df37bbSVladimir Kotal for command in self.cleanup: 9580df37bbSVladimir Kotal check_command_property(command) 9680df37bbSVladimir Kotal 972d57dc69SVladimir Kotal self.loglevel = loglevel 982d57dc69SVladimir Kotal self.driveon = driveon 99b2d29daeSVladimir Kotal self.env = env 10089229afdSVladimir Kotal self.http_headers = http_headers 101732be1c2SVladimir Kotal self.api_timeout = api_timeout 1022d57dc69SVladimir Kotal 1032d97c0a2SVladimir Kotal self.url = url 1042d97c0a2SVladimir Kotal 1052d57dc69SVladimir Kotal def __str__(self): 1062d57dc69SVladimir Kotal return str(self.name) 1072d57dc69SVladimir Kotal 1082d57dc69SVladimir Kotal def get_cmd_output(self, cmd, indent=""): 109b2d29daeSVladimir Kotal """ 110b2d29daeSVladimir Kotal :param cmd: command 111b2d29daeSVladimir Kotal :param indent: prefix for each line 112b2d29daeSVladimir Kotal :return: command output as string 113b2d29daeSVladimir Kotal """ 114b2d29daeSVladimir Kotal 11580df37bbSVladimir Kotal str_out = "" 1162d57dc69SVladimir Kotal for line in self.outputs.get(cmd, []): 11780df37bbSVladimir Kotal str_out += '{}{}'.format(indent, line) 1182d57dc69SVladimir Kotal 11980df37bbSVladimir Kotal return str_out 1202d57dc69SVladimir Kotal 1212d57dc69SVladimir Kotal def fill(self, retcodes, outputs, failed): 1222d57dc69SVladimir Kotal self.retcodes = retcodes 1232d57dc69SVladimir Kotal self.outputs = outputs 1242d57dc69SVladimir Kotal self.failed = failed 1252d57dc69SVladimir Kotal 1262d57dc69SVladimir Kotal 1272d57dc69SVladimir Kotalclass CommandSequence(CommandSequenceBase): 1282d57dc69SVladimir Kotal re_program = re.compile('ERROR[:]*\\s+') 1292d57dc69SVladimir Kotal 1302d57dc69SVladimir Kotal def __init__(self, base): 1312d57dc69SVladimir Kotal super().__init__(base.name, base.commands, loglevel=base.loglevel, 1322d97c0a2SVladimir Kotal cleanup=base.cleanup, driveon=base.driveon, 133b369c884SVladimir Kotal url=base.url, env=base.env, 134732be1c2SVladimir Kotal http_headers=base.http_headers, 135732be1c2SVladimir Kotal api_timeout=base.api_timeout) 1362d57dc69SVladimir Kotal 1372d57dc69SVladimir Kotal self.logger = logging.getLogger(__name__) 1382d57dc69SVladimir Kotal self.logger.setLevel(base.loglevel) 1392d57dc69SVladimir Kotal 1402d57dc69SVladimir Kotal def run_command(self, cmd): 1412d57dc69SVladimir Kotal """ 1422d57dc69SVladimir Kotal Execute a command and return its return code. 1432d57dc69SVladimir Kotal """ 1442d57dc69SVladimir Kotal cmd.execute() 1452d57dc69SVladimir Kotal self.retcodes[str(cmd)] = cmd.getretcode() 1462d57dc69SVladimir Kotal self.outputs[str(cmd)] = cmd.getoutput() 1472d57dc69SVladimir Kotal 1482d57dc69SVladimir Kotal return cmd.getretcode() 1492d57dc69SVladimir Kotal 1502d57dc69SVladimir Kotal def run(self): 1512d57dc69SVladimir Kotal """ 1522d57dc69SVladimir Kotal Run the sequence of commands and capture their output and return code. 1532d57dc69SVladimir Kotal First command that returns code other than 0 terminates the sequence. 1542d57dc69SVladimir Kotal If the command has return code 2, the sequence will be terminated 1552d57dc69SVladimir Kotal however it will not be treated as error (unless the 'driveon' parameter 1562d57dc69SVladimir Kotal is True). 1572d57dc69SVladimir Kotal 1582d57dc69SVladimir Kotal If a command contains PROJECT_SUBST pattern, it will be replaced 1592d57dc69SVladimir Kotal by project name, otherwise project name will be appended to the 1602d57dc69SVladimir Kotal argument list of the command. 1612d57dc69SVladimir Kotal 162*519c2abbSVladimir Kotal Any command entry that is a URI, will be used to submit REST API 1632d57dc69SVladimir Kotal request. 1642d57dc69SVladimir Kotal """ 1652d57dc69SVladimir Kotal 1662d57dc69SVladimir Kotal for command in self.commands: 167c41895f8SVladimir Kotal if command.get(CALL_PROPERTY): 1682d57dc69SVladimir Kotal try: 169c41895f8SVladimir Kotal call_rest_api(command.get(CALL_PROPERTY), 170c41895f8SVladimir Kotal {PROJECT_SUBST: self.name, 17189229afdSVladimir Kotal URL_SUBST: self.url}, 172732be1c2SVladimir Kotal self.http_headers, self.api_timeout) 17396aeefc4SVladimir Kotal except RequestException as e: 174c41895f8SVladimir Kotal self.logger.error("REST API call {} failed: {}". 1752d57dc69SVladimir Kotal format(command, e)) 1762d57dc69SVladimir Kotal self.failed = True 1772d57dc69SVladimir Kotal self.retcodes[str(command)] = FAILURE_EXITVAL 1782d57dc69SVladimir Kotal 1792d57dc69SVladimir Kotal break 180c41895f8SVladimir Kotal elif command.get(COMMAND_PROPERTY): 1812d57dc69SVladimir Kotal command_args = command.get(COMMAND_PROPERTY) 1822d57dc69SVladimir Kotal command = Command(command_args, 1832d57dc69SVladimir Kotal env_vars=command.get("env"), 184b2d29daeSVladimir Kotal logger=self.logger, 1852d57dc69SVladimir Kotal resource_limits=command.get("limits"), 1862d97c0a2SVladimir Kotal args_subst={PROJECT_SUBST: self.name, 1872d97c0a2SVladimir Kotal URL_SUBST: self.url}, 1882d57dc69SVladimir Kotal args_append=[self.name], excl_subst=True) 189c41895f8SVladimir Kotal ret_code = self.run_command(command) 1902d57dc69SVladimir Kotal 1912d57dc69SVladimir Kotal # If a command exits with non-zero return code, 1922d57dc69SVladimir Kotal # terminate the sequence of commands. 193c41895f8SVladimir Kotal if ret_code != SUCCESS_EXITVAL: 194c41895f8SVladimir Kotal if ret_code == CONTINUE_EXITVAL: 1952d57dc69SVladimir Kotal if not self.driveon: 1962d57dc69SVladimir Kotal self.logger.debug("command '{}' for project {} " 1972d57dc69SVladimir Kotal "requested break". 1982d57dc69SVladimir Kotal format(command, self.name)) 1992d57dc69SVladimir Kotal self.run_cleanup() 2002d57dc69SVladimir Kotal else: 2012d57dc69SVladimir Kotal self.logger.debug("command '{}' for project {} " 2022d57dc69SVladimir Kotal "requested break however " 2032d57dc69SVladimir Kotal "the 'driveon' option is set " 2042d57dc69SVladimir Kotal "so driving on.". 2052d57dc69SVladimir Kotal format(command, self.name)) 2062d57dc69SVladimir Kotal continue 2072d57dc69SVladimir Kotal else: 208923e5a3aSVladimir Kotal self.logger.error("command '{}' for project {} failed " 209923e5a3aSVladimir Kotal "with code {}, breaking". 210c41895f8SVladimir Kotal format(command, self.name, ret_code)) 2112d57dc69SVladimir Kotal self.failed = True 2122d57dc69SVladimir Kotal self.run_cleanup() 2132d57dc69SVladimir Kotal 2142d57dc69SVladimir Kotal break 215c41895f8SVladimir Kotal else: 216c41895f8SVladimir Kotal raise Exception(f"unknown command: {command}") 2172d57dc69SVladimir Kotal 2182d57dc69SVladimir Kotal def run_cleanup(self): 2192d57dc69SVladimir Kotal """ 2202d57dc69SVladimir Kotal Call cleanup sequence in case the command sequence failed 2212d57dc69SVladimir Kotal or termination was requested. 2222d57dc69SVladimir Kotal """ 2232d57dc69SVladimir Kotal if self.cleanup is None: 2242d57dc69SVladimir Kotal return 2252d57dc69SVladimir Kotal 2262d57dc69SVladimir Kotal for cleanup_cmd in self.cleanup: 227c41895f8SVladimir Kotal if cleanup_cmd.get(CALL_PROPERTY): 2282d57dc69SVladimir Kotal try: 229c41895f8SVladimir Kotal call_rest_api(cleanup_cmd.get(CALL_PROPERTY), 230c41895f8SVladimir Kotal {PROJECT_SUBST: self.name, 231b369c884SVladimir Kotal URL_SUBST: self.url}, 232732be1c2SVladimir Kotal self.http_headers, self.api_timeout) 23396aeefc4SVladimir Kotal except RequestException as e: 234c41895f8SVladimir Kotal self.logger.error("API call {} failed: {}". 2352d57dc69SVladimir Kotal format(cleanup_cmd, e)) 236c41895f8SVladimir Kotal elif cleanup_cmd.get(COMMAND_PROPERTY): 2372d57dc69SVladimir Kotal command_args = cleanup_cmd.get(COMMAND_PROPERTY) 2382d57dc69SVladimir Kotal self.logger.debug("Running cleanup command '{}'". 2392d57dc69SVladimir Kotal format(command_args)) 2402d57dc69SVladimir Kotal cmd = Command(command_args, 241b2d29daeSVladimir Kotal logger=self.logger, 2422d97c0a2SVladimir Kotal args_subst={PROJECT_SUBST: self.name, 2432d97c0a2SVladimir Kotal URL_SUBST: self.url}, 2442d57dc69SVladimir Kotal args_append=[self.name], excl_subst=True) 2452d57dc69SVladimir Kotal cmd.execute() 2462d57dc69SVladimir Kotal if cmd.getretcode() != SUCCESS_EXITVAL: 2472d57dc69SVladimir Kotal self.logger.error("cleanup command '{}' failed with " 2482d57dc69SVladimir Kotal "code {}". 2492d57dc69SVladimir Kotal format(cmd.cmd, cmd.getretcode())) 2502d57dc69SVladimir Kotal self.logger.info('output: {}'.format(cmd.getoutputstr())) 251c41895f8SVladimir Kotal else: 252c41895f8SVladimir Kotal raise Exception(f"unknown type of action: {cleanup_cmd}") 2532d57dc69SVladimir Kotal 254b2d29daeSVladimir Kotal def print_outputs(self, logger, loglevel=logging.INFO, lines=False): 255b2d29daeSVladimir Kotal """ 256b2d29daeSVladimir Kotal Print command outputs. 257b2d29daeSVladimir Kotal """ 258b2d29daeSVladimir Kotal 259b2d29daeSVladimir Kotal logger.debug("Output for project '{}':".format(self.name)) 260b2d29daeSVladimir Kotal for cmd in self.outputs.keys(): 261b2d29daeSVladimir Kotal if self.outputs[cmd] and len(self.outputs[cmd]) > 0: 262b2d29daeSVladimir Kotal if lines: 263b2d29daeSVladimir Kotal logger.log(loglevel, "Output from '{}':".format(cmd)) 264b2d29daeSVladimir Kotal logger.log(loglevel, '{}'.format(self.get_cmd_output(cmd))) 265b2d29daeSVladimir Kotal else: 266b2d29daeSVladimir Kotal logger.log(loglevel, "'{}': {}". 267b2d29daeSVladimir Kotal format(cmd, self.outputs[cmd])) 268b2d29daeSVladimir Kotal 2692d57dc69SVladimir Kotal def check(self, ignore_errors): 2702d57dc69SVladimir Kotal """ 2712d57dc69SVladimir Kotal Check the output of the commands and perform logging. 2722d57dc69SVladimir Kotal 2735a30ebc3SVladimir Kotal Return SUCCESS_EXITVAL on success, 1 if error was detected. 2742d57dc69SVladimir Kotal """ 2752d57dc69SVladimir Kotal 2762d57dc69SVladimir Kotal ret = SUCCESS_EXITVAL 277b2d29daeSVladimir Kotal self.print_outputs(self.logger, loglevel=logging.DEBUG) 2782d57dc69SVladimir Kotal 2795a30ebc3SVladimir Kotal if ignore_errors and self.name in ignore_errors: 2802d57dc69SVladimir Kotal self.logger.debug("errors of project '{}' ignored". 2812d57dc69SVladimir Kotal format(self.name)) 2822d57dc69SVladimir Kotal return 2832d57dc69SVladimir Kotal 2842d57dc69SVladimir Kotal self.logger.debug("retcodes = {}".format(self.retcodes)) 2852d57dc69SVladimir Kotal if any(rv != SUCCESS_EXITVAL and rv != CONTINUE_EXITVAL 2862d57dc69SVladimir Kotal for rv in self.retcodes.values()): 2872d57dc69SVladimir Kotal ret = 1 2882d57dc69SVladimir Kotal self.logger.error("processing of project '{}' failed". 2892d57dc69SVladimir Kotal format(self)) 2902d57dc69SVladimir Kotal indent = " " 2912d57dc69SVladimir Kotal self.logger.error("{}failed commands:".format(indent)) 2922d57dc69SVladimir Kotal failed_cmds = {k: v for k, v in 2932d57dc69SVladimir Kotal self.retcodes.items() if v != SUCCESS_EXITVAL} 2942d57dc69SVladimir Kotal indent = " " 2952d57dc69SVladimir Kotal for cmd in failed_cmds.keys(): 2962d57dc69SVladimir Kotal self.logger.error("{}'{}': {}". 2972d57dc69SVladimir Kotal format(indent, cmd, failed_cmds[cmd])) 2982d57dc69SVladimir Kotal out = self.get_cmd_output(cmd, 2992d57dc69SVladimir Kotal indent=indent + " ") 3002d57dc69SVladimir Kotal if out: 3012d57dc69SVladimir Kotal self.logger.error(out) 3022d57dc69SVladimir Kotal self.logger.error("") 3032d57dc69SVladimir Kotal 3042d57dc69SVladimir Kotal errored_cmds = {k: v for k, v in self.outputs.items() 3052d57dc69SVladimir Kotal if self.re_program.match(str(v))} 3062d57dc69SVladimir Kotal if len(errored_cmds) > 0: 3072d57dc69SVladimir Kotal ret = 1 3082d57dc69SVladimir Kotal self.logger.error("Command output in project '{}'" 3092d57dc69SVladimir Kotal " contains errors:".format(self.name)) 3102d57dc69SVladimir Kotal indent = " " 3112d57dc69SVladimir Kotal for cmd in errored_cmds.keys(): 3122d57dc69SVladimir Kotal self.logger.error("{}{}".format(indent, cmd)) 3132d57dc69SVladimir Kotal out = self.get_cmd_output(cmd, 3142d57dc69SVladimir Kotal indent=indent + " ") 3152d57dc69SVladimir Kotal if out: 3162d57dc69SVladimir Kotal self.logger.error(out) 3172d57dc69SVladimir Kotal self.logger.error("") 3182d57dc69SVladimir Kotal 3192d57dc69SVladimir Kotal return ret 320