Merge "Implementing Lock and Unlock upgrade strategy states"

This commit is contained in:
Zuul 2020-06-02 14:58:05 +00:00 committed by Gerrit Code Review
commit 3514f5cd9a
21 changed files with 941 additions and 58 deletions

View File

@ -80,11 +80,38 @@ class SysinvClient(base.DriverBase):
except exceptions.ServiceUnavailable:
raise
def get_host(self, hostname_or_id):
"""Get a host by its hostname or id."""
return self.sysinv_client.ihost.get(hostname_or_id)
def get_controller_hosts(self):
"""Get a list of controller hosts."""
return self.sysinv_client.ihost.list_personality(
sysinv_constants.CONTROLLER)
def _do_host_action(self, host_id, action_value):
"""Protected method to invoke an action on a host."""
patch = [{'op': 'replace',
'path': '/action',
'value': action_value}, ]
return self.sysinv_client.ihost.update(host_id, patch)
def lock_host(self, host_id, force=False):
"""Lock a host"""
if force:
action_value = 'force-lock'
else:
action_value = 'lock'
return self._do_host_action(host_id, action_value)
def unlock_host(self, host_id, force=False):
"""Unlock a host"""
if force:
action_value = 'force-unlock'
else:
action_value = 'unlock'
return self._do_host_action(host_id, action_value)
def get_management_interface(self, hostname):
"""Get the management interface for a host."""
interfaces = self.sysinv_client.iinterface.list(hostname)

View File

@ -39,6 +39,10 @@ MANAGEMENT_MANAGED = "managed"
AVAILABILITY_OFFLINE = "offline"
AVAILABILITY_ONLINE = "online"
# Admin status for hosts
ADMIN_LOCKED = 'locked'
ADMIN_UNLOCKED = 'unlocked'
# Subcloud sync status
SYNC_STATUS_UNKNOWN = "unknown"
SYNC_STATUS_IN_SYNC = "in-sync"
@ -99,7 +103,7 @@ STRATEGY_STATE_FAILED = "failed"
STRATEGY_STATE_INSTALLING_LICENSE = "installing license"
STRATEGY_STATE_IMPORTING_LOAD = "importing load"
STRATEGY_STATE_STARTING = "starting"
STRATEGY_STATE_STARTING_UPGRADE = "starting upgrade"
STRATEGY_STATE_LOCKING_CONTROLLER = "locking controller"
STRATEGY_STATE_UPGRADING_SIMPLEX = "upgrading simplex"
STRATEGY_STATE_MIGRATING_DATA = "migrating data"

View File

@ -0,0 +1,63 @@
#
# Copyright (c) 2020 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import abc
import six
from oslo_log import log as logging
from dccommon.drivers.openstack.sdk_platform import OpenStackDriver
from dccommon.drivers.openstack.sysinv_v1 import SysinvClient
from dcmanager.common import consts
LOG = logging.getLogger(__name__)
@six.add_metaclass(abc.ABCMeta)
class BaseState(object):
def __init__(self):
super(BaseState, self).__init__()
def debug_log(self, strategy_step, details):
LOG.debug("Stage: %s, State: %s, Subcloud: %s, Details: %s"
% (strategy_step.stage,
strategy_step.state,
self.get_region_name(strategy_step),
details))
@staticmethod
def get_region_name(strategy_step):
"""Get the region name for a strategy step"""
if strategy_step.subcloud_id is None:
# This is the SystemController.
return consts.DEFAULT_REGION_NAME
return strategy_step.subcloud.name
@staticmethod
def get_keystone_client(region_name=consts.DEFAULT_REGION_NAME):
"""Construct a (cached) keystone client (and token)"""
try:
os_client = OpenStackDriver(region_name=region_name,
region_clients=None)
return os_client.keystone_client
except Exception:
LOG.warning('Failure initializing KeystoneClient for region: %s'
% region_name)
raise
@staticmethod
def get_sysinv_client(region_name, session):
"""construct a sysinv client
todo(abailey): determine if this client can be cached
"""
return SysinvClient(region_name, session)
@abc.abstractmethod
def perform_state_action(self, strategy_step):
"""Perform the action for this state on the strategy_step"""
pass

View File

@ -0,0 +1,77 @@
#
# Copyright (c) 2020 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import time
from oslo_log import log as logging
from dcmanager.common.consts import ADMIN_LOCKED
from dcmanager.manager.states.base import BaseState
LOG = logging.getLogger(__name__)
DEFAULT_MAX_QUERIES = 6
DEFAULT_SLEEP_DURATION = 10
class LockHostState(BaseState):
"""Orchestration state for locking a host"""
def __init__(self,
hostname='controller-0',
max_queries=DEFAULT_MAX_QUERIES,
sleep_duration=DEFAULT_SLEEP_DURATION):
super(LockHostState, self).__init__()
self.target_hostname = hostname
# max time to wait (in seconds) is: sleep_duration * max_queries
self.sleep_duration = sleep_duration
self.max_queries = max_queries
def perform_state_action(self, strategy_step):
"""Locks a host on the subcloud
Any exceptions raised by this method set the strategy to FAILED
Returning normally from this method set the strategy to the next step
"""
# Create a sysinv client on the subcloud
ks_client = self.get_keystone_client(strategy_step.subcloud.name)
sysinv_client = self.get_sysinv_client(strategy_step.subcloud.name,
ks_client.session)
host = sysinv_client.get_host(self.target_hostname)
# if the host is already in the desired state, no need for action
if host.administrative == ADMIN_LOCKED:
msg = "Host: %s already: %s." % (self.target_hostname,
host.administrative)
self.debug_log(strategy_step, msg)
return True
# Invoke the action
# ihost_action is 'lock' and task is set to 'Locking'
response = sysinv_client.lock_host(host.id)
if (response.ihost_action != 'lock' or response.task != 'Locking'):
raise Exception("Unable to lock host %s" % self.target_hostname)
# this action is asynchronous, query until it completes or times out
counter = 0
while True:
# query the administrative state to see if it is the new state.
host = sysinv_client.get_host(self.target_hostname)
if host.administrative == ADMIN_LOCKED:
msg = "Host: %s is now: %s" % (self.target_hostname,
host.administrative)
self.debug_log(strategy_step, msg)
break
counter += 1
if counter >= self.max_queries:
raise Exception("Timeout waiting for lock to complete")
time.sleep(self.sleep_duration)
# todo(abailey): add support for checking if the thread is stopped
# If we are here, the loop broke out cleanly and the action succeeded
# When we return from this method without throwing an exception, the
# state machine can proceed to the next state
return True

View File

@ -0,0 +1,80 @@
#
# Copyright (c) 2020 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import time
from oslo_log import log as logging
from dcmanager.common.consts import ADMIN_UNLOCKED
from dcmanager.manager.states.base import BaseState
LOG = logging.getLogger(__name__)
DEFAULT_MAX_QUERIES = 6
DEFAULT_SLEEP_DURATION = 10
class UnlockHostState(BaseState):
"""Orchestration state for unlocking a host"""
def __init__(self,
hostname='controller-0',
max_queries=DEFAULT_MAX_QUERIES,
sleep_duration=DEFAULT_SLEEP_DURATION):
super(UnlockHostState, self).__init__()
self.target_hostname = hostname
# max time to wait (in seconds) is: sleep_duration * max_queries
self.sleep_duration = sleep_duration
self.max_queries = max_queries
def check_async_counter(self, counter):
if counter >= self.max_queries:
raise Exception("Timeout waiting for unlock to complete")
time.sleep(self.sleep_duration)
def perform_state_action(self, strategy_step):
"""Unlocks a host on the subcloud
Any exceptions raised by this method set the strategy to FAILED
Returning normally from this method set the strategy to the next step
"""
# Create a sysinv client on the subcloud
ks_client = self.get_keystone_client(strategy_step.subcloud.name)
sysinv_client = self.get_sysinv_client(strategy_step.subcloud.name,
ks_client.session)
host = sysinv_client.get_host(self.target_hostname)
# if the host is already in the desired state, no need for action
if host.administrative == ADMIN_UNLOCKED:
msg = "Host: %s already: %s." % (self.target_hostname,
host.administrative)
self.debug_log(strategy_step, msg)
return True
# Invoke the action
# ihost_action is 'unlock' and task is set to 'Unlocking'
response = sysinv_client.unlock_host(host.id)
if (response.ihost_action != 'unlock' or response.task != 'Unlocking'):
raise Exception("Unable to unlock host %s" % self.target_hostname)
# this action is asynchronous, query until it completes or times out
async_counter = 0
while True:
# query the administrative state to see if it is the new state.
host = sysinv_client.get_host(self.target_hostname)
if host.administrative == ADMIN_UNLOCKED:
msg = "Host: %s is now: %s" % (self.target_hostname,
host.administrative)
self.debug_log(strategy_step, msg)
break
async_counter += 1
# check_async_counter throws exception if loops exceeded or aborted
self.check_async_counter(async_counter)
# If we are here, the loop broke out cleanly and the action succeeded
# When we return from this method without throwing an exception, the
# state machine can proceed to the next state
return True

View File

@ -0,0 +1,30 @@
#
# Copyright (c) 2020 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
from oslo_log import log as logging
from dcmanager.manager.states.base import BaseState
LOG = logging.getLogger(__name__)
class ActivatingState(BaseState):
"""Upgrade state actions for activating an upgrade"""
def __init__(self):
super(ActivatingState, self).__init__()
def perform_state_action(self, strategy_step):
"""Activate an upgrade on a subcloud
Any exceptions raised by this method set the strategy to FAILED
Returning normally from this method set the strategy to the next step
"""
LOG.warning("ActivatingState has not been implemented yet.")
# When we return from this method without throwing an exception, the
# state machine can proceed to the next state
LOG.warning("Faking transition to next state")
return True

View File

@ -0,0 +1,30 @@
#
# Copyright (c) 2020 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
from oslo_log import log as logging
from dcmanager.manager.states.base import BaseState
LOG = logging.getLogger(__name__)
class CompletingState(BaseState):
"""Upgrade state actions for completing an upgrade"""
def __init__(self):
super(CompletingState, self).__init__()
def perform_state_action(self, strategy_step):
"""Complete an upgrade on a subcloud
Any exceptions raised by this method set the strategy to FAILED
Returning normally from this method set the strategy to the next step
"""
LOG.warning("CompletingState has not been implemented yet.")
# When we return from this method without throwing an exception, the
# state machine can proceed to the next state
LOG.warning("Faking transition to next state")
return True

View File

@ -0,0 +1,30 @@
#
# Copyright (c) 2020 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
from oslo_log import log as logging
from dcmanager.manager.states.base import BaseState
LOG = logging.getLogger(__name__)
class ImportLoadState(BaseState):
"""Upgrade state for importing a load"""
def __init__(self):
super(ImportLoadState, self).__init__()
def perform_state_action(self, strategy_step):
"""Import a load on a subcloud
Any exceptions raised by this method set the strategy to FAILED
Returning normally from this method set the strategy to the next step
"""
LOG.warning("ImportLoadState has not been implemented yet.")
# When we return from this method without throwing an exception, the
# state machine can proceed to the next state
LOG.warning("Faking transition to next state")
return True

View File

@ -0,0 +1,30 @@
#
# Copyright (c) 2020 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
from oslo_log import log as logging
from dcmanager.manager.states.base import BaseState
LOG = logging.getLogger(__name__)
class MigratingDataState(BaseState):
"""Upgrade step for migrating data"""
def __init__(self):
super(MigratingDataState, self).__init__()
def perform_state_action(self, strategy_step):
"""Migrate data for an upgrade on a subcloud
Any exceptions raised by this method set the strategy to FAILED
Returning normally from this method set the strategy to the next step
"""
LOG.warning("MigratingDataState has not been implemented yet.")
# When we return from this method without throwing an exception, the
# state machine can proceed to the next state
LOG.warning("Faking transition to next state")
return True

View File

@ -0,0 +1,30 @@
#
# Copyright (c) 2020 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
from oslo_log import log as logging
from dcmanager.manager.states.base import BaseState
LOG = logging.getLogger(__name__)
class StartingUpgradeState(BaseState):
"""Upgrade state for starting an upgrade on a subcloud"""
def __init__(self):
super(StartingUpgradeState, self).__init__()
def perform_state_action(self, strategy_step):
"""Start an upgrade on a subcloud
Any exceptions raised by this method set the strategy to FAILED
Returning normally from this method set the strategy to the next step
"""
LOG.warning("StartingUpgradeState has not been implemented yet.")
# When we return from this method without throwing an exception, the
# state machine can proceed to the next state
LOG.warning("Faking transition to next state")
return True

View File

@ -0,0 +1,30 @@
#
# Copyright (c) 2020 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
from oslo_log import log as logging
from dcmanager.manager.states.base import BaseState
LOG = logging.getLogger(__name__)
class UpgradingSimplexState(BaseState):
"""Upgrade state for upgrading a simplex subcloud host"""
def __init__(self):
super(UpgradingSimplexState, self).__init__()
def perform_state_action(self, strategy_step):
"""Upgrade a simplex host on a subcloud
Any exceptions raised by this method set the strategy to FAILED
Returning normally from this method set the strategy to the next step
"""
LOG.warning("UpgradingSimplexState has not been implemented yet.")
# When we return from this method without throwing an exception, the
# state machine can proceed to the next state
LOG.warning("Faking transition to next state")
return True

View File

@ -196,7 +196,7 @@ class SwUpdateManager(manager.Manager):
if strategy_type == consts.SW_UPDATE_TYPE_PATCH:
db_api.strategy_step_create(
context,
None,
None, # None means not a subcloud. ie: SystemController
stage=1,
state=consts.STRATEGY_STATE_INITIAL,
details='')

View File

@ -111,6 +111,15 @@ class SwUpgradeOrchThread(threading.Thread):
return consts.DEFAULT_REGION_NAME
return strategy_step.subcloud.name
@staticmethod
def format_update_details(last_state, info):
# include the last state, since the current state is likely 'failed'
details = "%s: %s" % (last_state, info)
# details cannot exceed 255 chars. truncate and add '..'
if len(details) > 255:
details = details[:253] + '..'
return details
@staticmethod
def license_up_to_date(target_license, existing_license):
return target_license == existing_license
@ -289,6 +298,8 @@ class SwUpgradeOrchThread(threading.Thread):
strategy_step,
self.install_subcloud_license,
log_error=True)
# todo(abailey): state and their method invoked can be managed
# using a dictionary to make this more maintainable.
elif strategy_step.state == \
consts.STRATEGY_STATE_INSTALLING_LICENSE:
self.process_upgrade_step(region,
@ -299,7 +310,43 @@ class SwUpgradeOrchThread(threading.Thread):
consts.STRATEGY_STATE_IMPORTING_LOAD:
self.process_upgrade_step(region,
strategy_step,
self.install_subcloud_load,
self.import_subcloud_load,
log_error=False)
elif strategy_step.state == \
consts.STRATEGY_STATE_STARTING_UPGRADE:
self.process_upgrade_step(region,
strategy_step,
self.start_subcloud_upgrade,
log_error=False)
elif strategy_step.state == \
consts.STRATEGY_STATE_LOCKING_CONTROLLER:
self.process_upgrade_step(region,
strategy_step,
self.lock_subcloud_controller,
log_error=False)
elif strategy_step.state == \
consts.STRATEGY_STATE_UPGRADING_SIMPLEX:
self.process_upgrade_step(region,
strategy_step,
self.upgrade_subcloud_simplex,
log_error=False)
elif strategy_step.state == \
consts.STRATEGY_STATE_MIGRATING_DATA:
self.process_upgrade_step(region,
strategy_step,
self.migrate_subcloud_data,
log_error=False)
elif strategy_step.state == \
consts.STRATEGY_STATE_UNLOCKING_CONTROLLER:
self.process_upgrade_step(region,
strategy_step,
self.unlock_subcloud_controller,
log_error=False)
elif strategy_step.state == \
consts.STRATEGY_STATE_ACTIVATING:
self.process_upgrade_step(region,
strategy_step,
self.activate_subcloud,
log_error=False)
# todo(abailey): Add calls to self.process_upgrade_step
# for each additional state, with the appropriate thread
@ -358,27 +405,6 @@ class SwUpgradeOrchThread(threading.Thread):
if region in self.subcloud_workers:
del self.subcloud_workers[region]
def install_subcloud_load(self, strategy_step):
"""Install the load on the subcloud
Removes the worker reference after the operation is complete.
"""
try:
# self.do_install_subcloud_load(strategy_step)
raise NotImplementedError
except Exception:
# Catch ALL exceptions and set the strategy to failed
LOG.exception("Install load failed")
self.strategy_step_update(strategy_step.subcloud_id,
state=consts.STRATEGY_STATE_FAILED,
details=("Install load failed"))
finally:
# The worker is done.
region = self.get_region_name(strategy_step)
if region in self.subcloud_workers:
del self.subcloud_workers[region]
def do_install_subcloud_license(self, strategy_step):
"""Install the License for a software upgrade in this subcloud"""
@ -402,7 +428,7 @@ class SwUpgradeOrchThread(threading.Thread):
target_error = system_controller_license.get('error')
# If the system controller does not have a license, do not attempt
# to install licenses on subcluds, and simply proceed to the next stage
# to install licenses on subclouds, and simply proceed to the next stage
if len(target_error) != 0:
if LICENSE_FILE_NOT_FOUND_SUBSTRING in target_error:
LOG.debug("Stage:<%s>, Subcloud:<%s>. "
@ -421,7 +447,7 @@ class SwUpgradeOrchThread(threading.Thread):
self.get_region_name(strategy_step),
target_error))
raise exceptions.LicenseMissingError(
subcloud_id="SystemController")
subcloud_id=consts.SYSTEM_CONTROLLER_NAME)
# retrieve the keystone session for the subcloud and query its license
subcloud_ks_client = self.get_ks_client(strategy_step.subcloud.name)
@ -509,3 +535,74 @@ class SwUpgradeOrchThread(threading.Thread):
except Exception as e:
LOG.exception(e)
raise e
def perform_state_action(self, strategy_step, state_operator, next_state):
"""Extensible state handler for processing and transitioning states """
try:
LOG.info("Stage: %s, State: %s, Subcloud: %s"
% (strategy_step.stage,
strategy_step.state,
self.get_region_name(strategy_step)))
state_operator.perform_state_action(strategy_step)
# If we get here without an exception raised, proceed to next state
self.strategy_step_update(strategy_step.subcloud_id,
state=next_state)
except Exception as e:
# Catch ALL exceptions and set the strategy to failed
LOG.exception("Failed! Stage: %s, State: %s, Subcloud: %s"
% (strategy_step.stage,
strategy_step.state,
self.get_region_name(strategy_step)))
details = self.format_update_details(strategy_step.state, str(e))
self.strategy_step_update(strategy_step.subcloud_id,
state=consts.STRATEGY_STATE_FAILED,
details=details)
finally:
# The worker is done.
region = self.get_region_name(strategy_step)
if region in self.subcloud_workers:
del self.subcloud_workers[region]
# todo(abailey): convert license install to the same pattern as the other states
def import_subcloud_load(self, strategy_step):
from dcmanager.manager.states.upgrade.import_load import ImportLoadState
self.perform_state_action(strategy_step,
ImportLoadState(),
consts.STRATEGY_STATE_STARTING_UPGRADE)
def start_subcloud_upgrade(self, strategy_step):
from dcmanager.manager.states.upgrade.starting_upgrade import StartingUpgradeState
self.perform_state_action(strategy_step,
StartingUpgradeState(),
consts.STRATEGY_STATE_LOCKING_CONTROLLER)
def lock_subcloud_controller(self, strategy_step):
from dcmanager.manager.states.lock_host import LockHostState
self.perform_state_action(strategy_step,
LockHostState(),
consts.STRATEGY_STATE_UPGRADING_SIMPLEX)
def upgrade_subcloud_simplex(self, strategy_step):
from dcmanager.manager.states.upgrade.upgrading_simplex import UpgradingSimplexState
self.perform_state_action(strategy_step,
UpgradingSimplexState(),
consts.STRATEGY_STATE_MIGRATING_DATA)
def migrate_subcloud_data(self, strategy_step):
from dcmanager.manager.states.upgrade.migrating_data import MigratingDataState
self.perform_state_action(strategy_step,
MigratingDataState(),
consts.STRATEGY_STATE_UNLOCKING_CONTROLLER)
def unlock_subcloud_controller(self, strategy_step):
from dcmanager.manager.states.unlock_host import UnlockHostState
self.perform_state_action(strategy_step,
UnlockHostState(),
consts.STRATEGY_STATE_ACTIVATING)
def activate_subcloud(self, strategy_step):
from dcmanager.manager.states.upgrade.activating import ActivatingState
self.perform_state_action(strategy_step,
ActivatingState(),
consts.STRATEGY_STATE_COMPLETE)

View File

@ -0,0 +1,67 @@
#
# Copyright (c) 2020 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import mock
from dcmanager.manager.states.base import BaseState
from sysinv.common import constants as sysinv_constants
from dcmanager.tests.unit.manager.test_sw_upgrade import TestSwUpgrade
CURRENT_LOAD = '20.01'
UPDATED_LOAD = '20.06'
class FakeKeystoneClient(object):
def __init__(self):
self.session = mock.MagicMock()
class FakeSysinvClient(object):
def __init__(self):
pass
class FakeController(object):
def __init__(self,
host_id=1,
hostname='controller-0',
administrative=sysinv_constants.ADMIN_UNLOCKED,
availability=sysinv_constants.AVAILABILITY_AVAILABLE,
ihost_action=None,
target_load=CURRENT_LOAD,
task=None):
self.id = host_id
self.hostname = hostname
self.administrative = administrative
self.availability = availability
self.ihost_action = ihost_action
self.target_load = target_load
self.task = task
class TestSwUpgradeState(TestSwUpgrade):
def setUp(self):
super(TestSwUpgradeState, self).setUp()
# Mock the host environment.
self.controller_0 = self.fake_controller('controller-0')
# Mock the keystone client defined in the base upgrade state class
self.keystone_client = FakeKeystoneClient()
p = mock.patch.object(BaseState, 'get_keystone_client')
self.mock_keystone_client = p.start()
self.mock_keystone_client.return_value = self.keystone_client
self.addCleanup(p.stop)
# Mock the sysinv client defined in the base upgrade state class
self.sysinv_client = FakeSysinvClient()
p = mock.patch.object(BaseState, 'get_sysinv_client')
self.mock_sysinv_client = p.start()
self.mock_sysinv_client.return_value = self.sysinv_client
self.addCleanup(p.stop)
def fake_controller(self, hostname):
return FakeController(hostname=hostname)

View File

@ -0,0 +1,146 @@
#
# Copyright (c) 2020 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import itertools
import mock
from dcmanager.common import consts
from dcmanager.manager.states.lock_host import DEFAULT_MAX_QUERIES
from dcmanager.tests.unit.manager.states.upgrade.test_base \
import FakeController
from dcmanager.tests.unit.manager.states.upgrade.test_base \
import TestSwUpgradeState
CONTROLLER_0_UNLOCKED = FakeController(administrative=consts.ADMIN_UNLOCKED)
CONTROLLER_0_LOCKED = FakeController(administrative=consts.ADMIN_LOCKED)
CONTROLLER_0_LOCKING = FakeController(administrative=consts.ADMIN_UNLOCKED,
ihost_action='lock',
task='Locking')
CONTROLLER_0_LOCKING_FAILED = \
FakeController(administrative=consts.ADMIN_UNLOCKED,
ihost_action='force-swact',
task='Swacting')
class TestSwUpgradeLockControllerStage(TestSwUpgradeState):
def setUp(self):
super(TestSwUpgradeLockControllerStage, self).setUp()
# next state after a successful lock is upgrading simplex
self.on_success_state = consts.STRATEGY_STATE_UPGRADING_SIMPLEX
# Add the strategy_step state being processed by this unit test
self.strategy_step = \
self.setup_strategy_step(consts.STRATEGY_STATE_LOCKING_CONTROLLER)
# Add mock API endpoints for sysinv client calls invcked by this state
self.sysinv_client.get_host = mock.MagicMock()
self.sysinv_client.lock_host = mock.MagicMock()
def test_lock_success(self):
"""Test the lock command returns a success"""
# mock the controller host queries
# first query is the starting state
# query 2,3 are are during the lock phase
# query 4 : the host is now locked
self.sysinv_client.get_host.side_effect = [CONTROLLER_0_UNLOCKED,
CONTROLLER_0_LOCKING,
CONTROLLER_0_LOCKING,
CONTROLLER_0_LOCKED]
# mock the API call as failed on the subcloud
self.sysinv_client.lock_host.return_value = CONTROLLER_0_LOCKING
# invoke the strategy state operation
self.worker.lock_subcloud_controller(self.strategy_step)
# verify the lock command was actually attempted
self.sysinv_client.lock_host.assert_called()
# verify that the API moved to the next state on success
self.assert_step_updated(self.strategy_step.subcloud_id,
self.on_success_state)
def test_lock_skipped_when_already_locked(self):
"""Test the lock command skips if host is already locked"""
# mock the controller host query as being already locked
self.sysinv_client.get_host.return_value = CONTROLLER_0_LOCKED
# invoke the strategy state operation
self.worker.lock_subcloud_controller(self.strategy_step)
# verify the lock command was never attempted
self.sysinv_client.lock_host.assert_not_called()
# verify that the state moves to the next state
self.assert_step_updated(self.strategy_step.subcloud_id,
self.on_success_state)
def test_lock_attempt_timeout(self):
"""Test lock invoked and fails if timeout before host becomes locked"""
# mock the get_host queries
# first query is the starting state
# all remaining queries, the host returns 'locking'
self.sysinv_client.get_host.side_effect = itertools.chain(
[CONTROLLER_0_UNLOCKED, ],
itertools.repeat(CONTROLLER_0_LOCKING))
# mock the API call as successful on the subcloud
self.sysinv_client.lock_host.return_value = CONTROLLER_0_LOCKING
# invoke the strategy state operation
self.worker.lock_subcloud_controller(self.strategy_step)
# verify the lock command was actually attempted
self.sysinv_client.lock_host.assert_called()
# verify the query was invoked: 1 + max_attempts times
self.assertEqual(DEFAULT_MAX_QUERIES + 1,
self.sysinv_client.get_host.call_count)
# verify that state failed due to subcloud never finishing the lock
self.assert_step_updated(self.strategy_step.subcloud_id,
consts.STRATEGY_STATE_FAILED)
def test_lock_failure(self):
"""Test the lock command returns a failure"""
# mock the controller get_host query
self.sysinv_client.get_host.return_value = CONTROLLER_0_UNLOCKED
# mock the API call as failed on the subcloud
self.sysinv_client.lock_host.return_value = CONTROLLER_0_LOCKING_FAILED
# invoke the strategy state operation
self.worker.lock_subcloud_controller(self.strategy_step)
# verify the lock command was actually attempted
self.sysinv_client.lock_host.assert_called()
# verify that the API error for the lock leads to a failed state
self.assert_step_updated(self.strategy_step.subcloud_id,
consts.STRATEGY_STATE_FAILED)
def test_lock_fails_when_host_query_fails(self):
"""Test the lock command fails when it cannot get the controllers"""
# mock the get_host query is empty and raises an exception
self.sysinv_client.get_host.side_effect = \
Exception("Unable to find host controller-0")
# invoke the strategy state operation
self.worker.lock_subcloud_controller(self.strategy_step)
# verify the lock command was never attempted
self.sysinv_client.lock_host.assert_not_called()
# verify that the state moves to the next state
self.assert_step_updated(self.strategy_step.subcloud_id,
consts.STRATEGY_STATE_FAILED)

View File

@ -0,0 +1,147 @@
#
# Copyright (c) 2020 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import itertools
import mock
from dcmanager.common import consts
from dcmanager.manager.states.unlock_host import DEFAULT_MAX_QUERIES
from dcmanager.tests.unit.manager.states.upgrade.test_base \
import FakeController
from dcmanager.tests.unit.manager.states.upgrade.test_base \
import TestSwUpgradeState
CONTROLLER_0_UNLOCKED = FakeController(administrative=consts.ADMIN_UNLOCKED)
CONTROLLER_0_LOCKED = FakeController(administrative=consts.ADMIN_LOCKED)
CONTROLLER_0_UNLOCKING = FakeController(administrative=consts.ADMIN_LOCKED,
ihost_action='unlock',
task='Unlocking')
CONTROLLER_0_UNLOCKING_FAILED = \
FakeController(administrative=consts.ADMIN_LOCKED,
ihost_action='force-swact',
task='Swacting')
class TestSwUpgradeUnlockControllerStage(TestSwUpgradeState):
def setUp(self):
super(TestSwUpgradeUnlockControllerStage, self).setUp()
# next state after a successful unlock is 'activating'
self.on_success_state = consts.STRATEGY_STATE_ACTIVATING
# Add the strategy_step state being processed by this unit test
self.strategy_step = self.setup_strategy_step(
consts.STRATEGY_STATE_UNLOCKING_CONTROLLER)
# Add mock API endpoints for sysinv client calls invcked by this state
self.sysinv_client.get_host = mock.MagicMock()
self.sysinv_client.unlock_host = mock.MagicMock()
def test_unlock_success(self):
"""Test the unlock command returns a success"""
# mock the get_host queries
# first query is the starting state
# query 2,3 are are during the unlock phase
# query 4 : the host is now unlocked
self.sysinv_client.get_host.side_effect = [CONTROLLER_0_LOCKED,
CONTROLLER_0_UNLOCKING,
CONTROLLER_0_UNLOCKING,
CONTROLLER_0_UNLOCKED, ]
# mock the API call as failed on the subcloud
self.sysinv_client.unlock_host.return_value = CONTROLLER_0_UNLOCKING
# invoke the strategy state operation
self.worker.unlock_subcloud_controller(self.strategy_step)
# verify the unlock command was actually attempted
self.sysinv_client.unlock_host.assert_called()
# verify that the API moved to the next state on success
self.assert_step_updated(self.strategy_step.subcloud_id,
self.on_success_state)
def test_unlock_skipped_when_already_unlocked(self):
"""Test the unlock command skips if host is already unlocked"""
# mock the controller host query as being already unlocked
self.sysinv_client.get_host.return_value = CONTROLLER_0_UNLOCKED
# invoke the strategy state operation
self.worker.unlock_subcloud_controller(self.strategy_step)
# verify the unlock command was never attempted
self.sysinv_client.unlock_host.assert_not_called()
# verify that the state moves to the next state
self.assert_step_updated(self.strategy_step.subcloud_id,
self.on_success_state)
def test_unlock_attempt_timeout(self):
"""Test unlock invoked handles timeout if unlocking takes too long"""
# mock the get_host queries
# first query is the starting state
# all remaining queries, the host returns 'unlocking'
self.sysinv_client.get_host.side_effect = itertools.chain(
[CONTROLLER_0_LOCKED, ],
itertools.repeat(CONTROLLER_0_UNLOCKING))
# mock the API call as successful on the subcloud
self.sysinv_client.unlock_host.return_value = CONTROLLER_0_UNLOCKING
# invoke the strategy state operation
self.worker.unlock_subcloud_controller(self.strategy_step)
# verify the lock command was actually attempted
self.sysinv_client.unlock_host.assert_called()
# verify the query was invoked: 1 + max_attempts times
self.assertEqual(DEFAULT_MAX_QUERIES + 1,
self.sysinv_client.get_host.call_count)
# verify that state failed due to subcloud never finishing the unlock
self.assert_step_updated(self.strategy_step.subcloud_id,
consts.STRATEGY_STATE_FAILED)
def test_unlock_failure(self):
"""Test the unlock command returns a failure"""
# mock the get_host query
self.sysinv_client.get_host.return_value = CONTROLLER_0_LOCKED
# mock the API call as failed on the subcloud
self.sysinv_client.unlock_host.return_value = \
CONTROLLER_0_UNLOCKING_FAILED
# invoke the strategy state operation
self.worker.unlock_subcloud_controller(self.strategy_step)
# verify the unlock command was actually attempted
self.sysinv_client.unlock_host.assert_called()
# verify that the API error for the unlock leads to a failed state
self.assert_step_updated(self.strategy_step.subcloud_id,
consts.STRATEGY_STATE_FAILED)
def test_unlock_fails_when_host_query_fails(self):
"""Test the unlock command fails when it cannot get the controllers"""
# mock the get_host query fails and raises an exception
self.sysinv_client.get_host.side_effect = \
Exception("Unable to find host controller-0")
# invoke the strategy state operation
self.worker.unlock_subcloud_controller(self.strategy_step)
# verify the unlock command was never attempted
self.sysinv_client.unlock_host.assert_not_called()
# verify that the state moves to the next state
self.assert_step_updated(self.strategy_step.subcloud_id,
consts.STRATEGY_STATE_FAILED)

View File

@ -10,7 +10,7 @@
# License for the specific language governing permissions and limitations
# under the License.
#
# Copyright (c) 2017 Wind River Systems, Inc.
# Copyright (c) 2017-2020 Wind River Systems, Inc.
#
# The right to copy, distribute, modify, or otherwise make use
# of this software may be licensed only pursuant to the terms
@ -112,6 +112,16 @@ class TestSwUpgrade(base.DCManagerTestCase):
self.mock_db_api = p.start()
self.addCleanup(p.stop)
def setup_strategy_step(self, strategy_state):
data = copy.copy(FAKE_STRATEGY_STEP_DATA)
data['state'] = strategy_state
data['subcloud'] = Subcloud(1,
'subcloud1',
is_managed=True,
is_online=True)
fake_strategy_step = StrategyStep(**data)
return fake_strategy_step
def setup_upgrade_worker(self):
sw_update_manager.SwUpgradeOrchThread.stopped = lambda x: False
mock_strategy_lock = mock.Mock()
@ -136,16 +146,8 @@ class TestSwUpgradeLicenseStage(TestSwUpgrade):
def setUp(self):
super(TestSwUpgradeLicenseStage, self).setUp()
def setup_license_install_step(self):
data = copy.copy(FAKE_STRATEGY_STEP_DATA)
data['state'] = consts.STRATEGY_STATE_INSTALLING_LICENSE
data['subcloud'] = Subcloud(1,
'subcloud1',
is_managed=True,
is_online=True)
fake_strategy_step = StrategyStep(**data)
return fake_strategy_step
self.strategy_step = \
self.setup_strategy_step(consts.STRATEGY_STATE_INSTALLING_LICENSE)
def test_upgrade_subcloud_license_install_failure(self):
# Test the install subcloud license step where the system controller
@ -162,14 +164,13 @@ class TestSwUpgradeLicenseStage(TestSwUpgrade):
self.fake_sysinv_client.install_license.return_value = \
MISSING_LICENSE_RESPONSE
fake_strategy_step = self.setup_license_install_step()
self.worker.install_subcloud_license(fake_strategy_step)
self.worker.install_subcloud_license(self.strategy_step)
# verify the license install was invoked
self.fake_sysinv_client.install_license.assert_called()
# Verify a install_license failure leads to a state failure
self.assert_step_updated(fake_strategy_step.subcloud_id,
self.assert_step_updated(self.strategy_step.subcloud_id,
consts.STRATEGY_STATE_FAILED)
def test_upgrade_subcloud_license_install_success(self):
@ -187,14 +188,13 @@ class TestSwUpgradeLicenseStage(TestSwUpgrade):
self.fake_sysinv_client.install_license.return_value = \
LICENSE_VALID_RESPONSE
fake_strategy_step = self.setup_license_install_step()
self.worker.install_subcloud_license(fake_strategy_step)
self.worker.install_subcloud_license(self.strategy_step)
# verify the license install was invoked
self.fake_sysinv_client.install_license.assert_called()
# On success, the next state after installing license is importing load
self.assert_step_updated(fake_strategy_step.subcloud_id,
self.assert_step_updated(self.strategy_step.subcloud_id,
consts.STRATEGY_STATE_IMPORTING_LOAD)
def test_upgrade_subcloud_license_skip_existing(self):
@ -207,14 +207,13 @@ class TestSwUpgradeLicenseStage(TestSwUpgrade):
self.fake_sysinv_client.get_license.side_effect = \
[LICENSE_VALID_RESPONSE,
LICENSE_VALID_RESPONSE]
fake_strategy_step = self.setup_license_install_step()
self.worker.install_subcloud_license(fake_strategy_step)
self.worker.install_subcloud_license(self.strategy_step)
# A license install should not have been attempted due to the license
# already being up to date
self.fake_sysinv_client.install_license.assert_not_called()
# On success, the next state after installing license is importing load
self.assert_step_updated(fake_strategy_step.subcloud_id,
self.assert_step_updated(self.strategy_step.subcloud_id,
consts.STRATEGY_STATE_IMPORTING_LOAD)
def test_upgrade_subcloud_license_overrides_mismatched_license(self):
@ -233,14 +232,13 @@ class TestSwUpgradeLicenseStage(TestSwUpgrade):
self.fake_sysinv_client.install_license.return_value = \
LICENSE_VALID_RESPONSE
fake_strategy_step = self.setup_license_install_step()
self.worker.install_subcloud_license(fake_strategy_step)
self.worker.install_subcloud_license(self.strategy_step)
# verify the license install was invoked
self.fake_sysinv_client.install_license.assert_called()
# On success, the next state after installing license is importing load
self.assert_step_updated(fake_strategy_step.subcloud_id,
self.assert_step_updated(self.strategy_step.subcloud_id,
consts.STRATEGY_STATE_IMPORTING_LOAD)
def test_upgrade_subcloud_license_skip_when_no_sys_controller_lic(self):
@ -250,15 +248,14 @@ class TestSwUpgradeLicenseStage(TestSwUpgrade):
# Only makes one query to system controller
self.fake_sysinv_client.get_license.side_effect = \
[MISSING_LICENSE_RESPONSE, ]
fake_strategy_step = self.setup_license_install_step()
# Test the install subcloud license stage
self.worker.install_subcloud_license(fake_strategy_step)
self.worker.install_subcloud_license(self.strategy_step)
# A license install should proceed to the next state without
# calling a license install
self.fake_sysinv_client.install_license.assert_not_called()
# Skip license install and move to next state
self.assert_step_updated(fake_strategy_step.subcloud_id,
self.assert_step_updated(self.strategy_step.subcloud_id,
consts.STRATEGY_STATE_IMPORTING_LOAD)
def test_upgrade_subcloud_license_handle_failure(self):
@ -276,14 +273,13 @@ class TestSwUpgradeLicenseStage(TestSwUpgrade):
self.fake_sysinv_client.install_license.return_value = \
MISSING_LICENSE_RESPONSE
fake_strategy_step = self.setup_license_install_step()
self.worker.install_subcloud_license(fake_strategy_step)
self.worker.install_subcloud_license(self.strategy_step)
# verify the license install was invoked
self.fake_sysinv_client.install_license.assert_called()
# Verify a install_license failure leads to a state failure
self.assert_step_updated(fake_strategy_step.subcloud_id,
self.assert_step_updated(self.strategy_step.subcloud_id,
consts.STRATEGY_STATE_FAILED)
def test_upgrade_subcloud_license_installs(self):
@ -301,12 +297,11 @@ class TestSwUpgradeLicenseStage(TestSwUpgrade):
self.fake_sysinv_client.install_license.return_value = \
LICENSE_VALID_RESPONSE
fake_strategy_step = self.setup_license_install_step()
self.worker.install_subcloud_license(fake_strategy_step)
self.worker.install_subcloud_license(self.strategy_step)
# verify the license install was invoked
self.fake_sysinv_client.install_license.assert_called()
# On success, the next state after installing license is importing load
self.assert_step_updated(fake_strategy_step.subcloud_id,
self.assert_step_updated(self.strategy_step.subcloud_id,
consts.STRATEGY_STATE_IMPORTING_LOAD)