xref: /OpenGrok/tools/src/main/python/opengrok_tools/utils/commandsequence.py (revision 2d57dc692b4dd10e696ca6922510f6cecded1bfd)
1*2d57dc69SVladimir Kotal#
2*2d57dc69SVladimir Kotal# CDDL HEADER START
3*2d57dc69SVladimir Kotal#
4*2d57dc69SVladimir Kotal# The contents of this file are subject to the terms of the
5*2d57dc69SVladimir Kotal# Common Development and Distribution License (the "License").
6*2d57dc69SVladimir Kotal# You may not use this file except in compliance with the License.
7*2d57dc69SVladimir Kotal#
8*2d57dc69SVladimir Kotal# See LICENSE.txt included in this distribution for the specific
9*2d57dc69SVladimir Kotal# language governing permissions and limitations under the License.
10*2d57dc69SVladimir Kotal#
11*2d57dc69SVladimir Kotal# When distributing Covered Code, include this CDDL HEADER in each
12*2d57dc69SVladimir Kotal# file and include the License file at LICENSE.txt.
13*2d57dc69SVladimir Kotal# If applicable, add the following below this CDDL HEADER, with the
14*2d57dc69SVladimir Kotal# fields enclosed by brackets "[]" replaced with your own identifying
15*2d57dc69SVladimir Kotal# information: Portions Copyright [yyyy] [name of copyright owner]
16*2d57dc69SVladimir Kotal#
17*2d57dc69SVladimir Kotal# CDDL HEADER END
18*2d57dc69SVladimir Kotal#
19*2d57dc69SVladimir Kotal
20*2d57dc69SVladimir Kotal#
21*2d57dc69SVladimir Kotal# Copyright (c) 2017, 2020, Oracle and/or its affiliates. All rights reserved.
22*2d57dc69SVladimir Kotal#
23*2d57dc69SVladimir Kotal
24*2d57dc69SVladimir Kotalimport logging
25*2d57dc69SVladimir Kotal
26*2d57dc69SVladimir Kotalfrom requests.exceptions import HTTPError
27*2d57dc69SVladimir Kotal
28*2d57dc69SVladimir Kotalfrom .command import Command
29*2d57dc69SVladimir Kotalfrom .utils import is_web_uri
30*2d57dc69SVladimir Kotalfrom .exitvals import (
31*2d57dc69SVladimir Kotal    CONTINUE_EXITVAL,
32*2d57dc69SVladimir Kotal    SUCCESS_EXITVAL,
33*2d57dc69SVladimir Kotal    FAILURE_EXITVAL
34*2d57dc69SVladimir Kotal)
35*2d57dc69SVladimir Kotalfrom .restful import call_rest_api
36*2d57dc69SVladimir Kotalfrom .patterns import PROJECT_SUBST, COMMAND_PROPERTY
37*2d57dc69SVladimir Kotalimport re
38*2d57dc69SVladimir Kotal
39*2d57dc69SVladimir Kotal
40*2d57dc69SVladimir Kotalclass CommandSequenceBase:
41*2d57dc69SVladimir Kotal    """
42*2d57dc69SVladimir Kotal    Wrap the run of a set of Command instances.
43*2d57dc69SVladimir Kotal
44*2d57dc69SVladimir Kotal    This class intentionally does not contain any logging
45*2d57dc69SVladimir Kotal    so that it can be passed through Pool.map().
46*2d57dc69SVladimir Kotal    """
47*2d57dc69SVladimir Kotal
48*2d57dc69SVladimir Kotal    def __init__(self, name, commands, loglevel=logging.INFO, cleanup=None,
49*2d57dc69SVladimir Kotal                 driveon=False):
50*2d57dc69SVladimir Kotal        self.name = name
51*2d57dc69SVladimir Kotal        self.commands = commands
52*2d57dc69SVladimir Kotal        self.failed = False
53*2d57dc69SVladimir Kotal        self.retcodes = {}
54*2d57dc69SVladimir Kotal        self.outputs = {}
55*2d57dc69SVladimir Kotal        if cleanup and not isinstance(cleanup, list):
56*2d57dc69SVladimir Kotal            raise Exception("cleanup is not a list of commands")
57*2d57dc69SVladimir Kotal
58*2d57dc69SVladimir Kotal        self.cleanup = cleanup
59*2d57dc69SVladimir Kotal        self.loglevel = loglevel
60*2d57dc69SVladimir Kotal        self.driveon = driveon
61*2d57dc69SVladimir Kotal
62*2d57dc69SVladimir Kotal    def __str__(self):
63*2d57dc69SVladimir Kotal        return str(self.name)
64*2d57dc69SVladimir Kotal
65*2d57dc69SVladimir Kotal    def get_cmd_output(self, cmd, indent=""):
66*2d57dc69SVladimir Kotal        str = ""
67*2d57dc69SVladimir Kotal        for line in self.outputs.get(cmd, []):
68*2d57dc69SVladimir Kotal            str += '{}{}'.format(indent, line)
69*2d57dc69SVladimir Kotal
70*2d57dc69SVladimir Kotal        return str
71*2d57dc69SVladimir Kotal
72*2d57dc69SVladimir Kotal    def fill(self, retcodes, outputs, failed):
73*2d57dc69SVladimir Kotal        self.retcodes = retcodes
74*2d57dc69SVladimir Kotal        self.outputs = outputs
75*2d57dc69SVladimir Kotal        self.failed = failed
76*2d57dc69SVladimir Kotal
77*2d57dc69SVladimir Kotal
78*2d57dc69SVladimir Kotalclass CommandSequence(CommandSequenceBase):
79*2d57dc69SVladimir Kotal
80*2d57dc69SVladimir Kotal    re_program = re.compile('ERROR[:]*\\s+')
81*2d57dc69SVladimir Kotal
82*2d57dc69SVladimir Kotal    def __init__(self, base):
83*2d57dc69SVladimir Kotal        super().__init__(base.name, base.commands, loglevel=base.loglevel,
84*2d57dc69SVladimir Kotal                         cleanup=base.cleanup, driveon=base.driveon)
85*2d57dc69SVladimir Kotal
86*2d57dc69SVladimir Kotal        self.logger = logging.getLogger(__name__)
87*2d57dc69SVladimir Kotal        self.logger.setLevel(base.loglevel)
88*2d57dc69SVladimir Kotal
89*2d57dc69SVladimir Kotal    def run_command(self, cmd):
90*2d57dc69SVladimir Kotal        """
91*2d57dc69SVladimir Kotal        Execute a command and return its return code.
92*2d57dc69SVladimir Kotal        """
93*2d57dc69SVladimir Kotal        cmd.execute()
94*2d57dc69SVladimir Kotal        self.retcodes[str(cmd)] = cmd.getretcode()
95*2d57dc69SVladimir Kotal        self.outputs[str(cmd)] = cmd.getoutput()
96*2d57dc69SVladimir Kotal
97*2d57dc69SVladimir Kotal        return cmd.getretcode()
98*2d57dc69SVladimir Kotal
99*2d57dc69SVladimir Kotal    def run(self):
100*2d57dc69SVladimir Kotal        """
101*2d57dc69SVladimir Kotal        Run the sequence of commands and capture their output and return code.
102*2d57dc69SVladimir Kotal        First command that returns code other than 0 terminates the sequence.
103*2d57dc69SVladimir Kotal        If the command has return code 2, the sequence will be terminated
104*2d57dc69SVladimir Kotal        however it will not be treated as error (unless the 'driveon' parameter
105*2d57dc69SVladimir Kotal        is True).
106*2d57dc69SVladimir Kotal
107*2d57dc69SVladimir Kotal        If a command contains PROJECT_SUBST pattern, it will be replaced
108*2d57dc69SVladimir Kotal        by project name, otherwise project name will be appended to the
109*2d57dc69SVladimir Kotal        argument list of the command.
110*2d57dc69SVladimir Kotal
111*2d57dc69SVladimir Kotal        Any command entry that is a URI, will be used to submit RESTful API
112*2d57dc69SVladimir Kotal        request.
113*2d57dc69SVladimir Kotal        """
114*2d57dc69SVladimir Kotal
115*2d57dc69SVladimir Kotal        for command in self.commands:
116*2d57dc69SVladimir Kotal            if is_web_uri(command.get(COMMAND_PROPERTY)[0]):
117*2d57dc69SVladimir Kotal                try:
118*2d57dc69SVladimir Kotal                    call_rest_api(command, PROJECT_SUBST, self.name)
119*2d57dc69SVladimir Kotal                except HTTPError as e:
120*2d57dc69SVladimir Kotal                    self.logger.error("RESTful command {} failed: {}".
121*2d57dc69SVladimir Kotal                                      format(command, e))
122*2d57dc69SVladimir Kotal                    self.failed = True
123*2d57dc69SVladimir Kotal                    self.retcodes[str(command)] = FAILURE_EXITVAL
124*2d57dc69SVladimir Kotal
125*2d57dc69SVladimir Kotal                    break
126*2d57dc69SVladimir Kotal            else:
127*2d57dc69SVladimir Kotal                command_args = command.get(COMMAND_PROPERTY)
128*2d57dc69SVladimir Kotal                command = Command(command_args,
129*2d57dc69SVladimir Kotal                                  env_vars=command.get("env"),
130*2d57dc69SVladimir Kotal                                  resource_limits=command.get("limits"),
131*2d57dc69SVladimir Kotal                                  args_subst={PROJECT_SUBST: self.name},
132*2d57dc69SVladimir Kotal                                  args_append=[self.name], excl_subst=True)
133*2d57dc69SVladimir Kotal                retcode = self.run_command(command)
134*2d57dc69SVladimir Kotal
135*2d57dc69SVladimir Kotal                # If a command exits with non-zero return code,
136*2d57dc69SVladimir Kotal                # terminate the sequence of commands.
137*2d57dc69SVladimir Kotal                if retcode != SUCCESS_EXITVAL:
138*2d57dc69SVladimir Kotal                    if retcode == CONTINUE_EXITVAL:
139*2d57dc69SVladimir Kotal                        if not self.driveon:
140*2d57dc69SVladimir Kotal                            self.logger.debug("command '{}' for project {} "
141*2d57dc69SVladimir Kotal                                              "requested break".
142*2d57dc69SVladimir Kotal                                              format(command, self.name))
143*2d57dc69SVladimir Kotal                            self.run_cleanup()
144*2d57dc69SVladimir Kotal                        else:
145*2d57dc69SVladimir Kotal                            self.logger.debug("command '{}' for project {} "
146*2d57dc69SVladimir Kotal                                              "requested break however "
147*2d57dc69SVladimir Kotal                                              "the 'driveon' option is set "
148*2d57dc69SVladimir Kotal                                              "so driving on.".
149*2d57dc69SVladimir Kotal                                              format(command, self.name))
150*2d57dc69SVladimir Kotal                            continue
151*2d57dc69SVladimir Kotal                    else:
152*2d57dc69SVladimir Kotal                        self.logger.error("command '{}' for project {} failed "
153*2d57dc69SVladimir Kotal                                          "with code {}, breaking".
154*2d57dc69SVladimir Kotal                                          format(command, self.name, retcode))
155*2d57dc69SVladimir Kotal                        self.failed = True
156*2d57dc69SVladimir Kotal                        self.run_cleanup()
157*2d57dc69SVladimir Kotal
158*2d57dc69SVladimir Kotal                    break
159*2d57dc69SVladimir Kotal
160*2d57dc69SVladimir Kotal    def run_cleanup(self):
161*2d57dc69SVladimir Kotal        """
162*2d57dc69SVladimir Kotal        Call cleanup sequence in case the command sequence failed
163*2d57dc69SVladimir Kotal        or termination was requested.
164*2d57dc69SVladimir Kotal        """
165*2d57dc69SVladimir Kotal        if self.cleanup is None:
166*2d57dc69SVladimir Kotal            return
167*2d57dc69SVladimir Kotal
168*2d57dc69SVladimir Kotal        for cleanup_cmd in self.cleanup:
169*2d57dc69SVladimir Kotal            if is_web_uri(cleanup_cmd.get(COMMAND_PROPERTY)[0]):
170*2d57dc69SVladimir Kotal                try:
171*2d57dc69SVladimir Kotal                    call_rest_api(cleanup_cmd, PROJECT_SUBST, self.name)
172*2d57dc69SVladimir Kotal                except HTTPError as e:
173*2d57dc69SVladimir Kotal                    self.logger.error("RESTful command {} failed: {}".
174*2d57dc69SVladimir Kotal                                      format(cleanup_cmd, e))
175*2d57dc69SVladimir Kotal            else:
176*2d57dc69SVladimir Kotal                command_args = cleanup_cmd.get(COMMAND_PROPERTY)
177*2d57dc69SVladimir Kotal                self.logger.debug("Running cleanup command '{}'".
178*2d57dc69SVladimir Kotal                                  format(command_args))
179*2d57dc69SVladimir Kotal                cmd = Command(command_args,
180*2d57dc69SVladimir Kotal                              args_subst={PROJECT_SUBST: self.name},
181*2d57dc69SVladimir Kotal                              args_append=[self.name], excl_subst=True)
182*2d57dc69SVladimir Kotal                cmd.execute()
183*2d57dc69SVladimir Kotal                if cmd.getretcode() != SUCCESS_EXITVAL:
184*2d57dc69SVladimir Kotal                    self.logger.error("cleanup command '{}' failed with "
185*2d57dc69SVladimir Kotal                                      "code {}".
186*2d57dc69SVladimir Kotal                                      format(cmd.cmd, cmd.getretcode()))
187*2d57dc69SVladimir Kotal                    self.logger.info('output: {}'.format(cmd.getoutputstr()))
188*2d57dc69SVladimir Kotal
189*2d57dc69SVladimir Kotal    def check(self, ignore_errors):
190*2d57dc69SVladimir Kotal        """
191*2d57dc69SVladimir Kotal        Check the output of the commands and perform logging.
192*2d57dc69SVladimir Kotal
193*2d57dc69SVladimir Kotal        Return 0 on success, 1 if error was detected.
194*2d57dc69SVladimir Kotal        """
195*2d57dc69SVladimir Kotal
196*2d57dc69SVladimir Kotal        ret = SUCCESS_EXITVAL
197*2d57dc69SVladimir Kotal        self.logger.debug("Output for project '{}':".format(self.name))
198*2d57dc69SVladimir Kotal        for cmd in self.outputs.keys():
199*2d57dc69SVladimir Kotal            if self.outputs[cmd] and len(self.outputs[cmd]) > 0:
200*2d57dc69SVladimir Kotal                self.logger.debug("'{}': {}".
201*2d57dc69SVladimir Kotal                                  format(cmd, self.outputs[cmd]))
202*2d57dc69SVladimir Kotal
203*2d57dc69SVladimir Kotal        if self.name in ignore_errors:
204*2d57dc69SVladimir Kotal            self.logger.debug("errors of project '{}' ignored".
205*2d57dc69SVladimir Kotal                              format(self.name))
206*2d57dc69SVladimir Kotal            return
207*2d57dc69SVladimir Kotal
208*2d57dc69SVladimir Kotal        self.logger.debug("retcodes = {}".format(self.retcodes))
209*2d57dc69SVladimir Kotal        if any(rv != SUCCESS_EXITVAL and rv != CONTINUE_EXITVAL
210*2d57dc69SVladimir Kotal               for rv in self.retcodes.values()):
211*2d57dc69SVladimir Kotal            ret = 1
212*2d57dc69SVladimir Kotal            self.logger.error("processing of project '{}' failed".
213*2d57dc69SVladimir Kotal                              format(self))
214*2d57dc69SVladimir Kotal            indent = "  "
215*2d57dc69SVladimir Kotal            self.logger.error("{}failed commands:".format(indent))
216*2d57dc69SVladimir Kotal            failed_cmds = {k: v for k, v in
217*2d57dc69SVladimir Kotal                           self.retcodes.items() if v != SUCCESS_EXITVAL}
218*2d57dc69SVladimir Kotal            indent = "    "
219*2d57dc69SVladimir Kotal            for cmd in failed_cmds.keys():
220*2d57dc69SVladimir Kotal                self.logger.error("{}'{}': {}".
221*2d57dc69SVladimir Kotal                                  format(indent, cmd, failed_cmds[cmd]))
222*2d57dc69SVladimir Kotal                out = self.get_cmd_output(cmd,
223*2d57dc69SVladimir Kotal                                          indent=indent + "  ")
224*2d57dc69SVladimir Kotal                if out:
225*2d57dc69SVladimir Kotal                    self.logger.error(out)
226*2d57dc69SVladimir Kotal            self.logger.error("")
227*2d57dc69SVladimir Kotal
228*2d57dc69SVladimir Kotal        errored_cmds = {k: v for k, v in self.outputs.items()
229*2d57dc69SVladimir Kotal                        if self.re_program.match(str(v))}
230*2d57dc69SVladimir Kotal        if len(errored_cmds) > 0:
231*2d57dc69SVladimir Kotal            ret = 1
232*2d57dc69SVladimir Kotal            self.logger.error("Command output in project '{}'"
233*2d57dc69SVladimir Kotal                              " contains errors:".format(self.name))
234*2d57dc69SVladimir Kotal            indent = "  "
235*2d57dc69SVladimir Kotal            for cmd in errored_cmds.keys():
236*2d57dc69SVladimir Kotal                self.logger.error("{}{}".format(indent, cmd))
237*2d57dc69SVladimir Kotal                out = self.get_cmd_output(cmd,
238*2d57dc69SVladimir Kotal                                          indent=indent + "  ")
239*2d57dc69SVladimir Kotal                if out:
240*2d57dc69SVladimir Kotal                    self.logger.error(out)
241*2d57dc69SVladimir Kotal                self.logger.error("")
242*2d57dc69SVladimir Kotal
243*2d57dc69SVladimir Kotal        return ret
244