1from collections import namedtuple 2import json 3import logging 4import pprint 5import re 6import six 7 8(STATUS_OK, STATUS_ERROR) = range(2) 9 10CommandsResponse = namedtuple('CommandsResponse', ['status', 'value']) 11 12LOG = logging.getLogger('bgpspeaker.operator.command') 13 14 15def default_help_formatter(quick_helps): 16 """Apply default formatting for help messages 17 18 :param quick_helps: list of tuples containing help info 19 """ 20 ret = '' 21 for line in quick_helps: 22 cmd_path, param_hlp, cmd_hlp = line 23 ret += ' '.join(cmd_path) + ' ' 24 if param_hlp: 25 ret += param_hlp + ' ' 26 ret += '- ' + cmd_hlp + '\n' 27 return ret 28 29 30class Command(object): 31 """Command class is used as a node in tree of commands. 32 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`. 41 """ 42 43 help_msg = '' 44 param_help_msg = '' 45 command = '' 46 cli_resp_line_template = '{0}: {1}\n' 47 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 57 sub-command as well. 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. 63 """ 64 65 self.resp_formatter_name = resp_formatter_name 66 67 if hasattr(self, resp_formatter_name + '_resp_formatter'): 68 self.resp_formatter = \ 69 getattr(self, resp_formatter_name + '_resp_formatter') 70 else: 71 self.resp_formatter = self.cli_resp_formatter 72 73 self.api = api 74 self.parent_cmd = parent 75 self.help_formatter = help_formatter 76 if not hasattr(self, 'subcommands'): 77 self.subcommands = {} 78 79 def __call__(self, params): 80 """You run command by calling it. 81 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. 90 """ 91 if len(params) == 0: 92 return self._action_wrapper([]) 93 94 first_param = params[0] 95 96 if first_param == '?': 97 return self.question_mark() 98 99 if first_param in self.subcommands: 100 return self._instantiate_subcommand(first_param)(params[1:]) 101 102 return self._action_wrapper(params) 103 104 @classmethod 105 def cli_resp_formatter(cls, resp): 106 """Override this method to provide custom formatting of cli response. 107 """ 108 if not resp.value: 109 return '' 110 111 if resp.status == STATUS_OK: 112 113 if type(resp.value) in (str, bool, int, float, six.text_type): 114 return str(resp.value) 115 116 ret = '' 117 val = resp.value 118 if not isinstance(val, list): 119 val = [val] 120 for line in val: 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) 125 ) 126 else: 127 ret += cls.cli_resp_line_template.format(k, v) 128 return ret 129 else: 130 return "Error: {0}".format(resp.value) 131 132 @classmethod 133 def json_resp_formatter(cls, resp): 134 """Override this method to provide custom formatting of json response. 135 """ 136 return json.dumps(resp.value) 137 138 @classmethod 139 def dict_resp_formatter(cls, resp): 140 return resp.value 141 142 def _action_wrapper(self, params): 143 filter_params = [] 144 if '|' in params: 145 ind = params.index('|') 146 new_params = params[:ind] 147 filter_params = params[ind:] 148 params = new_params 149 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( 155 action_resp.status, 156 self.resp_formatter(action_resp) 157 ) 158 return action_resp, self.__class__ 159 160 def action(self, params): 161 """Override this method to define what command should do. 162 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 167 """ 168 return CommandsResponse(STATUS_ERROR, 'Not implemented') 169 170 def filter_resp(self, action_resp, filter_params): 171 """Filter response of action. Used to make printed results more 172 specific 173 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. 178 """ 179 if action_resp.status == STATUS_OK: 180 try: 181 return CommandsResponse( 182 STATUS_OK, 183 TextFilter.filter(action_resp.value, filter_params) 184 ) 185 except FilterError as e: 186 return CommandsResponse(STATUS_ERROR, str(e)) 187 else: 188 return action_resp 189 190 def question_mark(self): 191 """Shows help for this command and it's sub-commands. 192 """ 193 ret = [] 194 if self.param_help_msg or len(self.subcommands) == 0: 195 ret.append(self._quick_help()) 196 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)) 203 204 return ( 205 CommandsResponse(STATUS_OK, self.help_formatter(ret)), 206 self.__class__ 207 ) 208 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 212 completions. 213 """ 214 if nested: 215 return self.command_path(), None, self.help_msg 216 else: 217 return self.command_path(), self.param_help_msg, self.help_msg 218 219 def command_path(self): 220 if self.parent_cmd: 221 return self.parent_cmd.command_path() + [self.command] 222 else: 223 return [self.command] 224 225 def _instantiate_subcommand(self, key): 226 return self.subcommands[key]( 227 api=self.api, 228 parent=self, 229 help_formatter=self.help_formatter, 230 resp_formatter_name=self.resp_formatter_name 231 ) 232 233 234class TextFilter(object): 235 236 @classmethod 237 def filter(cls, action_resp_value, filter_params): 238 try: 239 action, expected_value = filter_params 240 except ValueError: 241 raise FilterError('Wrong number of filter parameters') 242 if action == 'regexp': 243 244 if isinstance(action_resp_value, list): 245 resp = list(action_resp_value) 246 iterator = enumerate(action_resp_value) 247 else: 248 resp = dict(action_resp_value) 249 iterator = iter(action_resp_value.items()) 250 251 remove = [] 252 253 for key, value in iterator: 254 if not re.search(expected_value, str(value)): 255 remove.append(key) 256 257 if isinstance(resp, list): 258 resp = [resp[key] for key, value in enumerate(resp) 259 if key not in remove] 260 else: 261 resp = dict([(key, value) 262 for key, value in resp.items() 263 if key not in remove]) 264 265 return resp 266 else: 267 raise FilterError('Unknown filter') 268 269 270class FilterError(Exception): 271 pass 272