Merge "Allow using per-site network_data schema"

This commit is contained in:
Zuul 2021-03-29 16:09:49 +00:00 committed by Gerrit Code Review
commit 851aac397e
6 changed files with 151 additions and 68 deletions

View File

@ -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

View File

@ -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',

View File

@ -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()

View File

@ -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):

View File

@ -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):

View File

@ -0,0 +1,5 @@
---
features:
- |
The network data schema is now configurable via the new configuration
options ``[api]network_data_schema``.