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# 21*2d97c0a2SVladimir Kotal# Copyright (c) 2017, 2021, Oracle and/or its affiliates. All rights reserved. 222d57dc69SVladimir Kotal# 232d57dc69SVladimir Kotal 242d57dc69SVladimir Kotalimport logging 252d57dc69SVladimir Kotal 262d57dc69SVladimir Kotalfrom requests.exceptions import HTTPError 272d57dc69SVladimir Kotal 282d57dc69SVladimir Kotalfrom .command import Command 292d57dc69SVladimir Kotalfrom .utils import is_web_uri 302d57dc69SVladimir Kotalfrom .exitvals import ( 312d57dc69SVladimir Kotal CONTINUE_EXITVAL, 322d57dc69SVladimir Kotal SUCCESS_EXITVAL, 332d57dc69SVladimir Kotal FAILURE_EXITVAL 342d57dc69SVladimir Kotal) 352d57dc69SVladimir Kotalfrom .restful import call_rest_api 36*2d97c0a2SVladimir Kotalfrom .patterns import PROJECT_SUBST, COMMAND_PROPERTY, URL_SUBST 372d57dc69SVladimir Kotalimport re 382d57dc69SVladimir Kotal 392d57dc69SVladimir Kotal 402d57dc69SVladimir Kotalclass CommandSequenceBase: 412d57dc69SVladimir Kotal """ 422d57dc69SVladimir Kotal Wrap the run of a set of Command instances. 432d57dc69SVladimir Kotal 442d57dc69SVladimir Kotal This class intentionally does not contain any logging 452d57dc69SVladimir Kotal so that it can be passed through Pool.map(). 462d57dc69SVladimir Kotal """ 472d57dc69SVladimir Kotal 482d57dc69SVladimir Kotal def __init__(self, name, commands, loglevel=logging.INFO, cleanup=None, 49*2d97c0a2SVladimir Kotal driveon=False, url=None): 502d57dc69SVladimir Kotal self.name = name 512d57dc69SVladimir Kotal self.commands = commands 522d57dc69SVladimir Kotal self.failed = False 532d57dc69SVladimir Kotal self.retcodes = {} 542d57dc69SVladimir Kotal self.outputs = {} 552d57dc69SVladimir Kotal if cleanup and not isinstance(cleanup, list): 562d57dc69SVladimir Kotal raise Exception("cleanup is not a list of commands") 572d57dc69SVladimir Kotal 582d57dc69SVladimir Kotal self.cleanup = cleanup 592d57dc69SVladimir Kotal self.loglevel = loglevel 602d57dc69SVladimir Kotal self.driveon = driveon 612d57dc69SVladimir Kotal 62*2d97c0a2SVladimir Kotal self.url = url 63*2d97c0a2SVladimir Kotal 642d57dc69SVladimir Kotal def __str__(self): 652d57dc69SVladimir Kotal return str(self.name) 662d57dc69SVladimir Kotal 672d57dc69SVladimir Kotal def get_cmd_output(self, cmd, indent=""): 682d57dc69SVladimir Kotal str = "" 692d57dc69SVladimir Kotal for line in self.outputs.get(cmd, []): 702d57dc69SVladimir Kotal str += '{}{}'.format(indent, line) 712d57dc69SVladimir Kotal 722d57dc69SVladimir Kotal return str 732d57dc69SVladimir Kotal 742d57dc69SVladimir Kotal def fill(self, retcodes, outputs, failed): 752d57dc69SVladimir Kotal self.retcodes = retcodes 762d57dc69SVladimir Kotal self.outputs = outputs 772d57dc69SVladimir Kotal self.failed = failed 782d57dc69SVladimir Kotal 792d57dc69SVladimir Kotal 802d57dc69SVladimir Kotalclass CommandSequence(CommandSequenceBase): 812d57dc69SVladimir Kotal 822d57dc69SVladimir Kotal re_program = re.compile('ERROR[:]*\\s+') 832d57dc69SVladimir Kotal 842d57dc69SVladimir Kotal def __init__(self, base): 852d57dc69SVladimir Kotal super().__init__(base.name, base.commands, loglevel=base.loglevel, 86*2d97c0a2SVladimir Kotal cleanup=base.cleanup, driveon=base.driveon, 87*2d97c0a2SVladimir Kotal url=base.url) 882d57dc69SVladimir Kotal 892d57dc69SVladimir Kotal self.logger = logging.getLogger(__name__) 902d57dc69SVladimir Kotal self.logger.setLevel(base.loglevel) 912d57dc69SVladimir Kotal 922d57dc69SVladimir Kotal def run_command(self, cmd): 932d57dc69SVladimir Kotal """ 942d57dc69SVladimir Kotal Execute a command and return its return code. 952d57dc69SVladimir Kotal """ 962d57dc69SVladimir Kotal cmd.execute() 972d57dc69SVladimir Kotal self.retcodes[str(cmd)] = cmd.getretcode() 982d57dc69SVladimir Kotal self.outputs[str(cmd)] = cmd.getoutput() 992d57dc69SVladimir Kotal 1002d57dc69SVladimir Kotal return cmd.getretcode() 1012d57dc69SVladimir Kotal 1022d57dc69SVladimir Kotal def run(self): 1032d57dc69SVladimir Kotal """ 1042d57dc69SVladimir Kotal Run the sequence of commands and capture their output and return code. 1052d57dc69SVladimir Kotal First command that returns code other than 0 terminates the sequence. 1062d57dc69SVladimir Kotal If the command has return code 2, the sequence will be terminated 1072d57dc69SVladimir Kotal however it will not be treated as error (unless the 'driveon' parameter 1082d57dc69SVladimir Kotal is True). 1092d57dc69SVladimir Kotal 1102d57dc69SVladimir Kotal If a command contains PROJECT_SUBST pattern, it will be replaced 1112d57dc69SVladimir Kotal by project name, otherwise project name will be appended to the 1122d57dc69SVladimir Kotal argument list of the command. 1132d57dc69SVladimir Kotal 1142d57dc69SVladimir Kotal Any command entry that is a URI, will be used to submit RESTful API 1152d57dc69SVladimir Kotal request. 1162d57dc69SVladimir Kotal """ 1172d57dc69SVladimir Kotal 1182d57dc69SVladimir Kotal for command in self.commands: 119*2d97c0a2SVladimir Kotal cmd_value = command.get(COMMAND_PROPERTY)[0] 120*2d97c0a2SVladimir Kotal if cmd_value.startswith(URL_SUBST) or is_web_uri(cmd_value): 1212d57dc69SVladimir Kotal try: 122*2d97c0a2SVladimir Kotal call_rest_api(command, {PROJECT_SUBST: self.name, 123*2d97c0a2SVladimir Kotal URL_SUBST: self.url}) 1242d57dc69SVladimir Kotal except HTTPError as e: 1252d57dc69SVladimir Kotal self.logger.error("RESTful command {} failed: {}". 1262d57dc69SVladimir Kotal format(command, e)) 1272d57dc69SVladimir Kotal self.failed = True 1282d57dc69SVladimir Kotal self.retcodes[str(command)] = FAILURE_EXITVAL 1292d57dc69SVladimir Kotal 1302d57dc69SVladimir Kotal break 1312d57dc69SVladimir Kotal else: 1322d57dc69SVladimir Kotal command_args = command.get(COMMAND_PROPERTY) 1332d57dc69SVladimir Kotal command = Command(command_args, 1342d57dc69SVladimir Kotal env_vars=command.get("env"), 1352d57dc69SVladimir Kotal resource_limits=command.get("limits"), 136*2d97c0a2SVladimir Kotal args_subst={PROJECT_SUBST: self.name, 137*2d97c0a2SVladimir Kotal URL_SUBST: self.url}, 1382d57dc69SVladimir Kotal args_append=[self.name], excl_subst=True) 1392d57dc69SVladimir Kotal retcode = self.run_command(command) 1402d57dc69SVladimir Kotal 1412d57dc69SVladimir Kotal # If a command exits with non-zero return code, 1422d57dc69SVladimir Kotal # terminate the sequence of commands. 1432d57dc69SVladimir Kotal if retcode != SUCCESS_EXITVAL: 1442d57dc69SVladimir Kotal if retcode == CONTINUE_EXITVAL: 1452d57dc69SVladimir Kotal if not self.driveon: 1462d57dc69SVladimir Kotal self.logger.debug("command '{}' for project {} " 1472d57dc69SVladimir Kotal "requested break". 1482d57dc69SVladimir Kotal format(command, self.name)) 1492d57dc69SVladimir Kotal self.run_cleanup() 1502d57dc69SVladimir Kotal else: 1512d57dc69SVladimir Kotal self.logger.debug("command '{}' for project {} " 1522d57dc69SVladimir Kotal "requested break however " 1532d57dc69SVladimir Kotal "the 'driveon' option is set " 1542d57dc69SVladimir Kotal "so driving on.". 1552d57dc69SVladimir Kotal format(command, self.name)) 1562d57dc69SVladimir Kotal continue 1572d57dc69SVladimir Kotal else: 158d7281829SVladimir Kotal if self.driveon: 159d7281829SVladimir Kotal do = "driving on" 160d7281829SVladimir Kotal self.logger.debug("command '{}' for project '{}'' failed " 161d7281829SVladimir Kotal "with code {}, {}". 162d7281829SVladimir Kotal format(command, self.name, retcode, do)) 163d7281829SVladimir Kotal else: 164d7281829SVladimir Kotal do = "breaking" 165d7281829SVladimir Kotal self.logger.error("command '{}' for project '{}'' failed " 166d7281829SVladimir Kotal "with code {}, {}". 167d7281829SVladimir Kotal format(command, self.name, retcode, do)) 168d7281829SVladimir Kotal 169d7281829SVladimir Kotal if self.driveon: 170d7281829SVladimir Kotal continue 171d7281829SVladimir Kotal else: 1722d57dc69SVladimir Kotal self.failed = True 1732d57dc69SVladimir Kotal self.run_cleanup() 1742d57dc69SVladimir Kotal 1752d57dc69SVladimir Kotal break 1762d57dc69SVladimir Kotal 1772d57dc69SVladimir Kotal def run_cleanup(self): 1782d57dc69SVladimir Kotal """ 1792d57dc69SVladimir Kotal Call cleanup sequence in case the command sequence failed 1802d57dc69SVladimir Kotal or termination was requested. 1812d57dc69SVladimir Kotal """ 1822d57dc69SVladimir Kotal if self.cleanup is None: 1832d57dc69SVladimir Kotal return 1842d57dc69SVladimir Kotal 1852d57dc69SVladimir Kotal for cleanup_cmd in self.cleanup: 186*2d97c0a2SVladimir Kotal arg0 = cleanup_cmd.get(COMMAND_PROPERTY)[0] 187*2d97c0a2SVladimir Kotal if arg0.startswith(URL_SUBST) or is_web_uri(arg0): 1882d57dc69SVladimir Kotal try: 189*2d97c0a2SVladimir Kotal call_rest_api(cleanup_cmd, {PROJECT_SUBST: self.name, 190*2d97c0a2SVladimir Kotal URL_SUBST: self.url}) 1912d57dc69SVladimir Kotal except HTTPError as e: 1922d57dc69SVladimir Kotal self.logger.error("RESTful command {} failed: {}". 1932d57dc69SVladimir Kotal format(cleanup_cmd, e)) 1942d57dc69SVladimir Kotal else: 1952d57dc69SVladimir Kotal command_args = cleanup_cmd.get(COMMAND_PROPERTY) 1962d57dc69SVladimir Kotal self.logger.debug("Running cleanup command '{}'". 1972d57dc69SVladimir Kotal format(command_args)) 1982d57dc69SVladimir Kotal cmd = Command(command_args, 199*2d97c0a2SVladimir Kotal args_subst={PROJECT_SUBST: self.name, 200*2d97c0a2SVladimir Kotal URL_SUBST: self.url}, 2012d57dc69SVladimir Kotal args_append=[self.name], excl_subst=True) 2022d57dc69SVladimir Kotal cmd.execute() 2032d57dc69SVladimir Kotal if cmd.getretcode() != SUCCESS_EXITVAL: 2042d57dc69SVladimir Kotal self.logger.error("cleanup command '{}' failed with " 2052d57dc69SVladimir Kotal "code {}". 2062d57dc69SVladimir Kotal format(cmd.cmd, cmd.getretcode())) 2072d57dc69SVladimir Kotal self.logger.info('output: {}'.format(cmd.getoutputstr())) 2082d57dc69SVladimir Kotal 2092d57dc69SVladimir Kotal def check(self, ignore_errors): 2102d57dc69SVladimir Kotal """ 2112d57dc69SVladimir Kotal Check the output of the commands and perform logging. 2122d57dc69SVladimir Kotal 2132d57dc69SVladimir Kotal Return 0 on success, 1 if error was detected. 2142d57dc69SVladimir Kotal """ 2152d57dc69SVladimir Kotal 2162d57dc69SVladimir Kotal ret = SUCCESS_EXITVAL 2172d57dc69SVladimir Kotal self.logger.debug("Output for project '{}':".format(self.name)) 2182d57dc69SVladimir Kotal for cmd in self.outputs.keys(): 2192d57dc69SVladimir Kotal if self.outputs[cmd] and len(self.outputs[cmd]) > 0: 2202d57dc69SVladimir Kotal self.logger.debug("'{}': {}". 2212d57dc69SVladimir Kotal format(cmd, self.outputs[cmd])) 2222d57dc69SVladimir Kotal 2232d57dc69SVladimir Kotal if self.name in ignore_errors: 2242d57dc69SVladimir Kotal self.logger.debug("errors of project '{}' ignored". 2252d57dc69SVladimir Kotal format(self.name)) 2262d57dc69SVladimir Kotal return 2272d57dc69SVladimir Kotal 2282d57dc69SVladimir Kotal self.logger.debug("retcodes = {}".format(self.retcodes)) 2292d57dc69SVladimir Kotal if any(rv != SUCCESS_EXITVAL and rv != CONTINUE_EXITVAL 2302d57dc69SVladimir Kotal for rv in self.retcodes.values()): 2312d57dc69SVladimir Kotal ret = 1 2322d57dc69SVladimir Kotal self.logger.error("processing of project '{}' failed". 2332d57dc69SVladimir Kotal format(self)) 2342d57dc69SVladimir Kotal indent = " " 2352d57dc69SVladimir Kotal self.logger.error("{}failed commands:".format(indent)) 2362d57dc69SVladimir Kotal failed_cmds = {k: v for k, v in 2372d57dc69SVladimir Kotal self.retcodes.items() if v != SUCCESS_EXITVAL} 2382d57dc69SVladimir Kotal indent = " " 2392d57dc69SVladimir Kotal for cmd in failed_cmds.keys(): 2402d57dc69SVladimir Kotal self.logger.error("{}'{}': {}". 2412d57dc69SVladimir Kotal format(indent, cmd, failed_cmds[cmd])) 2422d57dc69SVladimir Kotal out = self.get_cmd_output(cmd, 2432d57dc69SVladimir Kotal indent=indent + " ") 2442d57dc69SVladimir Kotal if out: 2452d57dc69SVladimir Kotal self.logger.error(out) 2462d57dc69SVladimir Kotal self.logger.error("") 2472d57dc69SVladimir Kotal 2482d57dc69SVladimir Kotal errored_cmds = {k: v for k, v in self.outputs.items() 2492d57dc69SVladimir Kotal if self.re_program.match(str(v))} 2502d57dc69SVladimir Kotal if len(errored_cmds) > 0: 2512d57dc69SVladimir Kotal ret = 1 2522d57dc69SVladimir Kotal self.logger.error("Command output in project '{}'" 2532d57dc69SVladimir Kotal " contains errors:".format(self.name)) 2542d57dc69SVladimir Kotal indent = " " 2552d57dc69SVladimir Kotal for cmd in errored_cmds.keys(): 2562d57dc69SVladimir Kotal self.logger.error("{}{}".format(indent, cmd)) 2572d57dc69SVladimir Kotal out = self.get_cmd_output(cmd, 2582d57dc69SVladimir Kotal indent=indent + " ") 2592d57dc69SVladimir Kotal if out: 2602d57dc69SVladimir Kotal self.logger.error(out) 2612d57dc69SVladimir Kotal self.logger.error("") 2622d57dc69SVladimir Kotal 2632d57dc69SVladimir Kotal return ret 264