Enable agent_token for virtual media boot

This provides an embedded agent_token through out of band
means which ensures greater security for such deployments.

Also some changes to the redfish virtual media boot unit
tests as the entirety of conductor utilties was mocked.

Downside with the pregenerated token, the logic had to be
slightly revised as the existence of a token with an older
client is a case that virtual media deployments will hit.

This was addressed, and tests updated as a result.

Story: 2007025
Task: 37819
Change-Id: Iaa2a52c2e53534cbf6998ad24f1f1184c0f222d8
This commit is contained in:
Julia Kreger 2019-12-05 14:24:17 -08:00
parent 7a15df60c3
commit 6121fbf100
9 changed files with 92 additions and 48 deletions

View File

@ -2959,7 +2959,8 @@ class ConductorManager(base_manager.BaseConductorManager):
# either tokens are required and they are present, # either tokens are required and they are present,
# or a token is present in general and needs to be # or a token is present in general and needs to be
# validated. # validated.
if token_required or utils.is_agent_token_present(task.node): if (token_required
or (utils.is_agent_token_present(task.node) and agent_token)):
if not utils.is_agent_token_valid(task.node, agent_token): if not utils.is_agent_token_valid(task.node, agent_token):
LOG.error('Invalid agent_token receieved for node ' LOG.error('Invalid agent_token receieved for node '
'%(node)s', {'node': node_id}) '%(node)s', {'node': node_id})

View File

@ -1010,13 +1010,22 @@ def get_node_next_deploy_steps(task, skip_current_step=True):
skip_current_step=skip_current_step) skip_current_step=skip_current_step)
def add_secret_token(node): def add_secret_token(node, pregenerated=False):
"""Adds a secret token to driver_internal_info for IPA verification.""" """Adds a secret token to driver_internal_info for IPA verification.
:param node: Node object
:param pregenerated: Boolean value, default False, which indicates if
the token should be marked as "pregenerated" in
order to facilitate virtual media booting where
the token is embedded into the configuration.
"""
characters = string.ascii_letters + string.digits characters = string.ascii_letters + string.digits
token = ''.join( token = ''.join(
random.SystemRandom().choice(characters) for i in range(128)) random.SystemRandom().choice(characters) for i in range(128))
i_info = node.driver_internal_info i_info = node.driver_internal_info
i_info['agent_secret_token'] = token i_info['agent_secret_token'] = token
if pregenerated:
i_info['agent_secret_token_pregenerated'] = True
node.driver_internal_info = i_info node.driver_internal_info = i_info

View File

@ -509,6 +509,13 @@ class IloVirtualMediaBoot(base.BootInterface):
# during boot. # during boot.
ilo_common.eject_vmedia_devices(task) ilo_common.eject_vmedia_devices(task)
# NOTE(TheJulia): Since we're deploying, cleaning, or rescuing,
# with virtual media boot, we should generate a token!
manager_utils.add_secret_token(task.node, pregenerated=True)
ramdisk_params['ipa-agent-token'] = \
task.node.driver_internal_info['agent_secret_token']
task.node.save()
deploy_nic_mac = deploy_utils.get_single_nic_with_vif_port_id(task) deploy_nic_mac = deploy_utils.get_single_nic_with_vif_port_id(task)
ramdisk_params['BOOTIF'] = deploy_nic_mac ramdisk_params['BOOTIF'] = deploy_nic_mac
if node.provision_state == states.RESCUING: if node.provision_state == states.RESCUING:

View File

@ -990,6 +990,13 @@ class IRMCVirtualMediaBoot(base.BootInterface, IRMCVolumeBootMixIn):
{'node': task.node.uuid}) {'node': task.node.uuid})
return return
# NOTE(TheJulia): Since we're deploying, cleaning, or rescuing,
# with virtual media boot, we should generate a token!
manager_utils.add_secret_token(task.node, pregenerated=True)
ramdisk_params['ipa-agent-token'] = \
task.node.driver_internal_info['agent_secret_token']
task.node.save()
deploy_nic_mac = deploy_utils.get_single_nic_with_vif_port_id(task) deploy_nic_mac = deploy_utils.get_single_nic_with_vif_port_id(task)
ramdisk_params['BOOTIF'] = deploy_nic_mac ramdisk_params['BOOTIF'] = deploy_nic_mac

View File

@ -682,6 +682,13 @@ class RedfishVirtualMediaBoot(base.BootInterface):
states.INSPECTING): states.INSPECTING):
return return
# NOTE(TheJulia): Since we're deploying, cleaning, or rescuing,
# with virtual media boot, we should generate a token!
manager_utils.add_secret_token(node, pregenerated=True)
node.save()
ramdisk_params['ipa-agent-token'] = \
node.driver_internal_info['agent_secret_token']
manager_utils.node_power_action(task, states.POWER_OFF) manager_utils.node_power_action(task, states.POWER_OFF)
d_info = self._parse_driver_info(node) d_info = self._parse_driver_info(node)

View File

@ -7082,6 +7082,30 @@ class DoNodeAdoptionTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
mock_heartbeat.assert_called_with(mock.ANY, mock.ANY, mock_heartbeat.assert_called_with(mock.ANY, mock.ANY,
'http://callback', '1.4.1') 'http://callback', '1.4.1')
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat',
autospec=True)
@mock.patch('ironic.conductor.manager.ConductorManager._spawn_worker',
autospec=True)
def test_heartbeat_with_agent_pregenerated_token(
self, mock_spawn, mock_heartbeat):
"""Test heartbeating."""
node = obj_utils.create_test_node(
self.context, driver='fake-hardware',
provision_state=states.DEPLOYING,
target_provision_state=states.ACTIVE,
driver_internal_info={'agent_secret_token': 'a secret'})
self._start_service()
mock_spawn.reset_mock()
mock_spawn.side_effect = self._fake_spawn
self.service.heartbeat(
self.context, node.uuid, 'http://callback', '6.0.1',
agent_token=None)
mock_heartbeat.assert_called_with(mock.ANY, mock.ANY,
'http://callback', '6.0.1')
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat', @mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat',
autospec=True) autospec=True)
@mock.patch('ironic.conductor.manager.ConductorManager._spawn_worker', @mock.patch('ironic.conductor.manager.ConductorManager._spawn_worker',
@ -7209,32 +7233,6 @@ class DoNodeAdoptionTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
agent_token='evil', agent_version='4.0.0') agent_token='evil', agent_version='4.0.0')
self.assertFalse(mock_heartbeat.called) self.assertFalse(mock_heartbeat.called)
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat',
autospec=True)
@mock.patch('ironic.conductor.manager.ConductorManager._spawn_worker',
autospec=True)
def test_heartbeat_invalid_when_token_on_file_older_agent(
self, mock_spawn, mock_heartbeat):
"""Heartbeat rejected if a token is on file."""
self.config(require_agent_token=False)
node = obj_utils.create_test_node(
self.context, driver='fake-hardware',
provision_state=states.DEPLOYING,
target_provision_state=states.ACTIVE,
driver_internal_info={'agent_secret_token': 'a secret'})
self._start_service()
mock_spawn.reset_mock()
mock_spawn.side_effect = self._fake_spawn
self.assertRaises(exception.InvalidParameterValue,
self.service.heartbeat, self.context,
node.uuid, 'http://callback',
agent_token=None, agent_version='4.0.0')
self.assertFalse(mock_heartbeat.called)
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat', @mock.patch('ironic.drivers.modules.fake.FakeDeploy.heartbeat',
autospec=True) autospec=True)
@mock.patch('ironic.conductor.manager.ConductorManager._spawn_worker', @mock.patch('ironic.conductor.manager.ConductorManager._spawn_worker',

View File

@ -821,7 +821,8 @@ class IloVirtualMediaBootTestCase(test_common.BaseIloTest):
prepare_node_for_deploy_mock.assert_called_once_with(task) prepare_node_for_deploy_mock.assert_called_once_with(task)
eject_mock.assert_called_once_with(task) eject_mock.assert_called_once_with(task)
expected_ramdisk_opts = {'a': 'b', 'BOOTIF': '12:34:56:78:90:ab'} expected_ramdisk_opts = {'a': 'b', 'BOOTIF': '12:34:56:78:90:ab',
'ipa-agent-token': mock.ANY}
get_nic_mock.assert_called_once_with(task) get_nic_mock.assert_called_once_with(task)
setup_vmedia_mock.assert_called_once_with(task, iso, setup_vmedia_mock.assert_called_once_with(task, iso,
expected_ramdisk_opts) expected_ramdisk_opts)

View File

@ -990,7 +990,9 @@ class IRMCVirtualMediaBootTestCase(test_common.BaseIRMCTest):
shared=False) as task: shared=False) as task:
task.driver.boot.prepare_ramdisk(task, ramdisk_params) task.driver.boot.prepare_ramdisk(task, ramdisk_params)
expected_ramdisk_opts = {'a': 'b', 'BOOTIF': '12:34:56:78:90:ab'} expected_ramdisk_opts = {'a': 'b', 'BOOTIF': '12:34:56:78:90:ab',
'ipa-agent-token': mock.ANY}
get_single_nic_with_vif_port_id_mock.assert_called_once_with( get_single_nic_with_vif_port_id_mock.assert_called_once_with(
task) task)
_setup_vmedia_mock.assert_called_once_with( _setup_vmedia_mock.assert_called_once_with(

View File

@ -552,6 +552,8 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
self.assertRaises(exception.UnsupportedDriverExtension, self.assertRaises(exception.UnsupportedDriverExtension,
task.driver.boot.validate_inspection, task) task.driver.boot.validate_inspection, task)
@mock.patch.object(redfish_boot.manager_utils, 'node_set_boot_device',
autospec=True)
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
'_prepare_deploy_iso', autospec=True) '_prepare_deploy_iso', autospec=True)
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
@ -560,15 +562,16 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
'_insert_vmedia', autospec=True) '_insert_vmedia', autospec=True)
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
'_parse_driver_info', autospec=True) '_parse_driver_info', autospec=True)
@mock.patch.object(redfish_boot, 'manager_utils', autospec=True) @mock.patch.object(redfish_boot.manager_utils, 'node_power_action',
autospec=True)
@mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True)
def test_prepare_ramdisk_with_params( def test_prepare_ramdisk_with_params(
self, mock_boot_mode_utils, mock_manager_utils, self, mock_boot_mode_utils, mock_node_power_action,
mock__parse_driver_info, mock__insert_vmedia, mock__eject_vmedia, mock__parse_driver_info, mock__insert_vmedia, mock__eject_vmedia,
mock__prepare_deploy_iso): mock__prepare_deploy_iso, mock_node_set_boot_device):
with task_manager.acquire(self.context, self.node.uuid, with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task: shared=False) as task:
task.node.provision_state = states.DEPLOYING task.node.provision_state = states.DEPLOYING
mock__parse_driver_info.return_value = {} mock__parse_driver_info.return_value = {}
@ -576,7 +579,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
task.driver.boot.prepare_ramdisk(task, {}) task.driver.boot.prepare_ramdisk(task, {})
mock_manager_utils.node_power_action.assert_called_once_with( mock_node_power_action.assert_called_once_with(
task, states.POWER_OFF) task, states.POWER_OFF)
mock__eject_vmedia.assert_called_once_with( mock__eject_vmedia.assert_called_once_with(
@ -587,17 +590,20 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
expected_params = { expected_params = {
'BOOTIF': None, 'BOOTIF': None,
'ipa-agent-token': mock.ANY,
'ipa-debug': '1', 'ipa-debug': '1',
} }
mock__prepare_deploy_iso.assert_called_once_with( mock__prepare_deploy_iso.assert_called_once_with(
task, expected_params, 'deploy') task, expected_params, 'deploy')
mock_manager_utils.node_set_boot_device.assert_called_once_with( mock_node_set_boot_device.assert_called_once_with(
task, boot_devices.CDROM, False) task, boot_devices.CDROM, False)
mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task)
@mock.patch.object(redfish_boot.manager_utils, 'node_set_boot_device',
autospec=True)
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
'_prepare_deploy_iso', autospec=True) '_prepare_deploy_iso', autospec=True)
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
@ -606,12 +612,13 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
'_insert_vmedia', autospec=True) '_insert_vmedia', autospec=True)
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
'_parse_driver_info', autospec=True) '_parse_driver_info', autospec=True)
@mock.patch.object(redfish_boot, 'manager_utils', autospec=True) @mock.patch.object(redfish_boot.manager_utils, 'node_power_action',
autospec=True)
@mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True)
def test_prepare_ramdisk_no_debug( def test_prepare_ramdisk_no_debug(
self, mock_boot_mode_utils, mock_manager_utils, self, mock_boot_mode_utils, mock_node_power_action,
mock__parse_driver_info, mock__insert_vmedia, mock__eject_vmedia, mock__parse_driver_info, mock__insert_vmedia, mock__eject_vmedia,
mock__prepare_deploy_iso): mock__prepare_deploy_iso, mock_node_set_boot_device):
self.config(debug=False) self.config(debug=False)
with task_manager.acquire(self.context, self.node.uuid, with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task: shared=True) as task:
@ -622,7 +629,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
task.driver.boot.prepare_ramdisk(task, {}) task.driver.boot.prepare_ramdisk(task, {})
mock_manager_utils.node_power_action.assert_called_once_with( mock_node_power_action.assert_called_once_with(
task, states.POWER_OFF) task, states.POWER_OFF)
mock__eject_vmedia.assert_called_once_with( mock__eject_vmedia.assert_called_once_with(
@ -633,16 +640,19 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
expected_params = { expected_params = {
'BOOTIF': None, 'BOOTIF': None,
'ipa-agent-token': mock.ANY,
} }
mock__prepare_deploy_iso.assert_called_once_with( mock__prepare_deploy_iso.assert_called_once_with(
task, expected_params, 'deploy') task, expected_params, 'deploy')
mock_manager_utils.node_set_boot_device.assert_called_once_with( mock_node_set_boot_device.assert_called_once_with(
task, boot_devices.CDROM, False) task, boot_devices.CDROM, False)
mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task)
@mock.patch.object(redfish_boot.manager_utils, 'node_set_boot_device',
autospec=True)
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
'_prepare_floppy_image', autospec=True) '_prepare_floppy_image', autospec=True)
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
@ -655,16 +665,17 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
'_insert_vmedia', autospec=True) '_insert_vmedia', autospec=True)
@mock.patch.object(redfish_boot.RedfishVirtualMediaBoot, @mock.patch.object(redfish_boot.RedfishVirtualMediaBoot,
'_parse_driver_info', autospec=True) '_parse_driver_info', autospec=True)
@mock.patch.object(redfish_boot, 'manager_utils', autospec=True) @mock.patch.object(redfish_boot.manager_utils, 'node_power_action',
autospec=True)
@mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True) @mock.patch.object(redfish_boot, 'boot_mode_utils', autospec=True)
def test_prepare_ramdisk_with_floppy( def test_prepare_ramdisk_with_floppy(
self, mock_boot_mode_utils, mock_manager_utils, self, mock_boot_mode_utils, mock_node_power_action,
mock__parse_driver_info, mock__insert_vmedia, mock__eject_vmedia, mock__parse_driver_info, mock__insert_vmedia, mock__eject_vmedia,
mock__has_vmedia_device, mock__prepare_deploy_iso, mock__has_vmedia_device, mock__prepare_deploy_iso,
mock__prepare_floppy_image): mock__prepare_floppy_image, mock_node_set_boot_device):
with task_manager.acquire(self.context, self.node.uuid, with task_manager.acquire(self.context, self.node.uuid,
shared=True) as task: shared=False) as task:
task.node.provision_state = states.DEPLOYING task.node.provision_state = states.DEPLOYING
mock__parse_driver_info.return_value = { mock__parse_driver_info.return_value = {
@ -677,7 +688,7 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
task.driver.boot.prepare_ramdisk(task, {}) task.driver.boot.prepare_ramdisk(task, {})
mock_manager_utils.node_power_action.assert_called_once_with( mock_node_power_action.assert_called_once_with(
task, states.POWER_OFF) task, states.POWER_OFF)
mock__has_vmedia_device.assert_called_once_with( mock__has_vmedia_device.assert_called_once_with(
@ -703,12 +714,13 @@ class RedfishVirtualMediaBootTestCase(db_base.DbTestCase):
'BOOTIF': None, 'BOOTIF': None,
'boot_method': 'vmedia', 'boot_method': 'vmedia',
'ipa-debug': '1', 'ipa-debug': '1',
'ipa-agent-token': mock.ANY,
} }
mock__prepare_deploy_iso.assert_called_once_with( mock__prepare_deploy_iso.assert_called_once_with(
task, expected_params, 'deploy') task, expected_params, 'deploy')
mock_manager_utils.node_set_boot_device.assert_called_once_with( mock_node_set_boot_device.assert_called_once_with(
task, boot_devices.CDROM, False) task, boot_devices.CDROM, False)
mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task) mock_boot_mode_utils.sync_boot_mode.assert_called_once_with(task)