diff --git a/solar/solar/core/resource/virtual_resource.py b/solar/solar/core/resource/virtual_resource.py index 9dd9d3fd..0de2986d 100644 --- a/solar/solar/core/resource/virtual_resource.py +++ b/solar/solar/core/resource/virtual_resource.py @@ -20,8 +20,9 @@ import yaml from jinja2 import Template, Environment, meta from solar.core import provider -from solar.core import resource from solar.core import signals +from solar.core.resource import load as load_resource +from solar.core.resource import Resource from solar.events.api import add_event from solar.events.controls import React, Dep @@ -57,20 +58,22 @@ def create_resource(name, base_path, args=None, virtual_resource=None): # List args init with empty list. Elements will be added later args = {key: (value if not isinstance(value, list) else []) for key, value in args.items()} - r = resource.Resource( + r = Resource( name, base_path, args=args, tags=[], virtual_resource=virtual_resource ) return r def create_virtual_resource(vr_name, template): - template_resources = template['resources'] - template_events = template.get('events', {}) + template_resources = template.get('resources', []) + template_events = template.get('events', []) + resources_to_update = template.get('updates', []) created_resources = create_resources(template_resources) events = parse_events(template_events) for event in events: add_event(event) + update_resources(resources_to_update) return created_resources @@ -113,16 +116,36 @@ def create_resources(resources): cwd = os.getcwd() for r in resources: resource_name = r['id'] - base_path = os.path.join(cwd, r['from']) args = r['values'] - new_resources = create(resource_name, base_path, args) + from_path = r.get('from', None) + base_path = os.path.join(cwd, from_path) + new_resources = create(resource_name, base_path) created_resources += new_resources - if not is_virtual(base_path): - add_connections(resource_name, args) + update_inputs(resource_name, args) return created_resources +def update_resources(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) + for c in connections: + mapping = {} + parent = load_resource(c['parent']) + events = c['events'] + mapping[c['parent_input']] = c['child_input'] + signals.connect(parent, child, mapping, events) + + child.update(assignments) + + def parse_events(events): parsed_events = [] for event in events: @@ -140,38 +163,46 @@ def parse_events(events): return parsed_events -def add_connections(resource_name, args): +def parse_inputs(args): connections = [] - for receiver_input, arg in args.items(): + assignments = {} + for r_input, arg in args.items(): if isinstance(arg, list): - for item in arg: - c = parse_connection(resource_name, receiver_input, item) - connections.append(c) + c, a = parse_list_input(r_input, arg) + connections.extend(c) + assignments.update(a) else: - c = parse_connection(resource_name, receiver_input, arg) - connections.append(c) - - connections = [c for c in connections if c is not None] - for c in connections: - parent = resource.load(c['parent']) - child = resource.load(c['child']) - events = c['events'] - mapping = {c['parent_input'] : c['child_input']} - signals.connect(parent, child, mapping, events) + if isinstance(arg, basestring) and '::' in arg: + c = parse_connection(r_input, arg) + connections.append(c) + else: + assignments[r_input] = arg + return connections, assignments -def parse_connection(receiver, receiver_input, element): - if isinstance(element, basestring) and '::' in element: - emitter, src = element.split('::', 1) - try: - src, events = src.split('::') - if events == 'NO_EVENTS': - events = False - except ValueError: - events = None - return {'child': receiver, - 'child_input': receiver_input, - 'parent' : emitter, - 'parent_input': src, - 'events' : events - } +def parse_list_input(r_input, args): + connections = [] + assignments = {} + for arg in args: + if isinstance(arg, basestring) and '::' in arg: + c = parse_connection(r_input, arg) + connections.append(c) + else: + # Not supported yet + raise Exception('Only connections are supported in lists') + return connections, assignments + + +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 + } diff --git a/solar/solar/test/resource_fixtures/update.yaml.tmpl b/solar/solar/test/resource_fixtures/update.yaml.tmpl new file mode 100644 index 00000000..14aaf12f --- /dev/null +++ b/solar/solar/test/resource_fixtures/update.yaml.tmpl @@ -0,0 +1,5 @@ +id: simple_multinode +updates: + - id: node1 + values: + ip: '10.0.0.4' diff --git a/solar/solar/test/test_virtual_resource.py b/solar/solar/test/test_virtual_resource.py index dec78d89..e9d47cd1 100644 --- a/solar/solar/test/test_virtual_resource.py +++ b/solar/solar/test/test_virtual_resource.py @@ -46,6 +46,11 @@ def bad_event_type(): ''' return yaml.load(StringIO(events)) +def test_create_path_does_not_exists(): + with pytest.raises(Exception) as excinfo: + vr.create('node1', '/path/does/not/exists') + err = 'Base resource does not exist: /path/does/not/exists' + assert str(excinfo.value) == err def test_create_resource(): node_path = os.path.join( @@ -68,6 +73,26 @@ def test_create_virtual_resource(tmpdir): resources = vr.create('nodes', str(vr_file)) assert len(resources) == 2 +def test_update(tmpdir): + # XXX: make helper for it + base_path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + 'resource_fixtures') + vr_node_tmpl_path = os.path.join(base_path, 'nodes.yaml.tmpl') + vr_update_tmpl_path = os.path.join(base_path, 'update.yaml.tmpl') + update_path = os.path.join(base_path, 'update') + node_resource_path = os.path.join(base_path, 'node') + with open(vr_node_tmpl_path) as f: + vr_data = f.read().format(resource_path=node_resource_path) + with open(vr_update_tmpl_path) as f: + update_data = f.read().format(resource_path=update_path) + vr_file = tmpdir.join('nodes.yaml') + vr_file.write(vr_data) + update_file = tmpdir.join('update.yaml') + update_file.write(update_data) + resources = vr.create('nodes', str(vr_file)) + vr.create('updates', str(update_file)) + assert resources[0].args['ip'] == '10.0.0.4' def test_parse_events(good_events): events =[Dep(parent='service1', parent_action='run', @@ -92,30 +117,23 @@ def test_add_connections(mocker, resources): 'servers': ['node1::ip', 'node2::ip'], 'alias': 'ser1' } - vr.add_connections('service1', args) + vr.update_inputs('service1', args) assert mocked_signals.connect.call_count == 3 - def test_parse_connection(): - correct_connection = {'child': 'host_file', - 'child_input': 'ip', + correct_connection = {'child_input': 'ip', 'parent' : 'node1', 'parent_input': 'ip', 'events' : None } - connection = vr.parse_connection('host_file', 'ip', 'node1::ip') + connection = vr.parse_connection('ip', 'node1::ip') assert correct_connection == connection def test_parse_connection_disable_events(): - correct_connection = {'child': 'host_file', - 'child_input': 'ip', + correct_connection = {'child_input': 'ip', 'parent' : 'node1', 'parent_input': 'ip', 'events' : False } - connection = vr.parse_connection('host_file', 'ip', 'node1::ip::NO_EVENTS') + connection = vr.parse_connection('ip', 'node1::ip::NO_EVENTS') assert correct_connection == connection - -def test_parse_connection_no_connection(): - connection = vr.parse_connection('host_file', 'ip', '10.0.0.2') - assert None == connection