Cache agent clean steps on node
In order to make getting clean steps a synchronous call, instead of one that blocks on communication with the agent, the agent clean steps are cached in the node's driver_internal_info. Any time cleaning is started, the steps will be fetched from the agent and cached. This is needed for the 'GET /nodes/<node_ident>/cleaning/steps' API, which is not yet implemented but see the spec: (http://specs.openstack.org/openstack/ironic-specs/specs/approved/manual-cleaning.html#get-nodes-node-ident-cleaning-steps) Change-Id: I26288802d06683fd99138bfea488233c88260a7f Partial-Bug: #1526290 Co-Authored-By: Josh Gachnang <josh@pcsforeducation.com>
This commit is contained in:
parent
edc37cbe1d
commit
74f6661404
@ -182,6 +182,10 @@ class BaseInterface(object):
|
|||||||
|
|
||||||
:param task: A TaskManager object, useful for interfaces overriding
|
:param task: A TaskManager object, useful for interfaces overriding
|
||||||
this function
|
this function
|
||||||
|
:raises NodeCleaningFailure: if there is a problem getting the steps
|
||||||
|
from the driver. For example, when a node (using an agent driver)
|
||||||
|
has just been enrolled and the agent isn't alive yet to be queried
|
||||||
|
for the available clean steps.
|
||||||
:returns: A list of clean step dictionaries
|
:returns: A list of clean step dictionaries
|
||||||
"""
|
"""
|
||||||
return self.clean_steps
|
return self.clean_steps
|
||||||
|
@ -359,7 +359,9 @@ class AgentDeploy(base.DeployInterface):
|
|||||||
"""Get the list of clean steps from the agent.
|
"""Get the list of clean steps from the agent.
|
||||||
|
|
||||||
:param task: a TaskManager object containing the node
|
:param task: a TaskManager object containing the node
|
||||||
|
:raises NodeCleaningFailure: if the clean steps are not yet
|
||||||
|
available (cached), for example, when a node has just been
|
||||||
|
enrolled and has not been cleaned yet.
|
||||||
:returns: A list of clean step dictionaries
|
:returns: A list of clean step dictionaries
|
||||||
"""
|
"""
|
||||||
new_priorities = {
|
new_priorities = {
|
||||||
|
@ -16,12 +16,13 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import collections
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
from oslo_utils import excutils
|
from oslo_utils import excutils
|
||||||
|
from oslo_utils import timeutils
|
||||||
import retrying
|
import retrying
|
||||||
|
|
||||||
from ironic.common import boot_devices
|
from ironic.common import boot_devices
|
||||||
@ -215,6 +216,62 @@ class BaseAgentVendor(base.VendorInterface):
|
|||||||
task.release_resources()
|
task.release_resources()
|
||||||
rpc.continue_node_clean(task.context, uuid, topic=topic)
|
rpc.continue_node_clean(task.context, uuid, topic=topic)
|
||||||
|
|
||||||
|
def _refresh_clean_steps(self, task):
|
||||||
|
"""Refresh the node's cached clean steps from the booted agent.
|
||||||
|
|
||||||
|
Gets the node's clean steps from the booted agent and caches them.
|
||||||
|
The steps are cached to make get_clean_steps() calls synchronous, and
|
||||||
|
should be refreshed as soon as the agent boots to start cleaning or
|
||||||
|
if cleaning is restarted because of a cleaning version mismatch.
|
||||||
|
|
||||||
|
:param task: a TaskManager instance
|
||||||
|
:raises: NodeCleaningFailure if the agent returns invalid results
|
||||||
|
"""
|
||||||
|
node = task.node
|
||||||
|
previous_steps = node.driver_internal_info.get(
|
||||||
|
'agent_cached_clean_steps')
|
||||||
|
LOG.debug('Refreshing agent clean step cache for node %(node)s. '
|
||||||
|
'Previously cached steps: %(steps)s',
|
||||||
|
{'node': node.uuid, 'steps': previous_steps})
|
||||||
|
|
||||||
|
agent_result = self._client.get_clean_steps(node, task.ports).get(
|
||||||
|
'command_result', {})
|
||||||
|
missing = set(['clean_steps', 'hardware_manager_version']).difference(
|
||||||
|
agent_result)
|
||||||
|
if missing:
|
||||||
|
raise exception.NodeCleaningFailure(_(
|
||||||
|
'agent get_clean_steps for node %(node)s returned an invalid '
|
||||||
|
'result. Keys: %(keys)s are missing from result: %(result)s.')
|
||||||
|
% ({'node': node.uuid, 'keys': missing,
|
||||||
|
'result': agent_result}))
|
||||||
|
|
||||||
|
# agent_result['clean_steps'] looks like
|
||||||
|
# {'HardwareManager': [{step1},{steps2}...], ...}
|
||||||
|
steps = collections.defaultdict(list)
|
||||||
|
for step_list in agent_result['clean_steps'].values():
|
||||||
|
for step in step_list:
|
||||||
|
missing = set(['interface', 'step', 'priority']).difference(
|
||||||
|
step)
|
||||||
|
if missing:
|
||||||
|
raise exception.NodeCleaningFailure(_(
|
||||||
|
'agent get_clean_steps for node %(node)s returned an '
|
||||||
|
'invalid clean step. Keys: %(keys)s are missing from '
|
||||||
|
'step: %(step)s.') % ({'node': node.uuid,
|
||||||
|
'keys': missing, 'step': step}))
|
||||||
|
|
||||||
|
steps[step['interface']].append(step)
|
||||||
|
|
||||||
|
# Save hardware manager version, steps, and date
|
||||||
|
info = node.driver_internal_info
|
||||||
|
info['hardware_manager_version'] = agent_result[
|
||||||
|
'hardware_manager_version']
|
||||||
|
info['agent_cached_clean_steps'] = dict(steps)
|
||||||
|
info['agent_cached_clean_steps_refreshed'] = str(timeutils.utcnow())
|
||||||
|
node.driver_internal_info = info
|
||||||
|
node.save()
|
||||||
|
LOG.debug('Refreshed agent clean step cache for node %(node)s: '
|
||||||
|
'%(steps)s', {'node': node.uuid, 'steps': steps})
|
||||||
|
|
||||||
def continue_cleaning(self, task, **kwargs):
|
def continue_cleaning(self, task, **kwargs):
|
||||||
"""Start the next cleaning step if the previous one is complete.
|
"""Start the next cleaning step if the previous one is complete.
|
||||||
|
|
||||||
@ -249,6 +306,16 @@ class BaseAgentVendor(base.VendorInterface):
|
|||||||
LOG.error(msg)
|
LOG.error(msg)
|
||||||
return manager_utils.cleaning_error_handler(task, msg)
|
return manager_utils.cleaning_error_handler(task, msg)
|
||||||
elif command.get('command_status') == 'CLEAN_VERSION_MISMATCH':
|
elif command.get('command_status') == 'CLEAN_VERSION_MISMATCH':
|
||||||
|
# Cache the new clean steps (and 'hardware_manager_version')
|
||||||
|
try:
|
||||||
|
self._refresh_clean_steps(task)
|
||||||
|
except exception.NodeCleaningFailure as e:
|
||||||
|
msg = (_('Could not continue cleaning on node '
|
||||||
|
'%(node)s: %(err)s.') %
|
||||||
|
{'node': node.uuid, 'err': e})
|
||||||
|
LOG.exception(msg)
|
||||||
|
return manager_utils.cleaning_error_handler(task, msg)
|
||||||
|
|
||||||
if manual_clean:
|
if manual_clean:
|
||||||
# Don't restart manual cleaning if agent reboots to a new
|
# Don't restart manual cleaning if agent reboots to a new
|
||||||
# version. Both are operator actions, unlike automated
|
# version. Both are operator actions, unlike automated
|
||||||
@ -259,27 +326,7 @@ class BaseAgentVendor(base.VendorInterface):
|
|||||||
'continuing from current step %(step)s.'),
|
'continuing from current step %(step)s.'),
|
||||||
{'node': node.uuid, 'step': node.clean_step})
|
{'node': node.uuid, 'step': node.clean_step})
|
||||||
|
|
||||||
result = self._client.get_clean_steps(
|
|
||||||
task.node, task.ports).get('command_result')
|
|
||||||
|
|
||||||
required_keys = ('clean_steps', 'hardware_manager_version')
|
|
||||||
missing_keys = [key for key in required_keys
|
|
||||||
if key not in result]
|
|
||||||
if missing_keys:
|
|
||||||
msg = (_('Could not continue manual cleaning from step '
|
|
||||||
'%(step)s on node %(node)s. get_clean_steps '
|
|
||||||
'returned invalid result. The keys %(keys)s are '
|
|
||||||
'missing from result %(result)s.') %
|
|
||||||
{'node': node.uuid,
|
|
||||||
'step': node.clean_step,
|
|
||||||
'keys': missing_keys,
|
|
||||||
'result': result})
|
|
||||||
LOG.error(msg)
|
|
||||||
return manager_utils.cleaning_error_handler(task, msg)
|
|
||||||
|
|
||||||
driver_internal_info = node.driver_internal_info
|
driver_internal_info = node.driver_internal_info
|
||||||
driver_internal_info['hardware_manager_version'] = result[
|
|
||||||
'hardware_manager_version']
|
|
||||||
driver_internal_info['skip_current_clean_step'] = False
|
driver_internal_info['skip_current_clean_step'] = False
|
||||||
node.driver_internal_info = driver_internal_info
|
node.driver_internal_info = driver_internal_info
|
||||||
node.save()
|
node.save()
|
||||||
@ -397,6 +444,9 @@ class BaseAgentVendor(base.VendorInterface):
|
|||||||
node.uuid)
|
node.uuid)
|
||||||
msg = _('Node failed to start the first cleaning '
|
msg = _('Node failed to start the first cleaning '
|
||||||
'step.')
|
'step.')
|
||||||
|
# First, cache the clean steps
|
||||||
|
self._refresh_clean_steps(task)
|
||||||
|
# Then set/verify node clean steps and start cleaning
|
||||||
manager_utils.set_node_cleaning_steps(task)
|
manager_utils.set_node_cleaning_steps(task)
|
||||||
self.notify_conductor_resume_clean(task)
|
self.notify_conductor_resume_clean(task)
|
||||||
else:
|
else:
|
||||||
|
@ -550,10 +550,12 @@ def parse_instance_info_capabilities(node):
|
|||||||
|
|
||||||
|
|
||||||
def agent_get_clean_steps(task, interface=None, override_priorities=None):
|
def agent_get_clean_steps(task, interface=None, override_priorities=None):
|
||||||
"""Get the list of clean steps from the agent.
|
"""Get the list of cached clean steps from the agent.
|
||||||
|
|
||||||
#TODO(JoshNang) move to BootInterface
|
#TODO(JoshNang) move to BootInterface
|
||||||
|
|
||||||
|
The clean steps cache is updated at the beginning of cleaning.
|
||||||
|
|
||||||
:param task: a TaskManager object containing the node
|
:param task: a TaskManager object containing the node
|
||||||
:param interface: The interface for which clean steps
|
:param interface: The interface for which clean steps
|
||||||
are to be returned. If this is not provided, it returns the
|
are to be returned. If this is not provided, it returns the
|
||||||
@ -561,42 +563,34 @@ def agent_get_clean_steps(task, interface=None, override_priorities=None):
|
|||||||
:param override_priorities: a dictionary with keys being step names and
|
:param override_priorities: a dictionary with keys being step names and
|
||||||
values being new priorities for them. If a step isn't in this
|
values being new priorities for them. If a step isn't in this
|
||||||
dictionary, the step's original priority is used.
|
dictionary, the step's original priority is used.
|
||||||
:raises: NodeCleaningFailure if the agent returns invalid results
|
:raises NodeCleaningFailure: if the clean steps are not yet cached,
|
||||||
|
for example, when a node has just been enrolled and has not been
|
||||||
|
cleaned yet.
|
||||||
:returns: A list of clean step dictionaries
|
:returns: A list of clean step dictionaries
|
||||||
"""
|
"""
|
||||||
override_priorities = override_priorities or {}
|
node = task.node
|
||||||
client = agent_client.AgentClient()
|
try:
|
||||||
ports = objects.Port.list_by_node_id(
|
all_steps = node.driver_internal_info['agent_cached_clean_steps']
|
||||||
task.context, task.node.id)
|
except KeyError:
|
||||||
result = client.get_clean_steps(task.node, ports).get('command_result')
|
raise exception.NodeCleaningFailure(_('Cleaning steps are not yet '
|
||||||
|
'available for node %(node)s')
|
||||||
|
% {'node': node.uuid})
|
||||||
|
|
||||||
if ('clean_steps' not in result or
|
if interface:
|
||||||
'hardware_manager_version' not in result):
|
steps = [step.copy() for step in all_steps.get(interface, [])]
|
||||||
raise exception.NodeCleaningFailure(_(
|
else:
|
||||||
'get_clean_steps for node %(node)s returned invalid result:'
|
steps = [step.copy() for step_list in all_steps.values()
|
||||||
' %(result)s') % ({'node': task.node.uuid, 'result': result}))
|
|
||||||
|
|
||||||
driver_internal_info = task.node.driver_internal_info
|
|
||||||
driver_internal_info['hardware_manager_version'] = result[
|
|
||||||
'hardware_manager_version']
|
|
||||||
task.node.driver_internal_info = driver_internal_info
|
|
||||||
task.node.save()
|
|
||||||
|
|
||||||
# Clean steps looks like {'HardwareManager': [{step1},{steps2}..]..}
|
|
||||||
# Flatten clean steps into one list
|
|
||||||
steps_list = [step for step_list in
|
|
||||||
result['clean_steps'].values()
|
|
||||||
for step in step_list]
|
for step in step_list]
|
||||||
result = []
|
|
||||||
for step in steps_list:
|
if not steps or not override_priorities:
|
||||||
if interface and step.get('interface') != interface:
|
return steps
|
||||||
continue
|
|
||||||
|
for step in steps:
|
||||||
new_priority = override_priorities.get(step.get('step'))
|
new_priority = override_priorities.get(step.get('step'))
|
||||||
if new_priority is not None:
|
if new_priority is not None:
|
||||||
step['priority'] = new_priority
|
step['priority'] = new_priority
|
||||||
result.append(step)
|
|
||||||
|
|
||||||
return result
|
return steps
|
||||||
|
|
||||||
|
|
||||||
def agent_execute_clean_step(task, step):
|
def agent_execute_clean_step(task, step):
|
||||||
|
@ -213,6 +213,9 @@ class IloVirtualMediaAgentDeploy(agent.AgentDeploy):
|
|||||||
"""Get the list of clean steps from the agent.
|
"""Get the list of clean steps from the agent.
|
||||||
|
|
||||||
:param task: a TaskManager object containing the node
|
:param task: a TaskManager object containing the node
|
||||||
|
:raises NodeCleaningFailure: if the clean steps are not yet
|
||||||
|
available (cached), for example, when a node has just been
|
||||||
|
enrolled and has not been cleaned yet.
|
||||||
:returns: A list of clean step dictionaries
|
:returns: A list of clean step dictionaries
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -751,7 +751,9 @@ class ISCSIDeploy(base.DeployInterface):
|
|||||||
"""Get the list of clean steps from the agent.
|
"""Get the list of clean steps from the agent.
|
||||||
|
|
||||||
:param task: a TaskManager object containing the node
|
:param task: a TaskManager object containing the node
|
||||||
|
:raises NodeCleaningFailure: if the clean steps are not yet
|
||||||
|
available (cached), for example, when a node has just been
|
||||||
|
enrolled and has not been cleaned yet.
|
||||||
:returns: A list of clean step dictionaries. If bash ramdisk is
|
:returns: A list of clean step dictionaries. If bash ramdisk is
|
||||||
used for this node, it returns an empty list.
|
used for this node, it returns an empty list.
|
||||||
"""
|
"""
|
||||||
|
@ -357,11 +357,13 @@ class TestBaseAgentVendor(db_base.DbTestCase):
|
|||||||
'is done. Exception: LlamaException')
|
'is done. Exception: LlamaException')
|
||||||
|
|
||||||
@mock.patch.object(objects.node.Node, 'touch_provisioning', autospec=True)
|
@mock.patch.object(objects.node.Node, 'touch_provisioning', autospec=True)
|
||||||
|
@mock.patch.object(agent_base_vendor.BaseAgentVendor,
|
||||||
|
'_refresh_clean_steps', autospec=True)
|
||||||
@mock.patch.object(manager_utils, 'set_node_cleaning_steps', autospec=True)
|
@mock.patch.object(manager_utils, 'set_node_cleaning_steps', autospec=True)
|
||||||
@mock.patch.object(agent_base_vendor.BaseAgentVendor,
|
@mock.patch.object(agent_base_vendor.BaseAgentVendor,
|
||||||
'notify_conductor_resume_clean', autospec=True)
|
'notify_conductor_resume_clean', autospec=True)
|
||||||
def test_heartbeat_resume_clean(self, mock_notify, mock_set_steps,
|
def test_heartbeat_resume_clean(self, mock_notify, mock_set_steps,
|
||||||
mock_touch):
|
mock_refresh, mock_touch):
|
||||||
kwargs = {
|
kwargs = {
|
||||||
'agent_url': 'http://127.0.0.1:9999/bar'
|
'agent_url': 'http://127.0.0.1:9999/bar'
|
||||||
}
|
}
|
||||||
@ -374,13 +376,55 @@ class TestBaseAgentVendor(db_base.DbTestCase):
|
|||||||
self.passthru.heartbeat(task, **kwargs)
|
self.passthru.heartbeat(task, **kwargs)
|
||||||
|
|
||||||
mock_touch.assert_called_once_with(mock.ANY)
|
mock_touch.assert_called_once_with(mock.ANY)
|
||||||
|
mock_refresh.assert_called_once_with(mock.ANY, task)
|
||||||
mock_notify.assert_called_once_with(mock.ANY, task)
|
mock_notify.assert_called_once_with(mock.ANY, task)
|
||||||
mock_set_steps.assert_called_once_with(task)
|
mock_set_steps.assert_called_once_with(task)
|
||||||
# Reset mocks for the next interaction
|
# Reset mocks for the next interaction
|
||||||
mock_touch.reset_mock()
|
mock_touch.reset_mock()
|
||||||
|
mock_refresh.reset_mock()
|
||||||
mock_notify.reset_mock()
|
mock_notify.reset_mock()
|
||||||
mock_set_steps.reset_mock()
|
mock_set_steps.reset_mock()
|
||||||
|
|
||||||
|
@mock.patch.object(manager_utils, 'cleaning_error_handler')
|
||||||
|
@mock.patch.object(objects.node.Node, 'touch_provisioning', autospec=True)
|
||||||
|
@mock.patch.object(agent_base_vendor.BaseAgentVendor,
|
||||||
|
'_refresh_clean_steps', autospec=True)
|
||||||
|
@mock.patch.object(manager_utils, 'set_node_cleaning_steps', autospec=True)
|
||||||
|
@mock.patch.object(agent_base_vendor.BaseAgentVendor,
|
||||||
|
'notify_conductor_resume_clean', autospec=True)
|
||||||
|
def test_heartbeat_resume_clean_fails(self, mock_notify, mock_set_steps,
|
||||||
|
mock_refresh, mock_touch,
|
||||||
|
mock_handler):
|
||||||
|
mocks = [mock_refresh, mock_set_steps, mock_notify]
|
||||||
|
kwargs = {
|
||||||
|
'agent_url': 'http://127.0.0.1:9999/bar'
|
||||||
|
}
|
||||||
|
self.node.clean_step = {}
|
||||||
|
self.node.save()
|
||||||
|
for state in (states.CLEANWAIT, states.CLEANING):
|
||||||
|
self.node.provision_state = state
|
||||||
|
self.node.save()
|
||||||
|
for i in range(len(mocks)):
|
||||||
|
before_failed_mocks = mocks[:i]
|
||||||
|
failed_mock = mocks[i]
|
||||||
|
after_failed_mocks = mocks[i + 1:]
|
||||||
|
failed_mock.side_effect = Exception()
|
||||||
|
with task_manager.acquire(
|
||||||
|
self.context, self.node.uuid, shared=True) as task:
|
||||||
|
self.passthru.heartbeat(task, **kwargs)
|
||||||
|
|
||||||
|
mock_touch.assert_called_once_with(mock.ANY)
|
||||||
|
mock_handler.assert_called_once_with(task, mock.ANY)
|
||||||
|
for called in before_failed_mocks + [failed_mock]:
|
||||||
|
self.assertTrue(called.called)
|
||||||
|
for not_called in after_failed_mocks:
|
||||||
|
self.assertFalse(not_called.called)
|
||||||
|
|
||||||
|
# Reset mocks for the next interaction
|
||||||
|
for m in mocks + [mock_touch, mock_handler]:
|
||||||
|
m.reset_mock()
|
||||||
|
failed_mock.side_effect = None
|
||||||
|
|
||||||
@mock.patch.object(objects.node.Node, 'touch_provisioning', autospec=True)
|
@mock.patch.object(objects.node.Node, 'touch_provisioning', autospec=True)
|
||||||
@mock.patch.object(agent_base_vendor.BaseAgentVendor,
|
@mock.patch.object(agent_base_vendor.BaseAgentVendor,
|
||||||
'continue_cleaning', autospec=True)
|
'continue_cleaning', autospec=True)
|
||||||
@ -897,25 +941,13 @@ class TestBaseAgentVendor(db_base.DbTestCase):
|
|||||||
@mock.patch.object(manager_utils, 'set_node_cleaning_steps', autospec=True)
|
@mock.patch.object(manager_utils, 'set_node_cleaning_steps', autospec=True)
|
||||||
@mock.patch.object(agent_base_vendor.BaseAgentVendor,
|
@mock.patch.object(agent_base_vendor.BaseAgentVendor,
|
||||||
'notify_conductor_resume_clean', autospec=True)
|
'notify_conductor_resume_clean', autospec=True)
|
||||||
@mock.patch.object(agent_client.AgentClient, 'get_clean_steps',
|
@mock.patch.object(agent_base_vendor.BaseAgentVendor,
|
||||||
autospec=True)
|
'_refresh_clean_steps', autospec=True)
|
||||||
@mock.patch.object(agent_client.AgentClient, 'get_commands_status',
|
@mock.patch.object(agent_client.AgentClient, 'get_commands_status',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
def _test_continue_cleaning_clean_version_mismatch(
|
def _test_continue_cleaning_clean_version_mismatch(
|
||||||
self, status_mock, get_steps_mock, notify_mock, steps_mock,
|
self, status_mock, refresh_steps_mock, notify_mock, steps_mock,
|
||||||
manual=False):
|
manual=False):
|
||||||
get_steps_mock.return_value = {
|
|
||||||
'command_status': 'CLEAN_VERSION_MISMATCH',
|
|
||||||
'command_name': 'get_clean_step',
|
|
||||||
'command_result': {
|
|
||||||
'hardware_manager_version': {'Generic': '1'},
|
|
||||||
'clean_steps': {
|
|
||||||
'GenericHardwareManager': [
|
|
||||||
{'interface': 'deploy',
|
|
||||||
'step': 'erase_devices',
|
|
||||||
'priority': 20}]}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
status_mock.return_value = [{
|
status_mock.return_value = [{
|
||||||
'command_status': 'CLEAN_VERSION_MISMATCH',
|
'command_status': 'CLEAN_VERSION_MISMATCH',
|
||||||
'command_name': 'execute_clean_step',
|
'command_name': 'execute_clean_step',
|
||||||
@ -928,17 +960,12 @@ class TestBaseAgentVendor(db_base.DbTestCase):
|
|||||||
shared=False) as task:
|
shared=False) as task:
|
||||||
self.passthru.continue_cleaning(task)
|
self.passthru.continue_cleaning(task)
|
||||||
notify_mock.assert_called_once_with(mock.ANY, task)
|
notify_mock.assert_called_once_with(mock.ANY, task)
|
||||||
|
refresh_steps_mock.assert_called_once_with(mock.ANY, task)
|
||||||
if manual:
|
if manual:
|
||||||
get_steps_mock.assert_called_once_with(mock.ANY, task.node,
|
|
||||||
task.ports)
|
|
||||||
self.assertFalse(
|
self.assertFalse(
|
||||||
task.node.driver_internal_info['skip_current_clean_step'])
|
task.node.driver_internal_info['skip_current_clean_step'])
|
||||||
self.assertEqual(
|
|
||||||
{'Generic': '1'},
|
|
||||||
task.node.driver_internal_info['hardware_manager_version'])
|
|
||||||
self.assertFalse(steps_mock.called)
|
self.assertFalse(steps_mock.called)
|
||||||
else:
|
else:
|
||||||
self.assertFalse(get_steps_mock.called)
|
|
||||||
steps_mock.assert_called_once_with(task)
|
steps_mock.assert_called_once_with(task)
|
||||||
self.assertFalse('skip_current_clean_step' in
|
self.assertFalse('skip_current_clean_step' in
|
||||||
task.node.driver_internal_info)
|
task.node.driver_internal_info)
|
||||||
@ -950,25 +977,23 @@ class TestBaseAgentVendor(db_base.DbTestCase):
|
|||||||
self._test_continue_cleaning_clean_version_mismatch(manual=True)
|
self._test_continue_cleaning_clean_version_mismatch(manual=True)
|
||||||
|
|
||||||
@mock.patch.object(manager_utils, 'cleaning_error_handler', autospec=True)
|
@mock.patch.object(manager_utils, 'cleaning_error_handler', autospec=True)
|
||||||
|
@mock.patch.object(manager_utils, 'set_node_cleaning_steps', autospec=True)
|
||||||
@mock.patch.object(agent_base_vendor.BaseAgentVendor,
|
@mock.patch.object(agent_base_vendor.BaseAgentVendor,
|
||||||
'notify_conductor_resume_clean', autospec=True)
|
'notify_conductor_resume_clean', autospec=True)
|
||||||
@mock.patch.object(agent_client.AgentClient, 'get_clean_steps',
|
@mock.patch.object(agent_base_vendor.BaseAgentVendor,
|
||||||
autospec=True)
|
'_refresh_clean_steps', autospec=True)
|
||||||
@mock.patch.object(agent_client.AgentClient, 'get_commands_status',
|
@mock.patch.object(agent_client.AgentClient, 'get_commands_status',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
def test_continue_cleaning_manual_version_mismatch_bad(
|
def test_continue_cleaning_clean_version_mismatch_fail(
|
||||||
self, status_mock, get_steps_mock, notify_mock, error_mock):
|
self, status_mock, refresh_steps_mock, notify_mock, steps_mock,
|
||||||
get_steps_mock.return_value = {
|
error_mock, manual=False):
|
||||||
'command_status': 'CLEAN_VERSION_MISMATCH',
|
|
||||||
'command_name': 'get_clean_step',
|
|
||||||
'command_result': {
|
|
||||||
'hardware_manager_version': {'Generic': '1'}}
|
|
||||||
}
|
|
||||||
status_mock.return_value = [{
|
status_mock.return_value = [{
|
||||||
'command_status': 'CLEAN_VERSION_MISMATCH',
|
'command_status': 'CLEAN_VERSION_MISMATCH',
|
||||||
'command_name': 'execute_clean_step',
|
'command_name': 'execute_clean_step',
|
||||||
|
'command_result': {'hardware_manager_version': {'Generic': '1'}}
|
||||||
}]
|
}]
|
||||||
tgt_prov_state = states.MANAGEABLE
|
refresh_steps_mock.side_effect = exception.NodeCleaningFailure("boo")
|
||||||
|
tgt_prov_state = states.MANAGEABLE if manual else states.AVAILABLE
|
||||||
self.node.provision_state = states.CLEANWAIT
|
self.node.provision_state = states.CLEANWAIT
|
||||||
self.node.target_provision_state = tgt_prov_state
|
self.node.target_provision_state = tgt_prov_state
|
||||||
self.node.save()
|
self.node.save()
|
||||||
@ -976,12 +1001,11 @@ class TestBaseAgentVendor(db_base.DbTestCase):
|
|||||||
shared=False) as task:
|
shared=False) as task:
|
||||||
self.passthru.continue_cleaning(task)
|
self.passthru.continue_cleaning(task)
|
||||||
|
|
||||||
get_steps_mock.assert_called_once_with(mock.ANY, task.node,
|
status_mock.assert_called_once_with(mock.ANY, task.node)
|
||||||
task.ports)
|
refresh_steps_mock.assert_called_once_with(mock.ANY, task)
|
||||||
error_mock.assert_called_once_with(task, mock.ANY)
|
error_mock.assert_called_once_with(task, mock.ANY)
|
||||||
self.assertFalse(notify_mock.called)
|
self.assertFalse(notify_mock.called)
|
||||||
self.assertFalse('skip_current_clean_step' in
|
self.assertFalse(steps_mock.called)
|
||||||
task.node.driver_internal_info)
|
|
||||||
|
|
||||||
@mock.patch.object(manager_utils, 'cleaning_error_handler', autospec=True)
|
@mock.patch.object(manager_utils, 'cleaning_error_handler', autospec=True)
|
||||||
@mock.patch.object(agent_client.AgentClient, 'get_commands_status',
|
@mock.patch.object(agent_client.AgentClient, 'get_commands_status',
|
||||||
@ -1076,3 +1100,91 @@ class TestBaseAgentVendor(db_base.DbTestCase):
|
|||||||
self.node.save()
|
self.node.save()
|
||||||
hook_returned = agent_base_vendor._get_post_clean_step_hook(self.node)
|
hook_returned = agent_base_vendor._get_post_clean_step_hook(self.node)
|
||||||
self.assertIsNone(hook_returned)
|
self.assertIsNone(hook_returned)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRefreshCleanSteps(TestBaseAgentVendor):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestRefreshCleanSteps, self).setUp()
|
||||||
|
self.node.driver_internal_info['agent_url'] = 'http://127.0.0.1:9999'
|
||||||
|
self.ports = [object_utils.create_test_port(self.context,
|
||||||
|
node_id=self.node.id)]
|
||||||
|
|
||||||
|
self.clean_steps = {
|
||||||
|
'hardware_manager_version': '1',
|
||||||
|
'clean_steps': {
|
||||||
|
'GenericHardwareManager': [
|
||||||
|
{'interface': 'deploy',
|
||||||
|
'step': 'erase_devices',
|
||||||
|
'priority': 20},
|
||||||
|
],
|
||||||
|
'SpecificHardwareManager': [
|
||||||
|
{'interface': 'deploy',
|
||||||
|
'step': 'update_firmware',
|
||||||
|
'priority': 30},
|
||||||
|
{'interface': 'raid',
|
||||||
|
'step': 'create_configuration',
|
||||||
|
'priority': 10},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mock.patch.object(agent_client.AgentClient, 'get_clean_steps',
|
||||||
|
autospec=True)
|
||||||
|
def test__refresh_clean_steps(self, client_mock):
|
||||||
|
client_mock.return_value = {
|
||||||
|
'command_result': self.clean_steps}
|
||||||
|
|
||||||
|
with task_manager.acquire(
|
||||||
|
self.context, self.node.uuid, shared=False) as task:
|
||||||
|
self.passthru._refresh_clean_steps(task)
|
||||||
|
|
||||||
|
client_mock.assert_called_once_with(mock.ANY, task.node,
|
||||||
|
task.ports)
|
||||||
|
self.assertEqual('1', task.node.driver_internal_info[
|
||||||
|
'hardware_manager_version'])
|
||||||
|
self.assertTrue('agent_cached_clean_steps_refreshed' in
|
||||||
|
task.node.driver_internal_info)
|
||||||
|
steps = task.node.driver_internal_info['agent_cached_clean_steps']
|
||||||
|
# Since steps are returned in dicts, they have non-deterministic
|
||||||
|
# ordering
|
||||||
|
self.assertEqual(2, len(steps))
|
||||||
|
self.assertIn(self.clean_steps['clean_steps'][
|
||||||
|
'GenericHardwareManager'][0], steps['deploy'])
|
||||||
|
self.assertIn(self.clean_steps['clean_steps'][
|
||||||
|
'SpecificHardwareManager'][0], steps['deploy'])
|
||||||
|
self.assertEqual([self.clean_steps['clean_steps'][
|
||||||
|
'SpecificHardwareManager'][1]], steps['raid'])
|
||||||
|
|
||||||
|
@mock.patch.object(agent_client.AgentClient, 'get_clean_steps',
|
||||||
|
autospec=True)
|
||||||
|
def test__refresh_clean_steps_missing_steps(self, client_mock):
|
||||||
|
del self.clean_steps['clean_steps']
|
||||||
|
client_mock.return_value = {
|
||||||
|
'command_result': self.clean_steps}
|
||||||
|
|
||||||
|
with task_manager.acquire(
|
||||||
|
self.context, self.node.uuid, shared=False) as task:
|
||||||
|
self.assertRaisesRegex(exception.NodeCleaningFailure,
|
||||||
|
'invalid result',
|
||||||
|
self.passthru._refresh_clean_steps,
|
||||||
|
task)
|
||||||
|
client_mock.assert_called_once_with(mock.ANY, task.node,
|
||||||
|
task.ports)
|
||||||
|
|
||||||
|
@mock.patch.object(agent_client.AgentClient, 'get_clean_steps',
|
||||||
|
autospec=True)
|
||||||
|
def test__refresh_clean_steps_missing_interface(self, client_mock):
|
||||||
|
step = self.clean_steps['clean_steps']['SpecificHardwareManager'][1]
|
||||||
|
del step['interface']
|
||||||
|
client_mock.return_value = {
|
||||||
|
'command_result': self.clean_steps}
|
||||||
|
|
||||||
|
with task_manager.acquire(
|
||||||
|
self.context, self.node.uuid, shared=False) as task:
|
||||||
|
self.assertRaisesRegex(exception.NodeCleaningFailure,
|
||||||
|
'invalid clean step',
|
||||||
|
self.passthru._refresh_clean_steps,
|
||||||
|
task)
|
||||||
|
client_mock.assert_called_once_with(mock.ANY, task.node,
|
||||||
|
task.ports)
|
||||||
|
@ -1521,142 +1521,75 @@ class AgentMethodsTestCase(db_base.DbTestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(AgentMethodsTestCase, self).setUp()
|
super(AgentMethodsTestCase, self).setUp()
|
||||||
mgr_utils.mock_the_extension_manager(driver='fake_agent')
|
mgr_utils.mock_the_extension_manager(driver='fake_agent')
|
||||||
n = {'driver': 'fake_agent',
|
|
||||||
'driver_internal_info': {'agent_url': 'http://127.0.0.1:9999'}}
|
|
||||||
|
|
||||||
|
self.clean_steps = {
|
||||||
|
'deploy': [
|
||||||
|
{'interface': 'deploy',
|
||||||
|
'step': 'erase_devices',
|
||||||
|
'priority': 20},
|
||||||
|
{'interface': 'deploy',
|
||||||
|
'step': 'update_firmware',
|
||||||
|
'priority': 30}
|
||||||
|
],
|
||||||
|
'raid': [
|
||||||
|
{'interface': 'raid',
|
||||||
|
'step': 'create_configuration',
|
||||||
|
'priority': 10}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
n = {'driver': 'fake_agent',
|
||||||
|
'driver_internal_info': {
|
||||||
|
'agent_cached_clean_steps': self.clean_steps}}
|
||||||
self.node = obj_utils.create_test_node(self.context, **n)
|
self.node = obj_utils.create_test_node(self.context, **n)
|
||||||
self.ports = [obj_utils.create_test_port(self.context,
|
self.ports = [obj_utils.create_test_port(self.context,
|
||||||
node_id=self.node.id)]
|
node_id=self.node.id)]
|
||||||
|
|
||||||
self.clean_steps = {
|
def test_agent_get_clean_steps(self):
|
||||||
'hardware_manager_version': '1',
|
|
||||||
'clean_steps': {
|
|
||||||
'GenericHardwareManager': [
|
|
||||||
{'interface': 'deploy',
|
|
||||||
'step': 'erase_devices',
|
|
||||||
'priority': 20},
|
|
||||||
],
|
|
||||||
'SpecificHardwareManager': [
|
|
||||||
{'interface': 'deploy',
|
|
||||||
'step': 'update_firmware',
|
|
||||||
'priority': 30},
|
|
||||||
{'interface': 'raid',
|
|
||||||
'step': 'create_configuration',
|
|
||||||
'priority': 10},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mock.patch('ironic.objects.Port.list_by_node_id',
|
|
||||||
spec_set=types.FunctionType)
|
|
||||||
@mock.patch.object(agent_client.AgentClient, 'get_clean_steps',
|
|
||||||
autospec=True)
|
|
||||||
def test_get_clean_steps(self, client_mock, list_ports_mock):
|
|
||||||
client_mock.return_value = {
|
|
||||||
'command_result': self.clean_steps}
|
|
||||||
list_ports_mock.return_value = self.ports
|
|
||||||
|
|
||||||
with task_manager.acquire(
|
with task_manager.acquire(
|
||||||
self.context, self.node['uuid'], shared=False) as task:
|
self.context, self.node.uuid, shared=False) as task:
|
||||||
response = utils.agent_get_clean_steps(task)
|
response = utils.agent_get_clean_steps(task)
|
||||||
client_mock.assert_called_once_with(mock.ANY, task.node,
|
|
||||||
self.ports)
|
|
||||||
self.assertEqual('1', task.node.driver_internal_info[
|
|
||||||
'hardware_manager_version'])
|
|
||||||
|
|
||||||
# Since steps are returned in dicts, they have non-deterministic
|
# Since steps are returned in dicts, they have non-deterministic
|
||||||
# ordering
|
# ordering
|
||||||
self.assertThat(response, matchers.HasLength(3))
|
self.assertThat(response, matchers.HasLength(3))
|
||||||
self.assertIn(self.clean_steps['clean_steps'][
|
self.assertIn(self.clean_steps['deploy'][0], response)
|
||||||
'GenericHardwareManager'][0], response)
|
self.assertIn(self.clean_steps['deploy'][1], response)
|
||||||
self.assertIn(self.clean_steps['clean_steps'][
|
self.assertIn(self.clean_steps['raid'][0], response)
|
||||||
'SpecificHardwareManager'][0], response)
|
|
||||||
self.assertIn(self.clean_steps['clean_steps'][
|
|
||||||
'SpecificHardwareManager'][1], response)
|
|
||||||
|
|
||||||
@mock.patch('ironic.objects.Port.list_by_node_id',
|
|
||||||
spec_set=types.FunctionType)
|
|
||||||
@mock.patch.object(agent_client.AgentClient, 'get_clean_steps',
|
|
||||||
autospec=True)
|
|
||||||
def test_get_clean_steps_custom_interface(
|
|
||||||
self, client_mock, list_ports_mock):
|
|
||||||
client_mock.return_value = {
|
|
||||||
'command_result': self.clean_steps}
|
|
||||||
list_ports_mock.return_value = self.ports
|
|
||||||
|
|
||||||
|
def test_get_clean_steps_custom_interface(self):
|
||||||
with task_manager.acquire(
|
with task_manager.acquire(
|
||||||
self.context, self.node.uuid, shared=False) as task:
|
self.context, self.node.uuid, shared=False) as task:
|
||||||
response = utils.agent_get_clean_steps(task, interface='raid')
|
response = utils.agent_get_clean_steps(task, interface='raid')
|
||||||
client_mock.assert_called_once_with(mock.ANY, task.node,
|
|
||||||
self.ports)
|
|
||||||
self.assertEqual('1', task.node.driver_internal_info[
|
|
||||||
'hardware_manager_version'])
|
|
||||||
|
|
||||||
self.assertThat(response, matchers.HasLength(1))
|
self.assertThat(response, matchers.HasLength(1))
|
||||||
self.assertIn(self.clean_steps['clean_steps'][
|
self.assertEqual(self.clean_steps['raid'], response)
|
||||||
'SpecificHardwareManager'][1], response)
|
|
||||||
|
|
||||||
@mock.patch('ironic.objects.Port.list_by_node_id',
|
|
||||||
spec_set=types.FunctionType)
|
|
||||||
@mock.patch.object(agent_client.AgentClient, 'get_clean_steps',
|
|
||||||
autospec=True)
|
|
||||||
def test_get_clean_steps_override_priorities(
|
|
||||||
self, client_mock, list_ports_mock):
|
|
||||||
client_mock.return_value = {
|
|
||||||
'command_result': self.clean_steps}
|
|
||||||
list_ports_mock.return_value = self.ports
|
|
||||||
|
|
||||||
|
def test_get_clean_steps_override_priorities(self):
|
||||||
with task_manager.acquire(
|
with task_manager.acquire(
|
||||||
self.context, self.node.uuid, shared=False) as task:
|
self.context, self.node.uuid, shared=False) as task:
|
||||||
new_priorities = {'create_configuration': 42}
|
new_priorities = {'create_configuration': 42}
|
||||||
response = utils.agent_get_clean_steps(
|
response = utils.agent_get_clean_steps(
|
||||||
task, interface='raid', override_priorities=new_priorities)
|
task, interface='raid', override_priorities=new_priorities)
|
||||||
client_mock.assert_called_once_with(mock.ANY, task.node,
|
|
||||||
self.ports)
|
|
||||||
self.assertEqual('1', task.node.driver_internal_info[
|
|
||||||
'hardware_manager_version'])
|
|
||||||
self.assertEqual(42, response[0]['priority'])
|
self.assertEqual(42, response[0]['priority'])
|
||||||
|
|
||||||
@mock.patch('ironic.objects.Port.list_by_node_id',
|
def test_get_clean_steps_override_priorities_none(self):
|
||||||
spec_set=types.FunctionType)
|
|
||||||
@mock.patch.object(agent_client.AgentClient, 'get_clean_steps',
|
|
||||||
autospec=True)
|
|
||||||
def test_get_clean_steps_override_priorities_none(
|
|
||||||
self, client_mock, list_ports_mock):
|
|
||||||
client_mock.return_value = {
|
|
||||||
'command_result': self.clean_steps}
|
|
||||||
list_ports_mock.return_value = self.ports
|
|
||||||
|
|
||||||
with task_manager.acquire(
|
with task_manager.acquire(
|
||||||
self.context, self.node.uuid, shared=False) as task:
|
self.context, self.node.uuid, shared=False) as task:
|
||||||
# this is simulating the default value of a configuration option
|
# this is simulating the default value of a configuration option
|
||||||
new_priorities = {'create_configuration': None}
|
new_priorities = {'create_configuration': None}
|
||||||
response = utils.agent_get_clean_steps(
|
response = utils.agent_get_clean_steps(
|
||||||
task, interface='raid', override_priorities=new_priorities)
|
task, interface='raid', override_priorities=new_priorities)
|
||||||
client_mock.assert_called_once_with(mock.ANY, task.node,
|
|
||||||
self.ports)
|
|
||||||
self.assertEqual('1', task.node.driver_internal_info[
|
|
||||||
'hardware_manager_version'])
|
|
||||||
self.assertEqual(10, response[0]['priority'])
|
self.assertEqual(10, response[0]['priority'])
|
||||||
|
|
||||||
@mock.patch('ironic.objects.Port.list_by_node_id',
|
def test_get_clean_steps_missing_steps(self):
|
||||||
spec_set=types.FunctionType)
|
info = self.node.driver_internal_info
|
||||||
@mock.patch.object(agent_client.AgentClient, 'get_clean_steps',
|
del info['agent_cached_clean_steps']
|
||||||
autospec=True)
|
self.node.driver_internal_info = info
|
||||||
def test_get_clean_steps_missing_steps(self, client_mock,
|
self.node.save()
|
||||||
list_ports_mock):
|
|
||||||
del self.clean_steps['clean_steps']
|
|
||||||
client_mock.return_value = {
|
|
||||||
'command_result': self.clean_steps}
|
|
||||||
list_ports_mock.return_value = self.ports
|
|
||||||
|
|
||||||
with task_manager.acquire(
|
with task_manager.acquire(
|
||||||
self.context, self.node.uuid, shared=False) as task:
|
self.context, self.node.uuid, shared=False) as task:
|
||||||
self.assertRaises(exception.NodeCleaningFailure,
|
self.assertRaises(exception.NodeCleaningFailure,
|
||||||
utils.agent_get_clean_steps,
|
utils.agent_get_clean_steps,
|
||||||
task)
|
task)
|
||||||
client_mock.assert_called_once_with(mock.ANY, task.node,
|
|
||||||
self.ports)
|
|
||||||
|
|
||||||
@mock.patch('ironic.objects.Port.list_by_node_id',
|
@mock.patch('ironic.objects.Port.list_by_node_id',
|
||||||
spec_set=types.FunctionType)
|
spec_set=types.FunctionType)
|
||||||
@ -1668,10 +1601,10 @@ class AgentMethodsTestCase(db_base.DbTestCase):
|
|||||||
list_ports_mock.return_value = self.ports
|
list_ports_mock.return_value = self.ports
|
||||||
|
|
||||||
with task_manager.acquire(
|
with task_manager.acquire(
|
||||||
self.context, self.node['uuid'], shared=False) as task:
|
self.context, self.node.uuid, shared=False) as task:
|
||||||
response = utils.agent_execute_clean_step(
|
response = utils.agent_execute_clean_step(
|
||||||
task,
|
task,
|
||||||
self.clean_steps['clean_steps']['GenericHardwareManager'][0])
|
self.clean_steps['deploy'][0])
|
||||||
self.assertEqual(states.CLEANWAIT, response)
|
self.assertEqual(states.CLEANWAIT, response)
|
||||||
|
|
||||||
@mock.patch('ironic.objects.Port.list_by_node_id',
|
@mock.patch('ironic.objects.Port.list_by_node_id',
|
||||||
@ -1684,10 +1617,10 @@ class AgentMethodsTestCase(db_base.DbTestCase):
|
|||||||
list_ports_mock.return_value = self.ports
|
list_ports_mock.return_value = self.ports
|
||||||
|
|
||||||
with task_manager.acquire(
|
with task_manager.acquire(
|
||||||
self.context, self.node['uuid'], shared=False) as task:
|
self.context, self.node.uuid, shared=False) as task:
|
||||||
response = utils.agent_execute_clean_step(
|
response = utils.agent_execute_clean_step(
|
||||||
task,
|
task,
|
||||||
self.clean_steps['clean_steps']['GenericHardwareManager'][0])
|
self.clean_steps['deploy'][0])
|
||||||
self.assertEqual(states.CLEANWAIT, response)
|
self.assertEqual(states.CLEANWAIT, response)
|
||||||
|
|
||||||
@mock.patch('ironic.objects.Port.list_by_node_id',
|
@mock.patch('ironic.objects.Port.list_by_node_id',
|
||||||
@ -1701,16 +1634,16 @@ class AgentMethodsTestCase(db_base.DbTestCase):
|
|||||||
list_ports_mock.return_value = self.ports
|
list_ports_mock.return_value = self.ports
|
||||||
|
|
||||||
with task_manager.acquire(
|
with task_manager.acquire(
|
||||||
self.context, self.node['uuid'], shared=False) as task:
|
self.context, self.node.uuid, shared=False) as task:
|
||||||
response = utils.agent_execute_clean_step(
|
response = utils.agent_execute_clean_step(
|
||||||
task,
|
task,
|
||||||
self.clean_steps['clean_steps']['GenericHardwareManager'][0])
|
self.clean_steps['deploy'][0])
|
||||||
self.assertEqual(states.CLEANWAIT, response)
|
self.assertEqual(states.CLEANWAIT, response)
|
||||||
|
|
||||||
def test_agent_add_clean_params(self):
|
def test_agent_add_clean_params(self):
|
||||||
cfg.CONF.deploy.erase_devices_iterations = 2
|
cfg.CONF.deploy.erase_devices_iterations = 2
|
||||||
with task_manager.acquire(
|
with task_manager.acquire(
|
||||||
self.context, self.node['uuid'], shared=False) as task:
|
self.context, self.node.uuid, shared=False) as task:
|
||||||
utils.agent_add_clean_params(task)
|
utils.agent_add_clean_params(task)
|
||||||
self.assertEqual(task.node.driver_internal_info.get(
|
self.assertEqual(task.node.driver_internal_info.get(
|
||||||
'agent_erase_devices_iterations'), 2)
|
'agent_erase_devices_iterations'), 2)
|
||||||
|
Loading…
Reference in New Issue
Block a user