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
This commit is contained in:
Dmitry Tantsur 2021-05-10 16:50:48 +02:00
parent a362bbc9e4
commit 172d1b22df
15 changed files with 384 additions and 90 deletions

View File

@ -1423,6 +1423,9 @@ def node_sanitize(node, fields):
if not show_instance_secrets and node.get('instance_info'): if not show_instance_secrets and node.get('instance_info'):
node['instance_info'] = strutils.mask_dict_password( node['instance_info'] = strutils.mask_dict_password(
node['instance_info'], "******") 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 # NOTE(tenbrae): agent driver may store a swift temp_url on the
# instance_info, which shouldn't be exposed to non-admin users. # instance_info, which shouldn't be exposed to non-admin users.
# Now that ironic supports additional policies, we need to hide # Now that ironic supports additional policies, we need to hide

View File

@ -130,8 +130,6 @@ def do_node_deploy(task, conductor_id=None, configdrive=None,
utils.wipe_deploy_internal_info(task) utils.wipe_deploy_internal_info(task)
try: try:
if configdrive: if configdrive:
if isinstance(configdrive, dict):
configdrive = utils.build_configdrive(node, configdrive)
_store_configdrive(node, configdrive) _store_configdrive(node, configdrive)
except (exception.SwiftOperationError, exception.ConfigInvalid) as e: except (exception.SwiftOperationError, exception.ConfigInvalid) as e:
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
@ -417,6 +415,10 @@ def _store_configdrive(node, configdrive):
""" """
if CONF.deploy.configdrive_use_object_store: 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 # NOTE(lucasagomes): No reason to use a different timeout than
# the one used for deploying the node # the one used for deploying the node
timeout = (CONF.conductor.configdrive_swift_temp_url_duration timeout = (CONF.conductor.configdrive_swift_temp_url_duration

View File

@ -1005,6 +1005,21 @@ def build_configdrive(node, configdrive):
vendor_data=configdrive.get('vendor_data')) 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): def fast_track_able(task):
"""Checks if the operation can be a streamlined deployment sequence. """Checks if the operation can be a streamlined deployment sequence.

View File

@ -561,7 +561,11 @@ class AgentDeploy(CustomAgentDeploy):
if disk_label is not None: if disk_label is not None:
image_info['disk_label'] = disk_label 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 # Now switch into the corresponding in-band deploy step and let the
# result be polled normally. # result be polled normally.
new_step = {'interface': 'deploy', new_step = {'interface': 'deploy',

View File

@ -284,7 +284,7 @@ def _prepare_variables(task):
image['checksum'] = 'md5:%s' % checksum image['checksum'] = 'md5:%s' % checksum
_add_ssl_image_options(image) _add_ssl_image_options(image)
variables = {'image': image} variables = {'image': image}
configdrive = i_info.get('configdrive') configdrive = manager_utils.get_configdrive_image(task.node)
if configdrive: if configdrive:
if urlparse.urlparse(configdrive).scheme in ('http', 'https'): if urlparse.urlparse(configdrive).scheme in ('http', 'https'):
cfgdrv_type = 'url' cfgdrv_type = 'url'

View File

@ -636,21 +636,12 @@ class RedfishVirtualMediaBoot(base.BootInterface):
managers = redfish_utils.get_system(task.node).managers managers = redfish_utils.get_system(task.node).managers
deploy_info = _parse_deploy_info(node) deploy_info = _parse_deploy_info(node)
configdrive = node.instance_info.get('configdrive')
iso_ref = image_utils.prepare_boot_iso(task, deploy_info, **params) iso_ref = image_utils.prepare_boot_iso(task, deploy_info, **params)
_eject_vmedia(task, managers, sushy.VIRTUAL_MEDIA_CD) _eject_vmedia(task, managers, sushy.VIRTUAL_MEDIA_CD)
_insert_vmedia(task, managers, iso_ref, sushy.VIRTUAL_MEDIA_CD) _insert_vmedia(task, managers, iso_ref, sushy.VIRTUAL_MEDIA_CD)
if configdrive and boot_option == 'ramdisk': if boot_option == 'ramdisk':
_eject_vmedia(task, managers, sushy.VIRTUAL_MEDIA_USBSTICK) self._attach_configdrive(task, managers)
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)
del managers del managers
@ -660,6 +651,21 @@ class RedfishVirtualMediaBoot(base.BootInterface):
"%(device)s", {'node': task.node.uuid, "%(device)s", {'node': task.node.uuid,
'device': boot_devices.CDROM}) '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): def _eject_all(self, task):
managers = redfish_utils.get_system(task.node).managers 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) boot_option = deploy_utils.get_boot_option(task.node)
if (boot_option == 'ramdisk' 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) _eject_vmedia(task, managers, sushy.VIRTUAL_MEDIA_USBSTICK)
image_utils.cleanup_disk_image(task, prefix='configdrive') image_utils.cleanup_disk_image(task, prefix='configdrive')

View File

@ -174,11 +174,12 @@ class Node(base.IronicObject, object_base.VersionedObjectDictCompat):
d['driver_info'] = strutils.mask_dict_password( d['driver_info'] = strutils.mask_dict_password(
d.get('driver_info', {}), "******") d.get('driver_info', {}), "******")
iinfo = d.pop('instance_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, "******") d['instance_info'] = strutils.mask_dict_password(iinfo, "******")
if not mask_configdrive and configdrive: if configdrive is not None:
d['instance_info']['configdrive'] = configdrive d['instance_info']['configdrive'] = (
"******" if mask_configdrive else configdrive
)
d['driver_internal_info'] = strutils.mask_dict_password( d['driver_internal_info'] = strutils.mask_dict_password(
d.get('driver_internal_info', {}), "******") d.get('driver_internal_info', {}), "******")
return d return d

View File

@ -186,6 +186,25 @@ class TestListNodes(test_api_base.BaseApiTest):
self.assertNotIn('allocation_id', data) self.assertNotIn('allocation_id', data)
self.assertIn('allocation_uuid', 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): def test_get_one_with_json(self):
# Test backward compatibility with guess_content_type_from_ext # Test backward compatibility with guess_content_type_from_ext
node = obj_utils.create_test_node(self.context, node = obj_utils.create_test_node(self.context,

View File

@ -174,63 +174,6 @@ class DoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
def test__do_node_deploy_fast_track(self): def test__do_node_deploy_fast_track(self):
self._test__do_node_deploy_ok(fast_track=True) 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.object(swift, 'SwiftAPI', autospec=True)
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.prepare', @mock.patch('ironic.drivers.modules.fake.FakeDeploy.prepare',
autospec=True) autospec=True)
@ -1014,6 +957,37 @@ class StoreConfigDriveTestCase(db_base.DbTestCase):
self.node.refresh() self.node.refresh()
self.assertEqual(expected_instance_info, self.node.instance_info) 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): def test_store_configdrive_swift_no_deploy_timeout(self, mock_swift):
container_name = 'foo_container' container_name = 'foo_container'
expected_obj_name = 'configdrive-%s' % self.node.uuid expected_obj_name = 'configdrive-%s' % self.node.uuid

View File

@ -2308,3 +2308,71 @@ class CacheVendorTestCase(db_base.DbTestCase):
self.node.refresh() self.node.refresh()
self.assertNotIn('vendor', self.node.properties) self.assertNotIn('vendor', self.node.properties)
self.assertTrue(mock_log.called) 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)

View File

@ -495,6 +495,36 @@ class TestAnsibleMethods(AnsibleDeployTestCaseBase):
mock.call().write('fake-content'), mock.call().write('fake-content'),
mock.call().__exit__(None, None, None))) 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): def test__validate_clean_steps(self):
steps = [{"interface": "deploy", steps = [{"interface": "deploy",
"name": "foo", "name": "foo",

View File

@ -849,15 +849,16 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
@mock.patch.object(redfish_boot, '_eject_vmedia', 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, '_insert_vmedia', autospec=True)
@mock.patch.object(redfish_boot, '_parse_deploy_info', 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, 'deploy_utils', autospec=True)
@mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True)
@mock.patch.object(redfish_utils, 'get_system', autospec=True) @mock.patch.object(redfish_utils, 'get_system', autospec=True)
def test_prepare_instance_ramdisk_boot( def test_prepare_instance_ramdisk_boot(
self, mock_system, mock_boot_mode_utils, mock_deploy_utils, self, mock_system, mock_boot_mode_utils, mock_deploy_utils,
mock_manager_utils, mock__parse_deploy_info, mock__insert_vmedia, mock_node_set_boot_device, mock__parse_deploy_info,
mock__eject_vmedia, mock_prepare_boot_iso, mock_prepare_disk, mock__insert_vmedia, mock__eject_vmedia, mock_prepare_boot_iso,
mock_clean_up_instance): mock_prepare_disk, mock_clean_up_instance):
configdrive = 'Y29udGVudA==' configdrive = 'Y29udGVudA=='
managers = mock_system.return_value.managers managers = mock_system.return_value.managers
@ -899,7 +900,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
'cd-url', sushy.VIRTUAL_MEDIA_USBSTICK), '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) task, boot_devices.CDROM, persistent=True)
mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) 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, '_eject_vmedia', autospec=True)
@mock.patch.object(redfish_boot, '_insert_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, '_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, 'deploy_utils', autospec=True)
@mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True)
@mock.patch.object(redfish_utils, 'get_system', autospec=True) @mock.patch.object(redfish_utils, 'get_system', autospec=True)
def test_prepare_instance_ramdisk_boot_iso( def test_prepare_instance_ramdisk_boot_iso(
self, mock_system, mock_boot_mode_utils, mock_deploy_utils, self, mock_system, mock_boot_mode_utils, mock_deploy_utils,
mock_manager_utils, mock__parse_deploy_info, mock__insert_vmedia, mock_node_set_boot_device, mock__parse_deploy_info,
mock__eject_vmedia, mock_prepare_boot_iso, mock_clean_up_instance): mock__insert_vmedia, mock__eject_vmedia, mock_prepare_boot_iso,
mock_clean_up_instance):
managers = mock_system.return_value.managers managers = mock_system.return_value.managers
with task_manager.acquire(self.context, self.node.uuid, 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( mock__insert_vmedia.assert_called_once_with(
task, managers, 'image-url', sushy.VIRTUAL_MEDIA_CD) 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) task, boot_devices.CDROM, persistent=True)
mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) 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, '_eject_vmedia', autospec=True)
@mock.patch.object(redfish_boot, '_insert_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, '_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, 'deploy_utils', autospec=True)
@mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True)
@mock.patch.object(redfish_utils, 'get_system', autospec=True) @mock.patch.object(redfish_utils, 'get_system', autospec=True)
def test_prepare_instance_ramdisk_boot_iso_boot( def test_prepare_instance_ramdisk_boot_iso_boot(
self, mock_system, mock_boot_mode_utils, mock_deploy_utils, self, mock_system, mock_boot_mode_utils, mock_deploy_utils,
mock_manager_utils, mock__parse_deploy_info, mock__insert_vmedia, mock_node_set_boot_device, mock__parse_deploy_info,
mock__eject_vmedia, mock_prepare_boot_iso, mock_clean_up_instance): mock__insert_vmedia, mock__eject_vmedia, mock_prepare_boot_iso,
mock_clean_up_instance):
managers = mock_system.return_value.managers managers = mock_system.return_value.managers
with task_manager.acquire(self.context, self.node.uuid, 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( mock__insert_vmedia.assert_called_once_with(
task, managers, 'image-url', sushy.VIRTUAL_MEDIA_CD) 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) task, boot_devices.CDROM, persistent=True)
mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task)

View File

@ -1482,6 +1482,73 @@ class TestAgentDeploy(CommonTestsMixin, db_base.DbTestCase):
self.assertEqual(states.ACTIVE, self.assertEqual(states.ACTIVE,
task.node.target_provision_state) 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', @mock.patch.object(deploy_utils, 'remove_http_instance_symlink',
autospec=True) autospec=True)
@mock.patch.object(agent.LOG, 'warning', spec_set=True, autospec=True) @mock.patch.object(agent.LOG, 'warning', spec_set=True, autospec=True)

View File

@ -61,6 +61,18 @@ class TestNodeObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn):
# Ensure the node can be serialised. # Ensure the node can be serialised.
jsonutils.dumps(d) 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): def test_as_dict_secure_with_configdrive(self):
self.node.driver_info['ipmi_password'] = 'fake' self.node.driver_info['ipmi_password'] = 'fake'
self.node.instance_info['configdrive'] = 'data' self.node.instance_info['configdrive'] = 'data'
@ -73,6 +85,19 @@ class TestNodeObject(db_base.DbTestCase, obj_utils.SchemasTestMixIn):
# Ensure the node can be serialised. # Ensure the node can be serialised.
jsonutils.dumps(d) 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): def test_as_dict_with_traits(self):
self.fake_node['traits'] = ['CUSTOM_1'] self.fake_node['traits'] = ['CUSTOM_1']
self.node = obj_utils.get_test_node(self.ctxt, **self.fake_node) self.node = obj_utils.get_test_node(self.ctxt, **self.fake_node)

View File

@ -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.