Completely remove support for setting IPMI credentials

This experimental feature was deprecated in the Ocata release,
as it was found unstable, untested and dangerous.

API version is bumped to 1.12 to indicate this change to users.

Change-Id: I1aad6ddfd03946edc19ae510accd6c8daf5fc268
Closes-Bug: #1654318
This commit is contained in:
Dmitry Tantsur 2017-05-19 10:58:13 +02:00
parent 7a0be9aeb0
commit e05257035c
12 changed files with 49 additions and 384 deletions

View File

@ -15,11 +15,6 @@ done prior to calling the endpoint.
Requires X-Auth-Token header with Keystone token for authentication.
Deprecated parameters (only available in API before version ``1.9``):
* ``new_ipmi_password``
* ``new_ipmi_username``
Response:
* 202 - accepted introspection request
@ -289,12 +284,7 @@ Response:
* 403 - node is not on introspection
* 404 - node cannot be found or multiple nodes found
Response body: JSON dictionary. If setting IPMI credentials (deprecated
feature) is requested, body will contain the following keys:
* ``ipmi_setup_credentials`` boolean ``True``
* ``ipmi_username`` new IPMI user name
* ``ipmi_password`` new IPMI password
Response body: JSON dictionary with ``uuid`` key.
.. _hardware inventory: http://docs.openstack.org/developer/ironic-python-agent/#hardware-inventory
.. _Specifying the disk for deployment root device hints:
@ -398,3 +388,5 @@ Version History
* **1.10** adds node state to the GET /v1/introspection/<Node ID> and
GET /v1/introspection API response data.
* **1.11** adds invert&multiple fields into rules response data
* **1.12** this version indicates that support for setting IPMI credentials
was completely removed from API (all versions).

View File

@ -320,7 +320,6 @@ These steps are avoided, based on the feature requirements:
Limitations:
* IPMI credentials are not updated --- ramdisk not running
* there's no way to update the unprocessed data atm.
* the unprocessed data is never cleaned from the store
* check for stored data presence is performed in background;

View File

@ -27,7 +27,6 @@ LOG = utils.getProcessingLogger(__name__)
# See http://specs.openstack.org/openstack/ironic-specs/specs/kilo/new-ironic-state-machine.html # noqa
VALID_STATES = {'enroll', 'manageable', 'inspecting', 'inspect failed'}
SET_CREDENTIALS_VALID_STATES = {'enroll'}
# 1.19 is API version, which supports port.pxe_enabled
DEFAULT_IRONIC_API_VERSION = '1.19'
@ -143,15 +142,9 @@ def get_client(token=None,
return client.Client(1, **args)
def check_provision_state(node, with_credentials=False):
def check_provision_state(node):
state = node.provision_state.lower()
if with_credentials and state not in SET_CREDENTIALS_VALID_STATES:
msg = _('Invalid provision state for setting IPMI credentials: '
'"%(state)s", valid states are %(valid)s')
raise utils.Error(msg % {'state': state,
'valid': list(SET_CREDENTIALS_VALID_STATES)},
node_info=node)
elif not with_credentials and state not in VALID_STATES:
if state not in VALID_STATES:
msg = _('Invalid provision state for introspection: '
'"%(state)s", valid states are "%(valid)s"')
raise utils.Error(msg % {'state': state, 'valid': list(VALID_STATES)},

View File

@ -76,12 +76,6 @@ PROCESSING_OPTS = [
help=_('Whether to overwrite existing values in node '
'database. Disable this option to make '
'introspection a non-destructive operation.')),
cfg.BoolOpt('enable_setting_ipmi_credentials',
default=False,
help=_('Whether to enable setting IPMI credentials during '
'introspection. This feature will be removed in the '
'Pike release.'),
deprecated_for_removal=True),
cfg.StrOpt('default_processing_hooks',
default='ramdisk_error,root_disk_selection,scheduler,'
'validate_interfaces,capabilities,pci_devices',

View File

@ -14,7 +14,6 @@
"""Handling introspection request."""
import re
import string
import time
from eventlet import semaphore
@ -31,67 +30,32 @@ CONF = cfg.CONF
LOG = utils.getProcessingLogger(__name__)
PASSWORD_ACCEPTED_CHARS = set(string.ascii_letters + string.digits)
PASSWORD_MAX_LENGTH = 20 # IPMI v2.0
_LAST_INTROSPECTION_TIME = 0
_LAST_INTROSPECTION_LOCK = semaphore.BoundedSemaphore()
def _validate_ipmi_credentials(node, new_ipmi_credentials):
if not CONF.processing.enable_setting_ipmi_credentials:
raise utils.Error(
_('IPMI credentials setup is disabled in configuration'))
new_username, new_password = new_ipmi_credentials
if not new_username:
new_username = node.driver_info.get('ipmi_username')
if not new_username:
raise utils.Error(_('Setting IPMI credentials requested, but neither '
'new user name nor driver_info[ipmi_username] '
'are provided'),
node_info=node)
wrong_chars = {c for c in new_password
if c not in PASSWORD_ACCEPTED_CHARS}
if wrong_chars:
raise utils.Error(_('Forbidden characters encountered in new IPMI '
'password: "%s"; use only letters and numbers')
% ''.join(wrong_chars), node_info=node)
if not 0 < len(new_password) <= PASSWORD_MAX_LENGTH:
raise utils.Error(_('IPMI password length should be > 0 and <= %d')
% PASSWORD_MAX_LENGTH, node_info=node)
return new_username, new_password
def introspect(node_id, new_ipmi_credentials=None, token=None):
def introspect(node_id, token=None):
"""Initiate hardware properties introspection for a given node.
:param node_id: node UUID or name
:param new_ipmi_credentials: tuple (new username, new password) or None
:param token: authentication token
:raises: Error
"""
ironic = ir_utils.get_client(token)
node = ir_utils.get_node(node_id, ironic=ironic)
ir_utils.check_provision_state(node, with_credentials=new_ipmi_credentials)
if new_ipmi_credentials:
new_ipmi_credentials = (
_validate_ipmi_credentials(node, new_ipmi_credentials))
else:
validation = ironic.node.validate(node.uuid)
if not validation.power['result']:
msg = _('Failed validation of power interface, reason: %s')
raise utils.Error(msg % validation.power['reason'],
node_info=node)
ir_utils.check_provision_state(node)
validation = ironic.node.validate(node.uuid)
if not validation.power['result']:
msg = _('Failed validation of power interface, reason: %s')
raise utils.Error(msg % validation.power['reason'],
node_info=node)
bmc_address = ir_utils.get_ipmi_address(node)
node_info = node_cache.start_introspection(node.uuid,
bmc_address=bmc_address,
ironic=ironic)
node_info.set_option('new_ipmi_credentials', new_ipmi_credentials)
def _handle_exceptions(fut):
try:
@ -111,17 +75,16 @@ def introspect(node_id, new_ipmi_credentials=None, token=None):
def _background_introspect(ironic, node_info):
global _LAST_INTROSPECTION_TIME
if not node_info.options.get('new_ipmi_credentials'):
if re.match(CONF.introspection_delay_drivers, node_info.node().driver):
LOG.debug('Attempting to acquire lock on last introspection time')
with _LAST_INTROSPECTION_LOCK:
delay = (_LAST_INTROSPECTION_TIME - time.time()
+ CONF.introspection_delay)
if delay > 0:
LOG.debug('Waiting %d seconds before sending the next '
'node on introspection', delay)
time.sleep(delay)
_LAST_INTROSPECTION_TIME = time.time()
if re.match(CONF.introspection_delay_drivers, node_info.node().driver):
LOG.debug('Attempting to acquire lock on last introspection time')
with _LAST_INTROSPECTION_LOCK:
delay = (_LAST_INTROSPECTION_TIME - time.time()
+ CONF.introspection_delay)
if delay > 0:
LOG.debug('Waiting %d seconds before sending the next '
'node on introspection', delay)
time.sleep(delay)
_LAST_INTROSPECTION_TIME = time.time()
node_info.acquire_lock()
try:
@ -151,26 +114,21 @@ def _background_introspect_locked(node_info, ironic):
LOG.info('The following attributes will be used for look up: %s',
attrs, node_info=node_info)
if not node_info.options.get('new_ipmi_credentials'):
try:
ironic.node.set_boot_device(node_info.uuid, 'pxe',
persistent=False)
except Exception as exc:
LOG.warning('Failed to set boot device to PXE: %s',
exc, node_info=node_info)
try:
ironic.node.set_boot_device(node_info.uuid, 'pxe',
persistent=False)
except Exception as exc:
LOG.warning('Failed to set boot device to PXE: %s',
exc, node_info=node_info)
try:
ironic.node.set_power_state(node_info.uuid, 'reboot')
except Exception as exc:
raise utils.Error(_('Failed to power on the node, check it\'s '
'power management configuration: %s'),
exc, node_info=node_info)
LOG.info('Introspection started successfully',
node_info=node_info)
else:
LOG.info('Introspection environment is ready, manual power on is '
'required within %d seconds', CONF.timeout,
node_info=node_info)
try:
ironic.node.set_power_state(node_info.uuid, 'reboot')
except Exception as exc:
raise utils.Error(_('Failed to power on the node, check it\'s '
'power management configuration: %s'),
exc, node_info=node_info)
LOG.info('Introspection started successfully',
node_info=node_info)
def abort(node_id, token=None):

View File

@ -48,10 +48,8 @@ app = flask.Flask(__name__)
LOG = utils.getProcessingLogger(__name__)
MINIMUM_API_VERSION = (1, 0)
# TODO(dtantsur): set to the current version as soon we move setting IPMI
# credentials support completely.
DEFAULT_API_VERSION = (1, 8)
CURRENT_API_VERSION = (1, 11)
CURRENT_API_VERSION = (1, 12)
DEFAULT_API_VERSION = CURRENT_API_VERSION
_LOGGING_EXCLUDED_KEYS = ('logs',)
@ -214,23 +212,7 @@ def api_introspection(node_id):
utils.check_auth(flask.request)
if flask.request.method == 'POST':
new_ipmi_password = flask.request.args.get('new_ipmi_password',
type=str,
default=None)
if new_ipmi_password:
new_ipmi_username = flask.request.args.get('new_ipmi_username',
type=str,
default=None)
new_ipmi_credentials = (new_ipmi_username, new_ipmi_password)
else:
new_ipmi_credentials = None
if new_ipmi_credentials and _get_version() >= (1, 9):
return _('Setting IPMI credentials is deprecated and not allowed '
'starting with API version 1.9'), 400
introspect.introspect(node_id,
new_ipmi_credentials=new_ipmi_credentials,
token=flask.request.headers.get('X-Auth-Token'))
return '', 202
else:

View File

@ -18,7 +18,6 @@ import datetime
import json
import os
import eventlet
from oslo_config import cfg
from oslo_serialization import base64
from oslo_utils import excutils
@ -38,8 +37,6 @@ CONF = cfg.CONF
LOG = utils.getProcessingLogger(__name__)
_CREDENTIALS_WAIT_RETRIES = 10
_CREDENTIALS_WAIT_PERIOD = 3
_STORAGE_EXCLUDED_KEYS = {'logs'}
_UNPROCESSED_DATA_STORE_SUFFIX = 'UNPROCESSED'
@ -279,56 +276,12 @@ def _process_node(node_info, node, introspection_data):
resp = {'uuid': node.uuid}
if node_info.options.get('new_ipmi_credentials'):
new_username, new_password = (
node_info.options.get('new_ipmi_credentials'))
utils.executor().submit(_finish_set_ipmi_credentials,
node_info, ironic, node, introspection_data,
new_username, new_password)
resp['ipmi_setup_credentials'] = True
resp['ipmi_username'] = new_username
resp['ipmi_password'] = new_password
else:
utils.executor().submit(_finish, node_info, ironic, introspection_data,
power_off=CONF.processing.power_off)
utils.executor().submit(_finish, node_info, ironic, introspection_data,
power_off=CONF.processing.power_off)
return resp
@node_cache.fsm_transition(istate.Events.finish)
def _finish_set_ipmi_credentials(node_info, ironic, node, introspection_data,
new_username, new_password):
patch = [{'op': 'add', 'path': '/driver_info/ipmi_username',
'value': new_username},
{'op': 'add', 'path': '/driver_info/ipmi_password',
'value': new_password}]
new_ipmi_address = utils.get_ipmi_address_from_data(introspection_data)
if not ir_utils.get_ipmi_address(node) and new_ipmi_address:
patch.append({'op': 'add', 'path': '/driver_info/ipmi_address',
'value': new_ipmi_address})
node_info.patch(patch)
for attempt in range(_CREDENTIALS_WAIT_RETRIES):
try:
# We use this call because it requires valid credentials.
# We don't care about boot device, obviously.
ironic.node.get_boot_device(node_info.uuid)
except Exception as exc:
LOG.info('Waiting for credentials update, attempt %(attempt)d '
'current error is %(exc)s',
{'attempt': attempt, 'exc': exc},
node_info=node_info, data=introspection_data)
eventlet.greenthread.sleep(_CREDENTIALS_WAIT_PERIOD)
else:
_finish_common(node_info, ironic, introspection_data)
return
msg = (_('Failed to validate updated IPMI credentials for node '
'%s, node might require maintenance') % node_info.uuid)
node_info.finished(error=msg)
raise utils.Error(msg, node_info=node_info, data=introspection_data)
def _finish_common(node_info, ironic, introspection_data, power_off=True):
if power_off:
LOG.debug('Forcing power off of node %s', node_info.uuid)

View File

@ -54,8 +54,6 @@ os_password = password
os_tenant_name = tenant
[firewall]
manage_firewall = False
[processing]
enable_setting_ipmi_credentials = True
[DEFAULT]
debug = True
auth_strategy = noauth
@ -163,13 +161,8 @@ class Base(base.NodeTest):
raise AssertionError(msg)
return res
def call_introspect(self, uuid, new_ipmi_username=None,
new_ipmi_password=None, **kwargs):
def call_introspect(self, uuid, **kwargs):
endpoint = '/v1/introspection/%s' % uuid
if new_ipmi_password:
endpoint += '?new_ipmi_password=%s' % new_ipmi_password
if new_ipmi_username:
endpoint += '&new_ipmi_username=%s' % new_ipmi_username
return self.call('post', endpoint, **kwargs)
def call_get_status(self, uuid, **kwargs):
@ -309,37 +302,6 @@ class Test(Base):
status = self.call_get_status(self.uuid)
self.check_status(status, finished=True, state=istate.States.finished)
def test_setup_ipmi(self):
patch_credentials = [
{'op': 'add', 'path': '/driver_info/ipmi_username',
'value': 'admin'},
{'op': 'add', 'path': '/driver_info/ipmi_password',
'value': 'pwd'},
]
self.node.provision_state = 'enroll'
self.call_introspect(self.uuid, new_ipmi_username='admin',
new_ipmi_password='pwd')
eventlet.greenthread.sleep(DEFAULT_SLEEP)
self.assertFalse(self.cli.node.set_power_state.called)
status = self.call_get_status(self.uuid)
self.check_status(status, finished=False, state=istate.States.waiting)
res = self.call_continue(self.data)
self.assertEqual('admin', res['ipmi_username'])
self.assertEqual('pwd', res['ipmi_password'])
self.assertTrue(res['ipmi_setup_credentials'])
eventlet.greenthread.sleep(DEFAULT_SLEEP)
self.assertCalledWithPatch(self.patch + patch_credentials,
self.cli.node.update)
self.cli.port.create.assert_called_once_with(
node_uuid=self.uuid, address='11:22:33:44:55:66', extra={},
pxe_enabled=True)
status = self.call_get_status(self.uuid)
self.check_status(status, finished=True, state=istate.States.finished)
def test_introspection_statuses(self):
self.call_introspect(self.uuid)
eventlet.greenthread.sleep(DEFAULT_SLEEP)

View File

@ -72,8 +72,6 @@ class TestIntrospect(BaseTest):
persistent=False)
cli.node.set_power_state.assert_called_once_with(self.uuid,
'reboot')
self.node_info.set_option.assert_called_once_with(
'new_ipmi_credentials', None)
self.node_info.acquire_lock.assert_called_once_with()
self.node_info.release_lock.assert_called_once_with()
@ -99,8 +97,6 @@ class TestIntrospect(BaseTest):
persistent=False)
cli.node.set_power_state.assert_called_once_with(self.uuid,
'reboot')
self.node_info.set_option.assert_called_once_with(
'new_ipmi_credentials', None)
self.node_info.acquire_lock.assert_called_once_with()
self.node_info.release_lock.assert_called_once_with()
@ -332,89 +328,6 @@ class TestIntrospect(BaseTest):
self.assertEqual(42, introspect._LAST_INTROSPECTION_TIME)
@mock.patch.object(firewall, 'update_filters', autospec=True)
@mock.patch.object(node_cache, 'start_introspection', autospec=True)
@mock.patch.object(ir_utils, 'get_client', autospec=True)
class TestSetIpmiCredentials(BaseTest):
def setUp(self):
super(TestSetIpmiCredentials, self).setUp()
CONF.set_override('enable_setting_ipmi_credentials', True,
'processing')
self.new_creds = ('user', 'password')
self.node_info.options['new_ipmi_credentials'] = self.new_creds
self.node.provision_state = 'enroll'
def test_ok(self, client_mock, start_mock, filters_mock):
cli = self._prepare(client_mock)
start_mock.return_value = self.node_info
introspect.introspect(self.uuid, new_ipmi_credentials=self.new_creds)
start_mock.assert_called_once_with(self.uuid,
bmc_address=self.bmc_address,
ironic=cli)
filters_mock.assert_called_with(cli)
self.assertFalse(cli.node.validate.called)
self.assertFalse(cli.node.set_boot_device.called)
self.assertFalse(cli.node.set_power_state.called)
start_mock.return_value.set_option.assert_called_once_with(
'new_ipmi_credentials', self.new_creds)
def test_disabled(self, client_mock, start_mock, filters_mock):
CONF.set_override('enable_setting_ipmi_credentials', False,
'processing')
self._prepare(client_mock)
self.assertRaisesRegex(utils.Error, 'disabled',
introspect.introspect, self.uuid,
new_ipmi_credentials=self.new_creds)
def test_no_username(self, client_mock, start_mock, filters_mock):
self._prepare(client_mock)
self.assertRaises(utils.Error, introspect.introspect, self.uuid,
new_ipmi_credentials=(None, 'password'))
def test_default_username(self, client_mock, start_mock, filters_mock):
cli = self._prepare(client_mock)
start_mock.return_value = self.node_info
self.node.driver_info['ipmi_username'] = self.new_creds[0]
introspect.introspect(self.uuid,
new_ipmi_credentials=(None, self.new_creds[1]))
start_mock.assert_called_once_with(self.uuid,
bmc_address=self.bmc_address,
ironic=cli)
filters_mock.assert_called_with(cli)
self.assertFalse(cli.node.validate.called)
self.assertFalse(cli.node.set_boot_device.called)
self.assertFalse(cli.node.set_power_state.called)
start_mock.return_value.set_option.assert_called_once_with(
'new_ipmi_credentials', self.new_creds)
def test_wrong_letters(self, client_mock, start_mock, filters_mock):
self.new_creds = ('user', 'p ssw@rd')
self._prepare(client_mock)
self.assertRaises(utils.Error, introspect.introspect, self.uuid,
new_ipmi_credentials=self.new_creds)
def test_too_long(self, client_mock, start_mock, filters_mock):
self.new_creds = ('user', 'password' * 100)
self._prepare(client_mock)
self.assertRaises(utils.Error, introspect.introspect, self.uuid,
new_ipmi_credentials=self.new_creds)
def test_wrong_state(self, client_mock, start_mock, filters_mock):
self.node.provision_state = 'manageable'
self._prepare(client_mock)
self.assertRaises(utils.Error, introspect.introspect, self.uuid,
new_ipmi_credentials=self.new_creds)
@mock.patch.object(firewall, 'update_filters', autospec=True)
@mock.patch.object(node_cache, 'get_node', autospec=True)
@mock.patch.object(ir_utils, 'get_client', autospec=True)

View File

@ -59,38 +59,8 @@ class TestApiIntrospect(BaseAPITest):
res = self.app.post('/v1/introspection/%s' % self.uuid)
self.assertEqual(202, res.status_code)
introspect_mock.assert_called_once_with(self.uuid,
new_ipmi_credentials=None,
token=None)
@mock.patch.object(introspect, 'introspect', autospec=True)
def test_introspect_set_ipmi_credentials(self, introspect_mock):
res = self.app.post('/v1/introspection/%s?new_ipmi_username=user&'
'new_ipmi_password=password' % self.uuid)
self.assertEqual(202, res.status_code)
introspect_mock.assert_called_once_with(
self.uuid,
new_ipmi_credentials=('user', 'password'),
token=None)
@mock.patch.object(introspect, 'introspect', autospec=True)
def test_introspect_set_ipmi_credentials_disabled(self, introspect_mock):
headers = {conf.VERSION_HEADER: '1.9'}
res = self.app.post('/v1/introspection/%s?new_ipmi_username=user&'
'new_ipmi_password=password' % self.uuid,
headers=headers)
self.assertEqual(400, res.status_code)
self.assertFalse(introspect_mock.called)
@mock.patch.object(introspect, 'introspect', autospec=True)
def test_introspect_set_ipmi_credentials_no_user(self, introspect_mock):
res = self.app.post('/v1/introspection/%s?'
'new_ipmi_password=password' % self.uuid)
self.assertEqual(202, res.status_code)
introspect_mock.assert_called_once_with(
self.uuid,
new_ipmi_credentials=(None, 'password'),
token=None)
@mock.patch.object(introspect, 'introspect', autospec=True)
def test_intospect_failed(self, introspect_mock):
introspect_mock.side_effect = utils.Error("boom")
@ -101,7 +71,6 @@ class TestApiIntrospect(BaseAPITest):
json.loads(res.data.decode('utf-8'))['error']['message'])
introspect_mock.assert_called_once_with(
self.uuid,
new_ipmi_credentials=None,
token=None)
@mock.patch.object(utils, 'check_auth', autospec=True)

View File

@ -353,14 +353,6 @@ class TestProcessNode(BaseTest):
self.data['interfaces'] = self.valid_interfaces
self.ports = self.all_ports
self.new_creds = ('user', 'password')
self.patch_credentials = [
{'op': 'add', 'path': '/driver_info/ipmi_username',
'value': self.new_creds[0]},
{'op': 'add', 'path': '/driver_info/ipmi_password',
'value': self.new_creds[1]},
]
self.cli.node.get_boot_device.side_effect = (
[RuntimeError()] * self.validate_attempts + [None])
self.cli.port.create.side_effect = self.ports
@ -382,12 +374,6 @@ class TestProcessNode(BaseTest):
ret_val = process._process_node(self.node_info, self.node, self.data)
self.assertEqual(self.uuid, ret_val.get('uuid'))
def test_return_includes_uuid_with_ipmi_creds(self):
self.node_info.set_option('new_ipmi_credentials', self.new_creds)
ret_val = process._process_node(self.node_info, self.node, self.data)
self.assertEqual(self.uuid, ret_val.get('uuid'))
self.assertTrue(ret_val.get('ipmi_setup_credentials'))
@mock.patch.object(example_plugin.ExampleProcessingHook, 'before_update')
def test_wrong_provision_state(self, post_hook_mock):
self.node.provision_state = 'active'
@ -428,49 +414,6 @@ class TestProcessNode(BaseTest):
address=self.macs[1],
extra={}, pxe_enabled=False)
def test_set_ipmi_credentials(self):
self.node_info.set_option('new_ipmi_credentials', self.new_creds)
process._process_node(self.node_info, self.node, self.data)
self.cli.node.update.assert_any_call(self.uuid, self.patch_credentials)
self.cli.node.set_power_state.assert_called_once_with(self.uuid, 'off')
self.cli.node.get_boot_device.assert_called_with(self.uuid)
self.assertEqual(self.validate_attempts + 1,
self.cli.node.get_boot_device.call_count)
def test_set_ipmi_credentials_no_address(self):
self.node_info.set_option('new_ipmi_credentials', self.new_creds)
del self.node.driver_info['ipmi_address']
self.patch_credentials.append({'op': 'add',
'path': '/driver_info/ipmi_address',
'value': self.bmc_address})
process._process_node(self.node_info, self.node, self.data)
self.cli.node.update.assert_any_call(self.uuid, self.patch_credentials)
self.cli.node.set_power_state.assert_called_once_with(self.uuid, 'off')
self.cli.node.get_boot_device.assert_called_with(self.uuid)
self.assertEqual(self.validate_attempts + 1,
self.cli.node.get_boot_device.call_count)
@mock.patch.object(node_cache.NodeInfo, 'finished', autospec=True)
def test_set_ipmi_credentials_timeout(self, finished_mock):
self.node_info.set_option('new_ipmi_credentials', self.new_creds)
self.cli.node.get_boot_device.side_effect = RuntimeError('boom')
process._process_node(self.node_info, self.node, self.data)
self.cli.node.update.assert_any_call(self.uuid, self.patch_credentials)
self.assertEqual(2, self.cli.node.update.call_count)
self.assertEqual(process._CREDENTIALS_WAIT_RETRIES,
self.cli.node.get_boot_device.call_count)
self.assertFalse(self.cli.node.set_power_state.called)
finished_mock.assert_called_once_with(
mock.ANY,
error='Failed to validate updated IPMI credentials for node %s, '
'node might require maintenance' % self.uuid)
@mock.patch.object(node_cache.NodeInfo, 'finished', autospec=True)
def test_power_off_failed(self, finished_mock):
self.cli.node.set_power_state.side_effect = RuntimeError('boom')
@ -609,7 +552,6 @@ class TestReapplyNode(BaseTest):
started_at=self.started_at,
node=self.node)
self.node_info.invalidate_cache = mock.Mock()
self.new_creds = ('user', 'password')
self.cli.port.create.side_effect = self.ports
self.cli.node.update.return_value = self.node

View File

@ -0,0 +1,8 @@
---
upgrade:
- |
Experimental setting IPMI credentials support was removed from all versions
of the API. The current API version was bumped to 1.12 to mark this change.
- |
The default API version was synchronized with the current API version again
after removal of the IPMI credentials setting.