Commit 5028473c authored by Jérome Perrin's avatar Jérome Perrin

slap/request: emit a warning when requesting with parameters not matching schema

parent 80cf8cf1
...@@ -40,14 +40,17 @@ import os ...@@ -40,14 +40,17 @@ import os
import logging import logging
import re import re
from functools import wraps from functools import wraps
import warnings
import json import json
import jsonschema
import six import six
from .exception import ResourceNotReady, ServerError, NotFoundError, \ from .exception import ResourceNotReady, ServerError, NotFoundError, \
ConnectionError ConnectionError
from .hateoas import SlapHateoasNavigator, ConnectionHelper from .hateoas import SlapHateoasNavigator, ConnectionHelper
from slapos.util import loads, dumps, bytes2str, unicode2str, xml2dict, dict2xml, calculate_dict_hash from slapos.util import (SoftwareReleaseSchema, bytes2str, calculate_dict_hash,
dict2xml, dumps, loads, unicode2str, xml2dict)
from xml.sax import saxutils from xml.sax import saxutils
from zope.interface import implementer from zope.interface import implementer
...@@ -87,6 +90,27 @@ class SlapRequester(SlapDocument): ...@@ -87,6 +90,27 @@ class SlapRequester(SlapDocument):
""" """
def _requestComputerPartition(self, request_dict): def _requestComputerPartition(self, request_dict):
try:
SoftwareReleaseSchema(
request_dict['software_release'],
request_dict['software_type']
).validateInstanceParameterDict(
loads(request_dict['partition_parameter_xml']))
except jsonschema.ValidationError as e:
warnings.warn(
"Request parameters do not validate against schema definition:\n{e}".format(e=e),
UserWarning,
)
except Exception as e:
# note that we intentionally catch wide exceptions, so that if anything
# is wrong with fetching the schema or the schema itself this does not
# prevent users from requesting instances.
warnings.warn(
"Error validating request parameters against schema definition:\n{e.__class__.__name__} {e}".format(e=e),
UserWarning,
)
try: try:
xml = self._connection_helper.POST('requestComputerPartition', data=request_dict) xml = self._connection_helper.POST('requestComputerPartition', data=request_dict)
except ResourceNotReady: except ResourceNotReady:
......
...@@ -32,6 +32,7 @@ from six.moves.urllib import parse ...@@ -32,6 +32,7 @@ from six.moves.urllib import parse
from six import PY3 from six import PY3
import tempfile import tempfile
import logging import logging
import warnings
from collections import OrderedDict from collections import OrderedDict
import httmock import httmock
...@@ -872,6 +873,188 @@ class TestComputerPartition(SlapMixin): ...@@ -872,6 +873,188 @@ class TestComputerPartition(SlapMixin):
content_list = f.read().splitlines() content_list = f.read().splitlines()
self.assertEqual(sorted(content_list), ['myref', 'mysecondref']) self.assertEqual(sorted(content_list), ['myref', 'mysecondref'])
def test_request_validate_request_parameter(self):
def handler(url, req):
if url.path.endswith('/software.cfg.json'):
return json.dumps(
{
"name": "Test Software",
"description": "Dummy software for Test",
"serialisation": "json-in-xml",
"software-type": {
'default': {
"title": "Default",
"description": "Default type",
"request": "instance-default-input-schema.json",
"response": "instance-default-output-schema.json",
"index": 0
},
}
})
if url.path.endswith('/instance-default-input-schema.json'):
return json.dumps(
{
"$schema": "http://json-schema.org/draft-07/schema",
"description": "Simple instance parameters schema for tests",
"required": ["foo"],
"properties": {
"foo": {
"$ref": "./schemas-definitions.json#/foo"
}
},
"type": "object"
})
if url.path.endswith('/schemas-definitions.json'):
return json.dumps({"foo": {"type": "string", "const": "bar"}})
raise ValueError(404)
with httmock.HTTMock(handler):
with mock.patch.object(warnings, 'warn') as warn:
cp = slapos.slap.ComputerPartition('computer_id', 'partition_id')
cp._connection_helper = mock.Mock()
cp._connection_helper.POST.side_effect = slapos.slap.ResourceNotReady
cp.request(
'https://example.com/software.cfg', 'default', 'reference',
partition_parameter_kw={'foo': 'bar'})
warn.assert_not_called()
with httmock.HTTMock(handler):
with mock.patch.object(warnings, 'warn') as warn:
cp = slapos.slap.ComputerPartition('computer_id', 'partition_id')
cp._connection_helper = mock.Mock()
cp._connection_helper.POST.side_effect = slapos.slap.ResourceNotReady
cp.request(
'https://example.com/software.cfg', 'default', 'reference',
partition_parameter_kw={'foo': 'baz'})
if PY3:
warn.assert_called_with(
"Request parameters do not validate against schema definition:\n"
"'bar' was expected\n\n"
"Failed validating 'const' in schema['properties']['foo']:\n"
" {'const': 'bar', 'type': 'string'}\n\n"
"On instance['foo']:\n 'baz'", UserWarning
)
else: # BBB
warn.assert_called_with(
"Request parameters do not validate against schema definition:\n"
"u'bar' was expected\n\n"
"Failed validating u'const' in schema[u'properties'][u'foo']:\n"
" {u'const': u'bar', u'type': u'string'}\n\n"
"On instance[u'foo']:\n 'baz'", UserWarning
)
def test_request_validate_request_parameter_broken_software_release_schema(self):
"""Corner case tests for incorrect software release schema, these should
not prevent the request (mostly for backward compatibility)
"""
def wrong_software_cfg_schema(url, req):
if url.path.endswith('/software.cfg.json'):
return "wrong"
raise ValueError(404)
def wrong_instance_parameter_schema(url, req):
if url.path.endswith('/software.cfg.json'):
return json.dumps(
{
"name": "Test Software",
"description": "Dummy software for Test",
"serialisation": "json-in-xml",
"software-type": {
'default': {
"title": "Default",
"description": "Default type",
"request": "instance-default-input-schema.json",
"response": "instance-default-output-schema.json",
"index": 0
},
}
})
if url.path.endswith('/instance-default-input-schema.json'):
return "wrong"
raise ValueError(404)
def invalid_instance_parameter_schema(url, req):
if url.path.endswith('/software.cfg.json'):
return json.dumps(
{
"name": "Test Software",
"description": "Dummy software for Test",
"serialisation": "json-in-xml",
"software-type": {
'default': {
"title": "Default",
"description": "Default type",
"request": "instance-default-input-schema.json",
"response": "instance-default-output-schema.json",
"index": 0
},
}
})
if url.path.endswith('/instance-default-input-schema.json'):
return json.dumps(
{
"$schema": "http://json-schema.org/draft-07/schema",
"description": "Invalid json schema",
"required": {"wrong": True},
"properties": {
["wrong schema"]
},
"type": "object"
})
raise ValueError(404)
def broken_reference(url, req):
if url.path.endswith('/software.cfg.json'):
return json.dumps(
{
"name": "Test Software",
"description": "Dummy software for Test",
"serialisation": "json-in-xml",
"software-type": {
'default': {
"title": "Default",
"description": "Default type",
"request": "instance-default-input-schema.json",
"response": "instance-default-output-schema.json",
"index": 0
},
}
})
if url.path.endswith('/instance-default-input-schema.json'):
return json.dumps(
{
"$schema": "http://json-schema.org/draft-07/schema",
"description": "Simple instance parameters schema for tests",
"required": ["foo"],
"properties": {
"foo": {
"$ref": "broken"
}
},
"type": "object"
})
raise ValueError(404)
for handler, warning_expected in (
(broken_reference, True),
(wrong_software_cfg_schema, False),
(wrong_instance_parameter_schema, True),
(invalid_instance_parameter_schema, True),
):
with httmock.HTTMock(handler):
with mock.patch.object(warnings, 'warn') as warn:
cp = slapos.slap.ComputerPartition('computer_id', 'partition_id')
cp._connection_helper = mock.Mock()
cp._connection_helper.POST.side_effect = slapos.slap.ResourceNotReady
cp.request(
'https://example.com/software.cfg', 'default', 'reference',
partition_parameter_kw={'foo': 'bar'})
if warning_expected:
warn.assert_called()
else:
warn.assert_not_called()
def _test_new_computer_partition_state(self, state): def _test_new_computer_partition_state(self, state):
""" """
Helper method to automate assertions of failing states on new Computer Helper method to automate assertions of failing states on new Computer
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment