Merge "Completely remove support for setting IPMI credentials"

This commit is contained in:
Jenkins 2017-05-31 15:04:49 +00:00 committed by Gerrit Code Review
commit 095b4e3d72
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

@ -38,10 +38,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',)
@ -204,23 +202,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

@ -55,8 +55,6 @@ os_password = password
os_tenant_name = tenant
[firewall]
manage_firewall = False
[processing]
enable_setting_ipmi_credentials = True
[DEFAULT]
debug = True
auth_strategy = noauth
@ -164,13 +162,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):
@ -310,37 +303,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

@ -55,38 +55,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")
@ -97,7 +67,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.