summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/source/admin/drivers/fake.rst24
-rw-r--r--ironic/api/controllers/v1/node.py10
-rw-r--r--ironic/conductor/utils.py11
-rw-r--r--ironic/drivers/modules/pxe.py15
-rw-r--r--ironic/drivers/modules/pxe_base.py7
-rw-r--r--ironic/drivers/modules/snmp.py4
-rw-r--r--ironic/tests/unit/api/controllers/v1/test_node.py58
-rw-r--r--ironic/tests/unit/conductor/test_utils.py31
-rw-r--r--ironic/tests/unit/drivers/modules/test_pxe.py10
-rw-r--r--ironic/tests/unit/drivers/modules/test_snmp.py35
-rw-r--r--releasenotes/notes/bug-2010613-3ab1f32aaa776f28.yaml7
-rw-r--r--releasenotes/notes/fix-power-off-token-wipe-e7d605997f00d39d.yaml6
-rw-r--r--releasenotes/notes/fix_secure_boot_with_anaconda_deploy-84d7c1e3bbfa40f2.yaml4
-rw-r--r--releasenotes/notes/wipe-agent-token-upon-cleaning-timeout-c9add514fad1b02c.yaml7
-rw-r--r--tools/benchmark/generate-statistics.py2
15 files changed, 210 insertions, 21 deletions
diff --git a/doc/source/admin/drivers/fake.rst b/doc/source/admin/drivers/fake.rst
index ea7d7ef4c..2e2cc355e 100644
--- a/doc/source/admin/drivers/fake.rst
+++ b/doc/source/admin/drivers/fake.rst
@@ -23,6 +23,30 @@ Development
Developers can use ``fake-hardware`` hardware-type to mock out nodes for
testing without those nodes needing to exist with physical or virtual hardware.
+Scale testing
+-------------
+The ``fake`` drivers have a configurable delay in seconds which will result in
+those operations taking that long to complete. Two comma-delimited values will
+result in a delay with a triangular random distribution, weighted on the first
+value. These delays are applied to operations which typically block in other
+drivers. This allows more realistic scenarios to be arranged for performance and
+functional testing of an Ironic service without requiring real bare metal or
+faking at the BMC protocol level.
+
+.. code-block:: ini
+
+ [fake]
+ power_delay = 5
+ boot_delay = 10
+ deploy_delay = 60,360
+ vendor_delay = 1
+ management_delay = 5
+ inspect_delay = 360,480
+ raid_delay = 10
+ bios_delay = 5
+ storage_delay = 10
+ rescue_delay = 120
+
Adoption
--------
Some OpenStack deployers have used ``fake`` interfaces in Ironic to allow an
diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py
index d8d558a14..65adee544 100644
--- a/ironic/api/controllers/v1/node.py
+++ b/ironic/api/controllers/v1/node.py
@@ -1600,6 +1600,10 @@ def node_sanitize(node, fields, cdict=None,
node['driver_info'] = strutils.mask_dict_password(
node['driver_info'], "******")
+ _mask_fields(node['driver_info'],
+ ['snmp_auth_key', 'snmp_priv_key'],
+ "******")
+
if not show_instance_secrets and 'instance_info' in node_keys:
node['instance_info'] = strutils.mask_dict_password(
node['instance_info'], "******")
@@ -1663,6 +1667,12 @@ def _node_sanitize_extended(node, node_keys, target_dict, cdict):
'driver_internal_info permission. **'}
+def _mask_fields(dictionary, fields, secret):
+ for field in fields:
+ if dictionary.get(field):
+ dictionary[field] = secret
+
+
def node_list_convert_with_links(nodes, limit, url, fields=None, **kwargs):
cdict = api.request.context.to_policy_values()
target_dict = dict(cdict)
diff --git a/ironic/conductor/utils.py b/ironic/conductor/utils.py
index cdf3a99ee..2272c0df7 100644
--- a/ironic/conductor/utils.py
+++ b/ironic/conductor/utils.py
@@ -297,6 +297,13 @@ def node_power_action(task, new_state, timeout=None):
node = task.node
if _can_skip_state_change(task, new_state):
+ # NOTE(TheJulia): Even if we are not changing the power state,
+ # we need to wipe the token out, just in case for some reason
+ # the power was turned off outside of our interaction/management.
+ if new_state in (states.POWER_OFF, states.SOFT_POWER_OFF,
+ states.REBOOT, states.SOFT_REBOOT):
+ wipe_internal_info_on_power_off(node)
+ node.save()
return
target_state = _calculate_target_state(new_state)
@@ -481,9 +488,9 @@ def cleaning_error_handler(task, logmsg, errmsg=None, traceback=False,
node.del_driver_internal_info('cleaning_reboot')
node.del_driver_internal_info('cleaning_polling')
node.del_driver_internal_info('skip_current_clean_step')
- # We don't need to keep the old agent URL
+ # We don't need to keep the old agent URL, or token
# as it should change upon the next cleaning attempt.
- node.del_driver_internal_info('agent_url')
+ wipe_token_and_url(task)
# For manual cleaning, the target provision state is MANAGEABLE, whereas
# for automated cleaning, it is AVAILABLE.
manual_clean = node.target_provision_state == states.MANAGEABLE
diff --git a/ironic/drivers/modules/pxe.py b/ironic/drivers/modules/pxe.py
index fe93acefd..a55f5b9fd 100644
--- a/ironic/drivers/modules/pxe.py
+++ b/ironic/drivers/modules/pxe.py
@@ -27,6 +27,7 @@ from ironic.conductor import utils as manager_utils
from ironic.conf import CONF
from ironic.drivers import base
from ironic.drivers.modules import agent_base
+from ironic.drivers.modules import boot_mode_utils
from ironic.drivers.modules import deploy_utils
from ironic.drivers.modules import pxe_base
LOG = logging.getLogger(__name__)
@@ -114,21 +115,11 @@ class PXEAnacondaDeploy(agent_base.AgentBaseMixin, agent_base.HeartbeatMixin,
def reboot_to_instance(self, task):
node = task.node
try:
- # anaconda deploy will install the bootloader and the node is ready
- # to boot from disk.
-
- deploy_utils.try_set_boot_device(task, boot_devices.DISK)
- except Exception as e:
- msg = (_("Failed to change the boot device to %(boot_dev)s "
- "when deploying node %(node)s. Error: %(error)s") %
- {'boot_dev': boot_devices.DISK, 'node': node.uuid,
- 'error': e})
- agent_base.log_and_raise_deployment_error(task, msg)
-
- try:
task.process_event('resume')
self.clean_up(task)
manager_utils.node_power_action(task, states.POWER_OFF)
+ deploy_utils.try_set_boot_device(task, boot_devices.DISK)
+ boot_mode_utils.configure_secure_boot_if_needed(task)
task.driver.network.remove_provisioning_network(task)
task.driver.network.configure_tenant_networks(task)
manager_utils.node_power_action(task, states.POWER_ON)
diff --git a/ironic/drivers/modules/pxe_base.py b/ironic/drivers/modules/pxe_base.py
index daa90ba8d..f3ac49890 100644
--- a/ironic/drivers/modules/pxe_base.py
+++ b/ironic/drivers/modules/pxe_base.py
@@ -231,11 +231,12 @@ class PXEBaseMixin(object):
:returns: None
"""
boot_mode_utils.sync_boot_mode(task)
- boot_mode_utils.configure_secure_boot_if_needed(task)
-
node = task.node
- boot_option = deploy_utils.get_boot_option(node)
boot_device = None
+ boot_option = deploy_utils.get_boot_option(node)
+ if boot_option != "kickstart":
+ boot_mode_utils.configure_secure_boot_if_needed(task)
+
instance_image_info = {}
if boot_option == "ramdisk" or boot_option == "kickstart":
instance_image_info = pxe_utils.get_instance_image_info(
diff --git a/ironic/drivers/modules/snmp.py b/ironic/drivers/modules/snmp.py
index d544d5687..435d24b78 100644
--- a/ironic/drivers/modules/snmp.py
+++ b/ironic/drivers/modules/snmp.py
@@ -392,9 +392,9 @@ def _get_client(snmp_info):
snmp_info.get("read_community"),
snmp_info.get("write_community"),
snmp_info.get("user"),
- snmp_info.get("auth_proto"),
+ snmp_info.get("auth_protocol"),
snmp_info.get("auth_key"),
- snmp_info.get("priv_proto"),
+ snmp_info.get("priv_protocol"),
snmp_info.get("priv_key"),
snmp_info.get("context_engine_id"),
snmp_info.get("context_name"))
diff --git a/ironic/tests/unit/api/controllers/v1/test_node.py b/ironic/tests/unit/api/controllers/v1/test_node.py
index f050fd036..87a029057 100644
--- a/ironic/tests/unit/api/controllers/v1/test_node.py
+++ b/ironic/tests/unit/api/controllers/v1/test_node.py
@@ -843,6 +843,64 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertIn('retired_reason', data['nodes'][0])
self.assertIn('network_data', data['nodes'][0])
+ def test_detail_snmpv3(self):
+ driver_info = {
+ 'snmp_version': 3,
+ 'snmp_user': 'test-user',
+ 'snmp_auth_protocol': 'sha',
+ 'snmp_auth_key': 'test-auth-key',
+ 'snmp_priv_protocol': 'aes',
+ 'snmp_priv_key': 'test-priv-key'
+ }
+ sanitized_driver_info = driver_info.copy()
+ sanitized_driver_info['snmp_auth_key'] = '******'
+ sanitized_driver_info['snmp_priv_key'] = '******'
+
+ node = obj_utils.create_test_node(self.context,
+ chassis_id=self.chassis.id,
+ driver_info=driver_info)
+ data = self.get_json(
+ '/nodes/detail',
+ headers={api_base.Version.string: str(api_v1.max_version())})
+ self.assertEqual(node.uuid, data['nodes'][0]["uuid"])
+ self.assertIn('name', data['nodes'][0])
+ self.assertIn('driver', data['nodes'][0])
+ self.assertIn('driver_info', data['nodes'][0])
+ self.assertEqual(sanitized_driver_info,
+ data['nodes'][0]['driver_info'])
+ self.assertIn('extra', data['nodes'][0])
+ self.assertIn('properties', data['nodes'][0])
+ self.assertIn('chassis_uuid', data['nodes'][0])
+ self.assertIn('reservation', data['nodes'][0])
+ self.assertIn('maintenance', data['nodes'][0])
+ self.assertIn('console_enabled', data['nodes'][0])
+ self.assertIn('target_power_state', data['nodes'][0])
+ self.assertIn('target_provision_state', data['nodes'][0])
+ self.assertIn('provision_updated_at', data['nodes'][0])
+ self.assertIn('inspection_finished_at', data['nodes'][0])
+ self.assertIn('inspection_started_at', data['nodes'][0])
+ self.assertIn('raid_config', data['nodes'][0])
+ self.assertIn('target_raid_config', data['nodes'][0])
+ self.assertIn('network_interface', data['nodes'][0])
+ self.assertIn('resource_class', data['nodes'][0])
+ for field in api_utils.V31_FIELDS:
+ self.assertIn(field, data['nodes'][0])
+ self.assertIn('storage_interface', data['nodes'][0])
+ self.assertIn('traits', data['nodes'][0])
+ self.assertIn('conductor_group', data['nodes'][0])
+ self.assertIn('automated_clean', data['nodes'][0])
+ self.assertIn('protected', data['nodes'][0])
+ self.assertIn('protected_reason', data['nodes'][0])
+ self.assertIn('owner', data['nodes'][0])
+ self.assertIn('lessee', data['nodes'][0])
+ # never expose the chassis_id
+ self.assertNotIn('chassis_id', data['nodes'][0])
+ self.assertNotIn('allocation_id', data['nodes'][0])
+ self.assertIn('allocation_uuid', data['nodes'][0])
+ self.assertIn('retired', data['nodes'][0])
+ self.assertIn('retired_reason', data['nodes'][0])
+ self.assertIn('network_data', data['nodes'][0])
+
def test_detail_instance_uuid(self):
instance_uuid = '6eccd391-961c-4da5-b3c5-e2fa5cfbbd9d'
node = obj_utils.create_test_node(
diff --git a/ironic/tests/unit/conductor/test_utils.py b/ironic/tests/unit/conductor/test_utils.py
index a29da21a7..52fc72436 100644
--- a/ironic/tests/unit/conductor/test_utils.py
+++ b/ironic/tests/unit/conductor/test_utils.py
@@ -304,6 +304,31 @@ class NodePowerActionTestCase(db_base.DbTestCase):
node['driver_internal_info'])
@mock.patch.object(fake.FakePower, 'get_power_state', autospec=True)
+ def test_node_power_action_power_off_already(self, get_power_mock):
+ """Test node_power_action to turn node power off, but already off."""
+ dii = {'agent_secret_token': 'token',
+ 'agent_cached_deploy_steps': ['steps']}
+ node = obj_utils.create_test_node(self.context,
+ uuid=uuidutils.generate_uuid(),
+ driver='fake-hardware',
+ power_state=states.POWER_ON,
+ driver_internal_info=dii)
+ task = task_manager.TaskManager(self.context, node.uuid)
+
+ get_power_mock.return_value = states.POWER_OFF
+
+ conductor_utils.node_power_action(task, states.POWER_OFF)
+
+ node.refresh()
+ get_power_mock.assert_called_once_with(mock.ANY, mock.ANY)
+ self.assertEqual(states.POWER_OFF, node['power_state'])
+ self.assertIsNone(node['target_power_state'])
+ self.assertIsNone(node['last_error'])
+ self.assertNotIn('agent_secret_token', node['driver_internal_info'])
+ self.assertNotIn('agent_cached_deploy_steps',
+ node['driver_internal_info'])
+
+ @mock.patch.object(fake.FakePower, 'get_power_state', autospec=True)
def test_node_power_action_power_off_pregenerated_token(self,
get_power_mock):
dii = {'agent_secret_token': 'token',
@@ -1172,6 +1197,9 @@ class ErrorHandlersTestCase(db_base.DbTestCase):
self.node.set_driver_internal_info('skip_current_clean_step', True)
self.node.set_driver_internal_info('clean_step_index', 0)
self.node.set_driver_internal_info('agent_url', 'url')
+ self.node.set_driver_internal_info('agent_secret_token', 'foo')
+ self.node.set_driver_internal_info('agent_secret_token_pregenerated',
+ False)
msg = 'error bar'
last_error = "last error"
@@ -1184,6 +1212,9 @@ class ErrorHandlersTestCase(db_base.DbTestCase):
self.assertNotIn('cleaning_polling', self.node.driver_internal_info)
self.assertNotIn('skip_current_clean_step',
self.node.driver_internal_info)
+ self.assertNotIn('agent_secret_token', self.node.driver_internal_info)
+ self.assertNotIn('agent_secret_token_pregenerated',
+ self.node.driver_internal_info)
self.assertEqual(last_error, self.node.last_error)
self.assertTrue(self.node.maintenance)
self.assertEqual(last_error, self.node.maintenance_reason)
diff --git a/ironic/tests/unit/drivers/modules/test_pxe.py b/ironic/tests/unit/drivers/modules/test_pxe.py
index e7d444104..f16366470 100644
--- a/ironic/tests/unit/drivers/modules/test_pxe.py
+++ b/ironic/tests/unit/drivers/modules/test_pxe.py
@@ -550,6 +550,8 @@ class PXEBootTestCase(db_base.DbTestCase):
def test_prepare_instance_ramdisk_pxe_conf_exists(self):
self._test_prepare_instance_ramdisk(config_file_exits=False)
+ @mock.patch.object(boot_mode_utils, 'configure_secure_boot_if_needed',
+ autospec=True)
@mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True)
@mock.patch.object(deploy_utils, 'switch_pxe_config', autospec=True)
@mock.patch.object(pxe_utils, 'create_pxe_config', autospec=True)
@@ -567,7 +569,7 @@ class PXEBootTestCase(db_base.DbTestCase):
self, exec_mock, write_file_mock, render_mock, api_url_mock,
boot_opt_mock, get_image_info_mock, cache_mock, dhcp_factory_mock,
create_pxe_config_mock, switch_pxe_config_mock,
- set_boot_device_mock):
+ set_boot_device_mock, mock_conf_sec_boot):
image_info = {'kernel': ['ins_kernel_id', '/path/to/kernel'],
'ramdisk': ['ins_ramdisk_id', '/path/to/ramdisk'],
'stage2': ['ins_stage2_id', '/path/to/stage2'],
@@ -611,6 +613,7 @@ class PXEBootTestCase(db_base.DbTestCase):
set_boot_device_mock.assert_called_once_with(task,
boot_devices.PXE,
persistent=True)
+ self.assertFalse(mock_conf_sec_boot.called)
@mock.patch.object(manager_utils, 'node_set_boot_device', autospec=True)
@mock.patch.object(deploy_utils, 'switch_pxe_config', autospec=True)
@@ -786,11 +789,13 @@ class PXEAnacondaDeployTestCase(db_base.DbTestCase):
task.driver.deploy.prepare(task)
mock_prepare_instance.assert_called_once_with(mock.ANY, task)
+ @mock.patch.object(boot_mode_utils, 'configure_secure_boot_if_needed',
+ autospec=True)
@mock.patch.object(pxe_utils, 'clean_up_pxe_env', autospec=True)
@mock.patch.object(pxe_utils, 'get_instance_image_info', autospec=True)
@mock.patch.object(deploy_utils, 'try_set_boot_device', autospec=True)
def test_reboot_to_instance(self, mock_set_boot_dev, mock_image_info,
- mock_cleanup_pxe_env):
+ mock_cleanup_pxe_env, mock_conf_sec_boot):
image_info = {'kernel': ('', '/path/to/kernel'),
'ramdisk': ('', '/path/to/ramdisk'),
'stage2': ('', '/path/to/stage2'),
@@ -802,6 +807,7 @@ class PXEAnacondaDeployTestCase(db_base.DbTestCase):
with task_manager.acquire(self.context, self.node.uuid) as task:
task.driver.deploy.reboot_to_instance(task)
mock_set_boot_dev.assert_called_once_with(task, boot_devices.DISK)
+ mock_conf_sec_boot.assert_called_once_with(task)
mock_cleanup_pxe_env.assert_called_once_with(task, image_info,
ipxe_enabled=False)
diff --git a/ironic/tests/unit/drivers/modules/test_snmp.py b/ironic/tests/unit/drivers/modules/test_snmp.py
index 5391d7ac5..e1b6fc1df 100644
--- a/ironic/tests/unit/drivers/modules/test_snmp.py
+++ b/ironic/tests/unit/drivers/modules/test_snmp.py
@@ -46,6 +46,41 @@ class SNMPClientTestCase(base.TestCase):
self.value = 'value'
@mock.patch.object(pysnmp, 'SnmpEngine', autospec=True)
+ def test__get_client(self, mock_snmpengine):
+ driver_info = db_utils.get_test_snmp_info(
+ snmp_address=self.address,
+ snmp_port=self.port,
+ snmp_user='test-user',
+ snmp_auth_protocol='sha',
+ snmp_auth_key='test-auth-key',
+ snmp_priv_protocol='aes',
+ snmp_priv_key='test-priv-key',
+ snmp_context_engine_id='test-engine-id',
+ snmp_context_name='test-context-name',
+ snmp_version='3')
+ node = obj_utils.get_test_node(
+ self.context,
+ driver_info=driver_info)
+ info = snmp._parse_driver_info(node)
+
+ client = snmp._get_client(info)
+
+ mock_snmpengine.assert_called_once_with()
+ self.assertEqual(self.address, client.address)
+ self.assertEqual(int(self.port), client.port)
+ self.assertEqual(snmp.SNMP_V3, client.version)
+ self.assertNotIn('read_community', client.__dict__)
+ self.assertNotIn('write_community', client.__dict__)
+ self.assertEqual('test-user', client.user)
+ self.assertEqual(pysnmp.usmHMACSHAAuthProtocol, client.auth_proto)
+ self.assertEqual('test-auth-key', client.auth_key)
+ self.assertEqual(pysnmp.usmAesCfb128Protocol, client.priv_proto)
+ self.assertEqual('test-priv-key', client.priv_key)
+ self.assertEqual('test-engine-id', client.context_engine_id)
+ self.assertEqual('test-context-name', client.context_name)
+ self.assertEqual(mock_snmpengine.return_value, client.snmp_engine)
+
+ @mock.patch.object(pysnmp, 'SnmpEngine', autospec=True)
def test___init__(self, mock_snmpengine):
client = snmp.SNMPClient(self.address, self.port, snmp.SNMP_V1)
mock_snmpengine.assert_called_once_with()
diff --git a/releasenotes/notes/bug-2010613-3ab1f32aaa776f28.yaml b/releasenotes/notes/bug-2010613-3ab1f32aaa776f28.yaml
new file mode 100644
index 000000000..89769bfb8
--- /dev/null
+++ b/releasenotes/notes/bug-2010613-3ab1f32aaa776f28.yaml
@@ -0,0 +1,7 @@
+---
+fixes:
+ - |
+ [`bug 2010613 <https://storyboard.openstack.org/#!/story/2010613>`_]
+ Fixes issue with SNMP v3 auth protocol and priv protocol set in
+ driver info not being retrieved correctly when a SNMP client is
+ initialized.
diff --git a/releasenotes/notes/fix-power-off-token-wipe-e7d605997f00d39d.yaml b/releasenotes/notes/fix-power-off-token-wipe-e7d605997f00d39d.yaml
new file mode 100644
index 000000000..14a489b46
--- /dev/null
+++ b/releasenotes/notes/fix-power-off-token-wipe-e7d605997f00d39d.yaml
@@ -0,0 +1,6 @@
+---
+fixes:
+ - |
+ Fixes an issue where an agent token could be inadvertently orphaned
+ if a node is already in the target power state when we attempt to turn
+ the node off.
diff --git a/releasenotes/notes/fix_secure_boot_with_anaconda_deploy-84d7c1e3bbfa40f2.yaml b/releasenotes/notes/fix_secure_boot_with_anaconda_deploy-84d7c1e3bbfa40f2.yaml
new file mode 100644
index 000000000..a03289c42
--- /dev/null
+++ b/releasenotes/notes/fix_secure_boot_with_anaconda_deploy-84d7c1e3bbfa40f2.yaml
@@ -0,0 +1,4 @@
+---
+fixes:
+ - |
+ Fixes secure boot with anaconda deploy.
diff --git a/releasenotes/notes/wipe-agent-token-upon-cleaning-timeout-c9add514fad1b02c.yaml b/releasenotes/notes/wipe-agent-token-upon-cleaning-timeout-c9add514fad1b02c.yaml
new file mode 100644
index 000000000..0aa828ccd
--- /dev/null
+++ b/releasenotes/notes/wipe-agent-token-upon-cleaning-timeout-c9add514fad1b02c.yaml
@@ -0,0 +1,7 @@
+---
+fixes:
+ - |
+ Fixes an issue where an agent token was being orphaned if a baremetal node
+ timed out during cleaning operations, leading to issues where the node
+ would not be able to establish a new token with Ironic upon future
+ in some cases. We now always wipe the token in this case.
diff --git a/tools/benchmark/generate-statistics.py b/tools/benchmark/generate-statistics.py
index e8327f3ac..e9fe0f56d 100644
--- a/tools/benchmark/generate-statistics.py
+++ b/tools/benchmark/generate-statistics.py
@@ -235,6 +235,7 @@ def _assess_db_object_and_api_performance_ports(mock_log, mock_request):
node_ident=None,
address=None,
portgroup_ident=None,
+ shard=None,
marker=None,
limit=None,
sort_key="id",
@@ -249,6 +250,7 @@ def _assess_db_object_and_api_performance_ports(mock_log, mock_request):
node_ident=None,
address=None,
portgroup_ident=None,
+ shard=None,
marker=res['ports'][-1]['uuid'],
limit=None,
sort_key="id",