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