Added resource validation

- uses jsonschema
This commit is contained in:
Przemyslaw Kaminski 2015-05-14 17:45:40 +02:00
parent 51674c00ad
commit e86ac3f837
22 changed files with 408 additions and 58 deletions

View File

@ -2,3 +2,4 @@ click==4.0
jinja2==2.7.3
networkx==1.9.1
PyYAML==3.11
jsonschema==2.4.0

View File

@ -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

View File

@ -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:

View File

@ -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

View 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

View File

@ -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):

View File

@ -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(

View 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()

View File

@ -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)

View File

@ -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:

View File

@ -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: []

View File

@ -2,4 +2,6 @@ id: file
handler: shell
version: 1.0.0
input:
path: /tmp/test_file
path:
schema: str!
value: /tmp/test_file

View 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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -4,5 +4,11 @@ version: 1.0.0
actions:
input:
ip:
schema: str!
value:
ssh_key:
schema: str!
value:
ssh_user:
schema: str!
value: