summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDmitry Tantsur <dtantsur@protonmail.com>2021-05-10 16:50:48 +0200
committerDmitry Tantsur <dtantsur@protonmail.com>2021-05-19 15:17:49 +0200
commit172d1b22df0a86b63730379fe2a41553ed2b53c5 (patch)
tree6921e64d755132d08651171cdc89585242a6a6d7
parenta362bbc9e496a195079e561ff57bc0f0e8fed0af (diff)
downloadironic-172d1b22df0a86b63730379fe2a41553ed2b53c5.tar.gz
Delay rendering configdrive
When the configdrive input is JSON (meta_data, etc), delay the rendering until the ISO image is actually used. It has two benefits: 1) Avoid storing a large ISO image in instance_info, 2) Allow deploy steps to access the original user's input. Fix configdrive masking to correctly mask dicts. Story: #2008875 Task: #42419 Change-Id: I86d30bbb505b8c794bfa6412606f4516f8885aa9
-rw-r--r--ironic/api/controllers/v1/node.py3
-rw-r--r--ironic/conductor/deployments.py6
-rw-r--r--ironic/conductor/utils.py15
-rw-r--r--ironic/drivers/modules/agent.py6
-rw-r--r--ironic/drivers/modules/ansible/deploy.py2
-rw-r--r--ironic/drivers/modules/redfish/boot.py30
-rw-r--r--ironic/objects/node.py9
-rw-r--r--ironic/tests/unit/api/controllers/v1/test_node.py19
-rw-r--r--ironic/tests/unit/conductor/test_deployments.py88
-rw-r--r--ironic/tests/unit/conductor/test_utils.py68
-rw-r--r--ironic/tests/unit/drivers/modules/ansible/test_deploy.py30
-rw-r--r--ironic/tests/unit/drivers/modules/redfish/test_boot.py100
-rw-r--r--ironic/tests/unit/drivers/modules/test_agent.py67
-rw-r--r--ironic/tests/unit/objects/test_node.py25
-rw-r--r--releasenotes/notes/configdrive-render-8eb398d956393d60.yaml6
15 files changed, 384 insertions, 90 deletions
diff --git a/ironic/api/controllers/v1/node.py b/ironic/api/controllers/v1/node.py
index 401bd577c..173d7b8f5 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 e146a26d6..11670c0b2 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 3b8b54c22..2a3e6b4f8 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 3296fb934..300768969 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 2c17bccdb..218d046b2 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 b854fc994..b393fa71b 100644
--- a/ironic/drivers/modules/redfish/boot.py
+++ b/ironic/drivers/modules/redfish/boot.py
@@ -636,21 +636,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
@@ -660,6 +651,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
@@ -676,7 +682,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 c8f79f286..3eb997c51 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 cc778cd77..baa21d5a9 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 f0e894461..020735984 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 e8f20e94a..c94aff01c 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 17ab45786..886c01db5 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 d4471cda5..50a525e47 100644
--- a/ironic/tests/unit/drivers/modules/redfish/test_boot.py
+++ b/ironic/tests/unit/drivers/modules/redfish/test_boot.py
@@ -849,15 +849,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
@@ -899,7 +900,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)
@@ -910,14 +911,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,
@@ -948,7 +951,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)
@@ -959,14 +962,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,
@@ -991,7 +996,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 6ab5d71d0..2fad169aa 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 a9dd2684b..ab2b9cec8 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 000000000..889c4b76e
--- /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.