Added resource validation
- uses jsonschema
This commit is contained in:
parent
51674c00ad
commit
e86ac3f837
@ -2,3 +2,4 @@ click==4.0
|
||||
jinja2==2.7.3
|
||||
networkx==1.9.1
|
||||
PyYAML==3.11
|
||||
jsonschema==2.4.0
|
||||
|
@ -17,8 +17,9 @@ fi
|
||||
|
||||
pip install -r requirements.txt --download-cache=/tmp/$JOB_NAME
|
||||
|
||||
pushd x
|
||||
pushd solar/solar
|
||||
|
||||
PYTHONPATH=$WORKSPACE CONFIG_FILE=$CONFIG_FILE python test/test_signals.py
|
||||
PYTHONPATH=$WORKSPACE/solar CONFIG_FILE=$CONFIG_FILE python test/test_signals.py
|
||||
PYTHONPATH=$WORKSPACE/solar CONFIG_FILE=$CONFIG_FILE python test/test_validation.py
|
||||
|
||||
popd
|
||||
|
@ -13,6 +13,7 @@ from solar.core import db
|
||||
from solar.core import observer
|
||||
from solar.core import signals
|
||||
from solar.core import utils
|
||||
from solar.core import validation
|
||||
|
||||
|
||||
class Resource(object):
|
||||
@ -21,14 +22,12 @@ class Resource(object):
|
||||
self.base_dir = base_dir
|
||||
self.metadata = metadata
|
||||
self.actions = metadata['actions'].keys() if metadata['actions'] else None
|
||||
self.requires = metadata['input'].keys()
|
||||
self._validate_args(args, metadata['input'])
|
||||
self.args = {}
|
||||
for arg_name, arg_value in args.items():
|
||||
type_ = metadata.get('input-types', {}).get(arg_name) or 'simple'
|
||||
metadata_arg = self.metadata['input'][arg_name]
|
||||
type_ = validation.schema_input_type(metadata_arg.get('schema', 'str'))
|
||||
|
||||
self.args[arg_name] = observer.create(type_, self, arg_name, arg_value)
|
||||
self.metadata['input'] = args
|
||||
self.input_types = metadata.get('input-types', {})
|
||||
self.changed = []
|
||||
self.tags = tags or []
|
||||
|
||||
@ -95,22 +94,13 @@ class Resource(object):
|
||||
else:
|
||||
raise Exception('Uuups, action is not available')
|
||||
|
||||
def _validate_args(self, args, inputs):
|
||||
for req in self.requires:
|
||||
if req not in args:
|
||||
# If metadata input is filled with a value, use it as default
|
||||
# and don't report an error
|
||||
if inputs.get(req):
|
||||
args[req] = inputs[req]
|
||||
else:
|
||||
raise Exception('Requirement `{0}` is missing in args'.format(req))
|
||||
|
||||
# TODO: versioning
|
||||
def save(self):
|
||||
metadata = copy.deepcopy(self.metadata)
|
||||
|
||||
metadata['tags'] = self.tags
|
||||
metadata['input'] = self.args_dict()
|
||||
for k, v in self.args_dict().items():
|
||||
metadata['input'][k]['value'] = v
|
||||
|
||||
meta_file = os.path.join(self.base_dir, 'meta.yaml')
|
||||
with open(meta_file, 'w') as f:
|
||||
|
@ -83,8 +83,8 @@ def guess_mapping(emitter, receiver):
|
||||
:return:
|
||||
"""
|
||||
guessed = {}
|
||||
for key in emitter.requires:
|
||||
if key in receiver.requires:
|
||||
for key in emitter.args:
|
||||
if key in receiver.args:
|
||||
guessed[key] = key
|
||||
|
||||
return guessed
|
||||
|
86
solar/solar/core/validation.py
Normal file
86
solar/solar/core/validation.py
Normal file
@ -0,0 +1,86 @@
|
||||
from jsonschema import validate, ValidationError
|
||||
|
||||
|
||||
def schema_input_type(schema):
|
||||
"""Input type from schema
|
||||
|
||||
:param schema:
|
||||
:return: simple/list
|
||||
"""
|
||||
if isinstance(schema, list):
|
||||
return 'list'
|
||||
|
||||
return 'simple'
|
||||
|
||||
|
||||
def construct_jsonschema(schema):
|
||||
"""Construct jsonschema from our metadata input schema.
|
||||
|
||||
:param schema:
|
||||
:return:
|
||||
"""
|
||||
|
||||
if schema == 'str':
|
||||
return {'type': 'string'}
|
||||
|
||||
if schema == 'str!':
|
||||
return {'type': 'string', 'minLength': 1}
|
||||
|
||||
if schema == 'int' or schema == 'int!':
|
||||
return {'type': 'number'}
|
||||
|
||||
if isinstance(schema, list):
|
||||
return {
|
||||
'type': 'array',
|
||||
'items': construct_jsonschema(schema[0]),
|
||||
}
|
||||
|
||||
if isinstance(schema, dict):
|
||||
return {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
k: construct_jsonschema(v) for k, v in schema.items()
|
||||
},
|
||||
'required': [k for k, v in schema.items() if
|
||||
isinstance(v, basestring) and v.endswith('!')],
|
||||
}
|
||||
|
||||
|
||||
def validate_input(value, jsonschema=None, schema=None):
|
||||
"""Validate single input according to schema.
|
||||
|
||||
:param value: Value to be validated
|
||||
:param schema: Dict in jsonschema format
|
||||
:param schema: Our custom, simplified schema
|
||||
:return: list with errors
|
||||
"""
|
||||
try:
|
||||
if jsonschema:
|
||||
validate(value, jsonschema)
|
||||
else:
|
||||
validate(value, construct_jsonschema(schema))
|
||||
except ValidationError as e:
|
||||
return [e.message]
|
||||
|
||||
|
||||
def validate_resource(r):
|
||||
"""Check if resource inputs correspond to schema.
|
||||
|
||||
:param r: Resource instance
|
||||
:return: dict, keys are input names, value is array with error.
|
||||
"""
|
||||
ret = {}
|
||||
|
||||
input_schemas = r.metadata['input']
|
||||
args = r.args_dict()
|
||||
|
||||
for input_name, input_definition in input_schemas.items():
|
||||
errors = validate_input(
|
||||
args.get(input_name),
|
||||
jsonschema=input_definition.get('jsonschema'),
|
||||
schema=input_definition.get('schema')
|
||||
)
|
||||
if errors:
|
||||
ret[input_name] = errors
|
||||
|
||||
return ret
|
@ -4,9 +4,9 @@ import tempfile
|
||||
import unittest
|
||||
import yaml
|
||||
|
||||
from x import db
|
||||
from x import resource as xr
|
||||
from x import signals as xs
|
||||
from solar.core import db
|
||||
from solar.core import resource as xr
|
||||
from solar.core import signals as xs
|
||||
|
||||
|
||||
class BaseResourceTest(unittest.TestCase):
|
||||
|
@ -2,7 +2,7 @@ import unittest
|
||||
|
||||
import base
|
||||
|
||||
from x import signals as xs
|
||||
from solar.core import signals as xs
|
||||
|
||||
|
||||
class TestBaseInput(base.BaseResourceTest):
|
||||
@ -12,7 +12,9 @@ id: sample
|
||||
handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
values: {}
|
||||
values:
|
||||
schema: {a: int, b: int}
|
||||
value: {}
|
||||
""")
|
||||
|
||||
sample1 = self.create_resource(
|
||||
@ -63,7 +65,11 @@ handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
ip:
|
||||
schema: string
|
||||
value:
|
||||
port:
|
||||
schema: int
|
||||
value:
|
||||
""")
|
||||
sample_ip_meta_dir = self.make_resource_meta("""
|
||||
id: sample-ip
|
||||
@ -71,6 +77,8 @@ handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
ip:
|
||||
schema: string
|
||||
value:
|
||||
""")
|
||||
sample_port_meta_dir = self.make_resource_meta("""
|
||||
id: sample-port
|
||||
@ -78,6 +86,8 @@ handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
port:
|
||||
schema: int
|
||||
value:
|
||||
""")
|
||||
|
||||
sample = self.create_resource(
|
||||
@ -109,6 +119,8 @@ handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
ip:
|
||||
schema: string
|
||||
value:
|
||||
""")
|
||||
|
||||
sample = self.create_resource(
|
||||
@ -149,6 +161,8 @@ handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
ip:
|
||||
schema: str
|
||||
value:
|
||||
""")
|
||||
|
||||
sample1 = self.create_resource(
|
||||
@ -171,6 +185,8 @@ handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
ip:
|
||||
schema: str
|
||||
value:
|
||||
""")
|
||||
list_input_single_meta_dir = self.make_resource_meta("""
|
||||
id: list-input-single
|
||||
@ -178,8 +194,8 @@ handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
ips:
|
||||
input-types:
|
||||
ips: list
|
||||
schema: [str]
|
||||
value: []
|
||||
""")
|
||||
|
||||
sample1 = self.create_resource(
|
||||
@ -248,7 +264,11 @@ handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
ip:
|
||||
schema: str
|
||||
value:
|
||||
port:
|
||||
schema: int
|
||||
value:
|
||||
""")
|
||||
list_input_multi_meta_dir = self.make_resource_meta("""
|
||||
id: list-input-multi
|
||||
@ -256,10 +276,11 @@ handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
ips:
|
||||
schema: [str]
|
||||
value:
|
||||
ports:
|
||||
input-types:
|
||||
ips: list
|
||||
ports: list
|
||||
schema: [int]
|
||||
value:
|
||||
""")
|
||||
|
||||
sample1 = self.create_resource(
|
||||
|
106
solar/solar/test/test_validation.py
Normal file
106
solar/solar/test/test_validation.py
Normal file
@ -0,0 +1,106 @@
|
||||
import unittest
|
||||
|
||||
import base
|
||||
|
||||
from solar.core import validation as sv
|
||||
|
||||
|
||||
class TestInputValidation(base.BaseResourceTest):
|
||||
def test_input_str_type(self):
|
||||
sample_meta_dir = self.make_resource_meta("""
|
||||
id: sample
|
||||
handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
value:
|
||||
schema: str
|
||||
value:
|
||||
value-required:
|
||||
schema: str!
|
||||
value:
|
||||
""")
|
||||
|
||||
r = self.create_resource(
|
||||
'r1', sample_meta_dir, {'value': 'x', 'value-required': 'y'}
|
||||
)
|
||||
errors = sv.validate_resource(r)
|
||||
self.assertEqual(errors, {})
|
||||
|
||||
r = self.create_resource(
|
||||
'r2', sample_meta_dir, {'value': 1, 'value-required': 'y'}
|
||||
)
|
||||
errors = sv.validate_resource(r)
|
||||
self.assertListEqual(errors.keys(), ['value'])
|
||||
|
||||
r = self.create_resource(
|
||||
'r3', sample_meta_dir, {'value': ''}
|
||||
)
|
||||
errors = sv.validate_resource(r)
|
||||
self.assertListEqual(errors.keys(), ['value-required'])
|
||||
|
||||
def test_input_int_type(self):
|
||||
sample_meta_dir = self.make_resource_meta("""
|
||||
id: sample
|
||||
handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
value:
|
||||
schema: int
|
||||
value:
|
||||
value-required:
|
||||
schema: int!
|
||||
value:
|
||||
""")
|
||||
|
||||
r = self.create_resource(
|
||||
'r1', sample_meta_dir, {'value': 1, 'value-required': 2}
|
||||
)
|
||||
errors = sv.validate_resource(r)
|
||||
self.assertEqual(errors, {})
|
||||
|
||||
r = self.create_resource(
|
||||
'r2', sample_meta_dir, {'value': 'x', 'value-required': 2}
|
||||
)
|
||||
errors = sv.validate_resource(r)
|
||||
self.assertListEqual(errors.keys(), ['value'])
|
||||
|
||||
r = self.create_resource(
|
||||
'r3', sample_meta_dir, {'value': 1}
|
||||
)
|
||||
errors = sv.validate_resource(r)
|
||||
self.assertListEqual(errors.keys(), ['value-required'])
|
||||
|
||||
def test_input_dict_type(self):
|
||||
sample_meta_dir = self.make_resource_meta("""
|
||||
id: sample
|
||||
handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
values:
|
||||
schema: {a: int!, b: int}
|
||||
value: {}
|
||||
""")
|
||||
|
||||
r = self.create_resource(
|
||||
'r', sample_meta_dir, {'values': {'a': 1, 'b': 2}}
|
||||
)
|
||||
errors = sv.validate_resource(r)
|
||||
self.assertEqual(errors, {})
|
||||
|
||||
r.update({'values': None})
|
||||
errors = sv.validate_resource(r)
|
||||
self.assertListEqual(errors.keys(), ['values'])
|
||||
|
||||
r.update({'values': {'a': 1, 'c': 3}})
|
||||
errors = sv.validate_resource(r)
|
||||
self.assertEqual(errors, {})
|
||||
|
||||
r = self.create_resource(
|
||||
'r1', sample_meta_dir, {'values': {'b': 2}}
|
||||
)
|
||||
errors = sv.validate_resource(r)
|
||||
self.assertListEqual(errors.keys(), ['values'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -1,5 +1,7 @@
|
||||
# TODO
|
||||
|
||||
- grammar connections fuzzy matching algorithm (for example: type 'login' joins to type 'login' irrespective of names of both inputs)
|
||||
- resource connections JS frontend (?)
|
||||
- store all resource configurations somewhere globally (this is required to
|
||||
correctly perform an update on one resource and bubble down to all others)
|
||||
- config templates
|
||||
@ -9,6 +11,7 @@
|
||||
when some image is unused to conserve space
|
||||
|
||||
# DONE
|
||||
- CI
|
||||
- Deploy HAProxy, Keystone and MariaDB
|
||||
- ansible handler (loles)
|
||||
- tags are kept in resource mata file (pkaminski)
|
||||
|
@ -3,5 +3,11 @@ handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
ip:
|
||||
type: str!
|
||||
value:
|
||||
image:
|
||||
export_volumes:
|
||||
type: str!
|
||||
value:
|
||||
export_volumes:
|
||||
type: str!
|
||||
value:
|
||||
|
@ -3,13 +3,23 @@ handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
ip:
|
||||
schema: str!
|
||||
value:
|
||||
image:
|
||||
schema: str!
|
||||
value:
|
||||
ports:
|
||||
schema: [int]
|
||||
value: []
|
||||
host_binds:
|
||||
schema: [int]
|
||||
value: []
|
||||
volume_binds:
|
||||
schema: [int]
|
||||
value: []
|
||||
ssh_user:
|
||||
schema: str!
|
||||
value: []
|
||||
ssh_key:
|
||||
input-types:
|
||||
ports:
|
||||
host_binds: list
|
||||
volume_binds: list
|
||||
schema: str!
|
||||
value: []
|
||||
|
@ -2,4 +2,6 @@ id: file
|
||||
handler: shell
|
||||
version: 1.0.0
|
||||
input:
|
||||
path: /tmp/test_file
|
||||
path:
|
||||
schema: str!
|
||||
value: /tmp/test_file
|
||||
|
@ -3,15 +3,26 @@ handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
ip:
|
||||
config_dir: {src: /etc/solar/haproxy, dst: /etc/haproxy}
|
||||
schema: int!
|
||||
value:
|
||||
config_dir:
|
||||
schema: {src: str!, dst: str!}
|
||||
value: {src: /etc/solar/haproxy, dst: /etc/haproxy}
|
||||
listen_ports:
|
||||
schema: [int]
|
||||
value: []
|
||||
configs:
|
||||
schema: [[str]]
|
||||
value: []
|
||||
configs_names:
|
||||
schema: [str]
|
||||
value: []
|
||||
configs_ports:
|
||||
schema: [[int]]
|
||||
value: []
|
||||
ssh_user:
|
||||
schema: str!
|
||||
value:
|
||||
ssh_key:
|
||||
input-types:
|
||||
listen_ports: list
|
||||
configs: list
|
||||
configs_names: list
|
||||
configs_ports: list
|
||||
schema: str!
|
||||
value:
|
||||
|
@ -3,9 +3,14 @@ handler: none
|
||||
version: 1.0.0
|
||||
input:
|
||||
name:
|
||||
schema: str!
|
||||
value:
|
||||
listen_port:
|
||||
schema: int!
|
||||
value:
|
||||
ports:
|
||||
schema: [int]
|
||||
value:
|
||||
servers:
|
||||
input-types:
|
||||
ports: list
|
||||
servers: list
|
||||
schema: [str]
|
||||
value:
|
||||
|
@ -3,11 +3,29 @@ handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
config_dir:
|
||||
schema: str!
|
||||
value:
|
||||
admin_token:
|
||||
schema: str!
|
||||
value:
|
||||
db_user:
|
||||
schema: str!
|
||||
value:
|
||||
db_password:
|
||||
schema: str!
|
||||
value:
|
||||
db_host:
|
||||
schema: str!
|
||||
value:
|
||||
db_name:
|
||||
schema: str!
|
||||
value:
|
||||
ip:
|
||||
schema: str!
|
||||
value:
|
||||
ssh_key:
|
||||
schema: str!
|
||||
value:
|
||||
ssh_user:
|
||||
schema: str!
|
||||
value:
|
||||
|
@ -2,10 +2,24 @@ id: keystone
|
||||
handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
image: kollaglue/centos-rdo-keystone
|
||||
image:
|
||||
schema: str!
|
||||
value: kollaglue/centos-rdo-keystone
|
||||
config_dir:
|
||||
schema: str!
|
||||
value:
|
||||
port:
|
||||
schema: int!
|
||||
value:
|
||||
admin_port:
|
||||
schema: int!
|
||||
value:
|
||||
ip:
|
||||
schema: str!
|
||||
value:
|
||||
ssh_key:
|
||||
schema: str!
|
||||
value:
|
||||
ssh_user:
|
||||
schema: str!
|
||||
value:
|
||||
|
@ -3,12 +3,32 @@ handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
keystone_host:
|
||||
schema: str!
|
||||
value:
|
||||
keystone_port:
|
||||
schema: int!
|
||||
value:
|
||||
login_user:
|
||||
schema: str!
|
||||
value:
|
||||
login_token:
|
||||
schema: str!
|
||||
value:
|
||||
user_name:
|
||||
schema: str!
|
||||
value:
|
||||
user_password:
|
||||
schema: str!
|
||||
value:
|
||||
tenant_name:
|
||||
schema: str!
|
||||
value:
|
||||
ip:
|
||||
schema: str!
|
||||
value:
|
||||
ssh_key:
|
||||
schema: str!
|
||||
value:
|
||||
ssh_user:
|
||||
schema: str!
|
||||
value:
|
||||
|
@ -6,9 +6,23 @@ actions:
|
||||
remove: remove.yml
|
||||
input:
|
||||
db_name:
|
||||
schema: str!
|
||||
value:
|
||||
login_password:
|
||||
schema: str!
|
||||
value:
|
||||
login_port:
|
||||
schema: int!
|
||||
value:
|
||||
login_user:
|
||||
ip:
|
||||
ssh_key:
|
||||
ssh_user:
|
||||
schema: str!
|
||||
value:
|
||||
ip:
|
||||
schema: str!
|
||||
value:
|
||||
ssh_key:
|
||||
schema: str!
|
||||
value:
|
||||
ssh_user:
|
||||
schema: str!
|
||||
value:
|
||||
|
@ -3,8 +3,20 @@ handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
image:
|
||||
root_password:
|
||||
port:
|
||||
ip:
|
||||
ssh_key:
|
||||
ssh_user:
|
||||
schema: str!
|
||||
value:
|
||||
root_password:
|
||||
schema: str!
|
||||
value:
|
||||
port:
|
||||
schema: str!
|
||||
value:
|
||||
ip:
|
||||
schema: int!
|
||||
value:
|
||||
ssh_key:
|
||||
schema: str!
|
||||
value:
|
||||
ssh_user:
|
||||
schema: str!
|
||||
value:
|
||||
|
@ -6,11 +6,29 @@ actions:
|
||||
remove: remove.yml
|
||||
input:
|
||||
new_user_password:
|
||||
schema: str!
|
||||
value:
|
||||
new_user_name:
|
||||
schema: str!
|
||||
value:
|
||||
db_name:
|
||||
schema: str!
|
||||
value:
|
||||
login_password:
|
||||
schema: str!
|
||||
value:
|
||||
login_port:
|
||||
schema: int!
|
||||
value:
|
||||
login_user:
|
||||
ip:
|
||||
ssh_key:
|
||||
ssh_user:
|
||||
schema: str!
|
||||
value:
|
||||
ip:
|
||||
schema: str!
|
||||
value:
|
||||
ssh_key:
|
||||
schema: str!
|
||||
value:
|
||||
ssh_user:
|
||||
schema: str!
|
||||
value:
|
||||
|
@ -3,5 +3,11 @@ handler: ansible
|
||||
version: 1.0.0
|
||||
input:
|
||||
ip:
|
||||
port: 8774
|
||||
schema: str!
|
||||
value:
|
||||
port:
|
||||
schema: int!
|
||||
value: 8774
|
||||
image: # TODO
|
||||
schema: str!
|
||||
value:
|
||||
|
@ -4,5 +4,11 @@ version: 1.0.0
|
||||
actions:
|
||||
input:
|
||||
ip:
|
||||
schema: str!
|
||||
value:
|
||||
ssh_key:
|
||||
schema: str!
|
||||
value:
|
||||
ssh_user:
|
||||
schema: str!
|
||||
value:
|
||||
|
Loading…
Reference in New Issue
Block a user