diff --git a/cli.py b/cli.py deleted file mode 100755 index 2cadec93..00000000 --- a/cli.py +++ /dev/null @@ -1,236 +0,0 @@ -#!/usr/bin/env python -import click -import json -#import matplotlib -#matplotlib.use('Agg') # don't show windows -#import matplotlib.pyplot as plt -import networkx as nx -import os -import subprocess - -from solar.core import actions as xa -from solar.core import resource as xr -from solar.core import signals as xs -from solar import operations -from solar import state - -from solar.interfaces.db import get_db - -db = get_db() - - -@click.group() -def cli(): - pass - - -def init_cli_resource(): - @click.group() - def resource(): - pass - - cli.add_command(resource) - - @click.command() - @click.argument('resource_path') - @click.argument('action_name') - def action(action_name, resource_path): - print 'action', resource_path, action_name - r = xr.load(resource_path) - xa.resource_action(r, action_name) - - resource.add_command(action) - - @click.command() - @click.argument('name') - @click.argument('base_path') - @click.argument('dest_path') - @click.argument('args') - def create(args, dest_path, base_path, name): - print 'create', name, base_path, dest_path, args - args = json.loads(args) - xr.create(name, base_path, dest_path, args) - - resource.add_command(create) - - @click.command() - @click.argument('resource_path') - @click.argument('tag_name') - @click.option('--add/--delete', default=True) - def tag(add, tag_name, resource_path): - print 'Tag', resource_path, tag_name, add - r = xr.load(resource_path) - if add: - r.add_tag(tag_name) - else: - r.remove_tag(tag_name) - r.save() - - resource.add_command(tag) - - @click.command() - @click.argument('path') - @click.option('--all/--one', default=False) - @click.option('--tag', default=None) - @click.option('--use-json/--no-use-json', default=False) - def show(use_json, tag, all, path): - import json - import six - - printer = lambda r: six.print_(r) - if use_json: - printer = lambda r: six.print_(json.dumps(r.to_dict())) - - if all or tag: - for name, resource in xr.load_all(path).items(): - show = True - if tag: - if tag not in resource.tags: - show = False - - if show: - printer(resource) - else: - printer(xr.load(path)) - - resource.add_command(show) - - @click.command() - @click.argument('name') - @click.argument('args') - def update(name, args): - args = json.loads(args) - all = xr.load_all() - r = all[name] - r.update(args) - resource.add_command(update) - - -def init_cli_connect(): - @click.command() - @click.argument('emitter') - @click.argument('receiver') - @click.option('--mapping', default=None) - def connect(mapping, receiver, emitter): - print 'Connect', emitter, receiver - emitter = xr.load(emitter) - receiver = xr.load(receiver) - print emitter - print receiver - if mapping is not None: - mapping = json.loads(mapping) - xs.connect(emitter, receiver, mapping=mapping) - - cli.add_command(connect) - - @click.command() - @click.argument('emitter') - @click.argument('receiver') - def disconnect(receiver, emitter): - print 'Disconnect', emitter, receiver - emitter = xr.load(emitter) - receiver = xr.load(receiver) - print emitter - print receiver - xs.disconnect(emitter, receiver) - - cli.add_command(disconnect) - - -def init_changes(): - @click.group() - def changes(): - pass - - cli.add_command(changes) - - @click.command() - def stage(): - log = operations.stage_changes() - print log.show() - - changes.add_command(stage) - - @click.command() - @click.option('--one', is_flag=True, default=False) - def commit(one): - if one: - operations.commit_one() - else: - operations.commit_changes() - - changes.add_command(commit) - - @click.command() - @click.option('--limit', default=5) - def history(limit): - print state.CL().show() - - changes.add_command(history) - - @click.command() - @click.option('--last', is_flag=True, default=False) - @click.option('--all', is_flag=True, default=False) - @click.option('--uid', default=None) - def rollback(last, all, uid): - if last: - print operations.rollback_last() - elif all: - print operations.rollback_all() - elif uid: - print operations.rollback_uid(uid) - - changes.add_command(rollback) - - -def init_cli_connections(): - @click.group() - def connections(): - pass - - cli.add_command(connections) - - @click.command() - def show(): - print json.dumps(xs.CLIENTS, indent=2) - - connections.add_command(show) - - # TODO: this requires graphing libraries - @click.command() - def graph(): - #g = xs.connection_graph() - g = xs.detailed_connection_graph() - - nx.write_dot(g, 'graph.dot') - subprocess.call(['dot', '-Tpng', 'graph.dot', '-o', 'graph.png']) - - # Matplotlib - #pos = nx.spring_layout(g) - #nx.draw_networkx_nodes(g, pos) - #nx.draw_networkx_edges(g, pos, arrows=True) - #nx.draw_networkx_labels(g, pos) - #plt.axis('off') - #plt.savefig('graph.png') - - connections.add_command(graph) - - -def init_cli_deployment_config(): - @click.command() - @click.argument('filepath') - def deploy(filepath): - print 'Deploying from file {}'.format(filepath) - xd.deploy(filepath) - - cli.add_command(deploy) - - -if __name__ == '__main__': - init_cli_resource() - init_cli_connect() - init_cli_connections() - init_cli_deployment_config() - init_changes() - - cli() diff --git a/solar/setup.py b/solar/setup.py index d50a6575..1a846ea9 100644 --- a/solar/setup.py +++ b/solar/setup.py @@ -46,4 +46,4 @@ setup( include_package_data=True, entry_points={ 'console_scripts': [ - 'solar = solar.cli:main']}) + 'solar = solar.cli:run']}) diff --git a/solar/solar/cli.py b/solar/solar/cli.py index 3a25a237..ecbc10fb 100644 --- a/solar/solar/cli.py +++ b/solar/solar/cli.py @@ -17,17 +17,22 @@ On create "golden" resource should be moved to special place """ -import argparse +import click +import json +import networkx as nx import os -import sys import pprint - -import textwrap +import subprocess import yaml from solar import utils +from solar import operations +from solar import state +from solar.core import actions +from solar.core import resource from solar.core.resource import assign_resources_to_nodes from solar.core.resource import connect_resources +from solar.core import signals from solar.core.tags_set_parser import Expression from solar.interfaces.db import get_db @@ -36,98 +41,19 @@ from solar.interfaces.db import get_db from solar.extensions.modules.discovery import Discovery -class Cmd(object): +db = get_db() - def __init__(self): - self.parser = argparse.ArgumentParser( - description=textwrap.dedent(__doc__), - formatter_class=argparse.RawDescriptionHelpFormatter) - self.subparser = self.parser.add_subparsers( - title='actions', - description='Supported actions', - help='Provide of one valid actions') - self.register_actions() - self.db = get_db() - def parse(self, args): - parsed = self.parser.parse_args(args) - return parsed.func(parsed) +@click.group() +def main(): + pass - def register_actions(self): - parser = self.subparser.add_parser('discover') - parser.set_defaults(func=getattr(self, 'discover')) - - # Profile actions - parser = self.subparser.add_parser('profile') - parser.set_defaults(func=getattr(self, 'profile')) - parser.add_argument('-l', '--list', dest='list', action='store_true') - group = parser.add_argument_group('create') - group.add_argument('-c', '--create', dest='create', action='store_true') - group.add_argument('-t', '--tags', nargs='+', default=['env/test_env']) - group.add_argument('-i', '--id', default=utils.generate_uuid()) - - # Assign - parser = self.subparser.add_parser('assign') - parser.set_defaults(func=getattr(self, 'assign')) - parser.add_argument('-n', '--nodes') - parser.add_argument('-r', '--resources') - - # Run action on tags - parser = self.subparser.add_parser('run') - parser.set_defaults(func=getattr(self, 'run')) - parser.add_argument('-t', '--tags') - parser.add_argument('-a', '--action') - - # Perform resources connection - parser = self.subparser.add_parser('connect') - parser.set_defaults(func=getattr(self, 'connect')) - parser.add_argument( - '-p', - '--profile') - - def run(self, args): - from solar.core import actions - from solar.core import resource - - resources = filter( - lambda r: Expression(args.tags, r.get('tags', [])).evaluate(), - self.db.get_list('resource')) - - for resource in resources: - resource_obj = resource.load(resource['id']) - actions.resource_action(resource_obj, args.action) - - def profile(self, args): - if args.create: - params = {'tags': args.tags, 'id': args.id} - profile_template_path = os.path.join( - utils.read_config()['template-dir'], 'profile.yml') - data = yaml.load(utils.render_template(profile_template_path, params)) - self.db.store('profiles', data) - else: - pprint.pprint(self.db.get_list('profiles')) - - def discover(self, args): - Discovery({'id': 'discovery'}).discover() - - def connect(self, args): - profile = self.db.get_record('profiles', args.profile) - connect_resources(profile) - - def assign(self, args): - nodes = filter( - lambda n: Expression(args.nodes, n.get('tags', [])).evaluate(), - self.db.get_list('nodes')) - - resources = filter( - lambda r: Expression(args.resources, r.get('tags', [])).evaluate(), - self._get_resources_list()) - - print("For {0} nodes assign {1} resources".format(len(nodes), len(resources))) - assign_resources_to_nodes(resources, nodes) - - def _get_resources_list(self): +@main.command() +@click.option('-n', '--nodes') +@click.option('-r', '--resources') +def assign(resources, nodes): + def _get_resources_list(): result = [] for path in utils.find_by_mask(utils.read_config()['resources-files-mask']): resource = utils.yaml_load(path) @@ -137,11 +63,241 @@ class Cmd(object): return result + nodes = filter( + lambda n: Expression(nodes, n.get('tags', [])).evaluate(), + db.get_list('nodes')) -def main(): - api = Cmd() - api.parse(sys.argv[1:]) + resources = filter( + lambda r: Expression(resources, r.get('tags', [])).evaluate(), + _get_resources_list()) + + print("For {0} nodes assign {1} resources".format(len(nodes), len(resources))) + assign_resources_to_nodes(resources, nodes) + + +@main.command() +@click.option('-p', '--profile') +def connect(profile): + profile_ = db.get_record('profiles', profile) + connect_resources(profile_) + + +@main.command() +def discover(): + Discovery({'id': 'discovery'}).discover() + + +@main.command() +@click.option('-c', '--create', default=False, is_flag=True) +@click.option('-t', '--tags', multiple=True) +@click.option('-i', '--id') +def profile(id, tags, create): + if not id: + id = utils.generate_uuid() + if create: + params = {'tags': tags, 'id': id} + profile_template_path = os.path.join( + utils.read_config()['template-dir'], 'profile.yml') + data = yaml.load(utils.render_template(profile_template_path, params)) + db.store('profiles', data) + else: + pprint.pprint(db.get_list('profiles')) + + +@main.command() +@click.option('-t', '--tags') +@click.option('-a', '--action') +def run(action, tags): + from solar.core import actions + from solar.core import resource + + resources = filter( + lambda r: Expression(tags, r.get('tags', [])).evaluate(), + db.get_list('resource')) + + for resource in resources: + resource_obj = resource.load(resource['id']) + actions.resource_action(resource_obj, action) + + +def init_cli_resource(): + @main.group() + def resource(): + pass + + @resource.command() + @click.argument('resource_path') + @click.argument('action_name') + def action(action_name, resource_path): + print 'action', resource_path, action_name + r = resource.load(resource_path) + actions.resource_action(r, action_name) + + @resource.command() + @click.argument('name') + @click.argument('base_path') + @click.argument('dest_path') + @click.argument('args') + def create(args, dest_path, base_path, name): + print 'create', name, base_path, dest_path, args + args = json.loads(args) + resource.create(name, base_path, dest_path, args) + + @resource.command() + @click.argument('resource_path') + @click.argument('tag_name') + @click.option('--add/--delete', default=True) + def tag(add, tag_name, resource_path): + print 'Tag', resource_path, tag_name, add + r = resource.load(resource_path) + if add: + r.add_tag(tag_name) + else: + r.remove_tag(tag_name) + r.save() + + @resource.command() + @click.argument('path') + @click.option('--all/--one', default=False) + @click.option('--tag', default=None) + @click.option('--use-json/--no-use-json', default=False) + def show(use_json, tag, all, path): + import json + import six + + printer = lambda r: six.print_(r) + if use_json: + printer = lambda r: six.print_(json.dumps(r.to_dict())) + + if all or tag: + for name, res in resource.load_all(path).items(): + show = True + if tag: + if tag not in res.tags: + show = False + + if show: + printer(res) + else: + printer(resource.load(path)) + + @resource.command() + @click.argument('name') + @click.argument('args') + def update(name, args): + args = json.loads(args) + all = resource.load_all() + r = all[name] + r.update(args) + + +def init_cli_connect(): + @main.command() + @click.argument('emitter') + @click.argument('receiver') + @click.option('--mapping', default=None) + def connect(mapping, receiver, emitter): + print 'Connect', emitter, receiver + emitter = resource.load(emitter) + receiver = resource.load(receiver) + print emitter + print receiver + if mapping is not None: + mapping = json.loads(mapping) + signals.connect(emitter, receiver, mapping=mapping) + + @main.command() + @click.argument('emitter') + @click.argument('receiver') + def disconnect(receiver, emitter): + print 'Disconnect', emitter, receiver + emitter = resource.load(emitter) + receiver = resource.load(receiver) + print emitter + print receiver + signals.disconnect(emitter, receiver) + + +def init_changes(): + @main.group() + def changes(): + pass + + @changes.command() + def stage(): + log = operations.stage_changes() + print log.show() + + @changes.command() + @click.option('--one', is_flag=True, default=False) + def commit(one): + if one: + operations.commit_one() + else: + operations.commit_changes() + + @changes.command() + @click.option('--limit', default=5) + def history(limit): + print state.CL().show() + + @changes.command() + @click.option('--last', is_flag=True, default=False) + @click.option('--all', is_flag=True, default=False) + @click.option('--uid', default=None) + def rollback(last, all, uid): + if last: + print operations.rollback_last() + elif all: + print operations.rollback_all() + elif uid: + print operations.rollback_uid(uid) + + +def init_cli_connections(): + @main.group() + def connections(): + pass + + @connections.command() + def show(): + print json.dumps(signals.CLIENTS, indent=2) + + # TODO: this requires graphing libraries + @connections.command() + def graph(): + #g = xs.connection_graph() + g = signals.detailed_connection_graph() + + nx.write_dot(g, 'graph.dot') + subprocess.call(['dot', '-Tpng', 'graph.dot', '-o', 'graph.png']) + + # Matplotlib + #pos = nx.spring_layout(g) + #nx.draw_networkx_nodes(g, pos) + #nx.draw_networkx_edges(g, pos, arrows=True) + #nx.draw_networkx_labels(g, pos) + #plt.axis('off') + #plt.savefig('graph.png') + + +def init_cli_deployment_config(): + @main.command() + @click.argument('filepath') + def deploy(filepath): + print 'Deploying from file {}'.format(filepath) + xd.deploy(filepath) + + +def run(): + init_cli_resource() + init_cli_connect() + init_cli_connections() + init_cli_deployment_config() + init_changes() + + main() if __name__ == '__main__': - main() + run() diff --git a/solar/solar/core/observer.py b/solar/solar/core/observer.py index 39981f20..3ff35230 100644 --- a/solar/solar/core/observer.py +++ b/solar/solar/core/observer.py @@ -187,6 +187,8 @@ class ListObserver(BaseObserver): self.log('Unsubscribed emitter {}'.format(emitter)) idx = self._emitter_idx(emitter) self.value.pop(idx) + for receiver in self.receivers: + receiver.notify(self) def _emitter_idx(self, emitter): try: diff --git a/solar/solar/test/test_signals.py b/solar/solar/test/test_signals.py index 8b80e708..08348977 100644 --- a/solar/solar/test/test_signals.py +++ b/solar/solar/test/test_signals.py @@ -329,6 +329,91 @@ input: (sample2.args['port'].attached_to.name, 'port')] ) + # Test disconnect + xs.disconnect(sample2, list_input_multi) + self.assertEqual( + [ip['value'] for ip in list_input_multi.args['ips'].value], + [sample1.args['ip']] + ) + self.assertEqual( + [p['value'] for p in list_input_multi.args['ports'].value], + [sample1.args['port']] + ) + + def test_nested_list_input(self): + sample_meta_dir = self.make_resource_meta(""" +id: sample +handler: ansible +version: 1.0.0 +input: + ip: + schema: str + value: + port: + schema: int + value: + """) + list_input_meta_dir = self.make_resource_meta(""" +id: list-input +handler: ansible +version: 1.0.0 +input: + ips: + schema: [str] + value: [] + ports: + schema: [int] + value: [] + """) + list_input_nested_meta_dir = self.make_resource_meta(""" +id: list-input-nested +handler: ansible +version: 1.0.0 +input: + ipss: + schema: [[str]] + value: [] + portss: + schema: [[int]] + value: [] + """) + + sample1 = self.create_resource( + 'sample1', sample_meta_dir, {'ip': '10.0.0.1', 'port': '1000'} + ) + sample2 = self.create_resource( + 'sample2', sample_meta_dir, {'ip': '10.0.0.2', 'port': '1001'} + ) + list_input = self.create_resource( + 'list-input', list_input_meta_dir, {'ips': [], 'ports': []} + ) + list_input_nested = self.create_resource( + 'list-input-nested', list_input_nested_meta_dir, {'ipss': [], 'portss': []} + ) + + xs.connect(sample1, list_input, mapping={'ip': 'ips', 'port': 'ports'}) + xs.connect(sample2, list_input, mapping={'ip': 'ips', 'port': 'ports'}) + xs.connect(list_input, list_input_nested, mapping={'ips': 'ipss', 'ports': 'portss'}) + self.assertListEqual( + [ips['value'] for ips in list_input_nested.args['ipss'].value], + [list_input.args['ips'].value] + ) + self.assertListEqual( + [ps['value'] for ps in list_input_nested.args['portss'].value], + [list_input.args['ports'].value] + ) + + # Test disconnect + xs.disconnect(sample1, list_input) + self.assertListEqual( + [[ip['value'] for ip in ips['value']] for ips in list_input_nested.args['ipss'].value], + [[sample2.args['ip'].value]] + ) + self.assertListEqual( + [[p['value'] for p in ps['value']] for ps in list_input_nested.args['portss'].value], + [[sample2.args['port'].value]] + ) + ''' class TestMultiInput(base.BaseResourceTest):