xref: /OpenGrok/tools/src/main/python/opengrok_tools/utils/commandsequence.py (revision 4f5a8888d0f4b6c65af6583c44c6db483a95ad86)
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
2696aeefc4SVladimir Kotalfrom requests.exceptions import RequestException
272d57dc69SVladimir Kotal
282d57dc69SVladimir Kotalfrom .command import Command
292d57dc69SVladimir Kotalfrom .exitvals import (
302d57dc69SVladimir Kotal    CONTINUE_EXITVAL,
312d57dc69SVladimir Kotal    SUCCESS_EXITVAL,
322d57dc69SVladimir Kotal    FAILURE_EXITVAL
332d57dc69SVladimir Kotal)
342d57dc69SVladimir Kotalfrom .restful import call_rest_api
35c41895f8SVladimir Kotalfrom .patterns import PROJECT_SUBST, COMMAND_PROPERTY, CALL_PROPERTY, URL_SUBST
362d57dc69SVladimir Kotalimport re
372d57dc69SVladimir Kotal
382d57dc69SVladimir Kotal
3980df37bbSVladimir Kotalclass CommandConfigurationException(Exception):
4080df37bbSVladimir Kotal    pass
4180df37bbSVladimir Kotal
4280df37bbSVladimir Kotal
4380df37bbSVladimir Kotaldef check_command_property(command):
4480df37bbSVladimir Kotal    """
4580df37bbSVladimir Kotal    Check if the 'commands' parameter of CommandSequenceBase() has the right structure
4680df37bbSVladimir Kotal    w.r.t. individual commands.
4780df37bbSVladimir Kotal    :param command: command element
4880df37bbSVladimir Kotal    """
4980df37bbSVladimir Kotal    if not isinstance(command, dict):
5077cacb4dSVladimir Kotal        raise CommandConfigurationException("command '{}' is not a dictionary".format(command))
5180df37bbSVladimir Kotal
5280df37bbSVladimir Kotal    command_value = command.get(COMMAND_PROPERTY)
53c41895f8SVladimir Kotal    call_value = command.get(CALL_PROPERTY)
54c41895f8SVladimir Kotal    if command_value is None and call_value is None:
55c41895f8SVladimir Kotal        raise CommandConfigurationException(f"command dictionary has unknown key: {command}")
5680df37bbSVladimir Kotal
57c41895f8SVladimir Kotal    if command_value and not isinstance(command_value, list):
5880df37bbSVladimir Kotal        raise CommandConfigurationException("command value not a list: {}".
5980df37bbSVladimir Kotal                                            format(command_value))
60c41895f8SVladimir Kotal    if call_value and not isinstance(call_value, dict):
61c41895f8SVladimir Kotal        raise CommandConfigurationException("call value not a dictionary: {}".
62c41895f8SVladimir Kotal                                            format(call_value))
6380df37bbSVladimir Kotal
6480df37bbSVladimir Kotal
652d57dc69SVladimir Kotalclass CommandSequenceBase:
662d57dc69SVladimir Kotal    """
672d57dc69SVladimir Kotal    Wrap the run of a set of Command instances.
682d57dc69SVladimir Kotal
692d57dc69SVladimir Kotal    This class intentionally does not contain any logging
702d57dc69SVladimir Kotal    so that it can be passed through Pool.map().
712d57dc69SVladimir Kotal    """
722d57dc69SVladimir Kotal
732d57dc69SVladimir Kotal    def __init__(self, name, commands, loglevel=logging.INFO, cleanup=None,
74732be1c2SVladimir Kotal                 driveon=False, url=None, env=None, http_headers=None,
75*4f5a8888SVladimir Kotal                 api_timeout=None, async_api_timeout=None):
762d57dc69SVladimir Kotal        self.name = name
7780df37bbSVladimir Kotal
7880df37bbSVladimir Kotal        if commands is None:
7980df37bbSVladimir Kotal            raise CommandConfigurationException("commands is None")
8080df37bbSVladimir Kotal        if not isinstance(commands, list):
8180df37bbSVladimir Kotal            raise CommandConfigurationException("commands is not a list")
822d57dc69SVladimir Kotal        self.commands = commands
8380df37bbSVladimir Kotal        for command in self.commands:
8480df37bbSVladimir Kotal            check_command_property(command)
8580df37bbSVladimir Kotal
862d57dc69SVladimir Kotal        self.failed = False
872d57dc69SVladimir Kotal        self.retcodes = {}
882d57dc69SVladimir Kotal        self.outputs = {}
892d57dc69SVladimir Kotal
9080df37bbSVladimir Kotal        if cleanup and not isinstance(cleanup, list):
9180df37bbSVladimir Kotal            raise CommandConfigurationException("cleanup is not a list of commands")
922d57dc69SVladimir Kotal        self.cleanup = cleanup
9380df37bbSVladimir Kotal        if self.cleanup:
9480df37bbSVladimir Kotal            for command in self.cleanup:
9580df37bbSVladimir Kotal                check_command_property(command)
9680df37bbSVladimir Kotal
972d57dc69SVladimir Kotal        self.loglevel = loglevel
982d57dc69SVladimir Kotal        self.driveon = driveon
99b2d29daeSVladimir Kotal        self.env = env
10089229afdSVladimir Kotal        self.http_headers = http_headers
101732be1c2SVladimir Kotal        self.api_timeout = api_timeout
102*4f5a8888SVladimir Kotal        self.async_api_timeout = async_api_timeout
1032d57dc69SVladimir Kotal
1042d97c0a2SVladimir Kotal        self.url = url
1052d97c0a2SVladimir Kotal
1062d57dc69SVladimir Kotal    def __str__(self):
1072d57dc69SVladimir Kotal        return str(self.name)
1082d57dc69SVladimir Kotal
1092d57dc69SVladimir Kotal    def get_cmd_output(self, cmd, indent=""):
110b2d29daeSVladimir Kotal        """
111b2d29daeSVladimir Kotal        :param cmd: command
112b2d29daeSVladimir Kotal        :param indent: prefix for each line
113b2d29daeSVladimir Kotal        :return: command output as string
114b2d29daeSVladimir Kotal        """
115b2d29daeSVladimir Kotal
11680df37bbSVladimir Kotal        str_out = ""
1172d57dc69SVladimir Kotal        for line in self.outputs.get(cmd, []):
11880df37bbSVladimir Kotal            str_out += '{}{}'.format(indent, line)
1192d57dc69SVladimir Kotal
12080df37bbSVladimir Kotal        return str_out
1212d57dc69SVladimir Kotal
1222d57dc69SVladimir Kotal    def fill(self, retcodes, outputs, failed):
1232d57dc69SVladimir Kotal        self.retcodes = retcodes
1242d57dc69SVladimir Kotal        self.outputs = outputs
1252d57dc69SVladimir Kotal        self.failed = failed
1262d57dc69SVladimir Kotal
1272d57dc69SVladimir Kotal
1282d57dc69SVladimir Kotalclass CommandSequence(CommandSequenceBase):
1292d57dc69SVladimir Kotal    re_program = re.compile('ERROR[:]*\\s+')
1302d57dc69SVladimir Kotal
1312d57dc69SVladimir Kotal    def __init__(self, base):
1322d57dc69SVladimir Kotal        super().__init__(base.name, base.commands, loglevel=base.loglevel,
1332d97c0a2SVladimir Kotal                         cleanup=base.cleanup, driveon=base.driveon,
134b369c884SVladimir Kotal                         url=base.url, env=base.env,
135732be1c2SVladimir Kotal                         http_headers=base.http_headers,
136*4f5a8888SVladimir Kotal                         api_timeout=base.api_timeout,
137*4f5a8888SVladimir Kotal                         async_api_timeout=base.async_api_timeout)
1382d57dc69SVladimir Kotal
1392d57dc69SVladimir Kotal        self.logger = logging.getLogger(__name__)
1402d57dc69SVladimir Kotal        self.logger.setLevel(base.loglevel)
1412d57dc69SVladimir Kotal
1422d57dc69SVladimir Kotal    def run_command(self, cmd):
1432d57dc69SVladimir Kotal        """
1442d57dc69SVladimir Kotal        Execute a command and return its return code.
1452d57dc69SVladimir Kotal        """
1462d57dc69SVladimir Kotal        cmd.execute()
1472d57dc69SVladimir Kotal        self.retcodes[str(cmd)] = cmd.getretcode()
1482d57dc69SVladimir Kotal        self.outputs[str(cmd)] = cmd.getoutput()
1492d57dc69SVladimir Kotal
1502d57dc69SVladimir Kotal        return cmd.getretcode()
1512d57dc69SVladimir Kotal
1522d57dc69SVladimir Kotal    def run(self):
1532d57dc69SVladimir Kotal        """
1542d57dc69SVladimir Kotal        Run the sequence of commands and capture their output and return code.
1552d57dc69SVladimir Kotal        First command that returns code other than 0 terminates the sequence.
1562d57dc69SVladimir Kotal        If the command has return code 2, the sequence will be terminated
1572d57dc69SVladimir Kotal        however it will not be treated as error (unless the 'driveon' parameter
1582d57dc69SVladimir Kotal        is True).
1592d57dc69SVladimir Kotal
1602d57dc69SVladimir Kotal        If a command contains PROJECT_SUBST pattern, it will be replaced
1612d57dc69SVladimir Kotal        by project name, otherwise project name will be appended to the
1622d57dc69SVladimir Kotal        argument list of the command.
1632d57dc69SVladimir Kotal
164519c2abbSVladimir Kotal        Any command entry that is a URI, will be used to submit REST API
1652d57dc69SVladimir Kotal        request.
1662d57dc69SVladimir Kotal        """
1672d57dc69SVladimir Kotal
1682d57dc69SVladimir Kotal        for command in self.commands:
169c41895f8SVladimir Kotal            if command.get(CALL_PROPERTY):
1702d57dc69SVladimir Kotal                try:
171c41895f8SVladimir Kotal                    call_rest_api(command.get(CALL_PROPERTY),
172c41895f8SVladimir Kotal                                  {PROJECT_SUBST: self.name,
17389229afdSVladimir Kotal                                   URL_SUBST: self.url},
174*4f5a8888SVladimir Kotal                                  self.http_headers,
175*4f5a8888SVladimir Kotal                                  self.api_timeout,
176*4f5a8888SVladimir Kotal                                  self.async_api_timeout)
17796aeefc4SVladimir Kotal                except RequestException as e:
178c41895f8SVladimir Kotal                    self.logger.error("REST API call {} failed: {}".
1792d57dc69SVladimir Kotal                                      format(command, e))
1802d57dc69SVladimir Kotal                    self.failed = True
1812d57dc69SVladimir Kotal                    self.retcodes[str(command)] = FAILURE_EXITVAL
1822d57dc69SVladimir Kotal
1832d57dc69SVladimir Kotal                    break
184c41895f8SVladimir Kotal            elif command.get(COMMAND_PROPERTY):
1852d57dc69SVladimir Kotal                command_args = command.get(COMMAND_PROPERTY)
1862d57dc69SVladimir Kotal                command = Command(command_args,
1872d57dc69SVladimir Kotal                                  env_vars=command.get("env"),
188b2d29daeSVladimir Kotal                                  logger=self.logger,
1892d57dc69SVladimir Kotal                                  resource_limits=command.get("limits"),
1902d97c0a2SVladimir Kotal                                  args_subst={PROJECT_SUBST: self.name,
1912d97c0a2SVladimir Kotal                                              URL_SUBST: self.url},
1922d57dc69SVladimir Kotal                                  args_append=[self.name], excl_subst=True)
193c41895f8SVladimir Kotal                ret_code = self.run_command(command)
1942d57dc69SVladimir Kotal
1952d57dc69SVladimir Kotal                # If a command exits with non-zero return code,
1962d57dc69SVladimir Kotal                # terminate the sequence of commands.
197c41895f8SVladimir Kotal                if ret_code != SUCCESS_EXITVAL:
198c41895f8SVladimir Kotal                    if ret_code == CONTINUE_EXITVAL:
1992d57dc69SVladimir Kotal                        if not self.driveon:
2002d57dc69SVladimir Kotal                            self.logger.debug("command '{}' for project {} "
2012d57dc69SVladimir Kotal                                              "requested break".
2022d57dc69SVladimir Kotal                                              format(command, self.name))
2032d57dc69SVladimir Kotal                            self.run_cleanup()
2042d57dc69SVladimir Kotal                        else:
2052d57dc69SVladimir Kotal                            self.logger.debug("command '{}' for project {} "
2062d57dc69SVladimir Kotal                                              "requested break however "
2072d57dc69SVladimir Kotal                                              "the 'driveon' option is set "
2082d57dc69SVladimir Kotal                                              "so driving on.".
2092d57dc69SVladimir Kotal                                              format(command, self.name))
2102d57dc69SVladimir Kotal                            continue
2112d57dc69SVladimir Kotal                    else:
212923e5a3aSVladimir Kotal                        self.logger.error("command '{}' for project {} failed "
213923e5a3aSVladimir Kotal                                          "with code {}, breaking".
214c41895f8SVladimir Kotal                                          format(command, self.name, ret_code))
2152d57dc69SVladimir Kotal                        self.failed = True
2162d57dc69SVladimir Kotal                        self.run_cleanup()
2172d57dc69SVladimir Kotal
2182d57dc69SVladimir Kotal                    break
219c41895f8SVladimir Kotal            else:
220c41895f8SVladimir Kotal                raise Exception(f"unknown command: {command}")
2212d57dc69SVladimir Kotal
2222d57dc69SVladimir Kotal    def run_cleanup(self):
2232d57dc69SVladimir Kotal        """
2242d57dc69SVladimir Kotal        Call cleanup sequence in case the command sequence failed
2252d57dc69SVladimir Kotal        or termination was requested.
2262d57dc69SVladimir Kotal        """
2272d57dc69SVladimir Kotal        if self.cleanup is None:
2282d57dc69SVladimir Kotal            return
2292d57dc69SVladimir Kotal
2302d57dc69SVladimir Kotal        for cleanup_cmd in self.cleanup:
231c41895f8SVladimir Kotal            if cleanup_cmd.get(CALL_PROPERTY):
2322d57dc69SVladimir Kotal                try:
233c41895f8SVladimir Kotal                    call_rest_api(cleanup_cmd.get(CALL_PROPERTY),
234c41895f8SVladimir Kotal                                  {PROJECT_SUBST: self.name,
235b369c884SVladimir Kotal                                   URL_SUBST: self.url},
236*4f5a8888SVladimir Kotal                                  self.http_headers,
237*4f5a8888SVladimir Kotal                                  self.api_timeout,
238*4f5a8888SVladimir Kotal                                  self.async_api_timeout)
23996aeefc4SVladimir Kotal                except RequestException as e:
240c41895f8SVladimir Kotal                    self.logger.error("API call {} failed: {}".
2412d57dc69SVladimir Kotal                                      format(cleanup_cmd, e))
242c41895f8SVladimir Kotal            elif cleanup_cmd.get(COMMAND_PROPERTY):
2432d57dc69SVladimir Kotal                command_args = cleanup_cmd.get(COMMAND_PROPERTY)
2442d57dc69SVladimir Kotal                self.logger.debug("Running cleanup command '{}'".
2452d57dc69SVladimir Kotal                                  format(command_args))
2462d57dc69SVladimir Kotal                cmd = Command(command_args,
247b2d29daeSVladimir Kotal                              logger=self.logger,
2482d97c0a2SVladimir Kotal                              args_subst={PROJECT_SUBST: self.name,
2492d97c0a2SVladimir Kotal                                          URL_SUBST: self.url},
2502d57dc69SVladimir Kotal                              args_append=[self.name], excl_subst=True)
2512d57dc69SVladimir Kotal                cmd.execute()
2522d57dc69SVladimir Kotal                if cmd.getretcode() != SUCCESS_EXITVAL:
2532d57dc69SVladimir Kotal                    self.logger.error("cleanup command '{}' failed with "
2542d57dc69SVladimir Kotal                                      "code {}".
2552d57dc69SVladimir Kotal                                      format(cmd.cmd, cmd.getretcode()))
2562d57dc69SVladimir Kotal                    self.logger.info('output: {}'.format(cmd.getoutputstr()))
257c41895f8SVladimir Kotal            else:
258c41895f8SVladimir Kotal                raise Exception(f"unknown type of action: {cleanup_cmd}")
2592d57dc69SVladimir Kotal
260b2d29daeSVladimir Kotal    def print_outputs(self, logger, loglevel=logging.INFO, lines=False):
261b2d29daeSVladimir Kotal        """
262b2d29daeSVladimir Kotal        Print command outputs.
263b2d29daeSVladimir Kotal        """
264b2d29daeSVladimir Kotal
265b2d29daeSVladimir Kotal        logger.debug("Output for project '{}':".format(self.name))
266b2d29daeSVladimir Kotal        for cmd in self.outputs.keys():
267b2d29daeSVladimir Kotal            if self.outputs[cmd] and len(self.outputs[cmd]) > 0:
268b2d29daeSVladimir Kotal                if lines:
269b2d29daeSVladimir Kotal                    logger.log(loglevel, "Output from '{}':".format(cmd))
270b2d29daeSVladimir Kotal                    logger.log(loglevel, '{}'.format(self.get_cmd_output(cmd)))
271b2d29daeSVladimir Kotal                else:
272b2d29daeSVladimir Kotal                    logger.log(loglevel, "'{}': {}".
273b2d29daeSVladimir Kotal                               format(cmd, self.outputs[cmd]))
274b2d29daeSVladimir Kotal
2752d57dc69SVladimir Kotal    def check(self, ignore_errors):
2762d57dc69SVladimir Kotal        """
2772d57dc69SVladimir Kotal        Check the output of the commands and perform logging.
2782d57dc69SVladimir Kotal
2795a30ebc3SVladimir Kotal        Return SUCCESS_EXITVAL on success, 1 if error was detected.
2802d57dc69SVladimir Kotal        """
2812d57dc69SVladimir Kotal
2822d57dc69SVladimir Kotal        ret = SUCCESS_EXITVAL
283b2d29daeSVladimir Kotal        self.print_outputs(self.logger, loglevel=logging.DEBUG)
2842d57dc69SVladimir Kotal
2855a30ebc3SVladimir Kotal        if ignore_errors and self.name in ignore_errors:
2862d57dc69SVladimir Kotal            self.logger.debug("errors of project '{}' ignored".
2872d57dc69SVladimir Kotal                              format(self.name))
2882d57dc69SVladimir Kotal            return
2892d57dc69SVladimir Kotal
2902d57dc69SVladimir Kotal        self.logger.debug("retcodes = {}".format(self.retcodes))
2912d57dc69SVladimir Kotal        if any(rv != SUCCESS_EXITVAL and rv != CONTINUE_EXITVAL
2922d57dc69SVladimir Kotal               for rv in self.retcodes.values()):
2932d57dc69SVladimir Kotal            ret = 1
2942d57dc69SVladimir Kotal            self.logger.error("processing of project '{}' failed".
2952d57dc69SVladimir Kotal                              format(self))
2962d57dc69SVladimir Kotal            indent = "  "
2972d57dc69SVladimir Kotal            self.logger.error("{}failed commands:".format(indent))
2982d57dc69SVladimir Kotal            failed_cmds = {k: v for k, v in
2992d57dc69SVladimir Kotal                           self.retcodes.items() if v != SUCCESS_EXITVAL}
3002d57dc69SVladimir Kotal            indent = "    "
3012d57dc69SVladimir Kotal            for cmd in failed_cmds.keys():
3022d57dc69SVladimir Kotal                self.logger.error("{}'{}': {}".
3032d57dc69SVladimir Kotal                                  format(indent, cmd, failed_cmds[cmd]))
3042d57dc69SVladimir Kotal                out = self.get_cmd_output(cmd,
3052d57dc69SVladimir Kotal                                          indent=indent + "  ")
3062d57dc69SVladimir Kotal                if out:
3072d57dc69SVladimir Kotal                    self.logger.error(out)
3082d57dc69SVladimir Kotal            self.logger.error("")
3092d57dc69SVladimir Kotal
3102d57dc69SVladimir Kotal        errored_cmds = {k: v for k, v in self.outputs.items()
3112d57dc69SVladimir Kotal                        if self.re_program.match(str(v))}
3122d57dc69SVladimir Kotal        if len(errored_cmds) > 0:
3132d57dc69SVladimir Kotal            ret = 1
3142d57dc69SVladimir Kotal            self.logger.error("Command output in project '{}'"
3152d57dc69SVladimir Kotal                              " contains errors:".format(self.name))
3162d57dc69SVladimir Kotal            indent = "  "
3172d57dc69SVladimir Kotal            for cmd in errored_cmds.keys():
3182d57dc69SVladimir Kotal                self.logger.error("{}{}".format(indent, cmd))
3192d57dc69SVladimir Kotal                out = self.get_cmd_output(cmd,
3202d57dc69SVladimir Kotal                                          indent=indent + "  ")
3212d57dc69SVladimir Kotal                if out:
3222d57dc69SVladimir Kotal                    self.logger.error(out)
3232d57dc69SVladimir Kotal                self.logger.error("")
3242d57dc69SVladimir Kotal
3252d57dc69SVladimir Kotal        return ret
326