Commit c7d7097e authored by Antoine Millet's avatar Antoine Millet
Browse files

Implemented VMSpec validator

parent 2979a9b3
Loading
Loading
Loading
Loading
+14 −0
Original line number Diff line number Diff line
# This file is part of CloudControl.
#
# CloudControl is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# CloudControl is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CloudControl.  If not, see <http://www.gnu.org/licenses/>.
 No newline at end of file
+180 −0
Original line number Diff line number Diff line
# This file is part of CloudControl.
#
# CloudControl is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# CloudControl is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CloudControl.  If not, see <http://www.gnu.org/licenses/>.

import re


class VMSpecValidationError(Exception):
    """ Exception raised when a VMSpec validation error occurs.
    """


# Checks:

def check_type(object_, type_, message, **kwargs):
    if not isinstance(object_, type_):
        kwargs['type'] = type_.__class__.__name__
        kwargs['object'] = str(object_)
        raise VMSpecValidationError(message % kwargs)


def check_regex(object_, regex, message, **kwargs):
    if re.match(object_, regex):
        kwargs['object'] = str(object_)
        raise VMSpecValidationError(message % kwargs)


def check_integer(object_, message, min=None, max=None, **kwargs):
    if not isinstance(object_, int):
        raise VMSpecValidationError(message % kwargs)
    elif (min is not None and object_ < min) or (max is not None and object_ > max):
        raise VMSpecValidationError(message % kwargs)


def check_dict_key(dict_, key, message, **kwargs):
    if key not in dict_:
        kwargs['key'] = key
        raise VMSpecValidationError(message % kwargs)


# Validation rules:

def validate_riskgroup(data):
    """ Validate riskgroups.

    Example:
        {'name': {'tag': 1}}
    """

    check_type(data, dict, 'riskgroup: must be a dict')

    for name, tags in data.iteritems():
        env = {"name": name}

        check_type(name, basestring, 'riskgroup/[%(name)s]: must be a string', **env)
        check_type(tags, dict, 'riskgroup/%(name)s: must be a dict', **env)

        for tag, value in tags.iteritems():
            check_type(tag, basestring, 'riskgroup/%(name)s/[%(tag)s]: must be a string', tag=tag, **env)
            check_integer(value, 'riskgroup/%(name)s/%(tag)s: must be a positive integer', min=1, tag=tag, **env)


def validate_machine_volumes(hostname, data):
    """ Validate machines volumes.

    Example:
        {'root': {'size': 512000, 'pool': 'vg'}}
    """

    check_type(data, list, 'machines/%(hostname)s/volumes: must be a list', hostname=hostname)

    for i, disk in enumerate(data):
        env = {'hostname': hostname, 'index': i}
        check_type(disk, dict, 'machines/%(hostname)s/volumes/%(index)s: must be a %(type)s', **env)
        check_dict_key(disk, 'size', 'machines/%(hostname)s/volumes/%(index)s/[%(key)s]: is mandatory', **env)
        check_integer(disk['size'], 'machines/%(hostname)ss/volumes/%(index)s/size: must be a positive integer', min=0, **env)
        check_dict_key(disk, 'pool', 'machines/%(hostname)s/volumes/%(index)s/[%(key)s]: is mandatory', **env)
        check_type(disk['pool'], basestring, 'machines/%(hostname)ss/volumes/%(index)s/pool: must be a string', **env)


def validate_machine_spec(hostname, spec):
    """ Validate machines specifications.

    Example:
        {'cpu': 8,
         'memory': 512000,
         'flags': ['does_not_autostart'],
         'tags': {'platform': 'Infra'}}
    """

    env = {'hostname': hostname}

    check_integer(spec.get('cpu'), 'machines/%(hostname)s/cpu: must be a positive integer', min=1, **env)
    check_integer(spec.get('memory'), 'machines/%(hostname)s/memory: must be a positive integer', min=1, **env)

    if 'flags' in spec:
        if spec['flags'] is None:
            spec['flags'] = []
        check_type(spec['flags'], list, 'machines/%(hostname)s/flags: must be a list', **env)
        for i, flag in enumerate(spec['flags']):
            check_type(flag, basestring, 'machines/%(hostname)s/flags[%(i)s]: must be a string', i=i, **env)

    if 'tags' in spec:
        if spec['tags'] is None:
            spec['tags'] = {}
        check_type(spec['tags'], dict, 'machines/%(hostname)s/tags: must be a dict', **env)
        for tag, value in spec['tags'].iteritems():
            check_type(tag, basestring, 'machines/%(hostname)s/tags/[%(tag)s]: must be a string', tag=tag, **env)
            check_type(value, basestring, 'machines/%(hostname)s/tags/%(tag)s: must be a string', tag=tag, **env)

    if 'volumes' in spec:
        validate_machine_volumes(hostname, spec['volumes'])


def validate_machines(data):
    """ Validate machine entry.

    Example:
        {'foobar-1.example.org': ...}
    """

    check_type(data, dict, 'machines: must be a dict')

    if len(data) < 1:
        raise VMSpecValidationError('machines: at least one machine must be defined')

    for hostname, spec in data.iteritems():
        if not re.match('^[a-z0-9-]+(.[a-z0-9-]+)*$', hostname):
            raise VMSpecValidationError('machines/[%s]: bad hostname format' % hostname)

        validate_machine_spec(hostname, spec)


def validate_vmspec(data):
    """ Validate a vmspec.

    Example:
        {'riskgroups:' ..., 'machines': ...}
    """

    if 'riskgroups' in data:
        validate_riskgroup(data['riskgroups'])

    check_dict_key(data, 'machines', 'machines: is mandatory')
    validate_machines(data['machines'])

    if 'target' in data:
        check_type(data['target'], basestring, 'target: must be a string (a TQL)')


def expand_vmspec(vmspec):
    """ Expand the vmspec to a list of VM.
    """
    validate_vmspec(vmspec)
    vmspec_expanded = []
    riskgroups = vmspec.get('riskgroups', {})
    for title, specs in vmspec['machines'].iteritems():
        specs['title'] = title
        riskgroup = specs.get('tags', {}).get('riskgroup')
        if riskgroup is not None:
            if riskgroup not in riskgroups:
                raise VMSpecValidationError('machines/%s: riskgroup not defined')
            specs['riskgroup'] = riskgroups[riskgroup]
        vmspec_expanded.append(specs)
    return vmspec_expanded

if __name__ == '__main__':
    import sys, json
    validate_vmspec(json.load(sys.stdin))