Commit d962a0df authored by Jérome Perrin's avatar Jérome Perrin

util: use zc.buildout.download API to download schema

This will support buildout extension to download from gitlab API with
authentication.

The code has already been refactored a bit to be able to pass a
download instance to SoftwareReleaseSchema, for now to instanciate only
one Download but in the future this might be extended to pass some
configuration to the Download instance.
parent 0d13a6b1
Pipeline #37188 passed with stage
in 0 seconds
...@@ -875,15 +875,15 @@ class TestComputerPartition(SlapMixin): ...@@ -875,15 +875,15 @@ class TestComputerPartition(SlapMixin):
def test_request_validate_request_parameter(self): def test_request_validate_request_parameter(self):
def handler(url, req): def _readAsJson(url, set_schema_id=False):
if url.path.endswith('/software.cfg.json'): if url == 'https://example.com/software.cfg.json':
return json.dumps( assert not set_schema_id
{ return {
"name": "Test Software", "name": "Test Software",
"description": "Dummy software for Test", "description": "Dummy software for Test",
"serialisation": "json-in-xml", "serialisation": "json-in-xml",
"software-type": { "software-type": {
'default': { "default": {
"title": "Default", "title": "Default",
"description": "Default type", "description": "Default type",
"request": "instance-default-input-schema.json", "request": "instance-default-input-schema.json",
...@@ -891,78 +891,84 @@ class TestComputerPartition(SlapMixin): ...@@ -891,78 +891,84 @@ class TestComputerPartition(SlapMixin):
"index": 0 "index": 0
}, },
} }
}) }
if url.path.endswith('/instance-default-input-schema.json'): if url == 'https://example.com/instance-default-input-schema.json':
return json.dumps( assert set_schema_id
{ return {
"$id": url,
"$schema": "http://json-schema.org/draft-07/schema", "$schema": "http://json-schema.org/draft-07/schema",
"description": "Simple instance parameters schema for tests", "description": "Simple instance parameters schema for tests",
"required": ["foo"], "required": ["foo"],
"properties": { "properties": {
"foo": { "foo": {
"$ref": "./schemas-definitions.json#/foo" "$ref": "./schemas-definitions.json#/foo",
} }
}, },
"additionalProperties": False, "additionalProperties": False,
"type": "object" "type": "object",
}) }
if url.path.endswith('/schemas-definitions.json'): if url == 'https://example.com/schemas-definitions.json':
return json.dumps({"foo": {"type": "string", "const": "bar"}}) assert not set_schema_id
raise ValueError(404) return {
"foo": {
"type": "string",
"const": "bar",
},
}
assert False, "Unexpected url %s" % url
with httmock.HTTMock(handler): with mock.patch(
with mock.patch.object(warnings, 'warn') as warn: 'slapos.util.SoftwareReleaseSchema._readAsJson',
side_effect=_readAsJson) as _readAsJson_mock, \
mock.patch.object(warnings, 'warn') as warn:
cp = slapos.slap.ComputerPartition('computer_id', 'partition_id') cp = slapos.slap.ComputerPartition('computer_id', 'partition_id')
cp._connection_helper = mock.Mock() cp._connection_helper = mock.Mock()
cp._connection_helper.POST.side_effect = slapos.slap.ResourceNotReady cp._connection_helper.POST.side_effect = slapos.slap.ResourceNotReady
cp.request( cp.request(
'https://example.com/software.cfg', 'default', 'reference', 'https://example.com/software.cfg', 'default', 'reference',
partition_parameter_kw={'foo': 'bar'}) partition_parameter_kw={'foo': 'bar'})
self.assertEqual(
_readAsJson_mock.call_args_list,
[
mock.call('https://example.com/software.cfg.json'),
mock.call('https://example.com/instance-default-input-schema.json', True),
mock.call('https://example.com/schemas-definitions.json'),
])
warn.assert_not_called() warn.assert_not_called()
with httmock.HTTMock(handler): with mock.patch(
with mock.patch.object(warnings, 'warn') as warn: 'slapos.util.SoftwareReleaseSchema._readAsJson',
side_effect=_readAsJson), \
mock.patch.object(warnings, 'warn') as warn:
cp = slapos.slap.ComputerPartition('computer_id', 'partition_id') cp = slapos.slap.ComputerPartition('computer_id', 'partition_id')
cp._connection_helper = mock.Mock() cp._connection_helper = mock.Mock()
cp._connection_helper.POST.side_effect = slapos.slap.ResourceNotReady cp._connection_helper.POST.side_effect = slapos.slap.ResourceNotReady
cp.request( cp.request(
'https://example.com/software.cfg', 'default', 'reference', 'https://example.com/software.cfg', 'default', 'reference',
partition_parameter_kw={'foo': 'baz'}) partition_parameter_kw={'foo': 'baz'})
if PY3:
warn.assert_called_with( warn.assert_called_with(
"Request parameters do not validate against schema definition:\n" "Request parameters do not validate against schema definition:\n"
" $.foo: 'bar' was expected", " $.foo: 'bar' was expected",
UserWarning UserWarning
) )
else: # BBB
warn.assert_called_with(
"Request parameters do not validate against schema definition:\n"
" $.foo: u'bar' was expected",
UserWarning
)
with httmock.HTTMock(handler): with mock.patch(
with mock.patch.object(warnings, 'warn') as warn: 'slapos.util.SoftwareReleaseSchema._readAsJson',
side_effect=_readAsJson), \
mock.patch.object(warnings, 'warn') as warn:
cp = slapos.slap.ComputerPartition('computer_id', 'partition_id') cp = slapos.slap.ComputerPartition('computer_id', 'partition_id')
cp._connection_helper = mock.Mock() cp._connection_helper = mock.Mock()
cp._connection_helper.POST.side_effect = slapos.slap.ResourceNotReady cp._connection_helper.POST.side_effect = slapos.slap.ResourceNotReady
cp.request( cp.request(
'https://example.com/software.cfg', 'default', 'reference', 'https://example.com/software.cfg', 'default', 'reference',
partition_parameter_kw={'fooo': 'xxx'}) partition_parameter_kw={'fooo': 'xxx'})
if PY3:
warn.assert_called_with( warn.assert_called_with(
"Request parameters do not validate against schema definition:\n" "Request parameters do not validate against schema definition:\n"
" $: 'foo' is a required property\n" " $: 'foo' is a required property\n"
" $: Additional properties are not allowed ('fooo' was unexpected)", " $: Additional properties are not allowed ('fooo' was unexpected)",
UserWarning UserWarning
) )
else: # BBB
warn.assert_called_with(
"Request parameters do not validate against schema definition:\n"
" $: u'foo' is a required property\n"
" $: Additional properties are not allowed ('fooo' was unexpected)",
UserWarning
)
def test_request_validate_request_parameter_broken_software_release_schema(self): def test_request_validate_request_parameter_broken_software_release_schema(self):
"""Corner case tests for incorrect software release schema, these should """Corner case tests for incorrect software release schema, these should
......
...@@ -43,7 +43,7 @@ import warnings ...@@ -43,7 +43,7 @@ import warnings
import jsonschema import jsonschema
import netaddr import netaddr
import requests import zc.buildout.download
import six import six
from lxml import etree from lxml import etree
from six.moves.urllib import parse from six.moves.urllib import parse
...@@ -401,37 +401,15 @@ def rmtree(path): ...@@ -401,37 +401,15 @@ def rmtree(path):
def _readAsJson(url, set_schema_id=False):
# type: (str) -> Optional[Dict]
"""Reads and parse the json file located at `url`.
`url` can also be the path of a local file.
"""
try:
if url.startswith('http://') or url.startswith('https://'):
r = requests.get(url, timeout=60) # we need a timeout !
r.raise_for_status()
r = r.json()
else:
# XXX: https://discuss.python.org/t/file-uris-in-python/15600
if url.startswith('file://'):
path = url[7:]
else:
path = url
url = 'file:' + url
with open(path) as f:
r = json.load(f)
if set_schema_id and r:
r.setdefault('$id', url)
return r
except Exception as e:
warnings.warn("Unable to load JSON %s (%s: %s)"
% (url, type(e).__name__, e))
class _RefResolver(jsonschema.validators.RefResolver): class _RefResolver(jsonschema.validators.RefResolver):
@classmethod
def from_schema(cls, schema, read_as_json):
instance = super(_RefResolver, cls).from_schema(schema)
instance._read_as_json = read_as_json
return instance
def resolve_remote(self, uri): def resolve_remote(self, uri):
result = _readAsJson(uri) result = self._read_as_json(uri)
if self.cache_remote: if self.cache_remote:
self.store[uri] = result self.store[uri] = result
return result return result
...@@ -484,8 +462,8 @@ class SoftwareReleaseSchemaValidationError(ValueError): ...@@ -484,8 +462,8 @@ class SoftwareReleaseSchemaValidationError(ValueError):
class SoftwareReleaseSchema(object): class SoftwareReleaseSchema(object):
def __init__(self, software_url, software_type): def __init__(self, software_url, software_type, download=None):
# type: (str, Optional[str]) -> None # type: (str, Optional[str], Optional[zc.buildout.download.Download]) -> None
self.software_url = software_url self.software_url = software_url
# XXX: Transition from OLD_DEFAULT_SOFTWARE_TYPE ("RootSoftwareInstance") # XXX: Transition from OLD_DEFAULT_SOFTWARE_TYPE ("RootSoftwareInstance")
# to DEFAULT_SOFTWARE_TYPE ("default") is already complete for SR schemas. # to DEFAULT_SOFTWARE_TYPE ("default") is already complete for SR schemas.
...@@ -493,6 +471,9 @@ class SoftwareReleaseSchema(object): ...@@ -493,6 +471,9 @@ class SoftwareReleaseSchema(object):
if software_type == OLD_DEFAULT_SOFTWARE_TYPE: if software_type == OLD_DEFAULT_SOFTWARE_TYPE:
software_type = None software_type = None
self.software_type = software_type or DEFAULT_SOFTWARE_TYPE self.software_type = software_type or DEFAULT_SOFTWARE_TYPE
if download is None:
download = zc.buildout.download.Download()
self._download = download.download
def _warn(self, message, e): def _warn(self, message, e):
warnings.warn( warnings.warn(
...@@ -500,6 +481,37 @@ class SoftwareReleaseSchema(object): ...@@ -500,6 +481,37 @@ class SoftwareReleaseSchema(object):
% (message, self.software_type, self.software_url, type(e).__name__, e), % (message, self.software_type, self.software_url, type(e).__name__, e),
stacklevel=2) stacklevel=2)
def _readAsJson(self, url, set_schema_id=False):
# type: (str, bool) -> Optional[Dict]
"""Reads and parse the json file located at `url`.
`url` can also be the path of a local file.
"""
try:
if url.startswith('http://') or url.startswith('https://'):
path, is_temp = self._download(url)
try:
with open(path) as f:
r = json.load(f)
finally:
if is_temp:
os.remove(path)
else:
# XXX: https://discuss.python.org/t/file-uris-in-python/15600
if url.startswith('file://'):
path = url[7:]
else:
path = url
url = 'file:' + url
with open(path) as f:
r = json.load(f)
if set_schema_id and r:
r.setdefault('$id', url)
return r
except Exception as e:
warnings.warn("Unable to load JSON %s (%s: %s)"
% (url, type(e).__name__, e))
def getSoftwareSchema(self): def getSoftwareSchema(self):
# type: () -> Optional[Dict] # type: () -> Optional[Dict]
"""Returns the schema for this software. """Returns the schema for this software.
...@@ -507,7 +519,7 @@ class SoftwareReleaseSchema(object): ...@@ -507,7 +519,7 @@ class SoftwareReleaseSchema(object):
try: try:
return self._software_schema return self._software_schema
except AttributeError: except AttributeError:
schema = self._software_schema = _readAsJson(self.software_url + '.json') schema = self._software_schema = self._readAsJson(self.software_url + '.json')
return schema return schema
def getSoftwareTypeSchema(self): def getSoftwareTypeSchema(self):
...@@ -556,7 +568,7 @@ class SoftwareReleaseSchema(object): ...@@ -556,7 +568,7 @@ class SoftwareReleaseSchema(object):
return self._request_schema return self._request_schema
except AttributeError: except AttributeError:
url = self.getInstanceRequestParameterSchemaURL() url = self.getInstanceRequestParameterSchemaURL()
schema = None if url is None else _readAsJson(url, True) schema = None if url is None else self._readAsJson(url, True)
self._request_schema = schema self._request_schema = schema
return schema return schema
...@@ -577,7 +589,7 @@ class SoftwareReleaseSchema(object): ...@@ -577,7 +589,7 @@ class SoftwareReleaseSchema(object):
return self._response_schema return self._response_schema
except AttributeError: except AttributeError:
url = self.getInstanceConnectionParameterSchemaURL() url = self.getInstanceConnectionParameterSchemaURL()
schema = None if url is None else _readAsJson(url, True) schema = None if url is None else self._readAsJson(url, True)
self._response_schema = schema self._response_schema = schema
return schema return schema
...@@ -599,7 +611,7 @@ class SoftwareReleaseSchema(object): ...@@ -599,7 +611,7 @@ class SoftwareReleaseSchema(object):
validator = jsonschema.validators.validator_for(schema)( validator = jsonschema.validators.validator_for(schema)(
schema, schema,
resolver=_RefResolver.from_schema(schema), resolver=_RefResolver.from_schema(schema, self._readAsJson),
) )
errors = list(validator.iter_errors(parameter_dict)) errors = list(validator.iter_errors(parameter_dict))
if errors: if errors:
......
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