Commit 2b80692d authored by Julien Muchembled's avatar Julien Muchembled

Clean up SoftwareReleaseSchema

This is mainly about error handling, simpler and more useful.

- Always warn when a valid SR can't be loaded, with a message that
  contains the original exception (for example, compared to before
  this commit, it will tell if a JSON file can't be found,
  or where JSON has syntax errors).

- Same as previous point if serialisation type is invalid or missing.
  If the caller needs it to transform parameters (parameters file),
  it will raise with the original exception. Otherwise, it falls back
  on json-in-xml. In some places, such fallback is a change of
  behaviour and I have no opinion about it except that at least
  it's now consistent throughout slapos.core.

- Remove warning about RootSoftwareInstance/default: meaningless
  because contradicted the comment, and useless because the transition
  to 'default' is already complete for SR schemas. There are still 3
  lines of backward compatibility code for the rest of slapos.core.

- Don't read the same file several times. Note however that this
  performance fix is only for the SoftwareReleaseSchema class:
  the caller should be fixed to not instanciate several times with
  the same parameters (from do_request & _requestComputerPartition).

See merge request nexedi/slapos.core!621
parent 32bca087
...@@ -40,7 +40,6 @@ from slapos.util import ( ...@@ -40,7 +40,6 @@ from slapos.util import (
SoftwareReleaseSchema, SoftwareReleaseSchema,
SoftwareReleaseSerialisation, SoftwareReleaseSerialisation,
StrPrettyPrinter, StrPrettyPrinter,
UndefinedSerializationError,
xml2dict, xml2dict,
) )
...@@ -94,11 +93,7 @@ def do_info(logger, conf, local): ...@@ -94,11 +93,7 @@ def do_info(logger, conf, local):
else: else:
# this is slapproxy connection dict # this is slapproxy connection dict
connection_parameter_dict = xml2dict(instance._connection_dict) connection_parameter_dict = xml2dict(instance._connection_dict)
try: if software_schema.getSerialisation() == SoftwareReleaseSerialisation.JsonInXml:
software_serialisation = software_schema.getSerialisation()
except UndefinedSerializationError:
software_serialisation = SoftwareReleaseSerialisation.JsonInXml
if software_serialisation == SoftwareReleaseSerialisation.JsonInXml:
if '_' in connection_parameter_dict: if '_' in connection_parameter_dict:
connection_parameter_dict = json.loads(connection_parameter_dict['_']) connection_parameter_dict = json.loads(connection_parameter_dict['_'])
......
...@@ -41,7 +41,7 @@ from slapos.client import (ClientConfig, _getSoftwareReleaseFromSoftwareString, ...@@ -41,7 +41,7 @@ from slapos.client import (ClientConfig, _getSoftwareReleaseFromSoftwareString,
init) init)
from slapos.slap import ResourceNotReady from slapos.slap import ResourceNotReady
from slapos.util import (SoftwareReleaseSchema, SoftwareReleaseSerialisation, from slapos.util import (SoftwareReleaseSchema, SoftwareReleaseSerialisation,
UndefinedSerializationError, StrPrettyPrinter) StrPrettyPrinter)
try: try:
from typing import IO, Dict from typing import IO, Dict
...@@ -153,13 +153,13 @@ def do_request(logger, conf, local): ...@@ -153,13 +153,13 @@ def do_request(logger, conf, local):
conf.software_url = local[conf.software_url] conf.software_url = local[conf.software_url]
software_schema = SoftwareReleaseSchema(conf.software_url, conf.type) software_schema = SoftwareReleaseSchema(conf.software_url, conf.type)
parameters = conf.parameters
if conf.parameters_file: if conf.parameters_file:
if conf.force_serialisation: serialisation = conf.force_serialisation
parameters = getParametersFromFile(conf.parameters_file, SoftwareReleaseSerialisation(conf.force_serialisation)) parameters = getParametersFromFile(conf.parameters_file,
SoftwareReleaseSerialisation(serialisation) if serialisation else
software_schema.getSerialisation(strict=True))
else: else:
# getSerialisation will throw an exception if serialization cannot be found parameters = conf.parameters
parameters = getParametersFromFile(conf.parameters_file, software_schema.getSerialisation())
try: try:
partition = local['slap'].registerOpenOrder().request( partition = local['slap'].registerOpenOrder().request(
software_release=conf.software_url, software_release=conf.software_url,
...@@ -173,11 +173,10 @@ def do_request(logger, conf, local): ...@@ -173,11 +173,10 @@ def do_request(logger, conf, local):
logger.info('Instance requested.\nState is : %s.', partition.getState()) logger.info('Instance requested.\nState is : %s.', partition.getState())
logger.info('Connection parameters of instance are:') logger.info('Connection parameters of instance are:')
connection_parameter_dict = partition.getConnectionParameterDict() connection_parameter_dict = partition.getConnectionParameterDict()
try:
if software_schema.getSerialisation() == SoftwareReleaseSerialisation.JsonInXml: if software_schema.getSerialisation() == SoftwareReleaseSerialisation.JsonInXml:
if '_' in connection_parameter_dict: try:
connection_parameter_dict = json.loads(connection_parameter_dict['_']) connection_parameter_dict = json.loads(connection_parameter_dict['_'])
except UndefinedSerializationError: except KeyError:
pass pass
logger.info(StrPrettyPrinter().pformat(connection_parameter_dict)) logger.info(StrPrettyPrinter().pformat(connection_parameter_dict))
logger.info('You can rerun the command to get up-to-date information.') logger.info('You can rerun the command to get up-to-date information.')
......
...@@ -49,7 +49,7 @@ import six ...@@ -49,7 +49,7 @@ 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 (SoftwareReleaseSchema, UndefinedSerializationError, from slapos.util import (SoftwareReleaseSchema,
bytes2str, calculate_dict_hash, dict2xml, dumps, loads, bytes2str, calculate_dict_hash, dict2xml, dumps, loads,
unicode2str, xml2dict) unicode2str, xml2dict)
...@@ -110,11 +110,6 @@ class SlapRequester(SlapDocument): ...@@ -110,11 +110,6 @@ class SlapRequester(SlapDocument):
"Request parameters do not validate against schema definition:\n{e}".format(e=e), "Request parameters do not validate against schema definition:\n{e}".format(e=e),
UserWarning, UserWarning,
) )
except UndefinedSerializationError as e:
warnings.warn(
"No serialization type found:\n{e}".format(e=e),
UserWarning,
)
except Exception as e: except Exception as e:
# note that we intentionally catch wide exceptions, so that if anything # note that we intentionally catch wide exceptions, so that if anything
# is wrong with fetching the schema or the schema itself this does not # is wrong with fetching the schema or the schema itself this does not
......
...@@ -40,7 +40,7 @@ import pkg_resources ...@@ -40,7 +40,7 @@ import pkg_resources
from contextlib import contextmanager from contextlib import contextmanager
from mock import patch, create_autospec from mock import patch, create_autospec
import mock import mock
from slapos.util import sqlite_connect, bytes2str, UndefinedSerializationError, dict2xml from slapos.util import sqlite_connect, bytes2str, dict2xml
from slapos.slap.slap import DEFAULT_SOFTWARE_TYPE from slapos.slap.slap import DEFAULT_SOFTWARE_TYPE
import slapos.cli.console import slapos.cli.console
...@@ -83,7 +83,9 @@ x2IMeSwJ82BpdEI5niXxB+iT0HxhmR+XaMI= ...@@ -83,7 +83,9 @@ x2IMeSwJ82BpdEI5niXxB+iT0HxhmR+XaMI=
def raiseNotFoundError(*args, **kwargs): def raiseNotFoundError(*args, **kwargs):
raise slapos.slap.NotFoundError() raise slapos.slap.NotFoundError()
class CliMixin(unittest.TestCase): class CliMixin(unittest.TestCase):
def setUp(self): def setUp(self):
slap = slapos.slap.slap() slap = slapos.slap.slap()
self.logger = create_autospec(logging.Logger) self.logger = create_autospec(logging.Logger)
...@@ -91,6 +93,24 @@ class CliMixin(unittest.TestCase): ...@@ -91,6 +93,24 @@ class CliMixin(unittest.TestCase):
self.conf = create_autospec(ClientConfig) self.conf = create_autospec(ClientConfig)
self.sign_cert_list = signature_certificate_list self.sign_cert_list = signature_certificate_list
def _do_request(self, connection_dict={}, json_in_xml=False):
cp = slapos.slap.ComputerPartition(
'computer_' + self.id(),
'partition_' + self.id())
cp._requested_state = 'started'
cp._connection_dict = {'_': json.dumps(connection_dict)} \
if json_in_xml else connection_dict
with patch.object(
slapos.slap.slap,
'registerOpenOrder',
return_value=mock.create_autospec(slapos.slap.OpenOrder)) as registerOpenOrder:
registerOpenOrder().request.return_value = cp
slapos.cli.request.do_request(self.logger, self.conf, self.local)
return registerOpenOrder().request
class TestCliCacheBinarySr(CliMixin): class TestCliCacheBinarySr(CliMixin):
test_url = "https://lab.nexedi.com/nexedi/slapos/raw/1.0.102/software/slaprunner/software.cfg" test_url = "https://lab.nexedi.com/nexedi/slapos/raw/1.0.102/software/slaprunner/software.cfg"
...@@ -901,9 +921,9 @@ class TestCliRequest(CliMixin): ...@@ -901,9 +921,9 @@ class TestCliRequest(CliMixin):
self.assertEqual(parse_option_dict(['a=a\nb']), {'a': 'a\nb'}) self.assertEqual(parse_option_dict(['a=a\nb']), {'a': 'a\nb'})
self.assertEqual(parse_option_dict([]), {}) self.assertEqual(parse_option_dict([]), {})
def test_request(self): def _test_request(self, software_url, json_in_xml=False):
self.conf.reference = 'instance reference' self.conf.reference = 'instance reference'
self.conf.software_url = 'software URL' self.conf.software_url = software_url
self.conf.parameters = {'key': 'value'} self.conf.parameters = {'key': 'value'}
self.conf.parameters_file = None self.conf.parameters_file = None
self.conf.node = {'computer_guid': 'COMP-1234'} self.conf.node = {'computer_guid': 'COMP-1234'}
...@@ -912,14 +932,9 @@ class TestCliRequest(CliMixin): ...@@ -912,14 +932,9 @@ class TestCliRequest(CliMixin):
self.conf.slave = False self.conf.slave = False
self.conf.force_serialisation = None self.conf.force_serialisation = None
with patch.object( connection_dict = {'foo': 'bar'}
slapos.slap.slap, self._do_request(connection_dict, json_in_xml).assert_called_once_with(
'registerOpenOrder', software_release=software_url,
return_value=mock.create_autospec(slapos.slap.OpenOrder)) as registerOpenOrder:
slapos.cli.request.do_request(self.logger, self.conf, self.local)
registerOpenOrder().request.assert_called_once_with(
software_release='software URL',
partition_reference='instance reference', partition_reference='instance reference',
partition_parameter_kw={'key': 'value'}, partition_parameter_kw={'key': 'value'},
software_type=None, software_type=None,
...@@ -927,17 +942,23 @@ class TestCliRequest(CliMixin): ...@@ -927,17 +942,23 @@ class TestCliRequest(CliMixin):
state=None, state=None,
shared=False, shared=False,
) )
self.assertEqual(self.logger.info.mock_calls, [
mock.call('Requesting %s as instance of %s...',
'instance reference', software_url),
mock.call('Instance requested.\nState is : %s.', 'started'),
mock.call('Connection parameters of instance are:'),
mock.call("{'foo': 'bar'}"),
mock.call('You can rerun the command to get up-to-date information.'),
])
self.logger.info.assert_any_call( def test_request(self):
'Requesting %s as instance of %s...', self._test_request('software URL', {'foo': 'bar'})
'instance reference',
'software URL',
)
def test_request_json_in_xml_published_parameters(self): def test_request_json_in_xml_published_parameters(self):
tmpdir = tempfile.mkdtemp() tmpdir = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, tmpdir) self.addCleanup(shutil.rmtree, tmpdir)
with open(os.path.join(tmpdir, 'software.cfg.json'), 'w') as f: sr = os.path.join(tmpdir, 'software.cfg')
with open(sr + '.json', 'w') as f:
json.dump( json.dump(
{ {
"name": "Test Software", "name": "Test Software",
...@@ -953,40 +974,7 @@ class TestCliRequest(CliMixin): ...@@ -953,40 +974,7 @@ class TestCliRequest(CliMixin):
}, },
} }
}, f) }, f)
self._test_request(sr, True)
self.conf.reference = 'instance reference'
self.conf.software_url = os.path.join(tmpdir, 'software.cfg')
self.conf.parameters = {'key': 'value'}
self.conf.parameters_file = None
self.conf.node = {'computer_guid': 'COMP-1234'}
self.conf.type = None
self.conf.state = None
self.conf.slave = False
self.conf.force_serialisation = None
cp = slapos.slap.ComputerPartition(
'computer_%s' % self.id(),
'partition_%s' % self.id())
cp._requested_state = 'started'
cp._connection_dict = {'_': json.dumps({'foo': 'bar'})}
with patch.object(
slapos.slap.slap,
'registerOpenOrder',
return_value=mock.create_autospec(slapos.slap.OpenOrder)) as registerOpenOrder:
registerOpenOrder().request.return_value = cp
slapos.cli.request.do_request(self.logger, self.conf, self.local)
registerOpenOrder().request.assert_called_once()
self.assertEqual(self.logger.info.mock_calls, [
mock.call('Requesting %s as instance of %s...', self.conf.reference,
self.conf.software_url),
mock.call('Instance requested.\nState is : %s.', 'started'),
mock.call('Connection parameters of instance are:'),
mock.call("{'foo': 'bar'}"),
mock.call('You can rerun the command to get up-to-date information.'),
])
class TestCliRequestParameterFile(CliMixin): class TestCliRequestParameterFile(CliMixin):
...@@ -1031,7 +1019,7 @@ class TestCliRequestParameterFileUndefinedSerialization(TestCliRequestParameterF ...@@ -1031,7 +1019,7 @@ class TestCliRequestParameterFileUndefinedSerialization(TestCliRequestParameterF
def test_request_parameters_file(self): def test_request_parameters_file(self):
self._request_parameters_file_setup() self._request_parameters_file_setup()
self.assertRaises( self.assertRaises(
UndefinedSerializationError, TypeError,
slapos.cli.request.do_request, slapos.cli.request.do_request,
self.logger, self.logger,
self.conf, self.conf,
...@@ -1055,13 +1043,9 @@ class TestCliRequestParametersFileJson(TestCliRequestParameterFile): ...@@ -1055,13 +1043,9 @@ class TestCliRequestParametersFileJson(TestCliRequestParameterFile):
with mock.patch( with mock.patch(
'slapos.cli.request.SoftwareReleaseSchema.getSerialisation', 'slapos.cli.request.SoftwareReleaseSchema.getSerialisation',
return_value=self.serialization): return_value=self.serialization):
with patch.object( self._do_request(
slapos.slap.slap, json_in_xml=self.serialization!='xml',
'registerOpenOrder', ).assert_called_once_with(
return_value=mock.create_autospec(slapos.slap.OpenOrder)) as registerOpenOrder:
slapos.cli.request.do_request(self.logger, self.conf, self.local)
registerOpenOrder().request.assert_called_once_with(
software_release='software URL', software_release='software URL',
partition_reference='instance reference', partition_reference='instance reference',
partition_parameter_kw=self.expected_partition_parameter_kw, partition_parameter_kw=self.expected_partition_parameter_kw,
...@@ -1138,13 +1122,7 @@ class TestCliRequestForceSerialisation(TestCliRequestParameterFile): ...@@ -1138,13 +1122,7 @@ class TestCliRequestForceSerialisation(TestCliRequestParameterFile):
def test_request_parameters_file(self): def test_request_parameters_file(self):
self._request_parameters_file_setup() self._request_parameters_file_setup()
self.conf.force_serialisation = 'json-in-xml' self.conf.force_serialisation = 'json-in-xml'
with patch.object( self._do_request(json_in_xml=True).assert_called_once_with(
slapos.slap.slap,
'registerOpenOrder',
return_value=mock.create_autospec(slapos.slap.OpenOrder)) as registerOpenOrder:
slapos.cli.request.do_request(self.logger, self.conf, self.local)
registerOpenOrder().request.assert_called_once_with(
software_release='software URL', software_release='software URL',
partition_reference='instance reference', partition_reference='instance reference',
partition_parameter_kw=self.expected_partition_parameter_kw, partition_parameter_kw=self.expected_partition_parameter_kw,
......
...@@ -1036,11 +1036,11 @@ class TestComputerPartition(SlapMixin): ...@@ -1036,11 +1036,11 @@ class TestComputerPartition(SlapMixin):
}) })
raise ValueError(404) raise ValueError(404)
for handler, warning_expected in ( for handler in (
(broken_reference, True), broken_reference,
(wrong_software_cfg_schema, False), wrong_software_cfg_schema,
(wrong_instance_parameter_schema, True), wrong_instance_parameter_schema,
(invalid_instance_parameter_schema, True), invalid_instance_parameter_schema,
): ):
with httmock.HTTMock(handler): with httmock.HTTMock(handler):
with mock.patch.object(warnings, 'warn') as warn: with mock.patch.object(warnings, 'warn') as warn:
...@@ -1050,10 +1050,7 @@ class TestComputerPartition(SlapMixin): ...@@ -1050,10 +1050,7 @@ class TestComputerPartition(SlapMixin):
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'})
if warning_expected:
warn.assert_called() warn.assert_called()
else:
warn.assert_not_called()
def _test_new_computer_partition_state(self, state): def _test_new_computer_partition_state(self, state):
""" """
......
...@@ -38,7 +38,6 @@ from six.moves import SimpleHTTPServer ...@@ -38,7 +38,6 @@ from six.moves import SimpleHTTPServer
import jsonschema import jsonschema
import slapos.util import slapos.util
from slapos.slap.slap import DEFAULT_SOFTWARE_TYPE
from slapos.testing.utils import ManagedHTTPServer from slapos.testing.utils import ManagedHTTPServer
from slapos.util import (SoftwareReleaseSchema, SoftwareReleaseSerialisation, from slapos.util import (SoftwareReleaseSchema, SoftwareReleaseSerialisation,
string_to_boolean, unicode2str) string_to_boolean, unicode2str)
...@@ -270,11 +269,11 @@ class SoftwareReleaseSchemaTestMixin(object): ...@@ -270,11 +269,11 @@ class SoftwareReleaseSchemaTestMixin(object):
def test_serialisation(self): def test_serialisation(self):
schema = SoftwareReleaseSchema(self.software_url, None) schema = SoftwareReleaseSchema(self.software_url, None)
self.assertEqual(schema.getSerialisation(), self.serialisation) self.assertEqual(schema.getSerialisation(strict=True), self.serialisation)
def test_serialisation_alternate_software_type(self): def test_serialisation_alternate_software_type(self):
schema = SoftwareReleaseSchema(self.software_url, 'alternate') schema = SoftwareReleaseSchema(self.software_url, 'alternate')
self.assertEqual(schema.getSerialisation(), self.serialisation_alt) self.assertEqual(schema.getSerialisation(strict=True), self.serialisation_alt)
def test_instance_request_parameter_schema_default_software_type(self): def test_instance_request_parameter_schema_default_software_type(self):
schema = SoftwareReleaseSchema(self.software_url, None) schema = SoftwareReleaseSchema(self.software_url, None)
...@@ -354,7 +353,7 @@ class SoftwareReleaseSchemaTestFileSoftwareReleaseMixin(SoftwareReleaseSchemaTes ...@@ -354,7 +353,7 @@ class SoftwareReleaseSchemaTestFileSoftwareReleaseMixin(SoftwareReleaseSchemaTes
"description": "Dummy software for Test", "description": "Dummy software for Test",
"serialisation": self.serialisation, "serialisation": self.serialisation,
"software-type": { "software-type": {
DEFAULT_SOFTWARE_TYPE: { 'default': {
"title": "Default", "title": "Default",
"description": "Default type", "description": "Default type",
"request": "instance-default-input-schema.json", "request": "instance-default-input-schema.json",
...@@ -373,9 +372,8 @@ class SoftwareReleaseSchemaTestFileSoftwareReleaseMixin(SoftwareReleaseSchemaTes ...@@ -373,9 +372,8 @@ class SoftwareReleaseSchemaTestFileSoftwareReleaseMixin(SoftwareReleaseSchemaTes
}, f) }, f)
for software_type in ('default', 'alternate'): for software_type in ('default', 'alternate'):
with open( with open(tmpfile('instance-%s-input-schema.json' % software_type),
tmpfile('instance-{software_type}-input-schema.json'.format( 'w') as f:
software_type=software_type)), 'w') as f:
json.dump( json.dump(
{ {
"$schema": "http://json-schema.org/draft-07/schema", "$schema": "http://json-schema.org/draft-07/schema",
...@@ -392,9 +390,8 @@ class SoftwareReleaseSchemaTestFileSoftwareReleaseMixin(SoftwareReleaseSchemaTes ...@@ -392,9 +390,8 @@ class SoftwareReleaseSchemaTestFileSoftwareReleaseMixin(SoftwareReleaseSchemaTes
}, },
"type": "object" "type": "object"
}, f) }, f)
with open( with open(tmpfile('instance-%s-output-schema.json' % software_type),
tmpfile('instance-{software_type}-output-schema.json'.format( 'w') as f:
software_type=software_type)), 'w') as f:
json.dump( json.dump(
{ {
"$schema": "http://json-schema.org/draft-07/schema", "$schema": "http://json-schema.org/draft-07/schema",
......
...@@ -72,11 +72,6 @@ _ALLOWED_CLASS_SET = frozenset(( ...@@ -72,11 +72,6 @@ _ALLOWED_CLASS_SET = frozenset((
)) ))
class UndefinedSerializationError(ValueError):
"""Raised when the serialization type is not found"""
pass
class SafeXMLMarshaller(Marshaller): class SafeXMLMarshaller(Marshaller):
def m_instance(self, value, kw): def m_instance(self, value, kw):
cls = value.__class__ cls = value.__class__
...@@ -406,30 +401,32 @@ def rmtree(path): ...@@ -406,30 +401,32 @@ def rmtree(path):
def _readAsJson(url): def _readAsJson(url, set_schema_id=False):
# type: (str) -> Optional[Dict] # type: (str) -> Optional[Dict]
"""Reads and parse the json file located at `url`. """Reads and parse the json file located at `url`.
`url` can also be the path of a local file. `url` can also be the path of a local file.
""" """
if url.startswith('file://'):
url = url[len('file://'):]
path = url if os.path.exists(url) else None
if path:
with open(path) as f:
try: try:
return json.load(f)
except ValueError:
return None
if url.startswith('http://') or url.startswith('https://'): if url.startswith('http://') or url.startswith('https://'):
try:
r = requests.get(url, timeout=60) # we need a timeout ! r = requests.get(url, timeout=60) # we need a timeout !
r.raise_for_status() r.raise_for_status()
return r.json() r = r.json()
except (requests.exceptions.RequestException, ValueError): else:
return None # XXX: https://discuss.python.org/t/file-uris-in-python/15600
return None 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 SoftwareReleaseSerialisation(str, enum.Enum): class SoftwareReleaseSerialisation(str, enum.Enum):
...@@ -438,53 +435,61 @@ class SoftwareReleaseSerialisation(str, enum.Enum): ...@@ -438,53 +435,61 @@ class SoftwareReleaseSerialisation(str, enum.Enum):
class SoftwareReleaseSchema(object): class SoftwareReleaseSchema(object):
def __init__(self, software_url, software_type): def __init__(self, software_url, software_type):
# type: (str, Optional[str]) -> None # type: (str, Optional[str]) -> None
self.software_url = software_url self.software_url = software_url
self.software_type = software_type # XXX: Transition from DEFAULT_SOFTWARE_TYPE ("RootSoftwareInstance")
# to "default" is already complete for SR schemas.
from slapos.slap.slap import DEFAULT_SOFTWARE_TYPE
if software_type == DEFAULT_SOFTWARE_TYPE:
software_type = None
self.software_type = software_type or 'default'
def _warn(self, message, e):
warnings.warn(
"%s for software type %r of software release %s (%s: %s)"
% (message, self.software_type, self.software_url, type(e).__name__, e),
stacklevel=2)
def getSoftwareSchema(self): def getSoftwareSchema(self):
# type: () -> Optional[Dict] # type: () -> Optional[Dict]
"""Returns the schema for this software. """Returns the schema for this software.
""" """
return _readAsJson(self.software_url + '.json') try:
return self._software_schema
except AttributeError:
schema = self._software_schema = _readAsJson(self.software_url + '.json')
return schema
def getSoftwareTypeSchema(self): def getSoftwareTypeSchema(self):
# type: () -> Optional[Dict] # type: () -> Optional[Dict]
"""Returns schema for this software type. """Returns schema for this software type.
""" """
software_schema = self.getSoftwareSchema() software_schema = self.getSoftwareSchema()
if software_schema is None: if software_schema is not None:
return None try:
return software_schema['software-type'][self.software_type]
software_type = self.software_type except Exception as e:
from slapos.slap.slap import DEFAULT_SOFTWARE_TYPE self._warn("No schema defined", e)
if software_type is None:
software_type = DEFAULT_SOFTWARE_TYPE
# XXX Some software are using "default" for default software type, because
# we are transitionning from DEFAULT_SOFTWARE_TYPE ("RootSoftwareInstance")
# to "default".
if software_type == DEFAULT_SOFTWARE_TYPE \
and software_type not in software_schema['software-type'] \
and 'default' in software_schema['software-type']:
warnings.warn(
"Software release {} does not have schema for DEFAULT_SOFTWARE_TYPE but has one for 'default'."
" Using 'default' instead.".format(self.software_url),
UserWarning,
)
software_type = 'default'
return software_schema['software-type'].get(software_type)
def getSerialisation(self): def getSerialisation(self, strict=False):
# type: () -> SoftwareReleaseSerialisation # type: (bool) -> SoftwareReleaseSerialisation
"""Returns the serialisation method used for parameters. """Returns the serialisation method used for parameters.
If strict is False, catch exceptions and return JsonInXml.
""" """
software_schema = self.getSoftwareTypeSchema() software_schema = self.getSoftwareTypeSchema()
if software_schema is None or 'serialisation' not in software_schema: if software_schema is None or 'serialisation' not in software_schema:
software_schema = self.getSoftwareSchema() software_schema = self.getSoftwareSchema()
if software_schema is None: try:
raise UndefinedSerializationError
return SoftwareReleaseSerialisation(software_schema['serialisation']) return SoftwareReleaseSerialisation(software_schema['serialisation'])
except Exception as e:
if software_schema is not None: # else there was already a warning
self._warn("Invalid or undefined serialisation", e)
if strict:
raise
return SoftwareReleaseSerialisation.JsonInXml
def getInstanceRequestParameterSchemaURL(self): def getInstanceRequestParameterSchemaURL(self):
# type: () -> Optional[str] # type: () -> Optional[str]
...@@ -493,22 +498,18 @@ class SoftwareReleaseSchema(object): ...@@ -493,22 +498,18 @@ class SoftwareReleaseSchema(object):
software_type_schema = self.getSoftwareTypeSchema() software_type_schema = self.getSoftwareTypeSchema()
if software_type_schema is None: if software_type_schema is None:
return None return None
software_url = self.software_url return urljoin(self.software_url, software_type_schema['request'])
if os.path.exists(software_url):
software_url = 'file://' + software_url
return urljoin(software_url, software_type_schema['request'])
def getInstanceRequestParameterSchema(self): def getInstanceRequestParameterSchema(self):
# type: () -> Optional[Dict] # type: () -> Optional[Dict]
"""Returns the schema defining instance parameters. """Returns the schema defining instance parameters.
""" """
instance_parameter_schema_url = self.getInstanceRequestParameterSchemaURL() try:
if instance_parameter_schema_url is None: return self._request_schema
return None except AttributeError:
schema = _readAsJson(instance_parameter_schema_url) url = self.getInstanceRequestParameterSchemaURL()
if schema: schema = None if url is None else _readAsJson(url, True)
# so that jsonschema knows how to resolve references self._request_schema = schema
schema.setdefault('$id', instance_parameter_schema_url)
return schema return schema
def getInstanceConnectionParameterSchemaURL(self): def getInstanceConnectionParameterSchemaURL(self):
...@@ -524,13 +525,12 @@ class SoftwareReleaseSchema(object): ...@@ -524,13 +525,12 @@ class SoftwareReleaseSchema(object):
# type: () -> Optional[Dict] # type: () -> Optional[Dict]
"""Returns the schema defining connection parameters published by the instance. """Returns the schema defining connection parameters published by the instance.
""" """
instance_parameter_schema_url = self.getInstanceConnectionParameterSchemaURL() try:
if instance_parameter_schema_url is None: return self._response_schema
return None except AttributeError:
schema = _readAsJson(instance_parameter_schema_url) url = self.getInstanceConnectionParameterSchemaURL()
if schema: schema = None if url is None else _readAsJson(url, True)
# so that jsonschema knows how to resolve references self._response_schema = schema
schema.setdefault('$id', instance_parameter_schema_url)
return schema return schema
def validateInstanceParameterDict(self, parameter_dict): def validateInstanceParameterDict(self, parameter_dict):
...@@ -539,19 +539,15 @@ class SoftwareReleaseSchema(object): ...@@ -539,19 +539,15 @@ class SoftwareReleaseSchema(object):
Raise jsonschema.ValidationError if parameters does not validate. Raise jsonschema.ValidationError if parameters does not validate.
""" """
schema_url = self.getInstanceRequestParameterSchemaURL() schema = self.getInstanceRequestParameterSchema()
if schema_url: if schema:
instance = parameter_dict if self.getSerialisation(strict=True) == SoftwareReleaseSerialisation.JsonInXml:
if self.getSerialisation() == SoftwareReleaseSerialisation.JsonInXml:
try: try:
instance = json.loads(parameter_dict['_']) parameter_dict = json.loads(parameter_dict['_'])
except KeyError: except KeyError:
instance = parameter_dict pass
instance.pop('$schema', None) parameter_dict.pop('$schema', None)
jsonschema.validate( jsonschema.validate(instance=parameter_dict, schema=schema)
instance=instance,
schema=self.getInstanceRequestParameterSchema(),
)
# BBB on python3 we can use pprint.pformat # BBB on python3 we can use pprint.pformat
......
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