diff --git a/software/setup.cfg b/software/setup.cfg index 255ebb5d..7c696f7b 100644 --- a/software/setup.cfg +++ b/software/setup.cfg @@ -37,6 +37,7 @@ console_scripts = software-agent = software.software_agent:main software-migrate = software.utilities.migrate:migrate software-deploy-update = software.utilities.update_deploy_state:update_state + software-deploy-activate = software.utilities.activate:activate [wheel] diff --git a/software/software/deploy_state.py b/software/software/deploy_state.py index 6884ad10..18587c72 100644 --- a/software/software/deploy_state.py +++ b/software/software/deploy_state.py @@ -191,8 +191,9 @@ def require_deploy_state(require_states, prompt): return res else: msg = "" + require_states_text = ", ".join([state.value for state in require_states]) if prompt: - msg = prompt.format(state=state, require_states=require_states) + msg = prompt.format(state=state, require_states=require_states_text) raise InvalidOperation(msg) return exec_op return wrap diff --git a/software/software/software_controller.py b/software/software/software_controller.py index 2c33a50b..602fe891 100644 --- a/software/software/software_controller.py +++ b/software/software/software_controller.py @@ -51,6 +51,7 @@ from software.exceptions import ReleaseInvalidRequest from software.exceptions import ReleaseValidationFailure from software.exceptions import ReleaseIsoDeleteFailure from software.exceptions import SoftwareServiceError +from software.exceptions import InvalidOperation from software.release_data import reload_release_data from software.release_data import get_SWReleaseCollection from software.software_functions import collect_current_load_for_hosts @@ -708,7 +709,7 @@ class SWMessageDeployStateChanged(messages.PatchMessage): self.valid = True self.agent = None - valid_agents = ['deploy-start'] + valid_agents = ['deploy-start', 'deploy-activate'] if 'agent' in data: self.agent = data['agent'] else: @@ -722,7 +723,9 @@ class SWMessageDeployStateChanged(messages.PatchMessage): valid_state = { DEPLOY_STATES.START_DONE.value: DEPLOY_STATES.START_DONE, - DEPLOY_STATES.START_FAILED.value: DEPLOY_STATES.START_FAILED + DEPLOY_STATES.START_FAILED.value: DEPLOY_STATES.START_FAILED, + DEPLOY_STATES.ACTIVATE_FAILED.value: DEPLOY_STATES.ACTIVATE_FAILED, + DEPLOY_STATES.ACTIVATE_DONE.value: DEPLOY_STATES.ACTIVATE_DONE } if 'deploy-state' in data and data['deploy-state']: deploy_state = data['deploy-state'] @@ -2290,7 +2293,9 @@ class PatchController(PatchService): deploy_state = DeployState.get_instance() state_event = { DEPLOY_STATES.START_DONE: deploy_state.start_done, - DEPLOY_STATES.START_FAILED: deploy_state.start_failed + DEPLOY_STATES.START_FAILED: deploy_state.start_failed, + DEPLOY_STATES.ACTIVATE_DONE: deploy_state.activate_completed, + DEPLOY_STATES.ACTIVATE_FAILED: deploy_state.activate_failed } if new_state in state_event: state_event[new_state]() @@ -2680,11 +2685,66 @@ class PatchController(PatchService): return dict(info=msg_info, warning=msg_warning, error=msg_error) def _activate(self): - # TODO(bqian) activate the deployment + deploy = self.db_api_instance.get_deploy_all() + if deploy: + deploy = deploy[0] + else: + msg = "Deployment is missing unexpectedly" + raise InvalidOperation(msg) + + deploying = ReleaseState(release_state=states.DEPLOYING) + if deploying.is_major_release_deployment(): + return self._activate_major_release(deploy) + else: + return self.activate_patching_release() + + def activate_patching_release(self): + deploy_state = DeployState.get_instance() + deploy_state.activate() + # patching release activate operations go here + deploy_state.activate_completed() return True + def _activate_major_release(self, deploy): + cmd_path = "/usr/bin/software-deploy-activate" + from_release = utils.get_major_release_version(deploy.get("from_release")) + to_release = utils.get_major_release_version(deploy.get("to_release")) + + upgrade_activate_cmd = [cmd_path, from_release, to_release] + + try: + LOG.info("starting subprocess %s" % ' '.join(upgrade_activate_cmd)) + subprocess.Popen(' '.join(upgrade_activate_cmd), start_new_session=True, shell=True) + LOG.info("subprocess started") + except subprocess.SubprocessError as e: + LOG.error("Failed to start command: %s. Error %s" % (' '.join(upgrade_activate_cmd), e)) + return False + + return True + + def _check_pre_activate(self): + # check current deployment, deploy to all hosts have completed, + # the deploy state is host-done, or + # activate-failed' as reattempt from a previous failed activate + deploy_state = DeployState.get_deploy_state() + if deploy_state not in [DEPLOY_STATES.HOST_DONE, DEPLOY_STATES.ACTIVATE_FAILED]: + msg = "Must complete deploying all hosts before activating the deployment" + raise InvalidOperation(msg) + + deploy_hosts = self.db_api_instance.get_deploy_host() + invalid_hosts = [] + for deploy_host in deploy_hosts: + if deploy_host['state'] not in [states.DEPLOYED]: + invalid_hosts.append(deploy_host) + + if len(invalid_hosts) > 0: + msg = "All hosts must have completed deployment before activating the deployment" + for invalid_host in invalid_hosts: + msg += "%s: %s\n" % (invalid_host["hostname"], invalid_host["state"]) + raise InvalidOperation(msg) + @require_deploy_state([DEPLOY_STATES.HOST_DONE, DEPLOY_STATES.ACTIVATE_FAILED], - "Must complete deploying all hosts before activating the deployment") + "Activate deployment only when current deployment state is {require_states}") def software_deploy_activate_api(self) -> dict: """ Activates the deployment associated with the release @@ -2694,15 +2754,17 @@ class PatchController(PatchService): msg_warning = "" msg_error = "" + self._check_pre_activate() + deploy_state = DeployState.get_instance() deploy_state.activate() - if self._activate(): - deploy_state.activate_completed() - msg_info += "Deployment has been activated.\n" - else: + try: + self._activate() + msg_info = "Deploy activate has started" + except Exception: deploy_state.activate_failed() - msg_error += "Deployment activation has failed.\n" + raise return dict(info=msg_info, warning=msg_warning, error=msg_error) diff --git a/software/software/utilities/activate.py b/software/software/utilities/activate.py new file mode 100644 index 00000000..3057b03f --- /dev/null +++ b/software/software/utilities/activate.py @@ -0,0 +1,58 @@ +# +# Copyright (c) 2024 Wind River Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# +import argparse + +from oslo_log import log + +from software.states import DEPLOY_STATES +from software.utilities.update_deploy_state import update_deploy_state +from software.utilities.utils import execute_migration_scripts +from software.utilities.utils import ACTION_ACTIVATE + +LOG = log.getLogger(__name__) + + +def do_activate(from_release, to_release): + agent = 'deploy-activate' + res = True + state = DEPLOY_STATES.ACTIVATE_DONE.value + try: + execute_migration_scripts(from_release, to_release, ACTION_ACTIVATE) + except Exception: + state = DEPLOY_STATES.ACTIVATE_FAILED.value + res = False + finally: + try: + update_deploy_state(agent, deploy_state=state) + if res: + LOG.info("Deploy activate completed successfully") + else: + LOG.error("Deploy activate failed") + except Exception: + LOG.error("Update deploy state failed") + res = False + return res + + +def activate(): + # this is the entry point to start data migration + + parser = argparse.ArgumentParser(add_help=False) + + parser.add_argument("from_release", + default=False, + help="From release") + + parser.add_argument("to_release", + default=False, + help="To release") + + args = parser.parse_args() + + if do_activate(args.from_release, args.to_release): + exit(0) + else: + exit(1) diff --git a/software/software/utilities/update_deploy_state.py b/software/software/utilities/update_deploy_state.py index 96880410..d5bc7e99 100644 --- a/software/software/utilities/update_deploy_state.py +++ b/software/software/utilities/update_deploy_state.py @@ -32,7 +32,7 @@ def get_udp_socket(server_addr, server_port): return sock -def update_deploy_state(server_addr, server_port, agent, deploy_state=None, host=None, host_state=None, timeout=1): +def update_deploy_state(agent, deploy_state=None, host=None, host_state=None, timeout=1): """ Send MessageDeployStateChanged message to software-controller via upd packet, wait for ack or raise exception. @@ -47,6 +47,10 @@ def update_deploy_state(server_addr, server_port, agent, deploy_state=None, host } """ + server_addr = "controller" + cfg.read_config() + server_port = cfg.controller_port + msg = { "msgtype": PATCHMSG_DEPLOY_STATE_CHANGED, "msgversion": 1, @@ -104,9 +108,5 @@ def update_state(): args = parser.parse_args() - server = "controller" - cfg.read_config() - server_port = cfg.controller_port - update_deploy_state(server, int(server_port), args.agent, - deploy_state=args.state, + update_deploy_state(args.agent, deploy_state=args.state, host=args.host, host_state=args.host_state)