blob: d99ed413ab7e292c2bcdfb392396707457cc895e [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)
1355 for matching_command in matching_commands:
1356 command, prefix_match = matching_command
1357 self.handle_one_command(command, prefix_match, words)
1358 else:
1359 result = self.handle_unspecified_command()
1360 result = self.handle_command_results()
1361 except Exception, e:
1362 if sdnsh.debug or sdnsh.debug_backtrace:
1363 traceback.print_exc()
1364 if isinstance(e, error.CommandError):
1365 result = self.handle_command_error(e)
1366 else:
1367 result = self.handle_exception(e)
1368
1369 return result
1370
1371
1372class CommandExecutor(CommandHandler):
1373
1374 def __init__(self):
1375 super(CommandExecutor, self).__init__()
1376 self.matches = []
1377
1378 #def get_default_attribute_name(self):
1379 # return 'default-for-no' if self.is_no_command else 'default'
1380
1381 def handle_unspecified_command(self):
1382 raise error.CommandInvocationError('No command specified')
1383
1384 def handle_command_error(self, e):
1385 result = sdnsh.error_msg(str(e))
1386 return result
1387
1388 def handle_exception(self, e):
1389 if sdnsh.debug or sdnsh.debug_backtrace:
1390 traceback.print_exc()
1391 raise e
1392
1393 def handle_complete_command(self, prefix_matches, scopes, arg_data, fields, actions, command):
1394 self.matches.append((prefix_matches, scopes, arg_data, fields, actions, command))
1395
1396 def handle_command_results(self):
1397
1398 if len(self.matches) == 0:
1399 # No matching command. We need to run through the parse
1400 # errors we've collected to try to figure out the best error
1401 # message to use. The simple heuristic we use is that we'll
1402 # assume that errors that matched the most command words
1403 # before hitting a parse error are the most relevant, so
1404 # those are the ones we'll print. If it's a parse error
1405 # because it didn't match a literal token (i.e. as opposed
1406 # to a validation error), then we'll collect all of the
1407 # valid tokens and print a single error message for all of
1408 # them. Otherwise we'll print out all of the individual
1409 # error message. This isn't great, because in a lot of
1410 # cases there we'll be multiple error messages, which
1411 # may be either conflicting or almost identical (identical
1412 # messages are eliminated because we use a set when we
1413 # collect the parse errors).
1414 most_words_matched = -1
1415 highest_priority = -1
1416 accumulated_error_messages = []
1417 accumulated_expected_tokens = []
1418 for error_message, words_matched, priority, expected_tokens in self.parse_errors:
1419 if words_matched > most_words_matched:
1420 # We found an error that matched more words than
1421 # the other errors we've seen so far, so reset
1422 # the highest priority, so we'll start looking for
1423 # the new highest priority at the current word match level.
1424 # the variable we're using to collect the error
1425 # messages and expected tokens
1426 most_words_matched = words_matched
1427 highest_priority = -1
1428 if words_matched == most_words_matched:
1429 if priority > highest_priority:
1430 # We found a higher priority error, so clear the
1431 # accumulated errors and expected tokens.
1432 accumulated_error_messages = []
1433 accumulated_expected_tokens = []
1434 highest_priority = priority
1435 if words_matched == most_words_matched and priority == highest_priority:
1436 # Collect the error message and check to see if there's
1437 # an expected token and if all of the errors at this level
1438 # so far also had expected tokens. If there's any error
1439 # that doesn't have an expected token, then we revert
1440 # to the mode where we'll print all of the individual
1441 # error messages.
1442 if expected_tokens is not None:
1443 if _is_string(expected_tokens):
1444 expected_tokens = [expected_tokens]
1445 elif isinstance(expected_tokens, tuple):
1446 expected_tokens = list(expected_tokens)
1447 accumulated_expected_tokens += expected_tokens
1448 else:
1449 accumulated_error_messages.append(error_message)
1450
1451 # We've collected the errors and the expected tokens. Now we
1452 # just need to format the error message to use with the
1453 # CommandSyntaxError exception.
1454 error_message = ''
1455 if accumulated_expected_tokens:
1456 word = self.words[most_words_matched]
1457 error_message += 'Unexpected argument \"%s\"; expected ' % word
1458 expected_tokens = ['"' + expected_token + '"'
1459 for expected_token in accumulated_expected_tokens]
1460 if len(expected_tokens) > 1:
1461 expected_tokens.sort()
1462 error_message += 'one of '
1463 error_message += '(' + ', '.join(expected_tokens) + ')'
1464 for message in accumulated_error_messages:
1465 if error_message:
1466 error_message += '\n'
1467 error_message += message
1468
1469 raise error.CommandSyntaxError(error_message)
1470
1471 # There may be multiple results. We need to figure out it there's
1472 # an unambiguous match. The heuristic we use is to iterate over
1473 # the command words and try to find a single result which has an
1474 # exact match on the word while all the others are prefix matches.
1475 # If there are multiple results that are exact matches, then we
1476 # discard the other results that were prefix matches and continue
1477 # with the next command word. If at an iteration all of the
1478 # results are prefix matches for the current word, then we check
1479 # all of the full tokens that were prefix matched by the command
1480 # word. If all of the results have the same full token, then we
1481 # continue. If there are multiple full tokens, then that's
1482 # indicative of an ambiguous command, so we break out of the loop
1483 # and raise an ambiguous command exception.
1484 unambiguous_match = None
1485 if len(self.matches) > 1:
1486 narrowed_matches = self.matches
1487 for i in range(len(self.words)):
1488 exact_match_matches = []
1489 prefix_match_matches = []
1490 prefix_match_full_tokens = {}
1491 for match in narrowed_matches:
1492 prefix_matches = match[0]
1493 for prefix_match in prefix_matches:
1494 if prefix_match[0] == i:
1495 prefix_match_matches.append(match)
1496 if type(prefix_match[1]) == dict: # regular expression
1497 # similar to _get_command_title()
1498 full_token = prefix_match[1]['title']
1499 else:
1500 full_token = prefix_match[1].lower()
1501 prefix_match_full_tokens[full_token] = None
1502 break
1503 else:
1504 exact_match_matches.append(match)
1505 if len(exact_match_matches) == 1:
1506 unambiguous_match = exact_match_matches[0]
1507 break
1508 elif len(exact_match_matches) > 1:
1509 narrowed_matches = exact_match_matches
1510 else:
1511 # No exact matches, check to see if the prefix matches
1512 # are all prefixes for the same token. If so, then let
1513 # the process continue with the next word in the command.
1514 # Otherwise it's an ambiguous command.
1515 assert len(prefix_match_matches) > 1
1516 if len(prefix_match_full_tokens) > 1:
1517 break
1518 narrowed_matches = prefix_match_matches
1519 else:
1520 # If we reach the end of the words without either finding an
1521 # unambiguous result or the word which was ambiguous then that
1522 # means that multiple command descriptions (or multiple
1523 # choices/path through a single command description) had the
1524 # exact same match tokens, which means that there are
1525 # conflicting command descriptions, which is a bug in the
1526 # command descriptions.
1527 if sdnsh.debug or sdnsh.debug_backtrace or sdnsh.description:
1528 for n in narrowed_matches:
1529 print _line(), n[1][0]['self']
1530 raise error.CommandAmbiguousError('Multiple command description match')
1531
1532 if unambiguous_match is None:
1533 assert prefix_match_full_tokens is not None
1534 assert len(prefix_match_full_tokens) > 1
1535 quoted_full_tokens = ['"' + full_token + '"'
1536 for full_token in prefix_match_full_tokens]
1537 # pylint: disable=W0631
1538 raise error.CommandAmbiguousError(
1539 'Ambiguous command word "%s"; matches [%s]' %
1540 (self.words[i], ', '.join(quoted_full_tokens)))
1541
1542 else:
1543 unambiguous_match = self.matches[0]
1544
1545 prefix_matches, scopes, arg_data, fields, actions, command = unambiguous_match
1546
1547 # XXX Possibly a better method of deciding when to loook deeper
1548 # in the scopes would be a good idea.
1549 if fields == None or len(fields) == 0:
1550 fields = _lookup_in_scopes('fields', scopes)
1551
1552 invocation_scope = {
1553 'data': arg_data,
1554 'fields': fields,
1555 'is-no-command': self.is_no_command,
1556 'current-mode-obj-type': sdnsh.get_current_mode_obj_type(),
1557 'current-mode-obj-id': sdnsh.get_current_mode_obj()
1558 }
1559
1560 #scopes = [invocation_scope] + scopes
1561 assert actions is not None
1562 if len(actions) == 0:
1563 raise error.CommandDescriptionError('No action specified', command)
1564 #if valid_args:
1565 # scopes = valid_args + scopes
1566 if sdnsh.description: # debug details
1567 print "Command Selected: %s" % command['self']
1568
1569 combined_result = None
1570 for action in actions:
1571 action_proc, action_scopes = action
1572 scopes = [invocation_scope] + action_scopes
1573 if sdnsh.description:
1574 print 'Action: ', action_proc
1575 result = _call_proc(action_proc, action_registry, scopes, command)
1576 if result is not None:
1577 combined_result = combined_result + result if combined_result else result
1578
1579 # Since a successful 'no' command may remove an object,
1580 # after a no command complete's all its actions, verify
1581 # the existance of every object in the mode stack.
1582 # Pop any nested modes where the object is missing.
1583 if self.is_no_command:
1584 # starting at the most nested submode level, determine if the
1585 # current submode object still exists
1586 any_removed = False
1587 for index in reversed(range(len(sdnsh.mode_stack))):
1588 mode = sdnsh.mode_stack[index]
1589 if mi.obj_type_in_use_as_related_config_type(mode.get('obj_type')):
1590 if sdnsh.description:
1591 print 'CONFIG TYPE:', \
1592 mode.get('obj_type'), \
1593 mi.obj_type_in_use_as_related_config_type(mode.get('obj_type'))
1594 continue
1595
1596 if mode.get('obj_type') and mode.get('obj'):
1597 try:
1598 sdnsh.get_object_from_store(mode['obj_type'], mode['obj'])
1599 except:
1600 # pop this item
1601 if sdnsh.description:
1602 print 'NO LOST OBJECT ', mode.get('obj_type'), mode.get('obj')
1603 del sdnsh.mode_stack[index]
1604 any_removed = True
1605
1606 if any_removed:
1607 sdnsh.warning("submode exited due to deleted object")
1608 sdnsh.update_prompt()
1609
1610 return combined_result
1611
1612
1613class CommandCompleter(CommandHandler):
1614
1615 def __init__(self, meta_completion = False):
1616 super(CommandCompleter, self).__init__()
1617 self.meta_completion = meta_completion
1618 self.completions = {}
1619
1620 def get_default_value(self, arg):
1621 default = arg.get('default')
1622 if self.is_no_command and default is None and 'field' in arg:
1623 default = mi.field_current_obj_type_default_value(arg['field'])
1624 if default != None:
1625 default = str(None)
1626 return default
1627
1628 def complete_help(self, arg_scopes):
1629 """
1630 Select the most appropriate localized text to associate with
1631 the completion-reason for some completion-text
1632 """
1633 c_help = _lookup_in_scopes('conpletion-text', arg_scopes)
1634 if c_help:
1635 return c_help
1636 c_help = _lookup_in_scopes('syntax-help', arg_scopes)
1637 if c_help:
1638 return c_help
1639 c_help = _lookup_in_scopes('all-help', arg_scopes)
1640 if c_help:
1641 return '!' + c_help
1642 c_help = _lookup_in_scopes('short-help', arg_scopes)
1643 if c_help:
1644 return c_help
1645 return ' <help missing> %s' % _lookup_in_scopes('self', arg_scopes)
1646
1647
1648 def completion_set(self, completions, text, reason):
1649 """
1650 In situations where multiple commands match for some completion
1651 text, the last command-iterated-over will populate the text for
1652 the completion dictionary. Use the first character of the
1653 completion string for this command to identify when the text
1654 represents the most "base" variant of the command to display
1655 for completion help text.
1656
1657 When the first character of the string is a '!', the printing
1658 procedures must drop the first character.
1659 """
1660 if completions == None:
1661 completions = {text : reason}
1662 return
1663 current_text = completions.get(text)
1664 # current_text could possibly be None from the lookup
1665 if current_text != None and current_text[0] == '!':
1666 return
1667 completions[text] = reason
1668
1669
1670 def command_help_text(self, command):
1671 c_help = command.get('all-help')
1672 if c_help:
1673 if c_help[0] != '!':
1674 return '!' + c_help
1675 return c_help
1676 c_help = command.get('short-help')
1677 return c_help
1678
1679
1680 def handle_unspecified_command(self):
1681 candidates = self.get_matching_commands(self.text, self.is_no_command, command_registry)
1682 for candidate in candidates:
1683 self.completion_set(self.completions, candidate[0]['name'] + ' ',
1684 self.command_help_text(candidate[0]))
1685
1686 def handle_incomplete_command(self, arg, arg_scopes, arg_data, fields,
1687 parsed_tag, command):
1688
1689 completions = None
1690 tag = arg.get('tag')
1691 lower_text = self.text.lower()
1692
1693 if tag is not None and not parsed_tag:
1694 if tag.lower().startswith(lower_text):
1695 if completions == None:
1696 completions = {}
1697 completions[tag + ' '] = self.complete_help(arg_scopes)
1698 else:
1699 token = arg.get('token')
1700 type_name = arg.get('type')
1701 if type_name == None:
1702 type_name = arg.get('base-type')
1703 typedef = typedef_registry.get(type_name) if type_name else None
1704
1705 # See if this argument is an enumerated value. This can either
1706 # be by a type/values specified inline in the argument or via
1707 # an enum typedef. So first we check for those two cases and
1708 # get the object that contains the values, either the argument
1709 # itself or the typedef.
1710 enum_obj = None
1711 if token is not None:
1712 if token.lower().startswith(lower_text):
1713 if completions == None:
1714 completions = {}
1715 completions[token + ' '] = self.complete_help(arg_scopes)
1716 elif type_name == 'enum':
1717 enum_obj = arg
1718 elif typedef:
1719 # Note: This doesn't recursively check up the type hierarchy,
1720 # but it doesn't seem like a valid use case to have more than
1721 # one level of inheritance for enumerations, so it should be OK.
1722 base_type = typedef.get('base-type')
1723 if base_type == 'enum':
1724 enum_obj = typedef
1725
1726 # If we have an enum object, then get the values and
1727 # build the completion list
1728 if enum_obj:
1729 enum_values = enum_obj.get('values')
1730 if enum_values:
1731 if _is_string(enum_values):
1732 enum_values = (enum_values,)
1733 # enum_values may be either a array/tuple of strings or
1734 # else a dict whose keys are the enum strings. In both
1735 # cases we can use the for loop to iterate over the
1736 # enum string names to be used for completion.
1737 completions = {}
1738 for enum_value in enum_values:
1739 if enum_value.lower().startswith(lower_text):
1740 completions[enum_value + ' '] = \
1741 self.complete_help(arg_scopes)
1742
1743 if completions == None or len(completions) == 0:
1744 # Check to see if there's a custom completion proc
1745 completion_proc = _lookup_in_scopes('completion', arg_scopes)
1746 if completion_proc:
1747 completions = {}
1748 curr_obj_type = sdnsh.get_current_mode_obj_type()
1749 curr_obj_id = sdnsh.get_current_mode_obj()
1750 command_mode = command.get('mode')
1751 if command_mode and command_mode[-1] == '*':
1752 command_mode = command_mode[:-1]
Srikanth Vavilapallib5c3ca52014-12-15 15:59:33 -08001753 if (command_mode == 'config-tunnel' and
1754 sdnsh.current_mode() == 'config-tunnelset-tunnel'):
1755 command_mode = 'config-tunnelset-tunnel'
1756 if (command_mode and
1757 ((_is_list(command_mode) and not sdnsh.current_mode() in command_mode)
1758 or (sdnsh.current_mode() != command_mode))):
1759
Srikanth Vavilapalli1725e492014-12-01 17:50:52 -08001760 # this is completing a different item on the stack.
1761 # XXX needs better api's here.
1762 found_in_mode_stack = False
Srikanth Vavilapallib5c3ca52014-12-15 15:59:33 -08001763
1764 if _is_list(command_mode):
1765 for cmd_mode in command_mode:
1766 for x in sdnsh.mode_stack:
1767 if x['mode_name'] == cmd_mode:
1768 found_in_mode_stack = True
1769 curr_obj_type = x['obj_type']
1770 curr_obj_id = x['obj']
1771 break
1772 if found_in_mode_stack == True:
1773 break
1774 else:
1775 for x in sdnsh.mode_stack:
1776 if x['mode_name'] == command_mode:
1777 found_in_mode_stack = True
1778 curr_obj_type = x['obj_type']
1779 curr_obj_id = x['obj']
1780 break
Srikanth Vavilapalli1725e492014-12-01 17:50:52 -08001781 if not found_in_mode_stack:
1782 raise error.CommandDescriptionError(
1783 'Unable to find mode %s' % command_mode,
1784 command)
1785
1786 invocation_scope = {
1787 'words': self.words,
1788 'text': self.text,
1789 'data': arg_data,
1790 'completions': completions,
1791 'is-no-command': self.is_no_command,
1792 'mode' : command_mode,
1793 'current-mode-obj-type': curr_obj_type,
1794 'current-mode-obj-id': curr_obj_id,
1795 }
1796 arg_scopes.insert(0, invocation_scope)
1797 if sdnsh.description: # for deubugging, print command name
1798 print command['self'], completion_proc
1799 try:
1800 _result = _call_proc(completion_proc, completion_registry,
1801 arg_scopes, command)
1802 except TypeError, e:
1803 if sdnsh.debug or sdnsh.debug_backtrace:
1804 print "Issue: ", e
1805 traceback.print_exc()
1806 raise error.CommandDescriptionError("Unable to call completion",
1807 command)
1808
1809 if (completions == None or len(completions) == 0 or self.meta_completion) and not self.text:
1810 # TODO: It's not clear in this case how to detect if a
1811 # partially entered argument can really match this argument,
1812 # so we (conservatively) only include these completion text
1813 # strings when there's no partial text for the argument, which
1814 # is why we have the above check for self.text
1815
1816 # Note: We want to allow None for the value of completion-text
1817 # to mean that completion should be disabled for the argument,
1818 # so we use False for the default value of the get method.
1819 # Then if the completion-text is not set in the arg the
1820 # "if not completion_text" checks will be True, so we'll try to
1821 # set the completion text to the help-name or field value.
1822 completion_text = arg.get('completion-text', False)
1823 if completion_text is not None:
1824 # FIXME: Probably shouldn't use syntax help here. That
1825 # should probably be reserved for printing additional
1826 # syntax help for the argument, duh (presumably some longer
1827 # descriptive text for the argument).
1828 if not completion_text:
1829 completion_text = arg.get('help-name')
1830 if not completion_text:
1831 completion_text = tag if tag else arg.get('field')
1832 syntax_help = arg.get('syntax-help')
1833 if syntax_help == None:
1834 if sdnsh.description:
1835 syntax_help = 'Add syntax-help %s at %s' % (command['self'],
1836 completion_text)
1837 # exclude enum, since the values will included as text
1838 if typedef and type_name != 'enum':
1839 syntax_help = typedef.get('help-name')
1840 if syntax_help == None:
1841 syntax_help = ' <' + type_name + '> ' # add delim
1842
1843 if completion_text and syntax_help:
1844 #and completion_text.startswith(lower_text):
1845 completion_text = ' <' + completion_text + '> ' # add_delim
1846 if completions == None:
1847 completions = {}
1848 # The use of tuple here as he index provides a method
1849 # of identifying the completion values which are
1850 # 'meta-information', and not to be completed against
1851 completions[(completion_text, syntax_help)] = syntax_help
1852
1853 if completions and len(completions) > 0:
1854 for (completion, text) in completions.items():
1855 self.completion_set(self.completions, completion, text)
1856
1857 return None
1858
1859 def handle_complete_command(self, prefix_matches, scopes,
1860 arg_data, fields, actions, command):
1861 if not self.text:
1862 # complete command, intentionally use the short-help associated
1863 # with the top level of this command.
1864 # The use of tuple here as he index provides a method
1865 # of identifying the completion values which are
1866 # 'meta-information', and not to be completed against
1867 self.completions[(' <cr> ', '<cr>' )] = command.get('short-help')
1868
1869 def handle_command_error(self, e):
1870 # For completion ignore all errors exception for ones that are
1871 # indicative of an error in the command description, which will
1872 # (hopefully) be found by the developer while debugging the
1873 # command description.
1874 if isinstance(e, error.CommandDescriptionError):
1875 sdnsh.print_completion_help(sdnsh.error_msg(str(e)))
1876 if sdnsh.debug or sdnsh.debug_backtrace:
1877 traceback.print_exc()
1878
1879
1880 def print_completions(self, completions):
1881 """
1882 Print the completions ourselves in sorted multiple column format.
1883 We use this when the completions are are pseudo/help completions
1884 that aren't real tokens that we want readline to use for
1885 completions.
1886 """
1887 meta_completions = [completion[0] if isinstance(completion, tuple)
1888 else completion for completion in completions]
1889 meta_completions.sort()
1890
1891 sdnsh.print_completion_help(sdnsh.choices_text_builder(meta_completions))
1892
1893
1894 def handle_command_results(self):
1895 if self.meta_completion == True:
1896 # two-column display mode
1897 sdnsh.completion_print = False
1898 if len(self.completions):
1899 max_token_len = 0
1900 for completion in self.completions:
1901 item = completion
1902 if isinstance(completion, tuple):
1903 item = completion[0]
1904 max_token_len = max(max_token_len, len(item))
1905 text = []
1906 for completion in sorted(self.completions.keys(),
1907 cmp=utif.completion_trailing_integer_cmp):
1908 help = self.completions[completion]
1909 if help and help[0] == '!':
1910 help = help[1:]
1911
1912 left_side = completion
1913 if isinstance(completion, tuple):
1914 left_side = completion[0]
1915 text.append('%-*s %s\n' % (max_token_len, left_side.strip(), help.strip()))
1916 sdnsh.print_completion_help(''.join(text))
1917
1918 completions = list(self.completions)
1919 if len(completions) == 1:
1920 completion = completions[0]
1921 if isinstance(completion, tuple):
1922 help_text = completion[1]
1923 if not help_text:
1924 help_text = completion[0]
1925 if self.meta_completion == False:
1926 sdnsh.print_completion_help(help_text)
1927 completions = None
1928 elif len(completions) > 1:
1929 # Some of the completion results are really pseudo completions
1930 # that are meant to be more like a help prompt for the user rather
1931 # than an actual token that should be used for completion by readline
1932 # If all of the completions are like that and we return them to
1933 # readline, then it will actually complete the leading prefix, which
1934 # will be at least the leading '<'. To avoid that situation we print
1935 # out the completions ourselves using print_completion_help and then
1936 # return None for the completions.
1937 all_pseudo_completions = True
1938 for completion in completions:
1939 name = completion[0] if isinstance(completion, tuple) else completion
1940 if not name.endswith('> ') and not name.endswith('>'):
1941 all_pseudo_completions = False
1942 break
1943 if all_pseudo_completions:
1944 if self.meta_completion == False:
1945 self.print_completions(completions)
1946 completions = None
1947 else:
1948 completions = [completion[0] if isinstance(completion, tuple)
1949 else completion for completion in completions]
1950
1951 return completions
1952
1953
1954def do_command_completion(words, text):
1955 completer = CommandCompleter()
1956 completions = completer.handle_command(words, text)
1957 return completions
1958
1959
1960def do_command_completion_help(words, text):
1961 completer = CommandCompleter(meta_completion = True)
1962 completions = completer.handle_command(words, text)
1963 return completions
1964
1965
1966def _canonicalize_validation_result(result):
1967 """
1968 The canonical (i.e. most general) representation of a validation
1969 result is a list of 2-item tuples where the first item in the tuple
1970 is the validated value that should be used to update the arg_data
1971 and the second item is the actual token that was matched, which may
1972 not be the same as the word in the command. For example, in the
1973 results for an enum the word from the command may be a prefix for
1974 one of the allowed value for the enum. In that case the first item
1975 would be the return value for the enum value and the second item
1976 will be the full enum value, i.e. not the prefix that was typed
1977 in the command.
1978
1979 But there are a couple of simpler representations of the validation
1980 results. First, the result can be a simple scalar validated value.
1981 This is converted to the canonical representation by turning it
1982 into [(<value>, <value>)]. Second, the input result can be just
1983 the 2-item tuple. To canonicalize that we just enclose it in a
1984 list: [<tuple-result>].
1985 """
1986 if result is None:
1987 result = []
1988 elif _is_list(result):
1989 if len(result) > 0 and not _is_list(result[0]):
1990 result = [result]
1991 else:
1992 matching_token = result if _is_string(result) else str(result)
1993 result = [(result, matching_token)]
1994
1995 return result
1996
1997
1998def _combine_validation_results(result1, result2, typedef, value):
1999 if result1 is not None:
2000 result1 = _canonicalize_validation_result(result1)
2001 if result2 is not None:
2002 result2 = _canonicalize_validation_result(result2)
2003
2004 if result1 is not None:
2005 if result2 is not None:
2006 result = [r for r in result1 if r in result2]
2007 if len(result) == 0:
2008 _raise_argument_validation_exception(typedef, value, "no match")
2009 #elif len(result) == 1:
2010 # result = result[0]
2011 return result
2012 else:
2013 return result1
2014 elif result2 is not None:
2015 return result2
2016 else:
2017 return [(value, value)]
2018
2019
2020def validate_type(type_name, value, scopes, command):
2021 """
2022 Validate that the specified value matches the validation in the
2023 specified type definition and any inherited type definitions
2024 """
2025 # Look up the type definition and perform any validation specified there
2026 typedef = typedef_registry.get(type_name)
2027 if not typedef:
2028 raise error.CommandDescriptionError('Unknown type: %s' % type_name)
2029
2030 type_result = None
2031 validation_result = None
2032
2033 # If it's a subtype, validate the base type first
2034 base_type_name = typedef.get('base-type')
2035 if base_type_name:
2036 type_result = validate_type(base_type_name, value, scopes, command)
2037
2038 # FIXME: Seems like we shouldn't be handling obj-type here, since
2039 # that's just an attribute that's specific to certain validation
2040 # procs. Shouldn't that value already be available in the scopes
2041 # to be able to pass to the proc?
2042 obj_type = _lookup_in_scopes('obj-type', scopes)
2043
2044 # Now validate the subtype
2045 # _call_proc requires that the first scope be an invocation scope that
2046 # it possibly modifies by settings the 'scopes' variable, so we need
2047 # to include an empty invocation scope here
2048 invocation_scope = {}
2049 parameter_scope = {'typedef': typedef, 'value': value}
2050 if obj_type:
2051 parameter_scope['obj_type'] = obj_type
2052 scopes = [invocation_scope, parameter_scope, typedef]
2053 while base_type_name is not None:
2054 base_typedef = typedef_registry.get(base_type_name)
2055 if not base_typedef:
2056 raise error.CommandDescriptionError('Invalid base-type name: %s' % base_type_name)
2057 scopes.append(base_typedef)
2058 base_type_name = base_typedef.get('base-type')
2059
2060 #command_type = command.get('command-type')
2061 #command_defaults = command_type_defaults.get(command_type, {})
2062
2063 #scopes.append([command, command_defaults])
2064 validation = _lookup_in_scopes('validation', scopes)
2065 if validation:
2066 validation_result = _call_proc(validation, validation_registry,
2067 scopes, command)
2068 if validation_result is None:
2069 validation_result = value
2070
2071 result = _combine_validation_results(type_result, validation_result,
2072 typedef, value)
2073
2074 return result
2075
2076
2077def validate_argument(arg, value, scopes, command):
2078
2079 type_result = None
2080 validation_result = None
2081
2082 # Validate based on the type of the argument
2083 type_name = arg.get('type')
2084 if type_name:
2085 if type_name == 'enum':
2086 #values = arg.get('values')
2087 #result = validate_enum({'values': values}, value)
2088 type_result = c_validations.validate_enum(arg, value)
2089 else:
2090 type_result = validate_type(type_name, value, scopes, command)
2091 else:
2092 # If the argument description specifies a base-type value, then
2093 # we treat it as an anonymous typedef.
2094 # FIXME: Should probably be able to refactor this code a bit to
2095 # reduce code duplication.
2096 base_type_name = arg.get('base-type')
2097 if base_type_name:
2098 base_typedef = typedef_registry.get(base_type_name)
2099 if base_typedef:
2100 validation = _lookup_typedef_value(base_typedef, 'validation')
2101 if validation:
2102 scopes = [{'typedef': arg, 'value': value}] + scopes
2103 type_result = _call_proc(validation, validation_registry,
2104 scopes, command)
2105 if type_result is None:
2106 type_result = value
2107
2108 # Perform any custom validate proc in the argument
2109 validation = arg.get('validation')
2110 if validation:
2111 # Include 'typedef' in the local scope so that we can use
2112 # typedef validation functions inline for arguments (as opposed to
2113 # requiring a typedef definition).
2114 scopes = [{'arg': arg, 'typedef': arg, 'value': value}] + scopes
2115 validation_result = _call_proc(validation, validation_registry,
2116 scopes, command)
2117 if validation_result is None:
2118 validation_result = value
2119
2120 result = _combine_validation_results(type_result, validation_result,
2121 arg, value)
2122
2123 return result
2124
2125#
2126# CommandSyntaxer: Collect all the commands which "prefix match" the words
2127#
2128class CommandSyntaxer(CommandHandler):
2129
2130 def __init__(self, header):
2131 super(CommandSyntaxer, self).__init__()
2132 if header is None:
2133 header = ''
2134 self.header = header
2135 self.is_no_command = False
2136 self.commands = []
2137
2138 def add_known_command(self, command):
2139 self.commands.append(command)
2140
2141 def handle_incomplete_command(self, arg, arg_scopes, arg_data, fields, parsed_tag, command):
2142 if command not in self.commands:
2143 self.commands.append(command)
2144
2145 def handle_complete_command(self, prefix_matches, scopes, arg_data, fields, actions, command):
2146 if command not in self.commands:
2147 self.commands.append(command)
2148 def handle_command_results(self):
2149 if len(self.commands) == 0:
2150 return 'No applicable command: %s' % ' '.join(self.words)
2151
2152 help_lines = ['']
2153
2154 if self.header:
2155 help_lines.append(self.header)
2156 help_lines.append('')
2157
2158 help_strings = []
2159 for command in self.commands:
2160 command_help_string = _get_command_syntax_help_string(
2161 command, self.prefix)
2162 if type(command_help_string) == list:
2163 help_strings += command_help_string
2164 else:
2165 help_strings.append(command_help_string)
2166
2167 (term_cols, term_rows) = sdnsh.pp.get_terminal_size()
2168 for h in sorted(help_strings):
2169 # use 75% of the width, since reformat_line isn't perfect.
2170 smaller_term_cols = (term_cols * 3) / 4
2171 if len(h) > smaller_term_cols:
2172 help_lines += reformat_line(h, smaller_term_cols, 0, 0)
2173 else:
2174 help_lines.append(h)
2175
2176 if len(self.commands) == 1:
2177 short_help = None
2178 if self.is_no_command:
2179 #
2180 # Cobble together a help string for a no command, build
2181 # a default value for subcommand's, possibly other command
2182 # descriptions
2183 #
2184 short_help = self.commands[0].get('no-help')
2185 if not short_help:
2186 command_type = self.commands[0].get('command-type')
2187 action = self.commands[0].get('action')
2188 if ((command_type and command_type == 'config-submode') or
2189 (action and action == 'push-mode-stack')):
2190 mode_name = self.commands[0].get('name')
2191 short_help = 'Remove %s configuration' % mode_name
2192 else:
2193 short_help = self.commands[0].get('short-help')
2194
2195 if short_help:
2196 #help_lines.append('')
2197 help_lines.append(short_help)
2198
2199 return '\n'.join(help_lines)
2200
2201
2202def get_command_syntax_help(words, header=None):
2203 handler = CommandSyntaxer(header)
2204 # Hack to make "help no" work
2205 if len(words) == 1 and words[0] == 'no':
2206 words = words + ['']
2207 result = handler.handle_command(words)
2208 return result
2209
2210
2211def get_command_short_help_for_pattern(word):
2212 # try to find the command based on the pattern.
2213 for command in command_registry:
2214 if type(command['name']) == dict:
2215 if command['name']['title'] == word:
2216 return command.get('short-help')
2217
2218
2219def get_command_short_help(word):
2220 handler = CommandHandler()
2221 matching_commands = handler.get_matching_commands(word, False,
2222 command_registry)
2223 if len(matching_commands) == 0:
2224 return get_command_short_help_for_pattern(word)
2225
2226 if len(matching_commands) >= 1:
2227 # This will only work once all commands have command descriptions.
2228 #matching_commands[0][0]['self'], _exact_mode_match(current_mode,
2229 #(matching_commands[0][0]).get('mode'))
2230 return (matching_commands[0][0]).get('short-help')
2231 return None
2232
2233
2234def dumb_formatter(out, text, left, right = None):
2235 """
2236 Build output lines from the passed in text. If the
2237 text has leading spaces, then leave the spaces intact
2238 (starting at the indent).
2239 """
2240 if right == None:
2241 (right, line_length) = sdnsh.pp.get_terminal_size()
2242 if right - 20 > left: # XXX needs work
2243 right = right - 20
2244 right = min(right, 120)
2245
2246 left_indent = ' ' * left
2247 out_len = left
2248 out_line = left_indent
2249
2250 for line in text.split('\n'):
2251 if len(line) == 0:
2252 if out_len > left:
2253 out.append(out_line)
2254 out_len = left
2255 out_line = left_indent
2256 out.append('')
2257 elif line[0] == ' ': # leading spaces
2258 if out_len > left:
2259 out.append(out_line)
2260 out_len = left
2261 out_line = left_indent
2262 out.append( left_indent + line )
2263 else: # text formatting
2264
2265 for word in line.split():
2266 sep = ' '
2267 if word.endswith('.'):
2268 sep = ' '
2269 if len(word) + out_len + len(sep) > right:
2270 if out_len > left:
2271 out.append(out_line)
2272 out_len = left + len(word) + len(sep)
2273 out_line = left_indent + word + sep
2274 else:
2275 out_line += word + sep
2276 out_len += len(sep) + len(word)
2277 if out_len > left:
2278 out.append(out_line)
2279
2280
2281#
2282# Documentations: Collect all the commands which "prefix match" the words
2283#
2284class CommandDocumentor(CommandHandler):
2285
2286 def __init__(self, header, format = None):
2287 super(CommandDocumentor, self).__init__()
2288 if header is None:
2289 header = ''
2290 self.header = header
2291 self.commands = []
2292 self.docs = {}
2293 self.docs_stack = []
2294 self.is_no_command = False
2295 self.words = None
2296 self.format = format
2297 (self.term_cols, self.term_rows) = sdnsh.pp.get_terminal_size()
2298
2299
2300 def add_docs(self, name, value):
2301 if name in sdnsh.reserved_words and value == None:
2302 value = 'reserved|%s' % name
2303 if name in self.docs:
2304 if value != self.docs[name]:
2305 if value != None and None in self.docs[name] and \
2306 len(self.docs[name]) == 1 :
2307 self.docs[name] = [value]
2308 elif value != None and value not in self.docs[name]:
2309 self.docs[name].append(value)
2310 # if the value is already there, don't do nuttn
2311 else:
2312 self.docs[name] = [value]
2313
2314 def add_dict_doc(self, arg):
2315 if 'choices' in arg:
2316 self.add_tuple_docs(arg['choices'])
2317 return
2318 if 'args' in arg:
2319 args_arg = arg['args']
2320 if type(args_arg) == tuple:
2321 self.add_tuple_docs(args_arg)
2322 elif type(args_arg) == dict:
2323 self.add_dict_doc(args_arg)
2324 else:
2325 print 'add_dict_doc ', type(args_arg), args_arg
2326 return
2327
2328 tag = arg.get('tag')
2329 if 'field' in arg:
2330 if tag:
2331 name = '%s <%s>' % (tag, arg['field'])
2332 else:
2333 name = arg['field']
2334 elif 'token' in arg:
2335 name = arg['token']
2336
2337 doc = arg.get('doc')
2338
2339 if doc and 'type' in arg:
2340 values = arg.get('values')
2341 if arg['type'] == 'enum' and values:
2342 if isinstance(values,str):
2343 self.add_docs(values,doc)
2344 else:
2345 for v in values:
2346 if doc and doc[-1] == '+':
2347 self.add_docs(v, doc[:-1] + v)
2348 else:
2349 self.add_docs(v, doc)
2350 return
2351
2352 if doc:
2353 self.add_docs(name, doc)
2354
2355
2356 def add_tuple_docs(self, tuple_arg):
2357 for t in tuple_arg:
2358 if type(t) == tuple:
2359 self.add_tuple_docs(t)
2360 elif type(t) == dict:
2361 self.add_dict_doc(t)
2362
2363 def add_command(self, command):
2364 self.commands.append(command)
2365 args = command.get('args')
2366 self.add_tuple_docs((args,))
2367
2368 def handle_first_matched_result(self, command):
2369 self.docs_stack.append({})
2370
2371 def handle_matched_result(self, command, result, arg_scopes):
2372 matched_doc = _lookup_in_scopes('doc', arg_scopes)
2373 if matched_doc and matched_doc[-1] == '+':
2374 item = result
2375 if type(item) == tuple:
2376 item = item[0]
2377 matched_doc = matched_doc[:-1] + item
2378 self.docs_stack[-1][command['self']] = (result, matched_doc)
2379
2380 def handle_incomplete_command(self, arg, arg_scopes, arg_data,
2381 fields, parsed_tag, command):
2382 tag = arg.get('tag')
2383 doc = arg.get('doc')
2384 doc_any = doc
2385 if doc_any == None:
2386 doc_any = _lookup_in_scopes('doc', arg_scopes)
2387 # note: doc may still be None
2388
2389 token = arg.get('token')
2390
2391 type_name = arg.get('type')
2392 if type_name == None:
2393 type_name = arg.get('base-type')
2394 typedef = typedef_registry.get(type_name) if type_name else None
2395
2396 field = arg.get('field')
2397 help_name = arg.get('help-name', field)
2398
2399 field_doc = arg.get('completion-text', False)
2400 if field_doc == None:
2401 field_doc = arg.get('help-name')
2402 if field_doc == None:
2403 field_doc = arg.get('syntax-help')
2404
2405 def manage_enum(arg, doc):
2406 enum_values = arg.get('values')
2407 if _is_string(enum_values):
2408 enum_values = [enum_values]
2409 for enum in enum_values:
2410 if doc and doc[-1] == '+':
2411 self.add_docs(enum, doc[:-1] + enum)
2412 else:
2413 self.add_docs(enum, doc)
2414
2415 if token != None: # token is a loner.
2416 self.add_docs(token.lower(), doc_any)
2417 elif tag and field and field_doc:
2418 self.add_docs(tag + ' <' + help_name + '>', doc if doc else field_doc)
2419 elif field and typedef and typedef.get('name') == 'enum':
2420 manage_enum(arg, doc)
2421 elif field and doc:
2422 self.add_docs(help_name, doc)
2423 elif tag and field and typedef:
2424 self.add_docs(tag + ' <' + help_name + '>', 'types|' + typedef.get('name'))
2425 elif field and field_doc:
2426 self.add_docs(help_name, field_doc)
2427 elif typedef:
2428 typedef_name = typedef.get('name').lower()
2429 if typedef_name != 'enum':
2430 if field:
2431 # magic reference to doc: types/<typename>
2432 self.add_docs(field, 'types|' + typedef.get('name'))
2433 else:
2434 self.add_docs(typedef.get('name').lower(), doc)
2435 else:
2436 manage_enum(arg, doc)
2437
2438 if command not in self.commands:
2439 self.commands.append(command)
2440
2441
2442 def handle_complete_command(self, prefix_matches, scopes, arg_data,
2443 fields, actions, command):
2444 # only provide help for exact command matches
2445 if len(prefix_matches):
2446 return
2447
2448 if command not in self.commands:
2449 if sdnsh.description:
2450 print 'COMPLETE COMMAND DOC', command['self'], arg_data, prefix_matches
2451 self.commands.append(command)
2452
2453 def handle_command_results_wiki(self):
2454 def handle_plaintext_wiki(inputstr):
2455 inputstr=inputstr.replace("{","\{")
2456 inputstr=inputstr.replace("}","\}")
2457 inputstr=inputstr.replace("[","\[")
2458 inputstr=inputstr.replace("]","\]")
2459 inputstr=inputstr.replace("\\\\","<br>")
2460 inputstr=inputstr.replace("\n\n", "\\@")
2461 inputstr=inputstr.replace("\n"," ")
2462 inputstr=inputstr.replace("\\@", "\n\n")
2463 inputstr=inputstr.replace("<br>", "\n")
2464
2465 return inputstr
2466
2467 if len(self.commands) == 0:
2468 if self.words:
2469 return 'No applicable command: %s' % ' '.join(self.words)
2470 else:
2471 return '\nNo command for ' + self.header
2472 help_lines=[]
2473 help_lines.append('')
2474 last_desc_name = None
2475 last_syntax_name = None
2476 # Keep track of paragraphs displayed to prevent repeated
2477 # display of the same doc tags.
2478 shown_items = []
2479 for command in self.commands:
2480 doc_tag = command.get('doc')
2481 name = _get_command_title(command)
2482 short_help = command.get('short-help')
2483 if short_help:
2484 if self.header:
2485 help_lines.append('\nh2. %s Command' % name.capitalize())
2486 help_lines.append('\nh4. %s' % short_help.capitalize())
2487 cmdmode=command.get('mode')
2488 if isinstance(cmdmode,list):
2489 cmdmodestr=''
2490 for cmdmodemem in cmdmode:
2491 if cmdmodemem[-1]=='*':
2492 cmdmodemem=cmdmodemem[:-1]
2493 cmdmodestr= cmdmodestr +' ' + cmdmodemem
2494 else:
2495 cmdmodestr=cmdmode
2496 if cmdmodestr[-1]=='*':
2497 cmdmodestr=cmdmodestr[:-1]
2498 help_lines.append('\n*Command Mode:* %s mode' % cmdmodestr)
2499 last_syntax_name = None # display syntax header
2500
2501 help_strings = []
2502 command_help_string = _get_command_syntax_help_string(
2503 command, self.prefix)
2504 help_strings.append(command_help_string)
2505 for h in sorted(help_strings):
2506 if isinstance(h,list):
2507 h = handle_plaintext_wiki(h[0])
2508 else:
2509 h = handle_plaintext_wiki(h)
2510 if last_syntax_name != name:
2511 help_lines.append('\n*Command Syntax:* {{%s}}' % h)
2512 last_syntax_name = name
2513 last_desc_name = None # print description header
2514
2515 if doc_tag:
2516 text = doc.get_text(self, doc_tag)
2517 if text != '' and doc_tag not in shown_items:
2518 shown_items.append(doc_tag)
2519 if last_desc_name != name:
2520 help_lines.append('\n*Command Description:*')
2521 last_desc_name = name
2522 text=handle_plaintext_wiki(text)
2523 help_lines.append(text)
2524 #dumb_formatter(help_lines, text, 3)
2525 last_syntax_name = None # print 'Syntax' header
2526 if len(self.commands) == 1:
2527 short_help = None
2528 if self.is_no_command:
2529 #
2530 # Cobble together a help string for a no command, build
2531 # a default value for subcommand's, possibly other command
2532 # descriptions
2533 #
2534 short_help = self.commands[0].get('no-help')
2535 if not short_help:
2536 command_type = self.commands[0].get('command-type')
2537 action = self.commands[0].get('action')
2538 if ((command_type and command_type == 'config-submode') or
2539 (action and action == 'push-mode-stack')):
2540 mode_name = self.commands[0].get('name')
2541 short_help = 'Remove %s configuration' % mode_name
2542 else:
2543 short_help = self.commands[0].get('short-help')
2544 for last_doc in self.docs_stack:
2545 if command['self'] in last_doc:
2546 (keyword, last_doc) = last_doc[command['self']]
2547 text = doc.get_text(self, last_doc)
2548 if type(keyword) == tuple:
2549 keyword = keyword[0]
2550 if sdnsh.description:
2551 help_lines.append("\t%s: %s %s" %
2552 (command['self'], keyword, last_doc))
2553 if text != '' and last_doc not in shown_items:
2554 shown_items.append(last_doc)
2555 help_lines.append('\nKeyword %s Description:' %
2556 keyword.capitalize())
2557 text=handle_plaintext_wiki(text)
2558 help_lines.append(text)
2559 #dumb_formatter(help_lines, text, 4)
2560
2561 if len(self.docs) > 0:
2562 help_lines.append('\n*Next Keyword Descriptions:*')
2563 for (doc_name, doc_tags) in self.docs.items():
2564 if len(doc_tags) == 1 and doc_tags[0] == None:
2565 if sdnsh.description:
2566 help_lines.append("\t%s: %s missing doc attribute" %
2567 (command['self'], doc_name))
2568 help_lines.append('* %s:' % doc_name)
2569 elif len(doc_tags) == 1 and doc_tags[0].split('|')[0] == 'types' and \
2570 not doc_name.startswith(doc_tags[0].split('|')[1]):
2571 type_name = doc_tags[0].split('|')[1].capitalize()
2572 help_lines.append(' %s: type %s' %
2573 (doc_name, type_name))
2574 else:
2575 help_lines.append('* %s:' % doc_name)
2576 for doc_tag in doc_tags:
2577 if sdnsh.description:
2578 help_lines.append("\t%s: %s %s" %
2579 (command['self'], doc_name, doc_tag))
2580 text = doc.get_text(self, doc_tag)
2581 if text == '':
2582 help_lines.append('')
2583 if text != '' and doc_tag not in shown_items:
2584 if len(doc_tags) > 1 and doc_tag.split('|')[0] == 'types':
2585 type_name = doc_tag.split('|')[1].capitalize()
2586 help_lines.append('\tType: %s' % type_name)
2587 shown_items.append(doc_tag)
2588 text=handle_plaintext_wiki(text)
2589 help_lines.append(text)
2590 #dumb_formatter(help_lines, text, 4)
2591 doc_example = command.get('doc-example')
2592 if doc_example:
2593 text = doc.get_text(self, doc_example)
2594 if text != '':
2595 help_lines.append('\n*Command Examples:*')
2596 help_lines.append('{noformat}')
2597 help_lines.append(text)
2598 #dumb_formatter(help_lines, text, 0)
2599 help_lines.append('{noformat}')
2600 return '\n'.join(help_lines)
2601
2602
2603 def handle_command_results(self):
2604 if len(self.commands) == 0:
2605 if self.words:
2606 return 'No applicable command: %s' % ' '.join(self.words)
2607 else:
2608 return '\nNo command for ' + self.header
2609
2610 help_lines = []
2611 # Deal with too much output
2612 if len(self.commands) > 10: # 10 is a magic number,
2613 help_lines.append('Help: Too many commands match (%d), '
2614 'Please choose more detail from item: ' %
2615 len(self.commands))
2616 help_lines.append(sdnsh.choices_text_builder(self.docs))
2617 return '\n'.join(help_lines)
2618
2619
2620 if self.header:
2621 help_lines.append(self.header)
2622 help_lines.append('')
2623
2624 (term_cols, term_rows) = sdnsh.pp.get_terminal_size()
2625 last_desc_name = None
2626 last_syntax_name = None
2627
2628 # all-help in intended to provide an overall short-help for
2629 # a collection of similarly syntax'ed commands.
2630 all_help = {}
2631 for command in self.commands:
2632 if command.get('all-help'):
2633 all_help[command.get('name')] = command.get('all-help')
2634 # when len(all_help) == 1, only ONE command collection group
2635 # is getting described
2636 if len(all_help) == 1:
2637 name = all_help.keys()[0]
2638 help_lines.append('%s Command: %s' %
2639 (name.capitalize(), all_help[name].capitalize()))
2640 all_help = True
2641 else:
2642 all_help = False
2643
2644 # Keep track of paragraphs displayed to prevent repeated
2645 # display of the same doc tags.
2646 shown_items = []
2647
2648 for command in self.commands:
2649
2650 doc_tag = command.get('doc')
2651 name = _get_command_title(command)
2652 short_help = command.get('short-help')
2653 if not all_help and short_help:
2654 help_lines.append('\n%s Command: %s\n' %
2655 (name.capitalize(), short_help.capitalize()))
2656 last_syntax_name = None # display syntax header
2657
2658 help_strings = []
2659 command_help_string = _get_command_syntax_help_string(
2660 command, self.prefix)
2661
2662 if type(command_help_string) == list:
2663 help_strings += command_help_string
2664 else:
2665 help_strings.append(command_help_string)
2666
2667 for h in sorted(help_strings):
2668 if last_syntax_name != name:
2669 if len(self.commands) > 1:
2670 help_lines.append(' Command Syntax:')
2671 else:
2672 help_lines.append('%s Command Syntax:' %
2673 name.capitalize())
2674 last_syntax_name = name
2675
2676 # use 75% of the width, since reformat_line isn't perfect.
2677 smaller_term_cols = (self.term_cols * 3) / 4
2678 if len(h) > smaller_term_cols:
2679 help_lines += reformat_line(h, smaller_term_cols, 2, 0)
2680 else:
2681 help_lines.append(' %s' % h)
2682 last_desc_name = None # print description header
2683
2684 if doc_tag:
2685 text = doc.get_text(self, doc_tag)
2686 if text != '' and doc_tag not in shown_items:
2687 shown_items.append(doc_tag)
2688 if last_desc_name != name:
2689 if self.format == None:
2690 help_lines.append('\n%s Command Description:' %
2691 name.capitalize())
2692 elif self.format == 'clidoc':
2693 help_lines.append('\n Description:')
2694 last_desc_name = name
2695 dumb_formatter(help_lines, text, 4)
2696 last_syntax_name = None # print 'Syntax' header
2697
2698
2699 if len(self.commands) > 1:
2700 for last_doc in self.docs_stack:
2701 if command['self'] in last_doc:
2702 (keyword, cmd_doc) = last_doc[command['self']]
2703 text = doc.get_text(self, cmd_doc)
2704 if text != '' and cmd_doc not in shown_items:
2705 shown_items.append(cmd_doc)
2706 if type(keyword) == tuple:
2707 keyword = keyword[0]
2708 help_lines.append('\nKeyword %s:' % keyword.capitalize())
2709 dumb_formatter(help_lines, text, 4)
2710
2711 doc_example = command.get('doc-example')
2712 if len(self.commands) > 1 and doc_example:
2713 text = doc.get_text(self, doc_example)
2714 if text != '' and doc_example not in shown_items:
2715 help_lines.append('Examples:')
2716 shown_items.append(doc_example)
2717 dumb_formatter(help_lines, text, 4)
2718
2719 if len(self.commands) == 1:
2720 short_help = None
2721 if self.is_no_command:
2722 #
2723 # Cobble together a help string for a no command, build
2724 # a default value for subcommand's, possibly other command
2725 # descriptions
2726 #
2727 short_help = self.commands[0].get('no-help')
2728 if not short_help:
2729 command_type = self.commands[0].get('command-type')
2730 action = self.commands[0].get('action')
2731 if ((command_type and command_type == 'config-submode') or
2732 (action and action == 'push-mode-stack')):
2733 mode_name = self.commands[0].get('name')
2734 short_help = 'Remove %s configuration' % mode_name
2735 else:
2736 short_help = self.commands[0].get('short-help')
2737
2738 for last_doc in self.docs_stack:
2739 if command['self'] in last_doc:
2740 (keyword, last_doc) = last_doc[command['self']]
2741 text = doc.get_text(self, last_doc)
2742 if type(keyword) == tuple:
2743 keyword = keyword[0]
2744 if sdnsh.description:
2745 help_lines.append("\t%s: %s %s" %
2746 (command['self'], keyword, last_doc))
2747 if text != '' and last_doc not in shown_items:
2748 shown_items.append(last_doc)
2749 help_lines.append('\nKeyword %s Description:' %
2750 keyword.capitalize())
2751 dumb_formatter(help_lines, text, 4)
2752
2753 if len(self.docs) > 0:
2754 if self.format == None:
2755 indent = ' '
2756 help_lines.append('\nNext Keyword Descriptions;')
2757 elif self.format == 'clidoc':
2758 indent = ' '
2759
2760 for (doc_name, doc_tags) in sorted(self.docs.items()):
2761 if len(doc_tags) == 1 and doc_tags[0] == None:
2762 if sdnsh.description:
2763 help_lines.append("\t%s: %s missing doc attribute" %
2764 (command['self'], doc_name))
2765 help_lines.append('%s%s:' % (indent, doc_name))
2766 elif len(doc_tags) == 1 and doc_tags[0].split('|')[0] == 'types' and \
2767 not doc_name.startswith(doc_tags[0].split('|')[1]):
2768 type_name = doc_tags[0].split('|')[1].capitalize()
2769 help_lines.append('%s%s: type %s' %
2770 (indent, doc_name, type_name))
2771 else:
2772 help_lines.append('%s%s:' % (indent, doc_name))
2773 for doc_tag in doc_tags:
2774 if sdnsh.description:
2775 help_lines.append("\t%s: %s %s" %
2776 (command['self'], doc_name, doc_tag))
2777 text = doc.get_text(self, doc_tag)
2778 if text == '':
2779 help_lines.append('')
2780 if text != '' and doc_tag not in shown_items:
2781 if len(doc_tags) > 1 and doc_tag.split('|')[0] == 'types':
2782 type_name = doc_tag.split('|')[1].capitalize()
2783 help_lines.append('\tType: %s' % type_name)
2784 shown_items.append(doc_tag)
2785 dumb_formatter(help_lines, text, 8)
2786
2787 doc_example = command.get('doc-example')
2788 if doc_example:
2789 text = doc.get_text(self, doc_example)
2790 if text != '':
2791 help_lines.append('Examples:')
2792 dumb_formatter(help_lines, text, 4)
2793
2794 if len(self.commands) > 1 and len(self.docs) > 0:
2795 if self.format == None:
2796 help_lines.append('\nNext Keyword Descriptions;')
2797 for (doc_name, doc_tags) in sorted(self.docs.items()):
2798 if len(doc_tags) == 1 and doc_tags[0] == None:
2799 if sdnsh.description:
2800 help_lines.append("\t%s: missing doc attribute" %
2801 command['self'])
2802 help_lines.append(' %s:' % doc_name)
2803 elif len(doc_tags) == 1 and doc_tags[0].split('|')[0] == 'types' and \
2804 not doc_name.startswith(doc_tags[0].split('|')[1]):
2805 type_name = doc_tags[0].split('|')[1].capitalize()
2806 help_lines.append(' %s: type %s' %
2807 (doc_name, type_name))
2808 else:
2809 help_lines.append(' %s:' % doc_name)
2810 for doc_tag in doc_tags:
2811 if doc_tag:
2812 if len(doc_tags) > 1 and doc_tag.split('|')[0] == 'types':
2813 type_name = doc_tag.split('|')[1].capitalize()
2814 help_lines.append('\tType: %s' % type_name)
2815 if sdnsh.description:
2816 help_lines.append("\t%s: %s %s" %
2817 (command['self'], doc_name, doc_tag))
2818 text = doc.get_text(self, doc_tag)
2819 if text != '' and doc_tag not in shown_items:
2820 shown_items.append(doc_tag)
2821 dumb_formatter(help_lines, text, 8)
2822 else:
2823 help_lines.append('')
2824 else:
2825 help_lines.append('')
2826
2827 return '\n'.join(help_lines)
2828
2829
2830def get_command_doc_tag(word):
2831 handler = CommandHandler()
2832 matching_commands = handler.get_matching_commands(word, False,
2833 command_registry)
2834 if len(matching_commands) == 1:
2835 return (matching_commands[0][0]).get('doc')
2836 if len(matching_commands) > 1:
2837 # error? retur value for multiple commands?
2838 pass
2839 return None
2840
2841
2842def get_command_documentation(words, header=None):
2843 handler = CommandDocumentor(header)
2844 # Hack to make "help no" work
2845 if len(words) == 1 and words[0] == 'no':
2846 words = words + ['']
2847 result = handler.handle_command(words)
2848 return result
2849
2850
2851def command_submode_dictionary(modes = None):
2852 if modes == None:
2853 modes = sdnsh.command_dict.keys() + sdnsh.command_nested_dict.keys()
2854 #
2855 # try to find all submode commands, build a dictionary
2856 reached_modes = []
2857 mode_nodes = {}
2858 mode_entries = []
2859 for c in command_registry:
2860 c_type = c.get('command-type')
2861 if c_type and c_type == 'config-submode':
2862 from_mode = c.get('mode')
2863 to_mode = c.get('submode-name')
2864 reached_modes.append(to_mode)
2865 if not from_mode in mode_nodes:
2866 mode_nodes[from_mode] = []
2867 mode_nodes[from_mode].append((to_mode, _get_command_title(c)))
2868 mode_entries.append({'mode' : from_mode,
2869 'submode' : to_mode,
2870 'command' : _get_command_title(c)})
2871 if sdnsh.description:
2872 print ', '.join(mode_nodes)
2873 print mode_nodes
2874 if [x for x in modes if not x in reached_modes]:
2875 print 'Missing ', [x for x in modes if not x in reached_modes]
2876
2877 return mode_entries
2878
2879
2880def get_clidoc(words):
2881 if len(words):
2882 handler = CommandDocumentor(header = None, format = 'clidoc')
2883 for word in words:
2884 for c in command_registry:
2885 if word == c['self']:
2886 handler.add_command(c)
2887 result = handler.handle_command_results()
2888 return result
2889 else:
2890 clidoc = []
2891 def commands_for_mode(mode):
2892 clidoc.append('\n\n\n============================= MODE ' + mode + '\n\n')
2893 for c in command_registry:
2894 if mode == c['mode']:
2895 handler = CommandDocumentor(header = '\n\n\n ---- MODE ' + mode,
2896 format = 'clidoc')
2897 handler.add_command(c)
2898 clidoc.append(handler.handle_command_results())
2899
2900 mode = 'login'
2901 commands_for_mode(mode)
2902
2903 # select commands by submode.
2904 mode_entries = command_submode_dictionary()
2905 for mode_entry in mode_entries:
2906 mode = mode_entry['submode']
2907 if mode[-1] == '*':
2908 mode = mode[:-1]
2909 if mode == 'config-':
2910 mode = 'config'
2911
2912 commands_for_mode(mode)
2913
2914 return ''.join(clidoc)
2915
2916def get_cliwiki(words):
2917 def wiki_special_character_handling(inputstr):
2918 inputstr=inputstr.replace('{','\{')
2919 inputstr=inputstr.replace('}','\}')
2920 inputstr=inputstr.replace('[','\[')
2921 inputstr=inputstr.replace(']','\]')
2922 return inputstr
2923 cliwikifile=open('cliwiki.txt', 'w')
2924 if not cliwikifile:
2925 print 'File creation failed \n'
2926 return
2927 cliwikifile.write('{toc:printable=true|style=disc|maxLevel=3|minLevel=1|class=bigpink|exclude=[1//2]|type=list|include=.*}')
2928 #write the introduction part
2929 introductionfile=open('documentation/en_US/introduction')
2930 str1=introductionfile.read()
2931 cliwikifile.write('\n')
2932 cliwikifile.write(str1)
2933 str1='\nh1. CLI Commands'
2934 cliwikifile.write(str1)
2935 #generate all commands in login/enable mode except show cmds
2936 mode=['login','enable']
2937 category=['show']
2938 for c in command_registry:
2939 name = c['name']
2940 if name not in category and c['mode'] in mode:
2941 category.append(name)
2942 handler = CommandDocumentor(header = 'login')
2943 for cc in command_registry:
2944 if name==cc['name'] and cc['mode'] in mode:
2945 handler.add_command(cc)
2946 str1 = handler.handle_command_results_wiki()
2947 str1= str1+ '\n'
2948 cliwikifile.write(str1)
2949 handler = CommandDocumentor(header = None)
2950 #generate all configuration commands: exclude internal commands
2951 mode=['config', 'config*']
2952 category=['internal'] #prune out all internal commands
2953 str1='\nh2. Configuration Commands'
2954 cliwikifile.write(str1)
2955 for c in command_registry:
2956 name = c['name']
2957 if name not in category and c['mode'] in mode:
2958 category.append(name)
2959 submode='config-' + name
2960 submode1=c.get('submode-name')
2961 if submode1 is not None:
2962 if submode1.find(submode)==-1:
2963 submode=submode1
2964 if name=='vns-definition':
2965 print submode
2966 str1="%s" % name
2967 str1=wiki_special_character_handling(str1)
2968 str1="\nh3. %s Commands " % str1.capitalize()
2969 cliwikifile.write(str1)
2970 handler = CommandDocumentor(header = None)
2971 handler.add_command(c)
2972 str1 = handler.handle_command_results_wiki()
2973 str1= str1+ '\n'
2974 cliwikifile.write(str1)
2975 for cc in command_registry:
2976 cmdmode=cc['mode']
2977 if (isinstance(cmdmode, str)):
2978 if (cmdmode.find(submode)!=-1) and (cc['name']!='show'):
2979 #special handling: prune vns command from tenant mode
2980 if (name != 'tenant') or ((name == 'tenant') and cmdmode.find('config-tenant-vns')==-1 and cmdmode.find('config-tenant-def-vns')==-1):
2981 handler = CommandDocumentor(header = None)
2982 handler.add_command(cc)
2983 str1 = handler.handle_command_results_wiki()
2984 str1= str1+ '\n'
2985 cliwikifile.write(str1)
2986 #generate all show commands
2987 name='show'
2988 str1='\nh2. Show Commands'
2989 cliwikifile.write(str1)
2990 mode=['config-internal'] #prune out all internal commands
2991 obj_type=[]
2992 for c in command_registry:
2993 if c['name']==name and c['mode'] not in mode:
2994 obj=c.get('obj-type')
2995 if obj and obj not in obj_type:
2996 obj_type.append(obj)
2997 str1="%s" % obj
2998 str1=wiki_special_character_handling(str1)
2999 str1="\nh3. Show %s Commands " % obj.capitalize()
3000 cliwikifile.write(str1)
3001 for cc in command_registry:
3002 if name==cc['name'] and obj==cc.get('obj-type') and cc['mode'] not in mode:
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
3009 cliwikifile.close()
3010 print 'CLI reference wiki markup file is generated successfully at: cli/cliwiki.txt.\n '
3011
3012#
3013# Lint: Verify requested commands
3014#
3015class CommandLint(CommandHandler):
3016
3017 def __init__(self):
3018 super(CommandLint, self).__init__()
3019 self.commands = []
3020
3021 def handle_incomplete_command(self, arg, arg_scopes, arg_data, fields, parsed_tag, command):
3022 if command not in self.commands:
3023 self.commands.append(command)
3024
3025 def handle_complete_command(self, prefix_matches, scopes, arg_data, fields, actions, command):
3026 if command not in self.commands:
3027 self.commands.append(command)
3028
3029 def lint_action_query_table(self, c_self, command, c_scope):
3030 # look for items which are not parameters
3031 attrs = ['proc', 'key', 'scoped', 'sort', 'crack', 'obj-type', 'proc']
3032 for a in c_scope:
3033 if not a in attrs:
3034 print '%s: unknown attribute %s for query-table' % (c_self, a)
3035 req = ['obj-type']
3036 for r in req:
3037 if not r in c_scope:
3038 print '%s: missing required attribute %s for query-table' % (
3039 c_self, r)
3040 if 'obj-type' in c_scope:
3041 if not mi.obj_type_exists(c_scope['obj-type']):
3042 print '%s: query-table no such obj-type %s' % (
3043 c_self, c_scope['obj-type'])
3044
3045 def lint_action_query_rest(self, c_self, command, c_scope):
3046 # look for items which are not parameters
3047 attrs = ['proc', 'url', 'rest-type', 'key', 'scoped', 'sort', 'append']
3048 for a in c_scope:
3049 if not a in attrs:
3050 print '%s: unknown attribute %s for query-rest' % (c_self, a)
3051 req = ['url']
3052 for r in req:
3053 if not r in c_scope:
3054 print '%s: missing required attribute %s for query-rest' % (
3055 c_self, r)
3056 if 'append' in c_scope:
3057 if type(c_scope['append']) != dict:
3058 print '%s: append parameter in query_rest' \
3059 'required to be a dict' % (c_self)
3060
3061 def lint_action_join_table(self, c_self, command, c_scope):
3062 # look for items which are not parameters
3063 attrs = ['proc', 'obj-type', 'key', 'key-value', 'add-field', 'join-field', 'crack']
3064 for a in c_scope:
3065 if not a in attrs:
3066 print '%s: unknown attribute %s for join-table' % (c_self, a)
3067 req = ['obj-type', 'key', 'join-field']
3068 for r in req:
3069 if not r in c_scope:
3070 print '%s: missing required attribute %s for query-table' % (
3071 c_self, r)
3072 if 'obj-type' in c_scope:
3073 if not mi.obj_type_exists(c_scope['obj-type']):
3074 print '%s: join-table no such obj-type %s' % (
3075 c_self, c_scope['obj-type'])
3076
3077 def lint_dict_action(self, c_self, command, c_action, c_scope):
3078 if not 'proc' in c_action:
3079 print '%s: "proc" expected for dict in action' % c_self
3080 return
3081 action = c_action['proc']
3082 if not action in action_registry:
3083 print '%s: action %s unknown' % (c_self, c_action)
3084 if c_action == 'display-rest':
3085 if not 'url' in c_action:
3086 print '%s: missing "url" for display-rest' % c_self
3087 if action == 'display-table':
3088 if not 'obj-type' in c_action:
3089 # could be in c_scope, or other nests.
3090 print '%s: missing "obj-type" for display-table' % c_self
3091 if action in ['query-table', 'query-table-append']:
3092 self.lint_action_query_table(c_self, command, c_action)
3093 if action in ['query-rest', 'query-table-rest']:
3094 self.lint_action_query_rest(c_self, command, c_action)
3095 if action == 'join-table':
3096 self.lint_action_join_table(c_self, command, c_action)
3097 if action == 'legacy-cli':
3098 print '%s: LEGACY-CLI, consider reimplementation' % c_self
3099
3100 def lint_action(self, c_self, command, c_action, c_type, c_scope):
3101 if type(c_action) == tuple:
3102 for t in c_action:
3103 if type(t) == list:
3104 print '%s: LIST nost supported as a member of actions' % c_self
3105 elif type(t) == dict:
3106 self.lint_dict_action(c_self, command, t, c_scope)
3107
3108 if type(c_action) == dict:
3109 self.lint_dict_action(c_self, command, c_action, c_scope)
3110 if type(c_action) == str:
3111 if not c_action in action_registry:
3112 print '%s: action %s unknown' % (c_self, c_action)
3113 if c_action == 'display-rest':
3114 if not 'url' in c_scope:
3115 print '%s: missing "url" for display-rest' % c_self
3116 if c_action in ['query-table', 'query-table-append']:
3117 self.lint_action_query_table(c_self, command, c_scope)
3118 if c_action in ['query-rest', 'query-table-rest']:
3119 self.lint_action_query_rest(c_self, command, c_scope)
3120 if c_action == 'join-table':
3121 self.lint_action_join_table(c_self, command, c_scope)
3122 if c_action == 'legacy-cli':
3123 print '%s: LEGACY-CLI, consider reimplementation' % c_self
3124
3125 def lint_choice(self, c_self, command, c_choice, c_type):
3126 for choice in c_choice:
3127 if type(choice) == tuple:
3128 # in order collection of terms, these aren't choices, but
3129 # here e can treat them that way
3130 for t in choice:
3131 if type(t) == list:
3132 print '%s: LIST nost supported as a member of choices' % c_self
3133 elif type(t) == dict:
3134 self.lint_choice(c_self, command, (t,), c_type)
3135 elif type(t) == str:
3136 # token, ought to validate char's in token
3137 pass
3138 else:
3139 print '%s: bad element type -> %s' % (c_self, type(t))
3140
3141 if 'command-type' in choice:
3142 print '%s: CHOICE contains "command-type", only' \
3143 ' intended to be used at top level' % c_self
3144 if not choice['command-type'] in command_type_defaults:
3145 print '%s: missing command-type %s' % (c_self, choice['command-type'])
3146 if 'action' in choice:
3147 self.lint_action(c_self, command, choice['action'], c_type, choice)
3148 if 'choices' in choice:
3149 self.lint_choice(c_self, command, choice['choices'], c_type)
3150
3151 def lint_command(self,command):
3152 c_self = command.get('self', 'UNKNOWN SELF NANE')
3153 print command['self']
3154
3155 c_name = command.get('name', None)
3156 if c_name == None:
3157 print '%s: no name defined' % c_self
3158
3159 c_mode = command.get('mode', None)
3160 if c_mode == None:
3161 print '%s: no submode defined' % c_self
3162
3163 c_short_help = command.get('short-help', None)
3164 if c_short_help == None:
3165 print '%s: no short-help defined' % c_self
3166
3167 c_obj_type = command.get('obj-type', None)
3168
3169 c_current_mode_obj_id = command.get('current-mode-obj-id', None)
3170 if 'current-mode-obj-id' in command:
3171 if c_current_mode_obj_id == None:
3172 print '%s: "current-mode-obj-id" not needed at top level' % c_self
3173
3174 c_parent_id = command.get('parent-id', None)
3175 if 'parent-id' in command:
3176 if c_parent_id == None:
3177 print '%s: "parent-id" not needed at top level' % c_self
3178
3179 c_type = command.get('command-type', None)
3180
3181 c_args = command.get('args', None)
3182 if c_args == None:
3183 print '%s: no args defined' % c_args
3184
3185 c_action = command.get('action', None)
3186
3187 if c_action or c_type:
3188 self.lint_action(c_self, command, c_action, c_type, command)
3189
3190 if type(c_args) == dict:
3191 if 'action' in c_args:
3192 self.lint_action(c_self, command, c_args['action'], c_type, c_args)
3193 if 'choices' in c_args:
3194 self.lint_choice(c_self, command, c_args['choices'], c_type)
3195 if 'choice' in c_args:
3196 print '%s: "choices" not "choice"' % c_self
3197 elif type(c_args) == tuple or type(c_args) == list:
3198 for a in c_args:
3199 if 'action' in a:
3200 self.lint_action(c_self, command, a['action'], c_type, a)
3201 if 'choices' in a:
3202 self.lint_choice(c_self, command, a['choices'], c_type)
3203 if 'choice' in a:
3204 print '%s: "choices" not "choice"' % c_self
3205 if 'field' in a:
3206 if c_obj_type:
3207 if not mi.obj_type_has_field(c_obj_type, a['field']):
3208 print '%s: %s MISSING FIELD %s' % (c_self,
3209 c_obj_type, a['field'])
3210
3211
3212 def handle_command_results(self):
3213 if len(self.commands) == 0:
3214 return 'No applicable command: %s' % ' '.join(self.words)
3215
3216 for command in self.commands:
3217 self.lint_command(command)
3218
3219 return '--'
3220
3221
3222def lint_command(words):
3223 lint = CommandLint()
3224 if len(words) == 0:
3225 for command in command_registry:
3226 lint.lint_command(command)
3227 else:
3228 for command in command_registry:
3229 if command['self'] in words:
3230 lint.lint_command(command)
3231 return ''
3232
3233
3234class CommandPermutor(CommandHandler):
3235
3236 def __init__(self, qualify = False):
3237 """
3238 @param qualify Generate qualify version of the permutations
3239 """
3240 super(CommandPermutor, self).__init__()
3241 self.commands = []
3242 self.collect = []
3243 self.qualify = qualify
3244
3245 self.obj_type = None
3246 self.obj_type_other = None
3247
3248
3249 def collect_complete_command(self, line):
3250 if self.qualify:
3251 # collect together parts, when a token appears,
3252 # replace the token with a procedure call.
3253 if len(line) == 0:
3254 return
3255 new_line = ['"']
3256 quoted = True
3257 if line[0][0] == '+': # unusual
3258 new_line = ['%s ' % line[0][1:]]
3259 line = line[1:]
3260 quoted = False
3261 for item in line:
3262 if quoted:
3263 if item[0] == '+':
3264 new_line.append('" + %s ' % item[1:])
3265 quoted = False
3266 else:
3267 new_line.append('%s ' % item)
3268 else:
3269 if item[0] == '+':
3270 new_line.append(' + " " + %s ' % item[1:])
3271 quoted = False
3272 else:
3273 new_line.append('+ " %s ' % item)
3274 quoted = True
3275 if quoted:
3276 new_line.append('"')
3277
3278 self.collect.append(''.join(new_line))
3279 else:
3280 self.collect.append(' '.join(line))
3281
3282
3283 def field_to_generator(self, field):
3284 """
3285 Convert the field name to a text field.
3286 When 'qualify' is set, replace the field name
3287 with a likely procedure to call instead
3288 """
3289 if self.qualify:
3290 # Many of the fields are actually fields in
3291 # the named obj-type. Take a shot at seeing if that's
3292 # the case, and call a sample value collector
3293
3294 if self.obj_type_other:
3295 # These are typically an obj_type|field names.
3296 parts = self.obj_type_other.split('|')
3297 o_field = field
3298 if len(parts) > 1:
3299 o_field = parts[1]
3300 # by using obj_type_has_model() instead of
3301 # obj_type_exists(), 'curl(1)' can be used in
3302 # the testing to find sample objects.
3303 if mi.obj_type_has_model(parts[0]):
3304 sample_obj_type = 'sample_obj_type'
3305 if self.is_no_command:
3306 sample_obj_type = 'sample_no_obj_type'
3307 if mi.obj_type_has_field(parts[0], o_field):
3308 return '+id.%s("%s", "%s")' \
3309 % (sample_obj_type, parts[0], o_field)
3310
3311 python_field = field.replace("-", "_")
3312 if self.obj_type:
3313 sample_obj_type = 'sample_obj_type'
3314 if self.is_no_command:
3315 sample_obj_type = 'sample_no_obj_type'
3316 if mi.obj_type_has_field(self.obj_type, field):
3317 return '+id.%s("%s", "%s")' \
3318 % (sample_obj_type, self.obj_type, field)
3319 else:
3320 return '+id.%s("%s")' % (python_field, self.obj_type)
3321 else:
3322 return '+id.%s()' % python_field
3323
3324 else:
3325 return "<%s>" % field
3326
3327
3328 def permute_item(self, item, line, next_args):
3329 """
3330 Deals with a single dictionary item. This can be:
3331 a choice, an arg, a token, or a field.
3332 """
3333
3334 if type(item) == str:
3335 line.append(item)
3336 self.permute_in_order(next_args[0], line, next_args[1:])
3337 return
3338
3339 if self.is_no_command:
3340 optional = item.get('optional-for-no', False)
3341 else:
3342 optional = item.get('optional', False)
3343
3344
3345 skip = item.get('permute') # XXX permute vs qualify skip?
3346
3347 if skip != 'skip' and optional:
3348 if len(next_args):
3349 self.permute_in_order(None, line, next_args)
3350 else:
3351 self.collect_complete_command(line)
3352
3353 choices = item.get('choices')
3354 if choices:
3355 self.permute_choices(choices, list(line), next_args)
3356 return
3357
3358 if skip == 'skip':
3359 return
3360
3361 token = item.get('token')
3362 tag = item.get('tag')
3363 field = item.get('field')
3364 args = item.get('args')
3365 obj_type = item.get('obj-type')
3366 if obj_type:
3367 self.obj_type = obj_type
3368
3369 # must do this after the recursive 'optional' calls.
3370 self.obj_type_other = item.get('other')
3371
3372 if args:
3373 if type(args) == dict:
3374 self.permute_item(args, line, next_args)
3375 elif type(args) == tuple:
3376 self.permute_in_order(args, list(line), next_args)
3377 else:
3378 print 'TYPE ARGS ', type(args)
3379 return
3380 elif token:
3381 line.append(token)
3382 elif field:
3383 item_type = item.get('type')
3384 if item_type == None:
3385 item_type = item.get('base-type')
3386 if item_type == 'enum':
3387 values = item.get('values')
3388 if values:
3389 if tag:
3390 line.append(tag)
3391 # convert the values into a tuple of dicts,
3392 # so that permute_choices can manage it
3393 if type(values) == str:
3394 values = [values]
3395 choices = tuple([{'token' : x} for x in values])
3396 self.permute_choices(choices, list(line), next_args)
3397 return
3398 else:
3399
3400 if tag:
3401 line.append(tag)
3402 line.append(self.field_to_generator(field))
3403
3404 #
3405 # fall-through to this common code, which should only be
3406 # reached when no other recursive descent is performed.
3407
3408 if len(next_args):
3409 self.permute_in_order(None, line, next_args)
3410 else:
3411 self.collect_complete_command(line)
3412
3413
3414 def permute_choices(self, choices, line, next_args):
3415 """
3416 When a 'choices': is reached, enumerate all the choices,
3417 and continue forward.
3418 """
3419 for pick in choices:
3420 if type(pick) == dict:
3421 new_line = list(line)
3422 self.permute_item(pick, new_line, next_args)
3423 if next_args == None:
3424 self.collect_complete_command(line)
3425 elif type(pick) == tuple:
3426 self.permute_in_order(pick, list(line), next_args)
3427 else:
3428 print "CHOICE? ", type(pick)
3429
3430
3431 def permute_in_order(self, items, line, next_args):
3432 if items == None and len(next_args):
3433 items = next_args
3434 next_args = []
3435 if len(items) == 0:
3436 # done
3437 self.collect_complete_command(line)
3438 return
3439 # see if the item is optional, if so then don't add the item,
3440 # and go to the next item in a recursive call
3441
3442 while len(items) and type(items[0]) == str:
3443 line.append(items[0])
3444 items = items[1:]
3445
3446 if len(items):
3447 item = items[0]
3448
3449 if len(items) > 1:
3450 if len(next_args) == 0:
3451 next_args = items[1:]
3452 else:
3453 if type(items[1:]) == tuple:
3454 next_args = list(items[1:]) + next_args
3455 else:
3456 next_args = items[1:] + next_args
3457 else:
3458 next_args = []
3459
3460 token = False
3461 if type(item) == tuple:
3462 self.permute_in_order(item, line, list(next_args))
3463 elif type(item) == dict:
3464 self.permute_item(item, list(line), list(next_args))
3465 else:
3466 print 'permute_in_order: need type', type(item)
3467 else:
3468 self.collect_complete_command(line)
3469
3470 return
3471
3472 def permute_command_common(self, command):
3473 args = command.get('args')
3474 if args == None:
3475 if sdnsh.description:
3476 print 'PERMUTE: missing args for %s' % command['self']
3477 return
3478 name = _get_command_title(command)
3479
3480 obj_type = command.get('obj-type')
3481 if obj_type:
3482 self.obj_type = obj_type
3483
3484 if type(command.get('name')) == dict:
3485 if self.qualify:
3486 name = self.field_to_generator(command['name']['field'])
3487
3488 if self.is_no_command:
3489 line = ['no', name]
3490 else:
3491 line = [name]
3492
3493 if type(args) == tuple:
3494 self.permute_in_order(args, line, [])
3495 elif type(args) == dict:
3496 self.permute_item(args, line, [])
3497 else:
3498 print 'PC ', type(args)
3499
3500 return '\n'.join(self.collect)
3501
3502
3503 def permute_command(self, command):
3504 print 'PERMUTE', command['self'], is_no_command_supported(command)
3505
3506 self.is_no_command = False
3507 result = self.permute_command_common(command)
3508
3509 if is_no_command_supported(command):
3510 # Now add the command for 'no'
3511 self.is_no_command = True
3512 result = self.permute_command_common(command)
3513
3514 return result
3515
3516
3517 def handle_command_results(self):
3518 collections = []
3519 for command in self.commands:
3520 collections.append(self.permute_command(command))
3521 return '\n'.join(collections)
3522
3523
3524 def handle_command(self, words):
3525 for word in words:
3526 for c in command_registry:
3527 if word == c['self']:
3528 return self.permute_command(c)
3529
3530
3531def permute_single_submode(submode_command, qualify):
3532 """
3533 Permute command for a submode, takes the command, finds
3534 all the related submodesmodes, permutes all the commands
3535 in the submode, then a recursive call to any submodes
3536 found within this one.
3537
3538 @param submode_command command dictionary of the submode command.
3539 Can be set to None as an indication of "root"
3540
3541 @param qualify boolean, describes whether to generate "permute"
3542 output or "qualify" output. Permute output is a human readable
3543 command permutation, while "qualify" is intended for script building
3544
3545 """
3546 permuted = []
3547 permute_submodes = []
3548
3549 if submode_command == None:
3550 mode = 'login'
3551 else:
3552 # first, the submode. There ought to be only one permutation.
3553 permute = CommandPermutor(qualify)
3554 permuted.append(permute.permute_command(submode_command))
3555 mode = submode_command.get('submode-name')
3556 print 'SUBMODE PERMUTE', submode_command['self'], 'MODE', mode
3557
3558 def submode_match(want, cmd):
3559 if type(cmd) == str:
3560 cmd = [cmd]
3561 if want.startswith('config'):
3562 for c in cmd:
3563 if c.endswith('*'):
3564 c = c[:-1]
3565 if c == 'config-':
3566 c = 'config'
3567 if want == c:
3568 return True
3569 else:
3570 return False
3571 for c in cmd:
3572 if want == c:
3573 return True
3574 return False
3575
3576 # there's no command to enter login.
3577 for c in command_registry:
3578 if submode_match(mode, c['mode']):
3579 # no submode commands.
3580 if c.get('command-type') == 'config-submode':
3581 print 'SUBMODE POSTPONE', c['self']
3582 permute_submodes.append(c)
3583 else:
3584 permute = CommandPermutor(qualify)
3585 permuted.append(permute.permute_command(c))
3586
3587 # now any submodes found
3588 for c in permute_submodes:
3589 permuted.append(permute_single_submode(c, qualify))
3590
3591 return '\n'.join(permuted)
3592
3593
3594def permute_command(words, qualify):
3595 """
3596 Permute all the commands (with no parameters), or a
3597 sigle command. The command is named via the name
3598 of the dictionary, for example:
3599 permute COMMAND_DESCRIPTION
3600 """
3601 if len(words) == 0:
3602 return permute_single_submode(None, qualify)
3603 else:
3604 permute = CommandPermutor(qualify)
3605 return permute.handle_command(words)
3606
3607
3608def _is_string(arg):
3609 """
3610 Returns whether or not the argument is a string (either a "str" or "unicode")
3611 """
3612 return isinstance(arg, types.StringTypes)
3613
3614
3615def _is_list(arg):
3616 """
3617 Returns whether or not the argument is a list. This means that it's an
3618 instance of a sequence, but not including the string types
3619 """
3620 return isinstance(arg, collections.Sequence) and not _is_string(arg)
3621
3622
3623def _lookup_in_scopes(name, scopes, default=None):
3624 """
3625 Look up the given name in the given list of scopes.
3626 Returns the default value if the name is not defined in any of the scopes.
3627 """
3628 assert name
3629 assert scopes is not None
3630
3631 # We iterate over the items in the scope (rather than using 'get')
3632 # so we can do a case-insensitive lookup.
3633 name_lower = name.lower()
3634 for scope in scopes:
3635 for key, value in scope.items():
3636 if key.lower() == name_lower:
3637 return value
3638 return default
3639
3640
3641def _call_proc(proc_descriptor, proc_registry, scopes, command):
3642 assert proc_descriptor is not None
3643 assert proc_registry is not None
3644
3645 if isinstance(proc_descriptor, collections.Sequence) and not _is_string(proc_descriptor):
3646 proc_descriptors = proc_descriptor
3647 else:
3648 proc_descriptors = (proc_descriptor,)
3649
3650 combined_result = None
3651
3652 saved_scopes = scopes
3653
3654 for proc_descriptor in proc_descriptors:
3655 args_descriptor = None
3656 scopes = saved_scopes
3657 if isinstance(proc_descriptor, collections.Mapping):
3658 # Look up the proc in the specified registry
3659 proc_name = proc_descriptor.get('proc')
3660 if not proc_name:
3661 raise error.CommandDescriptionError('Unspecified proc name: ',
3662 command)
3663 if proc_descriptor.get('args') or proc_descriptor.get('kwargs'):
3664 args_descriptor = proc_descriptor
3665 scopes = [proc_descriptor] + scopes
3666 else:
3667 proc_name = proc_descriptor
3668
3669 # Push an empty scope on the scope stack to hold a variable for the scopes
3670 # FIXME: This should be cleaned up when we change the calling conventions
3671 # for procs.
3672 scopes = [{}] + scopes
3673 scopes[0]['scopes'] = scopes
3674
3675 proc_and_arg = proc_registry.get(proc_name)
3676 if not proc_and_arg:
3677 raise error.CommandDescriptionError('Unknown proc name: %s' % proc_name, command)
3678 proc, default_args_descriptor = proc_and_arg
3679
3680 if not args_descriptor:
3681 args_descriptor = default_args_descriptor
3682
3683 converted_args = []
3684 converted_kwargs = {}
3685
3686 # Convert the positional args
3687 args = _get_args(args_descriptor)
3688 if args:
3689 for arg in args:
3690 if _is_string(arg) and len(arg) > 1 and arg[0] == '$':
3691 name = arg[1:]
3692 for scope in scopes:
3693 if name in scope:
3694 value = scope[name]
3695 break
3696 else:
3697 # FIXME: Disabling treating args that can't be resolved as errors, so that
3698 # they can instead be interpreted as just using the default value for the
3699 #raise error.CommandDescriptionError('Invalid proc argument: %s ' %
3700 # name, command)
3701 continue
3702 converted_args.append(value)
3703
3704 # Convert the keyword args
3705 kwargs = args_descriptor.get('kwargs')
3706 if kwargs:
3707 for key, value in kwargs.iteritems():
3708 if _is_string(value) and len(value) > 1 and value[0] == '$':
3709 name = value[1:]
3710 for scope in scopes:
3711 if name in scope:
3712 value = scope[name]
3713 break
3714 else:
3715 # FIXME: Don't treat args that can't be resolved as errors, so that
3716 # they can instead be interpreted as just using the default value for the
3717 #raise error.CommandDescriptionError('Invalid proc argument: %s ' %
3718 # name, command)
3719 continue
3720 converted_kwargs[key] = value
3721
3722 # Call the proc
3723 # pylint: disable=W0142
3724 result = proc(*converted_args, **converted_kwargs)
3725
3726 if result is not None:
3727 combined_result = combined_result + result if combined_result else result
3728
3729 return combined_result
3730
3731
3732def _add_applicable_modes(command, mode_dict):
3733 """
3734 Helper function for _get_applicable_modes to add any modes for this command
3735 """
3736 feature = command.get('feature')
3737 if feature:
3738 if not sdnsh.feature_enabled(feature):
3739 return
3740
3741 mode = command.get('mode')
3742 if mode:
3743 if type(mode) == list:
3744 for m in mode:
3745 mode_dict[m] = None
3746 else:
3747 mode_dict[mode] = None
3748
3749
3750def _get_applicable_modes(command):
3751 """
3752 Returns a list of all of the modes that are specified for this command
3753 """
3754 mode_dict = {}
3755 _add_applicable_modes(command, mode_dict)
3756 return mode_dict.keys()
3757
3758
3759def _exact_mode_match(current_mode, command_modes):
3760 """
3761 Return True when this command_mode is an exact match
3762 for this current mode (used to partition list of commands
3763 into two groups, one exact level, and for for related mode
3764 matches)
3765 """
3766 if not type(command_modes) == list:
3767 command_modes = [command_modes]
3768 for mode in command_modes:
3769 if mode == current_mode:
3770 return True
3771 if mode.endswith('*') and mode[:-1] == current_mode:
3772 return True
3773 return False
3774
3775
3776def _match_current_modes(command, current_mode, modes):
3777 """
3778 Even when the current mode isn't in the list of modes,
3779 there's a few modes in the mode list which are intended to
3780 be a collection of modes, eg: "config*", and "config-*",
3781 For any mode which ends in an ('*'), the current mode
3782 will match when the current mode is prefix of the mode
3783 (without the last '*' character)
3784 'config*' is intended to match any config mode, while
3785 'config-*' is intended to match any config submode.
3786 """
3787 if current_mode in modes:
3788 return True
3789 #
3790 # if the modes is enable, this works everywhere
3791 #
3792 if 'login' in modes:
3793 return True
3794 #
3795 # if the modes is login, and the mode is anything but login,
3796 # then this is true
3797 #
3798 if 'enable' in modes and current_mode != 'login':
3799 return True
3800 for mode in modes:
3801 if mode.endswith('*') and current_mode.startswith(mode[:-1]):
3802 return True
3803 #if command.get('command-type') == 'config-submode':
3804 # for mode in modes:
3805 # if current_mode.startswith(mode):
3806 # print "_match_current_modes: current command type is config-submode",current_mode,mode
3807 # return True
3808
3809 return False
3810
3811
3812def _get_command_title(command):
3813 if type(command['name']) == str:
3814 return command['name']
3815 if type(command['name']) == dict:
3816 return command['name']['title']
3817 else:
3818 raise Exception("Command %s has unknown title" % command['name'])
3819
3820
3821def _lookup_command_candidates(command_prefix, command_list):
3822 """
3823 Returns the list of command candidates from the given command list.
3824 A candidate must have a 'mode' value that matches the current mode,
3825 and its name must begin with the given command prefix.
3826 """
3827 candidates = []
3828 current_mode = sdnsh.current_mode()
3829 try:
3830 for command in command_list:
3831 modes = _get_applicable_modes(command)
3832 if _match_current_modes(command, current_mode, modes):
3833 name = command['name']
3834 if (type(name) == str and
3835 name.startswith(command_prefix.lower())):
3836 candidates.append(command)
3837 # should check the type of command_prefix,
3838 # and for str, ame.match(command_prefix):
3839 if type(name) == dict:
3840 if 're' not in name:
3841 command['name']['re'] = re.compile(name['pattern'])
3842 if name['re'].match(command_prefix):
3843 candidates.append(command)
3844 if type(name) == dict and \
3845 name['re'](command_prefix):
3846 candidates.append(command)
3847
3848 except Exception, _e:
3849 raise error.CommandDescriptionError('Missing mode or name: ', command)
3850
3851 return candidates
3852
3853def do_command(words):
3854 executor = CommandExecutor()
3855 result = executor.handle_command(words)
3856 return result
3857
3858def init_command(bs, modi):
3859 # FIXME HACK: sdnsh global to access top-level CLI object
3860 global sdnsh, mi
3861 sdnsh = bs
3862 mi = modi
3863
3864 c_actions.init_actions(bs, modi)
3865 c_data_handlers.init_data_handlers(bs, modi)
3866 c_completions.init_completions(bs, modi)
3867 c_validations.init_validations(bs, modi)
3868
3869
3870 add_command_type('config', {
3871 'action': 'write-fields',
3872 'no-action': 'reset-fields'
3873 })
3874
3875 add_command_type('config-object', {
3876 'action': 'write-object',
3877 'no-action': 'delete-objects',
3878 })
3879
3880 add_command_type('config-submode', {
3881 'action': 'push-mode-stack',
3882 #'no-action': 'delete-child-objects',
3883 'no-action': 'delete-objects',
3884 })
3885
Srikanth Vavilapallib5c3ca52014-12-15 15:59:33 -08003886 add_command_type('create-tunnelset', {
3887 'action': 'create-tunnelset'
3888 })
3889
3890 add_command_type('remove-tunnelset', {
3891 'action': 'remove-tunnelset'
3892 })
3893
Srikanth Vavilapalli1725e492014-12-01 17:50:52 -08003894 add_command_type('create-tunnel', {
3895 'action': 'create-tunnel'
3896 })
3897
3898 add_command_type('remove-tunnel', {
3899 'action': 'remove-tunnel'
3900 })
3901
3902 add_command_type('create-policy', {
3903 'action': 'create-policy'
3904 })
3905
3906 add_command_type('remove-policy', {
3907 'action': 'remove-policy'
3908 })
3909
3910 add_command_type('display-table', {
3911 'action': 'display-table'
3912 })
3913
3914 add_command_type('display-rest', {
3915 'action': 'display-rest'
3916 })
3917
3918 add_command_type('manage-alias', {
3919 'action' : 'create-alias',
3920 'no-action' : 'delete-alias',
3921 })
3922
3923 add_command_type('manage-tag', {
3924 'action' : 'create-tag',
3925 'no-action' : 'delete-tag',
3926 })
3927
3928 add_command_type('update-config', {
3929 'action' : 'update-config',
3930 'no-action' : 'update-config',
3931 })
3932
3933 # Initialize typedefs
3934 add_typedef({
3935 'name': 'string',
3936 'validation': 'validate-string'
3937 })
3938
3939 add_typedef({
3940 'name': 'integer',
3941 'validation': 'validate-integer'
3942 })
3943
3944 add_typedef({
3945 'name': 'hex-or-decimal-integer',
3946 'validation': 'validate-hex-or-dec-integer'
3947 })
3948
3949 add_typedef({
3950 'name': 'date',
3951 'validation': 'validate-date'
3952 })
3953
3954 add_typedef({
3955 'name': 'duration',
3956 'validation': 'validate-duration'
3957 })
3958
3959 add_typedef({
3960 'name': 'enum',
3961 'validation': 'validate-enum'
3962 })
3963
3964 add_typedef({
3965 'name' : 'mac-address',
3966 'help-name' : 'MAC Address',
3967 'base-type' : 'string',
3968 'validation' : 'validate-mac-address',
3969 })
3970
3971 add_typedef({
3972 'name' : 'host',
3973 'help-name' : 'Host Alias or MAC Address',
3974 'base-type' : 'string',
3975 'validation' : 'validate-host',
3976 })
3977
3978 add_typedef({
3979 'name' : 'vlan',
3980 'help-name' : 'Vlan',
3981 'base-type' : 'integer',
3982 'validation' : 'validate-integer',
3983 })
3984
3985 add_typedef({
3986 'name' : 'label',
3987 'help-name' : 'Segment Label',
3988 'base-type' : 'integer',
3989 'validation' : 'validate-integer',
3990 })
3991
3992 add_typedef({
3993 'name' : 'dpid',
3994 'help-name' : 'switch id (8-hex bytes)',
3995 'base-type' : 'string',
3996 'validation' : 'validate-switch-dpid',
3997 })
3998
3999 add_typedef({
4000 'name' : 'ip-address',
4001 'help-name' : 'IP address (dotted quad)',
4002 'base-type' : 'string',
4003 '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])$',
4004 })
4005
4006 add_typedef({
4007 'name' : 'ip-address-not-mask',
4008 'help-name' : 'IP Address',
4009 'base-type' : 'string',
4010 'pattern' : r'^(\d{1,3}\.){3}\d{1,3}$',
4011 'validation' : 'validate-ip-address-not-mask'
4012 })
4013
4014 add_typedef({
4015 'name' : 'cidr-range',
4016 'help-name' : 'cidr range (ip-address/int)',
4017 'base-type' : 'string',
4018 'pattern' : r'^(\d{1,3}\.){3}\d{1,3}/\d{1,2}?$',
4019 'validation' : 'validate-cidr-range',
4020 })
4021
4022 add_typedef({
4023 'name' : 'netmask',
4024 'help-name' : 'netmask (eg: 255.255.255.0)',
4025 'base-type' : 'ip-address',
4026 'validation' : 'validate-netmask'
4027 })
4028
4029 add_typedef({
4030 'name' : 'obj-type',
4031 'help-name' : 'configured object',
4032 'base-type' : 'string',
4033 'validation' : 'validate-existing-obj'
4034 })
4035
4036 add_typedef({
4037 'name' : 'inverse-netmask',
4038 'help-name' : 'inverse netmask (eg: 0.0.0.255)',
4039 'base-type' : 'ip-address',
4040 'validation' : 'validate-inverse-netmask'
4041 })
4042
4043 add_typedef({
4044 'name' : 'domain-name',
4045 'help-name' : 'Domain name',
4046 # Simple domain name checking.
4047 # Allows some things that aren't valid domain names, but doesn't
4048 # disallow things liky punycode and fully qualified domain names.
4049 'pattern' : r'^([a-zA-Z0-9-]+.?)+$',
4050 'base-type' : 'string'
4051 })
4052
4053 add_typedef({
4054 'name': 'ip-address-or-domain-name',
4055 'help-name': 'IP address or domain name',
4056 'base-type': 'string',
4057 'pattern': (
4058 r'^([a-zA-Z0-9-]+.?)+$', # simple domain name
4059 r'^(\d{1,3}\.){3}\d{1,3}$' # IP address
4060 ),
4061 # for ip addresses, ought to validate non-mask values, ie: 0.0.0.0, 255.255.255.255
4062 # ought to also validate ip addresses which aren't ip address, the dhcp 169.x values.
4063 })
4064
4065 add_typedef({
4066 'name' : 'resolvable-ip-address',
4067 'help-name' : 'resolvable ip address',
4068 'base-type' : 'string',
4069 'pattern' : (
4070 r'^([a-zA-Z0-9-]+.?)+$', # simple domain name
4071 r'^(\d{1,3}\.){3}\d{1,3}$' # IP address
4072 ),
4073 'validation' : 'validate-resolvable-ip-address',
4074 })
4075
4076 add_typedef({
4077 'name': 'enable-disable-flag',
4078 'help-name': 'Enter "enable" or "disable"',
4079 'base-type': 'enum',
4080 'values': ('disable', 'enable'),
4081 })
4082
4083 add_typedef({
4084 'name' : 'identifier',
4085 'help-name' : 'Alphabetic character, followed by alphanumerics',
4086 'base-type' : 'string',
4087 'validation' : 'validate-identifier',
4088 })
4089
4090 add_typedef({
4091 'name' : 'config',
4092 'help-name' : 'name of config file',
4093 'base-type' : 'string',
4094 'validation' : 'validate-config',
4095 })
4096