diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py index 401bd577c6..173d7b8f5a 100644 --- a/ironic/api/controllers/v1/node.py +++ b/ironic/api/controllers/v1/node.py @@ -1423,6 +1423,9 @@ def node_sanitize(node, fields): if not show_instance_secrets and node.get('instance_info'): node['instance_info'] = strutils.mask_dict_password( node['instance_info'], "******") + # NOTE(dtantsur): configdrive may be a dict + if node['instance_info'].get('configdrive'): + node['instance_info']['configdrive'] = "******" # NOTE(tenbrae): agent driver may store a swift temp_url on the # instance_info, which shouldn't be exposed to non-admin users. # Now that ironic supports additional policies, we need to hide diff --git a/ironic/conductor/deployments.py b/ironic/conductor/deployments.py index e146a26d60..11670c0b2c 100644 --- a/ironic/conductor/deployments.py +++ b/ironic/conductor/deployments.py @@ -130,8 +130,6 @@ def do_node_deploy(task, conductor_id=None, configdrive=None, utils.wipe_deploy_internal_info(task) try: if configdrive: - if isinstance(configdrive, dict): - configdrive = utils.build_configdrive(node, configdrive) _store_configdrive(node, configdrive) except (exception.SwiftOperationError, exception.ConfigInvalid) as e: with excutils.save_and_reraise_exception(): @@ -417,6 +415,10 @@ def _store_configdrive(node, configdrive): """ if CONF.deploy.configdrive_use_object_store: + # Don't store the JSON source in swift. + if isinstance(configdrive, dict): + configdrive = utils.build_configdrive(node, configdrive) + # NOTE(lucasagomes): No reason to use a different timeout than # the one used for deploying the node timeout = (CONF.conductor.configdrive_swift_temp_url_duration diff --git a/ironic/conductor/utils.py b/ironic/conductor/utils.py index 3b8b54c221..2a3e6b4f83 100644 --- a/ironic/conductor/utils.py +++ b/ironic/conductor/utils.py @@ -1005,6 +1005,21 @@ def build_configdrive(node, configdrive): vendor_data=configdrive.get('vendor_data')) +def get_configdrive_image(node): + """Get configdrive as an ISO image or a URL. + + Converts the JSON representation into an image. URLs and raw contents + are returned unchanged. + + :param node: an Ironic node object. + :returns: A gzipped and base64 encoded configdrive as a string. + """ + configdrive = node.instance_info.get('configdrive') + if isinstance(configdrive, dict): + configdrive = build_configdrive(node, configdrive) + return configdrive + + def fast_track_able(task): """Checks if the operation can be a streamlined deployment sequence. diff --git a/ironic/drivers/modules/agent.py b/ironic/drivers/modules/agent.py index 3296fb9347..3007689692 100644 --- a/ironic/drivers/modules/agent.py +++ b/ironic/drivers/modules/agent.py @@ -561,7 +561,11 @@ class AgentDeploy(CustomAgentDeploy): if disk_label is not None: image_info['disk_label'] = disk_label - configdrive = node.instance_info.get('configdrive') + configdrive = manager_utils.get_configdrive_image(node) + if configdrive: + # FIXME(dtantsur): remove this duplication once IPA is ready: + # https://review.opendev.org/c/openstack/ironic-python-agent/+/790471 + image_info['configdrive'] = configdrive # Now switch into the corresponding in-band deploy step and let the # result be polled normally. new_step = {'interface': 'deploy', diff --git a/ironic/drivers/modules/ansible/deploy.py b/ironic/drivers/modules/ansible/deploy.py index 2c17bccdb3..218d046b2e 100644 --- a/ironic/drivers/modules/ansible/deploy.py +++ b/ironic/drivers/modules/ansible/deploy.py @@ -284,7 +284,7 @@ def _prepare_variables(task): image['checksum'] = 'md5:%s' % checksum _add_ssl_image_options(image) variables = {'image': image} - configdrive = i_info.get('configdrive') + configdrive = manager_utils.get_configdrive_image(task.node) if configdrive: if urlparse.urlparse(configdrive).scheme in ('http', 'https'): cfgdrv_type = 'url' diff --git a/ironic/drivers/modules/redfish/boot.py b/ironic/drivers/modules/redfish/boot.py index 0f48b4d1eb..ab825d338e 100644 --- a/ironic/drivers/modules/redfish/boot.py +++ b/ironic/drivers/modules/redfish/boot.py @@ -630,21 +630,12 @@ class RedfishVirtualMediaBoot(base.BootInterface): managers = redfish_utils.get_system(task.node).managers deploy_info = _parse_deploy_info(node) - configdrive = node.instance_info.get('configdrive') iso_ref = image_utils.prepare_boot_iso(task, deploy_info, **params) _eject_vmedia(task, managers, sushy.VIRTUAL_MEDIA_CD) _insert_vmedia(task, managers, iso_ref, sushy.VIRTUAL_MEDIA_CD) - if configdrive and boot_option == 'ramdisk': - _eject_vmedia(task, managers, sushy.VIRTUAL_MEDIA_USBSTICK) - cd_ref = image_utils.prepare_configdrive_image(task, configdrive) - try: - _insert_vmedia(task, managers, cd_ref, - sushy.VIRTUAL_MEDIA_USBSTICK) - except exception.InvalidParameterValue: - raise exception.InstanceDeployFailure( - _('Cannot attach configdrive for node %s: no suitable ' - 'virtual USB slot has been found') % node.uuid) + if boot_option == 'ramdisk': + self._attach_configdrive(task, managers) del managers @@ -654,6 +645,21 @@ class RedfishVirtualMediaBoot(base.BootInterface): "%(device)s", {'node': task.node.uuid, 'device': boot_devices.CDROM}) + def _attach_configdrive(self, task, managers): + configdrive = manager_utils.get_configdrive_image(task.node) + if not configdrive: + return + + _eject_vmedia(task, managers, sushy.VIRTUAL_MEDIA_USBSTICK) + cd_ref = image_utils.prepare_configdrive_image(task, configdrive) + try: + _insert_vmedia(task, managers, cd_ref, + sushy.VIRTUAL_MEDIA_USBSTICK) + except exception.InvalidParameterValue: + raise exception.InstanceDeployFailure( + _('Cannot attach configdrive for node %s: no suitable ' + 'virtual USB slot has been found') % task.node.uuid) + def _eject_all(self, task): managers = redfish_utils.get_system(task.node).managers @@ -670,7 +676,7 @@ class RedfishVirtualMediaBoot(base.BootInterface): boot_option = deploy_utils.get_boot_option(task.node) if (boot_option == 'ramdisk' - and task.node.instance_info.get('configdrive')): + and task.node.instance_info.get('configdrive') is not None): _eject_vmedia(task, managers, sushy.VIRTUAL_MEDIA_USBSTICK) image_utils.cleanup_disk_image(task, prefix='configdrive') diff --git a/ironic/objects/node.py b/ironic/objects/node.py index c8f79f2868..3eb997c513 100644 --- a/ironic/objects/node.py +++ b/ironic/objects/node.py @@ -174,11 +174,12 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat): d['driver_info'] = strutils.mask_dict_password( d.get('driver_info', {}), "******") iinfo = d.pop('instance_info', {}) - if not mask_configdrive: - configdrive = iinfo.pop('configdrive', None) + configdrive = iinfo.pop('configdrive', None) d['instance_info'] = strutils.mask_dict_password(iinfo, "******") - if not mask_configdrive and configdrive: - d['instance_info']['configdrive'] = configdrive + if configdrive is not None: + d['instance_info']['configdrive'] = ( + "******" if mask_configdrive else configdrive + ) d['driver_internal_info'] = strutils.mask_dict_password( d.get('driver_internal_info', {}), "******") return d diff --git a/ironic/tests/unit/api/controllers/v1/test_node.py b/ironic/tests/unit/api/controllers/v1/test_node.py index cc778cd770..baa21d5a97 100644 --- a/ironic/tests/unit/api/controllers/v1/test_node.py +++ b/ironic/tests/unit/api/controllers/v1/test_node.py @@ -186,6 +186,25 @@ class TestListNodes(test_api_base.BaseApiTest): self.assertNotIn('allocation_id', data) self.assertIn('allocation_uuid', data) + def test_get_one_configdrive_dict(self): + fake_instance_info = { + "configdrive": {'user_data': 'data'}, + "image_url": "http://example.com/test_image_url", + "foo": "bar", + } + node = obj_utils.create_test_node(self.context, + chassis_id=self.chassis.id, + instance_info=fake_instance_info) + data = self.get_json( + '/nodes/%s' % node.uuid, + headers={api_base.Version.string: str(api_v1.max_version())}) + self.assertEqual(node.uuid, data['uuid']) + self.assertEqual('******', data['driver_info']['fake_password']) + self.assertEqual('bar', data['driver_info']['foo']) + self.assertEqual('******', data['instance_info']['configdrive']) + self.assertEqual('******', data['instance_info']['image_url']) + self.assertEqual('bar', data['instance_info']['foo']) + def test_get_one_with_json(self): # Test backward compatibility with guess_content_type_from_ext node = obj_utils.create_test_node(self.context, diff --git a/ironic/tests/unit/conductor/test_deployments.py b/ironic/tests/unit/conductor/test_deployments.py index f0e8944611..0207359846 100644 --- a/ironic/tests/unit/conductor/test_deployments.py +++ b/ironic/tests/unit/conductor/test_deployments.py @@ -174,63 +174,6 @@ class DoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase): def test__do_node_deploy_fast_track(self): self._test__do_node_deploy_ok(fast_track=True) - @mock.patch('openstack.baremetal.configdrive.build', autospec=True) - def test__do_node_deploy_configdrive_as_dict(self, mock_cd): - mock_cd.return_value = 'foo' - configdrive = {'user_data': 'abcd'} - self._test__do_node_deploy_ok(configdrive=configdrive, - expected_configdrive='foo') - mock_cd.assert_called_once_with({'uuid': self.node.uuid}, - network_data=None, - user_data=b'abcd', - vendor_data=None) - - @mock.patch('openstack.baremetal.configdrive.build', autospec=True) - def test__do_node_deploy_configdrive_as_dict_with_meta_data(self, mock_cd): - mock_cd.return_value = 'foo' - configdrive = {'meta_data': {'uuid': uuidutils.generate_uuid(), - 'name': 'new-name', - 'hostname': 'example.com'}} - self._test__do_node_deploy_ok(configdrive=configdrive, - expected_configdrive='foo') - mock_cd.assert_called_once_with(configdrive['meta_data'], - network_data=None, - user_data=None, - vendor_data=None) - - @mock.patch('openstack.baremetal.configdrive.build', autospec=True) - def test__do_node_deploy_configdrive_with_network_data(self, mock_cd): - mock_cd.return_value = 'foo' - configdrive = {'network_data': {'links': []}} - self._test__do_node_deploy_ok(configdrive=configdrive, - expected_configdrive='foo') - mock_cd.assert_called_once_with({'uuid': self.node.uuid}, - network_data={'links': []}, - user_data=None, - vendor_data=None) - - @mock.patch('openstack.baremetal.configdrive.build', autospec=True) - def test__do_node_deploy_configdrive_and_user_data_as_dict(self, mock_cd): - mock_cd.return_value = 'foo' - configdrive = {'user_data': {'user': 'data'}} - self._test__do_node_deploy_ok(configdrive=configdrive, - expected_configdrive='foo') - mock_cd.assert_called_once_with({'uuid': self.node.uuid}, - network_data=None, - user_data=b'{"user": "data"}', - vendor_data=None) - - @mock.patch('openstack.baremetal.configdrive.build', autospec=True) - def test__do_node_deploy_configdrive_with_vendor_data(self, mock_cd): - mock_cd.return_value = 'foo' - configdrive = {'vendor_data': {'foo': 'bar'}} - self._test__do_node_deploy_ok(configdrive=configdrive, - expected_configdrive='foo') - mock_cd.assert_called_once_with({'uuid': self.node.uuid}, - network_data=None, - user_data=None, - vendor_data={'foo': 'bar'}) - @mock.patch.object(swift, 'SwiftAPI', autospec=True) @mock.patch('ironic.drivers.modules.fake.FakeDeploy.prepare', autospec=True) @@ -1014,6 +957,37 @@ class StoreConfigDriveTestCase(db_base.DbTestCase): self.node.refresh() self.assertEqual(expected_instance_info, self.node.instance_info) + @mock.patch.object(conductor_utils, 'build_configdrive', autospec=True) + def test_store_configdrive_swift_build(self, mock_cd, mock_swift): + container_name = 'foo_container' + timeout = 123 + expected_obj_name = 'configdrive-%s' % self.node.uuid + expected_obj_header = {'X-Delete-After': str(timeout)} + expected_instance_info = {'configdrive': 'http://1.2.3.4'} + + mock_cd.return_value = 'fake' + + # set configs and mocks + CONF.set_override('configdrive_use_object_store', True, + group='deploy') + CONF.set_override('configdrive_swift_container', container_name, + group='conductor') + CONF.set_override('deploy_callback_timeout', timeout, + group='conductor') + mock_swift.return_value.get_temp_url.return_value = 'http://1.2.3.4' + + deployments._store_configdrive(self.node, {'meta_data': {}}) + + mock_swift.assert_called_once_with() + mock_swift.return_value.create_object.assert_called_once_with( + container_name, expected_obj_name, mock.ANY, + object_headers=expected_obj_header) + mock_swift.return_value.get_temp_url.assert_called_once_with( + container_name, expected_obj_name, timeout) + self.node.refresh() + self.assertEqual(expected_instance_info, self.node.instance_info) + mock_cd.assert_called_once_with(self.node, {'meta_data': {}}) + def test_store_configdrive_swift_no_deploy_timeout(self, mock_swift): container_name = 'foo_container' expected_obj_name = 'configdrive-%s' % self.node.uuid diff --git a/ironic/tests/unit/conductor/test_utils.py b/ironic/tests/unit/conductor/test_utils.py index e8f20e94af..c94aff01c1 100644 --- a/ironic/tests/unit/conductor/test_utils.py +++ b/ironic/tests/unit/conductor/test_utils.py @@ -2308,3 +2308,71 @@ class CacheVendorTestCase(db_base.DbTestCase): self.node.refresh() self.assertNotIn('vendor', self.node.properties) self.assertTrue(mock_log.called) + + +class GetConfigDriveImageTestCase(db_base.DbTestCase): + + def setUp(self): + super(GetConfigDriveImageTestCase, self).setUp() + self.node = obj_utils.create_test_node( + self.context, + uuid=uuidutils.generate_uuid(), + instance_info={}) + + def test_no_configdrive(self): + self.assertIsNone(conductor_utils.get_configdrive_image(self.node)) + + def test_string(self): + self.node.instance_info['configdrive'] = 'data' + self.assertEqual('data', + conductor_utils.get_configdrive_image(self.node)) + + @mock.patch('openstack.baremetal.configdrive.build', autospec=True) + def test_build_empty(self, mock_cd): + self.node.instance_info['configdrive'] = {} + self.assertEqual(mock_cd.return_value, + conductor_utils.get_configdrive_image(self.node)) + mock_cd.assert_called_once_with({'uuid': self.node.uuid}, + network_data=None, + user_data=None, + vendor_data=None) + + @mock.patch('openstack.baremetal.configdrive.build', autospec=True) + def test_build_populated(self, mock_cd): + configdrive = { + 'meta_data': {'uuid': uuidutils.generate_uuid(), + 'name': 'new-name', + 'hostname': 'example.com'}, + 'network_data': {'links': []}, + 'vendor_data': {'foo': 'bar'}, + } + self.node.instance_info['configdrive'] = configdrive + self.assertEqual(mock_cd.return_value, + conductor_utils.get_configdrive_image(self.node)) + mock_cd.assert_called_once_with( + configdrive['meta_data'], + network_data=configdrive['network_data'], + user_data=None, + vendor_data=configdrive['vendor_data']) + + @mock.patch('openstack.baremetal.configdrive.build', autospec=True) + def test_build_user_data_as_string(self, mock_cd): + self.node.instance_info['configdrive'] = {'user_data': 'abcd'} + self.assertEqual(mock_cd.return_value, + conductor_utils.get_configdrive_image(self.node)) + mock_cd.assert_called_once_with({'uuid': self.node.uuid}, + network_data=None, + user_data=b'abcd', + vendor_data=None) + + @mock.patch('openstack.baremetal.configdrive.build', autospec=True) + def test_build_user_data_as_dict(self, mock_cd): + self.node.instance_info['configdrive'] = { + 'user_data': {'user': 'data'} + } + self.assertEqual(mock_cd.return_value, + conductor_utils.get_configdrive_image(self.node)) + mock_cd.assert_called_once_with({'uuid': self.node.uuid}, + network_data=None, + user_data=b'{"user": "data"}', + vendor_data=None) diff --git a/ironic/tests/unit/drivers/modules/ansible/test_deploy.py b/ironic/tests/unit/drivers/modules/ansible/test_deploy.py index 17ab45786d..886c01db5d 100644 --- a/ironic/tests/unit/drivers/modules/ansible/test_deploy.py +++ b/ironic/tests/unit/drivers/modules/ansible/test_deploy.py @@ -495,6 +495,36 @@ class TestAnsibleMethods(AnsibleDeployTestCaseBase): mock.call().write('fake-content'), mock.call().__exit__(None, None, None))) + @mock.patch.object(utils, 'build_configdrive', autospec=True) + def test__prepare_variables_configdrive_json(self, mock_build_configdrive): + i_info = self.node.instance_info + i_info['configdrive'] = {'meta_data': {}} + self.node.instance_info = i_info + self.node.save() + mock_build_configdrive.return_value = 'fake-content' + configdrive_path = ('%(tempdir)s/%(node)s.cndrive' % + {'tempdir': ansible_deploy.CONF.tempdir, + 'node': self.node.uuid}) + expected = {"image": {"url": "http://image", + "validate_certs": "yes", + "source": "fake-image", + "disk_format": "qcow2", + "checksum": "md5:checksum"}, + 'configdrive': {'type': 'file', + 'location': configdrive_path}} + with mock.patch.object(ansible_deploy, 'open', mock.mock_open(), + create=True) as open_mock: + with task_manager.acquire(self.context, self.node.uuid) as task: + self.assertEqual(expected, + ansible_deploy._prepare_variables(task)) + mock_build_configdrive.assert_called_once_with( + task.node, {'meta_data': {}}) + open_mock.assert_has_calls(( + mock.call(configdrive_path, 'w'), + mock.call().__enter__(), + mock.call().write('fake-content'), + mock.call().__exit__(None, None, None))) + def test__validate_clean_steps(self): steps = [{"interface": "deploy", "name": "foo", diff --git a/ironic/tests/unit/drivers/modules/redfish/test_boot.py b/ironic/tests/unit/drivers/modules/redfish/test_boot.py index e0f0dbf17e..508021bed1 100644 --- a/ironic/tests/unit/drivers/modules/redfish/test_boot.py +++ b/ironic/tests/unit/drivers/modules/redfish/test_boot.py @@ -861,15 +861,16 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True) @mock.patch.object(redfish_boot, '_insert_vmedia', autospec=True) @mock.patch.object(redfish_boot, '_parse_deploy_info', autospec=True) - @mock.patch.object(redfish_boot, 'manager_utils', autospec=True) + @mock.patch.object(redfish_boot.manager_utils, 'node_set_boot_device', + autospec=True) @mock.patch.object(redfish_boot, 'deploy_utils', autospec=True) @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) @mock.patch.object(redfish_utils, 'get_system', autospec=True) def test_prepare_instance_ramdisk_boot( self, mock_system, mock_boot_mode_utils, mock_deploy_utils, - mock_manager_utils, mock__parse_deploy_info, mock__insert_vmedia, - mock__eject_vmedia, mock_prepare_boot_iso, mock_prepare_disk, - mock_clean_up_instance): + mock_node_set_boot_device, mock__parse_deploy_info, + mock__insert_vmedia, mock__eject_vmedia, mock_prepare_boot_iso, + mock_prepare_disk, mock_clean_up_instance): configdrive = 'Y29udGVudA==' managers = mock_system.return_value.managers @@ -911,7 +912,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): 'cd-url', sushy.VIRTUAL_MEDIA_USBSTICK), ]) - mock_manager_utils.node_set_boot_device.assert_called_once_with( + mock_node_set_boot_device.assert_called_once_with( task, boot_devices.CDROM, persistent=True) mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) @@ -922,14 +923,16 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True) @mock.patch.object(redfish_boot, '_insert_vmedia', autospec=True) @mock.patch.object(redfish_boot, '_parse_deploy_info', autospec=True) - @mock.patch.object(redfish_boot, 'manager_utils', autospec=True) + @mock.patch.object(redfish_boot.manager_utils, 'node_set_boot_device', + autospec=True) @mock.patch.object(redfish_boot, 'deploy_utils', autospec=True) @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) @mock.patch.object(redfish_utils, 'get_system', autospec=True) def test_prepare_instance_ramdisk_boot_iso( self, mock_system, mock_boot_mode_utils, mock_deploy_utils, - mock_manager_utils, mock__parse_deploy_info, mock__insert_vmedia, - mock__eject_vmedia, mock_prepare_boot_iso, mock_clean_up_instance): + mock_node_set_boot_device, mock__parse_deploy_info, + mock__insert_vmedia, mock__eject_vmedia, mock_prepare_boot_iso, + mock_clean_up_instance): managers = mock_system.return_value.managers with task_manager.acquire(self.context, self.node.uuid, @@ -960,7 +963,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): mock__insert_vmedia.assert_called_once_with( task, managers, 'image-url', sushy.VIRTUAL_MEDIA_CD) - mock_manager_utils.node_set_boot_device.assert_called_once_with( + mock_node_set_boot_device.assert_called_once_with( task, boot_devices.CDROM, persistent=True) mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) @@ -971,14 +974,16 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True) @mock.patch.object(redfish_boot, '_insert_vmedia', autospec=True) @mock.patch.object(redfish_boot, '_parse_deploy_info', autospec=True) - @mock.patch.object(redfish_boot, 'manager_utils', autospec=True) + @mock.patch.object(redfish_boot.manager_utils, 'node_set_boot_device', + autospec=True) @mock.patch.object(redfish_boot, 'deploy_utils', autospec=True) @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) @mock.patch.object(redfish_utils, 'get_system', autospec=True) def test_prepare_instance_ramdisk_boot_iso_boot( self, mock_system, mock_boot_mode_utils, mock_deploy_utils, - mock_manager_utils, mock__parse_deploy_info, mock__insert_vmedia, - mock__eject_vmedia, mock_prepare_boot_iso, mock_clean_up_instance): + mock_node_set_boot_device, mock__parse_deploy_info, + mock__insert_vmedia, mock__eject_vmedia, mock_prepare_boot_iso, + mock_clean_up_instance): managers = mock_system.return_value.managers with task_manager.acquire(self.context, self.node.uuid, @@ -1003,7 +1008,76 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase): mock__insert_vmedia.assert_called_once_with( task, managers, 'image-url', sushy.VIRTUAL_MEDIA_CD) - mock_manager_utils.node_set_boot_device.assert_called_once_with( + mock_node_set_boot_device.assert_called_once_with( + task, boot_devices.CDROM, persistent=True) + + mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) + + @mock.patch.object(redfish_boot.manager_utils, 'build_configdrive', + autospec=True) + @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, + '_eject_all', autospec=True) + @mock.patch.object(image_utils, 'prepare_configdrive_image', autospec=True) + @mock.patch.object(image_utils, 'prepare_boot_iso', autospec=True) + @mock.patch.object(redfish_boot, '_eject_vmedia', autospec=True) + @mock.patch.object(redfish_boot, '_insert_vmedia', autospec=True) + @mock.patch.object(redfish_boot, '_parse_deploy_info', autospec=True) + @mock.patch.object(redfish_boot.manager_utils, 'node_set_boot_device', + autospec=True) + @mock.patch.object(redfish_boot, 'deploy_utils', autospec=True) + @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + def test_prepare_instance_ramdisk_boot_render_configdrive( + self, mock_system, mock_boot_mode_utils, mock_deploy_utils, + mock_node_set_boot_device, mock__parse_deploy_info, + mock__insert_vmedia, mock__eject_vmedia, mock_prepare_boot_iso, + mock_prepare_disk, mock_clean_up_instance, mock_build_configdrive): + + configdrive = 'Y29udGVudA==' + managers = mock_system.return_value.managers + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.provision_state = states.DEPLOYING + task.node.driver_internal_info[ + 'root_uuid_or_disk_id'] = self.node.uuid + task.node.instance_info['configdrive'] = {'meta_data': {}} + + mock_build_configdrive.return_value = configdrive + + mock_deploy_utils.get_boot_option.return_value = 'ramdisk' + + d_info = { + 'deploy_kernel': 'kernel', + 'deploy_ramdisk': 'ramdisk', + 'bootloader': 'bootloader' + } + mock__parse_deploy_info.return_value = d_info + + mock_prepare_boot_iso.return_value = 'image-url' + mock_prepare_disk.return_value = 'cd-url' + + task.driver.boot.prepare_instance(task) + + mock_clean_up_instance.assert_called_once_with(mock.ANY, task) + + mock_build_configdrive.assert_called_once_with( + task.node, {'meta_data': {}}) + mock_prepare_boot_iso.assert_called_once_with(task, d_info) + mock_prepare_disk.assert_called_once_with(task, configdrive) + + mock__eject_vmedia.assert_has_calls([ + mock.call(task, managers, sushy.VIRTUAL_MEDIA_CD), + mock.call(task, managers, sushy.VIRTUAL_MEDIA_USBSTICK), + ]) + + mock__insert_vmedia.assert_has_calls([ + mock.call(task, managers, + 'image-url', sushy.VIRTUAL_MEDIA_CD), + mock.call(task, managers, + 'cd-url', sushy.VIRTUAL_MEDIA_USBSTICK), + ]) + + mock_node_set_boot_device.assert_called_once_with( task, boot_devices.CDROM, persistent=True) mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) diff --git a/ironic/tests/unit/drivers/modules/test_agent.py b/ironic/tests/unit/drivers/modules/test_agent.py index 6ab5d71d06..2fad169aac 100644 --- a/ironic/tests/unit/drivers/modules/test_agent.py +++ b/ironic/tests/unit/drivers/modules/test_agent.py @@ -1482,6 +1482,73 @@ class TestAgentDeploy(CommonTestsMixin, db_base.DbTestCase): self.assertEqual(states.ACTIVE, task.node.target_provision_state) + @mock.patch.object(manager_utils, 'build_configdrive', autospec=True) + def test_write_image_render_configdrive(self, mock_build_configdrive): + self.node.provision_state = states.DEPLOYWAIT + self.node.target_provision_state = states.ACTIVE + i_info = self.node.instance_info + i_info['kernel'] = 'kernel' + i_info['ramdisk'] = 'ramdisk' + i_info['root_gb'] = 10 + i_info['swap_mb'] = 10 + i_info['ephemeral_mb'] = 0 + i_info['ephemeral_format'] = 'abc' + i_info['configdrive'] = {'meta_data': {}} + i_info['preserve_ephemeral'] = False + i_info['image_type'] = 'partition' + i_info['root_mb'] = 10240 + i_info['deploy_boot_mode'] = 'bios' + i_info['capabilities'] = {"boot_option": "local", + "disk_label": "msdos"} + self.node.instance_info = i_info + driver_internal_info = self.node.driver_internal_info + driver_internal_info['is_whole_disk_image'] = False + self.node.driver_internal_info = driver_internal_info + self.node.save() + test_temp_url = 'http://image' + expected_image_info = { + 'urls': [test_temp_url], + 'id': 'fake-image', + 'node_uuid': self.node.uuid, + 'checksum': 'checksum', + 'disk_format': 'qcow2', + 'container_format': 'bare', + 'stream_raw_images': True, + 'kernel': 'kernel', + 'ramdisk': 'ramdisk', + 'root_gb': 10, + 'swap_mb': 10, + 'ephemeral_mb': 0, + 'ephemeral_format': 'abc', + 'configdrive': 'configdrive', + 'preserve_ephemeral': False, + 'image_type': 'partition', + 'root_mb': 10240, + 'boot_option': 'local', + 'deploy_boot_mode': 'bios', + 'disk_label': 'msdos' + } + + mock_build_configdrive.return_value = 'configdrive' + + client_mock = mock.MagicMock(spec_set=['execute_deploy_step']) + + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.driver.deploy._client = client_mock + task.driver.deploy.write_image(task) + + step = {'step': 'write_image', 'interface': 'deploy', + 'args': {'image_info': expected_image_info, + 'configdrive': 'configdrive'}} + client_mock.execute_deploy_step.assert_called_once_with( + step, task.node, mock.ANY) + self.assertEqual(states.DEPLOYWAIT, task.node.provision_state) + self.assertEqual(states.ACTIVE, + task.node.target_provision_state) + mock_build_configdrive.assert_called_once_with( + task.node, {'meta_data': {}}) + @mock.patch.object(deploy_utils, 'remove_http_instance_symlink', autospec=True) @mock.patch.object(agent.LOG, 'warning', spec_set=True, autospec=True) diff --git a/ironic/tests/unit/objects/test_node.py b/ironic/tests/unit/objects/test_node.py index a9dd2684b2..ab2b9cec88 100644 --- a/ironic/tests/unit/objects/test_node.py +++ b/ironic/tests/unit/objects/test_node.py @@ -61,6 +61,18 @@ class TestNodeObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn): # Ensure the node can be serialised. jsonutils.dumps(d) + def test_as_dict_secure_configdrive_as_dict(self): + self.node.driver_info['ipmi_password'] = 'fake' + self.node.instance_info['configdrive'] = {'user_data': 'data'} + self.node.driver_internal_info['agent_secret_token'] = 'abc' + d = self.node.as_dict(secure=True) + self.assertEqual('******', d['driver_info']['ipmi_password']) + self.assertEqual('******', d['instance_info']['configdrive']) + self.assertEqual('******', + d['driver_internal_info']['agent_secret_token']) + # Ensure the node can be serialised. + jsonutils.dumps(d) + def test_as_dict_secure_with_configdrive(self): self.node.driver_info['ipmi_password'] = 'fake' self.node.instance_info['configdrive'] = 'data' @@ -73,6 +85,19 @@ class TestNodeObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn): # Ensure the node can be serialised. jsonutils.dumps(d) + def test_as_dict_secure_with_configdrive_as_dict(self): + self.node.driver_info['ipmi_password'] = 'fake' + self.node.instance_info['configdrive'] = {'user_data': 'data'} + self.node.driver_internal_info['agent_secret_token'] = 'abc' + d = self.node.as_dict(secure=True, mask_configdrive=False) + self.assertEqual('******', d['driver_info']['ipmi_password']) + self.assertEqual({'user_data': 'data'}, + d['instance_info']['configdrive']) + self.assertEqual('******', + d['driver_internal_info']['agent_secret_token']) + # Ensure the node can be serialised. + jsonutils.dumps(d) + def test_as_dict_with_traits(self): self.fake_node['traits'] = ['CUSTOM_1'] self.node = obj_utils.get_test_node(self.ctxt, **self.fake_node) diff --git a/releasenotes/notes/configdrive-render-8eb398d956393d60.yaml b/releasenotes/notes/configdrive-render-8eb398d956393d60.yaml new file mode 100644 index 0000000000..889c4b76e0 --- /dev/null +++ b/releasenotes/notes/configdrive-render-8eb398d956393d60.yaml @@ -0,0 +1,6 @@ +--- +other: + - | + Configuration drives are now stored in their JSON representation and only + rendered when needed. This allows deploy steps to access the original + JSON representation rather than only the rendered ISO image.