Hash the rescue_password
In order to provide increased security, it is necessary to hash the rescue password in advance of it being stored into the database and to provide some sort of control for hash strength. This change IS incompatible with prior IPA versions with regard to use of the rescue feature, but I fully expect we will backport the change to IPA on to stable branches and perform a release as it is a security improvement. Change-Id: I1e118467a536229de6f7c245c1c48f0af38dcef2 Story: 2006777 Task: 27301
This commit is contained in:
parent
3eb0c16401
commit
fcaefdbe74
@ -610,9 +610,11 @@ class ConductorManager(base_manager.BaseConductorManager):
|
|||||||
|
|
||||||
# driver validation may check rescue_password, so save it on the
|
# driver validation may check rescue_password, so save it on the
|
||||||
# node early
|
# node early
|
||||||
instance_info = node.instance_info
|
i_info = node.instance_info
|
||||||
instance_info['rescue_password'] = rescue_password
|
i_info['rescue_password'] = rescue_password
|
||||||
node.instance_info = instance_info
|
i_info['hashed_rescue_password'] = utils.hash_password(
|
||||||
|
rescue_password)
|
||||||
|
node.instance_info = i_info
|
||||||
node.save()
|
node.save()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import crypt
|
||||||
import datetime
|
import datetime
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
import random
|
import random
|
||||||
@ -42,6 +43,12 @@ LOG = log.getLogger(__name__)
|
|||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
PASSWORD_HASH_FORMAT = {
|
||||||
|
'sha256': crypt.METHOD_SHA256,
|
||||||
|
'sha512': crypt.METHOD_SHA512,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@task_manager.require_exclusive_lock
|
@task_manager.require_exclusive_lock
|
||||||
def node_set_boot_device(task, device, persistent=False):
|
def node_set_boot_device(task, device, persistent=False):
|
||||||
"""Set the boot device for a node.
|
"""Set the boot device for a node.
|
||||||
@ -707,9 +714,13 @@ def remove_node_rescue_password(node, save=True):
|
|||||||
instance_info = node.instance_info
|
instance_info = node.instance_info
|
||||||
if 'rescue_password' in instance_info:
|
if 'rescue_password' in instance_info:
|
||||||
del instance_info['rescue_password']
|
del instance_info['rescue_password']
|
||||||
node.instance_info = instance_info
|
|
||||||
if save:
|
if 'hashed_rescue_password' in instance_info:
|
||||||
node.save()
|
del instance_info['hashed_rescue_password']
|
||||||
|
|
||||||
|
node.instance_info = instance_info
|
||||||
|
if save:
|
||||||
|
node.save()
|
||||||
|
|
||||||
|
|
||||||
def validate_instance_info_traits(node):
|
def validate_instance_info_traits(node):
|
||||||
@ -1108,3 +1119,21 @@ def is_agent_token_pregenerated(node):
|
|||||||
"""
|
"""
|
||||||
return node.driver_internal_info.get(
|
return node.driver_internal_info.get(
|
||||||
'agent_secret_token_pregenerated', False)
|
'agent_secret_token_pregenerated', False)
|
||||||
|
|
||||||
|
|
||||||
|
def make_salt():
|
||||||
|
"""Generate a random salt with the indicator tag for password type.
|
||||||
|
|
||||||
|
:returns: a valid salt for use with crypt.crypt
|
||||||
|
"""
|
||||||
|
return crypt.mksalt(
|
||||||
|
method=PASSWORD_HASH_FORMAT[
|
||||||
|
CONF.conductor.rescue_password_hash_algorithm])
|
||||||
|
|
||||||
|
|
||||||
|
def hash_password(password=''):
|
||||||
|
"""Hashes a supplied password.
|
||||||
|
|
||||||
|
:param value: Value to be hashed
|
||||||
|
"""
|
||||||
|
return crypt.crypt(password, make_salt())
|
||||||
|
@ -252,6 +252,18 @@ opts = [
|
|||||||
mutable=True,
|
mutable=True,
|
||||||
help=_('Glance ID, http:// or file:// URL of the initramfs of '
|
help=_('Glance ID, http:// or file:// URL of the initramfs of '
|
||||||
'the default rescue image.')),
|
'the default rescue image.')),
|
||||||
|
cfg.StrOpt('rescue_password_hash_algorithm',
|
||||||
|
default='sha256',
|
||||||
|
choices=['sha256', 'sha512'],
|
||||||
|
help=_('Password hash algorithm to be used for the rescue '
|
||||||
|
'password.')),
|
||||||
|
cfg.BoolOpt('require_rescue_password_hashed',
|
||||||
|
# TODO(TheJulia): Change this to True in Victoria.
|
||||||
|
default=False,
|
||||||
|
help=_('Option to cause the conductor to not fallback to '
|
||||||
|
'an un-hashed version of the rescue password, '
|
||||||
|
'permitting rescue with older ironic-python-agent '
|
||||||
|
'ramdisks.')),
|
||||||
cfg.StrOpt('bootloader',
|
cfg.StrOpt('bootloader',
|
||||||
mutable=True,
|
mutable=True,
|
||||||
help=_('Glance ID, http:// or file:// URL of the EFI system '
|
help=_('Glance ID, http:// or file:// URL of the EFI system '
|
||||||
|
@ -379,15 +379,35 @@ class AgentClient(object):
|
|||||||
to issue the request, or there was a malformed response from
|
to issue the request, or there was a malformed response from
|
||||||
the agent.
|
the agent.
|
||||||
:raises: AgentAPIError when agent failed to execute specified command.
|
:raises: AgentAPIError when agent failed to execute specified command.
|
||||||
|
:raises: InstanceRescueFailure when the agent ramdisk is too old
|
||||||
|
to support transmission of the rescue password.
|
||||||
:returns: A dict containing command response from agent.
|
:returns: A dict containing command response from agent.
|
||||||
See :func:`get_commands_status` for a command result sample.
|
See :func:`get_commands_status` for a command result sample.
|
||||||
"""
|
"""
|
||||||
rescue_pass = node.instance_info.get('rescue_password')
|
rescue_pass = node.instance_info.get('hashed_rescue_password')
|
||||||
|
# TODO(TheJulia): Remove fallback to use the fallback_rescue_password
|
||||||
|
# in the Victoria cycle.
|
||||||
|
fallback_rescue_pass = node.instance_info.get(
|
||||||
|
'rescue_password')
|
||||||
if not rescue_pass:
|
if not rescue_pass:
|
||||||
raise exception.IronicException(_('Agent rescue requires '
|
raise exception.IronicException(_('Agent rescue requires '
|
||||||
'rescue_password in '
|
'rescue_password in '
|
||||||
'instance_info'))
|
'instance_info'))
|
||||||
params = {'rescue_password': rescue_pass}
|
params = {'rescue_password': rescue_pass,
|
||||||
return self._command(node=node,
|
'hashed': True}
|
||||||
method='rescue.finalize_rescue',
|
try:
|
||||||
params=params)
|
return self._command(node=node,
|
||||||
|
method='rescue.finalize_rescue',
|
||||||
|
params=params)
|
||||||
|
except exception.AgentAPIError:
|
||||||
|
if CONF.conductor.require_rescue_password_hashed:
|
||||||
|
raise exception.InstanceRescueFailure(
|
||||||
|
_('Unable to rescue node due to an out of date agent '
|
||||||
|
'ramdisk. Please contact the administrator to update '
|
||||||
|
'the rescue ramdisk to contain an ironic-python-agent '
|
||||||
|
'version of at least 6.0.0.'))
|
||||||
|
else:
|
||||||
|
params = {'rescue_password': fallback_rescue_pass}
|
||||||
|
return self._command(node=node,
|
||||||
|
method='rescue.finalize_rescue',
|
||||||
|
params=params)
|
||||||
|
@ -2650,6 +2650,7 @@ class DoNodeRescueTestCase(mgr_utils.CommonMixIn, mgr_utils.ServiceSetUpMixin,
|
|||||||
call_args=(self.service._do_node_rescue, task),
|
call_args=(self.service._do_node_rescue, task),
|
||||||
err_handler=conductor_utils.spawn_rescue_error_handler)
|
err_handler=conductor_utils.spawn_rescue_error_handler)
|
||||||
self.assertIn('rescue_password', task.node.instance_info)
|
self.assertIn('rescue_password', task.node.instance_info)
|
||||||
|
self.assertIn('hashed_rescue_password', task.node.instance_info)
|
||||||
self.assertNotIn('agent_url', task.node.driver_internal_info)
|
self.assertNotIn('agent_url', task.node.driver_internal_info)
|
||||||
|
|
||||||
def test_do_node_rescue_invalid_state(self):
|
def test_do_node_rescue_invalid_state(self):
|
||||||
@ -2663,6 +2664,7 @@ class DoNodeRescueTestCase(mgr_utils.CommonMixIn, mgr_utils.ServiceSetUpMixin,
|
|||||||
self.context, node.uuid, "password")
|
self.context, node.uuid, "password")
|
||||||
node.refresh()
|
node.refresh()
|
||||||
self.assertNotIn('rescue_password', node.instance_info)
|
self.assertNotIn('rescue_password', node.instance_info)
|
||||||
|
self.assertNotIn('hashed_rescue_password', node.instance_info)
|
||||||
self.assertEqual(exception.InvalidStateRequested, exc.exc_info[0])
|
self.assertEqual(exception.InvalidStateRequested, exc.exc_info[0])
|
||||||
|
|
||||||
def _test_do_node_rescue_when_validate_fail(self, mock_validate):
|
def _test_do_node_rescue_when_validate_fail(self, mock_validate):
|
||||||
@ -2677,7 +2679,7 @@ class DoNodeRescueTestCase(mgr_utils.CommonMixIn, mgr_utils.ServiceSetUpMixin,
|
|||||||
self.service.do_node_rescue,
|
self.service.do_node_rescue,
|
||||||
self.context, node.uuid, "password")
|
self.context, node.uuid, "password")
|
||||||
node.refresh()
|
node.refresh()
|
||||||
self.assertNotIn('rescue_password', node.instance_info)
|
self.assertNotIn('hashed_rescue_password', node.instance_info)
|
||||||
# Compare true exception hidden by @messaging.expected_exceptions
|
# Compare true exception hidden by @messaging.expected_exceptions
|
||||||
self.assertEqual(exception.InstanceRescueFailure, exc.exc_info[0])
|
self.assertEqual(exception.InstanceRescueFailure, exc.exc_info[0])
|
||||||
|
|
||||||
@ -2712,10 +2714,11 @@ class DoNodeRescueTestCase(mgr_utils.CommonMixIn, mgr_utils.ServiceSetUpMixin,
|
|||||||
@mock.patch('ironic.drivers.modules.fake.FakeRescue.rescue')
|
@mock.patch('ironic.drivers.modules.fake.FakeRescue.rescue')
|
||||||
def test__do_node_rescue_returns_rescuewait(self, mock_rescue):
|
def test__do_node_rescue_returns_rescuewait(self, mock_rescue):
|
||||||
self._start_service()
|
self._start_service()
|
||||||
node = obj_utils.create_test_node(self.context, driver='fake-hardware',
|
node = obj_utils.create_test_node(
|
||||||
provision_state=states.RESCUING,
|
self.context, driver='fake-hardware',
|
||||||
instance_info={'rescue_password':
|
provision_state=states.RESCUING,
|
||||||
'password'})
|
instance_info={'rescue_password': 'password',
|
||||||
|
'hashed_rescue_password': '1234'})
|
||||||
with task_manager.TaskManager(self.context, node.uuid) as task:
|
with task_manager.TaskManager(self.context, node.uuid) as task:
|
||||||
mock_rescue.return_value = states.RESCUEWAIT
|
mock_rescue.return_value = states.RESCUEWAIT
|
||||||
self.service._do_node_rescue(task)
|
self.service._do_node_rescue(task)
|
||||||
@ -2723,14 +2726,17 @@ class DoNodeRescueTestCase(mgr_utils.CommonMixIn, mgr_utils.ServiceSetUpMixin,
|
|||||||
self.assertEqual(states.RESCUEWAIT, node.provision_state)
|
self.assertEqual(states.RESCUEWAIT, node.provision_state)
|
||||||
self.assertEqual(states.RESCUE, node.target_provision_state)
|
self.assertEqual(states.RESCUE, node.target_provision_state)
|
||||||
self.assertIn('rescue_password', node.instance_info)
|
self.assertIn('rescue_password', node.instance_info)
|
||||||
|
self.assertIn('hashed_rescue_password', node.instance_info)
|
||||||
|
|
||||||
@mock.patch('ironic.drivers.modules.fake.FakeRescue.rescue')
|
@mock.patch('ironic.drivers.modules.fake.FakeRescue.rescue')
|
||||||
def test__do_node_rescue_returns_rescue(self, mock_rescue):
|
def test__do_node_rescue_returns_rescue(self, mock_rescue):
|
||||||
self._start_service()
|
self._start_service()
|
||||||
node = obj_utils.create_test_node(self.context, driver='fake-hardware',
|
node = obj_utils.create_test_node(
|
||||||
provision_state=states.RESCUING,
|
self.context, driver='fake-hardware',
|
||||||
instance_info={'rescue_password':
|
provision_state=states.RESCUING,
|
||||||
'password'})
|
instance_info={
|
||||||
|
'rescue_password': 'password',
|
||||||
|
'hashed_rescue_password': '1234'})
|
||||||
with task_manager.TaskManager(self.context, node.uuid) as task:
|
with task_manager.TaskManager(self.context, node.uuid) as task:
|
||||||
mock_rescue.return_value = states.RESCUE
|
mock_rescue.return_value = states.RESCUE
|
||||||
self.service._do_node_rescue(task)
|
self.service._do_node_rescue(task)
|
||||||
@ -2738,15 +2744,18 @@ class DoNodeRescueTestCase(mgr_utils.CommonMixIn, mgr_utils.ServiceSetUpMixin,
|
|||||||
self.assertEqual(states.RESCUE, node.provision_state)
|
self.assertEqual(states.RESCUE, node.provision_state)
|
||||||
self.assertEqual(states.NOSTATE, node.target_provision_state)
|
self.assertEqual(states.NOSTATE, node.target_provision_state)
|
||||||
self.assertIn('rescue_password', node.instance_info)
|
self.assertIn('rescue_password', node.instance_info)
|
||||||
|
self.assertIn('hashed_rescue_password', node.instance_info)
|
||||||
|
|
||||||
@mock.patch.object(manager, 'LOG')
|
@mock.patch.object(manager, 'LOG')
|
||||||
@mock.patch('ironic.drivers.modules.fake.FakeRescue.rescue')
|
@mock.patch('ironic.drivers.modules.fake.FakeRescue.rescue')
|
||||||
def test__do_node_rescue_errors(self, mock_rescue, mock_log):
|
def test__do_node_rescue_errors(self, mock_rescue, mock_log):
|
||||||
self._start_service()
|
self._start_service()
|
||||||
node = obj_utils.create_test_node(self.context, driver='fake-hardware',
|
node = obj_utils.create_test_node(
|
||||||
provision_state=states.RESCUING,
|
self.context, driver='fake-hardware',
|
||||||
instance_info={'rescue_password':
|
provision_state=states.RESCUING,
|
||||||
'password'})
|
instance_info={
|
||||||
|
'rescue_password': 'password',
|
||||||
|
'hashed_rescue_password': '1234'})
|
||||||
mock_rescue.side_effect = exception.InstanceRescueFailure(
|
mock_rescue.side_effect = exception.InstanceRescueFailure(
|
||||||
'failed to rescue')
|
'failed to rescue')
|
||||||
with task_manager.TaskManager(self.context, node.uuid) as task:
|
with task_manager.TaskManager(self.context, node.uuid) as task:
|
||||||
@ -2756,6 +2765,7 @@ class DoNodeRescueTestCase(mgr_utils.CommonMixIn, mgr_utils.ServiceSetUpMixin,
|
|||||||
self.assertEqual(states.RESCUEFAIL, node.provision_state)
|
self.assertEqual(states.RESCUEFAIL, node.provision_state)
|
||||||
self.assertEqual(states.RESCUE, node.target_provision_state)
|
self.assertEqual(states.RESCUE, node.target_provision_state)
|
||||||
self.assertNotIn('rescue_password', node.instance_info)
|
self.assertNotIn('rescue_password', node.instance_info)
|
||||||
|
self.assertNotIn('hashed_rescue_password', node.instance_info)
|
||||||
self.assertTrue(node.last_error.startswith('Failed to rescue'))
|
self.assertTrue(node.last_error.startswith('Failed to rescue'))
|
||||||
self.assertTrue(mock_log.error.called)
|
self.assertTrue(mock_log.error.called)
|
||||||
|
|
||||||
@ -2763,10 +2773,12 @@ class DoNodeRescueTestCase(mgr_utils.CommonMixIn, mgr_utils.ServiceSetUpMixin,
|
|||||||
@mock.patch('ironic.drivers.modules.fake.FakeRescue.rescue')
|
@mock.patch('ironic.drivers.modules.fake.FakeRescue.rescue')
|
||||||
def test__do_node_rescue_bad_state(self, mock_rescue, mock_log):
|
def test__do_node_rescue_bad_state(self, mock_rescue, mock_log):
|
||||||
self._start_service()
|
self._start_service()
|
||||||
node = obj_utils.create_test_node(self.context, driver='fake-hardware',
|
node = obj_utils.create_test_node(
|
||||||
provision_state=states.RESCUING,
|
self.context, driver='fake-hardware',
|
||||||
instance_info={'rescue_password':
|
provision_state=states.RESCUING,
|
||||||
'password'})
|
instance_info={
|
||||||
|
'rescue_password': 'password',
|
||||||
|
'hashed_rescue_password': '1234'})
|
||||||
mock_rescue.return_value = states.ACTIVE
|
mock_rescue.return_value = states.ACTIVE
|
||||||
with task_manager.TaskManager(self.context, node.uuid) as task:
|
with task_manager.TaskManager(self.context, node.uuid) as task:
|
||||||
self.service._do_node_rescue(task)
|
self.service._do_node_rescue(task)
|
||||||
@ -2774,6 +2786,7 @@ class DoNodeRescueTestCase(mgr_utils.CommonMixIn, mgr_utils.ServiceSetUpMixin,
|
|||||||
self.assertEqual(states.RESCUEFAIL, node.provision_state)
|
self.assertEqual(states.RESCUEFAIL, node.provision_state)
|
||||||
self.assertEqual(states.RESCUE, node.target_provision_state)
|
self.assertEqual(states.RESCUE, node.target_provision_state)
|
||||||
self.assertNotIn('rescue_password', node.instance_info)
|
self.assertNotIn('rescue_password', node.instance_info)
|
||||||
|
self.assertNotIn('hashed_rescue_password', node.instance_info)
|
||||||
self.assertTrue(node.last_error.startswith('Failed to rescue'))
|
self.assertTrue(node.last_error.startswith('Failed to rescue'))
|
||||||
self.assertTrue(mock_log.error.called)
|
self.assertTrue(mock_log.error.called)
|
||||||
|
|
||||||
|
@ -1184,17 +1184,20 @@ class ErrorHandlersTestCase(tests_base.TestCase):
|
|||||||
@mock.patch.object(conductor_utils, 'LOG')
|
@mock.patch.object(conductor_utils, 'LOG')
|
||||||
def test_spawn_rescue_error_handler_no_worker(self, log_mock):
|
def test_spawn_rescue_error_handler_no_worker(self, log_mock):
|
||||||
exc = exception.NoFreeConductorWorker()
|
exc = exception.NoFreeConductorWorker()
|
||||||
self.node.instance_info = {'rescue_password': 'pass'}
|
self.node.instance_info = {'rescue_password': 'pass',
|
||||||
|
'hashed_rescue_password': '12'}
|
||||||
conductor_utils.spawn_rescue_error_handler(exc, self.node)
|
conductor_utils.spawn_rescue_error_handler(exc, self.node)
|
||||||
self.node.save.assert_called_once_with()
|
self.node.save.assert_called_once_with()
|
||||||
self.assertIn('No free conductor workers', self.node.last_error)
|
self.assertIn('No free conductor workers', self.node.last_error)
|
||||||
self.assertTrue(log_mock.warning.called)
|
self.assertTrue(log_mock.warning.called)
|
||||||
self.assertNotIn('rescue_password', self.node.instance_info)
|
self.assertNotIn('rescue_password', self.node.instance_info)
|
||||||
|
self.assertNotIn('hashed_rescue_password', self.node.instance_info)
|
||||||
|
|
||||||
@mock.patch.object(conductor_utils, 'LOG')
|
@mock.patch.object(conductor_utils, 'LOG')
|
||||||
def test_spawn_rescue_error_handler_other_error(self, log_mock):
|
def test_spawn_rescue_error_handler_other_error(self, log_mock):
|
||||||
exc = Exception('foo')
|
exc = Exception('foo')
|
||||||
self.node.instance_info = {'rescue_password': 'pass'}
|
self.node.instance_info = {'rescue_password': 'pass',
|
||||||
|
'hashed_rescue_password': '12'}
|
||||||
conductor_utils.spawn_rescue_error_handler(exc, self.node)
|
conductor_utils.spawn_rescue_error_handler(exc, self.node)
|
||||||
self.assertFalse(self.node.save.called)
|
self.assertFalse(self.node.save.called)
|
||||||
self.assertFalse(log_mock.warning.called)
|
self.assertFalse(log_mock.warning.called)
|
||||||
|
@ -1954,7 +1954,8 @@ class AgentRescueTestCase(db_base.DbTestCase):
|
|||||||
self.config(**config_kwarg)
|
self.config(**config_kwarg)
|
||||||
self.config(enabled_hardware_types=['fake-hardware'])
|
self.config(enabled_hardware_types=['fake-hardware'])
|
||||||
instance_info = INSTANCE_INFO
|
instance_info = INSTANCE_INFO
|
||||||
instance_info.update({'rescue_password': 'password'})
|
instance_info.update({'rescue_password': 'password',
|
||||||
|
'hashed_rescue_password': '1234'})
|
||||||
driver_info = DRIVER_INFO
|
driver_info = DRIVER_INFO
|
||||||
driver_info.update({'rescue_ramdisk': 'my_ramdisk',
|
driver_info.update({'rescue_ramdisk': 'my_ramdisk',
|
||||||
'rescue_kernel': 'my_kernel'})
|
'rescue_kernel': 'my_kernel'})
|
||||||
|
@ -330,8 +330,10 @@ class TestAgentClient(base.TestCase):
|
|||||||
def test_finalize_rescue(self):
|
def test_finalize_rescue(self):
|
||||||
self.client._command = mock.MagicMock(spec_set=[])
|
self.client._command = mock.MagicMock(spec_set=[])
|
||||||
self.node.instance_info['rescue_password'] = 'password'
|
self.node.instance_info['rescue_password'] = 'password'
|
||||||
|
self.node.instance_info['hashed_rescue_password'] = '1234'
|
||||||
expected_params = {
|
expected_params = {
|
||||||
'rescue_password': 'password',
|
'rescue_password': '1234',
|
||||||
|
'hashed': True,
|
||||||
}
|
}
|
||||||
self.client.finalize_rescue(self.node)
|
self.client.finalize_rescue(self.node)
|
||||||
self.client._command.assert_called_once_with(
|
self.client._command.assert_called_once_with(
|
||||||
@ -346,6 +348,36 @@ class TestAgentClient(base.TestCase):
|
|||||||
self.node)
|
self.node)
|
||||||
self.assertFalse(self.client._command.called)
|
self.assertFalse(self.client._command.called)
|
||||||
|
|
||||||
|
def test_finalize_rescue_fallback(self):
|
||||||
|
self.config(require_rescue_password_hashed=False, group="conductor")
|
||||||
|
self.client._command = mock.MagicMock(spec_set=[])
|
||||||
|
self.node.instance_info['rescue_password'] = 'password'
|
||||||
|
self.node.instance_info['hashed_rescue_password'] = '1234'
|
||||||
|
self.client._command.side_effect = [
|
||||||
|
exception.AgentAPIError('blah'),
|
||||||
|
('', '')]
|
||||||
|
self.client.finalize_rescue(self.node)
|
||||||
|
self.client._command.assert_has_calls([
|
||||||
|
mock.call(node=mock.ANY, method='rescue.finalize_rescue',
|
||||||
|
params={'rescue_password': '1234',
|
||||||
|
'hashed': True}),
|
||||||
|
mock.call(node=mock.ANY, method='rescue.finalize_rescue',
|
||||||
|
params={'rescue_password': 'password'})])
|
||||||
|
|
||||||
|
def test_finalize_rescue_fallback_restricted(self):
|
||||||
|
self.config(require_rescue_password_hashed=True, group="conductor")
|
||||||
|
self.client._command = mock.MagicMock(spec_set=[])
|
||||||
|
self.node.instance_info['rescue_password'] = 'password'
|
||||||
|
self.node.instance_info['hashed_rescue_password'] = '1234'
|
||||||
|
self.client._command.side_effect = exception.AgentAPIError('blah')
|
||||||
|
self.assertRaises(exception.InstanceRescueFailure,
|
||||||
|
self.client.finalize_rescue,
|
||||||
|
self.node)
|
||||||
|
self.client._command.assert_has_calls([
|
||||||
|
mock.call(node=mock.ANY, method='rescue.finalize_rescue',
|
||||||
|
params={'rescue_password': '1234',
|
||||||
|
'hashed': True})])
|
||||||
|
|
||||||
def test__command_agent_client(self):
|
def test__command_agent_client(self):
|
||||||
response_data = {'status': 'ok'}
|
response_data = {'status': 'ok'}
|
||||||
response_text = json.dumps(response_data)
|
response_text = json.dumps(response_data)
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Passwords for ``rescue`` operation are now hashed for
|
||||||
|
transmission to the ``ironic-python-agent``. This functionality
|
||||||
|
requires ``ironic-python-agent`` version ``6.0.0``.
|
||||||
|
|
||||||
|
The setting ``[conductor]rescue_password_hash_algorithm``
|
||||||
|
now defaults to ``sha256``, and may be set to
|
||||||
|
``sha256``, or ``sha512``.
|
||||||
|
upgrades:
|
||||||
|
- |
|
||||||
|
The version of ``ironic-python-agent`` should be upgraded to
|
||||||
|
at least version ``6.0.0`` for rescue passwords to be hashed
|
||||||
|
for transmission.
|
||||||
|
security:
|
||||||
|
- |
|
||||||
|
Operators wishing to enforce all rescue passwords to be hashed
|
||||||
|
should use the ``[conductor]require_rescue_password_hashed``
|
||||||
|
setting and set it to a value of ``True``.
|
||||||
|
|
||||||
|
This setting will be changed to a default of ``True`` in the
|
||||||
|
Victoria development cycle.
|
Loading…
Reference in New Issue
Block a user