""" Copyright (c) 2024 Wind River Systems, Inc. SPDX-License-Identifier: Apache-2.0 """ import logging from software.db.api import get_instance from software.exceptions import InvalidOperation from software.release_data import SWRelease from software.states import DEPLOY_STATES from software.states import DEPLOY_HOST_STATES LOG = logging.getLogger('main_logger') deploy_state_transition = { None: [DEPLOY_STATES.START], # Fake state for no deploy in progress DEPLOY_STATES.START: [DEPLOY_STATES.START_DONE, DEPLOY_STATES.START_FAILED], DEPLOY_STATES.START_FAILED: [DEPLOY_STATES.ABORT], DEPLOY_STATES.ABORT: [DEPLOY_STATES.ABORT_DONE], DEPLOY_STATES.START_DONE: [DEPLOY_STATES.ABORT, DEPLOY_STATES.HOST], DEPLOY_STATES.HOST: [DEPLOY_STATES.HOST, DEPLOY_STATES.ABORT, DEPLOY_STATES.HOST_FAILED, DEPLOY_STATES.HOST_DONE], DEPLOY_STATES.HOST_FAILED: [DEPLOY_STATES.HOST, # deploy-host can reattempt DEPLOY_STATES.ABORT, DEPLOY_STATES.HOST_FAILED, DEPLOY_STATES.HOST_DONE], DEPLOY_STATES.HOST_DONE: [DEPLOY_STATES.ABORT, DEPLOY_STATES.ACTIVATE], DEPLOY_STATES.ACTIVATE: [DEPLOY_STATES.ACTIVATE_DONE, DEPLOY_STATES.ACTIVATE_FAILED], DEPLOY_STATES.ACTIVATE_DONE: [DEPLOY_STATES.ABORT, None], # abort after deploy-activated? DEPLOY_STATES.ACTIVATE_FAILED: [DEPLOY_STATES.ACTIVATE, DEPLOY_STATES.ABORT], DEPLOY_STATES.ABORT_DONE: [] # waitng for being deleted } class DeployState(object): _callbacks = [] _instance = None @staticmethod def register_event_listener(callback): """register event listener to be triggered when a state transition is completed""" if callback is not None: if callback not in DeployState._callbacks: LOG.debug("Register event listener %s", callback.__qualname__) DeployState._callbacks.append(callback) @staticmethod def get_deploy_state(): db_api_instance = get_instance() deploys = db_api_instance.get_deploy_all() if not deploys: state = None # No deploy in progress == None else: deploy = deploys[0] state = DEPLOY_STATES(deploy['state']) return state @staticmethod def get_instance(): if DeployState._instance is None: DeployState._instance = DeployState() return DeployState._instance @staticmethod def host_deploy_updated(_hostname, _host_new_state): db_api_instance = get_instance() deploy_hosts = db_api_instance.get_deploy_host() deploy_state = DeployState.get_instance() all_states = [] for deploy_host in deploy_hosts: if deploy_host['state'] not in all_states: all_states.append(deploy_host['state']) LOG.info("Host deploy state %s" % str(all_states)) if DEPLOY_HOST_STATES.FAILED.value in all_states: deploy_state.deploy_host_failed() elif DEPLOY_HOST_STATES.PENDING.value in all_states or \ DEPLOY_HOST_STATES.DEPLOYING.value in all_states: deploy_state.deploy_host() elif all_states == [DEPLOY_HOST_STATES.DEPLOYED.value]: deploy_state.deploy_host_completed() def __init__(self): self._from_release = None self._to_release = None self._reboot_required = None def check_transition(self, target_state: DEPLOY_STATES): cur_state = DeployState.get_deploy_state() if cur_state is not None: cur_state = DEPLOY_STATES(cur_state) if target_state in deploy_state_transition[cur_state]: return True # TODO(bqian) reverse lookup the operation that is not permitted, as feedback msg = f"Deploy state transform not permitted from {str(cur_state)} to {str(target_state)}" LOG.info(msg) return False def transform(self, target_state: DEPLOY_STATES): db_api = get_instance() db_api.begin_update() try: if self.check_transition(target_state): # None means not existing or deleting if target_state is not None: db_api.update_deploy(target_state) else: # TODO(bqian) check the current state, and provide guidence on what is # the possible next operation if target_state is None: msg = "Deployment can not deleted in current state." else: msg = "Host can not transform to %s from current state" % target_state.value() raise InvalidOperation(msg) finally: db_api.end_update() for callback in DeployState._callbacks: LOG.debug("Calling event listener %s", callback.__qualname__) callback(target_state) # below are list of events to drive the FSM def start(self, from_release, to_release, feed_repo, commit_id, reboot_required): # start is special, it needs to create the deploy entity if isinstance(from_release, SWRelease): from_release = from_release.sw_release if isinstance(to_release, SWRelease): to_release = to_release.sw_release msg = f"Start deploy {to_release}, current sw {from_release}" LOG.info(msg) db_api_instance = get_instance() db_api_instance.create_deploy(from_release, to_release, feed_repo, commit_id, reboot_required) def start_failed(self): self.transform(DEPLOY_STATES.START_FAILED) def start_done(self): self.transform(DEPLOY_STATES.START_DONE) def deploy_host(self): self.transform(DEPLOY_STATES.HOST) def abort(self): self.transform(DEPLOY_STATES.ABORT) def deploy_host_completed(self): # depends on the deploy state, the deploy can be transformed # to HOST_DONE (from DEPLOY_HOST) or ABORT_DONE (ABORT) state = DeployState.get_deploy_state() if state == DEPLOY_STATES.ABORT: self.transform(DEPLOY_STATES.ABORT_DONE) else: self.transform(DEPLOY_STATES.HOST_DONE) def deploy_host_failed(self): self.transform(DEPLOY_STATES.HOST_FAILED) def activate(self): self.transform(DEPLOY_STATES.ACTIVATE) def activate_completed(self): self.transform(DEPLOY_STATES.ACTIVATE_DONE) def activate_failed(self): self.transform(DEPLOY_STATES.ACTIVATE_FAILED) def completed(self): self.transform(None) # delete the deploy and deploy host entities db_api = get_instance() db_api.begin_update() try: db_api.delete_deploy_host_all() db_api.delete_deploy() finally: db_api.end_update() def require_deploy_state(require_states, prompt): def wrap(func): def exec_op(*args, **kwargs): state = DeployState.get_deploy_state() if state in require_states: res = func(*args, **kwargs) return res else: msg = "" if prompt: msg = prompt.format(state=state, require_states=require_states) raise InvalidOperation(msg) return exec_op return wrap