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 set_endpoint(self, endpoint_url): self.endpoint = OpenCenterEndpoint(endpoint=endpoint_url, interactive=True)
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
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
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
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