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:
parent
b462ca420b
commit
7ebad2e344
@ -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".
|
||||||
|
@ -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)
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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'],
|
||||||
|
@ -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):
|
||||||
|
@ -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,
|
||||||
|
@ -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'})
|
||||||
|
@ -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')
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user