xref: /OpenGrok/tools/src/main/python/opengrok_tools/utils/commandsequence.py (revision 77cacb4d7341bca0bf4ce67f0b324fd89257bbf1)
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#
2180df37bbSVladimir Kotal# Copyright (c) 2017, 2022, 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
4080df37bbSVladimir Kotalclass CommandConfigurationException(Exception):
4180df37bbSVladimir Kotal    pass
4280df37bbSVladimir Kotal
4380df37bbSVladimir Kotal
4480df37bbSVladimir Kotaldef check_command_property(command):
4580df37bbSVladimir Kotal    """
4680df37bbSVladimir Kotal    Check if the 'commands' parameter of CommandSequenceBase() has the right structure
4780df37bbSVladimir Kotal    w.r.t. individual commands.
4880df37bbSVladimir Kotal    :param command: command element
4980df37bbSVladimir Kotal    """
5080df37bbSVladimir Kotal    if not isinstance(command, dict):
51*77cacb4dSVladimir Kotal        raise CommandConfigurationException("command '{}' is not a dictionary".format(command))
5280df37bbSVladimir Kotal
5380df37bbSVladimir Kotal    command_value = command.get(COMMAND_PROPERTY)
5480df37bbSVladimir Kotal    if command_value is None:
55*77cacb4dSVladimir Kotal        raise CommandConfigurationException("command dictionary has no '{}' key: {}".
5680df37bbSVladimir Kotal                                            format(COMMAND_PROPERTY, command))
5780df37bbSVladimir Kotal
5880df37bbSVladimir Kotal    if not isinstance(command_value, list):
5980df37bbSVladimir Kotal        raise CommandConfigurationException("command value not a list: {}".
6080df37bbSVladimir Kotal                                            format(command_value))
6180df37bbSVladimir Kotal
6280df37bbSVladimir Kotal
632d57dc69SVladimir Kotalclass CommandSequenceBase:
642d57dc69SVladimir Kotal    """
652d57dc69SVladimir Kotal    Wrap the run of a set of Command instances.
662d57dc69SVladimir Kotal
672d57dc69SVladimir Kotal    This class intentionally does not contain any logging
682d57dc69SVladimir Kotal    so that it can be passed through Pool.map().
692d57dc69SVladimir Kotal    """
702d57dc69SVladimir Kotal
712d57dc69SVladimir Kotal    def __init__(self, name, commands, loglevel=logging.INFO, cleanup=None,
72732be1c2SVladimir Kotal                 driveon=False, url=None, env=None, http_headers=None,
73732be1c2SVladimir Kotal                 api_timeout=None):
742d57dc69SVladimir Kotal        self.name = name
7580df37bbSVladimir Kotal
7680df37bbSVladimir Kotal        if commands is None:
7780df37bbSVladimir Kotal            raise CommandConfigurationException("commands is None")
7880df37bbSVladimir Kotal        if not isinstance(commands, list):
7980df37bbSVladimir Kotal            raise CommandConfigurationException("commands is not a list")
802d57dc69SVladimir Kotal        self.commands = commands
8180df37bbSVladimir Kotal        for command in self.commands:
8280df37bbSVladimir Kotal            check_command_property(command)
8380df37bbSVladimir Kotal
842d57dc69SVladimir Kotal        self.failed = False
852d57dc69SVladimir Kotal        self.retcodes = {}
862d57dc69SVladimir Kotal        self.outputs = {}
872d57dc69SVladimir Kotal
8880df37bbSVladimir Kotal        if cleanup and not isinstance(cleanup, list):
8980df37bbSVladimir Kotal            raise CommandConfigurationException("cleanup is not a list of commands")
902d57dc69SVladimir Kotal        self.cleanup = cleanup
9180df37bbSVladimir Kotal        if self.cleanup:
9280df37bbSVladimir Kotal            for command in self.cleanup:
9380df37bbSVladimir Kotal                check_command_property(command)
9480df37bbSVladimir Kotal
952d57dc69SVladimir Kotal        self.loglevel = loglevel
962d57dc69SVladimir Kotal        self.driveon = driveon
97b2d29daeSVladimir Kotal        self.env = env
9889229afdSVladimir Kotal        self.http_headers = http_headers
99732be1c2SVladimir Kotal        self.api_timeout = api_timeout
1002d57dc69SVladimir Kotal
1012d97c0a2SVladimir Kotal        self.url = url
1022d97c0a2SVladimir Kotal
1032d57dc69SVladimir Kotal    def __str__(self):
1042d57dc69SVladimir Kotal        return str(self.name)
1052d57dc69SVladimir Kotal
1062d57dc69SVladimir Kotal    def get_cmd_output(self, cmd, indent=""):
107b2d29daeSVladimir Kotal        """
108b2d29daeSVladimir Kotal        :param cmd: command
109b2d29daeSVladimir Kotal        :param indent: prefix for each line
110b2d29daeSVladimir Kotal        :return: command output as string
111b2d29daeSVladimir Kotal        """
112b2d29daeSVladimir Kotal
11380df37bbSVladimir Kotal        str_out = ""
1142d57dc69SVladimir Kotal        for line in self.outputs.get(cmd, []):
11580df37bbSVladimir Kotal            str_out += '{}{}'.format(indent, line)
1162d57dc69SVladimir Kotal
11780df37bbSVladimir Kotal        return str_out
1182d57dc69SVladimir Kotal
1192d57dc69SVladimir Kotal    def fill(self, retcodes, outputs, failed):
1202d57dc69SVladimir Kotal        self.retcodes = retcodes
1212d57dc69SVladimir Kotal        self.outputs = outputs
1222d57dc69SVladimir Kotal        self.failed = failed
1232d57dc69SVladimir Kotal
1242d57dc69SVladimir Kotal
1252d57dc69SVladimir Kotalclass CommandSequence(CommandSequenceBase):
1262d57dc69SVladimir Kotal
1272d57dc69SVladimir Kotal    re_program = re.compile('ERROR[:]*\\s+')
1282d57dc69SVladimir Kotal
1292d57dc69SVladimir Kotal    def __init__(self, base):
1302d57dc69SVladimir Kotal        super().__init__(base.name, base.commands, loglevel=base.loglevel,
1312d97c0a2SVladimir Kotal                         cleanup=base.cleanup, driveon=base.driveon,
132b369c884SVladimir Kotal                         url=base.url, env=base.env,
133732be1c2SVladimir Kotal                         http_headers=base.http_headers,
134732be1c2SVladimir Kotal                         api_timeout=base.api_timeout)
1352d57dc69SVladimir Kotal
1362d57dc69SVladimir Kotal        self.logger = logging.getLogger(__name__)
1372d57dc69SVladimir Kotal        self.logger.setLevel(base.loglevel)
1382d57dc69SVladimir Kotal
1392d57dc69SVladimir Kotal    def run_command(self, cmd):
1402d57dc69SVladimir Kotal        """
1412d57dc69SVladimir Kotal        Execute a command and return its return code.
1422d57dc69SVladimir Kotal        """
1432d57dc69SVladimir Kotal        cmd.execute()
1442d57dc69SVladimir Kotal        self.retcodes[str(cmd)] = cmd.getretcode()
1452d57dc69SVladimir Kotal        self.outputs[str(cmd)] = cmd.getoutput()
1462d57dc69SVladimir Kotal
1472d57dc69SVladimir Kotal        return cmd.getretcode()
1482d57dc69SVladimir Kotal
1492d57dc69SVladimir Kotal    def run(self):
1502d57dc69SVladimir Kotal        """
1512d57dc69SVladimir Kotal        Run the sequence of commands and capture their output and return code.
1522d57dc69SVladimir Kotal        First command that returns code other than 0 terminates the sequence.
1532d57dc69SVladimir Kotal        If the command has return code 2, the sequence will be terminated
1542d57dc69SVladimir Kotal        however it will not be treated as error (unless the 'driveon' parameter
1552d57dc69SVladimir Kotal        is True).
1562d57dc69SVladimir Kotal
1572d57dc69SVladimir Kotal        If a command contains PROJECT_SUBST pattern, it will be replaced
1582d57dc69SVladimir Kotal        by project name, otherwise project name will be appended to the
1592d57dc69SVladimir Kotal        argument list of the command.
1602d57dc69SVladimir Kotal
1612d57dc69SVladimir Kotal        Any command entry that is a URI, will be used to submit RESTful API
1622d57dc69SVladimir Kotal        request.
1632d57dc69SVladimir Kotal        """
1642d57dc69SVladimir Kotal
1652d57dc69SVladimir Kotal        for command in self.commands:
1662d97c0a2SVladimir Kotal            cmd_value = command.get(COMMAND_PROPERTY)[0]
1672d97c0a2SVladimir Kotal            if cmd_value.startswith(URL_SUBST) or is_web_uri(cmd_value):
1682d57dc69SVladimir Kotal                try:
1692d97c0a2SVladimir Kotal                    call_rest_api(command, {PROJECT_SUBST: self.name,
17089229afdSVladimir Kotal                                            URL_SUBST: self.url},
171732be1c2SVladimir Kotal                                  self.http_headers, self.api_timeout)
1722d57dc69SVladimir Kotal                except HTTPError as e:
1732d57dc69SVladimir Kotal                    self.logger.error("RESTful command {} failed: {}".
1742d57dc69SVladimir Kotal                                      format(command, e))
1752d57dc69SVladimir Kotal                    self.failed = True
1762d57dc69SVladimir Kotal                    self.retcodes[str(command)] = FAILURE_EXITVAL
1772d57dc69SVladimir Kotal
1782d57dc69SVladimir Kotal                    break
1792d57dc69SVladimir Kotal            else:
1802d57dc69SVladimir Kotal                command_args = command.get(COMMAND_PROPERTY)
1812d57dc69SVladimir Kotal                command = Command(command_args,
1822d57dc69SVladimir Kotal                                  env_vars=command.get("env"),
183b2d29daeSVladimir Kotal                                  logger=self.logger,
1842d57dc69SVladimir Kotal                                  resource_limits=command.get("limits"),
1852d97c0a2SVladimir Kotal                                  args_subst={PROJECT_SUBST: self.name,
1862d97c0a2SVladimir Kotal                                              URL_SUBST: self.url},
1872d57dc69SVladimir Kotal                                  args_append=[self.name], excl_subst=True)
1882d57dc69SVladimir Kotal                retcode = self.run_command(command)
1892d57dc69SVladimir Kotal
1902d57dc69SVladimir Kotal                # If a command exits with non-zero return code,
1912d57dc69SVladimir Kotal                # terminate the sequence of commands.
1922d57dc69SVladimir Kotal                if retcode != SUCCESS_EXITVAL:
1932d57dc69SVladimir Kotal                    if retcode == CONTINUE_EXITVAL:
1942d57dc69SVladimir Kotal                        if not self.driveon:
1952d57dc69SVladimir Kotal                            self.logger.debug("command '{}' for project {} "
1962d57dc69SVladimir Kotal                                              "requested break".
1972d57dc69SVladimir Kotal                                              format(command, self.name))
1982d57dc69SVladimir Kotal                            self.run_cleanup()
1992d57dc69SVladimir Kotal                        else:
2002d57dc69SVladimir Kotal                            self.logger.debug("command '{}' for project {} "
2012d57dc69SVladimir Kotal                                              "requested break however "
2022d57dc69SVladimir Kotal                                              "the 'driveon' option is set "
2032d57dc69SVladimir Kotal                                              "so driving on.".
2042d57dc69SVladimir Kotal                                              format(command, self.name))
2052d57dc69SVladimir Kotal                            continue
2062d57dc69SVladimir Kotal                    else:
207923e5a3aSVladimir Kotal                        self.logger.error("command '{}' for project {} failed "
208923e5a3aSVladimir Kotal                                          "with code {}, breaking".
209923e5a3aSVladimir Kotal                                          format(command, self.name, retcode))
2102d57dc69SVladimir Kotal                        self.failed = True
2112d57dc69SVladimir Kotal                        self.run_cleanup()
2122d57dc69SVladimir Kotal
2132d57dc69SVladimir Kotal                    break
2142d57dc69SVladimir Kotal
2152d57dc69SVladimir Kotal    def run_cleanup(self):
2162d57dc69SVladimir Kotal        """
2172d57dc69SVladimir Kotal        Call cleanup sequence in case the command sequence failed
2182d57dc69SVladimir Kotal        or termination was requested.
2192d57dc69SVladimir Kotal        """
2202d57dc69SVladimir Kotal        if self.cleanup is None:
2212d57dc69SVladimir Kotal            return
2222d57dc69SVladimir Kotal
2232d57dc69SVladimir Kotal        for cleanup_cmd in self.cleanup:
2242d97c0a2SVladimir Kotal            arg0 = cleanup_cmd.get(COMMAND_PROPERTY)[0]
2252d97c0a2SVladimir Kotal            if arg0.startswith(URL_SUBST) or is_web_uri(arg0):
2262d57dc69SVladimir Kotal                try:
2272d97c0a2SVladimir Kotal                    call_rest_api(cleanup_cmd, {PROJECT_SUBST: self.name,
228b369c884SVladimir Kotal                                                URL_SUBST: self.url},
229732be1c2SVladimir Kotal                                  self.http_headers, self.api_timeout)
2302d57dc69SVladimir Kotal                except HTTPError as e:
2312d57dc69SVladimir Kotal                    self.logger.error("RESTful command {} failed: {}".
2322d57dc69SVladimir Kotal                                      format(cleanup_cmd, e))
2332d57dc69SVladimir Kotal            else:
2342d57dc69SVladimir Kotal                command_args = cleanup_cmd.get(COMMAND_PROPERTY)
2352d57dc69SVladimir Kotal                self.logger.debug("Running cleanup command '{}'".
2362d57dc69SVladimir Kotal                                  format(command_args))
2372d57dc69SVladimir Kotal                cmd = Command(command_args,
238b2d29daeSVladimir Kotal                              logger=self.logger,
2392d97c0a2SVladimir Kotal                              args_subst={PROJECT_SUBST: self.name,
2402d97c0a2SVladimir Kotal                                          URL_SUBST: self.url},
2412d57dc69SVladimir Kotal                              args_append=[self.name], excl_subst=True)
2422d57dc69SVladimir Kotal                cmd.execute()
2432d57dc69SVladimir Kotal                if cmd.getretcode() != SUCCESS_EXITVAL:
2442d57dc69SVladimir Kotal                    self.logger.error("cleanup command '{}' failed with "
2452d57dc69SVladimir Kotal                                      "code {}".
2462d57dc69SVladimir Kotal                                      format(cmd.cmd, cmd.getretcode()))
2472d57dc69SVladimir Kotal                    self.logger.info('output: {}'.format(cmd.getoutputstr()))
2482d57dc69SVladimir Kotal
249b2d29daeSVladimir Kotal    def print_outputs(self, logger, loglevel=logging.INFO, lines=False):
250b2d29daeSVladimir Kotal        """
251b2d29daeSVladimir Kotal        Print command outputs.
252b2d29daeSVladimir Kotal        """
253b2d29daeSVladimir Kotal
254b2d29daeSVladimir Kotal        logger.debug("Output for project '{}':".format(self.name))
255b2d29daeSVladimir Kotal        for cmd in self.outputs.keys():
256b2d29daeSVladimir Kotal            if self.outputs[cmd] and len(self.outputs[cmd]) > 0:
257b2d29daeSVladimir Kotal                if lines:
258b2d29daeSVladimir Kotal                    logger.log(loglevel, "Output from '{}':".format(cmd))
259b2d29daeSVladimir Kotal                    logger.log(loglevel, '{}'.format(self.get_cmd_output(cmd)))
260b2d29daeSVladimir Kotal                else:
261b2d29daeSVladimir Kotal                    logger.log(loglevel, "'{}': {}".
262b2d29daeSVladimir Kotal                               format(cmd, self.outputs[cmd]))
263b2d29daeSVladimir Kotal
2642d57dc69SVladimir Kotal    def check(self, ignore_errors):
2652d57dc69SVladimir Kotal        """
2662d57dc69SVladimir Kotal        Check the output of the commands and perform logging.
2672d57dc69SVladimir Kotal
2685a30ebc3SVladimir Kotal        Return SUCCESS_EXITVAL on success, 1 if error was detected.
2692d57dc69SVladimir Kotal        """
2702d57dc69SVladimir Kotal
2712d57dc69SVladimir Kotal        ret = SUCCESS_EXITVAL
272b2d29daeSVladimir Kotal        self.print_outputs(self.logger, loglevel=logging.DEBUG)
2732d57dc69SVladimir Kotal
2745a30ebc3SVladimir Kotal        if ignore_errors and self.name in ignore_errors:
2752d57dc69SVladimir Kotal            self.logger.debug("errors of project '{}' ignored".
2762d57dc69SVladimir Kotal                              format(self.name))
2772d57dc69SVladimir Kotal            return
2782d57dc69SVladimir Kotal
2792d57dc69SVladimir Kotal        self.logger.debug("retcodes = {}".format(self.retcodes))
2802d57dc69SVladimir Kotal        if any(rv != SUCCESS_EXITVAL and rv != CONTINUE_EXITVAL
2812d57dc69SVladimir Kotal               for rv in self.retcodes.values()):
2822d57dc69SVladimir Kotal            ret = 1
2832d57dc69SVladimir Kotal            self.logger.error("processing of project '{}' failed".
2842d57dc69SVladimir Kotal                              format(self))
2852d57dc69SVladimir Kotal            indent = "  "
2862d57dc69SVladimir Kotal            self.logger.error("{}failed commands:".format(indent))
2872d57dc69SVladimir Kotal            failed_cmds = {k: v for k, v in
2882d57dc69SVladimir Kotal                           self.retcodes.items() if v != SUCCESS_EXITVAL}
2892d57dc69SVladimir Kotal            indent = "    "
2902d57dc69SVladimir Kotal            for cmd in failed_cmds.keys():
2912d57dc69SVladimir Kotal                self.logger.error("{}'{}': {}".
2922d57dc69SVladimir Kotal                                  format(indent, cmd, failed_cmds[cmd]))
2932d57dc69SVladimir Kotal                out = self.get_cmd_output(cmd,
2942d57dc69SVladimir Kotal                                          indent=indent + "  ")
2952d57dc69SVladimir Kotal                if out:
2962d57dc69SVladimir Kotal                    self.logger.error(out)
2972d57dc69SVladimir Kotal            self.logger.error("")
2982d57dc69SVladimir Kotal
2992d57dc69SVladimir Kotal        errored_cmds = {k: v for k, v in self.outputs.items()
3002d57dc69SVladimir Kotal                        if self.re_program.match(str(v))}
3012d57dc69SVladimir Kotal        if len(errored_cmds) > 0:
3022d57dc69SVladimir Kotal            ret = 1
3032d57dc69SVladimir Kotal            self.logger.error("Command output in project '{}'"
3042d57dc69SVladimir Kotal                              " contains errors:".format(self.name))
3052d57dc69SVladimir Kotal            indent = "  "
3062d57dc69SVladimir Kotal            for cmd in errored_cmds.keys():
3072d57dc69SVladimir Kotal                self.logger.error("{}{}".format(indent, cmd))
3082d57dc69SVladimir Kotal                out = self.get_cmd_output(cmd,
3092d57dc69SVladimir Kotal                                          indent=indent + "  ")
3102d57dc69SVladimir Kotal                if out:
3112d57dc69SVladimir Kotal                    self.logger.error(out)
3122d57dc69SVladimir Kotal                self.logger.error("")
3132d57dc69SVladimir Kotal
3142d57dc69SVladimir Kotal        return ret
315