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, 2019, Oracle and/or its affiliates. All rights reserved. 22# 23 24import logging 25from .command import Command 26from .utils import is_web_uri 27from .exitvals import ( 28 CONTINUE_EXITVAL, 29 SUCCESS_EXITVAL 30) 31from .restful import call_rest_api 32from .patterns import PROJECT_SUBST, COMMAND_PROPERTY 33import re 34 35 36class CommandSequenceBase: 37 """ 38 Wrap the run of a set of Command instances. 39 40 This class intentionally does not contain any logging 41 so that it can be passed through Pool.map(). 42 """ 43 44 def __init__(self, name, commands, loglevel=logging.INFO, cleanup=None, 45 driveon=False): 46 self.name = name 47 self.commands = commands 48 self.failed = False 49 self.retcodes = {} 50 self.outputs = {} 51 if cleanup and not isinstance(cleanup, list): 52 raise Exception("cleanup is not a list of commands") 53 54 self.cleanup = cleanup 55 self.loglevel = loglevel 56 self.driveon = driveon 57 58 def __str__(self): 59 return str(self.name) 60 61 def get_cmd_output(self, cmd, indent=""): 62 str = "" 63 if self.outputs[cmd]: 64 for line in self.outputs[cmd]: 65 str += '{}{}'.format(indent, line) 66 67 return str 68 69 def fill(self, retcodes, outputs, failed): 70 self.retcodes = retcodes 71 self.outputs = outputs 72 self.failed = failed 73 74 75class CommandSequence(CommandSequenceBase): 76 77 re_program = re.compile('ERROR[:]*\\s+') 78 79 def __init__(self, base): 80 super().__init__(base.name, base.commands, loglevel=base.loglevel, 81 cleanup=base.cleanup, driveon=base.driveon) 82 83 self.logger = logging.getLogger(__name__) 84 self.logger.setLevel(base.loglevel) 85 86 def run_command(self, cmd): 87 """ 88 Execute a command and return its return code. 89 """ 90 cmd.execute() 91 self.retcodes[str(cmd)] = cmd.getretcode() 92 self.outputs[str(cmd)] = cmd.getoutput() 93 94 return cmd.getretcode() 95 96 def run(self): 97 """ 98 Run the sequence of commands and capture their output and return code. 99 First command that returns code other than 0 terminates the sequence. 100 If the command has return code 2, the sequence will be terminated 101 however it will not be treated as error (unless the 'driveon' parameter 102 is True). 103 104 If a command contains PROJECT_SUBST pattern, it will be replaced 105 by project name, otherwise project name will be appended to the 106 argument list of the command. 107 108 Any command entry that is a URI, will be used to submit RESTful API 109 request. Return codes for these requests are not checked. 110 """ 111 112 for command in self.commands: 113 if is_web_uri(command.get(COMMAND_PROPERTY)[0]): 114 call_rest_api(command, PROJECT_SUBST, self.name) 115 else: 116 command_args = command.get(COMMAND_PROPERTY) 117 command = Command(command_args, 118 env_vars=command.get("env"), 119 resource_limits=command.get("limits"), 120 args_subst={PROJECT_SUBST: self.name}, 121 args_append=[self.name], excl_subst=True) 122 retcode = self.run_command(command) 123 124 # If a command exits with non-zero return code, 125 # terminate the sequence of commands. 126 if retcode != SUCCESS_EXITVAL: 127 if retcode == CONTINUE_EXITVAL: 128 if not self.driveon: 129 self.logger.debug("command '{}' for project {} " 130 "requested break". 131 format(command, self.name)) 132 self.run_cleanup() 133 else: 134 self.logger.debug("command '{}' for project {} " 135 "requested break however " 136 "the 'driveon' option is set " 137 "so driving on.". 138 format(command, self.name)) 139 continue 140 else: 141 self.logger.error("command '{}' for project {} failed " 142 "with code {}, breaking". 143 format(command, self.name, retcode)) 144 self.failed = True 145 self.run_cleanup() 146 147 break 148 149 def run_cleanup(self): 150 """ 151 Call cleanup sequence in case the command sequence failed 152 or termination was requested. 153 """ 154 if self.cleanup is None: 155 return 156 157 for cleanup_cmd in self.cleanup: 158 if is_web_uri(cleanup_cmd.get(COMMAND_PROPERTY)[0]): 159 call_rest_api(cleanup_cmd, PROJECT_SUBST, self.name) 160 else: 161 command_args = cleanup_cmd.get(COMMAND_PROPERTY) 162 self.logger.debug("Running cleanup command '{}'". 163 format(command_args)) 164 cmd = Command(command_args, 165 args_subst={PROJECT_SUBST: self.name}, 166 args_append=[self.name], excl_subst=True) 167 cmd.execute() 168 if cmd.getretcode() != SUCCESS_EXITVAL: 169 self.logger.info("cleanup command '{}' failed with " 170 "code {}". 171 format(cmd.cmd, cmd.getretcode())) 172 self.logger.info('output: {}'.format(cmd.getoutputstr())) 173 174 def check(self, ignore_errors): 175 """ 176 Check the output of the commands and perform logging. 177 178 Return 0 on success, 1 if error was detected. 179 """ 180 181 ret = SUCCESS_EXITVAL 182 self.logger.debug("Output for project '{}':".format(self.name)) 183 for cmd in self.outputs.keys(): 184 if self.outputs[cmd] and len(self.outputs[cmd]) > 0: 185 self.logger.debug("'{}': {}". 186 format(cmd, self.outputs[cmd])) 187 188 if self.name in ignore_errors: 189 self.logger.debug("errors of project '{}' ignored". 190 format(self.name)) 191 return 192 193 self.logger.debug("retcodes = {}".format(self.retcodes)) 194 if any(rv != SUCCESS_EXITVAL and rv != CONTINUE_EXITVAL 195 for rv in self.retcodes.values()): 196 ret = 1 197 self.logger.error("processing of project '{}' failed". 198 format(self)) 199 indent = " " 200 self.logger.error("{}failed commands:".format(indent)) 201 failed_cmds = {k: v for k, v in 202 self.retcodes.items() if v != SUCCESS_EXITVAL} 203 indent = " " 204 for cmd in failed_cmds.keys(): 205 self.logger.error("{}'{}': {}". 206 format(indent, cmd, failed_cmds[cmd])) 207 out = self.get_cmd_output(cmd, 208 indent=indent + " ") 209 if out: 210 self.logger.error(out) 211 self.logger.error("") 212 213 errored_cmds = {k: v for k, v in self.outputs.items() 214 if self.re_program.match(str(v))} 215 if len(errored_cmds) > 0: 216 ret = 1 217 self.logger.error("Command output in project '{}'" 218 " contains errors:".format(self.name)) 219 indent = " " 220 for cmd in errored_cmds.keys(): 221 self.logger.error("{}{}".format(indent, cmd)) 222 out = self.get_cmd_output(cmd, 223 indent=indent + " ") 224 if out: 225 self.logger.error(out) 226 self.logger.error("") 227 228 return ret 229