solar/solar/core/resource/virtual_resource.py

346 lines
11 KiB
Python

# -*- coding: utf-8 -*-
# Copyright 2015 Mirantis, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from collections import defaultdict
from StringIO import StringIO
from jinja2 import Environment
from jinja2 import meta
import os
import yaml
from solar.core.log import log
from solar.core import provider
from solar.core.resource import load as load_resource
from solar.core.resource import load_by_tags
from solar.core.resource.repository import Repository
from solar.core.resource import Resource
from solar.events.api import add_event
from solar.events.controls import Dep
from solar.events.controls import React
# Custom environment with custom blocks, to make yaml parsers happy
VR_ENV = Environment(block_start_string="#%",
block_end_string="%#",
variable_start_string="#{",
variable_end_string="}#",
trim_blocks=True,
lstrip_blocks=True)
def create(name, spec, args=None, tags=None, virtual_resource=None):
args = args or {}
if isinstance(spec, provider.BaseProvider):
spec = spec.directory
# fullpath
# TODO: (jnowak) find a better way to code this part
if spec.startswith('/'):
if os.path.isfile(spec):
template = _compile_file(name, spec, args)
yaml_template = yaml.load(StringIO(template))
rs = create_virtual_resource(name, yaml_template, tags)
else:
r = create_resource(name,
spec,
args=args,
tags=tags,
virtual_resource=virtual_resource)
rs = [r]
return rs
repo, parsed_spec = Repository.parse(spec)
if repo.is_virtual(spec):
path = repo.get_virtual_path(spec)
template = _compile_file(name, path, args)
yaml_template = yaml.load(StringIO(template))
rs = create_virtual_resource(name, yaml_template, tags)
else:
r = create_resource(name,
spec,
args=args,
tags=tags,
virtual_resource=virtual_resource)
rs = [r]
return rs
def create_resource(name, spec, args=None, tags=None,
virtual_resource=None):
args = args or {}
if isinstance(spec, provider.BaseProvider):
spec = spec.directory
# filter connections from lists and dicts
# will be added later
def _filter(value):
if isinstance(value, list):
return filter(lambda res: not is_connection(res), value)
if isinstance(value, dict):
return {key: None if is_connection(value) else value
for key, value in value.iteritems()}
else:
return value
args = {key: _filter(value) for key, value in args.items()}
r = Resource(name, spec, args=args,
tags=tags, virtual_resource=virtual_resource)
return r
def create_virtual_resource(vr_name, template, tags=None):
template_resources = template.get('resources', [])
template_events = template.get('events', [])
resources_to_update = template.get('updates', [])
created_resources = create_resources(template_resources, tags=tags)
events = parse_events(template_events)
for event in events:
add_event(event)
update_resources(resources_to_update)
return created_resources
def _compile_file(name, path, kwargs):
with open(path) as f:
content = f.read()
inputs = get_inputs(content)
template = _get_template(name, content, kwargs, inputs)
with open('/tmp/compiled', 'w') as c:
c.write(template)
return template
def get_inputs(content):
jinja_globals = VR_ENV.globals.keys()
ast = VR_ENV.parse(content)
return meta.find_undeclared_variables(ast) - set(jinja_globals)
def _get_template(name, content, kwargs, inputs):
missing = []
for input in inputs:
if input not in kwargs:
missing.append(input)
if missing:
raise Exception(
'[{0}] Validation error. Missing data in input: {1}'.format(name, missing)) # NOQA
template = VR_ENV.from_string(content)
template = template.render(str=str, zip=zip, **kwargs)
return template
def create_resources(resources, tags=None):
created_resources = []
for r in resources:
resource_name = r['id']
args = r.get('values', {})
node = r.get('location', None)
values_from = r.get('values_from')
spec = r.get('from', None)
tags = r.get('tags', [])
new_resources = create(resource_name, spec, args=args, tags=tags)
created_resources += new_resources
is_virtual = False
if not spec.startswith('/'):
repo, parsed_spec = Repository.parse(spec)
is_virtual = repo.is_virtual(spec)
if not is_virtual:
if node:
node = load_resource(node)
r = new_resources[0]
node.connect(r, mapping={})
r.add_tags('location={}'.format(node.name))
update_inputs(resource_name, args)
if values_from:
from_resource = load_resource(values_from)
from_resource.connect_with_events(r, use_defaults=False)
return created_resources
def extend_resources(template_resources):
resources = []
for r in template_resources:
if r.get('id'):
resources.append(r)
if r.get('with_tags'):
tags = r.get('with_tags')
filtered = load_by_tags(tags)
for f in filtered:
r = {'id': f.name,
'values': r['values']}
resources.append(r)
log.debug('Resource {} for tags {} found'.format(r, tags))
if not filtered:
log.debug('Warrning: no resources with tags: {}'.format(tags))
return resources
def update_resources(template_resources):
resources = extend_resources(template_resources)
for r in resources:
resource_name = r['id']
args = r['values']
update_inputs(resource_name, args)
def update_inputs(child, args):
child = load_resource(child)
connections, assignments = parse_inputs(args)
parents = defaultdict(lambda: defaultdict(dict))
for c in connections:
mapping = {c['parent_input']: c['child_input']}
parents[c['parent']]['mapping'].update(mapping)
if parents[c['parent']].get('events', None) is None:
parents[c['parent']]['events'] = c['events']
for parent, data in parents.iteritems():
parent = load_resource(parent)
use_defaults = not data['events'] is False
mapping = data['mapping']
parent.connect_with_events(
child, mapping, {}, use_defaults=use_defaults)
child.update(assignments)
def extend_events(template_events):
events = []
for e in template_events:
if e.get('parent_action', None):
events.append(e)
elif e.get('parent', None):
parent = e.get('parent')
tags = parent.get('with_tags')
resources = load_by_tags(tags)
for r in resources:
parent_action = '{}.{}'.format(r.name, parent['action'])
event = {'type': e['type'],
'state': e['state'],
'depend_action': e['depend_action'],
'parent_action': parent_action
}
events.append(event)
return events
def parse_events(template_events):
parsed_events = []
events = extend_events(template_events)
for event in events:
event_type = event['type']
parent, parent_action = event['parent_action'].split('.')
child, child_action = event['depend_action'].split('.')
state = event['state']
if event_type == Dep.etype:
event = Dep(parent, parent_action, state, child, child_action)
elif event_type == React.etype:
event = React(parent, parent_action, state, child, child_action)
else:
raise Exception('Invalid event type: {0}'.format(event_type))
parsed_events.append(event)
return parsed_events
def parse_inputs(args):
connections = []
assignments = {}
for r_input, arg in args.items():
if isinstance(arg, list):
c, a = parse_list_input(r_input, arg)
connections.extend(c)
assignments.update(a)
elif isinstance(arg, dict):
c, a = parse_dict_input(r_input, arg)
connections.extend(c)
assignments.update(a)
else:
if is_connection(arg):
c = parse_connection(r_input, arg)
connections.append(c)
else:
assignments[r_input] = arg
return connections, assignments
def parse_list_input(r_input, args):
connections = []
assignments = {}
for arg in args:
if isinstance(arg, dict):
n_connections, n_assign = parse_dict_input(
r_input, arg)
connections.extend(n_connections)
if n_assign:
add_assignment(assignments, r_input, n_assign)
elif is_connection(arg):
c = parse_connection(r_input, arg)
connections.append(c)
else:
add_assignment(assignments, r_input, arg)
return connections, assignments
def parse_dict_input(r_input, args):
connections = []
assignments = {}
for key, value in args.iteritems():
if is_connection(value):
dict_input = '{}:{}'.format(r_input, key)
c = parse_connection(dict_input, value)
connections.append(c)
else:
try:
assignments[r_input][key] = value
except KeyError:
assignments[r_input] = {key: value}
return connections, assignments
def add_assignment(assignments, r_input, arg):
try:
assignments[r_input].append(arg)
except KeyError:
assignments[r_input] = [arg]
def is_connection(arg):
if isinstance(arg, basestring) and '::' in arg:
return True
return False
def parse_connection(child_input, element):
parent, parent_input = element.split('::', 1)
try:
parent_input, events = parent_input.split('::')
if events == 'NO_EVENTS':
events = False
except ValueError:
events = None
return {'child_input': child_input,
'parent': parent,
'parent_input': parent_input,
'events': events
}