Merge "Allow using per-site network_data schema"
This commit is contained in:
commit
851aac397e
@ -17,7 +17,6 @@ import copy
|
|||||||
import datetime
|
import datetime
|
||||||
from http import client as http_client
|
from http import client as http_client
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
|
|
||||||
from ironic_lib import metrics_utils
|
from ironic_lib import metrics_utils
|
||||||
import jsonschema
|
import jsonschema
|
||||||
@ -123,62 +122,75 @@ ALLOWED_TARGET_POWER_STATES = (ir_states.POWER_ON,
|
|||||||
|
|
||||||
_NODE_DESCRIPTION_MAX_LENGTH = 4096
|
_NODE_DESCRIPTION_MAX_LENGTH = 4096
|
||||||
|
|
||||||
with open(os.path.join(os.path.dirname(__file__),
|
_NETWORK_DATA_SCHEMA = None
|
||||||
'network-data-schema.json'), 'rb') as fl:
|
|
||||||
NETWORK_DATA_SCHEMA = json.load(fl)
|
|
||||||
|
|
||||||
NODE_SCHEMA = {
|
|
||||||
'type': 'object',
|
|
||||||
'properties': {
|
|
||||||
'automated_clean': {'type': ['string', 'boolean', 'null']},
|
|
||||||
'bios_interface': {'type': ['string', 'null']},
|
|
||||||
'boot_interface': {'type': ['string', 'null']},
|
|
||||||
'chassis_uuid': {'type': ['string', 'null']},
|
|
||||||
'conductor_group': {'type': ['string', 'null']},
|
|
||||||
'console_enabled': {'type': ['string', 'boolean', 'null']},
|
|
||||||
'console_interface': {'type': ['string', 'null']},
|
|
||||||
'deploy_interface': {'type': ['string', 'null']},
|
|
||||||
'description': {'type': ['string', 'null'],
|
|
||||||
'maxLength': _NODE_DESCRIPTION_MAX_LENGTH},
|
|
||||||
'driver': {'type': 'string'},
|
|
||||||
'driver_info': {'type': ['object', 'null']},
|
|
||||||
'extra': {'type': ['object', 'null']},
|
|
||||||
'inspect_interface': {'type': ['string', 'null']},
|
|
||||||
'instance_info': {'type': ['object', 'null']},
|
|
||||||
'instance_uuid': {'type': ['string', 'null']},
|
|
||||||
'lessee': {'type': ['string', 'null']},
|
|
||||||
'management_interface': {'type': ['string', 'null']},
|
|
||||||
'maintenance': {'type': ['string', 'boolean', 'null']},
|
|
||||||
'name': {'type': ['string', 'null']},
|
|
||||||
'network_data': {'anyOf': [
|
|
||||||
{'type': 'null'},
|
|
||||||
{'type': 'object', 'additionalProperties': False},
|
|
||||||
NETWORK_DATA_SCHEMA
|
|
||||||
]},
|
|
||||||
'network_interface': {'type': ['string', 'null']},
|
|
||||||
'owner': {'type': ['string', 'null']},
|
|
||||||
'power_interface': {'type': ['string', 'null']},
|
|
||||||
'properties': {'type': ['object', 'null']},
|
|
||||||
'raid_interface': {'type': ['string', 'null']},
|
|
||||||
'rescue_interface': {'type': ['string', 'null']},
|
|
||||||
'resource_class': {'type': ['string', 'null'], 'maxLength': 80},
|
|
||||||
'retired': {'type': ['string', 'boolean', 'null']},
|
|
||||||
'retired_reason': {'type': ['string', 'null']},
|
|
||||||
'storage_interface': {'type': ['string', 'null']},
|
|
||||||
'uuid': {'type': ['string', 'null']},
|
|
||||||
'vendor_interface': {'type': ['string', 'null']},
|
|
||||||
},
|
|
||||||
'required': ['driver'],
|
|
||||||
'additionalProperties': False,
|
|
||||||
'definitions': NETWORK_DATA_SCHEMA.get('definitions')
|
|
||||||
}
|
|
||||||
|
|
||||||
NODE_PATCH_SCHEMA = copy.deepcopy(NODE_SCHEMA)
|
def network_data_schema():
|
||||||
# add schema for patchable fields
|
global _NETWORK_DATA_SCHEMA
|
||||||
NODE_PATCH_SCHEMA['properties']['protected'] = {
|
if _NETWORK_DATA_SCHEMA is None:
|
||||||
'type': ['string', 'boolean', 'null']}
|
with open(CONF.api.network_data_schema) as fl:
|
||||||
NODE_PATCH_SCHEMA['properties']['protected_reason'] = {
|
_NETWORK_DATA_SCHEMA = json.load(fl)
|
||||||
'type': ['string', 'null']}
|
return _NETWORK_DATA_SCHEMA
|
||||||
|
|
||||||
|
|
||||||
|
def node_schema():
|
||||||
|
network_data = network_data_schema()
|
||||||
|
return {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'automated_clean': {'type': ['string', 'boolean', 'null']},
|
||||||
|
'bios_interface': {'type': ['string', 'null']},
|
||||||
|
'boot_interface': {'type': ['string', 'null']},
|
||||||
|
'chassis_uuid': {'type': ['string', 'null']},
|
||||||
|
'conductor_group': {'type': ['string', 'null']},
|
||||||
|
'console_enabled': {'type': ['string', 'boolean', 'null']},
|
||||||
|
'console_interface': {'type': ['string', 'null']},
|
||||||
|
'deploy_interface': {'type': ['string', 'null']},
|
||||||
|
'description': {'type': ['string', 'null'],
|
||||||
|
'maxLength': _NODE_DESCRIPTION_MAX_LENGTH},
|
||||||
|
'driver': {'type': 'string'},
|
||||||
|
'driver_info': {'type': ['object', 'null']},
|
||||||
|
'extra': {'type': ['object', 'null']},
|
||||||
|
'inspect_interface': {'type': ['string', 'null']},
|
||||||
|
'instance_info': {'type': ['object', 'null']},
|
||||||
|
'instance_uuid': {'type': ['string', 'null']},
|
||||||
|
'lessee': {'type': ['string', 'null']},
|
||||||
|
'management_interface': {'type': ['string', 'null']},
|
||||||
|
'maintenance': {'type': ['string', 'boolean', 'null']},
|
||||||
|
'name': {'type': ['string', 'null']},
|
||||||
|
'network_data': {'anyOf': [
|
||||||
|
{'type': 'null'},
|
||||||
|
{'type': 'object', 'additionalProperties': False},
|
||||||
|
network_data
|
||||||
|
]},
|
||||||
|
'network_interface': {'type': ['string', 'null']},
|
||||||
|
'owner': {'type': ['string', 'null']},
|
||||||
|
'power_interface': {'type': ['string', 'null']},
|
||||||
|
'properties': {'type': ['object', 'null']},
|
||||||
|
'raid_interface': {'type': ['string', 'null']},
|
||||||
|
'rescue_interface': {'type': ['string', 'null']},
|
||||||
|
'resource_class': {'type': ['string', 'null'], 'maxLength': 80},
|
||||||
|
'retired': {'type': ['string', 'boolean', 'null']},
|
||||||
|
'retired_reason': {'type': ['string', 'null']},
|
||||||
|
'storage_interface': {'type': ['string', 'null']},
|
||||||
|
'uuid': {'type': ['string', 'null']},
|
||||||
|
'vendor_interface': {'type': ['string', 'null']},
|
||||||
|
},
|
||||||
|
'required': ['driver'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
'definitions': network_data.get('definitions', {})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def node_patch_schema():
|
||||||
|
node_patch = copy.deepcopy(node_schema())
|
||||||
|
# add schema for patchable fields
|
||||||
|
node_patch['properties']['protected'] = {
|
||||||
|
'type': ['string', 'boolean', 'null']}
|
||||||
|
node_patch['properties']['protected_reason'] = {
|
||||||
|
'type': ['string', 'null']}
|
||||||
|
return node_patch
|
||||||
|
|
||||||
|
|
||||||
NODE_VALIDATE_EXTRA = args.dict_valid(
|
NODE_VALIDATE_EXTRA = args.dict_valid(
|
||||||
automated_clean=args.boolean,
|
automated_clean=args.boolean,
|
||||||
@ -191,15 +203,30 @@ NODE_VALIDATE_EXTRA = args.dict_valid(
|
|||||||
uuid=args.uuid,
|
uuid=args.uuid,
|
||||||
)
|
)
|
||||||
|
|
||||||
NODE_VALIDATOR = args.and_valid(
|
|
||||||
args.schema(NODE_SCHEMA),
|
|
||||||
NODE_VALIDATE_EXTRA
|
|
||||||
)
|
|
||||||
|
|
||||||
NODE_PATCH_VALIDATOR = args.and_valid(
|
_NODE_VALIDATOR = None
|
||||||
args.schema(NODE_PATCH_SCHEMA),
|
_NODE_PATCH_VALIDATOR = None
|
||||||
NODE_VALIDATE_EXTRA
|
|
||||||
)
|
|
||||||
|
def node_validator(name, value):
|
||||||
|
global _NODE_VALIDATOR
|
||||||
|
if _NODE_VALIDATOR is None:
|
||||||
|
_NODE_VALIDATOR = args.and_valid(
|
||||||
|
args.schema(node_schema()),
|
||||||
|
NODE_VALIDATE_EXTRA
|
||||||
|
)
|
||||||
|
return _NODE_VALIDATOR(name, value)
|
||||||
|
|
||||||
|
|
||||||
|
def node_patch_validator(name, value):
|
||||||
|
global _NODE_PATCH_VALIDATOR
|
||||||
|
if _NODE_PATCH_VALIDATOR is None:
|
||||||
|
_NODE_PATCH_VALIDATOR = args.and_valid(
|
||||||
|
args.schema(node_patch_schema()),
|
||||||
|
NODE_VALIDATE_EXTRA
|
||||||
|
)
|
||||||
|
return _NODE_PATCH_VALIDATOR(name, value)
|
||||||
|
|
||||||
|
|
||||||
PATCH_ALLOWED_FIELDS = [
|
PATCH_ALLOWED_FIELDS = [
|
||||||
'automated_clean',
|
'automated_clean',
|
||||||
@ -334,7 +361,7 @@ def validate_network_data(network_data):
|
|||||||
:raises: Invalid if network data is not schema-compliant
|
:raises: Invalid if network data is not schema-compliant
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
jsonschema.validate(network_data, NETWORK_DATA_SCHEMA)
|
jsonschema.validate(network_data, network_data_schema())
|
||||||
|
|
||||||
except json_schema_exc.ValidationError as e:
|
except json_schema_exc.ValidationError as e:
|
||||||
# NOTE: Even though e.message is deprecated in general, it is
|
# NOTE: Even though e.message is deprecated in general, it is
|
||||||
@ -1838,7 +1865,7 @@ class NodesController(rest.RestController):
|
|||||||
api_utils.patch_update_changed_fields(
|
api_utils.patch_update_changed_fields(
|
||||||
node, rpc_node,
|
node, rpc_node,
|
||||||
fields=set(objects.Node.fields) - {'traits'},
|
fields=set(objects.Node.fields) - {'traits'},
|
||||||
schema=NODE_PATCH_SCHEMA,
|
schema=node_patch_schema(),
|
||||||
id_map={'chassis_id': chassis and chassis.id or None}
|
id_map={'chassis_id': chassis and chassis.id or None}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -2098,7 +2125,7 @@ class NodesController(rest.RestController):
|
|||||||
@METRICS.timer('NodesController.post')
|
@METRICS.timer('NodesController.post')
|
||||||
@method.expose(status_code=http_client.CREATED)
|
@method.expose(status_code=http_client.CREATED)
|
||||||
@method.body('node')
|
@method.body('node')
|
||||||
@args.validate(node=NODE_VALIDATOR)
|
@args.validate(node=node_validator)
|
||||||
def post(self, node):
|
def post(self, node):
|
||||||
"""Create a new node.
|
"""Create a new node.
|
||||||
|
|
||||||
@ -2335,7 +2362,7 @@ class NodesController(rest.RestController):
|
|||||||
node_dict = api_utils.apply_jsonpatch(node_dict, patch)
|
node_dict = api_utils.apply_jsonpatch(node_dict, patch)
|
||||||
|
|
||||||
api_utils.patched_validate_with_schema(
|
api_utils.patched_validate_with_schema(
|
||||||
node_dict, NODE_PATCH_SCHEMA, NODE_PATCH_VALIDATOR)
|
node_dict, node_patch_schema(), node_patch_validator)
|
||||||
|
|
||||||
self._update_changed_fields(node_dict, rpc_node)
|
self._update_changed_fields(node_dict, rpc_node)
|
||||||
# NOTE(tenbrae): we calculate the rpc topic here in case node.driver
|
# NOTE(tenbrae): we calculate the rpc topic here in case node.driver
|
||||||
|
@ -66,6 +66,10 @@ opts = [
|
|||||||
default=300,
|
default=300,
|
||||||
mutable=True,
|
mutable=True,
|
||||||
help=_('Maximum interval (in seconds) for agent heartbeats.')),
|
help=_('Maximum interval (in seconds) for agent heartbeats.')),
|
||||||
|
cfg.StrOpt(
|
||||||
|
'network_data_schema',
|
||||||
|
default='$pybasedir/api/controllers/v1/network-data-schema.json',
|
||||||
|
help=_("Schema for network data used by this deployment.")),
|
||||||
]
|
]
|
||||||
|
|
||||||
opt_group = cfg.OptGroup(name='api',
|
opt_group = cfg.OptGroup(name='api',
|
||||||
|
@ -27,6 +27,7 @@ from oslo_config import cfg
|
|||||||
import pecan
|
import pecan
|
||||||
import pecan.testing
|
import pecan.testing
|
||||||
|
|
||||||
|
from ironic.api.controllers.v1 import node as api_node
|
||||||
from ironic.tests.unit.db import base as db_base
|
from ironic.tests.unit.db import base as db_base
|
||||||
|
|
||||||
PATH_PREFIX = '/v1'
|
PATH_PREFIX = '/v1'
|
||||||
@ -65,6 +66,10 @@ class BaseApiTest(db_base.DbTestCase):
|
|||||||
self._check_version = p.start()
|
self._check_version = p.start()
|
||||||
self.addCleanup(p.stop)
|
self.addCleanup(p.stop)
|
||||||
|
|
||||||
|
api_node._NETWORK_DATA_SCHEMA = None
|
||||||
|
api_node._NODE_VALIDATOR = None
|
||||||
|
api_node._NODE_PATCH_VALIDATOR = None
|
||||||
|
|
||||||
def _make_app(self):
|
def _make_app(self):
|
||||||
# Determine where we are so we can set up paths in the config
|
# Determine where we are so we can set up paths in the config
|
||||||
root_dir = self.path_get()
|
root_dir = self.path_get()
|
||||||
|
@ -17,6 +17,7 @@ import datetime
|
|||||||
from http import client as http_client
|
from http import client as http_client
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import tempfile
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
from urllib import parse as urlparse
|
from urllib import parse as urlparse
|
||||||
|
|
||||||
@ -3807,6 +3808,47 @@ class TestPatch(test_api_base.BaseApiTest):
|
|||||||
self.assertEqual('application/json', response.content_type)
|
self.assertEqual('application/json', response.content_type)
|
||||||
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
|
self.assertEqual(http_client.NOT_ACCEPTABLE, response.status_code)
|
||||||
|
|
||||||
|
def test_update_network_data_wrong_format(self):
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
uuid=uuidutils.generate_uuid())
|
||||||
|
self.mock_update_node.return_value = node
|
||||||
|
headers = {api_base.Version.string: '1.66'}
|
||||||
|
|
||||||
|
response = self.patch_json('/nodes/%s' % node.uuid,
|
||||||
|
[{'path': '/network_data',
|
||||||
|
'value': {'cat': 'meow'},
|
||||||
|
'op': 'replace'}],
|
||||||
|
headers=headers,
|
||||||
|
expect_errors=True)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.BAD_REQUEST, response.status_code)
|
||||||
|
|
||||||
|
def test_update_network_data_custom(self):
|
||||||
|
custom_schema = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'cat': {'type': 'string'},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
with tempfile.NamedTemporaryFile('wt') as fp:
|
||||||
|
json.dump(custom_schema, fp)
|
||||||
|
fp.flush()
|
||||||
|
self.config(network_data_schema=fp.name, group='api')
|
||||||
|
|
||||||
|
node = obj_utils.create_test_node(self.context,
|
||||||
|
uuid=uuidutils.generate_uuid(),
|
||||||
|
provision_state='active')
|
||||||
|
self.mock_update_node.return_value = node
|
||||||
|
headers = {api_base.Version.string: '1.66'}
|
||||||
|
|
||||||
|
response = self.patch_json('/nodes/%s' % node.uuid,
|
||||||
|
[{'path': '/network_data',
|
||||||
|
'value': {'cat': 'meow'},
|
||||||
|
'op': 'replace'}],
|
||||||
|
headers=headers)
|
||||||
|
self.assertEqual('application/json', response.content_type)
|
||||||
|
self.assertEqual(http_client.OK, response.status_code)
|
||||||
|
|
||||||
@mock.patch.object(api_utils, 'check_multiple_node_policies_and_retrieve',
|
@mock.patch.object(api_utils, 'check_multiple_node_policies_and_retrieve',
|
||||||
autospec=True)
|
autospec=True)
|
||||||
def test_patch_policy_update(self, mock_cmnpar):
|
def test_patch_policy_update(self, mock_cmnpar):
|
||||||
|
@ -111,7 +111,7 @@ def node_post_data(**kw):
|
|||||||
node.pop(field, None)
|
node.pop(field, None)
|
||||||
|
|
||||||
return remove_other_fields(
|
return remove_other_fields(
|
||||||
node, node_controller.NODE_SCHEMA['properties'])
|
node, node_controller.node_schema()['properties'])
|
||||||
|
|
||||||
|
|
||||||
def port_post_data(**kw):
|
def port_post_data(**kw):
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
The network data schema is now configurable via the new configuration
|
||||||
|
options ``[api]network_data_schema``.
|
Loading…
Reference in New Issue
Block a user