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