Allow vendor_data to be included in a configdrive dict

configdrive can contain a vendor_data2.json file containing key/value
pairs injected by nova's vendordata mechanism[1].

This change lets Ironic accept a vendor_data key when configdrive is
provided as json, allowing parity with nova.

This change requires an openstacksdk release 0.37.0

[1] https://www.madebymikal.com/nova-vendordata-deployment-an-excessively-detailed-guide/

Change-Id: Id990b970619a113c5d5ead47fb550870d91b5e04
Task: 36756
Story: 2006597
Blueprint: nova-less-deploy
This commit is contained in:
Steve Baker 2019-09-23 05:16:05 +00:00
parent b462ca420b
commit 7ebad2e344
12 changed files with 97 additions and 27 deletions

View File

@ -561,6 +561,7 @@ configdrive:
* ``network_data`` (optional) - JSON object with networking configuration. * ``network_data`` (optional) - JSON object with networking configuration.
* ``user_data`` (optional) - user data. May be a string (which will be * ``user_data`` (optional) - user data. May be a string (which will be
UTF-8 encoded); a JSON object, or a JSON array. UTF-8 encoded); a JSON object, or a JSON array.
* ``vendor_data`` (optional) - JSON object with extra vendor data.
This parameter is only accepted when setting the state to "active" or This parameter is only accepted when setting the state to "active" or
"rebuild". "rebuild".

View File

@ -2,6 +2,12 @@
REST API Version History REST API Version History
======================== ========================
1.59 (Ussuri, master)
Added the ability to specify a ``vendor_data`` dictionary field in the
``configdrive`` parameter submitted with the deployment of a node. The value
is a dictionary which is served as ``vendor_data2.json`` in the config drive.
1.58 (Train, 12.2.0) 1.58 (Train, 12.2.0)
-------------------- --------------------

View File

@ -616,7 +616,8 @@ _CONFIG_DRIVE_SCHEMA = {
'network_data': {'type': 'object'}, 'network_data': {'type': 'object'},
'user_data': { 'user_data': {
'type': ['object', 'array', 'string', 'null'] 'type': ['object', 'array', 'string', 'null']
} },
'vendor_data': {'type': 'object'},
}, },
'additionalProperties': False 'additionalProperties': False
}, },
@ -648,13 +649,22 @@ def check_allow_configdrive(target, configdrive=None):
raise wsme.exc.ClientSideError( raise wsme.exc.ClientSideError(
msg, status_code=http_client.BAD_REQUEST) msg, status_code=http_client.BAD_REQUEST)
if isinstance(configdrive, dict) and not allow_build_configdrive(): if isinstance(configdrive, dict):
if not allow_build_configdrive():
msg = _('Providing a JSON object for configdrive is only supported' msg = _('Providing a JSON object for configdrive is only supported'
' starting with API version %(base)s.%(opr)s') % { ' starting with API version %(base)s.%(opr)s') % {
'base': versions.BASE_VERSION, 'base': versions.BASE_VERSION,
'opr': versions.MINOR_56_BUILD_CONFIGDRIVE} 'opr': versions.MINOR_56_BUILD_CONFIGDRIVE}
raise wsme.exc.ClientSideError( raise wsme.exc.ClientSideError(
msg, status_code=http_client.BAD_REQUEST) msg, status_code=http_client.BAD_REQUEST)
if ('vendor_data' in configdrive and
not allow_configdrive_vendor_data()):
msg = _('Providing vendor_data in configdrive is only supported'
' starting with API version %(base)s.%(opr)s') % {
'base': versions.BASE_VERSION,
'opr': versions.MINOR_59_CONFIGDRIVE_VENDOR_DATA}
raise wsme.exc.ClientSideError(
msg, status_code=http_client.BAD_REQUEST)
def check_allow_filter_by_fault(fault): def check_allow_filter_by_fault(fault):
@ -1163,6 +1173,15 @@ def allow_build_configdrive():
return api.request.version.minor >= versions.MINOR_56_BUILD_CONFIGDRIVE return api.request.version.minor >= versions.MINOR_56_BUILD_CONFIGDRIVE
def allow_configdrive_vendor_data():
"""Check if configdrive can contain a vendor_data key.
Version 1.59 of the API added support for configdrive vendor_data.
"""
return (api.request.version.minor >=
versions.MINOR_59_CONFIGDRIVE_VENDOR_DATA)
def allow_allocation_update(): def allow_allocation_update():
"""Check if updating an existing allocation is allowed or not. """Check if updating an existing allocation is allowed or not.

View File

@ -96,6 +96,7 @@ BASE_VERSION = 1
# v1.56: Add support for building configdrives. # v1.56: Add support for building configdrives.
# v1.57: Add support for updating an exisiting allocation. # v1.57: Add support for updating an exisiting allocation.
# v1.58: Add support for backfilling allocations. # v1.58: Add support for backfilling allocations.
# v1.59: Add support vendor data in configdrives.
MINOR_0_JUNO = 0 MINOR_0_JUNO = 0
MINOR_1_INITIAL_VERSION = 1 MINOR_1_INITIAL_VERSION = 1
@ -156,6 +157,7 @@ MINOR_55_DEPLOY_TEMPLATES = 55
MINOR_56_BUILD_CONFIGDRIVE = 56 MINOR_56_BUILD_CONFIGDRIVE = 56
MINOR_57_ALLOCATION_UPDATE = 57 MINOR_57_ALLOCATION_UPDATE = 57
MINOR_58_ALLOCATION_BACKFILL = 58 MINOR_58_ALLOCATION_BACKFILL = 58
MINOR_59_CONFIGDRIVE_VENDOR_DATA = 59
# When adding another version, update: # When adding another version, update:
# - MINOR_MAX_VERSION # - MINOR_MAX_VERSION
@ -163,7 +165,7 @@ MINOR_58_ALLOCATION_BACKFILL = 58
# explanation of what changed in the new version # explanation of what changed in the new version
# - common/release_mappings.py, RELEASE_MAPPING['master']['api'] # - common/release_mappings.py, RELEASE_MAPPING['master']['api']
MINOR_MAX_VERSION = MINOR_58_ALLOCATION_BACKFILL MINOR_MAX_VERSION = MINOR_59_CONFIGDRIVE_VENDOR_DATA
# String representations of the minor and maximum versions # String representations of the minor and maximum versions
_MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION) _MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_1_INITIAL_VERSION)

View File

@ -197,7 +197,7 @@ RELEASE_MAPPING = {
} }
}, },
'master': { 'master': {
'api': '1.58', 'api': '1.59',
'rpc': '1.48', 'rpc': '1.48',
'objects': { 'objects': {
'Allocation': ['1.0'], 'Allocation': ['1.0'],

View File

@ -831,7 +831,7 @@ def build_configdrive(node, configdrive):
:param node: an Ironic node object. :param node: an Ironic node object.
:param configdrive: A configdrive as a dict with keys ``meta_data``, :param configdrive: A configdrive as a dict with keys ``meta_data``,
``network_data`` and ``user_data`` (all optional). ``network_data``, ``user_data`` and ``vendor_data`` (all optional).
:returns: A gzipped and base64 encoded configdrive as a string. :returns: A gzipped and base64 encoded configdrive as a string.
""" """
meta_data = configdrive.setdefault('meta_data', {}) meta_data = configdrive.setdefault('meta_data', {})
@ -847,7 +847,8 @@ def build_configdrive(node, configdrive):
LOG.debug('Building a configdrive for node %s', node.uuid) LOG.debug('Building a configdrive for node %s', node.uuid)
return os_configdrive.build(meta_data, user_data=user_data, return os_configdrive.build(meta_data, user_data=user_data,
network_data=configdrive.get('network_data')) network_data=configdrive.get('network_data'),
vendor_data=configdrive.get('vendor_data'))
def fast_track_able(task): def fast_track_able(task):

View File

@ -4162,11 +4162,12 @@ class TestPut(test_api_base.BaseApiTest):
def test_provision_with_deploy_configdrive_as_dict_all_fields(self): def test_provision_with_deploy_configdrive_as_dict_all_fields(self):
fake_cd = {'user_data': {'serialize': 'me'}, fake_cd = {'user_data': {'serialize': 'me'},
'meta_data': {'hostname': 'example.com'}, 'meta_data': {'hostname': 'example.com'},
'network_data': {'links': []}} 'network_data': {'links': []},
'vendor_data': {'foo': 'bar'}}
ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid, ret = self.put_json('/nodes/%s/states/provision' % self.node.uuid,
{'target': states.ACTIVE, {'target': states.ACTIVE,
'configdrive': fake_cd}, 'configdrive': fake_cd},
headers={api_base.Version.string: '1.56'}) headers={api_base.Version.string: '1.59'})
self.assertEqual(http_client.ACCEPTED, ret.status_code) self.assertEqual(http_client.ACCEPTED, ret.status_code)
self.assertEqual(b'', ret.body) self.assertEqual(b'', ret.body)
self.mock_dnd.assert_called_once_with(context=mock.ANY, self.mock_dnd.assert_called_once_with(context=mock.ANY,

View File

@ -479,6 +479,12 @@ class TestCheckAllowFields(base.TestCase):
mock_request.version.minor = 34 mock_request.version.minor = 34
self.assertFalse(utils.allow_node_rebuild_with_configdrive()) self.assertFalse(utils.allow_node_rebuild_with_configdrive())
def test_allow_configdrive_vendor_data(self, mock_request):
mock_request.version.minor = 59
self.assertTrue(utils.allow_configdrive_vendor_data())
mock_request.version.minor = 58
self.assertFalse(utils.allow_configdrive_vendor_data())
def test_check_allow_configdrive_fails(self, mock_request): def test_check_allow_configdrive_fails(self, mock_request):
mock_request.version.minor = 35 mock_request.version.minor = 35
self.assertRaises(wsme.exc.ClientSideError, self.assertRaises(wsme.exc.ClientSideError,
@ -500,16 +506,27 @@ class TestCheckAllowFields(base.TestCase):
utils.check_allow_configdrive(states.ACTIVE, "abcd") utils.check_allow_configdrive(states.ACTIVE, "abcd")
def test_check_allow_configdrive_as_dict(self, mock_request): def test_check_allow_configdrive_as_dict(self, mock_request):
mock_request.version.minor = 56 mock_request.version.minor = 59
utils.check_allow_configdrive(states.ACTIVE, {'meta_data': {}}) utils.check_allow_configdrive(states.ACTIVE, {'meta_data': {}})
utils.check_allow_configdrive(states.ACTIVE, {'meta_data': {}, utils.check_allow_configdrive(states.ACTIVE, {'meta_data': {},
'network_data': {}, 'network_data': {},
'user_data': {}}) 'user_data': {},
'vendor_data': {}})
utils.check_allow_configdrive(states.ACTIVE, {'user_data': 'foo'}) utils.check_allow_configdrive(states.ACTIVE, {'user_data': 'foo'})
utils.check_allow_configdrive(states.ACTIVE, {'user_data': ['foo']}) utils.check_allow_configdrive(states.ACTIVE, {'user_data': ['foo']})
def test_check_allow_configdrive_vendor_data_failed(self, mock_request):
mock_request.version.minor = 58
self.assertRaises(wsme.exc.ClientSideError,
utils.check_allow_configdrive,
states.ACTIVE,
{'meta_data': {},
'network_data': {},
'user_data': {},
'vendor_data': {}})
def test_check_allow_configdrive_as_dict_invalid(self, mock_request): def test_check_allow_configdrive_as_dict_invalid(self, mock_request):
mock_request.version.minor = 56 mock_request.version.minor = 59
self.assertRaises(wsme.exc.ClientSideError, self.assertRaises(wsme.exc.ClientSideError,
utils.check_allow_configdrive, states.REBUILD, utils.check_allow_configdrive, states.REBUILD,
{'foo': 'bar'}) {'foo': 'bar'})

View File

@ -2205,7 +2205,7 @@ class DoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
configdrive = 'foo' configdrive = 'foo'
self._test__do_node_deploy_ok(configdrive=configdrive) self._test__do_node_deploy_ok(configdrive=configdrive)
@mock.patch('openstack.baremetal.configdrive.build', autospec=True) @mock.patch('openstack.baremetal.configdrive.build')
def test__do_node_deploy_configdrive_as_dict(self, mock_cd): def test__do_node_deploy_configdrive_as_dict(self, mock_cd):
mock_cd.return_value = 'foo' mock_cd.return_value = 'foo'
configdrive = {'user_data': 'abcd'} configdrive = {'user_data': 'abcd'}
@ -2213,9 +2213,10 @@ class DoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
expected_configdrive='foo') expected_configdrive='foo')
mock_cd.assert_called_once_with({'uuid': self.node.uuid}, mock_cd.assert_called_once_with({'uuid': self.node.uuid},
network_data=None, network_data=None,
user_data=b'abcd') user_data=b'abcd',
vendor_data=None)
@mock.patch('openstack.baremetal.configdrive.build', autospec=True) @mock.patch('openstack.baremetal.configdrive.build')
def test__do_node_deploy_configdrive_as_dict_with_meta_data(self, mock_cd): def test__do_node_deploy_configdrive_as_dict_with_meta_data(self, mock_cd):
mock_cd.return_value = 'foo' mock_cd.return_value = 'foo'
configdrive = {'meta_data': {'uuid': uuidutils.generate_uuid(), configdrive = {'meta_data': {'uuid': uuidutils.generate_uuid(),
@ -2225,9 +2226,10 @@ class DoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
expected_configdrive='foo') expected_configdrive='foo')
mock_cd.assert_called_once_with(configdrive['meta_data'], mock_cd.assert_called_once_with(configdrive['meta_data'],
network_data=None, network_data=None,
user_data=None) user_data=None,
vendor_data=None)
@mock.patch('openstack.baremetal.configdrive.build', autospec=True) @mock.patch('openstack.baremetal.configdrive.build')
def test__do_node_deploy_configdrive_with_network_data(self, mock_cd): def test__do_node_deploy_configdrive_with_network_data(self, mock_cd):
mock_cd.return_value = 'foo' mock_cd.return_value = 'foo'
configdrive = {'network_data': {'links': []}} configdrive = {'network_data': {'links': []}}
@ -2235,9 +2237,10 @@ class DoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
expected_configdrive='foo') expected_configdrive='foo')
mock_cd.assert_called_once_with({'uuid': self.node.uuid}, mock_cd.assert_called_once_with({'uuid': self.node.uuid},
network_data={'links': []}, network_data={'links': []},
user_data=None) user_data=None,
vendor_data=None)
@mock.patch('openstack.baremetal.configdrive.build', autospec=True) @mock.patch('openstack.baremetal.configdrive.build')
def test__do_node_deploy_configdrive_and_user_data_as_dict(self, mock_cd): def test__do_node_deploy_configdrive_and_user_data_as_dict(self, mock_cd):
mock_cd.return_value = 'foo' mock_cd.return_value = 'foo'
configdrive = {'user_data': {'user': 'data'}} configdrive = {'user_data': {'user': 'data'}}
@ -2245,7 +2248,19 @@ class DoNodeDeployTestCase(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
expected_configdrive='foo') expected_configdrive='foo')
mock_cd.assert_called_once_with({'uuid': self.node.uuid}, mock_cd.assert_called_once_with({'uuid': self.node.uuid},
network_data=None, network_data=None,
user_data=b'{"user": "data"}') user_data=b'{"user": "data"}',
vendor_data=None)
@mock.patch('openstack.baremetal.configdrive.build')
def test__do_node_deploy_configdrive_with_vendor_data(self, mock_cd):
mock_cd.return_value = 'foo'
configdrive = {'vendor_data': {'foo': 'bar'}}
self._test__do_node_deploy_ok(configdrive=configdrive,
expected_configdrive='foo')
mock_cd.assert_called_once_with({'uuid': self.node.uuid},
network_data=None,
user_data=None,
vendor_data={'foo': 'bar'})
@mock.patch.object(swift, 'SwiftAPI') @mock.patch.object(swift, 'SwiftAPI')
@mock.patch('ironic.drivers.modules.fake.FakeDeploy.prepare') @mock.patch('ironic.drivers.modules.fake.FakeDeploy.prepare')

View File

@ -20,7 +20,7 @@ keystoneauth1==3.15.0
keystonemiddleware==4.17.0 keystonemiddleware==4.17.0
mock==3.0.0 mock==3.0.0
openstackdocstheme==1.20.0 openstackdocstheme==1.20.0
openstacksdk==0.31.2 openstacksdk==0.37.0
os-api-ref==1.4.0 os-api-ref==1.4.0
os-traits==0.4.0 os-traits==0.4.0
oslo.concurrency==3.26.0 oslo.concurrency==3.26.0

View File

@ -0,0 +1,8 @@
---
features:
- |
Adds support for specifying vendor_data when building config drives.
Starting with API version 1.59, a JSON based ``configdrive`` parameter to
``/v1/nodes/<node>/states/provision`` can include the key vendor_data.
This data will be built into the configdrive contents as
vendor_data2.json.

View File

@ -47,4 +47,4 @@ jsonschema>=2.6.0 # MIT
psutil>=3.2.2 # BSD psutil>=3.2.2 # BSD
futurist>=1.2.0 # Apache-2.0 futurist>=1.2.0 # Apache-2.0
tooz>=1.58.0 # Apache-2.0 tooz>=1.58.0 # Apache-2.0
openstacksdk>=0.31.2 # Apache-2.0 openstacksdk>=0.37.0 # Apache-2.0