1 from collections import namedtuple
8 (STATUS_OK, STATUS_ERROR) = range(2)
10 CommandsResponse = namedtuple('CommandsResponse', ['status', 'value'])
12 LOG = logging.getLogger('bgpspeaker.operator.command')
15 def default_help_formatter(quick_helps):
16 """Apply default formatting for help messages
18 :param quick_helps: list of tuples containing help info
21 for line in quick_helps:
22 cmd_path, param_hlp, cmd_hlp = line
23 ret += ' '.join(cmd_path) + ' '
25 ret += param_hlp + ' '
26 ret += '- ' + cmd_hlp + '\n'
30 class Command(object):
31 """Command class is used as a node in tree of commands.
33 Each command can do some action or have some sub-commands, just like in IOS
34 Command with it's sub-commands form tree.
35 Each command can have one or more parameters. Parameters have to be
36 distinguishable from sub-commands.
37 One can inject dependency into command Cmd(api=my_object).
38 This dependency will be injected to every sub-command. And can be used
39 to interact with model/data etc.
40 Example of path in command tree `show count all`.
46 cli_resp_line_template = '{0}: {1}\n'
48 def __init__(self, api=None, parent=None,
49 help_formatter=default_help_formatter,
50 resp_formatter_name='cli'):
51 """:param api: object which is saved as self.api
52 and re-injected to every sub-command. You can use it to
53 manipulate your model from inside Commands'
54 :param parent: parent command instance.
55 :param help_formatter: function used to format
56 output of '?'command. Is re-injected to every
58 :param resp_formatter_name: used to select function to format
59 output of _action. cli_resp_formatter and json_resp_formatter
60 are defined by default, but you can define your own formatters.
61 If you use custom formatter(not cli nor json) remember to
62 implement it for every sub-command.
65 self.resp_formatter_name = resp_formatter_name
67 if hasattr(self, resp_formatter_name + '_resp_formatter'):
68 self.resp_formatter = \
69 getattr(self, resp_formatter_name + '_resp_formatter')
71 self.resp_formatter = self.cli_resp_formatter
74 self.parent_cmd = parent
75 self.help_formatter = help_formatter
76 if not hasattr(self, 'subcommands'):
79 def __call__(self, params):
80 """You run command by calling it.
82 :param params: As params you give list of subcommand names
83 and params to final subcommand. Kind of like in
84 cisco ios cli, ie. show int eth1 / 1, where show is command,
85 int subcommand and eth1 / 1 is param for subcommand.
86 :return: returns tuple of CommandsResponse and class of
87 sub - command on which _action was called. (last sub - command)
88 CommandsResponse.status is action status,
89 and CommandsResponse.value is formatted response.
92 return self._action_wrapper([])
94 first_param = params[0]
96 if first_param == '?':
97 return self.question_mark()
99 if first_param in self.subcommands:
100 return self._instantiate_subcommand(first_param)(params[1:])
102 return self._action_wrapper(params)
105 def cli_resp_formatter(cls, resp):
106 """Override this method to provide custom formatting of cli response.
111 if resp.status == STATUS_OK:
113 if type(resp.value) in (str, bool, int, float, six.text_type):
114 return str(resp.value)
118 if not isinstance(val, list):
121 for k, v in line.items():
122 if isinstance(v, dict):
123 ret += cls.cli_resp_line_template.format(
124 k, '\n' + pprint.pformat(v)
127 ret += cls.cli_resp_line_template.format(k, v)
130 return "Error: {0}".format(resp.value)
133 def json_resp_formatter(cls, resp):
134 """Override this method to provide custom formatting of json response.
136 return json.dumps(resp.value)
139 def dict_resp_formatter(cls, resp):
142 def _action_wrapper(self, params):
145 ind = params.index('|')
146 new_params = params[:ind]
147 filter_params = params[ind:]
150 action_resp = self.action(params)
151 if len(filter_params) > 1:
152 # we don't pass '|' around so filter_params[1:]
153 action_resp = self.filter_resp(action_resp, filter_params[1:])
154 action_resp = CommandsResponse(
156 self.resp_formatter(action_resp)
158 return action_resp, self.__class__
160 def action(self, params):
161 """Override this method to define what command should do.
163 :param params: list of text parameters applied to this command.
164 :return: returns CommandsResponse instance.
165 CommandsResponse.status can be STATUS_OK or STATUS_ERROR
166 CommandsResponse.value should be dict or str
168 return CommandsResponse(STATUS_ERROR, 'Not implemented')
170 def filter_resp(self, action_resp, filter_params):
171 """Filter response of action. Used to make printed results more
174 :param action_resp: named tuple (CommandsResponse)
175 containing response from action.
176 :param filter_params: params used after '|' specific for given filter
177 :return: filtered response.
179 if action_resp.status == STATUS_OK:
181 return CommandsResponse(
183 TextFilter.filter(action_resp.value, filter_params)
185 except FilterError as e:
186 return CommandsResponse(STATUS_ERROR, str(e))
190 def question_mark(self):
191 """Shows help for this command and it's sub-commands.
194 if self.param_help_msg or len(self.subcommands) == 0:
195 ret.append(self._quick_help())
197 if len(self.subcommands) > 0:
198 for k, _ in sorted(self.subcommands.items()):
199 command_path, param_help, cmd_help = \
200 self._instantiate_subcommand(k)._quick_help(nested=True)
201 if command_path or param_help or cmd_help:
202 ret.append((command_path, param_help, cmd_help))
205 CommandsResponse(STATUS_OK, self.help_formatter(ret)),
209 def _quick_help(self, nested=False):
210 """:param nested: True if help is requested directly for this command
211 and False when help is requested for a list of possible
215 return self.command_path(), None, self.help_msg
217 return self.command_path(), self.param_help_msg, self.help_msg
219 def command_path(self):
221 return self.parent_cmd.command_path() + [self.command]
223 return [self.command]
225 def _instantiate_subcommand(self, key):
226 return self.subcommands[key](
229 help_formatter=self.help_formatter,
230 resp_formatter_name=self.resp_formatter_name
234 class TextFilter(object):
237 def filter(cls, action_resp_value, filter_params):
239 action, expected_value = filter_params
241 raise FilterError('Wrong number of filter parameters')
242 if action == 'regexp':
244 if isinstance(action_resp_value, list):
245 resp = list(action_resp_value)
246 iterator = enumerate(action_resp_value)
248 resp = dict(action_resp_value)
249 iterator = iter(action_resp_value.items())
253 for key, value in iterator:
254 if not re.search(expected_value, str(value)):
257 if isinstance(resp, list):
258 resp = [resp[key] for key, value in enumerate(resp)
259 if key not in remove]
261 resp = dict([(key, value)
262 for key, value in resp.items()
263 if key not in remove])
267 raise FilterError('Unknown filter')
270 class FilterError(Exception):