blob: b710e0b142b7d0b300b6e6a51545f3556dbc9307 [file] [log] [blame]
Srikanth Vavilapalli1725e492014-12-01 17:50:52 -08001#
2# Copyright (c) 2011,2012,2013 Big Switch Networks, Inc.
3#
4# Licensed under the Eclipse Public License, Version 1.0 (the
5# "License"); you may not use this file except in compliance with the
6# License. You may obtain a copy of the License at
7#
8# http://www.eclipse.org/legal/epl-v10.html
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
13# implied. See the License for the specific language governing
14# permissions and limitations under the License.
15#
16
17import re
18import numbers
19import collections
20import traceback
21import types
22import json
23import time
24import sys
25import datetime
26import os
27import c_data_handlers
28import c_validations
29import c_completions
30import c_actions
31import utif
32import error
33import doc
34
35
36# TODO list
37#
38# - support for 'min' and 'max' as boundary values in range/length restrictions
39# - distinguish between completion help string and syntax/doc string
40# - allow multiple validation functions for typedefs/args
41# - verify that it handles unambiguous prefixes in subcommands and argument tags
42# - support for getting next available sequence number (e.g. ip name-server)
43# - test code to handle REST errors
44# - figure out how to deal with arguments that are optional for 'no' commands
45# - tests for length/range validations, both for strings and integers
46# - add validation patterns for domain-name and ip-address-or-domain-name typedefs
47# - get syntax help for "no" commands
48# - test using unambiguous prefix for command/subcommand names
49# - test using unambiguous prefix for argument tags
50# - test case insensitivity for command/subcommand/tag names (i.e. any fixed token)
51# - Allow/ignore missing command arguments for "no" commands (e.g. "no ip address")
52# - support for case-sensitive enum types (do we need this?)
53# - clean up 'description' vs. 'descriptor' terminology (i.e. pick one)
54# - return "<cr>" as completion if there are no args to complete
55# - be consistent about using _xyz function naming convention for private functions
56# - better handling of exceptions in do_command_completions. catch CommandErrors,
57# distinguish between expected error (e.g. ValidationError) and errors that
58# indicate bug (e.g. base Exception raised)
59# - sort issues with sequence numbers. They are treated as strings by the Django
60# Cassandra backend instead of integers. This means that, for example, the sequence
61# number 1,2,10 are incorrectly sorted in the order 1, 10, 2. Need to fix this in
62# the Django backend.
63# - Shouldn't allow "no" variant of a command if there's no default value for an argument.
64# For example, shouldn't allow "no clock set 01:30:00".
65# - treat scope stack as a stack with append & pop operations and search in reverse order
66# - rename the "data" item in the scopes to be "args" (or maybe not?)
67# - clean up syntax help message; includes "Syntax: " twice in message.
68# - clean up syntax help messages to not include extraneous white space characters
69# For example "test [foo <foo-value>]" instead of "test [ foo <foo-value> ]"
70# Or maybe not?
71# - support for explicit command abbreviations (e.g. "s" = "show" even if there are
72# other available commands that begin with "s".
73# - automated generation of running config
74# - rename 'config' command-type to 'config-fields'
75# - need to fix getting completions with alternation if any of the alternates don't
76# match the partial completion text (e.g. "firewall allow op<tab>").
77# - allow command description that don't have any args to omit the 'args' setting
78# (workaround for now is to just have an empty args tuple/list
79
80# - support "repeatable" attribute for args to support repetition
81# - Support for single token arguments with a custom action and/or arg handler
82# - allow multiple "name"s for a command description and allow each name to optionally
83# set a field in the arg data. Syntax would probably be that the "name" field could
84# be a list of names. So the name could either be a simple string, a dict (which
85# would support pattern-based names and setting a field with the command word, or a
86# list of names (where each name could be either the simple string or the dict)
87# - maybe get rid of specifying mode in the command description. Instead a command
88# description would be enabled for a mode by adding it to the list of commands that
89# are enabled for that mode. This would happen in some data (or code) that's separate
90# from the command description. I think this would be better so that we can prune
91# the number of command description we need to parse for a given command. Otherwise
92# we always need to check against all command descriptions.
93# - distinguish between exact match vs. prefix match in a choices block. If there's
94# only a single exact match then that should get precedence over other matches
95# and not be considered an ambiguous command
96# - add 'default' attribute for the default value of an optional field if it's not
97# specified. Should this be treated as mutually exclusive with 'optional'?
98# - convert reset-fields action to use fields instead of arg_data. If we're using
99# new-style hack then we probably need to have it be new-reset-fields or something
100# like that.
101# - clean up usage of return values for the command handling methods, i.e.
102# handle_one_command and all of the other handle_* methods. Should maybe
103# just not return anything for any of those and just capture the
104# matches/completion in the data member.
105# - fix behavior for printing <cr> as a completion at the end of a
106# complete command. Right now there's a bug where it will still print out
107# the syntax help.
108# - support string token argument as just a simple string in the argument list
109# - custom completion proc for cidr address. Currently if a command description
110# supports either cidr or ip+netmask it will handle either for command execution
111# but completion will only handle the ip+netmask variant. With a custom
112# completion proc you could combine the ip+netmask from the db and format as
113# a cidr address and return that as a completion
114
115
116
117command_type_defaults = {}
118
119
120# We rely on the higher level CLI object for certain operations, so we're initialized
121# with a reference that's used to call back to it. This is initialized in init_command.
122sdnsh = None
123
124# command_registry is the list of command descriptions that have been registered
125# via add_command. The reason this is a list (i.e. instead of a dictionary that's
126# keyed by the command name) is that's it's possible that you could have different
127# command descriptions with the same name but enabled for different modes.
128command_registry = []
129
130# command_submode_tree is a dictionary, where each submode, and its children
131# are saved.
132#
133command_submode_tree = { 'login' : { 'enable' : { 'config' : {} } } }
134
135# validation_registry is the dictionary of validation functions that have been
136# registered via add_validation. The key is the name of the function as specified
137# in typedef and command description. The value is a 2 item tuple where the first
138# item is the validation function to invoke and the second item is a dictionary
139# describing the arguments that are used when invoking the validation function.
140# See the comments for _call_proc for more information about the format of the
141# data describing the args.
142# invoke.
143validation_registry = {}
144
145# typedef_registry is the dictionary of typedef descriptions. The key is the
146# name of the typedef and the value is the typedef description.
147# value if the function to invoke
148typedef_registry = {}
149
150# action_registry is the dictionary of action functions that have been registered
151# via add_action. The key is the name of the function as specified in
152# command descriptions. The value is a 2 item tuple where the first
153# item is the action function to invoke and the second item is a dictionary
154# describing the arguments that are used when invoking the validation function.
155# See the comments for _call_proc for more information about the format of the
156# data describing the args.
157action_registry = {}
158
159# completion_registry is the dictionary of completion functions that have been
160# registered via add_completion. The key is the name of the function as specified
161# in command descriptions. The value is a 2 item tuple where the first
162# item is the completion function to invoke and the second item is a dictionary
163# describing the arguments that are used when invoking the validation function.
164# See the comments for _call_proc for more information about the format of the
165# data describing the args.
166completion_registry = {}
167
168
169# argument_data_handler_registry is the dictionary of custom argument data
170# handlers that have been registered via add_argument_data_handler. These are
171# used if custom processing is required to convert the argument value to the
172# data that is added to the argument data dictionary. The key is the name of
173# the function as specified in command descriptions. The value is a 2 item
174# tuple where the first item is the handler function to invoke and the second
175# item is a dictionary describing the arguments that are used when invoking
176# the validation function. See the comments for _call_proc for more information
177# about the format of the data describing the args.
178argument_data_handler_registry = {}
179
180#
181# keep a list of all the command module names
182command_added_modules = {}
183
184#
185command_syntax_version = {}
186
187def add_command(command):
188 command_registry.append(command)
189
190
191def model_obj_type_disable_submode(obj_type):
192 mi.obj_type_source_set_debug_only(obj_type)
193
194
195def model_obj_type_enable_cascade_delete(obj_type):
196 mi.cascade_delete_set_enable(obj_type)
197
198def model_obj_type_weak_with_cascade_delete(obj_type):
199 mi.cascade_delete_enable_force(obj_type)
200
201def model_obj_type_set_title(obj_type, title):
202 mi.obj_type_set_title(obj_type, title)
203
204
205def model_obj_type_disable_edit(obj_type, field):
206 mi.obj_type_disable_edit(obj_type, field)
207
208
209def model_obj_type_set_case(obj_type, field, case):
210 mi.set_obj_type_field_case_sensitive(obj_type, field, case)
211
212def model_obj_type_set_show_this(obj_type, show_this_list):
213 """
214 When an obj-type gets displayed via 'show this', complex
215 objects may require several different items to be displayed.
216 The second parameter is a list, where each item has several
217 fields: the first is the name of the object to display, and
218 the second is the format to use for that display
219 """
220 mi.obj_type_set_show_this(obj_type, show_this_list)
221
222
223def command_dict_minus_lec():
224 # lec -- 'login', 'enable', 'config'.
225 return [x for x in sdnsh.command_dict.keys()
226 if x not in ['login', 'enable', 'config', 'config-']]
227
228
229def command_dict_submode_index(submode):
230 if submode in ['login', 'enable', 'config', 'config-']:
231 return submode
232 if submode.startswith('config-'):
233 return submode
234 if sdnsh.description:
235 print _line(), "unknown submode ", submode
236
237
238def add_command_mode_recurse(mode_dict, mode, submode):
239 if mode in mode_dict:
240 if not submode in mode_dict[mode]:
241 mode_dict[mode][submode] = {}
242 else:
243 for d in mode_dict.keys():
244 add_command_mode_recurse(mode_dict[d], mode, submode)
245
246
247def add_command_submode(mode, submode):
248 add_command_mode_recurse(command_submode_tree, mode, submode)
249
250def show_command_submode_tree_recurse(mode_dict, level):
251 if len(mode_dict):
252 for d in mode_dict.keys():
253 index = command_dict_submode_index(d)
254 items = [x if _is_string(x) else x.pattern for x in sdnsh.command_dict.get(index, '')]
255 print " " * level, d, ': ' + ', '.join(items)
256 show_command_submode_tree_recurse(mode_dict[d], level + 2)
257
258
259def show_command_submode_tree():
260 show_command_submode_tree_recurse(command_submode_tree, 0)
261
262
263def command_dict_append(command, mode, name):
264 if not mode in sdnsh.command_dict:
265 sdnsh.command_dict[mode] = []
266 if type(name) == dict:
267 # this is a regular expression name, add the re.
268 if not 'pattern' in name:
269 print '%s: missing pattern' % command['self']
270 name['re'] = re.compile(name['pattern'])
271 sdnsh.command_dict[mode].append(name)
272
273
274def command_nested_dict_append(command, mode, name):
275 if not mode in sdnsh.command_nested_dict:
276 sdnsh.command_nested_dict[mode] = []
277 if type(name) == dict:
278 # this is a regular expression name, add the re.
279 if not 'pattern' in name:
280 print '%s: missing pattern' % command['self']
281 name['re'] = re.compile(name['pattern'])
282 if not name in sdnsh.command_nested_dict[mode]:
283 sdnsh.command_nested_dict[mode].append(name)
284
285
286def add_command_to_command_dict(command, mode, name, var):
287 #
288 if mode in ['login', 'enable']:
289 command_nested_dict_append(command, mode, name)
290 elif mode == 'config' or mode == 'config-':
291 command_dict_append(command, mode, name)
292 elif mode.endswith('*'):
293 index = mode[:-1]
294 command_nested_dict_append(command, index, name)
295 elif mode.startswith("config"):
296 command_dict_append(command, mode, name)
297
298
299
300def add_command_using_dict(command_description, value):
301 """
302 """
303 if not 'self' in value:
304 value['self'] = command_description
305
306 if not 'name' in value:
307 print '%s: missing \'name\': in description' % command_description
308 return
309 if not 'mode' in value:
310 print '%s: missing \'mode\': in description' % command_description
311 return
312
313 name = value['name']
314 mode = value['mode']
315 command_type = value.get('command-type')
316 if command_type == 'config-submode':
317 submode = value.get('submode-name')
318 # add_command_submode(mode,submode)
319
320 # XXX this needs improving
321 feature = value.get('feature')
322 if feature and name not in ['show']:
323 if not name in sdnsh.command_name_feature:
324 sdnsh.command_name_feature[name] = []
325 if not feature in sdnsh.command_name_feature[name]:
326 sdnsh.command_name_feature[name].append(feature)
327 #
328 # populate sdnsh.controller_dict based on the modes described for this command
329 #
330 if 'mode' in value:
331 if _is_list(mode):
332 for m in mode:
333 add_command_to_command_dict(value, m, name, command_description)
334 else:
335 add_command_to_command_dict(value, mode, name, command_description)
336 add_command(value)
337
338
339def add_commands_from_class(aclass):
340 """
341 """
342 for (var, value) in vars(aclass).items():
343 if re.match(r'.*COMMAND_DESCRIPTION$', var):
344 add_command_using_dict(var, value)
345
346
347def add_command_module_name(version, module):
348 """
349 Save some state about the version/module
350 """
351 if version not in command_syntax_version:
352 command_syntax_version[version] = [module]
353 else:
354 command_syntax_version[version].append(module)
355
356
357def add_commands_from_module(version, module, dump_syntax = False):
358 add_command_module_name(version, module)
359 for name in dir(module):
360 if re.match(r'.*COMMAND_DESCRIPTION$', name):
361 if module.__name__ not in command_added_modules:
362 command_added_modules[module.__name__] = [name]
363 if name not in command_added_modules[module.__name__]:
364 command_added_modules[module.__name__].append(name)
365 command = getattr(module, name)
366 add_command_using_dict(name, command)
367 if dump_syntax:
368 handler = CommandSyntaxer(name)
369 handler.add_known_command(command)
370 print handler.handle_command_results()
371
372
373def add_validation(name, validation_proc, default_args={'kwargs':
374 {'typedef': '$typedef', 'value': '$value'}}):
375 validation_registry[name] = (validation_proc, default_args)
376
377
378def add_typedef(typedef):
379 typedef_registry[typedef['name']] = typedef
380
381
382def add_action(name, action_proc, default_args={}):
383 action_registry[name] = (action_proc, default_args)
384
385def action_invoke(name, action_args):
386 # no validation to match args.
387 action = action_registry[name]
388 action[0](action_args)
389
390def add_completion(name, completion_proc, default_args= {'kwargs':
391 {'words': '$words', 'text': '$text', 'completions': '$completions'}}):
392 completion_registry[name] = (completion_proc, default_args)
393
394def add_argument_data_handler(name, handler_proc, default_args =
395 {'kwargs': {'name': '$name', 'value': '$value',
396 'data': '$data', 'scopes': '$scopes'}}):
397 argument_data_handler_registry[name] = (handler_proc, default_args)
398
399def add_command_type(name, command_defaults):
400 command_type_defaults[name] = command_defaults
401
402def _line():
403 # pylint: disable=W0212
404 f = sys._getframe().f_back
405 return '[%s:%d]' % (f.f_code.co_filename, f.f_lineno)
406
407#
408# VALIDATION PROCS
409#
410
411def _is_range_boundary(boundary):
412 """
413 Returns whether or not the given value is a valid boundary value.
414 Valid values are integers or the special strings 'min' or 'max'
415 (case-insensitive)
416 """
417 return (isinstance(boundary, numbers.Integral) or
418 (_is_string(boundary) and (boundary.lower() in ('min','max'))))
419
420
421def _convert_range_boundary(boundary, test_value):
422 """
423 Converts the given boundary value to an appropriate integral
424 boundary value. This is used to handle the special "min' and
425 'max' string values. The test_value argument should be the value
426 which is being tested for whether or not it's in a range. Since
427 long integral values have an unlimited precision there is no
428 actual MIN or MAX value, so we just make the value one less or
429 one more than the test_value, so that the range check in
430 validate_integer against the test_value will fail or succeed
431 appropriately. Note that this means that this function must be
432 called again for each different test_value (i.e. you can't
433 pre-process the ranges to remove the 'min' and 'max' values).
434 """
435 if _is_string(boundary):
436 if boundary.lower() == 'min':
437 boundary = test_value - 1
438 elif boundary.lower() == 'max':
439 boundary = test_value + 1
440 else:
441 raise error.CommandDescriptionError('Invalid range boundary constant; must be "min", "max" or integer value')
442
443 return boundary
444
445
446def _is_single_range(r):
447 """
448 Returns whether or not the argument is a valid single range.
449 A valid range is either a single integral value or else a
450 sequence (tuple or list) with 2 elements, each of which is a valid
451 range boundary value.
452 """
453 return (isinstance(r, numbers.Integral) or
454 (isinstance(r, collections.Sequence) and (len(r) == 2) and
455 _is_range_boundary(r[0]) and _is_range_boundary(r[1])))
456
457
458def _check_one_range(r):
459 """
460 Checks that the argument is a valid single range as determined
461 by _is_single_range.
462 If it is, then the function returns None.
463 It it's not, then a RangeSyntaxError exception is raised.
464 """
465 if not _is_single_range(r):
466 raise error.RangeSyntaxError(str(r))
467
468
469def _check_range(r):
470 """
471 Checks that the argument is a valid range.
472 If it is, then the function returns None.
473 It it's not, then a RangeSyntaxError exception is raised.
474 FIXME: This doesn't seem to be used anymore. Remove?
475 """
476 if _is_single_range(r):
477 _check_one_range(r)
478 elif isinstance(r, collections.Sequence):
479 for r2 in r:
480 _check_one_range(r2)
481 else:
482 raise error.RangeSyntaxError(str(r))
483
484
485def _lookup_typedef_value(typedef, name):
486 """
487 Look up the given name in a typedef.
488 If it's not found in the given typedef it recursively searches
489 for the value in the base typedefs.
490 Returns None if the value is not found.
491 FIXME: Note that this means there's currently no way to distinguish
492 between an explicit None value and the name not being specified.
493 """
494 assert typedef is not None
495 assert name is not None
496
497 # Check if the typedef has the attribute
498 value = typedef.get(name)
499 if value:
500 return value
501
502 # Otherwise, see if it's defined in the base type(s)
503 base_type_name = typedef.get('base-type')
504 if base_type_name:
505 base_typedef = typedef_registry.get(base_type_name)
506 if not base_typedef:
507 raise error.CommandDescriptionError('Unknown type name: %s' % base_type_name)
508 return _lookup_typedef_value(base_typedef, name)
509
510 return None
511
512
513def _raise_argument_validation_exception(typedef, value, detail, expected_tokens=None):
514 """
515 Called when an argument doesn't match the expected type. Raises an
516 ArgumentValidationError exception. The message for the exception
517 can be customized by specifying a error format string in the
518 type definition. The format string is called ' validation-error-format'.
519 The format string can use the following substitution variables:
520 - 'typedef': The name of the typedef
521 - 'value': The value that didn't match the typedef restrictions
522 - 'detail': Additional message for the nature of the type mismatch
523 The default format string is: 'Invalid %(typedef)s: %(value)s; %(detail)s'.
524 """
525 typedef_name = typedef.get('help-name')
526 if typedef_name is None:
527 typedef_name = typedef.get('name')
528 if typedef_name is None:
529 typedef_name = typedef.get('field')
530 if typedef_name is None:
531 typedef_name = '<unknown-type>'
532 if detail is None:
533 detail = ''
534 validation_error_format = typedef.get('validation-error-format',
535 'Invalid %(typedef)s: %(value)s; %(detail)s')
536 validation_error = (validation_error_format %
537 {'typedef': typedef_name, 'value': str(value), 'detail': detail})
538 raise error.ArgumentValidationError(validation_error, expected_tokens)
539
540
541#
542# COMMAND MODULE FUNCTIONS
543#
544
545def _get_args(item):
546 """
547 Gets the args item from the given item. The item may be a command
548 or subcommand description or a nested arg list inside a 'choice'
549 argument. If there's no item names "args", then it returns None.
550 If the args item is a single argument, then it's converted into
551 a tuple containing that single argument, so that the caller can
552 always treat the return value as an iterable sequence of args.
553 It doesn't do any additional validation of the args value
554 (e.g. it doesn't check that the individual arguments are dict objects).
555 """
556 args = item.get('args')
557 if args and not isinstance(args, collections.Sequence):
558 args = (args,)
559 return args
560
561
562def _get_choice_args(choice):
563 if isinstance(choice, collections.Sequence):
564 args = choice
565 else:
566 args = _get_args(choice)
567 if not args:
568 args = (choice,)
569 return args
570
571
572def _get_command_args_syntax_help_string(command, is_no_command, args):
573 """
574 Get the syntax help string for an argument list.
575 """
576 syntax_string = ''
577 if args:
578 for i, arg in enumerate(args):
579 if i > 0:
580 syntax_string += ' '
581
582 if _is_string(arg):
583 syntax_string += arg
584 continue
585
586 if type(arg) == tuple:
587 if sdnsh.description:
588 print _line(), command['self'], arg
589
590 if is_no_command:
591 optional = arg.get('optional-for-no', False)
592 else:
593 optional = arg.get('optional', False)
594 if optional:
595 syntax_string += '['
596
597 choices = arg.get('choices')
598 nested_args = arg.get('args')
599 if nested_args:
600 if choices:
601 raise error.CommandDescriptionError('An argument can\'t have both '
602 '"choices" and "args" attributes', command)
603 choices = (nested_args,)
604 if choices:
605 # Suppress choice delimiters if we've already emitted the square
606 # brackets to indicate an optional argument. This is so we get
607 # something simpler (e.g. "[this | that]" ) instead of getting
608 # doubled delimiters (e.g. "[{this | that}]").
609 if not optional:
610 syntax_string += '{'
611
612 for j, choice in enumerate(choices):
613 if j > 0:
614 syntax_string += ' | '
615 choice_args = _get_choice_args(choice)
616 choice_syntax_string = _get_command_args_syntax_help_string(command,
617 is_no_command,
618 choice_args)
619 syntax_string += choice_syntax_string
620
621 if not optional:
622 syntax_string += '}'
623 else:
624 field = arg.get('field')
625
626 tag = arg.get('tag')
627 if tag:
628 syntax_string += tag + ' '
629
630 token = arg.get('token')
631 if token:
632 syntax_string += token
633
634 if (field != None) and (arg.get('type') != 'boolean'):
635 help_name = arg.get('help-name')
636 if help_name:
637 help_name = '<' + help_name + '>'
638 else:
639 if arg.get('type') == 'enum':
640 values = arg.get('values')
641 if values:
642 if _is_string(values):
643 values = (values,)
644 help_name = ' | '.join(values)
645 if len(values) > 1:
646 help_name = '{' + help_name + '}'
647 if not help_name:
648 help_name = '<' + field + '>'
649 syntax_string += help_name
650 if optional:
651 syntax_string += ']'
652
653 return syntax_string
654
655
656def _get_command_syntax_help_string(command, command_prefix_string):
657 try:
658 parts = []
659 if command_prefix_string:
660 parts.append(command_prefix_string)
661 command_name = _get_command_title(command)
662 parts.append(command_name)
663 args = _get_args(command)
664 args_syntax_help_string=''
665 if args:
666 args_syntax_help_string = _get_command_args_syntax_help_string(command, False, args)
667 parts.append(args_syntax_help_string)
668
669 name = ''
670 if sdnsh.debug or sdnsh.description:
671 name = command['self'] + "\n "
672
673 if is_no_command_supported(command) and command_prefix_string != 'no':
674 no_syntax = _get_command_args_syntax_help_string(command, True, args)
675 if ' '.join(no_syntax) == ' '.join(args_syntax_help_string):
676 command_syntax_help_string = name + '[no] ' +' '.join(parts)
677 else:
678 return [
679 name + ' '.join(parts),
680 name + 'no ' + command_name + ' ' + no_syntax
681 ]
682 else:
683 command_syntax_help_string = name + ' '.join(parts)
684
685 except Exception, e:
686 if sdnsh.debug or sdnsh.debug_backtrace:
687 traceback.print_exc()
688 raise error.CommandDescriptionError(str(e))
689
690 return command_syntax_help_string
691
692
693def _quick_command_syntax(command):
694 title = _get_command_title(command)
695 args = _get_args(command)
696 args_syntax_help_string = _get_command_args_syntax_help_string(command, False, args)
697 return ' '.join((title, args_syntax_help_string))
698
699
700def reformat_line(string, width, indent, recurse):
701
702 result = []
703 split_chars = "0123456789"
704 depth = 0
705 align = 0
706 next_mark = False
707
708 if recurse > 8 or indent > width:
709 return [string]
710
711 string = ' ' * indent + string
712
713 for c in string:
714 if c in '{([<':
715 if align == 0:
716 align = len(result)
717 depth += 1
718 result.append(' ')
719 next_mark = False
720 elif c in '})]>':
721 next_mark = False
722 result.append(' ')
723 depth -= 1
724 if depth == 0:
725 next_mark = True
726 elif c == '|':
727 result.append(split_chars[depth])
728 next_mark = False
729 else:
730 if next_mark:
731 result.append(split_chars[depth])
732 else:
733 result.append(' ')
734 next_mark = False
735
736 if len(string) > width:
737 #split_depth = 0
738 r = ''.join(result)
739 splits = None
740 for s in range(0,9):
741 if split_chars[s] in r:
742 splits = ''.join(result).split(split_chars[s])
743 if len(splits[-1]) > 0:
744 break
745
746 if not splits:
747 return [string]
748
749 parts = []
750 first = len(splits[0])+1
751 f = ' ' * align
752 collect = string[0:len(splits[0])+1]
753 more = False
754 for s in splits[1:]:
755 if len(s) == 0:
756 continue
757 if len(collect) + len(s) > width:
758 parts.append(collect)
759 collect = f
760 more = False
761 # the next split can still be too wide
762 if len(collect) + len(s) > width:
763 reformat = reformat_line(string[first:first+len(s)+1], width, align, recurse+1)
764 first += len(s)+1
765 parts += reformat
766 collect = f
767 more = False
768 else:
769 collect += string[first:first+len(s)+1]
770 first += len(s)+1
771 more = True
772 if more:
773 parts.append(collect)
774 return parts
775
776 return [string]
777
778
779def isCommandFeatureActive(command, features):
780 if not features:
781 return True # this command is usable anywhere
782 if features:
783 if not _is_list(features):
784 features = (features,)
785 every_features_enabled = True
786 for feature in features:
787 if not sdnsh.feature_enabled(feature):
788 every_features_enabled = False
789 break
790 if not every_features_enabled:
791 return False
792 return True
793
794
795def is_no_command_supported(command):
796 """
797 Predicate to provide some indication of whether or not
798 the command allows the 'no' prefix
799 """
800 command_type = command.get('command-type')
801 if command_type:
802 if command_type in ['display-table','display-rest']:
803 return False
804 no_supported = command.get('no-supported', True)
805 if no_supported == False:
806 return False
807 return True
808
809
810class CommandHandler(object):
811
812 def __init__(self):
813 self.actions = None
814 self.parse_errors = set()
815 self.prefix = ''
816
817 def handle_command_error(self, e):
818 pass
819
820 def handle_exception(self, e):
821 pass
822
823 def handle_unspecified_command(self):
824 pass
825
826 # These are the values to use for the priority argument to
827 # handle_parse_error
828 UNEXPECTED_ADDITIONAL_ARGUMENTS_PRIORITY = 10
829 UNEXPECTED_END_OF_ARGUMENTS_PRIORITY = 20
830 UNEXPECTED_TOKEN_PRIORITY = 30
831 VALIDATION_ERROR_PRIORITY = 40
832
833 def handle_parse_error(self, error_message, word_index,
834 priority, expected_tokens=None):
835 # Need to convert a list to a tuple first to make it hashable so that we
836 # can add it to the parse_errors set.
837 if isinstance(expected_tokens, list):
838 expected_tokens = tuple(expected_tokens)
839 self.parse_errors.add((error_message, word_index, priority, expected_tokens))
840
841 def handle_incomplete_command(self, arg, arg_scopes, arg_data,
842 fields, parsed_tag, command):
843 pass
844
845 def handle_complete_command(self, prefix_matches, scopes, arg_data,
846 fields, actions, command):
847 pass
848
849 def handle_command_results(self):
850 pass
851
852 def handle_first_matched_result(self, command):
853 pass
854
855 def handle_matched_result(self, command, result, arg_scopes):
856 pass
857
858 def get_default_value(self, arg):
859 default = None
860 if self.is_no_command:
861 default = arg.get('default-for-no')
862 if default is None and 'field' in arg:
863 default = mi.field_current_obj_type_default_value(arg['field'])
864 if default != None:
865 default = str(default)
866 if default is None:
867 default = arg.get('default')
868 return default
869
870
871 def get_matching_commands(self, command_word, is_no_command, command_list):
872 """
873 Returns the list of matching command candidates from the command list.
874
875 To be a candidate a command description must satisfy three criteria:
876
877 1) If it specifies a feature (or a list of features), then all of
878 those features must be enabled.
879 2) Its 'mode' value (possibly a list of modes or a mode pattern)
880 must match the current mode.
881 3) It's name must begin with the command prefix
882 4) 'No' or not 'no', some commands allow 'no', other's dont
883
884 Each item in the candidate list is a 2 item tuple where the first item
885 is the command description and the second item is a boolean for whether
886 the command description was an exact match for the command word
887 (i.e. as opposed to a prefix match).
888 """
889 candidates = []
890 current_mode = sdnsh.current_mode()
891 command_word_lower = command_word.lower()
892
893 try:
894 for command in command_list:
895 # If this command is tied to a feature then check that the
896 # feature is enabled.
897 if isCommandFeatureActive(command, command.get('feature')) == False:
898 continue
899
900 # Check that the command is enabled for the current mode
901 modes = command.get('mode')
902 if not modes:
903 raise error.CommandDescriptionError(
904 'Command description must specify a mode', command)
905 if not _is_list(modes):
906 modes = (modes,)
907 if not _match_current_modes(command, current_mode, modes):
908 continue
909
910 # If a 'no' command was requested, verify this command
911 # support 'no' (can we tell from the type?)
912 if is_no_command:
913 if not is_no_command_supported(command):
914 continue
915
916 # Check that the name matches the command word
917 name = command['name']
918 if _is_string(name):
919 name = name.lower()
920 if name.startswith(command_word_lower):
921 prefix_match = len(command_word) < len(name)
922 candidates.append((command, prefix_match))
923 elif isinstance(name, collections.Mapping):
924 # FIXME: Should support dict-based names that aren't
925 # patterns. Will be useful when we support lists of names
926 # for a command description where the arg_data can be set with
927 # different fields based on which name was matched for the command
928 if 're' not in name:
929 command['name']['re'] = re.compile(name['pattern'])
930 if name['re'].match(command_word):
931 candidates.append((command, True))
932 # FIXME: Probably should get rid of the following pattern code
933 # and just use the above pattern compilation mechanism.
934 # The following won't work when we require the command
935 # descriptions to be pure data, e.g. derived from JSON data
936 # or something like that.
937 elif type(name) == dict and \
938 name['re'].match(command_word):
939 candidates.append((command, False))
940 else:
941 raise error.CommandDescriptionError('Command description name '
942 'must be either string, dict, or pattern', command)
943
944 except Exception, _e:
945 if sdnsh.debug or sdnsh.debug_backtrace:
946 traceback.print_exc()
947 raise error.CommandDescriptionError('Missing mode or name:', command)
948
949 return candidates
950
951 def check_command_type_and_actions(self, current_scope, scopes, actions):
952
953 # Push the scopes for the command defaults and the command itself
954 # onto the scope stack.
955 command_type = current_scope.get('command-type')
956 command_defaults = command_type_defaults.get(command_type, {})
957 if command_defaults:
958 # Replace the command defaults with the new ones
959 scopes = scopes[:-1] + [command_defaults]
960
961 # Get the actions for the command
962 action_name = 'no-action' if self.is_no_command else 'action'
963 current_scope_actions = current_scope.get(action_name)
964 if current_scope_actions or command_defaults:
965 actions = _lookup_in_scopes(action_name, scopes)
966 if actions is None:
967 actions = []
968 elif not _is_list(actions):
969 actions = [actions]
970
971 # Update the actions with the scopes that were in effect at the point
972 # we looked up that action. This is so we can re-create the
973 # appropriate scopes later when we execute the action.
974 actions = [(action, scopes) for action in actions]
975
976 return scopes, actions
977
978 def parse_arguments(self, args, words, start_word_index, scopes,
979 arg_data, fields, actions, prefix_matches, command):
980 """
981 Parse the given args from the given command words.
982 """
983
984 if len(args) == 0:
985 return [[0, [], scopes, arg_data, fields, actions]]
986
987 parse_results = []
988
989 arg = args[0]
990
991 if _is_string(arg):
992 arg = {'token': arg}
993
994 remaining_args = args[1:]
995
996 arg_scopes = [arg] + scopes
997 arg_parse_results = []
998
999 # Get the attributes we need from the arg
1000 # FIXME: Should possibly get rid of the 'data' mechanism
1001 # and handle it via a custom dta handler
1002 choices = arg.get('choices')
1003 nested_args = arg.get('args')
1004 if nested_args:
1005 # Convert the nested argument list into a choices argument with
1006 # a single choice, so that we can leverage the code below that
1007 # handles choices
1008 if choices:
1009 raise error.CommandDescriptionError('An argument can\'t have both '
1010 '"choices" and "args" attributes', command)
1011 choices = (nested_args,)
1012
1013 # Determine whether or not this argument is optional.
1014 # Default to making arguments optional for no commands, except for if
1015 # it's a choices argument. In that case it will probably be ambiguous
1016 # about which fields should be reset to the default values, so we just
1017 # don't try to handle that.
1018 optional_name = 'optional-for-no' if self.is_no_command else 'optional'
1019 #optional_default_value = self.is_no_command
1020 #optional_name = 'optional'
1021 optional_default_value = False
1022 #optional = arg.get(optional_name, optional_default_value)
1023 # FIXME: Disabling the special handling of optional arguments for no
1024 # command. That's causing spurious completions to be included. Not sure
1025 # how to fix that right now. Do we really need the special optional
1026 # handling anyway? Does Cisco actually support that.
1027 # For example, being able to use "no ip address" rather than
1028 # "no ip address 192.168.2.2 255.255.255.0". I haven't actually tried
1029 # both forms on a Cisco switch to see what it does.
1030 optional = arg.get(optional_name, optional_default_value)
1031
1032 # Check to see if this arg overrides either the command type or action
1033 # Note that we don't want to set the "actions" variable with the
1034 # updated actions yet until we know that the current argument
1035 # actually matched against the command words and wasn't an optional
1036 # argument that was skipped.
1037 arg_scopes, arg_actions = self.check_command_type_and_actions(
1038 arg, arg_scopes, actions)
1039
1040 if choices:
1041 if not _is_list(choices):
1042 raise error.CommandDescriptionError('"choices" argument must be a list '
1043 'or tuple of argument descriptions from which to choose',
1044 command)
1045
1046 for choice in choices:
1047 choice_args = _get_choice_args(choice)
1048 choice_arg_scopes = arg_scopes
1049 choice_actions = list(arg_actions)
1050 choice_prefix_matches = list(prefix_matches)
1051 if isinstance(choice, collections.Mapping):
1052 choice_arg_scopes = [choice] + choice_arg_scopes
1053 choice_optional = choice.get(optional_name, False)
1054 if choice_optional:
1055 optional = True
1056 choice_arg_scopes, choice_actions = \
1057 self.check_command_type_and_actions(
1058 choice, choice_arg_scopes, choice_actions)
1059 choice_arg_data = dict(arg_data)
1060 choice_fields = list(fields)
1061
1062 choice_parse_results = self.parse_arguments(choice_args,
1063 words, start_word_index, choice_arg_scopes,
1064 choice_arg_data, choice_fields, choice_actions,
1065 choice_prefix_matches, command)
1066 for choice_parse_result in choice_parse_results:
1067 words_matched = choice_parse_result[0]
1068 new_arg_data = choice_parse_result[3]
1069 # FIXME: Not sure if the code below is the best way to
1070 # handle things, but the idea is that we want to detect
1071 # the case where any of the choices in a choice block
1072 # is composed of all optional arguments. In that case
1073 # the overall choice block thus becomes optional. The
1074 # reason we propagate the optional attribute is that if
1075 # there are multiple choices that consist entirely of
1076 # optional arguments then we'd get mlutiple redundant
1077 # matches with exactly the same arg_data and prefix_matches
1078 # which would lead to an ambiguous command when we
1079 # process the results at the end. So by not adding a
1080 # result for each of those cases and instead just adding
1081 # a single result for the overall choice block.
1082 # The other thing we need to do is distinguish between
1083 # optional args and default args which will both lead to
1084 # cases where words_matched == 0. For the default arg
1085 # case though we will add the match in the nested call
1086 # since it will have changes to the arg_data which are
1087 # significant in the processing of the command action.
1088 # Since we've already added a result, we don't want to
1089 # set the overall choice to be optional or else again
1090 # we'll get multiple amibuous results. The way we detect
1091 # that case is if the arg_data from the parse_result is
1092 # different than the arg_data that was passed in. So
1093 # that's why we use the following test.
1094 if words_matched == 0 and new_arg_data == arg_data:
1095 # FIXME: I don't think this will work correctly
1096 # if/when we support default values for args. In that
1097 # case the choice may have matched 0 words, but it
1098 # may have updated the arg_data with some default
1099 # argument values, which we'll if we don't add the
1100 # parse_result at this point. Need to think more
1101 # about this.
1102 optional = True
1103 else:
1104 arg_parse_results.append(choice_parse_result)
1105 else:
1106 token = arg.get('token')
1107 field = arg.get('field')
1108 arg_type = arg.get('type')
1109 tag = arg.get('tag')
1110 default = self.get_default_value(arg)
1111
1112 tag_prefix_match = None
1113 parsed_tag = False
1114 is_match = True
1115 words_matched = 0
1116 results = None
1117
1118 # First try to parse the tag if there is one
1119 if tag and len(words) > 0:
1120 word = words[0]
1121 if tag.lower().startswith(word.lower()):
1122 if tag.lower() != word.lower():
1123 tag_prefix_match = [start_word_index+words_matched, tag]
1124 words_matched += 1
1125 parsed_tag = True
1126 else:
1127 self.handle_parse_error("Unexpected argument at \"%s\"" % word,
1128 start_word_index, CommandHandler.UNEXPECTED_TOKEN_PRIORITY, tag)
1129 is_match = False
1130
1131 # Handle incomplete argument matching
1132 if is_match:
1133 if words_matched < len(words):
1134 word = words[words_matched]
1135 else:
1136 self.handle_incomplete_command(arg, arg_scopes,
1137 arg_data, fields, parsed_tag, command)
1138 if default:
1139 word = default
1140 else:
1141 self.handle_parse_error("Unexpected end of command",
1142 start_word_index + words_matched,
1143 CommandHandler.UNEXPECTED_END_OF_ARGUMENTS_PRIORITY)
1144 is_match = False
1145
1146 # Handle the argument value
1147 if is_match:
1148 if token:
1149 if token.lower().startswith(word.lower()):
1150 value = True if arg_type == 'boolean' else token
1151 results = [(value, token)]
1152 else:
1153 self.handle_parse_error(
1154 "Unexpected argument at \"%s\"" % word,
1155 start_word_index + words_matched,
1156 CommandHandler.UNEXPECTED_TOKEN_PRIORITY, token)
1157 is_match = False
1158 else:
1159 # Check that the argument is valid
1160 try:
1161 results = validate_argument(arg, word, arg_scopes, command)
1162 except error.ArgumentValidationError, e:
1163 expected_tokens = e.expected_tokens
1164 if expected_tokens:
1165 if _is_string(expected_tokens):
1166 expected_tokens = (expected_tokens,)
1167 self.handle_parse_error(str(e),
1168 start_word_index + words_matched,
1169 CommandHandler.UNEXPECTED_TOKEN_PRIORITY,
1170 expected_tokens)
1171 else:
1172 self.handle_parse_error(str(e),
1173 start_word_index + words_matched,
1174 CommandHandler.VALIDATION_ERROR_PRIORITY)
1175 is_match = False
1176
1177 if is_match:
1178 assert results is not None
1179 assert _is_list(results)
1180 assert len(results) > 0
1181 # If we reach here we've successfully matched the word. The word
1182 # may have come from the commands words or it may have come from
1183 # the default value for the argument. We only want to bump the
1184 # words_matched in the former case, which is why we need to check
1185 # against the length of the words array. Note that we don't want
1186 # to bump words_matched in the code above where we get it from
1187 # the command words, because then the word offset we pass to
1188 # handle_parse_error would be off by 1 if the validation fails.
1189 if words_matched < len(words):
1190 words_matched += 1
1191 data = arg.get('data')
1192 arg_data_handler = _lookup_in_scopes('data-handler', arg_scopes)
1193 self.handle_first_matched_result(command)
1194
1195 for result in results:
1196 value, match_token = result
1197 new_arg_data = dict(arg_data)
1198 if data:
1199 new_arg_data.update(data)
1200 # XXX should the mode passed in here to the handler be
1201 # the mode of the command, or the current mode ?
1202 # (mode-of-the-command in case its a higher submode push)
1203 if arg_data_handler:
1204 invocation_scope = {
1205 # FIXME: The 'name' attribute is deprecated. Remove once
1206 # everything's been converted.
1207 'name': field,
1208 'field': field,
1209 'value': value,
1210 'data': new_arg_data,
1211 'is-no-command': self.is_no_command,
1212 'current-mode-obj-type': sdnsh.get_current_mode_obj_type(),
1213 'current-mode-obj-id': sdnsh.get_current_mode_obj()
1214 }
1215 new_arg_scopes = [invocation_scope] + arg_scopes
1216 try:
1217 result = _call_proc(arg_data_handler,
1218 argument_data_handler_registry, new_arg_scopes,
1219 command)
1220 except Exception, e:
1221 # XXX ought to not manage parameter exceptions for _call_proc
1222 if sdnsh.debug or sdnsh.debug_backtrace:
1223 traceback.print_exc()
1224 self.handle_parse_error(str(e),
1225 start_word_index + words_matched,
1226 CommandHandler.VALIDATION_ERROR_PRIORITY)
1227 return parse_results
1228 elif field is not None:
1229 new_arg_data[field] = value
1230
1231 self.handle_matched_result(command, result, arg_scopes)
1232
1233 # FIXME: Do we still need the separate fields dict?
1234 # If so, I don't think this is actually correct, since
1235 # we want fields to not necessarily be kept exactly in
1236 # sync with arg_data. Need to think about this more.
1237 new_fields = new_arg_data.keys()
1238 new_prefix_matches = list(prefix_matches)
1239 if tag_prefix_match:
1240 new_prefix_matches.append(tag_prefix_match)
1241 if len(match_token) > len(word):
1242 new_prefix_matches.append(
1243 [start_word_index+words_matched-1, match_token])
1244 arg_parse_results.append([words_matched, new_prefix_matches,
1245 arg_scopes, new_arg_data, new_fields, arg_actions])
1246
1247 if optional:
1248 arg_parse_results.append([0, prefix_matches, scopes,
1249 arg_data, fields, actions])
1250
1251 for arg_parse_result in arg_parse_results:
1252 (words_matched, prefix_matches, arg_scopes, arg_data,
1253 fields, actions) = arg_parse_result
1254 remaining_words = words[words_matched:]
1255 remaining_parse_results = self.parse_arguments(
1256 remaining_args, remaining_words,
1257 start_word_index + words_matched, scopes, arg_data,
1258 fields, actions, prefix_matches, command)
1259 # The first item in each tuple is the words consumed, but
1260 # that's relative to the remaining args that we passed to
1261 # it. For the parse results from this invocation of
1262 # parse args we also need to include the counts of the args
1263 # that we've already parsed plus the args that were parsed
1264 # for the current choice.
1265 for parse_result in remaining_parse_results:
1266 parse_result[0] += words_matched
1267# parse_prefix_matches = parse_result[1]
1268# for match in parse_prefix_matches:
1269# match[0] += words_matched
1270 parse_result[1] = prefix_matches + parse_result[1]
1271 parse_results.append(parse_result)
1272
1273 return parse_results
1274
1275
1276 def handle_one_command(self, command, prefix_match, words):
1277
1278 assert words is not None
1279 assert len(words) > 0
1280 assert command is not None
1281
1282 fields = {}
1283 command_word = words[0]
1284 command_name = command.get('name')
1285 arg_data = dict(command.get('data', {}))
1286
1287 # The first two elements in the scopes are the command and the command
1288 # defaults. The command defaults will be populated with the real
1289 # defaults when we call check_command_defaults_and_actions
1290 scopes = [command, {}]
1291 actions = []
1292 prefix_matches = []
1293 if prefix_match:
1294 prefix_matches.append([0, command_name])
1295
1296 if isinstance(command_name, collections.Mapping):
1297 field = command_name['field']
1298 if field:
1299 fields[field] = True
1300 arg_data[field] = command_word
1301 elif _is_string(command_name):
1302 assert command_name.lower().startswith(command_word.lower())
1303 else:
1304 raise error.CommandDescriptionError('Command name must be either'
1305 'a string or a dict', command)
1306
1307 # We've checked that the first word matches what we expect from the
1308 # command object and now we just care about the remaining words, so
1309 # we strip off the initial command word. Note that we slice/copy
1310 # the word list instead of deleting the first word in place, so that
1311 # we don't affect the word list of the calling function.
1312 words = words[1:]
1313
1314 scopes, actions = self.check_command_type_and_actions(command, scopes, actions)
1315
1316 # Parse the arguments
1317 args = _get_args(command)
1318 parse_results = self.parse_arguments(args, words, 1, scopes,
1319 arg_data, fields, actions, prefix_matches, command)
1320
1321 if parse_results:
1322 for parse_result in parse_results:
1323 words_consumed, prefix_matches, scopes, arg_data, fields, actions = parse_result
1324 if words_consumed == len(words):
1325 self.handle_complete_command(prefix_matches, scopes, arg_data, fields, actions, command)
1326 else:
1327 word = words[words_consumed]
1328 self.handle_parse_error(
1329 'Unexpected additional arguments at \"%s"' % word,
1330 words_consumed+1,
1331 CommandHandler.UNEXPECTED_ADDITIONAL_ARGUMENTS_PRIORITY)
1332
1333 return None
1334
1335
1336 def handle_command(self, words, text=None):
1337
1338 # Check if it's a no command and, if so, adjust the words
1339 self.is_no_command = len(words) > 0 and words[0].lower() == 'no'
1340 if self.is_no_command:
1341 self.prefix = words[0]
1342 words = words[1:]
1343
1344 self.words = words
1345 self.text = text
1346
1347 result = None
1348
1349 try:
1350 if len(words) > 0:
1351 command_word = words[0]
1352 matching_commands = self.get_matching_commands(command_word,
1353 self.is_no_command,
1354 command_registry)
Srikanth Vavilapalli40d79f82014-12-17 14:29:24 -08001355 # There may be multiple results. We need to figure out it there's
1356 # an unambiguous match. The heuristic we use is to iterate over
1357 # the command words and try to find a single result which has an
1358 # exact match on the word while all the others are prefix matches.
1359 # If there are multiple results that are exact matches, then we
1360 # discard the other results that were prefix matches and continue
1361 # with the next command word. If at an iteration all of the
1362 # results are prefix matches for the current word, then we check
1363 # all of the full tokens that were prefix matched by the command
1364 # word. If all of the results have the same full token, then we
1365 # continue. If there are multiple full tokens, then that's
1366 # indicative of an ambiguous command, so we break out of the loop
1367 # and raise an ambiguous command exception.
1368 exact_match_commands = []
1369 for matching_command in matching_commands:
1370 command, prefix_match = matching_command
1371 if prefix_match == False:
1372 #exact match found
1373 exact_match_commands.append(matching_command)
1374 if len(exact_match_commands) > 0:
1375 matching_commands = exact_match_commands
1376
Srikanth Vavilapalli1725e492014-12-01 17:50:52 -08001377 for matching_command in matching_commands:
1378 command, prefix_match = matching_command
1379 self.handle_one_command(command, prefix_match, words)
1380 else:
1381 result = self.handle_unspecified_command()
1382 result = self.handle_command_results()
1383 except Exception, e:
1384 if sdnsh.debug or sdnsh.debug_backtrace:
1385 traceback.print_exc()
1386 if isinstance(e, error.CommandError):
1387 result = self.handle_command_error(e)
1388 else:
1389 result = self.handle_exception(e)
1390
1391 return result
1392
1393
1394class CommandExecutor(CommandHandler):
1395
1396 def __init__(self):
1397 super(CommandExecutor, self).__init__()
1398 self.matches = []
1399
1400 #def get_default_attribute_name(self):
1401 # return 'default-for-no' if self.is_no_command else 'default'
1402
1403 def handle_unspecified_command(self):
1404 raise error.CommandInvocationError('No command specified')
1405
1406 def handle_command_error(self, e):
1407 result = sdnsh.error_msg(str(e))
1408 return result
1409
1410 def handle_exception(self, e):
1411 if sdnsh.debug or sdnsh.debug_backtrace:
1412 traceback.print_exc()
1413 raise e
1414
1415 def handle_complete_command(self, prefix_matches, scopes, arg_data, fields, actions, command):
1416 self.matches.append((prefix_matches, scopes, arg_data, fields, actions, command))
1417
1418 def handle_command_results(self):
1419
1420 if len(self.matches) == 0:
1421 # No matching command. We need to run through the parse
1422 # errors we've collected to try to figure out the best error
1423 # message to use. The simple heuristic we use is that we'll
1424 # assume that errors that matched the most command words
1425 # before hitting a parse error are the most relevant, so
1426 # those are the ones we'll print. If it's a parse error
1427 # because it didn't match a literal token (i.e. as opposed
1428 # to a validation error), then we'll collect all of the
1429 # valid tokens and print a single error message for all of
1430 # them. Otherwise we'll print out all of the individual
1431 # error message. This isn't great, because in a lot of
1432 # cases there we'll be multiple error messages, which
1433 # may be either conflicting or almost identical (identical
1434 # messages are eliminated because we use a set when we
1435 # collect the parse errors).
1436 most_words_matched = -1
1437 highest_priority = -1
1438 accumulated_error_messages = []
1439 accumulated_expected_tokens = []
1440 for error_message, words_matched, priority, expected_tokens in self.parse_errors:
1441 if words_matched > most_words_matched:
1442 # We found an error that matched more words than
1443 # the other errors we've seen so far, so reset
1444 # the highest priority, so we'll start looking for
1445 # the new highest priority at the current word match level.
1446 # the variable we're using to collect the error
1447 # messages and expected tokens
1448 most_words_matched = words_matched
1449 highest_priority = -1
1450 if words_matched == most_words_matched:
1451 if priority > highest_priority:
1452 # We found a higher priority error, so clear the
1453 # accumulated errors and expected tokens.
1454 accumulated_error_messages = []
1455 accumulated_expected_tokens = []
1456 highest_priority = priority
1457 if words_matched == most_words_matched and priority == highest_priority:
1458 # Collect the error message and check to see if there's
1459 # an expected token and if all of the errors at this level
1460 # so far also had expected tokens. If there's any error
1461 # that doesn't have an expected token, then we revert
1462 # to the mode where we'll print all of the individual
1463 # error messages.
1464 if expected_tokens is not None:
1465 if _is_string(expected_tokens):
1466 expected_tokens = [expected_tokens]
1467 elif isinstance(expected_tokens, tuple):
1468 expected_tokens = list(expected_tokens)
1469 accumulated_expected_tokens += expected_tokens
1470 else:
1471 accumulated_error_messages.append(error_message)
1472
1473 # We've collected the errors and the expected tokens. Now we
1474 # just need to format the error message to use with the
1475 # CommandSyntaxError exception.
1476 error_message = ''
1477 if accumulated_expected_tokens:
1478 word = self.words[most_words_matched]
1479 error_message += 'Unexpected argument \"%s\"; expected ' % word
1480 expected_tokens = ['"' + expected_token + '"'
1481 for expected_token in accumulated_expected_tokens]
1482 if len(expected_tokens) > 1:
1483 expected_tokens.sort()
1484 error_message += 'one of '
1485 error_message += '(' + ', '.join(expected_tokens) + ')'
1486 for message in accumulated_error_messages:
1487 if error_message:
1488 error_message += '\n'
1489 error_message += message
1490
1491 raise error.CommandSyntaxError(error_message)
1492
1493 # There may be multiple results. We need to figure out it there's
1494 # an unambiguous match. The heuristic we use is to iterate over
1495 # the command words and try to find a single result which has an
1496 # exact match on the word while all the others are prefix matches.
1497 # If there are multiple results that are exact matches, then we
1498 # discard the other results that were prefix matches and continue
1499 # with the next command word. If at an iteration all of the
1500 # results are prefix matches for the current word, then we check
1501 # all of the full tokens that were prefix matched by the command
1502 # word. If all of the results have the same full token, then we
1503 # continue. If there are multiple full tokens, then that's
1504 # indicative of an ambiguous command, so we break out of the loop
1505 # and raise an ambiguous command exception.
1506 unambiguous_match = None
1507 if len(self.matches) > 1:
1508 narrowed_matches = self.matches
1509 for i in range(len(self.words)):
1510 exact_match_matches = []
1511 prefix_match_matches = []
1512 prefix_match_full_tokens = {}
1513 for match in narrowed_matches:
1514 prefix_matches = match[0]
1515 for prefix_match in prefix_matches:
1516 if prefix_match[0] == i:
1517 prefix_match_matches.append(match)
1518 if type(prefix_match[1]) == dict: # regular expression
1519 # similar to _get_command_title()
1520 full_token = prefix_match[1]['title']
1521 else:
1522 full_token = prefix_match[1].lower()
1523 prefix_match_full_tokens[full_token] = None
1524 break
1525 else:
1526 exact_match_matches.append(match)
1527 if len(exact_match_matches) == 1:
1528 unambiguous_match = exact_match_matches[0]
1529 break
1530 elif len(exact_match_matches) > 1:
1531 narrowed_matches = exact_match_matches
1532 else:
1533 # No exact matches, check to see if the prefix matches
1534 # are all prefixes for the same token. If so, then let
1535 # the process continue with the next word in the command.
1536 # Otherwise it's an ambiguous command.
1537 assert len(prefix_match_matches) > 1
1538 if len(prefix_match_full_tokens) > 1:
1539 break
1540 narrowed_matches = prefix_match_matches
1541 else:
1542 # If we reach the end of the words without either finding an
1543 # unambiguous result or the word which was ambiguous then that
1544 # means that multiple command descriptions (or multiple
1545 # choices/path through a single command description) had the
1546 # exact same match tokens, which means that there are
1547 # conflicting command descriptions, which is a bug in the
1548 # command descriptions.
1549 if sdnsh.debug or sdnsh.debug_backtrace or sdnsh.description:
1550 for n in narrowed_matches:
1551 print _line(), n[1][0]['self']
1552 raise error.CommandAmbiguousError('Multiple command description match')
1553
1554 if unambiguous_match is None:
1555 assert prefix_match_full_tokens is not None
1556 assert len(prefix_match_full_tokens) > 1
1557 quoted_full_tokens = ['"' + full_token + '"'
1558 for full_token in prefix_match_full_tokens]
1559 # pylint: disable=W0631
1560 raise error.CommandAmbiguousError(
1561 'Ambiguous command word "%s"; matches [%s]' %
1562 (self.words[i], ', '.join(quoted_full_tokens)))
1563
1564 else:
1565 unambiguous_match = self.matches[0]
1566
1567 prefix_matches, scopes, arg_data, fields, actions, command = unambiguous_match
1568
1569 # XXX Possibly a better method of deciding when to loook deeper
1570 # in the scopes would be a good idea.
1571 if fields == None or len(fields) == 0:
1572 fields = _lookup_in_scopes('fields', scopes)
1573
1574 invocation_scope = {
1575 'data': arg_data,
1576 'fields': fields,
1577 'is-no-command': self.is_no_command,
1578 'current-mode-obj-type': sdnsh.get_current_mode_obj_type(),
1579 'current-mode-obj-id': sdnsh.get_current_mode_obj()
1580 }
1581
1582 #scopes = [invocation_scope] + scopes
1583 assert actions is not None
1584 if len(actions) == 0:
1585 raise error.CommandDescriptionError('No action specified', command)
1586 #if valid_args:
1587 # scopes = valid_args + scopes
1588 if sdnsh.description: # debug details
1589 print "Command Selected: %s" % command['self']
1590
1591 combined_result = None
1592 for action in actions:
1593 action_proc, action_scopes = action
1594 scopes = [invocation_scope] + action_scopes
1595 if sdnsh.description:
1596 print 'Action: ', action_proc
1597 result = _call_proc(action_proc, action_registry, scopes, command)
1598 if result is not None:
1599 combined_result = combined_result + result if combined_result else result
1600
1601 # Since a successful 'no' command may remove an object,
1602 # after a no command complete's all its actions, verify
1603 # the existance of every object in the mode stack.
1604 # Pop any nested modes where the object is missing.
1605 if self.is_no_command:
1606 # starting at the most nested submode level, determine if the
1607 # current submode object still exists
1608 any_removed = False
1609 for index in reversed(range(len(sdnsh.mode_stack))):
1610 mode = sdnsh.mode_stack[index]
1611 if mi.obj_type_in_use_as_related_config_type(mode.get('obj_type')):
1612 if sdnsh.description:
1613 print 'CONFIG TYPE:', \
1614 mode.get('obj_type'), \
1615 mi.obj_type_in_use_as_related_config_type(mode.get('obj_type'))
1616 continue
1617
1618 if mode.get('obj_type') and mode.get('obj'):
1619 try:
1620 sdnsh.get_object_from_store(mode['obj_type'], mode['obj'])
1621 except:
1622 # pop this item
1623 if sdnsh.description:
1624 print 'NO LOST OBJECT ', mode.get('obj_type'), mode.get('obj')
1625 del sdnsh.mode_stack[index]
1626 any_removed = True
1627
1628 if any_removed:
1629 sdnsh.warning("submode exited due to deleted object")
1630 sdnsh.update_prompt()
1631
1632 return combined_result
1633
1634
1635class CommandCompleter(CommandHandler):
1636
1637 def __init__(self, meta_completion = False):
1638 super(CommandCompleter, self).__init__()
1639 self.meta_completion = meta_completion
1640 self.completions = {}
1641
1642 def get_default_value(self, arg):
1643 default = arg.get('default')
1644 if self.is_no_command and default is None and 'field' in arg:
1645 default = mi.field_current_obj_type_default_value(arg['field'])
1646 if default != None:
1647 default = str(None)
1648 return default
1649
1650 def complete_help(self, arg_scopes):
1651 """
1652 Select the most appropriate localized text to associate with
1653 the completion-reason for some completion-text
1654 """
1655 c_help = _lookup_in_scopes('conpletion-text', arg_scopes)
1656 if c_help:
1657 return c_help
1658 c_help = _lookup_in_scopes('syntax-help', arg_scopes)
1659 if c_help:
1660 return c_help
1661 c_help = _lookup_in_scopes('all-help', arg_scopes)
1662 if c_help:
1663 return '!' + c_help
1664 c_help = _lookup_in_scopes('short-help', arg_scopes)
1665 if c_help:
1666 return c_help
1667 return ' <help missing> %s' % _lookup_in_scopes('self', arg_scopes)
1668
1669
1670 def completion_set(self, completions, text, reason):
1671 """
1672 In situations where multiple commands match for some completion
1673 text, the last command-iterated-over will populate the text for
1674 the completion dictionary. Use the first character of the
1675 completion string for this command to identify when the text
1676 represents the most "base" variant of the command to display
1677 for completion help text.
1678
1679 When the first character of the string is a '!', the printing
1680 procedures must drop the first character.
1681 """
1682 if completions == None:
1683 completions = {text : reason}
1684 return
1685 current_text = completions.get(text)
1686 # current_text could possibly be None from the lookup
1687 if current_text != None and current_text[0] == '!':
1688 return
1689 completions[text] = reason
1690
1691
1692 def command_help_text(self, command):
1693 c_help = command.get('all-help')
1694 if c_help:
1695 if c_help[0] != '!':
1696 return '!' + c_help
1697 return c_help
1698 c_help = command.get('short-help')
1699 return c_help
1700
1701
1702 def handle_unspecified_command(self):
1703 candidates = self.get_matching_commands(self.text, self.is_no_command, command_registry)
1704 for candidate in candidates:
1705 self.completion_set(self.completions, candidate[0]['name'] + ' ',
1706 self.command_help_text(candidate[0]))
1707
1708 def handle_incomplete_command(self, arg, arg_scopes, arg_data, fields,
1709 parsed_tag, command):
1710
1711 completions = None
1712 tag = arg.get('tag')
1713 lower_text = self.text.lower()
1714
1715 if tag is not None and not parsed_tag:
1716 if tag.lower().startswith(lower_text):
1717 if completions == None:
1718 completions = {}
1719 completions[tag + ' '] = self.complete_help(arg_scopes)
1720 else:
1721 token = arg.get('token')
1722 type_name = arg.get('type')
1723 if type_name == None:
1724 type_name = arg.get('base-type')
1725 typedef = typedef_registry.get(type_name) if type_name else None
1726
1727 # See if this argument is an enumerated value. This can either
1728 # be by a type/values specified inline in the argument or via
1729 # an enum typedef. So first we check for those two cases and
1730 # get the object that contains the values, either the argument
1731 # itself or the typedef.
1732 enum_obj = None
1733 if token is not None:
1734 if token.lower().startswith(lower_text):
1735 if completions == None:
1736 completions = {}
1737 completions[token + ' '] = self.complete_help(arg_scopes)
1738 elif type_name == 'enum':
1739 enum_obj = arg
1740 elif typedef:
1741 # Note: This doesn't recursively check up the type hierarchy,
1742 # but it doesn't seem like a valid use case to have more than
1743 # one level of inheritance for enumerations, so it should be OK.
1744 base_type = typedef.get('base-type')
1745 if base_type == 'enum':
1746 enum_obj = typedef
1747
1748 # If we have an enum object, then get the values and
1749 # build the completion list
1750 if enum_obj:
1751 enum_values = enum_obj.get('values')
1752 if enum_values:
1753 if _is_string(enum_values):
1754 enum_values = (enum_values,)
1755 # enum_values may be either a array/tuple of strings or
1756 # else a dict whose keys are the enum strings. In both
1757 # cases we can use the for loop to iterate over the
1758 # enum string names to be used for completion.
1759 completions = {}
1760 for enum_value in enum_values:
1761 if enum_value.lower().startswith(lower_text):
1762 completions[enum_value + ' '] = \
1763 self.complete_help(arg_scopes)
1764
1765 if completions == None or len(completions) == 0:
1766 # Check to see if there's a custom completion proc
1767 completion_proc = _lookup_in_scopes('completion', arg_scopes)
1768 if completion_proc:
1769 completions = {}
1770 curr_obj_type = sdnsh.get_current_mode_obj_type()
1771 curr_obj_id = sdnsh.get_current_mode_obj()
1772 command_mode = command.get('mode')
1773 if command_mode and command_mode[-1] == '*':
1774 command_mode = command_mode[:-1]
Srikanth Vavilapallib5c3ca52014-12-15 15:59:33 -08001775 if (command_mode == 'config-tunnel' and
1776 sdnsh.current_mode() == 'config-tunnelset-tunnel'):
1777 command_mode = 'config-tunnelset-tunnel'
1778 if (command_mode and
1779 ((_is_list(command_mode) and not sdnsh.current_mode() in command_mode)
1780 or (sdnsh.current_mode() != command_mode))):
1781
Srikanth Vavilapalli1725e492014-12-01 17:50:52 -08001782 # this is completing a different item on the stack.
1783 # XXX needs better api's here.
1784 found_in_mode_stack = False
Srikanth Vavilapallib5c3ca52014-12-15 15:59:33 -08001785
1786 if _is_list(command_mode):
1787 for cmd_mode in command_mode:
1788 for x in sdnsh.mode_stack:
1789 if x['mode_name'] == cmd_mode:
1790 found_in_mode_stack = True
1791 curr_obj_type = x['obj_type']
1792 curr_obj_id = x['obj']
1793 break
1794 if found_in_mode_stack == True:
1795 break
1796 else:
1797 for x in sdnsh.mode_stack:
1798 if x['mode_name'] == command_mode:
1799 found_in_mode_stack = True
1800 curr_obj_type = x['obj_type']
1801 curr_obj_id = x['obj']
1802 break
Srikanth Vavilapalli1725e492014-12-01 17:50:52 -08001803 if not found_in_mode_stack:
1804 raise error.CommandDescriptionError(
1805 'Unable to find mode %s' % command_mode,
1806 command)
1807
1808 invocation_scope = {
1809 'words': self.words,
1810 'text': self.text,
1811 'data': arg_data,
1812 'completions': completions,
1813 'is-no-command': self.is_no_command,
1814 'mode' : command_mode,
1815 'current-mode-obj-type': curr_obj_type,
1816 'current-mode-obj-id': curr_obj_id,
1817 }
1818 arg_scopes.insert(0, invocation_scope)
1819 if sdnsh.description: # for deubugging, print command name
Srikanth Vavilapalli40d79f82014-12-17 14:29:24 -08001820 print "command and completion_proc", command['self'], completion_proc
Srikanth Vavilapalli1725e492014-12-01 17:50:52 -08001821 try:
1822 _result = _call_proc(completion_proc, completion_registry,
1823 arg_scopes, command)
1824 except TypeError, e:
1825 if sdnsh.debug or sdnsh.debug_backtrace:
1826 print "Issue: ", e
1827 traceback.print_exc()
1828 raise error.CommandDescriptionError("Unable to call completion",
1829 command)
1830
1831 if (completions == None or len(completions) == 0 or self.meta_completion) and not self.text:
1832 # TODO: It's not clear in this case how to detect if a
1833 # partially entered argument can really match this argument,
1834 # so we (conservatively) only include these completion text
1835 # strings when there's no partial text for the argument, which
1836 # is why we have the above check for self.text
1837
1838 # Note: We want to allow None for the value of completion-text
1839 # to mean that completion should be disabled for the argument,
1840 # so we use False for the default value of the get method.
1841 # Then if the completion-text is not set in the arg the
1842 # "if not completion_text" checks will be True, so we'll try to
1843 # set the completion text to the help-name or field value.
1844 completion_text = arg.get('completion-text', False)
1845 if completion_text is not None:
1846 # FIXME: Probably shouldn't use syntax help here. That
1847 # should probably be reserved for printing additional
1848 # syntax help for the argument, duh (presumably some longer
1849 # descriptive text for the argument).
1850 if not completion_text:
1851 completion_text = arg.get('help-name')
1852 if not completion_text:
1853 completion_text = tag if tag else arg.get('field')
1854 syntax_help = arg.get('syntax-help')
1855 if syntax_help == None:
1856 if sdnsh.description:
1857 syntax_help = 'Add syntax-help %s at %s' % (command['self'],
1858 completion_text)
1859 # exclude enum, since the values will included as text
1860 if typedef and type_name != 'enum':
1861 syntax_help = typedef.get('help-name')
1862 if syntax_help == None:
1863 syntax_help = ' <' + type_name + '> ' # add delim
1864
1865 if completion_text and syntax_help:
1866 #and completion_text.startswith(lower_text):
1867 completion_text = ' <' + completion_text + '> ' # add_delim
1868 if completions == None:
1869 completions = {}
1870 # The use of tuple here as he index provides a method
1871 # of identifying the completion values which are
1872 # 'meta-information', and not to be completed against
1873 completions[(completion_text, syntax_help)] = syntax_help
1874
1875 if completions and len(completions) > 0:
1876 for (completion, text) in completions.items():
1877 self.completion_set(self.completions, completion, text)
1878
1879 return None
1880
1881 def handle_complete_command(self, prefix_matches, scopes,
1882 arg_data, fields, actions, command):
1883 if not self.text:
1884 # complete command, intentionally use the short-help associated
1885 # with the top level of this command.
1886 # The use of tuple here as he index provides a method
1887 # of identifying the completion values which are
1888 # 'meta-information', and not to be completed against
1889 self.completions[(' <cr> ', '<cr>' )] = command.get('short-help')
1890
1891 def handle_command_error(self, e):
1892 # For completion ignore all errors exception for ones that are
1893 # indicative of an error in the command description, which will
1894 # (hopefully) be found by the developer while debugging the
1895 # command description.
1896 if isinstance(e, error.CommandDescriptionError):
1897 sdnsh.print_completion_help(sdnsh.error_msg(str(e)))
1898 if sdnsh.debug or sdnsh.debug_backtrace:
1899 traceback.print_exc()
1900
1901
1902 def print_completions(self, completions):
1903 """
1904 Print the completions ourselves in sorted multiple column format.
1905 We use this when the completions are are pseudo/help completions
1906 that aren't real tokens that we want readline to use for
1907 completions.
1908 """
1909 meta_completions = [completion[0] if isinstance(completion, tuple)
1910 else completion for completion in completions]
1911 meta_completions.sort()
1912
1913 sdnsh.print_completion_help(sdnsh.choices_text_builder(meta_completions))
1914
1915
1916 def handle_command_results(self):
1917 if self.meta_completion == True:
1918 # two-column display mode
1919 sdnsh.completion_print = False
1920 if len(self.completions):
1921 max_token_len = 0
1922 for completion in self.completions:
1923 item = completion
1924 if isinstance(completion, tuple):
1925 item = completion[0]
1926 max_token_len = max(max_token_len, len(item))
1927 text = []
1928 for completion in sorted(self.completions.keys(),
1929 cmp=utif.completion_trailing_integer_cmp):
1930 help = self.completions[completion]
1931 if help and help[0] == '!':
1932 help = help[1:]
1933
1934 left_side = completion
1935 if isinstance(completion, tuple):
1936 left_side = completion[0]
1937 text.append('%-*s %s\n' % (max_token_len, left_side.strip(), help.strip()))
1938 sdnsh.print_completion_help(''.join(text))
1939
1940 completions = list(self.completions)
1941 if len(completions) == 1:
1942 completion = completions[0]
1943 if isinstance(completion, tuple):
1944 help_text = completion[1]
1945 if not help_text:
1946 help_text = completion[0]
1947 if self.meta_completion == False:
1948 sdnsh.print_completion_help(help_text)
1949 completions = None
1950 elif len(completions) > 1:
1951 # Some of the completion results are really pseudo completions
1952 # that are meant to be more like a help prompt for the user rather
1953 # than an actual token that should be used for completion by readline
1954 # If all of the completions are like that and we return them to
1955 # readline, then it will actually complete the leading prefix, which
1956 # will be at least the leading '<'. To avoid that situation we print
1957 # out the completions ourselves using print_completion_help and then
1958 # return None for the completions.
1959 all_pseudo_completions = True
1960 for completion in completions:
1961 name = completion[0] if isinstance(completion, tuple) else completion
1962 if not name.endswith('> ') and not name.endswith('>'):
1963 all_pseudo_completions = False
1964 break
1965 if all_pseudo_completions:
1966 if self.meta_completion == False:
1967 self.print_completions(completions)
1968 completions = None
1969 else:
1970 completions = [completion[0] if isinstance(completion, tuple)
1971 else completion for completion in completions]
1972
1973 return completions
1974
1975
1976def do_command_completion(words, text):
1977 completer = CommandCompleter()
1978 completions = completer.handle_command(words, text)
1979 return completions
1980
1981
1982def do_command_completion_help(words, text):
1983 completer = CommandCompleter(meta_completion = True)
1984 completions = completer.handle_command(words, text)
1985 return completions
1986
1987
1988def _canonicalize_validation_result(result):
1989 """
1990 The canonical (i.e. most general) representation of a validation
1991 result is a list of 2-item tuples where the first item in the tuple
1992 is the validated value that should be used to update the arg_data
1993 and the second item is the actual token that was matched, which may
1994 not be the same as the word in the command. For example, in the
1995 results for an enum the word from the command may be a prefix for
1996 one of the allowed value for the enum. In that case the first item
1997 would be the return value for the enum value and the second item
1998 will be the full enum value, i.e. not the prefix that was typed
1999 in the command.
2000
2001 But there are a couple of simpler representations of the validation
2002 results. First, the result can be a simple scalar validated value.
2003 This is converted to the canonical representation by turning it
2004 into [(<value>, <value>)]. Second, the input result can be just
2005 the 2-item tuple. To canonicalize that we just enclose it in a
2006 list: [<tuple-result>].
2007 """
2008 if result is None:
2009 result = []
2010 elif _is_list(result):
2011 if len(result) > 0 and not _is_list(result[0]):
2012 result = [result]
2013 else:
2014 matching_token = result if _is_string(result) else str(result)
2015 result = [(result, matching_token)]
2016
2017 return result
2018
2019
2020def _combine_validation_results(result1, result2, typedef, value):
2021 if result1 is not None:
2022 result1 = _canonicalize_validation_result(result1)
2023 if result2 is not None:
2024 result2 = _canonicalize_validation_result(result2)
2025
2026 if result1 is not None:
2027 if result2 is not None:
2028 result = [r for r in result1 if r in result2]
2029 if len(result) == 0:
2030 _raise_argument_validation_exception(typedef, value, "no match")
2031 #elif len(result) == 1:
2032 # result = result[0]
2033 return result
2034 else:
2035 return result1
2036 elif result2 is not None:
2037 return result2
2038 else:
2039 return [(value, value)]
2040
2041
2042def validate_type(type_name, value, scopes, command):
2043 """
2044 Validate that the specified value matches the validation in the
2045 specified type definition and any inherited type definitions
2046 """
2047 # Look up the type definition and perform any validation specified there
2048 typedef = typedef_registry.get(type_name)
2049 if not typedef:
2050 raise error.CommandDescriptionError('Unknown type: %s' % type_name)
2051
2052 type_result = None
2053 validation_result = None
2054
2055 # If it's a subtype, validate the base type first
2056 base_type_name = typedef.get('base-type')
2057 if base_type_name:
2058 type_result = validate_type(base_type_name, value, scopes, command)
2059
2060 # FIXME: Seems like we shouldn't be handling obj-type here, since
2061 # that's just an attribute that's specific to certain validation
2062 # procs. Shouldn't that value already be available in the scopes
2063 # to be able to pass to the proc?
2064 obj_type = _lookup_in_scopes('obj-type', scopes)
2065
2066 # Now validate the subtype
2067 # _call_proc requires that the first scope be an invocation scope that
2068 # it possibly modifies by settings the 'scopes' variable, so we need
2069 # to include an empty invocation scope here
2070 invocation_scope = {}
2071 parameter_scope = {'typedef': typedef, 'value': value}
2072 if obj_type:
2073 parameter_scope['obj_type'] = obj_type
2074 scopes = [invocation_scope, parameter_scope, typedef]
2075 while base_type_name is not None:
2076 base_typedef = typedef_registry.get(base_type_name)
2077 if not base_typedef:
2078 raise error.CommandDescriptionError('Invalid base-type name: %s' % base_type_name)
2079 scopes.append(base_typedef)
2080 base_type_name = base_typedef.get('base-type')
2081
2082 #command_type = command.get('command-type')
2083 #command_defaults = command_type_defaults.get(command_type, {})
2084
2085 #scopes.append([command, command_defaults])
2086 validation = _lookup_in_scopes('validation', scopes)
2087 if validation:
2088 validation_result = _call_proc(validation, validation_registry,
2089 scopes, command)
2090 if validation_result is None:
2091 validation_result = value
2092
2093 result = _combine_validation_results(type_result, validation_result,
2094 typedef, value)
2095
2096 return result
2097
2098
2099def validate_argument(arg, value, scopes, command):
2100
2101 type_result = None
2102 validation_result = None
2103
2104 # Validate based on the type of the argument
2105 type_name = arg.get('type')
2106 if type_name:
2107 if type_name == 'enum':
2108 #values = arg.get('values')
2109 #result = validate_enum({'values': values}, value)
2110 type_result = c_validations.validate_enum(arg, value)
2111 else:
2112 type_result = validate_type(type_name, value, scopes, command)
2113 else:
2114 # If the argument description specifies a base-type value, then
2115 # we treat it as an anonymous typedef.
2116 # FIXME: Should probably be able to refactor this code a bit to
2117 # reduce code duplication.
2118 base_type_name = arg.get('base-type')
2119 if base_type_name:
2120 base_typedef = typedef_registry.get(base_type_name)
2121 if base_typedef:
2122 validation = _lookup_typedef_value(base_typedef, 'validation')
2123 if validation:
2124 scopes = [{'typedef': arg, 'value': value}] + scopes
2125 type_result = _call_proc(validation, validation_registry,
2126 scopes, command)
2127 if type_result is None:
2128 type_result = value
2129
2130 # Perform any custom validate proc in the argument
2131 validation = arg.get('validation')
2132 if validation:
2133 # Include 'typedef' in the local scope so that we can use
2134 # typedef validation functions inline for arguments (as opposed to
2135 # requiring a typedef definition).
2136 scopes = [{'arg': arg, 'typedef': arg, 'value': value}] + scopes
2137 validation_result = _call_proc(validation, validation_registry,
2138 scopes, command)
2139 if validation_result is None:
2140 validation_result = value
2141
2142 result = _combine_validation_results(type_result, validation_result,
2143 arg, value)
2144
2145 return result
2146
2147#
2148# CommandSyntaxer: Collect all the commands which "prefix match" the words
2149#
2150class CommandSyntaxer(CommandHandler):
2151
2152 def __init__(self, header):
2153 super(CommandSyntaxer, self).__init__()
2154 if header is None:
2155 header = ''
2156 self.header = header
2157 self.is_no_command = False
2158 self.commands = []
2159
2160 def add_known_command(self, command):
2161 self.commands.append(command)
2162
2163 def handle_incomplete_command(self, arg, arg_scopes, arg_data, fields, parsed_tag, command):
2164 if command not in self.commands:
2165 self.commands.append(command)
2166
2167 def handle_complete_command(self, prefix_matches, scopes, arg_data, fields, actions, command):
2168 if command not in self.commands:
2169 self.commands.append(command)
2170 def handle_command_results(self):
2171 if len(self.commands) == 0:
2172 return 'No applicable command: %s' % ' '.join(self.words)
2173
2174 help_lines = ['']
2175
2176 if self.header:
2177 help_lines.append(self.header)
2178 help_lines.append('')
2179
2180 help_strings = []
2181 for command in self.commands:
2182 command_help_string = _get_command_syntax_help_string(
2183 command, self.prefix)
2184 if type(command_help_string) == list:
2185 help_strings += command_help_string
2186 else:
2187 help_strings.append(command_help_string)
2188
2189 (term_cols, term_rows) = sdnsh.pp.get_terminal_size()
2190 for h in sorted(help_strings):
2191 # use 75% of the width, since reformat_line isn't perfect.
2192 smaller_term_cols = (term_cols * 3) / 4
2193 if len(h) > smaller_term_cols:
2194 help_lines += reformat_line(h, smaller_term_cols, 0, 0)
2195 else:
2196 help_lines.append(h)
2197
2198 if len(self.commands) == 1:
2199 short_help = None
2200 if self.is_no_command:
2201 #
2202 # Cobble together a help string for a no command, build
2203 # a default value for subcommand's, possibly other command
2204 # descriptions
2205 #
2206 short_help = self.commands[0].get('no-help')
2207 if not short_help:
2208 command_type = self.commands[0].get('command-type')
2209 action = self.commands[0].get('action')
2210 if ((command_type and command_type == 'config-submode') or
2211 (action and action == 'push-mode-stack')):
2212 mode_name = self.commands[0].get('name')
2213 short_help = 'Remove %s configuration' % mode_name
2214 else:
2215 short_help = self.commands[0].get('short-help')
2216
2217 if short_help:
2218 #help_lines.append('')
2219 help_lines.append(short_help)
2220
2221 return '\n'.join(help_lines)
2222
2223
2224def get_command_syntax_help(words, header=None):
2225 handler = CommandSyntaxer(header)
2226 # Hack to make "help no" work
2227 if len(words) == 1 and words[0] == 'no':
2228 words = words + ['']
2229 result = handler.handle_command(words)
2230 return result
2231
2232
2233def get_command_short_help_for_pattern(word):
2234 # try to find the command based on the pattern.
2235 for command in command_registry:
2236 if type(command['name']) == dict:
2237 if command['name']['title'] == word:
2238 return command.get('short-help')
2239
2240
2241def get_command_short_help(word):
2242 handler = CommandHandler()
2243 matching_commands = handler.get_matching_commands(word, False,
2244 command_registry)
2245 if len(matching_commands) == 0:
2246 return get_command_short_help_for_pattern(word)
2247
2248 if len(matching_commands) >= 1:
2249 # This will only work once all commands have command descriptions.
2250 #matching_commands[0][0]['self'], _exact_mode_match(current_mode,
2251 #(matching_commands[0][0]).get('mode'))
2252 return (matching_commands[0][0]).get('short-help')
2253 return None
2254
2255
2256def dumb_formatter(out, text, left, right = None):
2257 """
2258 Build output lines from the passed in text. If the
2259 text has leading spaces, then leave the spaces intact
2260 (starting at the indent).
2261 """
2262 if right == None:
2263 (right, line_length) = sdnsh.pp.get_terminal_size()
2264 if right - 20 > left: # XXX needs work
2265 right = right - 20
2266 right = min(right, 120)
2267
2268 left_indent = ' ' * left
2269 out_len = left
2270 out_line = left_indent
2271
2272 for line in text.split('\n'):
2273 if len(line) == 0:
2274 if out_len > left:
2275 out.append(out_line)
2276 out_len = left
2277 out_line = left_indent
2278 out.append('')
2279 elif line[0] == ' ': # leading spaces
2280 if out_len > left:
2281 out.append(out_line)
2282 out_len = left
2283 out_line = left_indent
2284 out.append( left_indent + line )
2285 else: # text formatting
2286
2287 for word in line.split():
2288 sep = ' '
2289 if word.endswith('.'):
2290 sep = ' '
2291 if len(word) + out_len + len(sep) > right:
2292 if out_len > left:
2293 out.append(out_line)
2294 out_len = left + len(word) + len(sep)
2295 out_line = left_indent + word + sep
2296 else:
2297 out_line += word + sep
2298 out_len += len(sep) + len(word)
2299 if out_len > left:
2300 out.append(out_line)
2301
2302
2303#
2304# Documentations: Collect all the commands which "prefix match" the words
2305#
2306class CommandDocumentor(CommandHandler):
2307
2308 def __init__(self, header, format = None):
2309 super(CommandDocumentor, self).__init__()
2310 if header is None:
2311 header = ''
2312 self.header = header
2313 self.commands = []
2314 self.docs = {}
2315 self.docs_stack = []
2316 self.is_no_command = False
2317 self.words = None
2318 self.format = format
2319 (self.term_cols, self.term_rows) = sdnsh.pp.get_terminal_size()
2320
2321
2322 def add_docs(self, name, value):
2323 if name in sdnsh.reserved_words and value == None:
2324 value = 'reserved|%s' % name
2325 if name in self.docs:
2326 if value != self.docs[name]:
2327 if value != None and None in self.docs[name] and \
2328 len(self.docs[name]) == 1 :
2329 self.docs[name] = [value]
2330 elif value != None and value not in self.docs[name]:
2331 self.docs[name].append(value)
2332 # if the value is already there, don't do nuttn
2333 else:
2334 self.docs[name] = [value]
2335
2336 def add_dict_doc(self, arg):
2337 if 'choices' in arg:
2338 self.add_tuple_docs(arg['choices'])
2339 return
2340 if 'args' in arg:
2341 args_arg = arg['args']
2342 if type(args_arg) == tuple:
2343 self.add_tuple_docs(args_arg)
2344 elif type(args_arg) == dict:
2345 self.add_dict_doc(args_arg)
2346 else:
2347 print 'add_dict_doc ', type(args_arg), args_arg
2348 return
2349
2350 tag = arg.get('tag')
2351 if 'field' in arg:
2352 if tag:
2353 name = '%s <%s>' % (tag, arg['field'])
2354 else:
2355 name = arg['field']
2356 elif 'token' in arg:
2357 name = arg['token']
2358
2359 doc = arg.get('doc')
2360
2361 if doc and 'type' in arg:
2362 values = arg.get('values')
2363 if arg['type'] == 'enum' and values:
2364 if isinstance(values,str):
2365 self.add_docs(values,doc)
2366 else:
2367 for v in values:
2368 if doc and doc[-1] == '+':
2369 self.add_docs(v, doc[:-1] + v)
2370 else:
2371 self.add_docs(v, doc)
2372 return
2373
2374 if doc:
2375 self.add_docs(name, doc)
2376
2377
2378 def add_tuple_docs(self, tuple_arg):
2379 for t in tuple_arg:
2380 if type(t) == tuple:
2381 self.add_tuple_docs(t)
2382 elif type(t) == dict:
2383 self.add_dict_doc(t)
2384
2385 def add_command(self, command):
2386 self.commands.append(command)
2387 args = command.get('args')
2388 self.add_tuple_docs((args,))
2389
2390 def handle_first_matched_result(self, command):
2391 self.docs_stack.append({})
2392
2393 def handle_matched_result(self, command, result, arg_scopes):
2394 matched_doc = _lookup_in_scopes('doc', arg_scopes)
2395 if matched_doc and matched_doc[-1] == '+':
2396 item = result
2397 if type(item) == tuple:
2398 item = item[0]
2399 matched_doc = matched_doc[:-1] + item
2400 self.docs_stack[-1][command['self']] = (result, matched_doc)
2401
2402 def handle_incomplete_command(self, arg, arg_scopes, arg_data,
2403 fields, parsed_tag, command):
2404 tag = arg.get('tag')
2405 doc = arg.get('doc')
2406 doc_any = doc
2407 if doc_any == None:
2408 doc_any = _lookup_in_scopes('doc', arg_scopes)
2409 # note: doc may still be None
2410
2411 token = arg.get('token')
2412
2413 type_name = arg.get('type')
2414 if type_name == None:
2415 type_name = arg.get('base-type')
2416 typedef = typedef_registry.get(type_name) if type_name else None
2417
2418 field = arg.get('field')
2419 help_name = arg.get('help-name', field)
2420
2421 field_doc = arg.get('completion-text', False)
2422 if field_doc == None:
2423 field_doc = arg.get('help-name')
2424 if field_doc == None:
2425 field_doc = arg.get('syntax-help')
2426
2427 def manage_enum(arg, doc):
2428 enum_values = arg.get('values')
2429 if _is_string(enum_values):
2430 enum_values = [enum_values]
2431 for enum in enum_values:
2432 if doc and doc[-1] == '+':
2433 self.add_docs(enum, doc[:-1] + enum)
2434 else:
2435 self.add_docs(enum, doc)
2436
2437 if token != None: # token is a loner.
2438 self.add_docs(token.lower(), doc_any)
2439 elif tag and field and field_doc:
2440 self.add_docs(tag + ' <' + help_name + '>', doc if doc else field_doc)
2441 elif field and typedef and typedef.get('name') == 'enum':
2442 manage_enum(arg, doc)
2443 elif field and doc:
2444 self.add_docs(help_name, doc)
2445 elif tag and field and typedef:
2446 self.add_docs(tag + ' <' + help_name + '>', 'types|' + typedef.get('name'))
2447 elif field and field_doc:
2448 self.add_docs(help_name, field_doc)
2449 elif typedef:
2450 typedef_name = typedef.get('name').lower()
2451 if typedef_name != 'enum':
2452 if field:
2453 # magic reference to doc: types/<typename>
2454 self.add_docs(field, 'types|' + typedef.get('name'))
2455 else:
2456 self.add_docs(typedef.get('name').lower(), doc)
2457 else:
2458 manage_enum(arg, doc)
2459
2460 if command not in self.commands:
2461 self.commands.append(command)
2462
2463
2464 def handle_complete_command(self, prefix_matches, scopes, arg_data,
2465 fields, actions, command):
2466 # only provide help for exact command matches
2467 if len(prefix_matches):
2468 return
2469
2470 if command not in self.commands:
2471 if sdnsh.description:
2472 print 'COMPLETE COMMAND DOC', command['self'], arg_data, prefix_matches
2473 self.commands.append(command)
2474
2475 def handle_command_results_wiki(self):
2476 def handle_plaintext_wiki(inputstr):
2477 inputstr=inputstr.replace("{","\{")
2478 inputstr=inputstr.replace("}","\}")
2479 inputstr=inputstr.replace("[","\[")
2480 inputstr=inputstr.replace("]","\]")
2481 inputstr=inputstr.replace("\\\\","<br>")
2482 inputstr=inputstr.replace("\n\n", "\\@")
2483 inputstr=inputstr.replace("\n"," ")
2484 inputstr=inputstr.replace("\\@", "\n\n")
2485 inputstr=inputstr.replace("<br>", "\n")
2486
2487 return inputstr
2488
2489 if len(self.commands) == 0:
2490 if self.words:
2491 return 'No applicable command: %s' % ' '.join(self.words)
2492 else:
2493 return '\nNo command for ' + self.header
2494 help_lines=[]
2495 help_lines.append('')
2496 last_desc_name = None
2497 last_syntax_name = None
2498 # Keep track of paragraphs displayed to prevent repeated
2499 # display of the same doc tags.
2500 shown_items = []
2501 for command in self.commands:
2502 doc_tag = command.get('doc')
2503 name = _get_command_title(command)
2504 short_help = command.get('short-help')
2505 if short_help:
2506 if self.header:
2507 help_lines.append('\nh2. %s Command' % name.capitalize())
2508 help_lines.append('\nh4. %s' % short_help.capitalize())
2509 cmdmode=command.get('mode')
2510 if isinstance(cmdmode,list):
2511 cmdmodestr=''
2512 for cmdmodemem in cmdmode:
2513 if cmdmodemem[-1]=='*':
2514 cmdmodemem=cmdmodemem[:-1]
2515 cmdmodestr= cmdmodestr +' ' + cmdmodemem
2516 else:
2517 cmdmodestr=cmdmode
2518 if cmdmodestr[-1]=='*':
2519 cmdmodestr=cmdmodestr[:-1]
2520 help_lines.append('\n*Command Mode:* %s mode' % cmdmodestr)
2521 last_syntax_name = None # display syntax header
2522
2523 help_strings = []
2524 command_help_string = _get_command_syntax_help_string(
2525 command, self.prefix)
2526 help_strings.append(command_help_string)
2527 for h in sorted(help_strings):
2528 if isinstance(h,list):
2529 h = handle_plaintext_wiki(h[0])
2530 else:
2531 h = handle_plaintext_wiki(h)
2532 if last_syntax_name != name:
2533 help_lines.append('\n*Command Syntax:* {{%s}}' % h)
2534 last_syntax_name = name
2535 last_desc_name = None # print description header
2536
2537 if doc_tag:
2538 text = doc.get_text(self, doc_tag)
2539 if text != '' and doc_tag not in shown_items:
2540 shown_items.append(doc_tag)
2541 if last_desc_name != name:
2542 help_lines.append('\n*Command Description:*')
2543 last_desc_name = name
2544 text=handle_plaintext_wiki(text)
2545 help_lines.append(text)
2546 #dumb_formatter(help_lines, text, 3)
2547 last_syntax_name = None # print 'Syntax' header
2548 if len(self.commands) == 1:
2549 short_help = None
2550 if self.is_no_command:
2551 #
2552 # Cobble together a help string for a no command, build
2553 # a default value for subcommand's, possibly other command
2554 # descriptions
2555 #
2556 short_help = self.commands[0].get('no-help')
2557 if not short_help:
2558 command_type = self.commands[0].get('command-type')
2559 action = self.commands[0].get('action')
2560 if ((command_type and command_type == 'config-submode') or
2561 (action and action == 'push-mode-stack')):
2562 mode_name = self.commands[0].get('name')
2563 short_help = 'Remove %s configuration' % mode_name
2564 else:
2565 short_help = self.commands[0].get('short-help')
2566 for last_doc in self.docs_stack:
2567 if command['self'] in last_doc:
2568 (keyword, last_doc) = last_doc[command['self']]
2569 text = doc.get_text(self, last_doc)
2570 if type(keyword) == tuple:
2571 keyword = keyword[0]
2572 if sdnsh.description:
2573 help_lines.append("\t%s: %s %s" %
2574 (command['self'], keyword, last_doc))
2575 if text != '' and last_doc not in shown_items:
2576 shown_items.append(last_doc)
2577 help_lines.append('\nKeyword %s Description:' %
2578 keyword.capitalize())
2579 text=handle_plaintext_wiki(text)
2580 help_lines.append(text)
2581 #dumb_formatter(help_lines, text, 4)
2582
2583 if len(self.docs) > 0:
2584 help_lines.append('\n*Next Keyword Descriptions:*')
2585 for (doc_name, doc_tags) in self.docs.items():
2586 if len(doc_tags) == 1 and doc_tags[0] == None:
2587 if sdnsh.description:
2588 help_lines.append("\t%s: %s missing doc attribute" %
2589 (command['self'], doc_name))
2590 help_lines.append('* %s:' % doc_name)
2591 elif len(doc_tags) == 1 and doc_tags[0].split('|')[0] == 'types' and \
2592 not doc_name.startswith(doc_tags[0].split('|')[1]):
2593 type_name = doc_tags[0].split('|')[1].capitalize()
2594 help_lines.append(' %s: type %s' %
2595 (doc_name, type_name))
2596 else:
2597 help_lines.append('* %s:' % doc_name)
2598 for doc_tag in doc_tags:
2599 if sdnsh.description:
2600 help_lines.append("\t%s: %s %s" %
2601 (command['self'], doc_name, doc_tag))
2602 text = doc.get_text(self, doc_tag)
2603 if text == '':
2604 help_lines.append('')
2605 if text != '' and doc_tag not in shown_items:
2606 if len(doc_tags) > 1 and doc_tag.split('|')[0] == 'types':
2607 type_name = doc_tag.split('|')[1].capitalize()
2608 help_lines.append('\tType: %s' % type_name)
2609 shown_items.append(doc_tag)
2610 text=handle_plaintext_wiki(text)
2611 help_lines.append(text)
2612 #dumb_formatter(help_lines, text, 4)
2613 doc_example = command.get('doc-example')
2614 if doc_example:
2615 text = doc.get_text(self, doc_example)
2616 if text != '':
2617 help_lines.append('\n*Command Examples:*')
2618 help_lines.append('{noformat}')
2619 help_lines.append(text)
2620 #dumb_formatter(help_lines, text, 0)
2621 help_lines.append('{noformat}')
2622 return '\n'.join(help_lines)
2623
2624
2625 def handle_command_results(self):
2626 if len(self.commands) == 0:
2627 if self.words:
2628 return 'No applicable command: %s' % ' '.join(self.words)
2629 else:
2630 return '\nNo command for ' + self.header
2631
2632 help_lines = []
2633 # Deal with too much output
2634 if len(self.commands) > 10: # 10 is a magic number,
2635 help_lines.append('Help: Too many commands match (%d), '
2636 'Please choose more detail from item: ' %
2637 len(self.commands))
2638 help_lines.append(sdnsh.choices_text_builder(self.docs))
2639 return '\n'.join(help_lines)
2640
2641
2642 if self.header:
2643 help_lines.append(self.header)
2644 help_lines.append('')
2645
2646 (term_cols, term_rows) = sdnsh.pp.get_terminal_size()
2647 last_desc_name = None
2648 last_syntax_name = None
2649
2650 # all-help in intended to provide an overall short-help for
2651 # a collection of similarly syntax'ed commands.
2652 all_help = {}
2653 for command in self.commands:
2654 if command.get('all-help'):
2655 all_help[command.get('name')] = command.get('all-help')
2656 # when len(all_help) == 1, only ONE command collection group
2657 # is getting described
2658 if len(all_help) == 1:
2659 name = all_help.keys()[0]
2660 help_lines.append('%s Command: %s' %
2661 (name.capitalize(), all_help[name].capitalize()))
2662 all_help = True
2663 else:
2664 all_help = False
2665
2666 # Keep track of paragraphs displayed to prevent repeated
2667 # display of the same doc tags.
2668 shown_items = []
2669
2670 for command in self.commands:
2671
2672 doc_tag = command.get('doc')
2673 name = _get_command_title(command)
2674 short_help = command.get('short-help')
2675 if not all_help and short_help:
2676 help_lines.append('\n%s Command: %s\n' %
2677 (name.capitalize(), short_help.capitalize()))
2678 last_syntax_name = None # display syntax header
2679
2680 help_strings = []
2681 command_help_string = _get_command_syntax_help_string(
2682 command, self.prefix)
2683
2684 if type(command_help_string) == list:
2685 help_strings += command_help_string
2686 else:
2687 help_strings.append(command_help_string)
2688
2689 for h in sorted(help_strings):
2690 if last_syntax_name != name:
2691 if len(self.commands) > 1:
2692 help_lines.append(' Command Syntax:')
2693 else:
2694 help_lines.append('%s Command Syntax:' %
2695 name.capitalize())
2696 last_syntax_name = name
2697
2698 # use 75% of the width, since reformat_line isn't perfect.
2699 smaller_term_cols = (self.term_cols * 3) / 4
2700 if len(h) > smaller_term_cols:
2701 help_lines += reformat_line(h, smaller_term_cols, 2, 0)
2702 else:
2703 help_lines.append(' %s' % h)
2704 last_desc_name = None # print description header
2705
2706 if doc_tag:
2707 text = doc.get_text(self, doc_tag)
2708 if text != '' and doc_tag not in shown_items:
2709 shown_items.append(doc_tag)
2710 if last_desc_name != name:
2711 if self.format == None:
2712 help_lines.append('\n%s Command Description:' %
2713 name.capitalize())
2714 elif self.format == 'clidoc':
2715 help_lines.append('\n Description:')
2716 last_desc_name = name
2717 dumb_formatter(help_lines, text, 4)
2718 last_syntax_name = None # print 'Syntax' header
2719
2720
2721 if len(self.commands) > 1:
2722 for last_doc in self.docs_stack:
2723 if command['self'] in last_doc:
2724 (keyword, cmd_doc) = last_doc[command['self']]
2725 text = doc.get_text(self, cmd_doc)
2726 if text != '' and cmd_doc not in shown_items:
2727 shown_items.append(cmd_doc)
2728 if type(keyword) == tuple:
2729 keyword = keyword[0]
2730 help_lines.append('\nKeyword %s:' % keyword.capitalize())
2731 dumb_formatter(help_lines, text, 4)
2732
2733 doc_example = command.get('doc-example')
2734 if len(self.commands) > 1 and doc_example:
2735 text = doc.get_text(self, doc_example)
2736 if text != '' and doc_example not in shown_items:
2737 help_lines.append('Examples:')
2738 shown_items.append(doc_example)
2739 dumb_formatter(help_lines, text, 4)
2740
2741 if len(self.commands) == 1:
2742 short_help = None
2743 if self.is_no_command:
2744 #
2745 # Cobble together a help string for a no command, build
2746 # a default value for subcommand's, possibly other command
2747 # descriptions
2748 #
2749 short_help = self.commands[0].get('no-help')
2750 if not short_help:
2751 command_type = self.commands[0].get('command-type')
2752 action = self.commands[0].get('action')
2753 if ((command_type and command_type == 'config-submode') or
2754 (action and action == 'push-mode-stack')):
2755 mode_name = self.commands[0].get('name')
2756 short_help = 'Remove %s configuration' % mode_name
2757 else:
2758 short_help = self.commands[0].get('short-help')
2759
2760 for last_doc in self.docs_stack:
2761 if command['self'] in last_doc:
2762 (keyword, last_doc) = last_doc[command['self']]
2763 text = doc.get_text(self, last_doc)
2764 if type(keyword) == tuple:
2765 keyword = keyword[0]
2766 if sdnsh.description:
2767 help_lines.append("\t%s: %s %s" %
2768 (command['self'], keyword, last_doc))
2769 if text != '' and last_doc not in shown_items:
2770 shown_items.append(last_doc)
2771 help_lines.append('\nKeyword %s Description:' %
2772 keyword.capitalize())
2773 dumb_formatter(help_lines, text, 4)
2774
2775 if len(self.docs) > 0:
2776 if self.format == None:
2777 indent = ' '
2778 help_lines.append('\nNext Keyword Descriptions;')
2779 elif self.format == 'clidoc':
2780 indent = ' '
2781
2782 for (doc_name, doc_tags) in sorted(self.docs.items()):
2783 if len(doc_tags) == 1 and doc_tags[0] == None:
2784 if sdnsh.description:
2785 help_lines.append("\t%s: %s missing doc attribute" %
2786 (command['self'], doc_name))
2787 help_lines.append('%s%s:' % (indent, doc_name))
2788 elif len(doc_tags) == 1 and doc_tags[0].split('|')[0] == 'types' and \
2789 not doc_name.startswith(doc_tags[0].split('|')[1]):
2790 type_name = doc_tags[0].split('|')[1].capitalize()
2791 help_lines.append('%s%s: type %s' %
2792 (indent, doc_name, type_name))
2793 else:
2794 help_lines.append('%s%s:' % (indent, doc_name))
2795 for doc_tag in doc_tags:
2796 if sdnsh.description:
2797 help_lines.append("\t%s: %s %s" %
2798 (command['self'], doc_name, doc_tag))
2799 text = doc.get_text(self, doc_tag)
2800 if text == '':
2801 help_lines.append('')
2802 if text != '' and doc_tag not in shown_items:
2803 if len(doc_tags) > 1 and doc_tag.split('|')[0] == 'types':
2804 type_name = doc_tag.split('|')[1].capitalize()
2805 help_lines.append('\tType: %s' % type_name)
2806 shown_items.append(doc_tag)
2807 dumb_formatter(help_lines, text, 8)
2808
2809 doc_example = command.get('doc-example')
2810 if doc_example:
2811 text = doc.get_text(self, doc_example)
2812 if text != '':
2813 help_lines.append('Examples:')
2814 dumb_formatter(help_lines, text, 4)
2815
2816 if len(self.commands) > 1 and len(self.docs) > 0:
2817 if self.format == None:
2818 help_lines.append('\nNext Keyword Descriptions;')
2819 for (doc_name, doc_tags) in sorted(self.docs.items()):
2820 if len(doc_tags) == 1 and doc_tags[0] == None:
2821 if sdnsh.description:
2822 help_lines.append("\t%s: missing doc attribute" %
2823 command['self'])
2824 help_lines.append(' %s:' % doc_name)
2825 elif len(doc_tags) == 1 and doc_tags[0].split('|')[0] == 'types' and \
2826 not doc_name.startswith(doc_tags[0].split('|')[1]):
2827 type_name = doc_tags[0].split('|')[1].capitalize()
2828 help_lines.append(' %s: type %s' %
2829 (doc_name, type_name))
2830 else:
2831 help_lines.append(' %s:' % doc_name)
2832 for doc_tag in doc_tags:
2833 if doc_tag:
2834 if len(doc_tags) > 1 and doc_tag.split('|')[0] == 'types':
2835 type_name = doc_tag.split('|')[1].capitalize()
2836 help_lines.append('\tType: %s' % type_name)
2837 if sdnsh.description:
2838 help_lines.append("\t%s: %s %s" %
2839 (command['self'], doc_name, doc_tag))
2840 text = doc.get_text(self, doc_tag)
2841 if text != '' and doc_tag not in shown_items:
2842 shown_items.append(doc_tag)
2843 dumb_formatter(help_lines, text, 8)
2844 else:
2845 help_lines.append('')
2846 else:
2847 help_lines.append('')
2848
2849 return '\n'.join(help_lines)
2850
2851
2852def get_command_doc_tag(word):
2853 handler = CommandHandler()
2854 matching_commands = handler.get_matching_commands(word, False,
2855 command_registry)
2856 if len(matching_commands) == 1:
2857 return (matching_commands[0][0]).get('doc')
2858 if len(matching_commands) > 1:
2859 # error? retur value for multiple commands?
2860 pass
2861 return None
2862
2863
2864def get_command_documentation(words, header=None):
2865 handler = CommandDocumentor(header)
2866 # Hack to make "help no" work
2867 if len(words) == 1 and words[0] == 'no':
2868 words = words + ['']
2869 result = handler.handle_command(words)
2870 return result
2871
2872
2873def command_submode_dictionary(modes = None):
2874 if modes == None:
2875 modes = sdnsh.command_dict.keys() + sdnsh.command_nested_dict.keys()
2876 #
2877 # try to find all submode commands, build a dictionary
2878 reached_modes = []
2879 mode_nodes = {}
2880 mode_entries = []
2881 for c in command_registry:
2882 c_type = c.get('command-type')
2883 if c_type and c_type == 'config-submode':
2884 from_mode = c.get('mode')
2885 to_mode = c.get('submode-name')
2886 reached_modes.append(to_mode)
2887 if not from_mode in mode_nodes:
2888 mode_nodes[from_mode] = []
2889 mode_nodes[from_mode].append((to_mode, _get_command_title(c)))
2890 mode_entries.append({'mode' : from_mode,
2891 'submode' : to_mode,
2892 'command' : _get_command_title(c)})
2893 if sdnsh.description:
2894 print ', '.join(mode_nodes)
2895 print mode_nodes
2896 if [x for x in modes if not x in reached_modes]:
2897 print 'Missing ', [x for x in modes if not x in reached_modes]
2898
2899 return mode_entries
2900
2901
2902def get_clidoc(words):
2903 if len(words):
2904 handler = CommandDocumentor(header = None, format = 'clidoc')
2905 for word in words:
2906 for c in command_registry:
2907 if word == c['self']:
2908 handler.add_command(c)
2909 result = handler.handle_command_results()
2910 return result
2911 else:
2912 clidoc = []
2913 def commands_for_mode(mode):
2914 clidoc.append('\n\n\n============================= MODE ' + mode + '\n\n')
2915 for c in command_registry:
2916 if mode == c['mode']:
2917 handler = CommandDocumentor(header = '\n\n\n ---- MODE ' + mode,
2918 format = 'clidoc')
2919 handler.add_command(c)
2920 clidoc.append(handler.handle_command_results())
2921
2922 mode = 'login'
2923 commands_for_mode(mode)
2924
2925 # select commands by submode.
2926 mode_entries = command_submode_dictionary()
2927 for mode_entry in mode_entries:
2928 mode = mode_entry['submode']
2929 if mode[-1] == '*':
2930 mode = mode[:-1]
2931 if mode == 'config-':
2932 mode = 'config'
2933
2934 commands_for_mode(mode)
2935
2936 return ''.join(clidoc)
2937
2938def get_cliwiki(words):
2939 def wiki_special_character_handling(inputstr):
2940 inputstr=inputstr.replace('{','\{')
2941 inputstr=inputstr.replace('}','\}')
2942 inputstr=inputstr.replace('[','\[')
2943 inputstr=inputstr.replace(']','\]')
2944 return inputstr
2945 cliwikifile=open('cliwiki.txt', 'w')
2946 if not cliwikifile:
2947 print 'File creation failed \n'
2948 return
2949 cliwikifile.write('{toc:printable=true|style=disc|maxLevel=3|minLevel=1|class=bigpink|exclude=[1//2]|type=list|include=.*}')
2950 #write the introduction part
2951 introductionfile=open('documentation/en_US/introduction')
2952 str1=introductionfile.read()
2953 cliwikifile.write('\n')
2954 cliwikifile.write(str1)
2955 str1='\nh1. CLI Commands'
2956 cliwikifile.write(str1)
2957 #generate all commands in login/enable mode except show cmds
2958 mode=['login','enable']
2959 category=['show']
2960 for c in command_registry:
2961 name = c['name']
2962 if name not in category and c['mode'] in mode:
2963 category.append(name)
2964 handler = CommandDocumentor(header = 'login')
2965 for cc in command_registry:
2966 if name==cc['name'] and cc['mode'] in mode:
2967 handler.add_command(cc)
2968 str1 = handler.handle_command_results_wiki()
2969 str1= str1+ '\n'
2970 cliwikifile.write(str1)
2971 handler = CommandDocumentor(header = None)
2972 #generate all configuration commands: exclude internal commands
2973 mode=['config', 'config*']
2974 category=['internal'] #prune out all internal commands
2975 str1='\nh2. Configuration Commands'
2976 cliwikifile.write(str1)
2977 for c in command_registry:
2978 name = c['name']
2979 if name not in category and c['mode'] in mode:
2980 category.append(name)
2981 submode='config-' + name
2982 submode1=c.get('submode-name')
2983 if submode1 is not None:
2984 if submode1.find(submode)==-1:
2985 submode=submode1
2986 if name=='vns-definition':
2987 print submode
2988 str1="%s" % name
2989 str1=wiki_special_character_handling(str1)
2990 str1="\nh3. %s Commands " % str1.capitalize()
2991 cliwikifile.write(str1)
2992 handler = CommandDocumentor(header = None)
2993 handler.add_command(c)
2994 str1 = handler.handle_command_results_wiki()
2995 str1= str1+ '\n'
2996 cliwikifile.write(str1)
2997 for cc in command_registry:
2998 cmdmode=cc['mode']
2999 if (isinstance(cmdmode, str)):
3000 if (cmdmode.find(submode)!=-1) and (cc['name']!='show'):
3001 #special handling: prune vns command from tenant mode
3002 if (name != 'tenant') or ((name == 'tenant') and cmdmode.find('config-tenant-vns')==-1 and cmdmode.find('config-tenant-def-vns')==-1):
3003 handler = CommandDocumentor(header = None)
3004 handler.add_command(cc)
3005 str1 = handler.handle_command_results_wiki()
3006 str1= str1+ '\n'
3007 cliwikifile.write(str1)
3008 #generate all show commands
3009 name='show'
3010 str1='\nh2. Show Commands'
3011 cliwikifile.write(str1)
3012 mode=['config-internal'] #prune out all internal commands
3013 obj_type=[]
3014 for c in command_registry:
3015 if c['name']==name and c['mode'] not in mode:
3016 obj=c.get('obj-type')
3017 if obj and obj not in obj_type:
3018 obj_type.append(obj)
3019 str1="%s" % obj
3020 str1=wiki_special_character_handling(str1)
3021 str1="\nh3. Show %s Commands " % obj.capitalize()
3022 cliwikifile.write(str1)
3023 for cc in command_registry:
3024 if name==cc['name'] and obj==cc.get('obj-type') and cc['mode'] not in mode:
3025 handler = CommandDocumentor(header = None)
3026 handler.add_command(cc)
3027 str1 = handler.handle_command_results_wiki()
3028 str1= str1+ '\n'
3029 cliwikifile.write(str1)
3030
3031 cliwikifile.close()
3032 print 'CLI reference wiki markup file is generated successfully at: cli/cliwiki.txt.\n '
3033
3034#
3035# Lint: Verify requested commands
3036#
3037class CommandLint(CommandHandler):
3038
3039 def __init__(self):
3040 super(CommandLint, self).__init__()
3041 self.commands = []
3042
3043 def handle_incomplete_command(self, arg, arg_scopes, arg_data, fields, parsed_tag, command):
3044 if command not in self.commands:
3045 self.commands.append(command)
3046
3047 def handle_complete_command(self, prefix_matches, scopes, arg_data, fields, actions, command):
3048 if command not in self.commands:
3049 self.commands.append(command)
3050
3051 def lint_action_query_table(self, c_self, command, c_scope):
3052 # look for items which are not parameters
3053 attrs = ['proc', 'key', 'scoped', 'sort', 'crack', 'obj-type', 'proc']
3054 for a in c_scope:
3055 if not a in attrs:
3056 print '%s: unknown attribute %s for query-table' % (c_self, a)
3057 req = ['obj-type']
3058 for r in req:
3059 if not r in c_scope:
3060 print '%s: missing required attribute %s for query-table' % (
3061 c_self, r)
3062 if 'obj-type' in c_scope:
3063 if not mi.obj_type_exists(c_scope['obj-type']):
3064 print '%s: query-table no such obj-type %s' % (
3065 c_self, c_scope['obj-type'])
3066
3067 def lint_action_query_rest(self, c_self, command, c_scope):
3068 # look for items which are not parameters
3069 attrs = ['proc', 'url', 'rest-type', 'key', 'scoped', 'sort', 'append']
3070 for a in c_scope:
3071 if not a in attrs:
3072 print '%s: unknown attribute %s for query-rest' % (c_self, a)
3073 req = ['url']
3074 for r in req:
3075 if not r in c_scope:
3076 print '%s: missing required attribute %s for query-rest' % (
3077 c_self, r)
3078 if 'append' in c_scope:
3079 if type(c_scope['append']) != dict:
3080 print '%s: append parameter in query_rest' \
3081 'required to be a dict' % (c_self)
3082
3083 def lint_action_join_table(self, c_self, command, c_scope):
3084 # look for items which are not parameters
3085 attrs = ['proc', 'obj-type', 'key', 'key-value', 'add-field', 'join-field', 'crack']
3086 for a in c_scope:
3087 if not a in attrs:
3088 print '%s: unknown attribute %s for join-table' % (c_self, a)
3089 req = ['obj-type', 'key', 'join-field']
3090 for r in req:
3091 if not r in c_scope:
3092 print '%s: missing required attribute %s for query-table' % (
3093 c_self, r)
3094 if 'obj-type' in c_scope:
3095 if not mi.obj_type_exists(c_scope['obj-type']):
3096 print '%s: join-table no such obj-type %s' % (
3097 c_self, c_scope['obj-type'])
3098
3099 def lint_dict_action(self, c_self, command, c_action, c_scope):
3100 if not 'proc' in c_action:
3101 print '%s: "proc" expected for dict in action' % c_self
3102 return
3103 action = c_action['proc']
3104 if not action in action_registry:
3105 print '%s: action %s unknown' % (c_self, c_action)
3106 if c_action == 'display-rest':
3107 if not 'url' in c_action:
3108 print '%s: missing "url" for display-rest' % c_self
3109 if action == 'display-table':
3110 if not 'obj-type' in c_action:
3111 # could be in c_scope, or other nests.
3112 print '%s: missing "obj-type" for display-table' % c_self
3113 if action in ['query-table', 'query-table-append']:
3114 self.lint_action_query_table(c_self, command, c_action)
3115 if action in ['query-rest', 'query-table-rest']:
3116 self.lint_action_query_rest(c_self, command, c_action)
3117 if action == 'join-table':
3118 self.lint_action_join_table(c_self, command, c_action)
3119 if action == 'legacy-cli':
3120 print '%s: LEGACY-CLI, consider reimplementation' % c_self
3121
3122 def lint_action(self, c_self, command, c_action, c_type, c_scope):
3123 if type(c_action) == tuple:
3124 for t in c_action:
3125 if type(t) == list:
3126 print '%s: LIST nost supported as a member of actions' % c_self
3127 elif type(t) == dict:
3128 self.lint_dict_action(c_self, command, t, c_scope)
3129
3130 if type(c_action) == dict:
3131 self.lint_dict_action(c_self, command, c_action, c_scope)
3132 if type(c_action) == str:
3133 if not c_action in action_registry:
3134 print '%s: action %s unknown' % (c_self, c_action)
3135 if c_action == 'display-rest':
3136 if not 'url' in c_scope:
3137 print '%s: missing "url" for display-rest' % c_self
3138 if c_action in ['query-table', 'query-table-append']:
3139 self.lint_action_query_table(c_self, command, c_scope)
3140 if c_action in ['query-rest', 'query-table-rest']:
3141 self.lint_action_query_rest(c_self, command, c_scope)
3142 if c_action == 'join-table':
3143 self.lint_action_join_table(c_self, command, c_scope)
3144 if c_action == 'legacy-cli':
3145 print '%s: LEGACY-CLI, consider reimplementation' % c_self
3146
3147 def lint_choice(self, c_self, command, c_choice, c_type):
3148 for choice in c_choice:
3149 if type(choice) == tuple:
3150 # in order collection of terms, these aren't choices, but
3151 # here e can treat them that way
3152 for t in choice:
3153 if type(t) == list:
3154 print '%s: LIST nost supported as a member of choices' % c_self
3155 elif type(t) == dict:
3156 self.lint_choice(c_self, command, (t,), c_type)
3157 elif type(t) == str:
3158 # token, ought to validate char's in token
3159 pass
3160 else:
3161 print '%s: bad element type -> %s' % (c_self, type(t))
3162
3163 if 'command-type' in choice:
3164 print '%s: CHOICE contains "command-type", only' \
3165 ' intended to be used at top level' % c_self
3166 if not choice['command-type'] in command_type_defaults:
3167 print '%s: missing command-type %s' % (c_self, choice['command-type'])
3168 if 'action' in choice:
3169 self.lint_action(c_self, command, choice['action'], c_type, choice)
3170 if 'choices' in choice:
3171 self.lint_choice(c_self, command, choice['choices'], c_type)
3172
3173 def lint_command(self,command):
3174 c_self = command.get('self', 'UNKNOWN SELF NANE')
3175 print command['self']
3176
3177 c_name = command.get('name', None)
3178 if c_name == None:
3179 print '%s: no name defined' % c_self
3180
3181 c_mode = command.get('mode', None)
3182 if c_mode == None:
3183 print '%s: no submode defined' % c_self
3184
3185 c_short_help = command.get('short-help', None)
3186 if c_short_help == None:
3187 print '%s: no short-help defined' % c_self
3188
3189 c_obj_type = command.get('obj-type', None)
3190
3191 c_current_mode_obj_id = command.get('current-mode-obj-id', None)
3192 if 'current-mode-obj-id' in command:
3193 if c_current_mode_obj_id == None:
3194 print '%s: "current-mode-obj-id" not needed at top level' % c_self
3195
3196 c_parent_id = command.get('parent-id', None)
3197 if 'parent-id' in command:
3198 if c_parent_id == None:
3199 print '%s: "parent-id" not needed at top level' % c_self
3200
3201 c_type = command.get('command-type', None)
3202
3203 c_args = command.get('args', None)
3204 if c_args == None:
3205 print '%s: no args defined' % c_args
3206
3207 c_action = command.get('action', None)
3208
3209 if c_action or c_type:
3210 self.lint_action(c_self, command, c_action, c_type, command)
3211
3212 if type(c_args) == dict:
3213 if 'action' in c_args:
3214 self.lint_action(c_self, command, c_args['action'], c_type, c_args)
3215 if 'choices' in c_args:
3216 self.lint_choice(c_self, command, c_args['choices'], c_type)
3217 if 'choice' in c_args:
3218 print '%s: "choices" not "choice"' % c_self
3219 elif type(c_args) == tuple or type(c_args) == list:
3220 for a in c_args:
3221 if 'action' in a:
3222 self.lint_action(c_self, command, a['action'], c_type, a)
3223 if 'choices' in a:
3224 self.lint_choice(c_self, command, a['choices'], c_type)
3225 if 'choice' in a:
3226 print '%s: "choices" not "choice"' % c_self
3227 if 'field' in a:
3228 if c_obj_type:
3229 if not mi.obj_type_has_field(c_obj_type, a['field']):
3230 print '%s: %s MISSING FIELD %s' % (c_self,
3231 c_obj_type, a['field'])
3232
3233
3234 def handle_command_results(self):
3235 if len(self.commands) == 0:
3236 return 'No applicable command: %s' % ' '.join(self.words)
3237
3238 for command in self.commands:
3239 self.lint_command(command)
3240
3241 return '--'
3242
3243
3244def lint_command(words):
3245 lint = CommandLint()
3246 if len(words) == 0:
3247 for command in command_registry:
3248 lint.lint_command(command)
3249 else:
3250 for command in command_registry:
3251 if command['self'] in words:
3252 lint.lint_command(command)
3253 return ''
3254
3255
3256class CommandPermutor(CommandHandler):
3257
3258 def __init__(self, qualify = False):
3259 """
3260 @param qualify Generate qualify version of the permutations
3261 """
3262 super(CommandPermutor, self).__init__()
3263 self.commands = []
3264 self.collect = []
3265 self.qualify = qualify
3266
3267 self.obj_type = None
3268 self.obj_type_other = None
3269
3270
3271 def collect_complete_command(self, line):
3272 if self.qualify:
3273 # collect together parts, when a token appears,
3274 # replace the token with a procedure call.
3275 if len(line) == 0:
3276 return
3277 new_line = ['"']
3278 quoted = True
3279 if line[0][0] == '+': # unusual
3280 new_line = ['%s ' % line[0][1:]]
3281 line = line[1:]
3282 quoted = False
3283 for item in line:
3284 if quoted:
3285 if item[0] == '+':
3286 new_line.append('" + %s ' % item[1:])
3287 quoted = False
3288 else:
3289 new_line.append('%s ' % item)
3290 else:
3291 if item[0] == '+':
3292 new_line.append(' + " " + %s ' % item[1:])
3293 quoted = False
3294 else:
3295 new_line.append('+ " %s ' % item)
3296 quoted = True
3297 if quoted:
3298 new_line.append('"')
3299
3300 self.collect.append(''.join(new_line))
3301 else:
3302 self.collect.append(' '.join(line))
3303
3304
3305 def field_to_generator(self, field):
3306 """
3307 Convert the field name to a text field.
3308 When 'qualify' is set, replace the field name
3309 with a likely procedure to call instead
3310 """
3311 if self.qualify:
3312 # Many of the fields are actually fields in
3313 # the named obj-type. Take a shot at seeing if that's
3314 # the case, and call a sample value collector
3315
3316 if self.obj_type_other:
3317 # These are typically an obj_type|field names.
3318 parts = self.obj_type_other.split('|')
3319 o_field = field
3320 if len(parts) > 1:
3321 o_field = parts[1]
3322 # by using obj_type_has_model() instead of
3323 # obj_type_exists(), 'curl(1)' can be used in
3324 # the testing to find sample objects.
3325 if mi.obj_type_has_model(parts[0]):
3326 sample_obj_type = 'sample_obj_type'
3327 if self.is_no_command:
3328 sample_obj_type = 'sample_no_obj_type'
3329 if mi.obj_type_has_field(parts[0], o_field):
3330 return '+id.%s("%s", "%s")' \
3331 % (sample_obj_type, parts[0], o_field)
3332
3333 python_field = field.replace("-", "_")
3334 if self.obj_type:
3335 sample_obj_type = 'sample_obj_type'
3336 if self.is_no_command:
3337 sample_obj_type = 'sample_no_obj_type'
3338 if mi.obj_type_has_field(self.obj_type, field):
3339 return '+id.%s("%s", "%s")' \
3340 % (sample_obj_type, self.obj_type, field)
3341 else:
3342 return '+id.%s("%s")' % (python_field, self.obj_type)
3343 else:
3344 return '+id.%s()' % python_field
3345
3346 else:
3347 return "<%s>" % field
3348
3349
3350 def permute_item(self, item, line, next_args):
3351 """
3352 Deals with a single dictionary item. This can be:
3353 a choice, an arg, a token, or a field.
3354 """
3355
3356 if type(item) == str:
3357 line.append(item)
3358 self.permute_in_order(next_args[0], line, next_args[1:])
3359 return
3360
3361 if self.is_no_command:
3362 optional = item.get('optional-for-no', False)
3363 else:
3364 optional = item.get('optional', False)
3365
3366
3367 skip = item.get('permute') # XXX permute vs qualify skip?
3368
3369 if skip != 'skip' and optional:
3370 if len(next_args):
3371 self.permute_in_order(None, line, next_args)
3372 else:
3373 self.collect_complete_command(line)
3374
3375 choices = item.get('choices')
3376 if choices:
3377 self.permute_choices(choices, list(line), next_args)
3378 return
3379
3380 if skip == 'skip':
3381 return
3382
3383 token = item.get('token')
3384 tag = item.get('tag')
3385 field = item.get('field')
3386 args = item.get('args')
3387 obj_type = item.get('obj-type')
3388 if obj_type:
3389 self.obj_type = obj_type
3390
3391 # must do this after the recursive 'optional' calls.
3392 self.obj_type_other = item.get('other')
3393
3394 if args:
3395 if type(args) == dict:
3396 self.permute_item(args, line, next_args)
3397 elif type(args) == tuple:
3398 self.permute_in_order(args, list(line), next_args)
3399 else:
3400 print 'TYPE ARGS ', type(args)
3401 return
3402 elif token:
3403 line.append(token)
3404 elif field:
3405 item_type = item.get('type')
3406 if item_type == None:
3407 item_type = item.get('base-type')
3408 if item_type == 'enum':
3409 values = item.get('values')
3410 if values:
3411 if tag:
3412 line.append(tag)
3413 # convert the values into a tuple of dicts,
3414 # so that permute_choices can manage it
3415 if type(values) == str:
3416 values = [values]
3417 choices = tuple([{'token' : x} for x in values])
3418 self.permute_choices(choices, list(line), next_args)
3419 return
3420 else:
3421
3422 if tag:
3423 line.append(tag)
3424 line.append(self.field_to_generator(field))
3425
3426 #
3427 # fall-through to this common code, which should only be
3428 # reached when no other recursive descent is performed.
3429
3430 if len(next_args):
3431 self.permute_in_order(None, line, next_args)
3432 else:
3433 self.collect_complete_command(line)
3434
3435
3436 def permute_choices(self, choices, line, next_args):
3437 """
3438 When a 'choices': is reached, enumerate all the choices,
3439 and continue forward.
3440 """
3441 for pick in choices:
3442 if type(pick) == dict:
3443 new_line = list(line)
3444 self.permute_item(pick, new_line, next_args)
3445 if next_args == None:
3446 self.collect_complete_command(line)
3447 elif type(pick) == tuple:
3448 self.permute_in_order(pick, list(line), next_args)
3449 else:
3450 print "CHOICE? ", type(pick)
3451
3452
3453 def permute_in_order(self, items, line, next_args):
3454 if items == None and len(next_args):
3455 items = next_args
3456 next_args = []
3457 if len(items) == 0:
3458 # done
3459 self.collect_complete_command(line)
3460 return
3461 # see if the item is optional, if so then don't add the item,
3462 # and go to the next item in a recursive call
3463
3464 while len(items) and type(items[0]) == str:
3465 line.append(items[0])
3466 items = items[1:]
3467
3468 if len(items):
3469 item = items[0]
3470
3471 if len(items) > 1:
3472 if len(next_args) == 0:
3473 next_args = items[1:]
3474 else:
3475 if type(items[1:]) == tuple:
3476 next_args = list(items[1:]) + next_args
3477 else:
3478 next_args = items[1:] + next_args
3479 else:
3480 next_args = []
3481
3482 token = False
3483 if type(item) == tuple:
3484 self.permute_in_order(item, line, list(next_args))
3485 elif type(item) == dict:
3486 self.permute_item(item, list(line), list(next_args))
3487 else:
3488 print 'permute_in_order: need type', type(item)
3489 else:
3490 self.collect_complete_command(line)
3491
3492 return
3493
3494 def permute_command_common(self, command):
3495 args = command.get('args')
3496 if args == None:
3497 if sdnsh.description:
3498 print 'PERMUTE: missing args for %s' % command['self']
3499 return
3500 name = _get_command_title(command)
3501
3502 obj_type = command.get('obj-type')
3503 if obj_type:
3504 self.obj_type = obj_type
3505
3506 if type(command.get('name')) == dict:
3507 if self.qualify:
3508 name = self.field_to_generator(command['name']['field'])
3509
3510 if self.is_no_command:
3511 line = ['no', name]
3512 else:
3513 line = [name]
3514
3515 if type(args) == tuple:
3516 self.permute_in_order(args, line, [])
3517 elif type(args) == dict:
3518 self.permute_item(args, line, [])
3519 else:
3520 print 'PC ', type(args)
3521
3522 return '\n'.join(self.collect)
3523
3524
3525 def permute_command(self, command):
3526 print 'PERMUTE', command['self'], is_no_command_supported(command)
3527
3528 self.is_no_command = False
3529 result = self.permute_command_common(command)
3530
3531 if is_no_command_supported(command):
3532 # Now add the command for 'no'
3533 self.is_no_command = True
3534 result = self.permute_command_common(command)
3535
3536 return result
3537
3538
3539 def handle_command_results(self):
3540 collections = []
3541 for command in self.commands:
3542 collections.append(self.permute_command(command))
3543 return '\n'.join(collections)
3544
3545
3546 def handle_command(self, words):
3547 for word in words:
3548 for c in command_registry:
3549 if word == c['self']:
3550 return self.permute_command(c)
3551
3552
3553def permute_single_submode(submode_command, qualify):
3554 """
3555 Permute command for a submode, takes the command, finds
3556 all the related submodesmodes, permutes all the commands
3557 in the submode, then a recursive call to any submodes
3558 found within this one.
3559
3560 @param submode_command command dictionary of the submode command.
3561 Can be set to None as an indication of "root"
3562
3563 @param qualify boolean, describes whether to generate "permute"
3564 output or "qualify" output. Permute output is a human readable
3565 command permutation, while "qualify" is intended for script building
3566
3567 """
3568 permuted = []
3569 permute_submodes = []
3570
3571 if submode_command == None:
3572 mode = 'login'
3573 else:
3574 # first, the submode. There ought to be only one permutation.
3575 permute = CommandPermutor(qualify)
3576 permuted.append(permute.permute_command(submode_command))
3577 mode = submode_command.get('submode-name')
3578 print 'SUBMODE PERMUTE', submode_command['self'], 'MODE', mode
3579
3580 def submode_match(want, cmd):
3581 if type(cmd) == str:
3582 cmd = [cmd]
3583 if want.startswith('config'):
3584 for c in cmd:
3585 if c.endswith('*'):
3586 c = c[:-1]
3587 if c == 'config-':
3588 c = 'config'
3589 if want == c:
3590 return True
3591 else:
3592 return False
3593 for c in cmd:
3594 if want == c:
3595 return True
3596 return False
3597
3598 # there's no command to enter login.
3599 for c in command_registry:
3600 if submode_match(mode, c['mode']):
3601 # no submode commands.
3602 if c.get('command-type') == 'config-submode':
3603 print 'SUBMODE POSTPONE', c['self']
3604 permute_submodes.append(c)
3605 else:
3606 permute = CommandPermutor(qualify)
3607 permuted.append(permute.permute_command(c))
3608
3609 # now any submodes found
3610 for c in permute_submodes:
3611 permuted.append(permute_single_submode(c, qualify))
3612
3613 return '\n'.join(permuted)
3614
3615
3616def permute_command(words, qualify):
3617 """
3618 Permute all the commands (with no parameters), or a
3619 sigle command. The command is named via the name
3620 of the dictionary, for example:
3621 permute COMMAND_DESCRIPTION
3622 """
3623 if len(words) == 0:
3624 return permute_single_submode(None, qualify)
3625 else:
3626 permute = CommandPermutor(qualify)
3627 return permute.handle_command(words)
3628
3629
3630def _is_string(arg):
3631 """
3632 Returns whether or not the argument is a string (either a "str" or "unicode")
3633 """
3634 return isinstance(arg, types.StringTypes)
3635
3636
3637def _is_list(arg):
3638 """
3639 Returns whether or not the argument is a list. This means that it's an
3640 instance of a sequence, but not including the string types
3641 """
3642 return isinstance(arg, collections.Sequence) and not _is_string(arg)
3643
3644
3645def _lookup_in_scopes(name, scopes, default=None):
3646 """
3647 Look up the given name in the given list of scopes.
3648 Returns the default value if the name is not defined in any of the scopes.
3649 """
3650 assert name
3651 assert scopes is not None
3652
3653 # We iterate over the items in the scope (rather than using 'get')
3654 # so we can do a case-insensitive lookup.
3655 name_lower = name.lower()
3656 for scope in scopes:
3657 for key, value in scope.items():
3658 if key.lower() == name_lower:
3659 return value
3660 return default
3661
3662
3663def _call_proc(proc_descriptor, proc_registry, scopes, command):
3664 assert proc_descriptor is not None
3665 assert proc_registry is not None
3666
3667 if isinstance(proc_descriptor, collections.Sequence) and not _is_string(proc_descriptor):
3668 proc_descriptors = proc_descriptor
3669 else:
3670 proc_descriptors = (proc_descriptor,)
3671
3672 combined_result = None
3673
3674 saved_scopes = scopes
3675
3676 for proc_descriptor in proc_descriptors:
3677 args_descriptor = None
3678 scopes = saved_scopes
3679 if isinstance(proc_descriptor, collections.Mapping):
3680 # Look up the proc in the specified registry
3681 proc_name = proc_descriptor.get('proc')
3682 if not proc_name:
3683 raise error.CommandDescriptionError('Unspecified proc name: ',
3684 command)
3685 if proc_descriptor.get('args') or proc_descriptor.get('kwargs'):
3686 args_descriptor = proc_descriptor
3687 scopes = [proc_descriptor] + scopes
3688 else:
3689 proc_name = proc_descriptor
3690
3691 # Push an empty scope on the scope stack to hold a variable for the scopes
3692 # FIXME: This should be cleaned up when we change the calling conventions
3693 # for procs.
3694 scopes = [{}] + scopes
3695 scopes[0]['scopes'] = scopes
3696
3697 proc_and_arg = proc_registry.get(proc_name)
3698 if not proc_and_arg:
3699 raise error.CommandDescriptionError('Unknown proc name: %s' % proc_name, command)
3700 proc, default_args_descriptor = proc_and_arg
3701
3702 if not args_descriptor:
3703 args_descriptor = default_args_descriptor
3704
3705 converted_args = []
3706 converted_kwargs = {}
3707
3708 # Convert the positional args
3709 args = _get_args(args_descriptor)
3710 if args:
3711 for arg in args:
3712 if _is_string(arg) and len(arg) > 1 and arg[0] == '$':
3713 name = arg[1:]
3714 for scope in scopes:
3715 if name in scope:
3716 value = scope[name]
3717 break
3718 else:
3719 # FIXME: Disabling treating args that can't be resolved as errors, so that
3720 # they can instead be interpreted as just using the default value for the
3721 #raise error.CommandDescriptionError('Invalid proc argument: %s ' %
3722 # name, command)
3723 continue
3724 converted_args.append(value)
3725
3726 # Convert the keyword args
3727 kwargs = args_descriptor.get('kwargs')
3728 if kwargs:
3729 for key, value in kwargs.iteritems():
3730 if _is_string(value) and len(value) > 1 and value[0] == '$':
3731 name = value[1:]
3732 for scope in scopes:
3733 if name in scope:
3734 value = scope[name]
3735 break
3736 else:
3737 # FIXME: Don't treat args that can't be resolved as errors, so that
3738 # they can instead be interpreted as just using the default value for the
3739 #raise error.CommandDescriptionError('Invalid proc argument: %s ' %
3740 # name, command)
3741 continue
3742 converted_kwargs[key] = value
3743
3744 # Call the proc
3745 # pylint: disable=W0142
3746 result = proc(*converted_args, **converted_kwargs)
3747
3748 if result is not None:
3749 combined_result = combined_result + result if combined_result else result
3750
3751 return combined_result
3752
3753
3754def _add_applicable_modes(command, mode_dict):
3755 """
3756 Helper function for _get_applicable_modes to add any modes for this command
3757 """
3758 feature = command.get('feature')
3759 if feature:
3760 if not sdnsh.feature_enabled(feature):
3761 return
3762
3763 mode = command.get('mode')
3764 if mode:
3765 if type(mode) == list:
3766 for m in mode:
3767 mode_dict[m] = None
3768 else:
3769 mode_dict[mode] = None
3770
3771
3772def _get_applicable_modes(command):
3773 """
3774 Returns a list of all of the modes that are specified for this command
3775 """
3776 mode_dict = {}
3777 _add_applicable_modes(command, mode_dict)
3778 return mode_dict.keys()
3779
3780
3781def _exact_mode_match(current_mode, command_modes):
3782 """
3783 Return True when this command_mode is an exact match
3784 for this current mode (used to partition list of commands
3785 into two groups, one exact level, and for for related mode
3786 matches)
3787 """
3788 if not type(command_modes) == list:
3789 command_modes = [command_modes]
3790 for mode in command_modes:
3791 if mode == current_mode:
3792 return True
3793 if mode.endswith('*') and mode[:-1] == current_mode:
3794 return True
3795 return False
3796
3797
3798def _match_current_modes(command, current_mode, modes):
3799 """
3800 Even when the current mode isn't in the list of modes,
3801 there's a few modes in the mode list which are intended to
3802 be a collection of modes, eg: "config*", and "config-*",
3803 For any mode which ends in an ('*'), the current mode
3804 will match when the current mode is prefix of the mode
3805 (without the last '*' character)
3806 'config*' is intended to match any config mode, while
3807 'config-*' is intended to match any config submode.
3808 """
3809 if current_mode in modes:
3810 return True
3811 #
3812 # if the modes is enable, this works everywhere
3813 #
3814 if 'login' in modes:
3815 return True
3816 #
3817 # if the modes is login, and the mode is anything but login,
3818 # then this is true
3819 #
3820 if 'enable' in modes and current_mode != 'login':
3821 return True
3822 for mode in modes:
3823 if mode.endswith('*') and current_mode.startswith(mode[:-1]):
3824 return True
3825 #if command.get('command-type') == 'config-submode':
3826 # for mode in modes:
3827 # if current_mode.startswith(mode):
3828 # print "_match_current_modes: current command type is config-submode",current_mode,mode
3829 # return True
3830
3831 return False
3832
3833
3834def _get_command_title(command):
3835 if type(command['name']) == str:
3836 return command['name']
3837 if type(command['name']) == dict:
3838 return command['name']['title']
3839 else:
3840 raise Exception("Command %s has unknown title" % command['name'])
3841
3842
3843def _lookup_command_candidates(command_prefix, command_list):
3844 """
3845 Returns the list of command candidates from the given command list.
3846 A candidate must have a 'mode' value that matches the current mode,
3847 and its name must begin with the given command prefix.
3848 """
3849 candidates = []
3850 current_mode = sdnsh.current_mode()
3851 try:
3852 for command in command_list:
3853 modes = _get_applicable_modes(command)
3854 if _match_current_modes(command, current_mode, modes):
3855 name = command['name']
3856 if (type(name) == str and
3857 name.startswith(command_prefix.lower())):
3858 candidates.append(command)
3859 # should check the type of command_prefix,
3860 # and for str, ame.match(command_prefix):
3861 if type(name) == dict:
3862 if 're' not in name:
3863 command['name']['re'] = re.compile(name['pattern'])
3864 if name['re'].match(command_prefix):
3865 candidates.append(command)
3866 if type(name) == dict and \
3867 name['re'](command_prefix):
3868 candidates.append(command)
3869
3870 except Exception, _e:
3871 raise error.CommandDescriptionError('Missing mode or name: ', command)
3872
3873 return candidates
3874
3875def do_command(words):
3876 executor = CommandExecutor()
3877 result = executor.handle_command(words)
3878 return result
3879
3880def init_command(bs, modi):
3881 # FIXME HACK: sdnsh global to access top-level CLI object
3882 global sdnsh, mi
3883 sdnsh = bs
3884 mi = modi
3885
3886 c_actions.init_actions(bs, modi)
3887 c_data_handlers.init_data_handlers(bs, modi)
3888 c_completions.init_completions(bs, modi)
3889 c_validations.init_validations(bs, modi)
3890
3891
3892 add_command_type('config', {
3893 'action': 'write-fields',
3894 'no-action': 'reset-fields'
3895 })
3896
3897 add_command_type('config-object', {
3898 'action': 'write-object',
3899 'no-action': 'delete-objects',
3900 })
3901
3902 add_command_type('config-submode', {
3903 'action': 'push-mode-stack',
3904 #'no-action': 'delete-child-objects',
3905 'no-action': 'delete-objects',
3906 })
3907
Srikanth Vavilapallib5c3ca52014-12-15 15:59:33 -08003908 add_command_type('create-tunnelset', {
3909 'action': 'create-tunnelset'
3910 })
3911
3912 add_command_type('remove-tunnelset', {
3913 'action': 'remove-tunnelset'
3914 })
3915
Srikanth Vavilapalli1725e492014-12-01 17:50:52 -08003916 add_command_type('create-tunnel', {
3917 'action': 'create-tunnel'
3918 })
3919
3920 add_command_type('remove-tunnel', {
3921 'action': 'remove-tunnel'
3922 })
3923
3924 add_command_type('create-policy', {
3925 'action': 'create-policy'
3926 })
3927
3928 add_command_type('remove-policy', {
3929 'action': 'remove-policy'
3930 })
3931
3932 add_command_type('display-table', {
3933 'action': 'display-table'
3934 })
3935
3936 add_command_type('display-rest', {
3937 'action': 'display-rest'
3938 })
3939
3940 add_command_type('manage-alias', {
3941 'action' : 'create-alias',
3942 'no-action' : 'delete-alias',
3943 })
3944
3945 add_command_type('manage-tag', {
3946 'action' : 'create-tag',
3947 'no-action' : 'delete-tag',
3948 })
3949
3950 add_command_type('update-config', {
3951 'action' : 'update-config',
3952 'no-action' : 'update-config',
3953 })
3954
3955 # Initialize typedefs
3956 add_typedef({
3957 'name': 'string',
3958 'validation': 'validate-string'
3959 })
3960
3961 add_typedef({
3962 'name': 'integer',
3963 'validation': 'validate-integer'
3964 })
3965
3966 add_typedef({
3967 'name': 'hex-or-decimal-integer',
3968 'validation': 'validate-hex-or-dec-integer'
3969 })
3970
3971 add_typedef({
3972 'name': 'date',
3973 'validation': 'validate-date'
3974 })
3975
3976 add_typedef({
3977 'name': 'duration',
3978 'validation': 'validate-duration'
3979 })
3980
3981 add_typedef({
3982 'name': 'enum',
3983 'validation': 'validate-enum'
3984 })
3985
3986 add_typedef({
3987 'name' : 'mac-address',
3988 'help-name' : 'MAC Address',
3989 'base-type' : 'string',
3990 'validation' : 'validate-mac-address',
3991 })
3992
3993 add_typedef({
3994 'name' : 'host',
3995 'help-name' : 'Host Alias or MAC Address',
3996 'base-type' : 'string',
3997 'validation' : 'validate-host',
3998 })
3999
4000 add_typedef({
4001 'name' : 'vlan',
4002 'help-name' : 'Vlan',
4003 'base-type' : 'integer',
4004 'validation' : 'validate-integer',
4005 })
4006
4007 add_typedef({
4008 'name' : 'label',
4009 'help-name' : 'Segment Label',
4010 'base-type' : 'integer',
4011 'validation' : 'validate-integer',
4012 })
4013
4014 add_typedef({
4015 'name' : 'dpid',
4016 'help-name' : 'switch id (8-hex bytes)',
4017 'base-type' : 'string',
4018 'validation' : 'validate-switch-dpid',
4019 })
4020
4021 add_typedef({
4022 'name' : 'ip-address',
4023 'help-name' : 'IP address (dotted quad)',
4024 'base-type' : 'string',
4025 'pattern' : r'^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$',
4026 })
4027
4028 add_typedef({
4029 'name' : 'ip-address-not-mask',
4030 'help-name' : 'IP Address',
4031 'base-type' : 'string',
4032 'pattern' : r'^(\d{1,3}\.){3}\d{1,3}$',
4033 'validation' : 'validate-ip-address-not-mask'
4034 })
4035
4036 add_typedef({
4037 'name' : 'cidr-range',
4038 'help-name' : 'cidr range (ip-address/int)',
4039 'base-type' : 'string',
4040 'pattern' : r'^(\d{1,3}\.){3}\d{1,3}/\d{1,2}?$',
4041 'validation' : 'validate-cidr-range',
4042 })
4043
4044 add_typedef({
4045 'name' : 'netmask',
4046 'help-name' : 'netmask (eg: 255.255.255.0)',
4047 'base-type' : 'ip-address',
4048 'validation' : 'validate-netmask'
4049 })
4050
4051 add_typedef({
4052 'name' : 'obj-type',
4053 'help-name' : 'configured object',
4054 'base-type' : 'string',
4055 'validation' : 'validate-existing-obj'
4056 })
4057
4058 add_typedef({
4059 'name' : 'inverse-netmask',
4060 'help-name' : 'inverse netmask (eg: 0.0.0.255)',
4061 'base-type' : 'ip-address',
4062 'validation' : 'validate-inverse-netmask'
4063 })
4064
4065 add_typedef({
4066 'name' : 'domain-name',
4067 'help-name' : 'Domain name',
4068 # Simple domain name checking.
4069 # Allows some things that aren't valid domain names, but doesn't
4070 # disallow things liky punycode and fully qualified domain names.
4071 'pattern' : r'^([a-zA-Z0-9-]+.?)+$',
4072 'base-type' : 'string'
4073 })
4074
4075 add_typedef({
4076 'name': 'ip-address-or-domain-name',
4077 'help-name': 'IP address or domain name',
4078 'base-type': 'string',
4079 'pattern': (
4080 r'^([a-zA-Z0-9-]+.?)+$', # simple domain name
4081 r'^(\d{1,3}\.){3}\d{1,3}$' # IP address
4082 ),
4083 # for ip addresses, ought to validate non-mask values, ie: 0.0.0.0, 255.255.255.255
4084 # ought to also validate ip addresses which aren't ip address, the dhcp 169.x values.
4085 })
4086
4087 add_typedef({
4088 'name' : 'resolvable-ip-address',
4089 'help-name' : 'resolvable ip address',
4090 'base-type' : 'string',
4091 'pattern' : (
4092 r'^([a-zA-Z0-9-]+.?)+$', # simple domain name
4093 r'^(\d{1,3}\.){3}\d{1,3}$' # IP address
4094 ),
4095 'validation' : 'validate-resolvable-ip-address',
4096 })
4097
4098 add_typedef({
4099 'name': 'enable-disable-flag',
4100 'help-name': 'Enter "enable" or "disable"',
4101 'base-type': 'enum',
4102 'values': ('disable', 'enable'),
4103 })
4104
4105 add_typedef({
4106 'name' : 'identifier',
4107 'help-name' : 'Alphabetic character, followed by alphanumerics',
4108 'base-type' : 'string',
4109 'validation' : 'validate-identifier',
4110 })
4111
4112 add_typedef({
4113 'name' : 'config',
4114 'help-name' : 'name of config file',
4115 'base-type' : 'string',
4116 'validation' : 'validate-config',
4117 })
4118