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 jinja2==2.7.3
networkx==1.9.1 networkx==1.9.1
PyYAML==3.11 PyYAML==3.11
jsonschema==2.4.0

View File

@ -17,8 +17,9 @@ fi
pip install -r requirements.txt --download-cache=/tmp/$JOB_NAME 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 popd

View File

@ -13,6 +13,7 @@ from solar.core import db
from solar.core import observer from solar.core import observer
from solar.core import signals from solar.core import signals
from solar.core import utils from solar.core import utils
from solar.core import validation
class Resource(object): class Resource(object):
@ -21,14 +22,12 @@ class Resource(object):
self.base_dir = base_dir self.base_dir = base_dir
self.metadata = metadata self.metadata = metadata
self.actions = metadata['actions'].keys() if metadata['actions'] else None self.actions = metadata['actions'].keys() if metadata['actions'] else None
self.requires = metadata['input'].keys()
self._validate_args(args, metadata['input'])
self.args = {} self.args = {}
for arg_name, arg_value in args.items(): 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.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.changed = []
self.tags = tags or [] self.tags = tags or []
@ -95,22 +94,13 @@ class Resource(object):
else: else:
raise Exception('Uuups, action is not available') 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 # TODO: versioning
def save(self): def save(self):
metadata = copy.deepcopy(self.metadata) metadata = copy.deepcopy(self.metadata)
metadata['tags'] = self.tags 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') meta_file = os.path.join(self.base_dir, 'meta.yaml')
with open(meta_file, 'w') as f: with open(meta_file, 'w') as f:

View File

@ -83,8 +83,8 @@ def guess_mapping(emitter, receiver):
:return: :return:
""" """
guessed = {} guessed = {}
for key in emitter.requires: for key in emitter.args:
if key in receiver.requires: if key in receiver.args:
guessed[key] = key guessed[key] = key
return guessed 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 unittest
import yaml import yaml
from x import db from solar.core import db
from x import resource as xr from solar.core import resource as xr
from x import signals as xs from solar.core import signals as xs
class BaseResourceTest(unittest.TestCase): class BaseResourceTest(unittest.TestCase):

View File

@ -2,7 +2,7 @@ import unittest
import base import base
from x import signals as xs from solar.core import signals as xs
class TestBaseInput(base.BaseResourceTest): class TestBaseInput(base.BaseResourceTest):
@ -12,7 +12,9 @@ id: sample
handler: ansible handler: ansible
version: 1.0.0 version: 1.0.0
input: input:
values: {} values:
schema: {a: int, b: int}
value: {}
""") """)
sample1 = self.create_resource( sample1 = self.create_resource(
@ -63,7 +65,11 @@ handler: ansible
version: 1.0.0 version: 1.0.0
input: input:
ip: ip:
schema: string
value:
port: port:
schema: int
value:
""") """)
sample_ip_meta_dir = self.make_resource_meta(""" sample_ip_meta_dir = self.make_resource_meta("""
id: sample-ip id: sample-ip
@ -71,6 +77,8 @@ handler: ansible
version: 1.0.0 version: 1.0.0
input: input:
ip: ip:
schema: string
value:
""") """)
sample_port_meta_dir = self.make_resource_meta(""" sample_port_meta_dir = self.make_resource_meta("""
id: sample-port id: sample-port
@ -78,6 +86,8 @@ handler: ansible
version: 1.0.0 version: 1.0.0
input: input:
port: port:
schema: int
value:
""") """)
sample = self.create_resource( sample = self.create_resource(
@ -109,6 +119,8 @@ handler: ansible
version: 1.0.0 version: 1.0.0
input: input:
ip: ip:
schema: string
value:
""") """)
sample = self.create_resource( sample = self.create_resource(
@ -149,6 +161,8 @@ handler: ansible
version: 1.0.0 version: 1.0.0
input: input:
ip: ip:
schema: str
value:
""") """)
sample1 = self.create_resource( sample1 = self.create_resource(
@ -171,6 +185,8 @@ handler: ansible
version: 1.0.0 version: 1.0.0
input: input:
ip: ip:
schema: str
value:
""") """)
list_input_single_meta_dir = self.make_resource_meta(""" list_input_single_meta_dir = self.make_resource_meta("""
id: list-input-single id: list-input-single
@ -178,8 +194,8 @@ handler: ansible
version: 1.0.0 version: 1.0.0
input: input:
ips: ips:
input-types: schema: [str]
ips: list value: []
""") """)
sample1 = self.create_resource( sample1 = self.create_resource(
@ -248,7 +264,11 @@ handler: ansible
version: 1.0.0 version: 1.0.0
input: input:
ip: ip:
schema: str
value:
port: port:
schema: int
value:
""") """)
list_input_multi_meta_dir = self.make_resource_meta(""" list_input_multi_meta_dir = self.make_resource_meta("""
id: list-input-multi id: list-input-multi
@ -256,10 +276,11 @@ handler: ansible
version: 1.0.0 version: 1.0.0
input: input:
ips: ips:
schema: [str]
value:
ports: ports:
input-types: schema: [int]
ips: list value:
ports: list
""") """)
sample1 = self.create_resource( 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 # 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 - store all resource configurations somewhere globally (this is required to
correctly perform an update on one resource and bubble down to all others) correctly perform an update on one resource and bubble down to all others)
- config templates - config templates
@ -9,6 +11,7 @@
when some image is unused to conserve space when some image is unused to conserve space
# DONE # DONE
- CI
- Deploy HAProxy, Keystone and MariaDB - Deploy HAProxy, Keystone and MariaDB
- ansible handler (loles) - ansible handler (loles)
- tags are kept in resource mata file (pkaminski) - tags are kept in resource mata file (pkaminski)

View File

@ -3,5 +3,11 @@ handler: ansible
version: 1.0.0 version: 1.0.0
input: input:
ip: ip:
type: str!
value:
image: image:
export_volumes: type: str!
value:
export_volumes:
type: str!
value:

View File

@ -3,13 +3,23 @@ handler: ansible
version: 1.0.0 version: 1.0.0
input: input:
ip: ip:
schema: str!
value:
image: image:
schema: str!
value:
ports: ports:
schema: [int]
value: []
host_binds: host_binds:
schema: [int]
value: []
volume_binds: volume_binds:
schema: [int]
value: []
ssh_user: ssh_user:
schema: str!
value: []
ssh_key: ssh_key:
input-types: schema: str!
ports: value: []
host_binds: list
volume_binds: list

View File

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

View File

@ -3,15 +3,26 @@ handler: ansible
version: 1.0.0 version: 1.0.0
input: input:
ip: 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: listen_ports:
schema: [int]
value: []
configs: configs:
schema: [[str]]
value: []
configs_names: configs_names:
schema: [str]
value: []
configs_ports: configs_ports:
schema: [[int]]
value: []
ssh_user: ssh_user:
schema: str!
value:
ssh_key: ssh_key:
input-types: schema: str!
listen_ports: list value:
configs: list
configs_names: list
configs_ports: list

View File

@ -3,9 +3,14 @@ handler: none
version: 1.0.0 version: 1.0.0
input: input:
name: name:
schema: str!
value:
listen_port: listen_port:
schema: int!
value:
ports: ports:
schema: [int]
value:
servers: servers:
input-types: schema: [str]
ports: list value:
servers: list

View File

@ -3,11 +3,29 @@ handler: ansible
version: 1.0.0 version: 1.0.0
input: input:
config_dir: config_dir:
schema: str!
value:
admin_token: admin_token:
schema: str!
value:
db_user: db_user:
schema: str!
value:
db_password: db_password:
schema: str!
value:
db_host: db_host:
schema: str!
value:
db_name: db_name:
schema: str!
value:
ip: ip:
schema: str!
value:
ssh_key: ssh_key:
schema: str!
value:
ssh_user: ssh_user:
schema: str!
value:

View File

@ -2,10 +2,24 @@ id: keystone
handler: ansible handler: ansible
version: 1.0.0 version: 1.0.0
input: input:
image: kollaglue/centos-rdo-keystone image:
schema: str!
value: kollaglue/centos-rdo-keystone
config_dir: config_dir:
schema: str!
value:
port: port:
schema: int!
value:
admin_port: admin_port:
schema: int!
value:
ip: ip:
schema: str!
value:
ssh_key: ssh_key:
schema: str!
value:
ssh_user: ssh_user:
schema: str!
value:

View File

@ -3,12 +3,32 @@ handler: ansible
version: 1.0.0 version: 1.0.0
input: input:
keystone_host: keystone_host:
schema: str!
value:
keystone_port: keystone_port:
schema: int!
value:
login_user: login_user:
schema: str!
value:
login_token: login_token:
schema: str!
value:
user_name: user_name:
schema: str!
value:
user_password: user_password:
schema: str!
value:
tenant_name: tenant_name:
schema: str!
value:
ip: ip:
schema: str!
value:
ssh_key: ssh_key:
schema: str!
value:
ssh_user: ssh_user:
schema: str!
value:

View File

@ -6,9 +6,23 @@ actions:
remove: remove.yml remove: remove.yml
input: input:
db_name: db_name:
schema: str!
value:
login_password: login_password:
schema: str!
value:
login_port: login_port:
schema: int!
value:
login_user: login_user:
ip: schema: str!
ssh_key: value:
ssh_user: 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 version: 1.0.0
input: input:
image: image:
root_password: schema: str!
port: value:
ip: root_password:
ssh_key: schema: str!
ssh_user: 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 remove: remove.yml
input: input:
new_user_password: new_user_password:
schema: str!
value:
new_user_name: new_user_name:
schema: str!
value:
db_name: db_name:
schema: str!
value:
login_password: login_password:
schema: str!
value:
login_port: login_port:
schema: int!
value:
login_user: login_user:
ip: schema: str!
ssh_key: value:
ssh_user: 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 version: 1.0.0
input: input:
ip: ip:
port: 8774 schema: str!
value:
port:
schema: int!
value: 8774
image: # TODO image: # TODO
schema: str!
value:

View File

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