API schema validation

Story: 2007278
Task: #38717

Change-Id: I7a6fc62e8f2c0c3cc21560f9f889d0fe136ca33e
Signed-off-by: Tomi Juvonen <tomi.juvonen@nokia.com>
This commit is contained in:
Tomi Juvonen 2020-02-10 12:03:38 +02:00
parent e6b796f6b7
commit 219ab681a5
11 changed files with 333 additions and 46 deletions

View File

@ -47,7 +47,7 @@ Response codes
Update maintenance session (planned future functionality)
=========================================================
.. rest_method:: POST /v1/maintenance/{session_id}/
.. rest_method:: PUT /v1/maintenance/{session_id}/
Update existing maintenance session. This can be used to continue a failed
session.

View File

@ -17,9 +17,8 @@ session_id:
description: |
Session ID
in: path
required: false
required: true
type: string
min_version: \> 1
uuid-path:
description: |
@ -68,7 +67,7 @@ action-plugins:
description: |
List of action plug-ins.
in: body
required: true
required: false
type: list of dictionaries
boolean:
@ -90,7 +89,7 @@ hosts:
Hosts to be maintained. An empty list can indicate hosts are to be
discovered.
in: body
required: true
required: false
type: list of strings
instance-action:
@ -102,7 +101,8 @@ instance-action:
instance-actions:
description: |
instance ID : action string
instance ID : action string. This variable is not needed in reply to state
MAINTENANCE, SCALE_IN or MAINTENANCE_COMPLETE
in: body
required: true
type: dictionary
@ -133,7 +133,10 @@ lead-time:
How long lead time VNF needs for 'migration_type' operation. VNF needs to
report back to Fenix as soon as it is ready, but at least within this
time. Reporting as fast as can is crucial for optimizing
infrastructure upgrade/maintenance.
infrastructure upgrade/maintenance. Zero value means interaction with
VNFM is not used for this instance, but instance_group recovery_time
needs to be obeyed towards max_impacted_members.
in: body
required: true
type: integer

View File

@ -127,7 +127,7 @@ Request
- instance_id: uuid-path
- instance_id: uuid
- group_id: group-uuid
- name: instance-name
- instance_name: instance-name
- migration_type: migration-type
- max_interruption_time: max-interruption-time
- resource_mitigation: resource-mitigation
@ -160,7 +160,7 @@ Request
- instance_id: uuid-path
- instance_id: uuid
- group_id: group-uuid
- name: instance-name
- instance_name: instance-name
- migration_type: migration-type
- max_interruption_time: max-interruption-time
- resource_mitigation: resource-mitigation
@ -218,7 +218,7 @@ Request
- project_id: uuid
- instance_id: uuid-path
- instance_id: uuid
- name: instance-name
- group_name: instance-group
- migration_type: migration-type
- max_interruption_time: max-interruption-time
- resource_mitigation: resource-mitigation
@ -250,7 +250,7 @@ Request
- group_id: group-uuid-path
- group_id: group-uuid
- project_id: uuid
- name: instance-group
- group_name: instance-group
- anti_affinity_group: boolean
- max_instances_per_host: max-instances-per-host
- max_impacted_members: max-impacted-members

View File

@ -1,8 +1,8 @@
{
"project_id": "1ad1154137ac41799cefd5caebae379b",
"group_id": "a01d192c-328e-4708-9b3c-9d716cd24a92",
"name": "vm_ha_group",
"anti_affinity_group": "True",
"group_name": "vm_ha_group",
"anti_affinity_group": True,
"max_instances_per_host": 1,
"max_impacted_members": 1,
"recovery_time": 15,

View File

@ -14,6 +14,7 @@
# under the License.
import json
import jsonschema
from pecan import abort
from pecan import expose
from pecan import request
@ -24,6 +25,7 @@ from oslo_log import log
from oslo_serialization import jsonutils
from fenix.api.v1 import maintenance
from fenix.api.v1 import schema
from fenix import policy
LOG = log.getLogger(__name__)
@ -43,6 +45,12 @@ class ProjectController(rest.RestController):
if request.body:
LOG.error("Unexpected data")
abort(400)
try:
jsonschema.validate(session_id, schema.uid)
jsonschema.validate(project_id, schema.uid)
except jsonschema.exceptions.ValidationError as e:
LOG.error(str(e.message))
abort(422)
engine_data = self.engine_rpcapi.project_get_session(session_id,
project_id)
try:
@ -55,6 +63,13 @@ class ProjectController(rest.RestController):
@expose(content_type='application/json')
def put(self, session_id, project_id):
data = json.loads(request.body.decode('utf8'))
try:
jsonschema.validate(session_id, schema.uid)
jsonschema.validate(project_id, schema.uid)
jsonschema.validate(data, schema.maintenance_session_project_put)
except jsonschema.exceptions.ValidationError as e:
LOG.error(str(e.message))
abort(422)
engine_data = self.engine_rpcapi.project_update_session(session_id,
project_id,
data)
@ -76,6 +91,16 @@ class ProjectInstanceController(rest.RestController):
@expose(content_type='application/json')
def put(self, session_id, project_id, instance_id):
data = json.loads(request.body.decode('utf8'))
try:
jsonschema.validate(session_id, schema.uid)
jsonschema.validate(project_id, schema.uid)
jsonschema.validate(instance_id, schema.uid)
jsonschema.validate(
data,
schema.maintenance_session_project_instance_put)
except jsonschema.exceptions.ValidationError as e:
LOG.error(str(e.message))
abort(422)
engine_data = (
self.engine_rpcapi.project_update_session_instance(session_id,
project_id,
@ -98,13 +123,18 @@ class SessionController(rest.RestController):
@policy.authorize('maintenance:session', 'get')
@expose(content_type='application/json')
def get(self, session_id):
try:
jsonschema.validate(session_id, schema.uid)
except jsonschema.exceptions.ValidationError as e:
LOG.error(str(e.message))
abort(422)
if request.body:
LOG.error("Unexpected data")
abort(400)
session = self.engine_rpcapi.admin_get_session(session_id)
if session is None:
response.status = 404
return {"error": "Invalid session"}
LOG.error("Invalid session")
abort(404)
try:
response.text = jsonutils.dumps(session)
except TypeError:
@ -115,6 +145,13 @@ class SessionController(rest.RestController):
@expose(content_type='application/json')
def put(self, session_id):
data = json.loads(request.body.decode('utf8'))
try:
jsonschema.validate(session_id, schema.uid)
# TBD implement this API
# jsonschema.validate(data, schema.maintenance_session_put)
except jsonschema.exceptions.ValidationError as e:
LOG.error(str(e.message))
abort(422)
engine_data = self.engine_rpcapi.admin_update_session(session_id, data)
try:
response.text = jsonutils.dumps(engine_data)
@ -125,6 +162,11 @@ class SessionController(rest.RestController):
@policy.authorize('maintenance:session', 'delete')
@expose(content_type='application/json')
def delete(self, session_id):
try:
jsonschema.validate(session_id, schema.uid)
except jsonschema.exceptions.ValidationError as e:
LOG.error(str(e.message))
abort(422)
if request.body:
LOG.error("Unexpected data")
abort(400)
@ -160,10 +202,15 @@ class MaintenanceController(rest.RestController):
@expose(content_type='application/json')
def post(self):
data = json.loads(request.body.decode('utf8'))
try:
jsonschema.validate(data, schema.maintenance_post)
except jsonschema.exceptions.ValidationError as e:
LOG.error(str(e.message))
abort(422)
session = self.engine_rpcapi.admin_create_session(data)
if session is None:
response.status = 509
return {"error": "Too many sessions"}
LOG.error("Too many sessions")
abort(509)
try:
response.text = jsonutils.dumps(session)
except TypeError:
@ -181,13 +228,18 @@ class InstanceController(rest.RestController):
@policy.authorize('instance', 'get')
@expose(content_type='application/json')
def get(self, instance_id):
try:
jsonschema.validate(instance_id, schema.uid)
except jsonschema.exceptions.ValidationError as e:
LOG.error(str(e.message))
abort(422)
if request.body:
LOG.error("Unexpected data")
abort(400)
session = self.engine_rpcapi.get_instance(instance_id)
if session is None:
response.status = 404
return {"error": "Invalid session"}
LOG.error("Invalid session")
abort(404)
try:
response.text = jsonutils.dumps(session)
except TypeError:
@ -198,6 +250,12 @@ class InstanceController(rest.RestController):
@expose(content_type='application/json')
def put(self, instance_id):
data = json.loads(request.body.decode('utf8'))
try:
jsonschema.validate(instance_id, schema.uid)
jsonschema.validate(data, schema.instance_put)
except jsonschema.exceptions.ValidationError as e:
LOG.error(str(e.message))
abort(422)
engine_data = self.engine_rpcapi.update_instance(instance_id,
data)
try:
@ -209,6 +267,11 @@ class InstanceController(rest.RestController):
@policy.authorize('instance', 'delete')
@expose(content_type='application/json')
def delete(self, instance_id):
try:
jsonschema.validate(instance_id, schema.uid)
except jsonschema.exceptions.ValidationError as e:
LOG.error(str(e.message))
abort(422)
if request.body:
LOG.error("Unexpected data")
abort(400)
@ -230,13 +293,18 @@ class InstanceGroupController(rest.RestController):
@policy.authorize('instance_group', 'get')
@expose(content_type='application/json')
def get(self, group_id):
try:
jsonschema.validate(group_id, schema.uid)
except jsonschema.exceptions.ValidationError as e:
LOG.error(str(e.message))
abort(422)
if request.body:
LOG.error("Unexpected data")
abort(400)
session = self.engine_rpcapi.get_instance_group(group_id)
if session is None:
response.status = 404
return {"error": "Invalid session"}
LOG.error("Invalid session")
abort(404)
try:
response.text = jsonutils.dumps(session)
except TypeError:
@ -247,6 +315,12 @@ class InstanceGroupController(rest.RestController):
@expose(content_type='application/json')
def put(self, group_id):
data = json.loads(request.body.decode('utf8'))
try:
jsonschema.validate(group_id, schema.uid)
jsonschema.validate(data, schema.instance_group_put)
except jsonschema.exceptions.ValidationError as e:
LOG.error(str(e.message))
abort(422)
engine_data = (
self.engine_rpcapi.update_instance_group(group_id, data))
try:
@ -258,6 +332,11 @@ class InstanceGroupController(rest.RestController):
@policy.authorize('instance_group', 'delete')
@expose(content_type='application/json')
def delete(self, group_id):
try:
jsonschema.validate(group_id, schema.uid)
except jsonschema.exceptions.ValidationError as e:
LOG.error(str(e.message))
abort(422)
if request.body:
LOG.error("Unexpected data")
abort(400)

202
fenix/api/v1/schema.py Normal file
View File

@ -0,0 +1,202 @@
# Copyright 2020 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
uid = {
'type': 'string',
'minLength': 8,
'maxLength': 36,
}
states = ['MAINTENANCE',
'SCALE_IN',
'PREPARE_MAINTENANCE',
'START_MAINTENANCE',
'PLANNED_MAINTENANCE',
'MAINTENANCE_COMPLETE',
'MAINTENANCE_DONE',
'MAINTENANCE_FAILED']
reply_states = ['ACK_MAINTENANCE',
'ACK_SCALE_IN',
'ACK_PREPARE_MAINTENANCE',
'ACK_START_MAINTENANCE',
'ACK_PLANNED_MAINTENANCE',
'ACK_MAINTENANCE_COMPLETE',
'NACK_MAINTENANCE',
'NACK_SCALE_IN',
'NACK_PREPARE_MAINTENANCE',
'NACK_START_MAINTENANCE',
'NACK_PLANNED_MAINTENANCE',
'NACK_MAINTENANCE_COMPLETE']
allowed_actions = ['MIGRATE', 'LIVE_MIGRATE', 'OWN_ACTION']
maintenance_session_project_put = {
'type': 'object',
'properties': {
'instance_actions': {
'type': 'object'
},
'state': {
'type': 'string',
'enum': reply_states,
},
},
'required': ['state']
}
maintenance_session_project_instance_put = {
'type': 'object',
'properties': {
'instance_action': {
'type': 'string',
'enum': allowed_actions,
},
'state': {
'type': 'string',
'enum': reply_states,
}
},
'required': ['instance_action', 'state']
}
# TBD
# maintenance_session_put = {
#
# }
maintenance_post = {
'type': 'object',
'properties': {
'hosts': {
'type': 'array',
'minItems': 0,
'maxItems': 1000,
'items': {
'type': 'string',
'minLength': 2,
'maxLength': 255,
},
},
'state': {
'type': 'string',
'enum': states,
},
'maintenance_at': {
'type': 'string',
'format': 'date-time',
},
'metadata': {'type': 'object'},
'workflow': {
'type': 'string',
'minLength': 2,
'maxLength': 255,
},
'download': {
'type': 'array',
'minItems': 5,
'maxItems': 255,
'items': {
'type': 'string',
'minLength': 2,
'maxLength': 165,
},
},
'actions': {
'type': 'array',
'minItems': 0,
'maxItems': 255,
'items': {
'type': 'object',
'properties': {
'plugin': {
'type': 'string',
'minLength': 2,
'maxLength': 255,
},
'type': {
'type': 'string',
'minLength': 2,
'maxLength': 32,
},
'metadata': {'type': 'object'},
},
'required': ['plugin', 'type']
}
}
},
'required': ['state', 'maintenance_at', 'workflow', 'metadata']
}
instance_put = {
'type': 'object',
'properties': {
'instance_id': uid,
'project_id': uid,
'group_id': uid,
'instance_name': {
'type': 'string',
'minLength': 1,
'maxLength': 255,
},
'max_interruption_time': {
'type': 'number',
'maximum': 21600
},
'migration_type': {
'type': 'string',
'enum': allowed_actions,
},
'resource_mitigation': {'type': 'boolean'},
'lead_time': {
'type': 'number',
'maximum': 21600
},
},
'required': ['instance_id', 'project_id', 'group_id', 'instance_name',
'max_interruption_time', 'migration_type',
'resource_mitigation', 'lead_time']
}
instance_group_put = {
'type': 'object',
'properties': {
'project_id': uid,
'group_id': uid,
'group_name': {
'type': 'string',
'minLength': 1,
'maxLength': 255,
},
'anti_affinity_group': {'type': 'boolean'},
'max_instances_per_host': {
'type': 'number',
'maximum': 32000
},
'max_impacted_members': {
'type': 'number',
'minimum': 1,
'maximum': 32000
},
'recovery_time': {
'type': 'number',
'maximum': 21600
},
'resource_mitigation': {'type': 'boolean'},
},
'required': ['project_id', 'group_id', 'group_name',
'anti_affinity_group', 'max_instances_per_host',
'max_impacted_members', 'recovery_time',
'resource_mitigation']
}

View File

@ -1,11 +1,16 @@
coverage==4.0 # Apache-2.0
hacking==0.12.0 # Apache-2.0
coverage==4.0 # Apache-2.0
openstackdocstheme==1.18.1 # Apache-2.0
oslotest==1.10.0 # Apache-2.0
hacking==2.0 # Apache-2.0
openstackdocstheme==1.31.2 # Apache-2.0
oslotest==3.8.0 # Apache-2.0
pbr==2.0 # Apache-2.0
python-subunit==0.0.18 # Apache-2.0/BSD
reno==2.5.0 # Apache-2.0
sphinx==1.6.2 # BSD
python-subunit==1.2.0 # Apache-2.0/BSD
reno==2.11.3;python_version=='2.7'
reno==3.0.0;python_version=='3.5'
reno==3.0.0;python_version=='3.6'
reno==3.0.0;python_version=='3.7'
sphinx==1.8.5;python_version=='2.7'
sphinx==2.3.1;python_version=='3.5'
sphinx==2.3.1;python_version=='3.6'
sphinx==2.3.1;python_version=='3.7'
stestr==1.0.0 # Apache-2.0
testtools==1.4.0 # MIT
testtools==2.2.0 # MIT

View File

@ -2,4 +2,4 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
pbr>=2.0 # Apache-2.0
pbr!=2.1,>=2.0 # Apache-2.0

View File

@ -5,7 +5,8 @@ description-file =
README.rst
author = OpenStack
author-email = openstack-discuss@lists.openstack.org
home-page = http://www.openstack.org/
home-page = https://wiki.openstack.org/wiki/Fenix
python-requires = >=3.6
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology
@ -13,11 +14,10 @@ classifier =
License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3 :: Only
[global]
setup-hooks = pbr.hooks.setup_hook

View File

@ -2,10 +2,9 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
hacking>=0.12.0,<0.13 # Apache-2.0
hacking>=2.0,<2.1 # Apache-2.0
coverage>=4.0,!=4.4 # Apache-2.0
python-subunit>=0.0.18 # Apache-2.0/BSD
oslotest>=1.10.0 # Apache-2.0
python-subunit>=1.3.0 # Apache-2.0/BSD
oslotest>=3.8.0 # Apache-2.0
stestr>=1.0.0 # Apache-2.0
testtools>=1.4.0 # MIT
testtools>=2.2.0 # MIT

13
tox.ini
View File

@ -1,7 +1,7 @@
[tox]
minversion = 2.0
envlist = py36,py35,pep8,docs
skipsdist = True
minversion = 3.1.1
envlist = py36,py37,pep8,docs
ignore_basepython_conflict = True
[testenv]
usedevelop = True
@ -13,7 +13,7 @@ setenv =
OS_STDERR_CAPTURE=1
OS_TEST_TIMEOUT=60
deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt}
-c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
-r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands = stestr run {posargs}
@ -53,10 +53,9 @@ basepython = python3
commands = oslo_debug_helper {posargs}
[flake8]
# E123, E125 skipped as they are invalid PEP-8.
show-source = True
ignore = E123,E125
enable-extensions = H106,H203
ignore = E121,E122,E123,E124,E125,E126,E127,E128,E129,E131,E251,E305,E402,H405,W503,W504,E731
builtins = _
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build