Add ability to provide configdrive when rebuilding
Previously, the configdrive could only be set when setting the node's provisioning state to "active". When rebuilding, the old configdrive was used and therefore was never updated with latest content. This change introduces the API microversion 1.35 which will allow configdrive to be provided when setting the node's provisioning state to "rebuild". Closes-bug: #1575935 Change-Id: I9a5529f9fa796c75621e9f4354886bf3032cc248
This commit is contained in:
parent
7a2f3482d0
commit
6a8b38a2ca
@ -335,6 +335,9 @@ Acceptable target states depend on the Node's current provision state. More
|
||||
detailed documentation of the Ironic State Machine is available
|
||||
`in the developer docs <http://docs.openstack.org/ironic/latest/contributor/states.html>`_.
|
||||
|
||||
.. versionadded:: 1.35
|
||||
A ``configdrive`` can be provided when setting the node's provision target state to ``rebuild``.
|
||||
|
||||
Normal response code: 202
|
||||
|
||||
Error codes:
|
||||
|
@ -364,7 +364,7 @@ configdrive:
|
||||
description: |
|
||||
A gzip'ed and base-64 encoded config drive, to be written to a partition
|
||||
on the Node's boot disk. This parameter is only accepted when setting the
|
||||
state to "active".
|
||||
state to "active" or "rebuild".
|
||||
in: body
|
||||
required: false
|
||||
type: string or gzip+b64 blob
|
||||
|
@ -2,6 +2,12 @@
|
||||
REST API Version History
|
||||
========================
|
||||
|
||||
1.35 (Queens, 10.0.0)
|
||||
---------------------
|
||||
|
||||
Added ability to provide ``configdrive`` when node is updated
|
||||
to ``rebuild`` provision state.
|
||||
|
||||
1.34 (Pike, 9.0.0)
|
||||
------------------
|
||||
|
||||
|
@ -560,7 +560,7 @@ class NodeStatesController(rest.RestController):
|
||||
:param target: The desired provision state of the node or verb.
|
||||
:param configdrive: Optional. A gzipped and base64 encoded
|
||||
configdrive. Only valid when setting provision state
|
||||
to "active".
|
||||
to "active" or "rebuild".
|
||||
:param clean_steps: An ordered list of cleaning steps that will be
|
||||
performed on the node. A cleaning step is a dictionary with
|
||||
required keys 'interface' and 'step', and optional key 'args'. If
|
||||
@ -622,11 +622,8 @@ class NodeStatesController(rest.RestController):
|
||||
action=target, node=rpc_node.uuid,
|
||||
state=rpc_node.provision_state)
|
||||
|
||||
if configdrive and target != ir_states.ACTIVE:
|
||||
msg = (_('Adding a config drive is only supported when setting '
|
||||
'provision state to %s') % ir_states.ACTIVE)
|
||||
raise wsme.exc.ClientSideError(
|
||||
msg, status_code=http_client.BAD_REQUEST)
|
||||
if configdrive:
|
||||
api_utils.check_allow_configdrive(target)
|
||||
|
||||
if clean_steps and target != ir_states.VERBS['clean']:
|
||||
msg = (_('"clean_steps" is only valid when setting target '
|
||||
@ -637,14 +634,13 @@ class NodeStatesController(rest.RestController):
|
||||
# Note that there is a race condition. The node state(s) could change
|
||||
# by the time the RPC call is made and the TaskManager manager gets a
|
||||
# lock.
|
||||
if target == ir_states.ACTIVE:
|
||||
pecan.request.rpcapi.do_node_deploy(pecan.request.context,
|
||||
rpc_node.uuid, False,
|
||||
configdrive, topic)
|
||||
elif target == ir_states.REBUILD:
|
||||
pecan.request.rpcapi.do_node_deploy(pecan.request.context,
|
||||
rpc_node.uuid, True,
|
||||
None, topic)
|
||||
if target in (ir_states.ACTIVE, ir_states.REBUILD):
|
||||
rebuild = (target == ir_states.REBUILD)
|
||||
pecan.request.rpcapi.do_node_deploy(context=pecan.request.context,
|
||||
node_id=rpc_node.uuid,
|
||||
rebuild=rebuild,
|
||||
configdrive=configdrive,
|
||||
topic=topic)
|
||||
elif target == ir_states.DELETED:
|
||||
pecan.request.rpcapi.do_node_tear_down(
|
||||
pecan.request.context, rpc_node.uuid, topic)
|
||||
|
@ -392,6 +392,18 @@ def check_allow_driver_detail(detail):
|
||||
'opr': versions.MINOR_30_DYNAMIC_DRIVERS})
|
||||
|
||||
|
||||
def check_allow_configdrive(target):
|
||||
allowed_targets = [states.ACTIVE]
|
||||
if allow_node_rebuild_with_configdrive():
|
||||
allowed_targets.append(states.REBUILD)
|
||||
|
||||
if target not in allowed_targets:
|
||||
msg = (_('Adding a config drive is only supported when setting '
|
||||
'provision state to %s') % ', '.join(allowed_targets))
|
||||
raise wsme.exc.ClientSideError(
|
||||
msg, status_code=http_client.BAD_REQUEST)
|
||||
|
||||
|
||||
def initial_node_provision_state():
|
||||
"""Return node state to use by default when creating new nodes.
|
||||
|
||||
@ -581,6 +593,15 @@ def allow_port_physical_network():
|
||||
objects.Port.supports_physical_network())
|
||||
|
||||
|
||||
def allow_node_rebuild_with_configdrive():
|
||||
"""Check if we should support node rebuild with configdrive.
|
||||
|
||||
Version 1.35 of the API added support for node rebuild with configdrive.
|
||||
"""
|
||||
return (pecan.request.version.minor >=
|
||||
versions.MINOR_35_REBUILD_CONFIG_DRIVE)
|
||||
|
||||
|
||||
def get_controller_reserved_names(cls):
|
||||
"""Get reserved names for a given controller.
|
||||
|
||||
|
@ -65,6 +65,7 @@ BASE_VERSION = 1
|
||||
# v1.32: Add volume support.
|
||||
# v1.33: Add node storage interface
|
||||
# v1.34: Add physical network field to port.
|
||||
# v1.35: Add ability to provide configdrive when rebuilding node.
|
||||
|
||||
MINOR_0_JUNO = 0
|
||||
MINOR_1_INITIAL_VERSION = 1
|
||||
@ -101,11 +102,12 @@ MINOR_31_DYNAMIC_INTERFACES = 31
|
||||
MINOR_32_VOLUME = 32
|
||||
MINOR_33_STORAGE_INTERFACE = 33
|
||||
MINOR_34_PORT_PHYSICAL_NETWORK = 34
|
||||
MINOR_35_REBUILD_CONFIG_DRIVE = 35
|
||||
|
||||
# When adding another version, update MINOR_MAX_VERSION and also update
|
||||
# doc/source/dev/webapi-version-history.rst with a detailed explanation of
|
||||
# what the version has changed.
|
||||
MINOR_MAX_VERSION = MINOR_34_PORT_PHYSICAL_NETWORK
|
||||
MINOR_MAX_VERSION = MINOR_35_REBUILD_CONFIG_DRIVE
|
||||
|
||||
# String representations of the minor and maximum versions
|
||||
MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)
|
||||
|
@ -2966,8 +2966,11 @@ class TestPut(test_api_base.BaseApiTest):
|
||||
{'target': states.ACTIVE})
|
||||
self.assertEqual(http_client.ACCEPTED, ret.status_code)
|
||||
self.assertEqual(b'', ret.body)
|
||||
self.mock_dnd.assert_called_once_with(
|
||||
mock.ANY, self.node.uuid, False, None, 'test-topic')
|
||||
self.mock_dnd.assert_called_once_with(context=mock.ANY,
|
||||
node_id=self.node.uuid,
|
||||
rebuild=False,
|
||||
configdrive=None,
|
||||
topic='test-topic')
|
||||
# Check location header
|
||||
self.assertIsNotNone(ret.location)
|
||||
expected_location = '/v1/nodes/%s/states' % self.node.uuid
|
||||
@ -2986,16 +2989,74 @@ class TestPut(test_api_base.BaseApiTest):
|
||||
headers={api_base.Version.string: "1.5"})
|
||||
self.assertEqual(http_client.ACCEPTED, ret.status_code)
|
||||
self.assertEqual(b'', ret.body)
|
||||
self.mock_dnd.assert_called_once_with(
|
||||
mock.ANY, self.node.uuid, False, None, 'test-topic')
|
||||
|
||||
self.mock_dnd.assert_called_once_with(context=mock.ANY,
|
||||
node_id=self.node.uuid,
|
||||
rebuild=False,
|
||||
configdrive=None,
|
||||
topic='test-topic')
|
||||
|
||||
def test_provision_with_deploy_configdrive(self):
|
||||
ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
|
||||
{'target': states.ACTIVE, 'configdrive': 'foo'})
|
||||
self.assertEqual(http_client.ACCEPTED, ret.status_code)
|
||||
self.assertEqual(b'', ret.body)
|
||||
self.mock_dnd.assert_called_once_with(
|
||||
mock.ANY, self.node.uuid, False, 'foo', 'test-topic')
|
||||
self.mock_dnd.assert_called_once_with(context=mock.ANY,
|
||||
node_id=self.node.uuid,
|
||||
rebuild=False,
|
||||
configdrive='foo',
|
||||
topic='test-topic')
|
||||
# Check location header
|
||||
self.assertIsNotNone(ret.location)
|
||||
expected_location = '/v1/nodes/%s/states' % self.node.uuid
|
||||
self.assertEqual(urlparse.urlparse(ret.location).path,
|
||||
expected_location)
|
||||
|
||||
def test_provision_with_rebuild(self):
|
||||
node = self.node
|
||||
node.provision_state = states.ACTIVE
|
||||
node.target_provision_state = states.NOSTATE
|
||||
node.save()
|
||||
ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
|
||||
{'target': states.REBUILD})
|
||||
self.assertEqual(http_client.ACCEPTED, ret.status_code)
|
||||
self.assertEqual(b'', ret.body)
|
||||
self.mock_dnd.assert_called_once_with(context=mock.ANY,
|
||||
node_id=self.node.uuid,
|
||||
rebuild=True,
|
||||
configdrive=None,
|
||||
topic='test-topic')
|
||||
# Check location header
|
||||
self.assertIsNotNone(ret.location)
|
||||
expected_location = '/v1/nodes/%s/states' % self.node.uuid
|
||||
self.assertEqual(urlparse.urlparse(ret.location).path,
|
||||
expected_location)
|
||||
|
||||
def test_provision_with_rebuild_unsupported_configdrive(self):
|
||||
node = self.node
|
||||
node.provision_state = states.ACTIVE
|
||||
node.target_provision_state = states.NOSTATE
|
||||
node.save()
|
||||
ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
|
||||
{'target': states.REBUILD, 'configdrive': 'foo'},
|
||||
expect_errors=True)
|
||||
self.assertEqual(http_client.BAD_REQUEST, ret.status_code)
|
||||
|
||||
def test_provision_with_rebuild_configdrive(self):
|
||||
node = self.node
|
||||
node.provision_state = states.ACTIVE
|
||||
node.target_provision_state = states.NOSTATE
|
||||
node.save()
|
||||
ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
|
||||
{'target': states.REBUILD, 'configdrive': 'foo'},
|
||||
headers={api_base.Version.string: '1.35'})
|
||||
self.assertEqual(http_client.ACCEPTED, ret.status_code)
|
||||
self.assertEqual(b'', ret.body)
|
||||
self.mock_dnd.assert_called_once_with(context=mock.ANY,
|
||||
node_id=self.node.uuid,
|
||||
rebuild=True,
|
||||
configdrive='foo',
|
||||
topic='test-topic')
|
||||
# Check location header
|
||||
self.assertIsNotNone(ret.location)
|
||||
expected_location = '/v1/nodes/%s/states' % self.node.uuid
|
||||
@ -3081,8 +3142,11 @@ class TestPut(test_api_base.BaseApiTest):
|
||||
{'target': states.ACTIVE})
|
||||
self.assertEqual(http_client.ACCEPTED, ret.status_code)
|
||||
self.assertEqual(b'', ret.body)
|
||||
self.mock_dnd.assert_called_once_with(
|
||||
mock.ANY, node.uuid, False, None, 'test-topic')
|
||||
self.mock_dnd.assert_called_once_with(context=mock.ANY,
|
||||
node_id=self.node.uuid,
|
||||
rebuild=False,
|
||||
configdrive=None,
|
||||
topic='test-topic')
|
||||
# Check location header
|
||||
self.assertIsNotNone(ret.location)
|
||||
expected_location = '/v1/nodes/%s/states' % node.uuid
|
||||
|
@ -25,6 +25,7 @@ import wsme
|
||||
from ironic.api.controllers.v1 import node as api_node
|
||||
from ironic.api.controllers.v1 import utils
|
||||
from ironic.common import exception
|
||||
from ironic.common import states
|
||||
from ironic import objects
|
||||
from ironic.tests import base
|
||||
from ironic.tests.unit.api import utils as test_api_utils
|
||||
@ -444,6 +445,30 @@ class TestApiUtils(base.TestCase):
|
||||
mock_request.version.minor = 33
|
||||
self.assertFalse(utils.allow_port_physical_network())
|
||||
|
||||
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||
def test_allow_node_rebuild_with_configdrive(self, mock_request):
|
||||
mock_request.version.minor = 35
|
||||
self.assertTrue(utils.allow_node_rebuild_with_configdrive())
|
||||
mock_request.version.minor = 34
|
||||
self.assertFalse(utils.allow_node_rebuild_with_configdrive())
|
||||
|
||||
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||
def test_check_allow_configdrive_fails(self, mock_request):
|
||||
mock_request.version.minor = 35
|
||||
self.assertRaises(wsme.exc.ClientSideError,
|
||||
utils.check_allow_configdrive, states.DELETED)
|
||||
mock_request.version.minor = 34
|
||||
self.assertRaises(wsme.exc.ClientSideError,
|
||||
utils.check_allow_configdrive, states.REBUILD)
|
||||
|
||||
@mock.patch.object(pecan, 'request', spec_set=['version'])
|
||||
def test_check_allow_configdrive(self, mock_request):
|
||||
mock_request.version.minor = 35
|
||||
utils.check_allow_configdrive(states.ACTIVE)
|
||||
utils.check_allow_configdrive(states.REBUILD)
|
||||
mock_request.version.minor = 34
|
||||
utils.check_allow_configdrive(states.ACTIVE)
|
||||
|
||||
|
||||
class TestNodeIdent(base.TestCase):
|
||||
|
||||
|
10
releasenotes/notes/rebuild-configdrive-f52479fd55b0f5ce.yaml
Normal file
10
releasenotes/notes/rebuild-configdrive-f52479fd55b0f5ce.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Starting with REST API version 1.35, it is possible to provide
|
||||
a configdrive when rebuilding a node.
|
||||
fixes:
|
||||
- |
|
||||
Fixes the problem of an old configdrive (used for deploying the node)
|
||||
being used again when rebuilding the node. Starting with REST API 1.35,
|
||||
it is possible to specify a different configdrive when rebuilding a node.
|
Loading…
x
Reference in New Issue
Block a user