Exemplo n.º 1
0
    def __init__(self):

        #setup root logger
        self.logger = logging.getLogger('opencenter')

        if "OPENCENTER_CLIENT_DEBUG" in os.environ:
            self.logger.setLevel(logging.DEBUG)

        if not self.logger.handlers:
            self.logger.addHandler(logging.StreamHandler(sys.stderr))

        #Warn if using default endpoint.
        default_endpoint = 'http://localhost:8080'
        if 'OPENCENTER_ENDPOINT' in os.environ:
            endpoint_url = os.environ['OPENCENTER_ENDPOINT']
        else:
            self.logger.warn("OPENCENTER_ENDPOINT not found in environment"
                             ", using %s" % default_endpoint)
            endpoint_url = default_endpoint

        self.endpoint = OpenCenterEndpoint(endpoint=endpoint_url)
Exemplo n.º 2
0
 def set_endpoint(self, endpoint_url):
     self.endpoint = OpenCenterEndpoint(endpoint=endpoint_url,
                                        interactive=True)
Exemplo n.º 3
0
class OpenCenterShell():

    def set_endpoint(self, endpoint_url):
        self.endpoint = OpenCenterEndpoint(endpoint=endpoint_url,
                                           interactive=True)

    def set_log_level(self, level):
        self.logger = logging.getLogger('opencenter')
        self.logger.setLevel(level)
        streamHandler = logging.StreamHandler()
        streamFormat = logging.Formatter('%(asctime)s - %(name)s - '
                                         '%(levelname)s - %(message)s')
        streamHandler.setFormatter(streamFormat)
        self.logger.addHandler(streamHandler)

    def parse_args(self, argv):
        """Parse arguments using Argparse.

        Approach: arg_tree is a multi level dictionary that contains all the
        arguments. This is tree is walked in order to build a
        corresponding tree of ArgumentParsers.

        There is a default set of actions in the actions dictionary that can
        be grafted into the arg_tree to avoid repeating common subcommands.
        This is achieved via the deep_update function.

        """

        if 'OPENCENTER_CLIENT_ARGPARSE_DEBUG' in os.environ:
            arg_debug = True
            self.set_log_level(logging.DEBUG)
        else:
            arg_debug = False

        # Base list of actions. this can be included in the arg_tree as a
        # default set of actions for a noun (eg node, task).

        # help strings will be formated with .format . The argument will be
        # a list representing the path in the tree, eg:
        #       node > list
        #       0      1
        # so {0} will usually refer to the type of object specified.

        # In the help output, Subcommands are listed alphabetically.
        # Argument order can be influenced with an 'order' key under an
        # argument. The default order is 0 and negative numbers sort first.

        # Read Only Actions:
        ro_actions = {
            'list': {
                'help': 'List all {0}s',
                'args': {}
            },
            'show': {
                'help': 'Show the properties of a {0}',
                'args': {
                    'id_or_name': {
                        'help': 'name or id of the {0} to show'
                    },
                    '--property': {
                        'help': 'Only print one property of this '
                                '{0}. Example: --property id. If the '
                                'property is a nested structure, '
                                'then dotted paths can be specified. Example:'
                                ' --property attrs.opencenter_agent_actions.'
                                'upgrade_agent.timeout Lookup tries object'
                                ' attributes, dictionary keys and list '
                                'indices. '
                    }
                }
            },
            'filter': {
                'help': ('list {0}s that match filter-string. '
                         'Example: id=4 or name="workspace"'),
                'args': {
                    'filter_string': {
                        'help': 'filter string, '
                                'Example: id=4 or name="workspace"'
                    }
                }
            }
        }

        #ReadWrite actions = RO actions plus the following:
        rw_actions = deep_update(ro_actions, {
            'delete': {
                'help': 'Delete a {0}',
                'args': {
                    'id_or_name': {
                        'help': 'ID or name of {0} to delete.'
                    }
                }
            },
            'create': {
                'help': 'Create a {0}',
                'args': {
                    'name': {
                        'help': 'Name of the new {0}'
                    }
                }
            },
            'update': {
                'help': 'Modify a {0}',
                'args': {
                    'id': {
                        'help': 'id of {0} to update',
                        'order': -1
                    }
                }
            },

        })

        # Commands are nodes, args are leaves.
        # When there is a choice of subcommand, the chosen command is stored
        # in the namespace object under the key 'dest'. (The namespace
        # object is the thing returned by ArgumentParser.parse_args())
        arg_tree = {
            'node': {
                'help': 'An opencenter object, may represent a server'
                        ' or a container for other nodes. ',
                'dest': 'cli_action',
                'subcommands': deep_update(rw_actions, {
                    'adventure': {
                        'help': 'Adventure related commands for a node.',
                        'dest': 'node_adventure_subcommand',
                        'subcommands': {
                            'execute': {
                                'help': 'Execute an adventure against this '
                                'node.',
                                'args': {
                                    'node_id_or_name': {
                                        'help': 'Name or ID of node to '
                                                'execute adventure against.',
                                        'order': -1
                                    },
                                    'adventure_id_or_name': {
                                        'help': 'Name or ID of adventure to '
                                                'execute.'
                                    }
                                }
                            },
                            'list': {
                                'help': 'List adventures that can be executed '
                                        'against this node.',
                                'args': {
                                    'node_id_or_name': {}
                                }
                            }
                        }
                    },
                    'move': {
                        'help': 'Move a node to a different container. This '
                                'is an alias for "fact create node parent_id '
                                'new_parent". This operation is not available'
                                ' if either the node to be moved or '
                                'current/destination container has the '
                                'locked attribute set. ',
                        'args': {
                            'node_id_or_name': {
                                'help': 'id or name of node to move',
                                'order': -1
                            },
                            'new_parent_id_or_name': {
                                'help': 'id or name of container node to '
                                        'move into'
                            }
                        }
                    },
                    'file': {
                        'help': 'list or retrieve files from a node that is '
                                'running the opencenter agent',
                        'args': {
                            'node_id_or_name': {
                                'help': 'Name or ID of the node to list or '
                                        'retrieve files from.',
                                'order': -1
                            },
                            'action': {
                                'choices': ['list', 'get'],
                                'help': 'Retrieve a list of files at a path, '
                                        'or retrieve an individual file',
                                'order': -2
                            },
                            'path': {
                                'help': 'Path to directory to list or file to '
                                        'retrieve. This is a local filesystem '
                                        'path on the system that is running '
                                        'the OpenCenter agent.'
                            }
                        }
                    }

                })
            },
            'task': {
                'help': 'An action that runs against a node',
                'dest': 'cli_action',
                'subcommands': deep_update(rw_actions, {
                    'update': None,
                    'delete': None,
                    'create': {
                        'args': {
                            'name': None,
                            'action': {
                                'help': 'Action for this task to execute. '
                                        'Valid actions are listed in each '
                                        'node\'s opencenter_agent_actions'
                                        'attribute'
                            },
                            'node_id_or_name': {
                                'help': 'Node to execute this action on.',
                                'order': -1
                            },
                            'payload': {
                                'help': 'JSON string containing inputs for '
                                        'the task.',
                                'order': 1
                            }
                        }
                    },
                    'logs': {
                        'help': 'Retrieve task logs',
                        'args': {
                            'task_id': {
                                'help': 'ID of the task to retrieve logs for'
                            },
                            '--offset': {
                                'help': 'Log offset, '
                                        'usage: Display last n bites '
                                        'of'
                                        ' log: --offset -n. Skip first n '
                                        'bites of log: -offset +n. Retrieve '
                                        'whole log: --offset +0'
                                        '  '
                            }
                        }
                    }
                })
            },
            'fact': {
                'help': 'An inheritable property of a node',
                'dest': 'cli_action',
                'subcommands': deep_update(rw_actions, {
                    'create': {
                        'args': {
                            'key': {
                                'help': 'The name of the fact to create'
                            },
                            'value': {
                                'help': 'The value to store against the key'
                            },
                            'node_id_or_name': {
                                'help': 'The node to set this fact against',
                                'order': -1
                            },
                            'name': None
                        }
                    },
                    'update': {
                        'args': {
                            'value': {
                                'help': 'new value',
                                'order': 2
                            }
                        }
                    }
                })
            },
            'attr': {
                'help': 'A non-inherritable attribute of a node',
                'dest': 'cli_action',
                'subcommands': deep_update(rw_actions, {
                    'create': {
                        'args': {
                            'node_id_or_name': {
                                'help': 'The node to set this attribute on',
                                'order': -1
                            },
                            'key': {
                                'help': 'new key',
                                'order': 1
                            },
                            'value': {
                                'help': 'new value',
                                'order': 2
                            },
                            'name': None
                        }
                    },
                    'update': {
                        'args': {
                            'value': {
                                'help': 'new value',
                                'order': 2
                            }
                        }
                    }
                })
            },
            'adventure': {
                'help': 'A predefined set of tasks for achieving a goal.',
                'dest': 'cli_action',
                'subcommands': deep_update(rw_actions, {
                    'execute': {
                        'help': 'Execute an adventure',
                        'args': {
                            'adventure_id_or_name': {
                                'order': +1
                            },
                            'node_id_or_name': {}
                        }
                    },
                    'create': {
                        'args': {
                            'name': {
                                'help': 'Name of the new Adventure.',
                                'order': -1
                            },
                            'arguments': {
                                'help': 'Arguments for this Adventure, '
                                        'JSON string.',
                                'order': 1
                            },
                            'dsl': {
                                'help': 'Domain Specific Languague for '
                                        'defining adventures. For example: '
                                        '[ {{ "ns": {{}}, "primitive": '
                                        '"download_cookbooks" }} ]',
                                'order': 2
                            },
                            'criteria': {
                                'help': 'Filter string written in the '
                                        'opencenter filter languague.',
                                'order': 3
                            }
                        }
                    },
                    'update': {
                        'args': {
                            'id_or_name': {
                                'help': 'name or id of adventure to update',
                                'order': -1
                            },
                            '--name': {
                                'help': 'New name for this adventure.'
                            },
                            '--arguments': {
                                'help': 'Arguments for this Adventure, '
                                        'JSON string.',
                                'order': 1
                            },
                            '--dsl': {
                                'help': 'Domain Specific Languague for '
                                        'defining adventures. For example: '
                                        '[ {{ "ns": {{}}, "primitive": '
                                        '"download_cookbooks" }} ]',
                                'order': 2
                            },
                            '--criteria': {
                                'help': 'Filter string written in the '
                                        'opencenter filter languague.',
                                'order': 3
                            },
                            'id': None
                        }
                    }
                })
            },
            'primitive': {
                'help': 'A low level action that can be executed as part of '
                        'an OpenCenter adventure.',
                'dest': 'cli_action',
                'subcommands': ro_actions
            }
        }

        if arg_debug:
            self.logger.debug(json.dumps(arg_tree, sort_keys=True, indent=2,
                              separators=(',', ':')))

        def _traverse_arg_tree(tree, parser, parents=None, dest="", help="",
                               path=None):
            """Recursive function for walking the arg_tree and building a
            corresponding tree of ArgumentParsers"""

            if len(tree) == 0:
                return

            sub_parsers = None
            for command_name, command_dict in sorted(tree.items(),
                                                     key=lambda x: x[0]):
                _path = copy.deepcopy(path)
                _path.append(command_name)
                if arg_debug:
                    self.logger.debug(_path)
                if 'subcommands' in command_dict:

                    if sub_parsers is None:
                        sub_parsers = parser.add_subparsers(dest=dest,
                                                            help=help)
                    command_parser = sub_parsers.add_parser(
                        command_name,
                        help=command_dict['help'] if
                        'help' in command_dict else "",
                        parents=parents
                    )

                    _traverse_arg_tree(tree=command_dict['subcommands'],
                                       parser=command_parser,
                                       parents=parents,
                                       dest=command_dict['dest'],
                                       help="Commands relating to %s" %
                                            command_name,
                                       path=_path)

                elif 'args' in command_dict:
                    if sub_parsers is None:
                        sub_parsers = parser.add_subparsers(dest=dest,
                                                            help=help)
                    command_parser = sub_parsers.add_parser(
                        command_name,
                        help=command_dict['help'].format(*_path) if
                        'help'in command_dict else '',
                        parents=parents
                    )

                    # parents and dest are not needed as there will be no
                    # more sub levels - the next recusive call will be
                    # adding args, which are the leaves of this tree.
                    _traverse_arg_tree(tree={'args': command_dict['args']},
                                       parser=command_parser,
                                       help="Commands relating to %s" % (
                                           command_name),
                                       path=_path)

                elif command_name == 'args':
                    for arg_name, arg_dict in command_dict.items():
                        if arg_debug:
                            self.logger.debug('%s, %s' % (arg_name,
                                                          str(arg_dict)))
                        if 'order' not in arg_dict:
                            arg_dict['order'] = 0

                    for arg_name, arg_dict in sorted(command_dict.items(),
                                                     key=lambda x: x[1][
                                                         'order']):
                        if 'help' in arg_dict:
                            arg_dict['help'] = arg_dict['help'].format(*_path)
                        del arg_dict['order']
                        parser.add_argument(arg_name, **arg_dict)

        # The global_options parser will be added to all other parsers as a
        # parent. This ensures that these options are available at every
        # level of command.
        global_options = argparse.ArgumentParser(add_help=False)
        global_options.add_argument(
            "--debug",
            help="Print debug information such as API requests",
            action='store_true'
        )

        # Precedence for endpoint URL:
        #      command line option > environment variable > default
        global_options.add_argument(
            '--endpoint',
            default=os.environ['OPENCENTER_ENDPOINT'] if
            'OPENCENTER_ENDPOINT' in os.environ else "http://*****:*****@host:8443"
        )

        #Root parser - all other commands will be added as sub parsers.
        parser = argparse.ArgumentParser(description='OpenCenter CLI',
                                         prog='opencentercli',
                                         parents=[global_options]
                                         )

        #kick off arg_tree traversal
        _traverse_arg_tree(tree=arg_tree,
                           parser=parser,
                           parents=[global_options],
                           dest="cli_noun",
                           help="subcommands",
                           path=[])

        #parse args and return a namespace object
        return parser.parse_args(argv)

    def get_field_schema(self, command):
        obj = getattr(self.endpoint, command)
        schema = self.endpoint.get_schema(singularize(command))
        fields = schema.field_schema
        return fields

    def do_show(self, args, obj):
        """Print a whole object, or a specific property following a dotted
        path.

        When a dotted path is specified (eg:
        attrs.opencenter_agent_actions.upgrade_agent.timeout),
        lookup is done in three ways:
            1) Object Attribute: getattr
            2) Dictionary key:  []
            3) List Key: convert to int, then []

        """
        id = args.id
        act = getattr(self.endpoint, obj)
        if args.property is None:
            #No property specified, print whole item.
            print act[id]
        else:
            item = act[id]
            for path_section in args.property.split('.'):

                # Lookup by object attribute
                if hasattr(item, path_section):
                    item = getattr(item, path_section)
                    continue
                else:
                    try:
                        # Lookup by dictionary key
                        item = item[path_section]
                        continue
                    except:
                        try:
                            # Lookup by list index
                            item = item[int(path_section)]
                            continue
                        except:
                            pass

                # None of the lookup methods succeeded, so property path must
                # be invalid.
                raise ValueError(
                    'Cannot resolve "%s" from property string "%s" for'
                    ' %s %s' % (
                        path_section,
                        args.property,
                        singularize(obj),
                        act[id].name
                    )
                )

            # Assume the property is JSON and try to pretty-print. If that
            # fails, print the item normally
            try:
                print json.dumps(item, sort_keys=True, indent=2,
                                 separators=(',', ':'))
            except:
                print item

    def do_logs(self, args):
        id = args.task_id
        task = self.endpoint.tasks[id]
        print "=== Logs for task %s: %s > %s ===" % (id, task.node.name,
                                                     task.action)
        print task._logtail(offset=args.offset)
        print "=== End of Logs ==="

    def do_filter(self, args, obj):
        act = getattr(self.endpoint, obj)
        print act.filter(args.filter_string)

    def do_create(self, args, obj):
        field_schema = self.get_field_schema(obj)
        arguments = []
        for field in field_schema:
            arguments.append(field)

        ver = dict([(k, v) for k, v in args._get_kwargs()
                   if k in arguments and v is not None])
        act = getattr(self.endpoint, obj)
        new_node = act.create(**ver)
        new_node.save()
        return new_node

    def do_delete(self, args, obj):
        try:
            id = args.id
            act = getattr(self.endpoint, obj)
            act[id].delete()
            print "%s %s has been deleted." % tuple([obj, id])
        except Exception, e:
            print "%s" % e
Exemplo n.º 4
0
class OpenCenterShell():
    def __init__(self):

        #setup root logger
        self.logger = logging.getLogger('opencenter')

        if "OPENCENTER_CLIENT_DEBUG" in os.environ:
            self.logger.setLevel(logging.DEBUG)

        if not self.logger.handlers:
            self.logger.addHandler(logging.StreamHandler(sys.stderr))

        #Warn if using default endpoint.
        default_endpoint = 'http://localhost:8080'
        if 'OPENCENTER_ENDPOINT' in os.environ:
            endpoint_url = os.environ['OPENCENTER_ENDPOINT']
        else:
            self.logger.warn("OPENCENTER_ENDPOINT not found in environment"
                             ", using %s" % default_endpoint)
            endpoint_url = default_endpoint

        self.endpoint = OpenCenterEndpoint(endpoint=endpoint_url,
                                           interactive=True)

    def get_base_parser(self):
        parser = argparse.ArgumentParser(description='OpenCenter CLI',
                                         prog='opencentercli',
                                         )
        parser.add_argument('-v', '--verbose',
                            action='store_true',
                            help='Print more verbose output')

        #chicken-egg issues. Parser requires schema, which reuquires endpoint..
        #parser.add_argument('--endpoint',
        #                    help="OpenCenter endpoint URL.",metavar="URL")

        return parser

    def get_subcommand_parser(self):
        parser = self.get_base_parser()
        self.subcommands = {}
        type_parsers = parser.add_subparsers(help='subcommands',
                                             dest='cli_noun')
        self._construct_parse_tree(type_parsers)
        return parser

    def _construct_parse_tree(self, type_parsers):
        """
        obj_type = object type eg Task, Adventure
        action = command eg create, delete
        argument = required, or optional argument.
        """
        obj_types = self.endpoint._object_lists.keys()

        #information about each action
        actions = {
            'list': {'description': 'list all %ss',
                     'args': [],
                     },
            'show': {'description': 'show the properties of a %s',
                     'args': ['--id']
                     },
            'delete': {'description': 'remove a %s',
                       'args': ['id']
                       },
            'create': {'description': 'create a %s',
                       'args': ['schema']
                       },
            'update': {'description': 'modify a %s',
                       'args': ['schema']
                       },
            'execute': {'description': 'execute a %s',
                        'args': ['node_id', 'adventure_id'],
                        'applies_to': ['adventure']
                        },
            'filter': {'description': ('list %ss that match filter-string. '
                                       'Example filter string: '
                                       'name=workspace'),
                       'args': ['filter_string']
                       },
            'adventures': {'description': ('List currently available '
                                           'adventures for a %s'),
                           'args': ['id'],
                           'applies_to': ['node']
                           },
            'logs': {'description': 'Get output logged by a %s',
                     'args': ['id', '--offset'],
                     'applies_to': ['task']
                     }
        }

        # Hash for adding descriptions to specific arguments.
        # Useful for args that have come from the schema.
        descriptions = {
            'adventures': {
                'create': {
                    'dsl': ('Domain Specific Languague for defining '
                            ' adventures. For example: '
                            '[ { "ns": {}, "primitive": "download_cookbooks" '
                            '} ]')
                }

            }
        }

        def _get_help(obj, action, arg):
            """Function for retrieving help values from the descriptions
            hash if they exist."""
            arg_help = None
            if obj in descriptions\
                    and action in descriptions[obj]\
                    and arg in descriptions[obj][action]:
                arg_help = descriptions[obj][action][arg]
            return arg_help

        for obj_type in obj_types:
            schema = self.endpoint.get_schema(singularize(obj_type))
            arguments = schema.field_schema
            callback = getattr(self.endpoint, obj_type)
            desc = callback.__doc__ or ''

            type_parser = type_parsers.add_parser(singularize(obj_type),
                                                  help='%s actions' %
                                                  singularize(obj_type),
                                                  description=desc,
                                                  )

            #"action" clashses with the action attribute of some object types
            #for example task.action, so the action arg is stored as cli_action
            action_parsers = type_parser.add_subparsers(dest='cli_action')
            for action in actions:

                #skip this action if it doesn't apply to this obj_type.
                if 'applies_to' in actions[action]:
                    if singularize(obj_type) not in \
                            actions[action]['applies_to']:
                        continue

                action_parser = action_parsers.add_parser(
                    action,
                    help=actions[action]['description'] % singularize(obj_type)
                )

                #check the descriptions hash for argument help
                arg_help = None
                if obj_type in descriptions and action in \
                        descriptions[obj_type] and arg_name in \
                        descriptions[obj_type][action]:
                    arg_help = descriptions[obj_type][action][arg_name]

                action_args = actions[action]['args']
                if action_args == ['schema']:
                    for arg_name, arg in arguments.items():

                        arg_help = _get_help(obj_type, action, arg_name)

                        #id should be allocated rather than specified
                        if action == "create" and arg_name == 'id':
                            continue
                        opt_string = '--'
                        if arg['required']:
                            opt_string = ''
                        action_parser.add_argument('%s%s' %
                                                   (opt_string, arg_name),
                                                   help=arg_help)
                else:
                    for arg in action_args:
                        arg_help = _get_help(obj_type, action, arg)
                        action_parser.add_argument(arg, help=arg_help)
            self.subcommands[obj_type] = type_parser
            type_parser.set_defaults(func=callback)

    def get_field_schema(self, command):
        obj = getattr(self.endpoint, command)
        schema = self.endpoint.get_schema(singularize(command))
        fields = schema.field_schema
        return fields

    def do_show(self, args, obj):
        id = args.id
        act = getattr(self.endpoint, obj)
        print act[id]

    def do_logs(self, args, obj):
        id = args.id
        kwargs = {'offset': args.offset}
        act = getattr(self.endpoint, obj)
        task = act[id]
        print "=== Logs for task %s: %s > %s ===" % (id, task.node.name,
                                                     task.action)
        print task._logtail(**kwargs)
        print "=== End of Logs ==="

    def do_adventures(self, args, obj):
        act = getattr(self.endpoint, obj)
        print act[args.id]._adventures()

    def do_filter(self, args, obj):
        act = getattr(self.endpoint, obj)
        print act.filter(args.filter_string)

    def do_create(self, args, obj):
        field_schema = self.get_field_schema(obj)
        arguments = []
        for field in field_schema:
            arguments.append(field)

        ver = dict([(k, v) for k, v in args._get_kwargs()
                   if k in arguments and v is not None])
        act = getattr(self.endpoint, obj)
        new_node = act.create(**ver)
        new_node.save()

    def do_delete(self, args, obj):
        try:
            id = args.id
            act = getattr(self.endpoint, obj)
            act[id].delete()
            print "%s %s has been deleted." % tuple([obj, id])
        except Exception, e:
            print "%s" % e
Exemplo n.º 5
0
 def set_endpoint(self, endpoint_url):
     self.endpoint = OpenCenterEndpoint(endpoint=endpoint_url,
                                        interactive=True)
Exemplo n.º 6
0
class OpenCenterShell():
    def set_endpoint(self, endpoint_url):
        self.endpoint = OpenCenterEndpoint(endpoint=endpoint_url,
                                           interactive=True)

    def set_log_level(self, level):
        self.logger = logging.getLogger('opencenter')
        self.logger.setLevel(level)
        streamHandler = logging.StreamHandler()
        streamFormat = logging.Formatter('%(asctime)s - %(name)s - '
                                         '%(levelname)s - %(message)s')
        streamHandler.setFormatter(streamFormat)
        self.logger.addHandler(streamHandler)

    def parse_args(self, argv):
        """Parse arguments using Argparse.

        Approach: arg_tree is a multi level dictionary that contains all the
        arguments. This is tree is walked in order to build a
        corresponding tree of ArgumentParsers.

        There is a default set of actions in the actions dictionary that can
        be grafted into the arg_tree to avoid repeating common subcommands.
        This is achieved via the deep_update function.

        """

        if 'OPENCENTER_CLIENT_ARGPARSE_DEBUG' in os.environ:
            arg_debug = True
            self.set_log_level(logging.DEBUG)
        else:
            arg_debug = False

        # Base list of actions. this can be included in the arg_tree as a
        # default set of actions for a noun (eg node, task).

        # help strings will be formated with .format . The argument will be
        # a list representing the path in the tree, eg:
        #       node > list
        #       0      1
        # so {0} will usually refer to the type of object specified.

        # In the help output, Subcommands are listed alphabetically.
        # Argument order can be influenced with an 'order' key under an
        # argument. The default order is 0 and negative numbers sort first.

        # Read Only Actions:
        ro_actions = {
            'list': {
                'help': 'List all {0}s',
                'args': {}
            },
            'show': {
                'help': 'Show the properties of a {0}',
                'args': {
                    'id_or_name': {
                        'help': 'name or id of the {0} to show'
                    },
                    '--property': {
                        'help':
                        'Only print one property of this '
                        '{0}. Example: --property id. If the '
                        'property is a nested structure, '
                        'then dotted paths can be specified. Example:'
                        ' --property attrs.opencenter_agent_actions.'
                        'upgrade_agent.timeout Lookup tries object'
                        ' attributes, dictionary keys and list '
                        'indices. '
                    }
                }
            },
            'filter': {
                'help': ('list {0}s that match filter-string. '
                         'Example: id=4 or name="workspace"'),
                'args': {
                    'filter_string': {
                        'help':
                        'filter string, '
                        'Example: id=4 or name="workspace"'
                    }
                }
            }
        }

        #ReadWrite actions = RO actions plus the following:
        rw_actions = deep_update(
            ro_actions, {
                'delete': {
                    'help': 'Delete a {0}',
                    'args': {
                        'id_or_name': {
                            'help': 'ID or name of {0} to delete.'
                        }
                    }
                },
                'create': {
                    'help': 'Create a {0}',
                    'args': {
                        'name': {
                            'help': 'Name of the new {0}'
                        }
                    }
                },
                'update': {
                    'help': 'Modify a {0}',
                    'args': {
                        'id': {
                            'help': 'id of {0} to update',
                            'order': -1
                        }
                    }
                },
            })

        # Commands are nodes, args are leaves.
        # When there is a choice of subcommand, the chosen command is stored
        # in the namespace object under the key 'dest'. (The namespace
        # object is the thing returned by ArgumentParser.parse_args())
        arg_tree = {
            'node': {
                'help':
                'An opencenter object, may represent a server'
                ' or a container for other nodes. ',
                'dest':
                'cli_action',
                'subcommands':
                deep_update(
                    rw_actions, {
                        'adventure': {
                            'help': 'Adventure related commands for a node.',
                            'dest': 'node_adventure_subcommand',
                            'subcommands': {
                                'execute': {
                                    'help':
                                    'Execute an adventure against this '
                                    'node.',
                                    'args': {
                                        'node_id_or_name': {
                                            'help': 'Name or ID of node to '
                                            'execute adventure against.',
                                            'order': -1
                                        },
                                        'adventure_id_or_name': {
                                            'help':
                                            'Name or ID of adventure to '
                                            'execute.'
                                        }
                                    }
                                },
                                'list': {
                                    'help':
                                    'List adventures that can be executed '
                                    'against this node.',
                                    'args': {
                                        'node_id_or_name': {}
                                    }
                                }
                            }
                        },
                        'move': {
                            'help':
                            'Move a node to a different container. This '
                            'is an alias for "fact create node parent_id '
                            'new_parent". This operation is not available'
                            ' if either the node to be moved or '
                            'current/destination container has the '
                            'locked attribute set. ',
                            'args': {
                                'node_id_or_name': {
                                    'help': 'id or name of node to move',
                                    'order': -1
                                },
                                'new_parent_id_or_name': {
                                    'help':
                                    'id or name of container node to '
                                    'move into'
                                }
                            }
                        },
                        'file': {
                            'help':
                            'list or retrieve files from a node that is '
                            'running the opencenter agent',
                            'args': {
                                'node_id_or_name': {
                                    'help':
                                    'Name or ID of the node to list or '
                                    'retrieve files from.',
                                    'order': -1
                                },
                                'action': {
                                    'choices': ['list', 'get'],
                                    'help':
                                    'Retrieve a list of files at a path, '
                                    'or retrieve an individual file',
                                    'order': -2
                                },
                                'path': {
                                    'help':
                                    'Path to directory to list or file to '
                                    'retrieve. This is a local filesystem '
                                    'path on the system that is running '
                                    'the OpenCenter agent.'
                                }
                            }
                        }
                    })
            },
            'task': {
                'help':
                'An action that runs against a node',
                'dest':
                'cli_action',
                'subcommands':
                deep_update(
                    rw_actions, {
                        'update': None,
                        'delete': None,
                        'create': {
                            'args': {
                                'name': None,
                                'action': {
                                    'help':
                                    'Action for this task to execute. '
                                    'Valid actions are listed in each '
                                    'node\'s opencenter_agent_actions'
                                    'attribute'
                                },
                                'node_id_or_name': {
                                    'help': 'Node to execute this action on.',
                                    'order': -1
                                },
                                'payload': {
                                    'help':
                                    'JSON string containing inputs for '
                                    'the task.',
                                    'order': 1
                                }
                            }
                        },
                        'logs': {
                            'help': 'Retrieve task logs',
                            'args': {
                                'task_id': {
                                    'help':
                                    'ID of the task to retrieve logs for'
                                },
                                '--offset': {
                                    'help':
                                    'Log offset, '
                                    'usage: Display last n bites '
                                    'of'
                                    ' log: --offset -n. Skip first n '
                                    'bites of log: -offset +n. Retrieve '
                                    'whole log: --offset +0'
                                    '  '
                                }
                            }
                        }
                    })
            },
            'fact': {
                'help':
                'An inheritable property of a node',
                'dest':
                'cli_action',
                'subcommands':
                deep_update(
                    rw_actions, {
                        'create': {
                            'args': {
                                'key': {
                                    'help': 'The name of the fact to create'
                                },
                                'value': {
                                    'help':
                                    'The value to store against the key'
                                },
                                'node_id_or_name': {
                                    'help':
                                    'The node to set this fact against',
                                    'order': -1
                                },
                                'name': None
                            }
                        },
                        'update': {
                            'args': {
                                'value': {
                                    'help': 'new value',
                                    'order': 2
                                }
                            }
                        }
                    })
            },
            'attr': {
                'help':
                'A non-inherritable attribute of a node',
                'dest':
                'cli_action',
                'subcommands':
                deep_update(
                    rw_actions, {
                        'create': {
                            'args': {
                                'node_id_or_name': {
                                    'help':
                                    'The node to set this attribute on',
                                    'order': -1
                                },
                                'key': {
                                    'help': 'new key',
                                    'order': 1
                                },
                                'value': {
                                    'help': 'new value',
                                    'order': 2
                                },
                                'name': None
                            }
                        },
                        'update': {
                            'args': {
                                'value': {
                                    'help': 'new value',
                                    'order': 2
                                }
                            }
                        }
                    })
            },
            'adventure': {
                'help':
                'A predefined set of tasks for achieving a goal.',
                'dest':
                'cli_action',
                'subcommands':
                deep_update(
                    rw_actions, {
                        'execute': {
                            'help': 'Execute an adventure',
                            'args': {
                                'adventure_id_or_name': {
                                    'order': +1
                                },
                                'node_id_or_name': {}
                            }
                        },
                        'create': {
                            'args': {
                                'name': {
                                    'help': 'Name of the new Adventure.',
                                    'order': -1
                                },
                                'arguments': {
                                    'help': 'Arguments for this Adventure, '
                                    'JSON string.',
                                    'order': 1
                                },
                                'dsl': {
                                    'help':
                                    'Domain Specific Languague for '
                                    'defining adventures. For example: '
                                    '[ {{ "ns": {{}}, "primitive": '
                                    '"download_cookbooks" }} ]',
                                    'order':
                                    2
                                },
                                'criteria': {
                                    'help': 'Filter string written in the '
                                    'opencenter filter languague.',
                                    'order': 3
                                }
                            }
                        },
                        'update': {
                            'args': {
                                'id_or_name': {
                                    'help':
                                    'name or id of adventure to update',
                                    'order': -1
                                },
                                '--name': {
                                    'help': 'New name for this adventure.'
                                },
                                '--arguments': {
                                    'help': 'Arguments for this Adventure, '
                                    'JSON string.',
                                    'order': 1
                                },
                                '--dsl': {
                                    'help':
                                    'Domain Specific Languague for '
                                    'defining adventures. For example: '
                                    '[ {{ "ns": {{}}, "primitive": '
                                    '"download_cookbooks" }} ]',
                                    'order':
                                    2
                                },
                                '--criteria': {
                                    'help': 'Filter string written in the '
                                    'opencenter filter languague.',
                                    'order': 3
                                },
                                'id': None
                            }
                        }
                    })
            },
            'primitive': {
                'help': 'A low level action that can be executed as part of '
                'an OpenCenter adventure.',
                'dest': 'cli_action',
                'subcommands': ro_actions
            }
        }

        if arg_debug:
            self.logger.debug(
                json.dumps(arg_tree,
                           sort_keys=True,
                           indent=2,
                           separators=(',', ':')))

        def _traverse_arg_tree(tree,
                               parser,
                               parents=None,
                               dest="",
                               help="",
                               path=None):
            """Recursive function for walking the arg_tree and building a
            corresponding tree of ArgumentParsers"""

            if len(tree) == 0:
                return

            sub_parsers = None
            for command_name, command_dict in sorted(tree.items(),
                                                     key=lambda x: x[0]):
                _path = copy.deepcopy(path)
                _path.append(command_name)
                if arg_debug:
                    self.logger.debug(_path)
                if 'subcommands' in command_dict:

                    if sub_parsers is None:
                        sub_parsers = parser.add_subparsers(dest=dest,
                                                            help=help)
                    command_parser = sub_parsers.add_parser(
                        command_name,
                        help=command_dict['help']
                        if 'help' in command_dict else "",
                        parents=parents)

                    _traverse_arg_tree(tree=command_dict['subcommands'],
                                       parser=command_parser,
                                       parents=parents,
                                       dest=command_dict['dest'],
                                       help="Commands relating to %s" %
                                       command_name,
                                       path=_path)

                elif 'args' in command_dict:
                    if sub_parsers is None:
                        sub_parsers = parser.add_subparsers(dest=dest,
                                                            help=help)
                    command_parser = sub_parsers.add_parser(
                        command_name,
                        help=command_dict['help'].format(
                            *_path) if 'help' in command_dict else '',
                        parents=parents)

                    # parents and dest are not needed as there will be no
                    # more sub levels - the next recusive call will be
                    # adding args, which are the leaves of this tree.
                    _traverse_arg_tree(tree={'args': command_dict['args']},
                                       parser=command_parser,
                                       help="Commands relating to %s" %
                                       (command_name),
                                       path=_path)

                elif command_name == 'args':
                    for arg_name, arg_dict in command_dict.items():
                        if arg_debug:
                            self.logger.debug('%s, %s' %
                                              (arg_name, str(arg_dict)))
                        if 'order' not in arg_dict:
                            arg_dict['order'] = 0

                    for arg_name, arg_dict in sorted(
                            command_dict.items(), key=lambda x: x[1]['order']):
                        if 'help' in arg_dict:
                            arg_dict['help'] = arg_dict['help'].format(*_path)
                        del arg_dict['order']
                        parser.add_argument(arg_name, **arg_dict)

        # The global_options parser will be added to all other parsers as a
        # parent. This ensures that these options are available at every
        # level of command.
        global_options = argparse.ArgumentParser(add_help=False)
        global_options.add_argument(
            "--debug",
            help="Print debug information such as API requests",
            action='store_true')

        # Precedence for endpoint URL:
        #      command line option > environment variable > default
        global_options.add_argument(
            '--endpoint',
            default=os.environ['OPENCENTER_ENDPOINT'] if 'OPENCENTER_ENDPOINT'
            in os.environ else "http://*****:*****@host:8443")

        #Root parser - all other commands will be added as sub parsers.
        parser = argparse.ArgumentParser(description='OpenCenter CLI',
                                         prog='opencentercli',
                                         parents=[global_options])

        #kick off arg_tree traversal
        _traverse_arg_tree(tree=arg_tree,
                           parser=parser,
                           parents=[global_options],
                           dest="cli_noun",
                           help="subcommands",
                           path=[])

        #parse args and return a namespace object
        return parser.parse_args(argv)

    def get_field_schema(self, command):
        obj = getattr(self.endpoint, command)
        schema = self.endpoint.get_schema(singularize(command))
        fields = schema.field_schema
        return fields

    def do_show(self, args, obj):
        """Print a whole object, or a specific property following a dotted
        path.

        When a dotted path is specified (eg:
        attrs.opencenter_agent_actions.upgrade_agent.timeout),
        lookup is done in three ways:
            1) Object Attribute: getattr
            2) Dictionary key:  []
            3) List Key: convert to int, then []

        """
        id = args.id
        act = getattr(self.endpoint, obj)
        if args.property is None:
            #No property specified, print whole item.
            print act[id]
        else:
            item = act[id]
            for path_section in args.property.split('.'):

                # Lookup by object attribute
                if hasattr(item, path_section):
                    item = getattr(item, path_section)
                    continue
                else:
                    try:
                        # Lookup by dictionary key
                        item = item[path_section]
                        continue
                    except:
                        try:
                            # Lookup by list index
                            item = item[int(path_section)]
                            continue
                        except:
                            pass

                # None of the lookup methods succeeded, so property path must
                # be invalid.
                raise ValueError(
                    'Cannot resolve "%s" from property string "%s" for'
                    ' %s %s' % (path_section, args.property, singularize(obj),
                                act[id].name))

            # Assume the property is JSON and try to pretty-print. If that
            # fails, print the item normally
            try:
                print json.dumps(item,
                                 sort_keys=True,
                                 indent=2,
                                 separators=(',', ':'))
            except:
                print item

    def do_logs(self, args):
        id = args.task_id
        task = self.endpoint.tasks[id]
        print "=== Logs for task %s: %s > %s ===" % (id, task.node.name,
                                                     task.action)
        print task._logtail(offset=args.offset)
        print "=== End of Logs ==="

    def do_filter(self, args, obj):
        act = getattr(self.endpoint, obj)
        print act.filter(args.filter_string)

    def do_create(self, args, obj):
        field_schema = self.get_field_schema(obj)
        arguments = []
        for field in field_schema:
            arguments.append(field)

        ver = dict([(k, v) for k, v in args._get_kwargs()
                    if k in arguments and v is not None])
        act = getattr(self.endpoint, obj)
        new_node = act.create(**ver)
        new_node.save()
        return new_node

    def do_delete(self, args, obj):
        try:
            id = args.id
            act = getattr(self.endpoint, obj)
            act[id].delete()
            print "%s %s has been deleted." % tuple([obj, id])
        except Exception, e:
            print "%s" % e
Exemplo n.º 7
0
class OpenCenterShell():
    def __init__(self):

        #setup root logger
        self.logger = logging.getLogger('opencenter')

        if "OPENCENTER_CLIENT_DEBUG" in os.environ:
            self.logger.setLevel(logging.DEBUG)

        if not self.logger.handlers:
            self.logger.addHandler(logging.StreamHandler(sys.stderr))

        #Warn if using default endpoint.
        default_endpoint = 'http://localhost:8080'
        if 'OPENCENTER_ENDPOINT' in os.environ:
            endpoint_url = os.environ['OPENCENTER_ENDPOINT']
        else:
            self.logger.warn("OPENCENTER_ENDPOINT not found in environment"
                             ", using %s" % default_endpoint)
            endpoint_url = default_endpoint

        self.endpoint = OpenCenterEndpoint(endpoint=endpoint_url)

    def get_base_parser(self):
        parser = argparse.ArgumentParser(description='OpenCenter CLI',
                                         prog='opencentercli',
                                         )
        parser.add_argument('-v', '--verbose',
                            action='store_true',
                            help='Print more verbose output')

        #chicken-egg issues. Parser requires schema, which reuquires endpoint..
        #parser.add_argument('--endpoint',
        #                    help="OpenCenter endpoint URL.",metavar="URL")

        return parser

    def get_subcommand_parser(self):
        parser = self.get_base_parser()
        self.subcommands = {}
        type_parsers = parser.add_subparsers(help='subcommands',
                                             dest='cli_noun')
        self._construct_parse_tree(type_parsers)
        return parser

    def _construct_parse_tree(self, type_parsers):
        """
        obj_type = object type eg Task, Adventure
        action = command eg create, delete
        argument = required, or optional argument.
        """
        obj_types = self.endpoint._object_lists.keys()

        #information about each action
        actions = {
            'list': {'description': 'list all %ss',
                     'args': [],
                     },
            'show': {'description': 'show the properties of a %s',
                     'args': ['id']
                     },
            'delete': {'description': 'remove a %s',
                       'args': ['id']
                       },
            'create': {'description': 'create a %s',
                       'args': ['schema']
                       },
            # 'filter',
            'update': {'description': 'modify a %s',
                       'args': ['schema']
                       },
            'execute': {'description': 'execute a %s',
                        'args': ['node_id', 'adventure_id'],
                        'applies_to': ['adventure']
                        }
        }

        for obj_type in obj_types:
            schema = self.endpoint.get_schema(singularize(obj_type))
            arguments = schema.field_schema
            callback = getattr(self.endpoint, obj_type)
            desc = callback.__doc__ or ''

            type_parser = type_parsers.add_parser(singularize(obj_type),
                                                  help='%s actions' %
                                                  singularize(obj_type),
                                                  description=desc,
                                                  )

            #"action" clashses with the action attribute of some object types
            #for example task.action, so the action arg is stored as cli_action
            action_parsers = type_parser.add_subparsers(dest='cli_action')
            for action in actions:

                #skip this action if it doesn't apply to this obj_type.
                if 'applies_to' in actions[action]:
                    if singularize(obj_type) not in \
                            actions[action]['applies_to']:
                        continue

                action_parser = action_parsers.add_parser(
                    action,
                    help=actions[action]['description'] % singularize(obj_type)
                )

                action_args = actions[action]['args']
                if action_args == ['schema']:
                    for arg_name, arg in arguments.items():

                        #id should be allocated rather than specified
                        if action == "create" and arg_name == 'id':
                            continue
                        opt_string = '--'
                        if arg['required']:
                            opt_string = ''
                        action_parser.add_argument('%s%s' %
                                                   (opt_string, arg_name))
                else:
                    for arg in action_args:
                        action_parser.add_argument(arg)
            self.subcommands[obj_type] = type_parser
            type_parser.set_defaults(func=callback)

    def get_field_schema(self, command):
        obj = getattr(self.endpoint, command)
        schema = self.endpoint.get_schema(singularize(command))
        fields = schema.field_schema
        return fields

    def do_show(self, args, obj):
        id = args.id
        act = getattr(self.endpoint, obj)
        print act[id]

    def do_create(self, args, obj):
        field_schema = self.get_field_schema(obj)
        arguments = []
        for field in field_schema:
            arguments.append(field)

        ver = dict([(k, v) for k, v in args._get_kwargs()
                   if k in arguments and v is not None])
        act = getattr(self.endpoint, obj)
        new_node = act.create(**ver)
        new_node.save()

    def do_delete(self, args, obj):
        try:
            id = args.id
            act = getattr(self.endpoint, obj)
            act[id].delete()
            print "%s %s has been deleted." % tuple([obj, id])
        except Exception, e:
            print "%s" % e