Merge "Add cleaning/zapping support to IPA"
This commit is contained in:
commit
173f86963a
@ -284,6 +284,38 @@ class IncompatibleHardwareMethodError(RESTError):
|
|||||||
super(IncompatibleHardwareMethodError, self).__init__(details)
|
super(IncompatibleHardwareMethodError, self).__init__(details)
|
||||||
|
|
||||||
|
|
||||||
|
class CleanVersionMismatch(RESTError):
|
||||||
|
"""Error raised when Ironic and the Agent have different versions.
|
||||||
|
|
||||||
|
If the agent version has changed since get_clean_steps was called by
|
||||||
|
the Ironic conductor, it indicates the agent has been updated (either
|
||||||
|
on purpose, or a new agent was deployed and the node was rebooted).
|
||||||
|
Since we cannot know if the upgraded IPA will work with cleaning as it
|
||||||
|
stands (steps could have different priorities, either in IPA or in
|
||||||
|
other Ironic interfaces), we should restart cleaning from the start.
|
||||||
|
|
||||||
|
"""
|
||||||
|
message = 'Clean version mismatch, reload agent with correct version'
|
||||||
|
|
||||||
|
def __init__(self, agent_version, node_version):
|
||||||
|
self.status_code = 409
|
||||||
|
details = ('Agent clean version: {0}, node clean version: {1}'
|
||||||
|
.format(agent_version, node_version))
|
||||||
|
super(CleanVersionMismatch, self).__init__(details)
|
||||||
|
|
||||||
|
|
||||||
|
class CleaningError(RESTError):
|
||||||
|
"""Error raised when a cleaning step fails."""
|
||||||
|
message = 'Clean step failed.'
|
||||||
|
|
||||||
|
def __init__(self, details=None):
|
||||||
|
if details is not None:
|
||||||
|
details = details
|
||||||
|
else:
|
||||||
|
details = self.message
|
||||||
|
super(CleaningError, self).__init__(details)
|
||||||
|
|
||||||
|
|
||||||
class ISCSIError(RESTError):
|
class ISCSIError(RESTError):
|
||||||
"""Error raised when an image cannot be written to a device."""
|
"""Error raised when an image cannot be written to a device."""
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@ class AgentCommandStatus(object):
|
|||||||
RUNNING = u'RUNNING'
|
RUNNING = u'RUNNING'
|
||||||
SUCCEEDED = u'SUCCEEDED'
|
SUCCEEDED = u'SUCCEEDED'
|
||||||
FAILED = u'FAILED'
|
FAILED = u'FAILED'
|
||||||
|
CLEAN_VERSION_MISMATCH = u'CLEAN_VERSION_MISMATCH'
|
||||||
|
|
||||||
|
|
||||||
class BaseCommandResult(encoding.Serializable):
|
class BaseCommandResult(encoding.Serializable):
|
||||||
@ -153,6 +154,11 @@ class AsyncCommandResult(BaseCommandResult):
|
|||||||
with self.command_state_lock:
|
with self.command_state_lock:
|
||||||
self.command_result = result
|
self.command_result = result
|
||||||
self.command_status = AgentCommandStatus.SUCCEEDED
|
self.command_status = AgentCommandStatus.SUCCEEDED
|
||||||
|
except errors.CleanVersionMismatch as e:
|
||||||
|
with self.command_state_lock:
|
||||||
|
self.command_error = e
|
||||||
|
self.command_status = AgentCommandStatus.CLEAN_VERSION_MISMATCH
|
||||||
|
self.command_result = None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if not isinstance(e, errors.RESTError):
|
if not isinstance(e, errors.RESTError):
|
||||||
e = errors.CommandExecutionError(str(e))
|
e = errors.CommandExecutionError(str(e))
|
||||||
|
86
ironic_python_agent/extensions/clean.py
Normal file
86
ironic_python_agent/extensions/clean.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# Copyright 2015 Rackspace, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from ironic_python_agent import errors
|
||||||
|
from ironic_python_agent.extensions import base
|
||||||
|
from ironic_python_agent import hardware
|
||||||
|
|
||||||
|
|
||||||
|
class CleanExtension(base.BaseAgentExtension):
|
||||||
|
@base.sync_command('get_clean_steps')
|
||||||
|
def get_clean_steps(self, node, ports):
|
||||||
|
"""Get the list of clean steps supported for the node and ports
|
||||||
|
|
||||||
|
:param node: A dict representation of a node
|
||||||
|
:param ports: A dict representation of ports attached to node
|
||||||
|
|
||||||
|
:returns: A list of clean steps with keys step, priority, and
|
||||||
|
reboot_requested
|
||||||
|
"""
|
||||||
|
# Results should be a dict, not a list
|
||||||
|
steps = hardware.dispatch_to_all_managers('get_clean_steps',
|
||||||
|
node, ports)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'clean_steps': steps,
|
||||||
|
'hardware_manager_version': _get_current_clean_version()
|
||||||
|
}
|
||||||
|
|
||||||
|
@base.async_command('execute_clean_step')
|
||||||
|
def execute_clean_step(self, step, node, ports, clean_version=None,
|
||||||
|
**kwargs):
|
||||||
|
"""Execute a clean step
|
||||||
|
:param step: A clean step with 'step', 'priority' and 'interface' keys
|
||||||
|
:param node: A dict representation of a node
|
||||||
|
:param ports: A dict representation of ports attached to node
|
||||||
|
:param clean_version: The clean version as returned by
|
||||||
|
_get_current_clean_version() at the beginning of cleaning/zapping
|
||||||
|
:returns: a CommandResult object with command_result set to whatever
|
||||||
|
the step returns.
|
||||||
|
"""
|
||||||
|
# Ensure the agent is still the same version, or raise an exception
|
||||||
|
_check_clean_version(clean_version)
|
||||||
|
|
||||||
|
if 'step' not in step:
|
||||||
|
raise ValueError('Malformed clean_step, no "step" key: %s'.format(
|
||||||
|
step))
|
||||||
|
try:
|
||||||
|
result = hardware.dispatch_to_managers(step['step'], node, ports)
|
||||||
|
except Exception as e:
|
||||||
|
raise errors.CleaningError(
|
||||||
|
'Error performing clean_step %(step)s: %(err)s' %
|
||||||
|
{'step': step['step'], 'err': e})
|
||||||
|
# Return the step that was executed so we can dispatch
|
||||||
|
# to the appropriate Ironic interface
|
||||||
|
return {
|
||||||
|
'clean_result': result,
|
||||||
|
'clean_step': step
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _check_clean_version(clean_version=None):
|
||||||
|
"""Ensure the clean version hasn't changed."""
|
||||||
|
# If the version is None, assume this is the first run
|
||||||
|
if clean_version is None:
|
||||||
|
return
|
||||||
|
agent_version = _get_current_clean_version()
|
||||||
|
if clean_version != agent_version:
|
||||||
|
raise errors.CleanVersionMismatch(agent_version=agent_version,
|
||||||
|
node_version=clean_version)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_current_clean_version():
|
||||||
|
return {version.get('name'): version.get('version')
|
||||||
|
for version in hardware.dispatch_to_all_managers(
|
||||||
|
'get_version').values()}
|
@ -1,24 +0,0 @@
|
|||||||
# Copyright 2013 Rackspace, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
from ironic_python_agent.extensions import base
|
|
||||||
from ironic_python_agent import hardware
|
|
||||||
|
|
||||||
|
|
||||||
class DecomExtension(base.BaseAgentExtension):
|
|
||||||
@base.async_command('erase_hardware')
|
|
||||||
def erase_hardware(self):
|
|
||||||
hardware.dispatch_to_managers('erase_devices')
|
|
||||||
|
|
||||||
return 'finished'
|
|
@ -137,13 +137,16 @@ class HardwareManager(object):
|
|||||||
"""
|
"""
|
||||||
raise errors.IncompatibleHardwareMethodError
|
raise errors.IncompatibleHardwareMethodError
|
||||||
|
|
||||||
def erase_devices(self):
|
def erase_devices(self, node, ports):
|
||||||
"""Erase any device that holds user data.
|
"""Erase any device that holds user data.
|
||||||
|
|
||||||
By default this will attempt to erase block devices. This method can be
|
By default this will attempt to erase block devices. This method can be
|
||||||
overridden in an implementation-specific hardware manager in order to
|
overridden in an implementation-specific hardware manager in order to
|
||||||
erase additional hardware, although backwards-compatible upstream
|
erase additional hardware, although backwards-compatible upstream
|
||||||
submissions are encouraged.
|
submissions are encouraged.
|
||||||
|
|
||||||
|
:param node: Ironic node object
|
||||||
|
:param ports: list of Ironic port objects
|
||||||
"""
|
"""
|
||||||
block_devices = self.list_block_devices()
|
block_devices = self.list_block_devices()
|
||||||
for block_device in block_devices:
|
for block_device in block_devices:
|
||||||
@ -157,8 +160,73 @@ class HardwareManager(object):
|
|||||||
hardware_info['memory'] = self.get_memory()
|
hardware_info['memory'] = self.get_memory()
|
||||||
return hardware_info
|
return hardware_info
|
||||||
|
|
||||||
|
def get_clean_steps(self, node, ports):
|
||||||
|
"""Get a list of clean steps with priority.
|
||||||
|
|
||||||
|
Returns a list of dicts of the following form:
|
||||||
|
{'step': the HardwareManager function to call.
|
||||||
|
'priority': the order steps will be run in. Ironic will sort all the
|
||||||
|
clean steps from all the drivers, with the largest priority
|
||||||
|
step being run first. If priority is set to 0, the step will
|
||||||
|
not be run during cleaning, but may be run during zapping.
|
||||||
|
'reboot_requested': Whether the agent should request Ironic reboots
|
||||||
|
the node via the power driver after the operation completes.
|
||||||
|
}
|
||||||
|
|
||||||
|
Note: multiple hardware managers may return the same step name. The
|
||||||
|
priority of the step will be the largest priority of steps with
|
||||||
|
the same name. The steps will be called using
|
||||||
|
`hardware.dispatch_to_managers` and handled by the best suited
|
||||||
|
hardware manager. If you need a step to be executed by only your
|
||||||
|
hardware manager, ensure it has a unique step name.
|
||||||
|
|
||||||
|
`node` and `ports` can be used by other hardware managers to further
|
||||||
|
determine if a clean step is supported for the node.
|
||||||
|
|
||||||
|
:param node: Ironic node object
|
||||||
|
:param ports: list of Ironic port objects
|
||||||
|
:return: a default list of decommission steps, as a list of
|
||||||
|
dictionaries
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'step': 'erase_devices',
|
||||||
|
'priority': 10,
|
||||||
|
'interface': 'deploy',
|
||||||
|
'reboot_requested': False
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_version(self):
|
||||||
|
"""Get a name and version for this hardware manager.
|
||||||
|
|
||||||
|
In order to avoid errors and make agent upgrades painless, cleaning
|
||||||
|
will check the version of all hardware managers during get_clean_steps
|
||||||
|
at the beginning of cleaning and before executing each step in the
|
||||||
|
agent.
|
||||||
|
|
||||||
|
The agent isn't aware of the steps being taken before or after via
|
||||||
|
out of band steps, so it can never know if a new step is safe to run.
|
||||||
|
Therefore, we default to restarting the whole process.
|
||||||
|
|
||||||
|
:returns: a dictionary with two keys: `name` and
|
||||||
|
`version`, where `name` is a string identifying the hardware
|
||||||
|
manager and `version` is an arbitrary version string. `name` will
|
||||||
|
be a class variable called HARDWARE_MANAGER_NAME, or default to
|
||||||
|
the class name and `version` will be a class variable called
|
||||||
|
HARDWARE_MANAGER_VERSION or default to '1.0'.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'name': getattr(self, 'HARDWARE_MANAGER_NAME',
|
||||||
|
type(self).__name__),
|
||||||
|
'version': getattr(self, 'HARDWARE_MANAGER_VERSION', '1.0')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class GenericHardwareManager(HardwareManager):
|
class GenericHardwareManager(HardwareManager):
|
||||||
|
HARDWARE_MANAGER_NAME = 'generic_hardware_manager'
|
||||||
|
HARDWARE_MANAGER_VERSION = '1.0'
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.sys_path = '/sys'
|
self.sys_path = '/sys'
|
||||||
|
|
||||||
|
157
ironic_python_agent/tests/extensions/clean.py
Normal file
157
ironic_python_agent/tests/extensions/clean.py
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
# Copyright 2015 Rackspace, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import mock
|
||||||
|
from oslotest import base as test_base
|
||||||
|
|
||||||
|
from ironic_python_agent import errors
|
||||||
|
from ironic_python_agent.extensions import clean
|
||||||
|
|
||||||
|
|
||||||
|
class TestCleanExtension(test_base.BaseTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestCleanExtension, self).setUp()
|
||||||
|
self.agent_extension = clean.CleanExtension()
|
||||||
|
self.node = {'uuid': 'dda135fb-732d-4742-8e72-df8f3199d244'}
|
||||||
|
self.ports = []
|
||||||
|
self.step = {
|
||||||
|
'GenericHardwareManager':
|
||||||
|
[{'step': 'erase_devices',
|
||||||
|
'priority': 10,
|
||||||
|
'interface': 'deploy'}]
|
||||||
|
}
|
||||||
|
self.version = {'generic': '1', 'specific': '1'}
|
||||||
|
|
||||||
|
@mock.patch('ironic_python_agent.extensions.clean.'
|
||||||
|
'_get_current_clean_version')
|
||||||
|
@mock.patch('ironic_python_agent.hardware.dispatch_to_all_managers')
|
||||||
|
def test_get_clean_steps(self, mock_dispatch, mock_version):
|
||||||
|
mock_version.return_value = self.version
|
||||||
|
|
||||||
|
manager_steps = {
|
||||||
|
'SpecificHardwareManager': [
|
||||||
|
{
|
||||||
|
'step': 'erase_devices',
|
||||||
|
'priority': 10,
|
||||||
|
'interface': 'deploy',
|
||||||
|
'reboot_requested': False
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'step': 'upgrade_bios',
|
||||||
|
'priority': 20,
|
||||||
|
'interface': 'deploy',
|
||||||
|
'reboot_requested': True
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'FirmwareHardwareManager': [
|
||||||
|
{
|
||||||
|
'step': 'upgrade_firmware',
|
||||||
|
'priority': 30,
|
||||||
|
'interface': 'deploy',
|
||||||
|
'reboot_requested': False
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
mock_dispatch.return_value = manager_steps
|
||||||
|
expected_return = {
|
||||||
|
'hardware_manager_version': self.version,
|
||||||
|
'clean_steps': manager_steps
|
||||||
|
}
|
||||||
|
|
||||||
|
async_results = self.agent_extension.get_clean_steps(node=self.node,
|
||||||
|
ports=self.ports)
|
||||||
|
|
||||||
|
self.assertEqual(expected_return, async_results.join().command_result)
|
||||||
|
|
||||||
|
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers')
|
||||||
|
@mock.patch('ironic_python_agent.extensions.clean._check_clean_version')
|
||||||
|
def test_execute_clean_step(self, mock_version, mock_dispatch):
|
||||||
|
result = 'cleaned'
|
||||||
|
mock_dispatch.return_value = result
|
||||||
|
|
||||||
|
expected_result = {
|
||||||
|
'clean_step': self.step['GenericHardwareManager'][0],
|
||||||
|
'clean_result': result
|
||||||
|
}
|
||||||
|
async_result = self.agent_extension.execute_clean_step(
|
||||||
|
step=self.step['GenericHardwareManager'][0],
|
||||||
|
node=self.node, ports=self.ports,
|
||||||
|
clean_version=self.version)
|
||||||
|
async_result.join()
|
||||||
|
|
||||||
|
mock_version.assert_called_once_with(self.version)
|
||||||
|
mock_dispatch.assert_called_once_with(
|
||||||
|
self.step['GenericHardwareManager'][0]['step'],
|
||||||
|
self.node, self.ports)
|
||||||
|
self.assertEqual(expected_result, async_result.command_result)
|
||||||
|
|
||||||
|
@mock.patch('ironic_python_agent.extensions.clean._check_clean_version')
|
||||||
|
def test_execute_clean_step_no_step(self, mock_version):
|
||||||
|
async_result = self.agent_extension.execute_clean_step(
|
||||||
|
step={}, node=self.node, ports=self.ports,
|
||||||
|
clean_version=self.version)
|
||||||
|
async_result.join()
|
||||||
|
|
||||||
|
self.assertEqual('FAILED', async_result.command_status)
|
||||||
|
mock_version.assert_called_once_with(self.version)
|
||||||
|
|
||||||
|
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers')
|
||||||
|
@mock.patch('ironic_python_agent.extensions.clean._check_clean_version')
|
||||||
|
def test_execute_clean_step_fail(self, mock_version, mock_dispatch):
|
||||||
|
mock_dispatch.side_effect = RuntimeError
|
||||||
|
|
||||||
|
async_result = self.agent_extension.execute_clean_step(
|
||||||
|
step=self.step['GenericHardwareManager'][0], node=self.node,
|
||||||
|
ports=self.ports, clean_version=self.version)
|
||||||
|
async_result.join()
|
||||||
|
|
||||||
|
self.assertEqual('FAILED', async_result.command_status)
|
||||||
|
|
||||||
|
mock_version.assert_called_once_with(self.version)
|
||||||
|
mock_dispatch.assert_called_once_with(
|
||||||
|
self.step['GenericHardwareManager'][0]['step'],
|
||||||
|
self.node, self.ports)
|
||||||
|
|
||||||
|
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers')
|
||||||
|
@mock.patch('ironic_python_agent.extensions.clean._check_clean_version')
|
||||||
|
def test_execute_clean_step_version_mismatch(self, mock_version,
|
||||||
|
mock_dispatch):
|
||||||
|
mock_version.side_effect = errors.CleanVersionMismatch(
|
||||||
|
{'GenericHardwareManager': 1}, {'GenericHardwareManager': 2})
|
||||||
|
|
||||||
|
async_result = self.agent_extension.execute_clean_step(
|
||||||
|
step=self.step['GenericHardwareManager'][0], node=self.node,
|
||||||
|
ports=self.ports, clean_version=self.version)
|
||||||
|
async_result.join()
|
||||||
|
self.assertEqual('CLEAN_VERSION_MISMATCH', async_result.command_status)
|
||||||
|
|
||||||
|
mock_version.assert_called_once_with(self.version)
|
||||||
|
|
||||||
|
@mock.patch('ironic_python_agent.hardware.dispatch_to_all_managers')
|
||||||
|
def _get_current_clean_version(self, mock_dispatch):
|
||||||
|
mock_dispatch.return_value = {'SpecificHardwareManager':
|
||||||
|
{'name': 'specific', 'version': '1'},
|
||||||
|
'GenericHardwareManager':
|
||||||
|
{'name': 'generic', 'version': '1'}}
|
||||||
|
self.assertEqual(self.version, clean._get_current_clean_version())
|
||||||
|
|
||||||
|
@mock.patch('ironic_python_agent.hardware.dispatch_to_all_managers')
|
||||||
|
def test__check_clean_version_fail(self, mock_dispatch):
|
||||||
|
mock_dispatch.return_value = {'SpecificHardwareManager':
|
||||||
|
{'name': 'specific', 'version': '1'}}
|
||||||
|
|
||||||
|
self.assertRaises(errors.CleanVersionMismatch,
|
||||||
|
clean._check_clean_version,
|
||||||
|
{'not_specific': '1'})
|
@ -1,34 +0,0 @@
|
|||||||
# Copyright 2013 Rackspace, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
import mock
|
|
||||||
from oslotest import base as test_base
|
|
||||||
|
|
||||||
from ironic_python_agent.extensions import decom
|
|
||||||
|
|
||||||
|
|
||||||
class TestDecomExtension(test_base.BaseTestCase):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestDecomExtension, self).setUp()
|
|
||||||
self.agent_extension = decom.DecomExtension()
|
|
||||||
|
|
||||||
@mock.patch('ironic_python_agent.hardware.dispatch_to_managers',
|
|
||||||
autospec=True)
|
|
||||||
def test_erase_devices(self, mocked_dispatch):
|
|
||||||
result = self.agent_extension.erase_hardware()
|
|
||||||
result.join()
|
|
||||||
mocked_dispatch.assert_called_once_with('erase_devices')
|
|
||||||
self.assertTrue('result' in result.command_result.keys())
|
|
||||||
cmd_result_text = 'erase_hardware: finished'
|
|
||||||
self.assertEqual(cmd_result_text, result.command_result['result'])
|
|
@ -20,7 +20,7 @@ console_scripts =
|
|||||||
|
|
||||||
ironic_python_agent.extensions =
|
ironic_python_agent.extensions =
|
||||||
standby = ironic_python_agent.extensions.standby:StandbyExtension
|
standby = ironic_python_agent.extensions.standby:StandbyExtension
|
||||||
decom = ironic_python_agent.extensions.decom:DecomExtension
|
clean = ironic_python_agent.extensions.clean:CleanExtension
|
||||||
flow = ironic_python_agent.extensions.flow:FlowExtension
|
flow = ironic_python_agent.extensions.flow:FlowExtension
|
||||||
iscsi = ironic_python_agent.extensions.iscsi:ISCSIExtension
|
iscsi = ironic_python_agent.extensions.iscsi:ISCSIExtension
|
||||||
image = ironic_python_agent.extensions.image:ImageExtension
|
image = ironic_python_agent.extensions.image:ImageExtension
|
||||||
|
Loading…
x
Reference in New Issue
Block a user