xref: /OpenGrok/tools/src/main/python/opengrok_tools/utils/commandsequence.py (revision 5a30ebc358686f15d3a463f75ce8886f7d931352)
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#
212d97c0a2SVladimir 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
362d97c0a2SVladimir 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,
49b2d29daeSVladimir Kotal                 driveon=False, url=None, env=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
61b2d29daeSVladimir Kotal        self.env = env
622d57dc69SVladimir Kotal
632d97c0a2SVladimir Kotal        self.url = url
642d97c0a2SVladimir Kotal
652d57dc69SVladimir Kotal    def __str__(self):
662d57dc69SVladimir Kotal        return str(self.name)
672d57dc69SVladimir Kotal
682d57dc69SVladimir Kotal    def get_cmd_output(self, cmd, indent=""):
69b2d29daeSVladimir Kotal        """
70b2d29daeSVladimir Kotal        :param cmd: command
71b2d29daeSVladimir Kotal        :param indent: prefix for each line
72b2d29daeSVladimir Kotal        :return: command output as string
73b2d29daeSVladimir Kotal        """
74b2d29daeSVladimir Kotal
752d57dc69SVladimir Kotal        str = ""
762d57dc69SVladimir Kotal        for line in self.outputs.get(cmd, []):
772d57dc69SVladimir Kotal            str += '{}{}'.format(indent, line)
782d57dc69SVladimir Kotal
792d57dc69SVladimir Kotal        return str
802d57dc69SVladimir Kotal
812d57dc69SVladimir Kotal    def fill(self, retcodes, outputs, failed):
822d57dc69SVladimir Kotal        self.retcodes = retcodes
832d57dc69SVladimir Kotal        self.outputs = outputs
842d57dc69SVladimir Kotal        self.failed = failed
852d57dc69SVladimir Kotal
862d57dc69SVladimir Kotal
872d57dc69SVladimir Kotalclass CommandSequence(CommandSequenceBase):
882d57dc69SVladimir Kotal
892d57dc69SVladimir Kotal    re_program = re.compile('ERROR[:]*\\s+')
902d57dc69SVladimir Kotal
912d57dc69SVladimir Kotal    def __init__(self, base):
922d57dc69SVladimir Kotal        super().__init__(base.name, base.commands, loglevel=base.loglevel,
932d97c0a2SVladimir Kotal                         cleanup=base.cleanup, driveon=base.driveon,
94b2d29daeSVladimir Kotal                         url=base.url, env=base.env)
952d57dc69SVladimir Kotal
962d57dc69SVladimir Kotal        self.logger = logging.getLogger(__name__)
972d57dc69SVladimir Kotal        self.logger.setLevel(base.loglevel)
982d57dc69SVladimir Kotal
992d57dc69SVladimir Kotal    def run_command(self, cmd):
1002d57dc69SVladimir Kotal        """
1012d57dc69SVladimir Kotal        Execute a command and return its return code.
1022d57dc69SVladimir Kotal        """
1032d57dc69SVladimir Kotal        cmd.execute()
1042d57dc69SVladimir Kotal        self.retcodes[str(cmd)] = cmd.getretcode()
1052d57dc69SVladimir Kotal        self.outputs[str(cmd)] = cmd.getoutput()
1062d57dc69SVladimir Kotal
1072d57dc69SVladimir Kotal        return cmd.getretcode()
1082d57dc69SVladimir Kotal
1092d57dc69SVladimir Kotal    def run(self):
1102d57dc69SVladimir Kotal        """
1112d57dc69SVladimir Kotal        Run the sequence of commands and capture their output and return code.
1122d57dc69SVladimir Kotal        First command that returns code other than 0 terminates the sequence.
1132d57dc69SVladimir Kotal        If the command has return code 2, the sequence will be terminated
1142d57dc69SVladimir Kotal        however it will not be treated as error (unless the 'driveon' parameter
1152d57dc69SVladimir Kotal        is True).
1162d57dc69SVladimir Kotal
1172d57dc69SVladimir Kotal        If a command contains PROJECT_SUBST pattern, it will be replaced
1182d57dc69SVladimir Kotal        by project name, otherwise project name will be appended to the
1192d57dc69SVladimir Kotal        argument list of the command.
1202d57dc69SVladimir Kotal
1212d57dc69SVladimir Kotal        Any command entry that is a URI, will be used to submit RESTful API
1222d57dc69SVladimir Kotal        request.
1232d57dc69SVladimir Kotal        """
1242d57dc69SVladimir Kotal
1252d57dc69SVladimir Kotal        for command in self.commands:
1262d97c0a2SVladimir Kotal            cmd_value = command.get(COMMAND_PROPERTY)[0]
1272d97c0a2SVladimir Kotal            if cmd_value.startswith(URL_SUBST) or is_web_uri(cmd_value):
1282d57dc69SVladimir Kotal                try:
1292d97c0a2SVladimir Kotal                    call_rest_api(command, {PROJECT_SUBST: self.name,
1302d97c0a2SVladimir Kotal                                            URL_SUBST: self.url})
1312d57dc69SVladimir Kotal                except HTTPError as e:
1322d57dc69SVladimir Kotal                    self.logger.error("RESTful command {} failed: {}".
1332d57dc69SVladimir Kotal                                      format(command, e))
1342d57dc69SVladimir Kotal                    self.failed = True
1352d57dc69SVladimir Kotal                    self.retcodes[str(command)] = FAILURE_EXITVAL
1362d57dc69SVladimir Kotal
1372d57dc69SVladimir Kotal                    break
1382d57dc69SVladimir Kotal            else:
1392d57dc69SVladimir Kotal                command_args = command.get(COMMAND_PROPERTY)
1402d57dc69SVladimir Kotal                command = Command(command_args,
1412d57dc69SVladimir Kotal                                  env_vars=command.get("env"),
142b2d29daeSVladimir Kotal                                  logger=self.logger,
1432d57dc69SVladimir Kotal                                  resource_limits=command.get("limits"),
1442d97c0a2SVladimir Kotal                                  args_subst={PROJECT_SUBST: self.name,
1452d97c0a2SVladimir Kotal                                              URL_SUBST: self.url},
1462d57dc69SVladimir Kotal                                  args_append=[self.name], excl_subst=True)
1472d57dc69SVladimir Kotal                retcode = self.run_command(command)
1482d57dc69SVladimir Kotal
1492d57dc69SVladimir Kotal                # If a command exits with non-zero return code,
1502d57dc69SVladimir Kotal                # terminate the sequence of commands.
1512d57dc69SVladimir Kotal                if retcode != SUCCESS_EXITVAL:
1522d57dc69SVladimir Kotal                    if retcode == CONTINUE_EXITVAL:
1532d57dc69SVladimir Kotal                        if not self.driveon:
1542d57dc69SVladimir Kotal                            self.logger.debug("command '{}' for project {} "
1552d57dc69SVladimir Kotal                                              "requested break".
1562d57dc69SVladimir Kotal                                              format(command, self.name))
1572d57dc69SVladimir Kotal                            self.run_cleanup()
1582d57dc69SVladimir Kotal                        else:
1592d57dc69SVladimir Kotal                            self.logger.debug("command '{}' for project {} "
1602d57dc69SVladimir Kotal                                              "requested break however "
1612d57dc69SVladimir Kotal                                              "the 'driveon' option is set "
1622d57dc69SVladimir Kotal                                              "so driving on.".
1632d57dc69SVladimir Kotal                                              format(command, self.name))
1642d57dc69SVladimir Kotal                            continue
1652d57dc69SVladimir Kotal                    else:
166d7281829SVladimir Kotal                        if self.driveon:
167d7281829SVladimir Kotal                            do = "driving on"
168d7281829SVladimir Kotal                            self.logger.debug("command '{}' for project '{}'' failed "
169d7281829SVladimir Kotal                                              "with code {}, {}".
170d7281829SVladimir Kotal                                              format(command, self.name, retcode, do))
171d7281829SVladimir Kotal                        else:
172d7281829SVladimir Kotal                            do = "breaking"
173d7281829SVladimir Kotal                            self.logger.error("command '{}' for project '{}'' failed "
174d7281829SVladimir Kotal                                              "with code {}, {}".
175d7281829SVladimir Kotal                                              format(command, self.name, retcode, do))
176d7281829SVladimir Kotal
177d7281829SVladimir Kotal                        if self.driveon:
178d7281829SVladimir Kotal                            continue
179d7281829SVladimir Kotal                        else:
1802d57dc69SVladimir Kotal                            self.failed = True
1812d57dc69SVladimir Kotal                            self.run_cleanup()
1822d57dc69SVladimir Kotal
1832d57dc69SVladimir Kotal                    break
1842d57dc69SVladimir Kotal
1852d57dc69SVladimir Kotal    def run_cleanup(self):
1862d57dc69SVladimir Kotal        """
1872d57dc69SVladimir Kotal        Call cleanup sequence in case the command sequence failed
1882d57dc69SVladimir Kotal        or termination was requested.
1892d57dc69SVladimir Kotal        """
1902d57dc69SVladimir Kotal        if self.cleanup is None:
1912d57dc69SVladimir Kotal            return
1922d57dc69SVladimir Kotal
1932d57dc69SVladimir Kotal        for cleanup_cmd in self.cleanup:
1942d97c0a2SVladimir Kotal            arg0 = cleanup_cmd.get(COMMAND_PROPERTY)[0]
1952d97c0a2SVladimir Kotal            if arg0.startswith(URL_SUBST) or is_web_uri(arg0):
1962d57dc69SVladimir Kotal                try:
1972d97c0a2SVladimir Kotal                    call_rest_api(cleanup_cmd, {PROJECT_SUBST: self.name,
1982d97c0a2SVladimir Kotal                                                URL_SUBST: self.url})
1992d57dc69SVladimir Kotal                except HTTPError as e:
2002d57dc69SVladimir Kotal                    self.logger.error("RESTful command {} failed: {}".
2012d57dc69SVladimir Kotal                                      format(cleanup_cmd, e))
2022d57dc69SVladimir Kotal            else:
2032d57dc69SVladimir Kotal                command_args = cleanup_cmd.get(COMMAND_PROPERTY)
2042d57dc69SVladimir Kotal                self.logger.debug("Running cleanup command '{}'".
2052d57dc69SVladimir Kotal                                  format(command_args))
2062d57dc69SVladimir Kotal                cmd = Command(command_args,
207b2d29daeSVladimir Kotal                              logger=self.logger,
2082d97c0a2SVladimir Kotal                              args_subst={PROJECT_SUBST: self.name,
2092d97c0a2SVladimir Kotal                                          URL_SUBST: self.url},
2102d57dc69SVladimir Kotal                              args_append=[self.name], excl_subst=True)
2112d57dc69SVladimir Kotal                cmd.execute()
2122d57dc69SVladimir Kotal                if cmd.getretcode() != SUCCESS_EXITVAL:
2132d57dc69SVladimir Kotal                    self.logger.error("cleanup command '{}' failed with "
2142d57dc69SVladimir Kotal                                      "code {}".
2152d57dc69SVladimir Kotal                                      format(cmd.cmd, cmd.getretcode()))
2162d57dc69SVladimir Kotal                    self.logger.info('output: {}'.format(cmd.getoutputstr()))
2172d57dc69SVladimir Kotal
218b2d29daeSVladimir Kotal    def print_outputs(self, logger, loglevel=logging.INFO, lines=False):
219b2d29daeSVladimir Kotal        """
220b2d29daeSVladimir Kotal        Print command outputs.
221b2d29daeSVladimir Kotal        """
222b2d29daeSVladimir Kotal
223b2d29daeSVladimir Kotal        logger.debug("Output for project '{}':".format(self.name))
224b2d29daeSVladimir Kotal        for cmd in self.outputs.keys():
225b2d29daeSVladimir Kotal            if self.outputs[cmd] and len(self.outputs[cmd]) > 0:
226b2d29daeSVladimir Kotal                if lines:
227b2d29daeSVladimir Kotal                    logger.log(loglevel, "Output from '{}':".format(cmd))
228b2d29daeSVladimir Kotal                    logger.log(loglevel, '{}'.format(self.get_cmd_output(cmd)))
229b2d29daeSVladimir Kotal                else:
230b2d29daeSVladimir Kotal                    logger.log(loglevel, "'{}': {}".
231b2d29daeSVladimir Kotal                               format(cmd, self.outputs[cmd]))
232b2d29daeSVladimir Kotal
2332d57dc69SVladimir Kotal    def check(self, ignore_errors):
2342d57dc69SVladimir Kotal        """
2352d57dc69SVladimir Kotal        Check the output of the commands and perform logging.
2362d57dc69SVladimir Kotal
237*5a30ebc3SVladimir Kotal        Return SUCCESS_EXITVAL on success, 1 if error was detected.
2382d57dc69SVladimir Kotal        """
2392d57dc69SVladimir Kotal
2402d57dc69SVladimir Kotal        ret = SUCCESS_EXITVAL
241b2d29daeSVladimir Kotal        self.print_outputs(self.logger, loglevel=logging.DEBUG)
2422d57dc69SVladimir Kotal
243*5a30ebc3SVladimir Kotal        if ignore_errors and self.name in ignore_errors:
2442d57dc69SVladimir Kotal            self.logger.debug("errors of project '{}' ignored".
2452d57dc69SVladimir Kotal                              format(self.name))
2462d57dc69SVladimir Kotal            return
2472d57dc69SVladimir Kotal
2482d57dc69SVladimir Kotal        self.logger.debug("retcodes = {}".format(self.retcodes))
2492d57dc69SVladimir Kotal        if any(rv != SUCCESS_EXITVAL and rv != CONTINUE_EXITVAL
2502d57dc69SVladimir Kotal               for rv in self.retcodes.values()):
2512d57dc69SVladimir Kotal            ret = 1
2522d57dc69SVladimir Kotal            self.logger.error("processing of project '{}' failed".
2532d57dc69SVladimir Kotal                              format(self))
2542d57dc69SVladimir Kotal            indent = "  "
2552d57dc69SVladimir Kotal            self.logger.error("{}failed commands:".format(indent))
2562d57dc69SVladimir Kotal            failed_cmds = {k: v for k, v in
2572d57dc69SVladimir Kotal                           self.retcodes.items() if v != SUCCESS_EXITVAL}
2582d57dc69SVladimir Kotal            indent = "    "
2592d57dc69SVladimir Kotal            for cmd in failed_cmds.keys():
2602d57dc69SVladimir Kotal                self.logger.error("{}'{}': {}".
2612d57dc69SVladimir Kotal                                  format(indent, cmd, failed_cmds[cmd]))
2622d57dc69SVladimir Kotal                out = self.get_cmd_output(cmd,
2632d57dc69SVladimir Kotal                                          indent=indent + "  ")
2642d57dc69SVladimir Kotal                if out:
2652d57dc69SVladimir Kotal                    self.logger.error(out)
2662d57dc69SVladimir Kotal            self.logger.error("")
2672d57dc69SVladimir Kotal
2682d57dc69SVladimir Kotal        errored_cmds = {k: v for k, v in self.outputs.items()
2692d57dc69SVladimir Kotal                        if self.re_program.match(str(v))}
2702d57dc69SVladimir Kotal        if len(errored_cmds) > 0:
2712d57dc69SVladimir Kotal            ret = 1
2722d57dc69SVladimir Kotal            self.logger.error("Command output in project '{}'"
2732d57dc69SVladimir Kotal                              " contains errors:".format(self.name))
2742d57dc69SVladimir Kotal            indent = "  "
2752d57dc69SVladimir Kotal            for cmd in errored_cmds.keys():
2762d57dc69SVladimir Kotal                self.logger.error("{}{}".format(indent, cmd))
2772d57dc69SVladimir Kotal                out = self.get_cmd_output(cmd,
2782d57dc69SVladimir Kotal                                          indent=indent + "  ")
2792d57dc69SVladimir Kotal                if out:
2802d57dc69SVladimir Kotal                    self.logger.error(out)
2812d57dc69SVladimir Kotal                self.logger.error("")
2822d57dc69SVladimir Kotal
2832d57dc69SVladimir Kotal        return ret
284