| #!/usr/bin/python |
| # |
| # Copyright (c) 2010,2011,2012,2013 Big Switch Networks, Inc. |
| # |
| # Licensed under the Eclipse Public License, Version 1.0 (the |
| # "License"); you may not use this file except in compliance with the |
| # License. You may obtain a copy of the License at |
| # |
| # http://www.eclipse.org/legal/epl-v10.html |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
| # implied. See the License for the specific language governing |
| # permissions and limitations under the License. |
| # |
| |
| # sdnsh - The Controller Shell |
| # -------------------------------------------------------------------------------- |
| # |
| # LOOK! TODO -- |
| # |
| # create tokens for modify actions in StaticFlowEntryPusher and here |
| # |
| # extend completion to show help text |
| # |
| # need to verify we can talk to the controller |
| # |
| # flow-entry naming - unique across all switches? |
| # |
| # joins on show commands (port names, ids, etc.) |
| # |
| # accept enums in addition to numbers - "ether-type arp" or "ether-type ip" |
| # |
| # cli commands not mapping to database model (this is an -undo-) |
| # |
| # simple options/singletons in the CLI ("logging level 5" "twitter username blah") |
| # |
| # csv, left/right justification in columns, sorts |
| # |
| # completion with multi command lines |
| # |
| # disallow 'switch' field and name as options flow-entry ('key' fields in general) |
| # |
| # delayed commit semantics (depends on 1?) |
| # |
| # delete config files? |
| # |
| # -------------------------------------------------------------------------------- |
| # |
| # Notes: |
| # |
| # support functions for a command, consider, for example, 'vns-definition' |
| # |
| # For a new command, for example 'vns-definition', there will be a variety |
| # of different support functions to manage different required functionaly. |
| # do_vns_definition() describes what to do to execute the actual command. |
| # cp_vns_definition() describes how to perform command completion for the same. |
| # do_show_vns_definition() would describe how to show the vns-definitions |
| # (this procedure doesn't exist, but is implemented by 'show vns' via |
| # do_show_vns), while |
| # cp_show_vns_definition() would describe how to complete the show command for |
| # this particular command. Other functions may also be needed, for example |
| # specialized changes to the 'no' command for the same command. |
| # |
| # command completion |
| # |
| # currently in many cases, there's a specialized completion handler |
| # it would make sense to manage 'no' completion by overloading the |
| # same completion handler. |
| # |
| # |
| # error processing: |
| # |
| # Keep in mond that al errors ought to use a similar prefix so that |
| # all the error conditions acan be identifyed by the automated test |
| # procedures. these prefixes are regular expressions in bigtest/cli.py |
| # The prefixed currently recognized as errors are 'Error: Not Found", |
| # 'Error running command ', 'Syntax error:', and 'Syntax: ' the last |
| # of which is used to display both usage for some commands, and |
| # various specific syntax errors |
| # |
| # alias management: |
| # |
| # Aliases are managed by having a separate table/model for each |
| # of the aliases, for example the host table has an associated alias |
| # table called 'host-alias'. These alias models are nothing more |
| # than an id, which is the alias name and a foreign key for the other |
| # table, in the 'host-alias' model, the associated foreign key would |
| # point to the host. |
| # |
| # Alias tables or models are not directly configured as alias tables, |
| # the cli can find these tables by noticing the only fields within |
| # the model is a foreign key for another table. The cli builds the |
| # alias_obj_type_xref dictionary during initialization, and later uses |
| # that association to manage aliases. Knowing which of the tables |
| # are alias tables also allows the cli to exclude these tables from |
| # modification in typical config mode. |
| # |
| # Alias are created when within the submode for a particular obj_type, |
| # for example, the host-alias table is available within the host |
| # config submode, and it appears as the 'host-alias' table there. |
| # However no further submode for the host-alias is managed; it is |
| # treated as if it were a field in the submode config object. |
| # So if 'host-alias xxx' were entered while in host config submode, |
| # the cli converts the request into a host-alias table entry. |
| # |
| # Feature management. The cli is intended to manage the availability |
| # of specific feature groups. This isn't meant to prevent a user from |
| # getting at specific sku features (ie: licensing), but instead meant |
| # to allow a user to disable specific sub-groups of commands to |
| # prevent unintended enabling of features. For example, |
| # to prevent a 'static-flow' cli configured controller from enabling |
| # vns features likely resulting in a misconfigured environment. |
| # |
| # changes: |
| # |
| # -- new source field for obj_types in models, obj_types which are |
| # written based on discovered details are typically not editable. |
| # don't allow a config submode entry. and don't |
| # advertise such an entry is possible |
| # |
| # -- integrated alias support, no special cases through the use |
| # of a separated alias tables. this allows more integrated |
| # cli support for tables specifically constructed to manage |
| # alias names for other fields. Currently code in the cli |
| # prevents multiple alias for a single foreign key, but this |
| # behavior can be relaxed (perhaps it ought to be configurable?) |
| # |
| # -- perform alias substitution for 'no' commands obj_type's |
| # |
| # -- support for primary key/id changes within a row. |
| # if an update occurs which modifies the primary key of a row, |
| # the cli now identifies whether any foreign keys point at |
| # the existing entry, and updates the foreign key to reference |
| # the updated primary key. |
| # |
| # -- support for cascaded delete. |
| # when a row is deleted, if there are foreign keys associated |
| # with the row, the cli will remove them. the climodelinfo |
| # configures which obj_types are included in the cascade deletes. |
| # |
| # -- vns interface creation. |
| # vns interfaces are required to have a vns-interface-rule associated. |
| # sdnplatform writes both the interface name and the rule, but the cli needed |
| # to have code added to determine and validate the associated rule. |
| # The validation attempts to ensure the correct rule is associated with |
| # the use of the interface name (id). |
| # |
| |
| import subprocess |
| import os, atexit, stat |
| import sys, traceback # traceback.print_exc() |
| from optparse import OptionParser |
| from types import StringType |
| import collections |
| import datetime |
| import json |
| import re |
| import time |
| import urllib2 |
| import httplib # provides error processing for isinstance |
| import socket |
| import select |
| import fcntl |
| import posixpath |
| import random |
| import copy |
| import utif |
| import fmtcnv |
| import run_config |
| import imp |
| import locale |
| |
| from pkg_resources import resource_filename |
| from modi import Modi |
| from midw import * |
| from vnsw import * |
| from prettyprint import PrettyPrinter |
| from storeclient import StoreClient |
| from climodelinfo import CliModelInfo |
| from vendor import VendorDB |
| import error |
| import command |
| import rest_to_model |
| import tech_support |
| import url_cache |
| import doc |
| import sdndb |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # |
| |
| class ParamException(Exception): |
| def __init__(self, value): |
| self.value = value |
| def __str__(self): |
| return repr(self.value) |
| |
| class TooManyVNSException(Exception): |
| def __init__(self, value): |
| self.value = value |
| def __str__(self): |
| return repr(self.value) |
| |
| TIMESTAMP_HELP = \ |
| ('Enter an integer timestamp in milliseconds since the epoch or formatted\n' + |
| 'as one of the following options in your local timezone:\n' |
| '"YYYY-MM-DD HH:MM:SS" YYYY-MM-DDTHH:MM:SS\n' + |
| '"YYYY-MM-DD HH:MM:SS+TTT" YYYY-MM-DDTHH:MM:SS+TTT\n' + |
| 'YYYY-MM-DD MM-DD\n' + |
| 'HH:MM now') |
| |
| |
| # This is a bit of a hack. The completion and validation functions for time zones |
| # are static methods, but they need to access the SDNSh object to get at the |
| # controller field to be able to do a REST call to load the list of time zone |
| # strings. So we set up a global for the single SDNSh instance we have. An |
| # alternative would have been to have a global variable for the list of time |
| # zone strings that's initialized when the CLI is initialized, but it seems |
| # nicer to only load it on demand, especially since setting the time zone |
| # will be a pretty infrequently invoked command. |
| cli = None |
| |
| # |
| # Constants for packet tracing |
| # |
| SESSIONID = 'sessionId' |
| FT_VNS = 'vns' |
| FT_PORT = 'port' |
| FT_PORT_DPID = 'dpid' |
| FT_PORT_PORT = 'port' |
| FT_OUTPUT = 'output' |
| FT_DIRECTION = 'direction' |
| FT_PERIOD = 'period' |
| FT_TIMEOUT = "FilterTimeout" |
| FT_PERIOD_DEFAULT = 300 |
| |
| onos=1 |
| # |
| # -------------------------------------------------------------------------------- |
| # |
| class NotFound(Exception): |
| def __init__(self, obj_type, name): |
| self.obj_type = obj_type |
| self.name = name |
| def __str__(self): |
| return "Not Found: %s %s" % (self.obj_type, self.name) |
| |
| |
| # LOOK!: The next two functions are copied from the sdncon code. |
| # Should ideally figure out a way to share the code for utility |
| # functions like this. |
| |
| def dotted_decimal_to_int(ip): |
| """ |
| Converts a dotted decimal IP address string to a 32 bit integer |
| """ |
| bytes = ip.split('.') |
| ip_int = 0 |
| for b in bytes: |
| ip_int = (ip_int << 8) + int(b) |
| return ip_int |
| |
| |
| def same_subnet(ip1, ip2, netmask): |
| """ |
| Checks whether the two ip addresses are on the same subnet as |
| determined by the netmask argument. All of the arguments are |
| dotted decimal IP address strings. |
| """ |
| ip1_int = dotted_decimal_to_int(ip1) |
| ip2_int = dotted_decimal_to_int(ip2) |
| netmask_int = dotted_decimal_to_int(netmask) |
| return (ip1_int & netmask_int) == (ip2_int & netmask_int) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # |
| |
| class SDNSh(): |
| |
| debug = False # general cli debugging |
| debug_backtrace = False # backtrace on failures |
| description = False # help debug command descriptions |
| display_rest = False # display rest call details |
| display_reply_rest = False # display rest call replies details |
| |
| command_dict = {} |
| mode_stack = [] |
| reserved_words = ['unknown', 'all'] |
| |
| known_controllers = ["127.0.0.1:8000"] |
| controller = None |
| # LOOK! manage this separately until we can cetainly |
| # talk to eth0 addr instead of 127.0.0.1 |
| controller_for_prompt = "127.0.0.1:8000" |
| cluster = "default" |
| |
| run = True |
| |
| # Are we running in batch mode, i.e. "cat commands | cli" |
| batch = False |
| |
| # used for parsing as an arg |
| local_name_pattern = "config://([A-Za-z0-9_:@\-\.\/]+$)" |
| |
| # name of boot/startup-config |
| boot_config_filename = '/opt/sdnplatform/run/boot-config' |
| saved_configs_dirname = '/opt/sdnplatform/run/saved-configs/' |
| feature_allow_experimental = '/opt/sdnplatform/feature/experimental' |
| |
| # contained objects |
| pp = None |
| store = None |
| model_handler = None |
| |
| # cached metadata |
| stats_metadata = None |
| stats_type_metadata = None |
| stats_restapi_map = {'controller-node': 'controller'} |
| |
| stats_optional_params = { |
| 'start-time': {'type': 'string', |
| 'syntax_help': TIMESTAMP_HELP}, |
| 'end-time': {'type': 'string', |
| 'syntax_help': TIMESTAMP_HELP}, |
| 'duration': {'type': 'string', |
| 'syntax_help': |
| "Enter an integer followed by a time unit\n" + |
| "[integer]weeks [integer]days [integer]hours\n" + |
| "[integer]mins [integer]secs [integer]ms"}, |
| 'sample-interval': {'type': 'int', |
| 'syntax_help': 'Enter an integer number of milliseconds'}, |
| 'sample-count': {'type': 'int', |
| 'syntax_help': 'Enter an integer target sample count'}, |
| 'sample-window': {'type': 'string', |
| 'syntax_help': 'Enter an integer number of milliseconds for sampling window'}, |
| 'data-format': {'type': 'enum', 'values': ['value', 'rate']}, |
| 'display': {'type': 'enum', 'values': ['latest-value', 'graph', 'table']}, |
| #'limit': {'type': 'int'} |
| } |
| |
| netvirt_feature_enabled_cached = False |
| warning_suppress = False # |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # warning |
| # When a config is getting replayed, warnings are suporessed. Other situations |
| # may also require supression of warnings. |
| # |
| def warning(self, message): |
| if not self.warning_suppress: |
| print "Warning: %s" % message |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # supress_warnings(self) |
| # |
| def suppress_warnings(self): |
| self.warning_suppress = True |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # enable_warnings(self) |
| # |
| def enable_warnings(self): |
| self.warning_suppress = False |
| |
| |
| config_replay = False # only true while replaying config files |
| # |
| # -------------------------------------------------------------------------------- |
| # config_replay_start |
| # Behavior is different during config replay. Warnings are |
| # suppressed, and other error conditions may be relaxed, for example |
| # the switch submode interface command requires that the interface exist, |
| # and the interface names are discovered by sdnplatform, which means that during |
| # config replay for statup, an unknown-named-interface may need to be accepted |
| # since that interface may appear once the switch connected to sdnplatform, and |
| # sdnplatform writes the ports for this switch. |
| # |
| def config_replay_start(self): |
| self.suppress_warnings() |
| self.config_replay = True |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # config_replay_done |
| # |
| def config_replay_done(self): |
| self.enable_warnings() |
| self.config_replay = False |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # config_replay_active |
| # Returns true when a configuration is getting replayed |
| # |
| def config_replay_active(self): |
| return self.config_replay |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # note |
| # When a config is getting replayed, warnings/notes are suporessed. Other situations |
| # may also require supression of warnings. |
| # |
| def note(self, message): |
| if not self.warning_suppress: |
| print "Note: %s" % message |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # debug_msg |
| # Debugging message cover. Only enabled for display once 'debug cli' is performed. |
| # |
| def debug_msg(self, message): |
| if self.debug: |
| print "debug_msg: %s" % message |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # error_msg |
| # Error message cover, to ensure consistent error messages |
| # |
| @staticmethod |
| def error_msg(message): |
| return "Error: %s" % message |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # syntax_msg |
| # Syntax error message cover, to ensure consistent error messages |
| # |
| @staticmethod |
| def syntax_msg(message): |
| return "Syntax: %s" % message |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # completion_error_msg |
| # Error message cover during completion, to ensure consistent error messages |
| # |
| def completion_error_msg(self, message): |
| self.print_completion_help("Error: %s" % message) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # completion_syntax_msg |
| # Syntax error message cover during completion, to ensure consistent error messages |
| # |
| def completion_syntax_msg(self, message): |
| self.print_completion_help("Syntax: %s" % message) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # init_history_file |
| # |
| def init_history_file(self): |
| history_file = os.path.join(os.environ["HOME"], ".sdnsh_history") |
| # create the initial history file group-writable, |
| # so that the tacacs users can write to it |
| if os.path.exists(history_file): |
| st = os.stat(history_file) |
| if not (st[stat.ST_MODE] & stat.S_IWGRP): |
| buf = open(history_file).read() |
| os.rename(history_file, history_file + "~") |
| mask = os.umask(007) |
| fno = os.open(history_file, os.O_WRONLY | os.O_CREAT, 0660) |
| os.umask(mask) |
| os.write(fno, buf) |
| os.close(fno) |
| else: |
| mask = os.umask(007) |
| fno = os.open(history_file, os.O_WRONLY | os.O_CREAT, 0660) |
| os.umask(mask) |
| os.close(fno) |
| try: |
| readline.read_history_file(history_file) |
| except IOError: |
| pass |
| atexit.register(readline.write_history_file, history_file) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # init_command_dict |
| # |
| def init_command_dict(self): |
| |
| # |
| # command_submode_dict saves a little information about |
| # submode commands. The index is 'mode', and the resulting |
| # list contains strings, each of which is a submode command |
| # available in that mode. |
| self.command_submode_dict = {} |
| |
| # |
| # associate command names with features, so that when |
| # help is provided, command names which have associated |
| # features are validated to determine whether the feature |
| # is enabled or not. |
| self.command_name_feature = {} |
| |
| # |
| # command_nested_dict is indexed by mode, and contains |
| # command configured with a trailing '*', for example |
| # 'mode' : 'config-*', intended to mean "all nested |
| # submodes after config-'. This can't be computed until |
| # all submodes are known, which means all the command |
| # describption must be configured., for these to make sense. |
| self.command_nested_dict = {} |
| |
| # |
| # config mode commands |
| # |
| #self.command_dict['config'] = [ "boot" ] |
| |
| # |
| # commands which start at 'login' |
| # |
| if onos == 0: |
| self.command_nested_dict['login'] = [ 'show', 'logout', 'exit', |
| 'history', 'help', 'echo', |
| 'date', 'trace', 'traceroute', |
| 'ping', 'test', 'version', |
| 'connect', 'watch', 'no' ] |
| else: |
| self.command_nested_dict['login'] = [ 'show', 'logout', 'exit', |
| 'history', 'help', 'echo', |
| 'date', 'trace', |
| 'ping', 'test', 'version', |
| 'connect', 'watch', 'no' ] |
| |
| self.command_nested_dict['enable'] = [ 'clear', 'end' ] |
| |
| #self.command_dict['config-internal'] = ['lint', 'permute'] |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # make_unit_formatter |
| # |
| @staticmethod |
| def make_unit_formatter(units): |
| return lambda x, y: '%s %s' % (x, units) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # init_stats_metadata |
| # |
| def init_stats_metadata(self): |
| # Initialize the stats metadata by pulling it from the rest api |
| json_data = self.rest_simple_request_to_dict('http://%s/rest/v1/stats/metadata/%s/' % \ |
| (self.controller, self.cluster)) |
| metadata = {} |
| fields = {} |
| for item in json_data: |
| # XXX Requesting this stat causes a long delay in reading; bug in sdncon |
| # once this is fixed we can add back in; also add to field_orderings below |
| #if json_data[item]['name'] == 'OFActiveFlow': |
| # continue |
| |
| if json_data[item]['target_type'] in metadata: |
| metadata[json_data[item]['target_type']].append(json_data[item]) |
| else: |
| metadata[json_data[item]['target_type']] = [json_data[item]] |
| fields[json_data[item]['target_type']] = {} |
| |
| formatter = self.make_unit_formatter(json_data[item]['units']) |
| fields[json_data[item]['target_type']][json_data[item]['name']] = \ |
| {'verbose-name': json_data[item]['verbose_name'], |
| 'units': json_data[item]['units'], |
| 'formatter': formatter} |
| |
| self.stats_metadata = json_data |
| self.stats_type_metadata = metadata |
| |
| self.pp.add_format('STATS-CONTROLLER', |
| 'cli.init_stats_metadata()', # origina |
| {'stats-controller' : { |
| 'field-orderings' : {'default': |
| ['cpu-user', 'cpu-nice', 'cpu-system', |
| 'cpu-idle' |
| 'mem-free', 'mem-used', 'swap-used', |
| 'disk-root', 'disk-log', 'disk-boot', |
| 'sdnplatform-cpu', 'database-cpu', |
| 'apache-cpu', 'cli-cpu', 'statd-cpu']}, |
| 'fields' : fields['controller'] |
| }}) |
| |
| self.pp.add_format('STATS-SWITCH', |
| 'cli.init_stats_metadata()', # origina |
| { 'stats-switch' : { |
| 'field-orderings' : {'default': |
| ['OFPacketIn', 'OFFlowMod', 'OFActiveFlow']}, |
| 'fields' : fields['switch'] |
| }}) |
| |
| for obj_type in fields: |
| for field in fields[obj_type]: |
| for data_format in ['rate', 'value']: |
| units = fields[obj_type][field]['units'] |
| if data_format == 'rate': |
| units += '/s' |
| self.pp.add_format('STATS-%s-%s-%s' % (obj_type,field,data_format), |
| 'cli.init_stats_metadata()', |
| { 'stats-%s-%s-%s' % (obj_type,field,data_format) : { \ |
| 'field-orderings' : {'default': ['value', 'timestamp']}, |
| 'fields' : {'timestamp': {'verbose-name': 'Timestamp', |
| 'formatter': |
| lambda x,y: time.ctime(x/1000)}, |
| 'value': {'units': units, |
| 'verbose-name': |
| '%s (%s)' % |
| (fields[obj_type][field]['verbose-name'], |
| units)}} |
| }}) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # |
| def command_packages_path(self): |
| command_descriptions = 'desc' |
| desc_path = resource_filename(__name__, command_descriptions) |
| if desc_path: |
| # print "desc_path %s" % desc_path |
| return desc_path |
| desc_path = os.path.join(os.path.dirname(__file__), command_descriptions) |
| if os.path.exists(desc_path): |
| return desc_path |
| return None |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # |
| def command_packages_exists(self, version): |
| desc_path = self.command_packages_path() |
| if desc_path == None: |
| return None |
| version_path = os.path.join(desc_path, version) |
| if os.path.exists(version_path): |
| return version_path |
| return None |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # |
| def add_command_packages(self, version): |
| """ |
| Add all command and output format components |
| |
| """ |
| desc_path = self.command_packages_path() |
| if desc_path == None: |
| print 'No Command Descriptions subdirectory' |
| return |
| |
| for path_version in [x for x in os.listdir(desc_path) if x.startswith(version)]: |
| print path_version |
| result_tuple = imp.find_module(path_version, [desc_path]) |
| imp.load_module(version, result_tuple[0], result_tuple[1], result_tuple[2]) |
| new_path = result_tuple[1] |
| cmds_imported = [] |
| for cmds in os.listdir(new_path): |
| (prefix, suffix) = os.path.splitext(cmds) |
| if (suffix == '.py' or suffix == '.pyc') and prefix not in cmds_imported and prefix != '__init__': |
| cmds_imported.append(prefix) |
| result_tuple = imp.find_module(prefix, [new_path]) |
| module = imp.load_module(prefix, result_tuple[0], result_tuple[1], result_tuple[2]) |
| command.add_commands_from_module(version, module, self.dump_syntax) |
| self.pp.add_formats_from_module(version, module) |
| # print cmds_imported |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # |
| def choices_text_builder(self, matches, max_len = None, col_width = None): |
| """ |
| @param max_len integer, describes max width of any entry in matches |
| @param col_width integer, colun width |
| """ |
| # Sort the choices alphabetically, but if the token ends |
| # in a number, sort that number numerically. |
| try: |
| entries = sorted(matches, utif.completion_trailing_integer_cmp) |
| except Exception, e: |
| traceback.print_exc() |
| |
| if col_width == None: |
| # request to look it up |
| (col_width, line_length) = self.pp.get_terminal_size() |
| col_width = min(120, col_width) |
| |
| if max_len == None: |
| # request to compute the max length |
| max_len = len(max(matches, key=len)) |
| |
| count = len(matches) |
| max_len += 1 # space after each choice |
| if max_len > col_width: |
| # one per line? |
| pass |
| else: |
| per_line = col_width / max_len |
| lines = (count + (per_line - 1)) / per_line |
| if lines == 1: |
| return ''.join(['%-*s' % (max_len, m) for m in entries]) |
| else: |
| # fill the columns, which means skipping around the entries |
| result = [] |
| for l in range(lines): |
| result.append(['%-*s' % (max_len, entries[i]) |
| for i in range(l, count, lines)]) |
| lines = [''.join(x) for x in result] |
| return '\n'.join(lines) |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # |
| def matches_hook(self, subs, matches, max_len): |
| |
| # one shot disabler, used to disable printing of completion |
| # help for two-column help display (for '?' character). |
| # completion printing here can only display the possible selections, |
| # for two-column mode, the reason why each keyword was added |
| # needs to be displayed, which is no longer available here. |
| if self.completion_print == False: |
| self.completion_print = True |
| return |
| |
| choices_text = self.choices_text_builder(matches, max_len) |
| self.print_completion_help(choices_text) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # |
| def pre_input_hook(self): |
| """ |
| """ |
| pass |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # |
| def desc_version_to_path_elem(self, version): |
| """ |
| Version numbers like 1.0 need to be converted to the path |
| element associated with the number, like version100 |
| """ |
| try: |
| version_number = float(version) |
| version = 'version%s' % int(version_number * 100) |
| except: |
| pass |
| |
| # temporary, use until version100 exists. |
| if version == 'version100' and not self.command_packages_exists(version): |
| version = 'version200' |
| |
| return version |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # init |
| # |
| def init(self): |
| global mi |
| |
| self.completion_print = True |
| self.vendordb = VendorDB() |
| self.vendordb.init() |
| |
| parser = OptionParser() |
| parser.add_option("-c", "--controller", dest="controller", |
| help="set default controller to CONTROLLER", |
| metavar="CONTROLLER", default=self.controller) |
| parser.add_option("-S", "--syntax", dest='dump_syntax', |
| help="display syntax of loaded commands", |
| action='store_true', default=False) |
| parser.add_option("-i", "--init", dest='init', |
| help="do not perform initialization checks", |
| action='store_true', default=False) |
| parser.add_option("-d", "--debug", dest='debug', |
| help='enable debug for cli (debug cli)', |
| action='store_true', default=False) |
| parser.add_option("-v", "--version", dest='desc_version', |
| help='select command versions (description group)', |
| default=None) |
| parser.add_option('-m', "--mode", dest='starting_mode', |
| help='once the cli starts, nest into this mode') |
| parser.add_option('-q', "--quiet", dest='quiet', |
| help='suppress warning messages', |
| action='store_true', default=False) |
| (self.options, self.args) = parser.parse_args() |
| self.controller = self.options.controller |
| if not self.controller: |
| self.controller = "127.0.0.1:8000" |
| self.dump_syntax = self.options.dump_syntax |
| if not self.dump_syntax: |
| self.dump_syntax = False |
| self.debug = self.options.debug |
| |
| # command option, then env, then default |
| self.desc_version = self.options.desc_version |
| if self.desc_version == None: |
| self.desc_version = os.getenv('CLI_COMMAND_VERSION') |
| if self.desc_version == None: |
| self.desc_version = '2.0' # 'version200' |
| |
| if self.desc_version: |
| self.desc_version = self.desc_version_to_path_elem(self.desc_version) |
| |
| self.length = 0 # screen length. |
| |
| self.set_controller_for_prompt() |
| |
| if not sys.stdin.isatty(): |
| self.batch = True |
| |
| self.store = StoreClient() |
| self.store.set_controller(self.controller) |
| |
| mi = Modi(self, CliModelInfo()) |
| self.mi = mi |
| self.init_command_dict() |
| |
| self.completion_reset() |
| self.completion_skip = False |
| self.completion_cache = True |
| readline.set_completer(self.completer) |
| readline.set_completer_delims("\t ") |
| # readline.set_pre_input_hook(self.pre_input_hook) |
| readline.set_completion_display_matches_hook(self.matches_hook) |
| if not self.batch: |
| self.init_history_file() |
| self.push_mode("login") |
| |
| # starting mode, |
| starting_mode = self.options.starting_mode |
| if starting_mode == None: |
| starting_mode = os.getenv('CLI_STARTING_MODE') |
| |
| if starting_mode: |
| if starting_mode == 'login': |
| pass |
| elif starting_mode == 'enable': |
| self.push_mode("enable") |
| elif starting_mode == 'config': |
| self.push_mode("enable") |
| self.push_mode("config") |
| else: |
| print 'Only login, enable or config allowed as starting modes' |
| |
| # process quiet option |
| quiet = self.options.quiet |
| if quiet == None: |
| quiet = os.getenv('CLI_SUPPRESS_WARNING') |
| |
| if quiet: |
| self.supress_warnings() |
| |
| # |
| self.pp = PrettyPrinter(self) |
| |
| # |
| # pattern matches |
| # |
| self.IP_ADDR_RE = re.compile(r'^(\d{1,3}\.){3}\d{1,3}$') |
| self.CIDR_RE = re.compile(r'^((\d{1,3}\.){3}\d{1,3})/(\d{1,2}?)$') |
| self.MAC_RE = re.compile(r'^(([A-Fa-f\d]){2}:?){5}[A-Fa-f\d]{2}$') |
| self.DPID_RE = re.compile(r'^(([A-Fa-f\d]){2}:?){7}[A-Fa-f\d]{2}$') |
| self.ACL_RE = re.compile(r'^\d+$') # just the leading digits |
| self.DIGITS_RE = re.compile(r'^\d+$') |
| self.HEX_RE = re.compile(r'^0x[0-9a-fA-F]+$') |
| |
| # Initialize any middleware layers |
| init_midware(self, mi) |
| init_vnsw(self, mi) |
| rest_to_model.rest_to_model_init(self, mi) |
| run_config.init_running_config(self, mi) |
| |
| # Initialize all doc tags, use a subdirectory based on |
| # the locale. This may be incorect if there's nothing |
| # in the configured locale for the returned locale value. |
| lc = locale.getdefaultlocale() |
| if lc == None or lc[0] == None: |
| print 'Locale not configured ', lc |
| lc = ('en_US', 'UTF8') |
| doc_dir = resource_filename(__name__, 'documentation/%s' % lc[0]) |
| if doc_dir is None: |
| doc_dir = os.path.join(os.path.dirname(__file__), 'documentation', lc[0]) |
| # print "doc_dir %s" % doc_dir |
| doc.add_doc_tags(doc_dir) |
| |
| # Initialize the command module, than add command packages |
| command.init_command(self, mi) |
| self.add_command_packages(self.desc_version) |
| |
| # save for possible later use |
| #print self.pp.format_table(self.pp.format_details()) |
| |
| # |
| if self.debug: |
| for (n,v) in self.command_submode_dict.items(): |
| print "SUBMODE %s %s" % (n,v) |
| for (n,v) in self.command_dict.items(): |
| print "MODE %s %s" % (n,v) |
| |
| |
| self.time_zone_list = None |
| command.add_completion('time-zone-completion', SDNSh.time_zone_completion) |
| command.add_validation('time-zone-validation', SDNSh.time_zone_validation) |
| command.add_action('set-clock', SDNSh.set_clock_action, {'kwargs': {'data': '$data'}}) |
| command.add_action('begin-default-gateway-check', SDNSh.begin_default_gateway_check_action) |
| command.add_action('end-default-gateway-check', SDNSh.end_default_gateway_check_action) |
| |
| command.add_command_type('config-with-default-gateway-check', { |
| 'action': ('begin-default-gateway-check', 'write-fields', 'end-default-gateway-check'), |
| 'no-action': ('begin-default-gateway-check', 'reset-fields', 'end-default-gateway-check') |
| }) |
| |
| # |
| if self.feature_enabled('experimental'): |
| print '***Warning: experimental features enabled***' |
| if self.debug: |
| self.sdndb = sdndb.SDNDB(self, self, self.pp) |
| print '--BEGIN--' |
| self.sdndb.display('core/switch', style='table') |
| self.sdndb.display('core/controller', style='table') |
| self.sdndb.display('core/controller', style='list') |
| print '--LINK--', self.pp.table_info['link']['field-orderings'] |
| self.sdndb.display('topology/link') |
| self.sdndb.display('topology/switch-cluster') |
| self.sdndb.display('topology/switch-cluster', style='list') |
| self.sdndb.display('topology/enabled-port') |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # check_rest_result |
| @staticmethod |
| def check_rest_result(result, message=None): |
| if isinstance(result, collections.Mapping): |
| error_type = result.get('error_type') |
| if error_type: |
| raise error.CommandRestError(result, message) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # get_table_from_store |
| # Caches the last result to try to cut down on the number of times large tables |
| # are loaded from the REST API |
| # |
| def get_table_from_store(self, obj_type, key = None, val = None, match = None): |
| return self.store.get_table_from_store(obj_type, key, val, match) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # get_object_from_store |
| # cover for call to get a specific object from a table, only a convenience. |
| # |
| def get_object_from_store(self, obj_type, obj_name): |
| return self.store.get_object_from_store(obj_type, obj_name) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # rest_query_objects |
| # cover for call to query objects from the database, only a convenience. |
| # |
| def rest_query_objects(self, obj_type, query_params=None): |
| return self.store.rest_query_objects(obj_type, query_params) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # rest_create_object |
| # REST API call to create an object, if this is the last table which |
| # was fetched, then discard that table. |
| # |
| def rest_create_object(self, obj_type, ident): |
| return self.store.rest_create_object(obj_type, ident) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # rest_delete_objects |
| # |
| def rest_delete_objects(self, obj_type, delete_params): |
| return self.store.rest_delete_objects(obj_type, delete_params) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # rest_delete_object |
| # |
| def rest_delete_object(self, obj_type, name): |
| if type(name) == dict: |
| return self.store.rest_delete_object(obj_type, name) |
| |
| return self.store.rest_delete_object(obj_type, mi.pk(obj_type), name) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # rest_update_object |
| # |
| def rest_update_object(self, obj_type, name, obj_key_val, obj_key_data): |
| return self.store.rest_update_object(obj_type, name, obj_key_val, obj_key_data) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # rest_simple_resquest_to_dict |
| # Issue a store_simple_request and covert the result into a dict using json.load |
| # Performs no exception handling |
| # |
| def rest_simple_request_to_dict(self, url): |
| return json.loads(self.store.rest_simple_request(url)) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # rest_post_request |
| # Forward a low-level REST request to the store |
| # |
| def rest_post_request(self, url, obj, verb='PUT'): |
| return self.store.rest_post_request(url, obj, verb) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # create_object |
| # Helper function to help identify call sites which create objects |
| # Errors result in printing an error message, then returning True |
| # |
| def create_object(self, obj_type, field_dict): |
| try: |
| self.rest_create_object(obj_type, field_dict) |
| return False |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, obj_type) |
| |
| if errors != None: |
| print self.rest_error_dict_to_message(errors) |
| return True |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # create_row |
| # Helper function to help identify call sites which create new rows |
| # |
| # Return a pair of bool's, the first is an error indication, while the second |
| # is True when the row needed to be created. |
| # |
| # |
| def create_row(self, obj_type, name, row_dict = None): |
| # create the row with a dictionary which includes all the |
| # populated values for the foreign keys |
| # the row is created. |
| if row_dict == None: |
| updated_dict = {} |
| else: |
| updated_dict = row_dict |
| |
| # |
| # Only set the primary key for obj_keys which aren't managed |
| # by the CassandraSetting/COMPOUND_KEY_FIELDS (this is currently |
| # a slightly different predicate than is_compound_key) |
| # |
| key = mi.pk(obj_type) |
| type_info = mi.obj_type_info_dict[obj_type]['fields'][key]['type'] |
| if type_info != 'compound-key': |
| updated_dict[mi.pk(obj_type)] = name |
| updated_dict = self.associate_foreign_keys_with_dict(obj_type, updated_dict) |
| |
| if self.create_object(obj_type, updated_dict): |
| return [True, False] |
| return [False, True] |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # find_or_create_row |
| # Return a pair of bool's, the first is an error indication, while the second |
| # is True when the row needed to be created. |
| # |
| # When a new row is created, if the obj_type has a private_key, an attempt |
| # is made to identify all the private keys, and populate values for each |
| # of them. |
| # |
| def find_or_create_row(self, obj_type, name, row_dict = None): |
| if row_dict == None: |
| row_dict = {} |
| try: |
| result = self.get_object_from_store(obj_type, name) |
| if result[mi.pk(obj_type)] == name: |
| return [False, False] |
| except Exception: |
| pass |
| |
| return self.create_row(obj_type, name, row_dict) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # associate_foreign_keys_with_dict |
| # Passed an obj_type, and a dictionary, returns a dictionary with updated |
| # key:value pairs for all the known private keys. |
| # |
| # |
| def associate_foreign_keys_with_dict(self, obj_type, create_dict): |
| # |
| # Private key names are 'well known', so the names are used here to |
| # idenify how the field ought to be populated. |
| # |
| for foreign_key_id in mi.obj_type_foreign_keys(obj_type): |
| if foreign_key_id in create_dict: |
| # already set in the dictionary |
| pass |
| elif mi.is_null_allowed(obj_type, foreign_key_id): |
| continue |
| elif foreign_key_id == 'vns': |
| create_dict[foreign_key_id] = self.vns_name() |
| elif foreign_key_id == 'vns-access-list' and \ |
| self.get_current_mode_obj_type() == 'vns-access-list': |
| create_dict[foreign_key_id] = self.get_current_mode_obj() |
| elif foreign_key_id == 'rule': |
| # LOOK! this seems to need more work, what's the rule relationship? |
| if obj_type == 'vns-interface': |
| create_dict = associate_foreign_key_for_vns_interface(dict) |
| else: |
| traceback.print_stack() |
| print self.error_msg("Internal: %s can't determine value " \ |
| "for foreign key %s" % (obj_type, foreign_key_id)) |
| |
| return create_dict |
| |
| # |
| # methods to manage the mode_stack, prompts, and mode_obj/mode_obj_type |
| # |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # set_contoller_for_prompt |
| # |
| def set_controller_for_prompt(self): |
| if self.controller == "127.0.0.1:8000": |
| self.controller_for_prompt = socket.gethostname() |
| else: |
| self.controller_for_prompt = self.controller |
| self.update_prompt() |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # update_prompt |
| # There are several different prompts depending on the current mode: |
| # 'host'> -- login mode |
| # 'host'# -- enable mode |
| # 'host'(config)# -- config mode |
| # 'host'(config-vns-definition-name)# -- vns-definition mode |
| # 'host'(config-vns-definition-interface-rule-name)# -- vns-defn, |
| # interface-rule mode |
| # |
| def update_prompt(self): |
| if self.current_mode().startswith("config"): |
| current_mode = "(" + self.current_mode() |
| if self.get_current_mode_obj() != None: |
| if self.in_config_submode("config-vns"): |
| # substitute '|' with '-' |
| parts = self.get_current_mode_obj().split('|') |
| current_mode += "-" + '-'.join(parts) |
| self.prompt = str(self.controller_for_prompt) + current_mode + ")# " |
| elif self.current_mode() == "enable": |
| self.prompt = str(self.controller_for_prompt) + "# " |
| else: |
| self.prompt = str(self.controller_for_prompt) + "> " |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # push_mode |
| # Every pushed mode is a quad: <modeName, tableName, specificRow, exitCallback> |
| # |
| # obj_type is the name of an associated table/model (tableName) |
| # obj is the key's value for the table/model's key (specificRow) |
| # |
| # The cli currently supports a mode of table/model row edit, where the |
| # name of the table/mode is entered, along with an associated value for |
| # key-column of the table. Once in that mode, other fields of the table |
| # can be edited by entering the name of the field, along with a new value. |
| # |
| # The exitCallback is the nane of a method to call when the current pushed |
| # level is getting pop'd. |
| # |
| def push_mode(self, mode_name, obj_type=None, obj=None, exitCallback=None): |
| self.mode_stack.append( { 'mode_name' : mode_name, |
| 'obj_type' : obj_type, |
| 'obj' : obj, |
| 'exit' : exitCallback} ) |
| self.update_prompt() |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # pop_mode |
| # Pop the top of the stack of mode's. |
| # |
| def pop_mode(self): |
| m = self.mode_stack.pop() |
| if len(self.mode_stack) == 0: |
| self.run = False |
| else: |
| self.update_prompt() |
| return m |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # mode_stack_to_rest_dict |
| # Convert the stack of pushed modes into a collection of keys. |
| # Can be used to build the rest api dictionary used for row creates |
| # |
| def mode_stack_to_rest_dict(self, rest_dict): |
| # |
| for x in self.mode_stack: |
| if x['mode_name'].startswith('config-'): |
| rest_dict[x['obj_type']] = x['obj'] |
| |
| return rest_dict |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # current_mode |
| # Return the string describing the current (top) mode. |
| # |
| def current_mode(self): |
| if len(self.mode_stack) < 1: |
| return "" |
| return self.mode_stack[-1]['mode_name'] |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # in_config_submode |
| # Return true when the current mode is editing the contents of one of the |
| # rows of the table/model store. |
| # |
| def in_config_submode(self, prefix = "config-"): |
| return self.current_mode().startswith(prefix) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # in_config_mode |
| # Returns true for any config mode; this is any nested edit mode, |
| # along with the base config mode |
| # |
| def in_config_mode(self): |
| return self.current_mode().startswith("config") |
| # |
| # -------------------------------------------------------------------------------- |
| # in_config_switch_mode |
| # Returns true when the switch mode has been entered via the 'switch <dpid>' command. |
| # |
| def in_config_switch_mode(self): |
| return self.current_mode() == "config-switch" |
| # |
| # -------------------------------------------------------------------------------- |
| # in_config_switch_if_mode |
| # Returns true when the switch-interface mode has been entered via the |
| # 'switch <dpid>' command, then the interface <name> command |
| # |
| def in_config_switch_if_mode(self): |
| return self.current_mode() == "config-switch-if" |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # vns_name |
| # |
| def vns_name(self): |
| mode_dict = self.mode_stack_to_rest_dict({}) |
| if 'vns' in mode_dict: |
| return mode_dict['vns'] |
| if 'vns-definition' in mode_dict: |
| return mode_dict['vns-definition'] |
| return None |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # in_config_vns_mode |
| # Returns true when the vns mode has been entered via the 'vns <vnsId>' command. |
| # |
| def in_config_vns_mode(self): |
| return self.current_mode() == "config-vns" |
| # |
| # -------------------------------------------------------------------------------- |
| # in_config_vns_def_mode |
| # Returns true when the vns mode has been entered via the |
| # 'vns-definition <vnsId>' command. |
| # |
| def in_config_vns_def_mode(self): |
| return self.current_mode() == "config-vns-def" |
| # |
| # -------------------------------------------------------------------------------- |
| # in_config_vns_def_if_rule_mode |
| # Returns true when the vns mode has been entered via the |
| # 'vns-definition <vnsId>' command, then into interface-rule mode. |
| # |
| def in_config_vns_def_if_rule_mode(self): |
| return self.current_mode() == "config-vns-def-if-rule" |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # in_config_vns_acl_mode |
| # Returns true when the vns mode has been entered via the |
| # 'vns <vnsId>' command, then into acl mode. |
| # |
| def in_config_vns_acl_mode(self): |
| return self.current_mode() == "config-vns-acl" |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # in_config_vns_if_mode |
| # Returns true when the vns mode has been entered via the |
| # 'vns <vnsId>' command, then into interface mode. |
| # |
| def in_config_vns_if_mode(self): |
| return self.current_mode() == "config-vns-if" |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # in_config_host_mode |
| # Returns true when the vns mode has been entered via the |
| # 'host <host-id>' command |
| # |
| def in_config_host_mode(self): |
| return self.current_mode() == "config-host" |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # in_config_controller_node_mode |
| # Returns true when the vns mode has been entered via the |
| # 'controller-node <controller-node-id>' command |
| # |
| def in_config_controller_node_mode(self): |
| return self.current_mode() == "config-controller-node" |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # in_config_controller_interface_mode |
| # Returns true when the controller interface mode has been entered via the |
| # 'interface Ethernet <#>' command from the controller-node config mode |
| # |
| def in_config_controller_interface_mode(self): |
| return self.current_mode() == "config-controller-if" |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # in_config_port_channel_mode |
| # |
| def in_config_port_channel_mode(self): |
| return self.current_mode() == "config-port-channel" |
| |
| # -------------------------------------------------------------------------------- |
| # set_current_mode_obj |
| # Sets the name of the selected row (key's value) |
| # |
| def set_current_mode_obj(self, obj): |
| self.mode_stack[-1]['obj'] = obj |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # get_current_mode_obj |
| # Gets the name of the current mode's selected row value (key's value) |
| # This can return None. |
| # |
| def get_current_mode_obj(self): |
| return self.mode_stack[-1]['obj'] |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # set_current_mode_obj_type |
| # Set the table/model name for this current mode. |
| # |
| def set_current_mode_obj_type(self, obj_type): |
| self.mode_stack[-1]['obj_type'] = obj_type |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # get_current_mode_obj_type |
| # Get the table/model name for this current mode. |
| # |
| def get_current_mode_obj_type(self): |
| return self.mode_stack[-1]['obj_type'] |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # get_nested_mode_obj |
| # Get the id of the object that matches the given object type |
| # starting from the current mode and working backwards to the |
| # top-level mode. If there's no mode that matches the given |
| # object type, then return None (maybe should handle as exception?). |
| # |
| def get_nested_mode_obj(self, obj_type): |
| for i in range(1, len(self.mode_stack)+1): |
| # Use negative index so we search from the top/end of the stack |
| mode = self.mode_stack[-i] |
| if mode['obj_type'] == obj_type: |
| return mode['obj'] |
| return None |
| |
| # |
| # helper functions to access commands, obj_types, and fields |
| # |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # all_obj_types_starting_with |
| # Returns a list of all the current object types starting with the text |
| # parameter. |
| # |
| # |
| def all_obj_types_starting_with(self, text=""): |
| if onos == 0: |
| netvirt_feature = self.netvirt_feature_enabled() |
| else: |
| netvirt_feature = False |
| |
| matched_obj_types = [x for x in mi.obj_types |
| if x.startswith(text) and |
| self.debug_obj_type(x) and |
| (netvirt_feature or (x != 'vns-definition')) ] |
| # synthetic object type based on the vns config submode. |
| if self.in_config_vns_def_mode() and "interface-rule".startswith(text): |
| matched_obj_types.append("interface-rule") |
| if self.in_config_vns_mode() and "access-list".startswith(text): |
| matched_obj_types.append("access-list") |
| if self.in_config_vns_mode() and "interfaces".startswith(text): |
| matched_obj_types.append("interfaces") |
| return matched_obj_types |
| # |
| # -------------------------------------------------------------------------------- |
| # feature_enabled |
| # Return True when a particular feature is enabled. |
| # |
| def feature_enabled(self, feature): |
| # features not managed via store |
| # |
| if feature == 'experimental': |
| return os.path.exists(self.feature_allow_experimental) |
| |
| if feature == 'ha': |
| try: |
| with open(self.boot_config_filename, "r") as f: |
| for line in f: |
| if line.startswith('ha-config='): |
| parts = line.split('=') |
| if len(parts) > 0 and parts[1] == 'enabled\n': |
| return True |
| except: |
| pass |
| return False |
| |
| # only current features which can be enabled disabled |
| if feature not in [ |
| 'vns', 'static-flow-pusher', 'performance-monitor']: |
| return False |
| |
| # |
| try: |
| entry = self.get_table_from_store('feature') |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, 'feature') |
| print self.rest_error_dict_to_message(errors) |
| return True |
| |
| feature_to_field_dict = { |
| 'vns' : 'netvirt-feature', |
| 'static-flow-pusher' : 'static-flow-pusher-feature', |
| 'performance-monitor' : 'performance-monitor-feature', |
| } |
| |
| if len(entry) == 0: |
| if feature in feature_to_field_dict: |
| field = feature_to_field_dict[feature] |
| return mi.field_default_value('feature', field) |
| return True |
| |
| if len(entry) != 1: |
| # need a mechanism to select one from the list |
| return True |
| |
| if feature in feature_to_field_dict: |
| return entry[0][feature_to_field_dict[feature]] |
| |
| # use the default value from the model (every feature needs a default value) |
| defauilt_value = mi.field_default_value('feature', feature_to_field_dict[feature]) |
| if default_value == None: |
| print self.error_msg('Feature %s missing default value' % feature) |
| return True |
| return default_value |
| |
| ''' |
| # |
| # -------------------------------------------------------------------------- |
| # address_space_default_create |
| # |
| # Create a default address space if it doesn't exist, which enables |
| # 'address-space default', |
| # |
| def address_space_default_create(self): |
| key = mi.pk('address-space') |
| try: |
| entries = self.get_table_from_store('address-space', |
| key, 'default', 'exact') |
| errors = None |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, 'address-space') |
| if not 'see_other' in errors: |
| print self.rest_error_dict_to_message(errors) |
| return |
| |
| if len(entries) == 0: |
| self.create_row('address-space', 'default') |
| |
| # |
| # -------------------------------------------------------------------------- |
| # tenant_default_create |
| # |
| # Create default tenant and system tenant if it doesn't exist, which enables |
| # 'tenant default' and tenant system, router vrsystem, |
| # |
| def tenant_default_create(self): |
| key = mi.pk('tenant') |
| try: |
| entries = self.get_table_from_store('tenant', |
| key, 'default', 'exact') |
| errors = None |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, 'tenant') |
| if not 'see_other' in errors: |
| print self.rest_error_dict_to_message(errors) |
| return |
| |
| if len(entries) == 0: |
| self.create_row('tenant', 'default') |
| #external tenant |
| try: |
| entries = self.get_table_from_store('tenant', |
| key, 'external', 'exact') |
| errors = None |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, 'tenant') |
| if not 'see_other' in errors: |
| print self.rest_error_dict_to_message(errors) |
| return |
| |
| if len(entries) == 0: |
| self.create_row('tenant', 'external') |
| |
| #system tenant and system router: vrsystem |
| try: |
| entries = self.get_table_from_store('tenant', |
| key, 'system', 'exact') |
| errors = None |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, 'tenant') |
| if not 'see_other' in errors: |
| print self.rest_error_dict_to_message(errors) |
| return |
| |
| if len(entries) == 0: |
| self.create_row('tenant', 'system') |
| |
| try: |
| entries = self.get_table_from_store('virtualrouter', |
| key, 'vrsystem', 'exact') |
| errors = None |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, 'virtualrouter') |
| if not 'see_other' in errors: |
| print self.rest_error_dict_to_message(errors) |
| return |
| |
| if len(entries) == 0: |
| self.create_row('virtualrouter', 'vrsystem',{"tenant":"system","vrname":"vrsystem"}) |
| |
| # |
| # -------------------------------------------------------------------------- |
| # netvirt_feature_init |
| # perform vns featrue enablement: |
| # Create a default vns if it doesn't exist, which enables 'vns default', |
| # |
| def netvirt_feature_init(self): |
| self.address_space_default_create() |
| self.tenant_default_create() |
| key = mi.pk('vns-definition') |
| try: |
| entries = self.get_table_from_store('vns-definition', key, 'default|default', 'exact') |
| errors = None |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, 'vns-definition') |
| if not 'see_other' in errors: |
| print self.rest_error_dict_to_message(errors) |
| return |
| |
| if len(entries) == 0: |
| self.create_row('vns-definition', 'default', {"tenant":"default","vnsname":"default"}) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # netvirt_feature_enabled |
| # Return True when the vns feature is enabled |
| # |
| # When vns is disabled, particular vns commands are no longer |
| # available, along with a few specific objects. Particular |
| # code sites which are specifically concerned with these issues |
| # disable these items. Another possible approach would be to |
| # modify the base information sources when a feature is |
| # enabled or disabled, ie: mi.obj_types and command_dict. |
| # This more active approach isn't taken since during |
| # the initialization process, a variety of different object |
| # relationships are constructed; foreign key xref's, alias |
| # key xref's, object key dictionaries, etc. If the feature |
| # enabled were more active, then all these relationships would also |
| # need to be recomputed. |
| # |
| def netvirt_feature_enabled(self): |
| controller_netvirt_feature = self.feature_enabled("vns") |
| if self.netvirt_feature_enabled_cached != controller_netvirt_feature: |
| self.netvirt_feature_enabled_cached = controller_netvirt_feature |
| if self.netvirt_feature_enabled_cached: |
| self.netvirt_feature_init() |
| if controller_netvirt_feature == False: |
| return False |
| return True |
| ''' |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # get_obj_of_type |
| # Return a single row, or None, of the table who's name is passed as obj_type |
| # Returns None when when there's multiple matches of the nane in the table/model |
| # Performs no error processing |
| # |
| def get_obj_of_type(self, obj_type, name): |
| key = mi.pk(obj_type) |
| if key: |
| errors = None |
| try: |
| entries = self.get_table_from_store(obj_type) |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, obj_type) |
| |
| if errors: |
| print self.rest_error_dict_to_message(errors) |
| return None |
| |
| entries = [x for x in entries if x.get(key, 'None') == name] |
| if len(entries) != 1: |
| return None |
| return entries[0] |
| else: |
| return None |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # fields_for_current_submode_starting_with |
| # Return a list of choices which are fields names of model/store objects |
| # Any fields which are primary keys are excluded from edit, as are any |
| # foreign keys, this is managed through is_editable() |
| # |
| def fields_for_current_submode_starting_with(self, start_text=""): |
| if not self.in_config_submode(): |
| return [] |
| if self.in_config_vns_mode(): |
| return [] |
| obj_type = self.get_current_mode_obj_type() |
| if not obj_type in mi.obj_type_info_dict: |
| return [] |
| fields = [x for x in mi.obj_type_info_dict[obj_type]['fields'].keys() \ |
| if mi.is_editable(obj_type, x)] |
| |
| return [x for x in fields if x.startswith(start_text)] |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # obj_types_for_config_mode_starting_with |
| # |
| # what obj types are available in the given submode |
| # e.g., top-level config mode has switch and config-switch has |
| # flow-entry but flow-entry should not be available in top-level |
| # config nor should switch be available in config-switch submode! |
| # LOOK! hardwired for now to test parsing - need to merge with model |
| # |
| def obj_types_for_config_mode_starting_with(self, start_text=""): |
| if not self.in_config_mode(): |
| return [] |
| # |
| # vns features |
| if onos == 0: |
| netvirt_features = self.netvirt_feature_enabled() |
| else: |
| netvirt_features = False |
| vns_objects = [ 'vns-definition' ] |
| |
| ret_list = [x for x in mi.obj_types |
| if self.debug_obj_type(x) and |
| not mi.is_obj_type_source_not_user_config(x)] |
| return [x for x in ret_list if x.startswith(start_text) and |
| (netvirt_features or not x in vns_objects) ] |
| |
| @staticmethod |
| def title_of(command): |
| return command['title'] if type(command) is dict else command |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # commands_feature_enabled |
| # |
| def commands_feature_enabled(self, commands): |
| return [self.title_of(x) for x in commands |
| if (not self.title_of(x) in self.command_name_feature) or |
| command.isCommandFeatureActive(self.title_of(x), |
| self.command_name_feature[self.title_of(x)])] |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # commands_for_mode |
| # |
| def commands_for_mode(self, mode): |
| """ |
| Walk the command dict, using interior submodes and compiling |
| the list of available commands (could rebuild command_dict() |
| to contain all the possible commands, but its good to know |
| exactly which commands apply to this submode) |
| """ |
| |
| # make a new list, so that items don't get added to the source |
| ret_list = list(self.command_nested_dict.get('login', [])) |
| if mode == 'login': |
| ret_list += self.command_dict.get('login', []) |
| return ret_list |
| ret_list += self.command_nested_dict.get('enable', []) |
| if mode == 'enable': |
| ret_list += self.command_dict.get('enable', []) |
| return ret_list |
| |
| if mode == 'config': |
| ret_list += self.command_nested_dict.get('config', []) |
| ret_list += self.command_dict.get('config', []) |
| return ret_list |
| |
| for idx in [x for x in self.command_nested_dict.keys() if mode.startswith(x)]: |
| ret_list += self.command_nested_dict.get(idx, []) |
| |
| ret_list += self.command_dict.get(mode, []) |
| |
| # manage command who's names are regular expressions |
| result = [x['re'] if type(x) == dict else x for x in ret_list] |
| |
| return result |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # commands_for_current_mode_starting_with |
| # |
| def commands_for_current_mode_starting_with(self, |
| start_text = "", completion = None): |
| """ |
| One of the difficult issues here is when the first item |
| isn't a token, but rather a regular expression. This currently occur |
| in a few places in the command description, and the mechanism for |
| dealing with the issue here is ... uhm ... poor. The code here is |
| a stopgap, and assumes the only regular expression supported |
| is the <digits> one. This could be make a bit better based on |
| the submode, but really, this entire first-token management should |
| be improved. |
| """ |
| if completion == None: |
| completion = False |
| # |
| # vns features commands include: |
| netvirt_feature_commands = ['vns', 'vns-definition'] |
| if onos == 0: |
| netvirt_feature = self.netvirt_feature_enabled() |
| else: |
| netvirt_feature = False |
| |
| mode_list = self.commands_for_mode(self.current_mode()) |
| ret_list = self.commands_feature_enabled(utif.unique_list_from_list(mode_list)) |
| |
| def prefix(x, start_text, completion): |
| if type(x) == str and x.lower().startswith(start_text.lower()): |
| return True |
| if not completion and type(x) == re._pattern_type: |
| return x.match(start_text) |
| return False |
| |
| def pattern_items(ret_list, prefix): |
| matches = [] |
| for p in [x for x in ret_list if type(x) == re._pattern_type]: |
| for c in command.command_registry: |
| if c['mode'] != self.current_mode(): |
| continue |
| if type(c['name']) != dict: |
| continue |
| first_word = c['name'] |
| if 'completion' not in first_word: |
| continue |
| completion = first_word['completion'] |
| if first_word['pattern'] == p.pattern: |
| result = {} |
| scopes = [ first_word, |
| { |
| 'completions' : result, |
| 'data' : {}, |
| 'text' : prefix, |
| }, |
| ] |
| command._call_proc(completion, |
| command.completion_registry, |
| scopes, c) |
| matches = result.keys() |
| return matches |
| |
| matches = [x for x in ret_list if prefix(x, start_text, completion)] |
| if completion: |
| matches += pattern_items(ret_list, start_text) |
| return matches |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # complete_optional_parameters |
| # |
| # Parse optional parameters. These can occur in any order. |
| # Params argument is a hash mapping a parameter name to type |
| # information. |
| # words argument are remaining words in the command |
| # line that aren't yet parsed |
| # |
| def complete_optional_parameters(self, params, words, text): |
| i = 0 |
| while i < len(words): |
| final = i+1 >= len(words) |
| word = words[i] |
| possible = [x for x in params if x.startswith(word)] |
| param_name = possible[0] |
| param = params[param_name] |
| |
| if (param['type'] != 'flag'): |
| if (final): |
| argument = text |
| if (param['type'] == 'enum'): |
| return [x |
| for x in param['values'] |
| if x.startswith(argument)] |
| elif argument == '': |
| if ('syntax_help' in param): |
| self.print_completion_help(param['syntax_help']) |
| else: |
| self.print_completion_help('[%s argument]' % word) |
| return |
| i += 1 |
| i += 1 |
| |
| return [x for x in params if x.startswith(text)] |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # parse_optional_parameters |
| # |
| @staticmethod |
| def parse_optional_parameters(params, words): |
| parsed = {} |
| i = 0 |
| while i < len(words): |
| word = words[i] |
| possible = [x for x in params if x.startswith(word)] |
| if len(possible) == 0: |
| raise ParamException('unknown option: %s' % word) |
| elif len(possible) > 1: |
| raise ParamException('ambiguous option: %s\n%s' % |
| (word, "\n".join(possible))) |
| else: |
| param_name = possible[0] |
| param = params[param_name] |
| if (param['type'] == 'flag'): |
| parsed[param_name] = True |
| else: |
| if i+1 < len(words): |
| argument = words[i+1] |
| if (param['type'] == 'string'): |
| parsed[param_name] = argument |
| elif (param['type'] == 'int'): |
| try: |
| parsed[param_name] = int(argument) |
| except ValueError: |
| raise ParamException('option %s requires ' + |
| 'integer argument' |
| % word) |
| elif (param['type'] == 'enum'): |
| arg_possible = [x |
| for x in param['values'] |
| if x.startswith(argument)] |
| if (len(arg_possible) == 0): |
| raise ParamException('option %s value must be in (%s)' % |
| (word,", ".join(param['values']))) |
| elif (len(arg_possible) > 1): |
| raise ParamException('ambiguous option %s value:\n%s' % |
| (word, "\n".join(arg_possible))) |
| else: |
| parsed[param_name] = arg_possible[0] |
| i += 1 |
| else: |
| raise ParamException('option %s requires an argument' |
| % word) |
| i += 1 |
| return parsed |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # annotated_obj_type_id |
| # |
| # Given an id's value for a compound key, crack the value into |
| # parts, and annotate each part with the compound key name. |
| # |
| # For 'Not Found' errors, the obj_type is known, along with the value |
| # for the id which wasn't found. If this is an id with a compound key, |
| # leaking information about the key's implementiion should be avoided. |
| # Here the key's value is cracked based on the composition character, |
| # and the items are displayed with their key-value name. |
| # eg: Error: Not Found vns:xXx rule-id:snapple (vns-interface-rule) |
| # |
| def annotated_obj_type_id(self, obj_type, error): |
| key = mi.pk(obj_type) |
| if mi.is_compound_key(obj_type, key): |
| result = { key : error} |
| mi.split_compound_into_dict(obj_type, key, result) |
| return ', '.join(["%s:%s" % tuple(x) for x in result.items() |
| if x[0] != key]) |
| return error |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # |
| def find_master(self): |
| """ |
| Return a dictionary with two items: 'master', and 'ips' |
| 'master' identifes the ip address of the master, a string |
| which is an ip address, or None. |
| 'ips' is a list of interface rows with populated ip addresses. |
| """ |
| # collect all the controller-interfaces |
| ips = [x for x in local_interfaces_firewall_open('tcp', [80, 8000], 'all') |
| if (x['ip'] != '' or x['discovered-ip'] != '')] |
| |
| master = None |
| for ip in ips: |
| if ip['ip'] != '': |
| url = "http://%s/rest/v1/system/ha/role" % ip['ip'] |
| if ip['discovered-ip'] != '': # select discovered ip over ip |
| url = "http://%s/rest/v1/system/ha/role" % ip['discovered-ip'] |
| result = self.store.rest_simple_request(url, use_cache = False) |
| self.check_rest_result(result) |
| ha_role = json.loads(result) |
| if ha_role['role'] == 'MASTER': |
| if ip['ip'] != '': |
| master = ip['ip'] |
| if ip['discovered-ip'] != '': |
| master = ip['discovered-ip'] |
| |
| return {'master': master, 'ips': ips} |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # rest_error_to_dict |
| # Turn an exception into an error dictionary, which can later be printed. |
| # using rest_error_dict_to_message(). |
| # |
| def rest_error_to_dict(self, e, detail=None): |
| errors = None |
| # try to identifify validation requests |
| if isinstance(e, httplib.BadStatusLine): |
| errors = {'connection_error' : 'REST API server %s: ' |
| 'server not running' % |
| self.controller} |
| return errors |
| |
| elif isinstance(e, urllib2.HTTPError): |
| code = e.code |
| error_returned = e.readline() |
| |
| if code == 404: |
| if detail: |
| errors = {'not_found_error' : 'Not Found: %s' % detail} |
| else: |
| errors = {'not_found_error' : 'Not Found: %s' % error_returned} |
| elif code == 500 or code == 403: |
| # traceback.print_stack() |
| errors = {'connection_error' : 'REST API server %s failure: %s' % |
| (self.controller, error_returned)} |
| elif code == 400: |
| try: |
| errors = json.loads(error_returned) |
| except: |
| # if the error can't be converted into a dictionary, then imbed the complete |
| # errors as the value for a specific error key. |
| errors = {'error_result_error': "Can't convert returned error: %s" % error_returned} |
| elif code == 301: |
| errors = {'moved_error': 'HttpError Moved Permanently %s' % e} |
| elif code == 303: |
| # Attempt to determine what list of ip addresses is for |
| # the controller, try to find the Master. if any failure |
| # occurs anywhere, drop down to a simple error message. |
| try: |
| minfo = self.find_master() |
| ips = [x['ip'] for x in minfo['ips']] |
| master = minfo['master'] |
| |
| if master: |
| errors = {'see_other': 'This controller is in SLAVE ' |
| 'mode. The command must be run in MASTER mode.' |
| '\nMASTER at %s' % master} |
| elif len(ips): |
| errors = {'see_other': 'This controller is in SLAVE mode. ' |
| 'The command must be run in MASTER mode.' |
| '\nNo MASTER found, possible ip\'s %s' % |
| (', '.join(ips))} |
| else: |
| errors = {'see_other': 'This controller is in SLAVE mode. ' |
| 'The command must be run in MASTER mode.' |
| '\nNo MASTER currently exists '} |
| except: |
| errors = {'see_other': 'This controller is in SLAVE mode. ' |
| 'The command must be run in MASTER mode.' |
| '\nNo ip addresses for controller available'} |
| else: |
| errors = {'unknown_error': 'HttpError %s' % error_returned} |
| elif isinstance(e, NotFound): |
| if e.obj_type in mi.obj_type_info_dict: |
| errors = {'not_found_error' : 'Not Found: %s (%s)' % |
| (self.annotated_obj_type_id(e.obj_type, |
| e.name), |
| e.obj_type)} |
| else: |
| errors = {'not_found_error' : 'Not Found: %s (%s)' % |
| (e.name, e.obj_type)} |
| else: |
| if self.debug or self.debug_backtrace: |
| traceback.print_stack() |
| errors = {'unknown_error': "Need error management for error %s" % type(e)} |
| |
| return errors |
| |
| # |
| # |
| # -------------------------------------------------------------------------------- |
| # rest_error_is_not_found |
| # Return True if this specific error was the result of a not-found situation |
| # |
| @staticmethod |
| def rest_error_is_not_found(rest_error_dict): |
| if 'not_found_error' in rest_error_dict: |
| return True |
| return False |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # rest_error_dict_to_message |
| # Turn an rest_error_dict returned from rest_error_to_dict |
| # into an error message which can ge printed. Code assumes multiple errors |
| # won't occur; if a 'field_error' exists, for example, a 'model_error' won't |
| # also be posted in the error |
| # |
| def rest_error_dict_to_message(self, rest_error_dict): |
| if rest_error_dict == None: |
| return rest_error_dict |
| error_msg = "" |
| if 'field_errors' in rest_error_dict: |
| for (k, v) in rest_error_dict['field_errors'].items(): |
| error_msg += "Syntax error: field %s: %s" % (k, v) |
| # traceback.print_stack(), to find out why the error occured |
| elif 'model_error' in rest_error_dict: |
| error_msg += "%s" % rest_error_dict['model_error'] |
| elif 'not_found_error' in rest_error_dict: |
| error_msg += "Error: %s" % rest_error_dict['not_found_error'] |
| elif 'connection_error' in rest_error_dict: |
| error_msg += rest_error_dict['connection_error'] |
| elif 'error_result_error' in rest_error_dict: |
| error_msg += rest_error_dict['error_result_error'] |
| elif 'moved_error' in rest_error_dict: |
| error_msg += rest_error_dict['moved_error'] |
| elif 'see_other' in rest_error_dict: |
| error_msg += rest_error_dict['see_other'] |
| elif 'unknown_error' in rest_error_dict: |
| error_msg += rest_error_dict['unknown_error'] |
| elif 'description' in rest_error_dict and \ |
| rest_error_dict["description"] == 'saved': |
| # not an error: { 'description' : 'saved' } |
| return None |
| elif 'error_type' in rest_error_dict: # http 400 errors |
| description = rest_error_dict['description'] |
| if description.startswith('Error: '): |
| description = description[len('Error: '):] |
| return '%s (%s)' % (description, |
| rest_error_dict['error_type']) |
| else: |
| error_msg = "REST API server on controller-node %s " % self.controller |
| error_msg += "had %s error:\n" % rest_error_dict['error_type'] |
| error_msg += rest_error_dict['description'] |
| return error_msg |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # |
| # Commands of the CLI. A command "foo" should have two methods: |
| # cp_foo(self, text, state) - completion for this command, return list of options |
| # do_foo(self, line) - execute the command |
| # |
| # Given a string 'word', not containing any spaces, nor any break |
| # characters associated with python, return the a invocable method |
| # note that '-' are permitted, and converted to '_''s |
| # |
| # When in the vns or vns-definition mode, the selected functions |
| # to call back have an additional component added to the called |
| # back function. In the non-vns case, for example, the "show" |
| # command would result in a method called do_show, while in the |
| # vns modes, 'vns_' in inserted between the 'verb' and the noun, |
| # for example, resulting in 'do_vns_show' |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # method_from_name |
| # |
| def method_from_name(self, prefix, name): |
| if type(name) != str: |
| return None |
| if self.in_config_submode("config-vns") and \ |
| getattr(self, prefix + "vns_" + name.replace("-","_"), None): |
| return getattr(self, prefix + "vns_" + name.replace("-","_")) |
| return getattr(self, prefix+name.replace("-","_"), None) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # command_method_from_name |
| # |
| def command_method_from_name(self, name): |
| return self.method_from_name("do_", name) # do_XXX methods |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # command_show_method_from_name |
| # |
| def command_show_method_from_name(self, name): |
| return self.method_from_name("do_show_", name) # do_show_XXX methods |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # cloud_method_from_name |
| # |
| def cloud_method_from_name(self, name): |
| return self.method_from_name("cloud_", name) # cloud_XXX methods |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # completion_method_from_name |
| # |
| def completion_method_from_name(self, name): |
| return self.method_from_name("cp_", name) # cp_XXX (completion) methods |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # completion_show_method_from_name |
| # |
| def completion_show_method_from_name(self, name): |
| return self.method_from_name("cp_show_", name) # cp_show_XXX (completion) methods |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # unique_key_from_non_unique |
| # |
| # Primary keys for cassandra for some keys are contenations of |
| # several non-unique keys separated by a character not used for |
| # any other purpose (in this case '|'). The concatenation |
| # of non-unique keys is intended to create a unique key. |
| # |
| def unique_key_from_non_unique(self, words): |
| return "|".join(words) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # prefix_search_key |
| # Prefix's of primary keys for keys's built through unique_key_from_non_unique() |
| # |
| |
| def prefix_search_key(self, words): |
| return self.unique_key_from_non_unique(words) + "|" |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # implement_connect |
| # |
| def implement_connect(self, data): |
| new_ip = data.get('ip-address') |
| if new_ip == None: |
| new_ip = data.get('controller-id') |
| if not self.IP_ADDR_RE.match(new_ip): |
| controller_id = alias_lookup('controller-alias', new_ip) |
| |
| if controller_id == None: |
| controller_id = new_ip |
| |
| try: |
| ifs = self.rest_query_objects('controller-interface', |
| { 'controller' : controller_id } ) |
| if len(ifs) == 1: |
| new_ip = ifs[0]['ip'] |
| except: |
| return self.error_msg('Can\'t find controller named %s' % new_ip) |
| pass |
| |
| if 'port' in data: |
| new_ip += str(data['port']) |
| else: |
| new_ip += ":80" |
| # |
| # request the version, if it fails, then don't allow the switch |
| try: |
| version_url = 'http://%s/rest/v1/system/version' % new_ip |
| data = self.rest_simple_request_to_dict(version_url) |
| except Exception, e: |
| return self.error_msg('Could not connect to %s' % new_ip) |
| |
| self.controller = new_ip |
| self.store.set_controller(new_ip) |
| self.set_controller_for_prompt() |
| while (self.current_mode() != "login"): |
| self.pop_mode() |
| print "Switching to controller %s" % self.controller |
| |
| return self.pp.format_entry(data[0], 'version') |
| |
| def cp_exit(self, words, text, completion_char): |
| self.print_completion_help("<cr>") |
| |
| def do_exit(self, words=None): |
| if self.mode_stack[-1]['exit']: |
| method = self.mode_stack[-1]['exit'] |
| method() |
| self.pop_mode() |
| |
| def cp_logout(self, words, text, completion_char): |
| self.print_completion_help("<cr>") |
| |
| def do_logout(self, words=None): |
| self.run = False |
| |
| def cp_end(self, words, text, completion_char): |
| self.print_completion_help("<cr>") |
| |
| def do_end(self, words=None): |
| while self.current_mode().startswith("config"): |
| self.pop_mode() |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_show_tunnel |
| # Command descriptions has no practical support for show commands yet. |
| # |
| def do_show_tunnel(self, words): |
| if len(words) < 2: |
| print self.syntax_msg("Usage: show tunnel [ <switch> | all ] active") |
| return |
| if words[1] != "active": |
| return self.error_msg("Unknown request %s" % words[1]) |
| |
| # active -- |
| |
| if words[0] != 'all': |
| words[0] = convert_alias_to_object_key("switch", words[0]) |
| |
| try: |
| data = self.rest_simple_request_to_dict( |
| 'http://%s/rest/v1/tunnel-manager/%s/' % |
| (self.controller, words[0])) |
| except: |
| return self.error_msg('Could not load tunnel-manager details') |
| |
| if 'error' in data and data['error'] != None: |
| return self.error_msg('tunnel-manager response: %s' % data['error']) |
| |
| entries = [] |
| for entry in data['tunnMap'].keys(): |
| item = data['tunnMap'][entry] |
| for remote in item['tunnelPorts']: |
| remoteIp = "%s.%s.%s.%s" % ( int(remote[3:6]), |
| int(remote[6:9]), |
| int(remote[9:12]), |
| int(remote[12:])) |
| entries.append({ 'dpid': entry, |
| 'localTunnelIPAddr' : item['localTunnelIPAddr'], |
| 'tunnelPorts' : remoteIp |
| }) |
| |
| return self.display_obj_type_rows('tunnel-details', entries) |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # cp_show_vns_access_list_entry |
| # |
| def cp_show_vns_access_list_entry(self, words, text, completion_char): |
| return self.cp_vns_access_list_entry(words, text) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_show_vns_access_list_entry |
| # |
| def do_show_vns_access_list_entry(self, words): |
| with_search_key = "<no_key>" |
| if self.vns_name() is None: |
| if len(words) > 0: |
| search_object = ["vns-access-list-entry", words[0]] |
| else: |
| search_object = ["vns-access-list-entry"] |
| elif len(words) > 0: |
| with_search_key = '-'.join(words) |
| words.insert(0, self.get_current_mode_obj()) |
| search_object = ["vns-access-list-entry", |
| self.unique_key_from_non_unique(words)] |
| else: |
| search_object = ["vns-access-list-entry", |
| self.prefix_search_key([self.get_current_mode_obj()])] |
| return self.do_show_object(search_object, with_search_key) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_vns_no |
| # implement the no command while in the context of vns/vns-definition |
| # |
| def do_vns_no(self, words): |
| if len(words) < 1: |
| return "Syntax: no <item> <key> : delete from the named item the specific key" |
| else: |
| vns_name_to_table_name = { "interfaces" : "vns-interface", |
| "interface-rule" : "vns-interface-rule", |
| "access-list" : "vns-access-list", |
| "access-list-entry" : "vns-access-list-entry", |
| "access-group" : "vns-interface-access-list", |
| } |
| if words[0] in vns_name_to_table_name: |
| if len(words) < 2: |
| return "Syntax: no %s <item> <key>" % words[0] |
| # |
| # the item to remove in for access-list-entry is an integer, |
| # canonicalize the name before its used to search |
| if words[0] == 'access-list-entry': |
| if self.ACL_RE.match(words[1]): |
| words[1] = str(int(words[1])) |
| else: |
| return self.error_msg('%s key %s must be an integer' % |
| (words[0], words[1])) |
| |
| name = self.unique_key_from_non_unique([self.get_current_mode_obj(), words[1]]) |
| table = vns_name_to_table_name[words[0]] |
| errors = None |
| |
| if words[0] == "entry" and not self.in_config_vns_acl_mode(): |
| return self.do_no(words) # let do_no() print error messages |
| elif words[0] == "access-group" and not self.in_config_vns_if_mode(): |
| return self.do_no(words) # let do_no() print error messages |
| |
| # access-group/vns-interface-access-list has 'id's with an additional field, |
| # the in-out parameter, which is required to be able to delete the entry |
| if words[0] == "access-group": |
| if len(words) < 3: |
| return self.syntax_msg('no access-group <acl-name> <in|out>') |
| choices = [x for x in ["in", "out"] if x.startswith(words[2])] |
| if len(choices) != 1: |
| return self.syntax_msg('no access-group <acl-name> <in|out>') |
| name = self.unique_key_from_non_unique([self.get_current_mode_obj(), |
| self.vns_name(), |
| words[1]]) |
| name = name + "|" + choices[0] |
| |
| try: |
| self.rest_delete_object(table, name) |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, words[0] + " " + words[1]) |
| |
| if errors: |
| return self.rest_error_dict_to_message(errors) |
| # |
| # cascade delete? |
| if not errors: |
| self.cascade_delete(table, name) |
| |
| return None |
| |
| return self.do_no(words) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_no_tag |
| # Since the name and value must be present, this is currently a special |
| # case. additionally, the two tables 'tag-mapping' and 'tag' are |
| # intermingled. the 'tag' table manages the <namespace><name><value> |
| # tuple, while the tag-mapping add <host> to that relationship When |
| # the removal of the entry from the tag-mapping table leaves no further |
| # <namespace><name><value>'s for any hosts, then the row from the |
| # 'tag' table must be removed |
| # |
| def implement_no_tag(self, words): |
| if not words or len(words) != 2 or words[1].find('=') == -1: |
| return "Syntax: no tag <namespace.name>=<value>" |
| else: |
| name_and_value = words[1].split("=") |
| if len(name_and_value) > 2: |
| return "Syntax: no tag <namespace.name>=<value>" |
| |
| name_part = name_and_value[0].split('.') |
| if len(name_part) == 1: |
| namespace = 'default' |
| name = name_part[0] |
| elif len(name_part) >= 2: |
| namespace = '.'.join(name_part[:-1]) |
| name = name_part[-1] |
| |
| value = name_and_value[1] |
| |
| item = self.unique_key_from_non_unique([namespace, |
| name, |
| value, |
| self.get_current_mode_obj()]) |
| # |
| # To prevent leaking the unique key value in the 'Not Found' |
| # error message, look up the item first, and display a unique |
| # error message for this case |
| # |
| errors = None |
| try: |
| self.get_object_from_store('tag-mapping', item) |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, "tag " + item) |
| |
| if errors: |
| if self.rest_error_is_not_found(errors): |
| return self.error_msg("tag %s.%s=%s not found" % |
| (namespace, name, value)) |
| else: |
| return self.rest_error_dict_to_message(errors) |
| |
| try: |
| self.rest_delete_object('tag-mapping', item) |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, "tag " + item) |
| |
| if errors: |
| return self.rest_error_dict_to_message(errors) |
| # |
| # if there are no more items in tag-mapping for the |
| # <namespace>|<name>|<value>, then an entry needs to |
| # removed from the 'tag' table (for cleanup) |
| # |
| key = self.unique_key_from_non_unique([namespace, |
| name, |
| value]) |
| try: |
| tags = self.get_table_from_store('tag-mapping', 'tag', key) |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, "tag-mapping tag " + key) |
| |
| if errors: |
| print self.rest_error_dict_to_message(errors) |
| return |
| if len(tags) == 0: |
| try: |
| self.rest_delete_object('tag', key) |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, "tag " + item) |
| |
| if errors: |
| print self.rest_error_dict_to_message(errors) |
| return |
| # |
| # cascade delete? |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # vns_interface_rule_tags_validate |
| # Provides a warning when the associated tag doesn't exist |
| # |
| def vns_interface_rule_tags_validate(self, obj_type, field, value): |
| for tag_pair in value.split(','): |
| if tag_pair.find('=') == -1 or len(tag_pair.split('=')) != 2: |
| # field validation won't match the regular expression |
| return |
| (tag_name, tag_value) = tag_pair.split('=') |
| tag_name_parts = tag_name.split('.') |
| if len(tag_name_parts) == 1: |
| tag_namespace = 'default' |
| tag_name = tag_name_parts[0] |
| elif len(tag_name_parts) >= 2: |
| tag_namespace = '.'.join(tag_name_parts[:-1]) |
| tag_name = tag_name_parts[-1] |
| else: |
| # XXX not reached |
| pass |
| |
| # Validating using the 'tag' model to determine whether the |
| # tag exists depends on having the tag value removed when there's |
| # no more references to the entry. the cli manages the |
| # 'tag'/'tag-mapping' models that way, but other uses of the |
| # rest api may not. |
| # |
| key = self.unique_key_from_non_unique([tag_namespace, tag_name, tag_value]) |
| try: |
| table = self.get_object_from_store('tag', key) |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, 'tag') |
| # special case for 'not found' error message |
| if self.rest_error_is_not_found(errors): |
| self.warning("tag `%s.%s %s' not yet defined" % \ |
| (tag_namespace, tag_name, tag_value)) |
| else: |
| print self.rest_error_dict_to_message(errors) |
| return None |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # validate_switch_core_switch |
| # Intended to display a warning message when the value is set to True |
| # |
| def validate_switch_core_switch(self, obj_type, field, value): |
| if value == 'True': # XXX odd that the value needs to be quoted. |
| self.warning("enabling core-switch on a switch with " |
| "directly connected hosts will cause the same to " |
| "be unable to connect") |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # handle_specific_tag_fields |
| # Called by handle_specific_obj_type to convert fields values from the |
| # rest api into a displayable table |
| # |
| @staticmethod |
| def handle_specific_tag_fields(entries): |
| for entry in entries: |
| fields = entry['id'].split('|') |
| # XXX error if there's not four parts |
| entry['namespace'] = fields[0] |
| entry['name'] = fields[1] |
| entry['value'] = fields[2] |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # append_when_missing |
| # |
| @staticmethod |
| def append_when_missing(unique_list, item): |
| if not item in unique_list: |
| unique_list.append(item) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # show_command_prefix_matches |
| # show command's prefix matches, return True when the command matches |
| # fpr the prefix described, used by get_valid_show_options() during |
| # cp_show() to collect available command choice. |
| # |
| def show_command_prefix_matches(self, command, prefix): |
| # convert any '-'s in the prefix tp "_"'s since func's have _ separators |
| prefix = prefix.replace("-", "_") |
| if command.startswith("do_show_"+prefix) and not command.startswith("do_show_vns_"): |
| return True |
| elif command.startswith("do_show_vns_" + prefix): |
| if self.in_config_submode("config-vns"): |
| if command == 'do_show_vns_access_list' and self.in_config_vns_mode(): |
| return True |
| return self.vns_debug_show_commands(command) |
| return False |
| return False |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # get_valid_show_options |
| # used by cp_show to identify completion optinos |
| # |
| # returns a dictionary with two elements: 'commands', and 'objects'. |
| # the 'commands' element is a list of possible commands while |
| # the 'objects' element is a list of possible objects (tables/models in the store) |
| # |
| def get_valid_show_options(self, text): |
| ret_hash = {} |
| # first get commands |
| opts = [] |
| matching_methods = [x for x in dir(self) |
| if self.show_command_prefix_matches(x, text)] |
| |
| if onos == 0: |
| netvirt_feature = self.netvirt_feature_enabled() |
| else: |
| netvirt_feature = False |
| |
| for method in matching_methods: |
| m = re.search("do_show_(.*)", method) |
| n = re.search("do_show_vns_(.*)", method) |
| if n and self.in_config_submode('config-vns'): |
| self.append_when_missing(opts, n.group(1).replace("_","-")) |
| elif m: |
| if netvirt_feature or m.group(1) != 'vns': |
| self.append_when_missing(opts, m.group(1).replace("_","-")) |
| # |
| # remove command cases |
| opts = [x for x in opts if x not in ["statistics", "object"]] |
| if "access-group" in opts and not self.in_config_vns_if_mode(): |
| if not self.vns_debug_show_commands('vns-access-group'): |
| opts.remove("access-group") |
| if "access-list" in opts and not self.in_config_vns_mode(): |
| if not self.vns_debug_show_commands('vns-access-list'): |
| opts.remove("access-list") |
| if "interface" in opts and not self.in_config_vns_mode(): |
| if not self.vns_debug_show_commands('vns-interface'): |
| opts.remove("interface") |
| if "interface-rule" in opts and not self.in_config_vns_def_mode(): |
| if not self.vns_debug_show_commands('vns-interface-rule'): |
| opts.remove("interface-rule") |
| if "firewall" in opts and not self.in_config_controller_interface_mode(): |
| opts.remove("firewall") |
| # synthetic object type based on the vnsconfig submode. |
| if "access-list-entry" in opts and not self.in_config_vns_acl_mode(): |
| if not self.vns_debug_show_commands('vns-access-list-entry'): |
| opts.remove("access-list-entry") |
| ret_hash["commands"] = opts |
| |
| # now get obj_types we can show |
| opts = self.all_obj_types_starting_with(text) |
| if self.in_config_submode() and "this".startswith(text): |
| opts.append("this") |
| ret_hash["objects"] = opts |
| return ret_hash |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # cp_show_object |
| # show <obj_type> (words[0] == obj_type) |
| # |
| def cp_show_object(self, words, text, completion_char): |
| if len(words) == 1: |
| return objects_starting_with(words[0], text) |
| else: |
| self.print_completion_help("<cr>") |
| # |
| # -------------------------------------------------------------------------------- |
| # get_attachment_points |
| # |
| @staticmethod |
| def get_attachment_points(host, host_dap_dict): |
| items = host_dap_dict.get(host['mac']) |
| if not items or len(items) <= 1: |
| return items |
| # |
| # sort by most recent items, if the last-seen field |
| # exists for all entries |
| # |
| for item in items: |
| if not 'last-seen' in item or item['last-seen'] == '': |
| return items |
| |
| ordered = sorted(items, |
| key=lambda i: i['last-seen'], |
| cmp=lambda x,y: cmp(y,x)) |
| ordered[0]['prime'] = True |
| return ordered |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # create_attachment_dict |
| # |
| def create_attachment_dict(self): |
| return create_obj_type_dict('host-attachment-point', 'mac') |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # get_ip_addresses |
| # |
| @staticmethod |
| def get_ip_addresses(host, host_ip_dict): |
| items = host_ip_dict.get(host['mac']) |
| if not items or len(items) <= 1: |
| return items |
| |
| for item in items: |
| if not 'last-seen' in item or item['last-seen'] == '': |
| return items |
| # |
| # sort by most recent items, if every row has a 'last-seen' field |
| # |
| ordered = sorted(items, |
| key=lambda i: i['last-seen'], |
| cmp=lambda x,y: cmp(y,x)) |
| ordered[0]['prime'] = True |
| return ordered |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # create_ip_dict |
| # Returns a dictionary with the host id (mac address) as the key, |
| # and a list of associated rows for that host id. |
| # |
| def create_ip_dict(self): |
| return create_obj_type_dict('host-network-address', 'mac') |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # show_sort_obj_type |
| # Returns a sorted list of entries based on the sort described by |
| # the obj-dict |
| # |
| def show_sort_obj_type(self, obj_type, entries): |
| sort = mi.obj_type_show_sort(obj_type) |
| if not sort: |
| return entries |
| if sort == 'integer': |
| key = mi.pk(obj_type) |
| return sorted(entries, key=lambda k: k[key].split('|')[-1], |
| cmp=lambda x,y: cmp(utif.try_int(x), utif.try_int(y))) |
| elif sort == 'tail-integer': |
| key = mi.pk(obj_type) |
| def comparer(left, right): |
| prefix = cmp(left[:-1], right[:-1]) |
| if prefix != 0: |
| return prefix |
| return cmp(utif.try_int(left[-1]), utif.try_int(right[-1])) |
| |
| return sorted(entries, key=lambda k: k[key].split('|'), cmp=comparer) |
| else: |
| return self.error_msg("Unknown sort %s" % sort) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # display_obj_type_rows |
| # Given an obj_type, rows, and whether or not a key was provided to search |
| # with, generate the output table. |
| # |
| def display_obj_type_rows(self, obj_type, rows, with_key = '<no_key>', detail = "default"): |
| # |
| # handle spedicific issues with particular tables returned from storage |
| # - change vns-id's with "|" into separate columns |
| # - the host case does a 'merge' for attachment-points and IPs |
| # (used in 'show host') |
| def handle_specific_obj_type(obj_type, entries, field_orderings = 'default'): |
| if obj_type == "host": |
| # host_tag_dict = create_obj_type_dict('tag-mapping', 'host') |
| for host in entries: |
| host['vendor'] = self.vendordb.get_vendor(host['mac']) |
| # host['tag'] = host_tag_dict.get(host['mac'], []) |
| if obj_type == "host-config": |
| host_dap_dict = self.create_attachment_dict() |
| host_ip_dict = self.create_ip_dict() |
| host_tag_dict = create_obj_type_dict('tag-mapping', 'mac') |
| for host in entries: |
| host['vendor'] = self.vendordb.get_vendor(host['mac']) |
| host['attachment-points'] = self.get_attachment_points(host, host_dap_dict) |
| host['ips'] = self.get_ip_addresses(host, host_ip_dict) |
| host['tag'] = host_tag_dict.get(host['mac'], []) |
| if obj_type == 'controller-node': |
| for controller_node in entries: |
| domain_name_servers = [] |
| domain_name_servers = self.rest_query_objects( |
| 'controller-domain-name-server', |
| {'controller': controller_node['id'], |
| 'orderby' : 'timestamp'}) |
| controller_node['domain-name-servers'] = [domain_name_server['ip'] |
| for domain_name_server in domain_name_servers] |
| controller_node['id'] = ' '.join(controller_node['id'].split('|')) |
| if obj_type == 'controller-interface': |
| for intf in entries: |
| rules = [x['rule'] for x in self.get_firewall_rules(intf['id'])] |
| intf['firewall'] = ', '.join(rules) |
| # del intf['id'] XXX should the id really get displayed? |
| tables_with_vns_ids = [ "vns-interface-rule", "vns-interface", |
| "host-vns-interface", "vns-access-list", |
| "vns-access-list-entry", "vns-interface-access-list" ] |
| if obj_type in tables_with_vns_ids: |
| self.vns_table_id_to_vns_column(obj_type, entries) |
| if self.in_config_submode("config-vns") and field_orderings == 'default': |
| field_orderings = 'vns-config' |
| self.vns_join_host_fields(obj_type, entries) |
| self.vns_foreign_key_to_base_name(obj_type, entries) |
| if obj_type == 'vns-access-list-entry': |
| # display the alternative access-list-text format |
| vns_acl_entries_to_brief(entries) |
| # field_orderings = 'acl-brief' |
| if obj_type == 'vns-access-list-entry-brief': |
| # display the alternative access-list-text format |
| vns_acl_entries_to_brief(entries) |
| field_orderings = 'acl-brief' |
| #if obj_type == 'tag-mapping': |
| #self.handle_specific_tag_fields(entries) |
| |
| # XXX may need to revisit to prevent don't leak vns information |
| |
| if obj_type in ['dvs-port-group', 'firewall-rule', 'tag-mapping']: |
| for entry in entries: |
| mi.split_compound_into_dict(obj_type, |
| mi.pk(obj_type), |
| entry) |
| # |
| # objects where foreigh keys are references to |
| # compount key's, needing to be split. |
| # XXX these can be identified by scanning the objects during init |
| if obj_type in ['switch-interface-alias', |
| ]: |
| # only pick foreign keys which are compound keys |
| fks = [x for x in mi.obj_type_foreign_keys(obj_type) |
| if mi.is_compound_key(obj_type,x)] |
| for entry in entries: |
| for fk in fks: |
| if fk in entry: # fk may be null-able |
| mi.split_compound_into_dict(obj_type, fk, entry) |
| if 'switch-interface' in entry: |
| si = entry['switch-interface'] |
| |
| # |
| if obj_type == 'firewall-rule': |
| self.firewall_rule_add_rule_to_entries(entries) |
| return field_orderings |
| |
| field_orderings = handle_specific_obj_type(obj_type, rows, detail) |
| # |
| # join alias names. there is a good chance the alias's tables |
| # are small. since iteration over all the rows is needed, |
| # first identify the alias tables involved, then snap the |
| # complete table into memory, and then decorae with the |
| # specific alias fields. XXX it would be even better to |
| # get a size of 'rows' vs the 'alias' table, and choose |
| # the more performance path... |
| # |
| if obj_type in mi.alias_obj_type_xref: |
| obj_key = mi.pk(obj_type) |
| for alias in mi.alias_obj_type_xref[obj_type]: |
| alias_field = mi.alias_obj_type_field(alias) |
| if not alias_field: |
| print self.error_msg("internal alias in display_obj_type") |
| try: |
| alias_rows = self.get_table_from_store(alias) |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, alias) |
| print self.rest_error_dict_to_message(errors) |
| alias_rows = [] |
| |
| # invert the row once, moving the key to the data. |
| alias_key = mi.pk(alias) |
| rows_alias = dict([[a[alias_field], a[alias_key]] for a in alias_rows]) |
| |
| # collect the name of the field to join (fk_name) |
| # |
| (fk_obj_type, fk_name) = \ |
| mi.foreign_key_references(alias, alias_field) |
| # now associate alias's with the row's field: |
| for row in rows: |
| if fk_name in row: |
| if row[fk_name] in rows_alias: |
| row[alias] = rows_alias[row[fk_name]] |
| elif fk_obj_type in row: |
| if row[fk_obj_type] in rows_alias: |
| row[alias] = rows_alias[row[fk_obj_type]] |
| # |
| # this choice is better if the alias's table is large. |
| if 0: #for row in rows: |
| key = mi.pk(obj_type) |
| for alias in mi.alias_obj_type_xref[obj_type]: |
| field = mi.alias_obj_type_field(alias) |
| if not field: |
| print self.error_msg("internal alias in display_obj_type") |
| alias_row = self.get_table_from_store(alias, |
| field, |
| row[key], |
| "exact") |
| if len(alias_row) > 1: |
| print 'Error: multiple aliases for %s' % \ |
| alias |
| elif len(alias_row) == 1: |
| row[alias] = alias_row[0][mi.pk(alias)] |
| |
| if len(rows) == 0: |
| if with_key != '<no_key>': |
| return self.error_msg("No %s %s found" % (obj_type, with_key)) |
| else: |
| return self.pp.format_table(rows, obj_type, field_orderings) |
| elif len(rows) == 1 and with_key != '<no_key>': |
| return self.pp.format_entry(rows[0], obj_type, |
| debug = self.debug) |
| |
| return self.pp.format_table(self.show_sort_obj_type(obj_type, rows), |
| obj_type, |
| field_orderings) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_show_object |
| # Implements the show command for objects (table/model output of rows) |
| # |
| def do_show_object(self, words, with_search_key = None): |
| obj_type = words[0] |
| |
| # |
| # The cli replaces some user names with table names |
| if obj_type == 'tag': |
| obj_type = 'tag-mapping' |
| |
| match = "startswith" |
| # |
| # Allow "this" reference if we are in config-submode, change the |
| # match to an exact match to prevent prefix matching |
| if obj_type == "this": |
| if self.in_config_submode(): |
| obj_type = self.get_current_mode_obj_type() |
| words = [obj_type, "this"] |
| match = "exact" |
| else: |
| return self.error_msg("\"this\" can only be used in configuration" |
| " submode, such as switch, port, host, etc.") |
| # |
| # complete table lookup: 'show <table>' |
| if len(words) == 1: |
| if with_search_key == None: |
| with_search_key = "<no_key>" |
| errors = None |
| try: |
| entries = self.get_table_from_store(obj_type) |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, obj_type) |
| |
| if errors: |
| return self.rest_error_dict_to_message(errors) |
| return self.display_obj_type_rows(obj_type, entries, with_search_key) |
| |
| # table with key: "show <table> <key>" |
| if len(words) == 2: |
| obj = words[1] |
| if with_search_key == None: |
| with_search_key = obj |
| |
| # Allow "this" reference if we are in config-submode |
| if obj == "this" and \ |
| obj_type == self.get_current_mode_obj_type() \ |
| and self.in_config_submode(): |
| obj = self.get_current_mode_obj() |
| |
| key = mi.pk(obj_type) |
| if key: |
| obj = convert_alias_to_object_key(obj_type, obj) |
| if mi.obj_type_has_model(obj_type): |
| entries = self.get_table_from_store(obj_type, key, obj, match) |
| else: |
| # XXX raise internal error? model doesn't exist. |
| print command._line(), obj_type |
| entries = [] |
| return self.display_obj_type_rows(obj_type, entries, with_search_key) |
| else: |
| return self.pp.format_table(self.get_table_from_store(obj_type), |
| obj_type) |
| elif "stats".startswith(words[2]): |
| return self.helper_show_object_stats(words) |
| else: |
| return "Syntax: show <table> <key>: " \ |
| "Unrecognized input: " + " ".join(words[2:]) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # helper_show_object_stats |
| # |
| def helper_show_object_stats(self, words): |
| if self.stats_metadata == None: |
| self.init_stats_metadata() |
| |
| # get object type |
| object_type = words[0] |
| |
| # The rest stats api differs from the CLI in some of the |
| # object names, so this map allows us to fix it |
| if object_type in self.stats_restapi_map: |
| object_type = self.stats_restapi_map[object_type] |
| |
| if object_type not in self.stats_type_metadata: |
| return self.error_msg('no statistics available for object type %s' % |
| object_type) |
| |
| # Get the specific object to operate on |
| sobject = words[1]; |
| |
| # the controller-node objects don't currently have any real |
| # concept of a unique ID for a controller; for now the stats |
| # are always labeled 'localhost' even though that will never |
| # appear in the controller-node list |
| if ('controller-node'.startswith(object_type)): |
| sobject = 'localhost' |
| |
| tocheck = [] |
| # get statistic name |
| if len(words) > 3: |
| if (words[3] not in self.stats_metadata or |
| self.stats_metadata[words[3]]['target_type'] != object_type): |
| return self.error_msg('no statistic %s available for object type %s' % |
| (words[3], object_type)) |
| tocheck.append(words[3]) |
| else: |
| tocheck = [item['name'] |
| for item in self.stats_type_metadata[object_type]] |
| |
| parsed_options = {} |
| if (len(words) > 4): |
| try: |
| parsed_options = \ |
| self.parse_optional_parameters(self.stats_optional_params, |
| words[4:]) |
| except ParamException as e: |
| return self.error_msg(e) |
| |
| display = 'latest-value' |
| timespec = False |
| now = int(time.time()*1000) |
| query_params = {'start-time': now - 3600000, |
| 'end-time': now, |
| 'sample-count': 25, |
| 'data-format': 'value'} |
| |
| if 'start-time' in parsed_options: |
| timespec = True |
| try: |
| query_params['start-time'] = self.parse_date(parsed_options['start-time']) |
| except Exception as e: |
| return self.error_msg('invalid start time %s: %s' % |
| (parsed_options['start-time'], e)) |
| |
| if 'end-time' in parsed_options: |
| timespec = True |
| try: |
| query_params['end-time'] = self.parse_date(parsed_options['end-time']) |
| except Exception as e: |
| return self.error_msg('invalid end time %s: %s' % |
| (parsed_options['end-time'], e)) |
| |
| if 'duration' in parsed_options: |
| timespec = True |
| try: |
| query_params['duration'] = parsed_options['duration'] |
| if 'start-time' in parsed_options: |
| del query_params['end-time'] |
| else: |
| del query_params['start-time'] |
| |
| except: |
| return self.error_msg('invalid duration %s' % |
| parsed_options['duration']) |
| |
| if 'display' in parsed_options: |
| display = parsed_options['display'] |
| |
| if timespec and display == 'latest-value': |
| display = 'graph' |
| |
| if display == 'graph': |
| query_params['sample-count'] = self.pp.get_terminal_size()[0] |
| |
| for p in ['sample-count', 'limit', 'sample-window', |
| 'sample-interval', 'data-format']: |
| if p in parsed_options: |
| query_params[p] = parsed_options[p] |
| |
| if display == 'latest-value': |
| data = {} |
| for item in tocheck: |
| url = ('http://%s/rest/v1/stats/data/%s/%s/%s/%s/' % |
| (self.controller, self.cluster, object_type, |
| sobject, item)) |
| try: |
| (timestamp, value) = self.rest_simple_request_to_dict(url) |
| data[self.stats_metadata[item]['name']] = "%d" % value |
| except: |
| data[self.stats_metadata[item]['name']] = "[Null]" |
| |
| return self.pp.format_entry(data, 'stats-%s' % object_type) |
| else: |
| item = tocheck[0] |
| url = ('http://%s/rest/v1/stats/data/%s/%s/%s/%s/?%s' % |
| (self.controller, self.cluster, object_type, |
| sobject, item, "&".join(['%s=%s' % (k,v) |
| for k,v |
| in query_params.iteritems()]))) |
| try: |
| json_data = self.rest_simple_request_to_dict(url) |
| except Exception as e: |
| if self.debug_backtrace: |
| print 'FAILED URL', url |
| return self.error_msg('could not load stats data: %s' % e) |
| |
| if display == 'table': |
| data = [] |
| for (timestamp, value) in json_data: |
| data.append({'timestamp': int(timestamp), |
| 'value': value}) |
| return self.pp.format_table(data, 'stats-%s-%s-%s' % |
| (object_type, item, |
| query_params['data-format'])) |
| elif display == 'graph': |
| return self.pp.format_time_series_graph(json_data, |
| 'stats-%s-%s-%s' % |
| (object_type, item, |
| query_params['data-format'])) |
| # |
| # -------------------------------------------------------------------------------- |
| # parse_date |
| # |
| @staticmethod |
| def parse_date(text): |
| if (text == 'now' or text == 'current'): |
| return int(time.time()*1000) |
| |
| try: |
| return int(text) |
| except: |
| pass |
| |
| for f, pre in [('%Y-%m-%dT%H:%M:%S', None), |
| ('%Y-%m-%d %H:%M:%S', None), |
| ('%Y-%m-%dT%H:%M:%S%z', None), |
| ('%Y-%m-%d %H:%M:%S%z', None), |
| ('%Y-%m-%d', None), |
| ('%m-%d', '%Y-'), |
| ('%H:%M', '%Y-%m-%dT')]: |
| try: |
| t = text |
| if pre: |
| pref = datetime.datetime.now().strftime(pre) |
| f = pre + f |
| t = pref + t |
| |
| thetime = datetime.datetime.strptime(t, f) |
| return int(time.mktime(thetime.timetuple())*1000) |
| except: |
| pass |
| |
| raise ValueError('count not parse %s as a timestamp' % text) |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # display_vns_interface |
| # Search dist is a collection of compund key entities |
| # |
| def display_vns_interface(self, vns_name, search_dict, with_search_key, detail = 'default'): |
| obj_type = 'vns-interface' |
| obj_key = mi.pk(obj_type) |
| try: |
| entries = rest_to_model.get_model_from_url(obj_type, search_dict) |
| #entries = self.rest_query_objects(obj_type, |
| #search_dict) |
| errors = None |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, obj_type) |
| |
| if errors: |
| return self.rest_error_dict_to_message(errors) |
| vns_rules = create_obj_type_dict('vns-interface-rule', |
| mi.pk('vns-interface-rule'), |
| search_dict |
| ) |
| # |
| # divide up the result into two parts, the first part is |
| # the physical interfaces, the second is the mac interfaces... |
| phys = [] |
| macs = [] |
| |
| for entry in entries: |
| fields = entry[obj_key].split('|') |
| entry['tenant'] = fields[0] |
| entry['vns']=fields[1] |
| # all these fields must have two parts. |
| if len(fields) != 3: |
| continue |
| |
| # choose between switch based rules and mac/ip based rules |
| rule = 'default' |
| switch = None |
| virt = None |
| # |
| # Each address-space would have a default vns named |
| # named <as>-default, except the 'default' adddress-space which |
| # has a default vns named just 'default' |
| # |
| if fields[1] == "default" or fields[1].endswith("-default"): |
| virt = 'default' |
| if fields[2].find('/') >= 0: |
| virt = fields[2].split('/')[1] |
| else: |
| if not 'rule' in entry or entry['rule']=='default': |
| if_rule = [] |
| virt = True |
| # continue |
| else: |
| rule = entry['rule'] |
| if_rule = vns_rules[rule][0] |
| |
| if 'switch' in if_rule: |
| switch = if_rule['switch'] |
| elif 'mac' in if_rule: |
| virt = if_rule['mac'] |
| entry['mac'] = virt |
| elif 'ip-subnet' in if_rule: |
| virt = {'ip-address': if_rule['ip-subnet']} |
| if 'ips' in entry: |
| entry['ips'].append(virt) |
| else: |
| entry['ips'] = [virt] |
| elif 'tags' in if_rule: |
| virt = if_rule['tags'] |
| entry['tags'] = virt |
| elif 'vlans' in if_rule: |
| virt = "vlan %s" % if_rule['vlans'] |
| entry['vlans'] = virt |
| |
| if switch: |
| phys.append(entry) |
| if virt: |
| entry['veth'] = entry[obj_key].split('|')[2] |
| macs.append(entry) |
| |
| output = '' |
| if len(phys): |
| self.vns_join_switch_fields(vns_name, phys) |
| output += self.display_obj_type_rows('vns-interface-phys', phys, with_search_key, detail) |
| if len(macs): |
| if len(phys): |
| output += '\n' |
| self.vns_join_host_fields(obj_type, macs) |
| output += self.display_obj_type_rows('vns-interface-macs', macs, with_search_key, detail) |
| |
| if output != '': |
| return output |
| return self.pp.format_table([],'display-vns-interface') |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # find_and_display_vns_interfaces |
| # |
| def find_and_display_vns_interface(self, vns_name, words): |
| with_search_key = "<no_key>" |
| search_dict = { 'vns' : vns_name } |
| if len(words) == 1: |
| search_dict['interface'] = words[0] |
| elif len(words) > 1: |
| return self.error_msg("Additional search keys after interace") |
| |
| return self.display_vns_interface(vns_name, search_dict, with_search_key) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_show_vns_interfaces |
| # This definition is needed to allow the vns-interface table/model to |
| # be displayed in basic config mode. do_show() searches names of functions |
| # using the prefix of the 'show' argument; without this function, |
| # it find 'do_show_vns_interface_rule' and selects that function to |
| # display results for 'show vns-interface`. |
| # |
| def do_show_vns_interfaces(self, words): |
| with_search_key = "<no_key>" |
| local_vns_name = self.vns_name() |
| if self.vns_name() is None: |
| if len(words) > 0: |
| local_vns_name = words[0] |
| search_dict = { 'vns' : local_vns_name } |
| else: |
| # only case where the vns name is included is the output |
| return self.do_show_object(['vns-interface'], with_search_key) |
| else: |
| search_dict = { 'vns' : local_vns_name } |
| if len(words) == 1: |
| with_search_key = words |
| search_dict['interface'] = words[0] |
| elif len(words) > 2: |
| return self.error_msg("Additional search keys after interace") |
| |
| return self.display_vns_interface(local_vns_name, search_dict, with_search_key) |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # display_vns_mac_address_table |
| # Used by 'show vns <n> mac-address-table', and vns mode 'show mac-address-table' |
| # |
| def display_vns_mac_address_table(self, vns_name, words): |
| # note: 'config-vns' matches both vns and vns-definition mode, |
| # while 'config-vns-' matches only vns-definition mode. |
| # the id for the host-vns-interface table has the host as the 'prefix', |
| # preventing searching based on the prefix. the vns is not even a foreign |
| # key, which means the complete table must be read, then the vns association |
| # must be determined, and then matched |
| filter = { 'vns' : vns_name } if vns_name != 'all' else {} |
| if len(words): |
| filter['mac'] = words[0] |
| entries = rest_to_model.get_model_from_url('host-vns-interface', filter) |
| for entry in entries: |
| fields=entry['vns'].split('|') |
| entry['tenant']=fields[0] |
| entry['vns']=fields[1] |
| with_key = '<no_key>' |
| if len(words) > 0: |
| with_key = words[0] |
| |
| #detail = 'vns' # exclude vns column, vns is named |
| detail = 'default' # exclude vns column, vns is named |
| if vns_name == 'all': |
| detail = 'default' # include vns column, vns not named |
| |
| # self.vns_join_host_fields('host-vns-interface', entries) |
| return self.display_obj_type_rows('host-vns-interface-vns', entries, with_key, 'default') |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_show_vns_mac_address_table |
| # config and vns mode 'show mac-address-table' command |
| # |
| def do_show_vns_mac_address_table(self, words): |
| if words == None or len(words) > 2: |
| return self.syntax_msg('show mac-address-table <host>') |
| |
| if self.in_config_submode('config-vns'): |
| return self.display_vns_mac_address_table(self.vns_name(), words) |
| |
| if len(words) > 0: |
| return self.do_show_object(['host'] + words, 'with_key') |
| return self.do_show_object(['host']) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_show_switch_cluster |
| # |
| def do_show_switch_cluster(self, words): |
| data = self.rest_simple_request_to_dict( |
| 'http://%s/rest/v1/realtimestatus/network/cluster/' % self.controller) |
| |
| formatted_data = [] |
| for (clusterid, switchids) in data.items(): |
| row = {} |
| row['switches'] = switchids |
| row['cluster-id'] = clusterid |
| formatted_data.append(row) |
| return self.display_obj_type_rows('switch-cluster', formatted_data) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_show_controller_summary |
| # |
| def do_show_controller_summary(self, words): |
| data = self.rest_simple_request_to_dict( |
| 'http://%s/rest/v1/controller/summary' % self.controller) |
| |
| # XXX Need a way to sort the data in a way that makes sense |
| for (key, value) in data.items(): |
| yield key + ": " + str(value) + "\n" |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # get_statistics_type |
| # |
| @staticmethod |
| def get_statistics_type(stat_type): |
| for s in ["flow", "port", "table", "desc", "aggregate", "features", "trace",]: |
| if s.startswith(stat_type): |
| return s |
| return None |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # fix_realtime_flows_wildcard_fields |
| # |
| def fix_realtime_flow_wildcard_fields(self, in_data): |
| for l in in_data: |
| if 'wildcards' in l: |
| wildcards = l['wildcards'] |
| bit_field_map = { |
| 0 : 'inputPort', |
| 1 : 'dataLayerVirtualLan', |
| 2 : 'dataLayerSource', |
| 3 : 'dataLayerDestination', |
| 4 : 'dataLayerType', |
| 5 : 'networkProtocol', |
| 6 : 'transportSource', |
| 7 : 'transportDestination', |
| 13 : 'networkSource', |
| 19 : 'networkDestination', |
| 20 : 'dataLayerVirtualLanPriorityCodePoint', |
| 21 : 'networkTypeOfService', |
| } |
| for (k, v) in bit_field_map.items(): |
| if wildcards & (1 << k): |
| l[v] = "*" |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # fix_realtime_flows |
| # |
| def fix_realtime_flows(self, in_data): |
| if not in_data: |
| return |
| # flatten match data |
| for l in in_data: |
| match_dict = l['match'] |
| l.update(match_dict) |
| del(l['match']) |
| |
| self.fix_realtime_flow_wildcard_fields(in_data) |
| return in_data |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # add_dpid_to_data |
| # |
| @staticmethod |
| def add_dpid_to_data(data): |
| if not type(data) is dict: # backward-compatible |
| return data |
| formatted_list = [] |
| for (dpid, rows) in data.items(): |
| if rows != None: |
| for row in rows: |
| row['switch'] = dpid |
| formatted_list.append(row) |
| ## TODO - Alex - handle timeouts + sorting |
| return formatted_list |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_show_statistics |
| # |
| # LOOK! we could get rid of show statistics at this point as it is all under |
| # show switch! |
| # |
| # it's confusing to remap it over here... |
| # we could remove show flow-entry realtime too for now |
| # |
| def do_show_statistics(self, words): |
| if len(words) < 1: |
| return "Syntax: show statistics < flow | port | table | desc | aggregate | " \ |
| "features | trace > < Switch DPID | Switch alias | all >" |
| stat_type = self.get_statistics_type(words[0]) |
| dpid = None |
| if len(words) > 1: |
| words[1] = convert_alias_to_object_key("switch", words[1]) |
| dpid = words[1] # LOOK! Eventually optional so we can show across switches |
| if not dpid in objects_starting_with("switch", dpid) and dpid != "all": |
| return self.error_msg("No switch %s found" % dpid) |
| if stat_type == "flow": |
| data = self.rest_simple_request_to_dict( |
| 'http://%s/rest/v1/realtimestats/flow/%s/' % (self.controller,dpid)) |
| data = self.add_dpid_to_data(data) |
| data = self.fix_realtime_flows(data) |
| |
| # |
| # it may make sense to move ['details', 'brief'] tp some sort of |
| # 'show_object_optional_format_keywords' list |
| field_ordering = "default" |
| if len(words) == 3: |
| choices = [x for x in ["details", "brief"] if x.startswith(words[2])] |
| if len(choices) != 1: |
| return self.error_msg("switch flow options are either 'details' or 'brief'") |
| else: |
| field_ordering = choices[0] |
| table_data = self.pp.format_table(data, "realtime_flow", field_ordering) |
| return table_data |
| elif stat_type == "trace": |
| return self.do_trace(words) |
| elif words[0] == "switches": |
| data = self.rest_simple_request_to_dict( |
| 'http://%s/rest/v1/controller/stats/%s/' % (self.controller, words[0])) |
| table_data = self.pp.format_table(data, "controller_"+words[0]) |
| return table_data |
| else: |
| data = self.rest_simple_request_to_dict( |
| 'http://%s/rest/v1/realtimestats/%s/%s/' % (self.controller, stat_type, dpid)) |
| if stat_type == "features": |
| for dpid in data: |
| data[dpid] = data[dpid]["ports"] |
| data = self.add_dpid_to_data(data) |
| table_data = self.pp.format_table(data, "realtime_"+stat_type) |
| return table_data |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_show_version |
| # |
| def do_show_version(self, words): |
| # LOOK! this should probably come from the controller, but for now |
| # we assume the CLI/controller are from the same build |
| version_string = "SDNOS 1.0 - custom version" |
| try: |
| fh = open("/opt/sdnplatform/release") |
| version_string = fh.readline().rstrip() |
| fh.close() |
| except: |
| pass |
| return version_string |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_date |
| # |
| @staticmethod |
| def do_date(words): |
| return time.strftime("%Y-%m-%d %H:%M:%S %Z", time.localtime()) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # cp_show_logging |
| # |
| def cp_show_logging(self, words, text, completion_char): |
| if len(words) == 1: |
| return [x for x in ["sdnplatform", "cassandra", "console-access", |
| "syslog", "authlog", "orchestrationlog", |
| "all" ] if x.startswith(text)] |
| else: |
| self.print_completion_help("<cr>") |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # generate_subpprocess_output |
| # |
| def generate_subprocess_output(self, cmd): |
| process = subprocess.Popen(cmd, shell=True, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT, |
| bufsize=1) |
| while True: |
| line = process.stdout.readline() |
| if line != None and line != "": |
| yield line |
| else: |
| break |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # |
| def implement_show_logging(self, words): |
| log_files = { "sdnplatform" : "cat /opt/sdnplatform/sdnplatform/log/sdnplatform.log", |
| "cassandra" : "cat /opt/sdnplatform/db/log/system.log", |
| "console-access" : "cat /opt/sdnplatform/con/log/access.log", |
| "syslog" : "cat /var/log/syslog", |
| "authlog" : "cat /var/log/auth.log", |
| "orchestrationlog" : "cat /tmp/networkservice.log", |
| "dmesg" : "dmesg", |
| "all" : "" # special |
| } |
| if len(words) < 1: |
| yield "Syntax: show logging < %s > " % " | ".join(log_files.keys()) |
| return |
| for (f, cmd) in log_files.items(): |
| if (f.startswith(words[0]) or "all".startswith(words[0])) and f != "all": |
| yield "*"*80 + "\n" |
| yield "output of %s for %s\n" % (cmd, f) |
| yield "*"*80 + "\n" |
| for item in self.generate_subprocess_output(cmd): |
| yield item |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_show_tech_support |
| # |
| def do_show_tech_support(self, words): |
| |
| oldd = self.debug |
| |
| tech_support_table = [] |
| |
| # Add the user customized command in tech-support-config table |
| entries = self.get_table_from_store('tech-support-config') |
| for entry in entries: |
| # need explicit type casting str() to make command handling work |
| tech_support_table.append([str(entry['cmd-type']), str(entry['cmd']), None]) |
| |
| # Add the list of command configured for the basic support |
| for entry in tech_support.get_show_tech_cmd_table(): |
| tech_support_table.append(entry) |
| |
| |
| for entry in tech_support_table: |
| cmd_type = entry[0] |
| cmd = entry[1] |
| feature = entry[2] |
| |
| # If feature is non-null, then check feature is enabled |
| # before executing the command |
| if feature != None and self.feature_enabled(feature) == False : |
| continue |
| |
| if cmd_type == 'cli': |
| yield "\n\n" + "-"*80 + "\nExecute cli: " + cmd + "\n\n" |
| try: |
| for item in self.generate_command_output(self.handle_single_line(cmd)): |
| yield item |
| except Exception as e: |
| yield "Failed to execute %s: %s\n" % (cmd, e) |
| elif cmd_type == 'shell': |
| yield "\n\n" + "-"*80 + "\nExecuting os command: " + cmd + "\n\n" |
| try: |
| for item in self.generate_subprocess_output(cmd): |
| yield item |
| except Exception as e: |
| yield "Failed to run command %s: %s\n" % (cmd, e) |
| else: |
| yield "Unknown command type %s: %s\n" % (cmd_type, cmd) |
| |
| self.debug = oldd |
| |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # ell_ess_minus_r |
| # Do "ls -R", returing the list of files underneath "prefix". |
| # The intended external call is |
| # ell_ess_minus_r(returned_items, "/opt/sdnplatform/...", ""), |
| # where the path parameter is null to begin the walk. |
| # |
| # The returned items won't have the original prefix in the items |
| # |
| # XXX: There's no bounding for the recursive descent. |
| # |
| def ell_ess_minus_r(self, items, prefix, path): |
| p = os.path.join(prefix, path) |
| if os.path.isdir(p): |
| for entry in os.listdir(p): |
| new_path = os.path.join(path, entry) |
| if os.path.isdir(os.path.join(prefix, new_path)): |
| self.ell_ess_minus_r(items, prefix, new_path) |
| else: |
| items.append(os.path.join(path, entry)) |
| return items |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # implement_show_config_file |
| # 'show config file' displays the local files under self.saved_configs_dirname |
| # currently /opt/sdnplatform/run/saved-configs |
| # |
| def implement_show_config_file(self, words): |
| items = [] |
| self.ell_ess_minus_r(items, self.saved_configs_dirname, "") |
| |
| if len(words) == 1: |
| entries = [] |
| for i in items: |
| cf = os.path.join(self.saved_configs_dirname, i) |
| t = time.strftime("%Y-%m-%d.%H:%M:%S", |
| time.localtime(os.path.getmtime(cf))) |
| entries.append({'name' : i, |
| 'timestamp': t, |
| 'length' : str(os.path.getsize(cf)), |
| }) |
| return self.pp.format_table(entries,'config') |
| elif len(words) == 2: |
| if words[1] in items: |
| src_file = self.path_concat(self.saved_configs_dirname, words[1]) |
| if src_file == "": |
| return self.error_msg("file %s could not be " |
| "interpreted as a valid source" % words[1]) |
| try: |
| return self.store.get_text_from_url("file://" + src_file) |
| except Exception, e: |
| return self.error_msg(words[1] + ":" + str(e)) |
| else: |
| return self.error_msg("%s file unknown" % words[1]) |
| # |
| # |
| # -------------------------------------------------------------------------------- |
| # implement_show_config |
| # |
| def implement_show_config(self, words): |
| name = None |
| show_version = "latest" |
| # handle sh config [ <name> [ all | <version #> ] ] |
| |
| if len(words) == 1: |
| if words[0] == "all": |
| show_version = "all" |
| elif words[0] == "file": |
| return self.implement_show_config_file(words) |
| elif words[0].startswith('config://'): |
| name = words[0][len('config://'):] |
| else: |
| name = words[0] |
| elif len(words) == 2: |
| if words[0] == 'file': |
| return self.implement_show_config_file(words) |
| name = words[0] |
| show_version = words[1] |
| elif len(words) == 3 and words[1] == "diff": |
| return self.diff_two_configs(words[0], words[2]) |
| elif len(words) == 4 and words[1] == "diff": |
| return self.diff_two_versions(words[0], words[2], words[3]) |
| |
| data = self.store.get_user_data_table(name, show_version) |
| if data and len(data) > 1 or name == None: |
| return self.pp.format_table(data,'config') |
| elif data and len(data) == 1: |
| return self.store.get_user_data_file(data[0]['full_name'])[:-1] |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # diff_two_versions |
| # |
| def diff_two_versions(self, name, va, vb): |
| va_info = self.store.get_user_data_table(name, va) |
| if len(va_info) == 0: |
| return self.error_msg("Version %s missing for %s" % (va, name)) |
| va_txt = self.store.get_user_data_file(va_info[0]['full_name']) |
| vb_info = self.store.get_user_data_table(name, vb) |
| if len(vb_info) == 0: |
| return self.error_msg("Version %s missing for %s" % (vb, name)) |
| vb_txt = self.store.get_user_data_file(vb_info[0]['full_name']) |
| import tempfile |
| fa = tempfile.NamedTemporaryFile(delete=False) |
| fa.write(va_txt) |
| fa.close() |
| fb = tempfile.NamedTemporaryFile(delete=False) |
| fb.write(vb_txt) |
| fb.close() |
| cmd = 'diff %s %s ' % (fa.name, fb.name) |
| process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) |
| (output, stderr) = process.communicate() |
| os.unlink(fa.name) |
| os.unlink(fb.name) |
| return output |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # diff_two_configs |
| # |
| def diff_two_configs(self, config_a, config_b): |
| items = self.store.get_user_data_table(config_a, "latest") |
| if len(items) == 0: |
| if config_a == 'running-config': |
| txt_a = run_config.implement_show_running_config([]) + "\n" |
| else: |
| return self.error_msg("%s config missing" % config_a) |
| else: |
| txt_a = self.store.get_user_data_file(items[0]['full_name']) |
| |
| items = self.store.get_user_data_table(config_b, "latest") |
| if len(items) == 0: |
| if config_b == 'running-config': |
| txt_b = run_config.implement_show_running_config([]) + "\n" |
| else: |
| return self.error_msg("%s config missing" % config_b) |
| else: |
| txt_b = self.store.get_user_data_file(items[0]['full_name']) |
| |
| import tempfile |
| fa = tempfile.NamedTemporaryFile(delete=False) |
| fa.write(txt_a) |
| fa.close() |
| fb = tempfile.NamedTemporaryFile(delete=False) |
| fb.write(txt_b) |
| fb.close() |
| cmd = 'diff %s %s ' % (fa.name, fb.name) |
| process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) |
| (output, stderr) = process.communicate() |
| os.unlink(fa.name) |
| os.unlink(fb.name) |
| return output |
| |
| # |
| # -------------------------------------------------------------------------- |
| # |
| |
| def get_host_attachment_switch_port (self, query_dict): |
| """ |
| Get a host's attachment point if it is not already provided. |
| see if the host has an attachment point, use the switch and port |
| of the attachment point, if its available |
| """ |
| host = rest_to_model.get_model_from_url('host', query_dict) |
| if len(host) == 0: |
| print self.error_msg('Can\'t identify host details: %s' % query_dict) |
| return (False,None,None) |
| attachment_points = host[0]['attachment-points'] |
| if attachment_points == None or len(attachment_points) == 0: |
| print self.error_msg('Can\'t identify attachment point %s' % query_dict) |
| return (False,None,None) |
| if len(attachment_points) == 1: |
| src_sw = attachment_points[0]['switch'] |
| src_sw_port = attachment_points[0]['ingress-port'] |
| return (True, src_sw, src_sw_port) |
| |
| interface_dict = create_obj_type_dict('interfaces', 'id') |
| show_list = [] |
| for ap in attachment_points: |
| dpid = ap['switch'] |
| ingress_port = ap['ingress-port'] |
| |
| # alias for dpid |
| switch_name = alias_lookup_with_foreign_key('switch-alias', dpid) |
| if not switch_name: |
| switch_name = dpid |
| # interface name for attachment point |
| ap_port_id = dpid + '|' + str(ingress_port) |
| if ap_port_id in interface_dict: |
| show_list.append("%s/%s" % (switch_name, |
| interface_port[ap_port_od][0]['portName'])) |
| else: |
| show_list.append("%s/port %s" % (switch_name, ingress_port)) |
| |
| print self.error_msg('src-host %s: multiple attachment points: %s' % |
| (src_mac, ', '.join(show_list))) |
| print self.error_msg('rerun, and identify src-switch and interface') |
| return (False,None,None) |
| |
| |
| # |
| # -------------------------------------------------------------------------- |
| # test packet-in: swap source and destination attributes |
| # |
| def test_pkt_in_swap_src_dest_attributes(self, post_data): |
| # Remove the source switch and port if needed |
| if ("srcSwitchDpid" in post_data): |
| post_data.pop("srcSwitchDpid") |
| if ("srcSwitchInPort" in post_data): |
| post_data.pop("srcSwitchInPort") |
| |
| # |
| # Get destination host's attachment switch/port information |
| # |
| (result, src_sw, src_sw_port) = self.get_host_attachment_switch_port( |
| { 'mac' : post_data["destinationMACAddress"]}) |
| if not result: |
| return |
| post_data["srcSwitchDpid"] = src_sw |
| post_data["srcSwitchInPort"] = src_sw_port |
| |
| # Swap the macs |
| temp = post_data["sourceMACAddress"] |
| post_data["sourceMACAddress"] = post_data["destinationMACAddress"] |
| post_data["destinationMACAddress"] = temp |
| # swap the IP addresses if specified |
| tempSrc = None |
| tempDst = None |
| if ("sourceIpAddress" in post_data): |
| tempSrc = post_data["sourceIpAddress"] |
| post_data.pop("sourceIpAddress") |
| if ("destinationIpAddress" in post_data): |
| tempDst = post_data["destinationIpAddress"] |
| post_data.pop("destinationIpAddress") |
| if (tempSrc is not None): |
| post_data["destinationIpAddress"] = tempSrc |
| if (tempDst is not None): |
| post_data["sourceIpAddress"] = tempDst |
| # swap the IP ports if specified |
| tempSrc = None |
| tempDst = None |
| if ("ipL4SourcePort" in post_data): |
| tempSrc = post_data["ipL4SourcePort"] |
| post_data.pop("ipL4SourcePort") |
| if ("ipL4DestinationPort" in post_data): |
| tempDst = post_data["ipL4DestinationPort"] |
| post_data.pop("ipL4DestinationPort") |
| if (tempSrc is not None): |
| post_data["ipL4DestinationPort"] = tempSrc |
| if (tempDst is not None): |
| post_data["ipL4SourcePort"] = tempDst |
| |
| # |
| # -------------------------------------------------------------------------- |
| # test packet-in: display packet-in processing result for test packet-in |
| # |
| def test_pkt_in_result_display(self, response): |
| out = "\nResult of packet-in processing\n" |
| out += ("------------------------------\n") |
| src_vns_name = response['srcVNSName'] |
| dst_vns_name = response['destVNSName'] |
| |
| if (response['explanation'] == "Source switch not found"): |
| out += response['explanation'] |
| return out |
| |
| out += "\nVirtual Routing Processing iterations\n" |
| out += ("--------------------------------------\n") |
| ep_vrouting = response['expPktVRouting'] |
| for ep_vr in ep_vrouting: |
| srcIface = ep_vr['srcIface'] |
| dstIface = ep_vr['dstIface'] |
| action = ep_vr['action'] |
| out += ("src VNS iface : %s\n" % srcIface) |
| out += ("dst VNS iface : %s\n" % dstIface) |
| out += ("action : %s\n" % action) |
| if action == 'DROP': |
| dropReason = ep_vr['dropReason'] |
| out += ("drop reason : %s\n" % dropReason) |
| else: |
| nextHopIp = ep_vr['nextHopIp'] |
| out += ("next hop IP : %s\n" % nextHopIp) |
| nextHopGatewayPool = ep_vr['nextHopGatewayPool'] |
| if (nextHopGatewayPool != None): |
| out += ("next hop Gateway Pool : %s\n" % nextHopGatewayPool) |
| out += "\n" |
| |
| if (src_vns_name == None) and (dst_vns_name == None): |
| out += "Source and destination hosts are not in the same vns" |
| return out |
| |
| out += ("Status : %s\n" % response['status']) |
| out += ("Explanation : %s\n" % response['explanation']) |
| |
| out += "\nResult of Virtual Routing\n" |
| out += ("--------------------------\n") |
| out += ("Source VNS : %s\n" % src_vns_name) |
| out += ("Dest. VNS : %s\n" % dst_vns_name) |
| out += ("Input ACL:\n") |
| out += (" Input ACL Name : %s\n" % response['inputAcl']['aclName']) |
| out += (" Input ACL Entry : %s\n" % response['inputAcl']['aclEntry']) |
| out += (" Input ACL Action : %s\n" % response['inputAcl']['aclResult']) |
| out += ("Output ACL:\n") |
| out += (" Output ACL Name : %s\n" % response['outputAcl']['aclName']) |
| out += (" Output ACL Entry : %s\n" % response['outputAcl']['aclEntry']) |
| out += (" Output ACL Action : %s\n" % response['outputAcl']['aclResult']) |
| |
| if response['expPktRoute'] == None: |
| out += "\n" |
| return out |
| |
| out += ("Routing Action : %s\n" % response['routingAction']) |
| |
| obj_type_show_alias_update('test-pktin-route') |
| out += ('\nFlow Path:\n') |
| ep_route = response['expPktRoute'] |
| for ep_cluster in ep_route: |
| ep_cluster_path = ep_cluster['path'] |
| hop = 1 |
| route_data = [] |
| for ep_path in ep_cluster_path: |
| oneHop = {} |
| oneHop['cluster'] = ep_cluster['clusterNum'] |
| oneHop['hop'] = hop |
| hop += 1 |
| oneHop.update(ep_path) |
| route_data.append(oneHop) |
| route_data = self.pp.format_table(route_data, 'test-pktin-route') |
| out += route_data |
| out += "\n" |
| return out |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # implement_test_packet_in |
| # |
| def implement_test_packet_in(self, data): |
| # Currently only supports <test packet-in> for "explain packet" feature |
| self.debug_msg("In do_test(): data=%s" % (data)) |
| |
| # REST API expects MAC not host aliases |
| if not 'src-host' in data: |
| print self.error_msg("Missing src-host") |
| return |
| src_mac = data['src-host'] |
| |
| if not 'dst-host' in data: |
| print self.error_msg("Missing dst-host") |
| return |
| dst_mac = data['dst-host'] |
| |
| # Check that src and dest hosts are not the same |
| if src_mac == dst_mac: |
| print self.error_msg("source and destination hosts can not be same") |
| return |
| |
| # |
| if 'src-switch' in data: |
| src_sw = data['src-switch'] |
| src_sw_port = data['src-switch-port'] |
| else: |
| |
| # |
| # Get host's attachment switch/port information |
| # |
| (result, src_sw, src_sw_port) = \ |
| self.get_host_attachment_switch_port({'mac' : src_mac}) |
| if not result: |
| return |
| |
| post_data = { |
| "sourceMACAddress": src_mac, "destinationMACAddress": dst_mac, |
| "srcSwitchDpid" : src_sw, "srcSwitchInPort" : src_sw_port |
| } |
| |
| if 'vlan' in data: |
| post_data["vlanID"] = data['vlan'] |
| if 'ether-type' in data: |
| post_data["etherType"] = data['ether-type'] |
| if (post_data['etherType'] != "2048"): |
| yield(self.error_msg("Supported ethertypes: 2048 (ip)")) |
| return |
| if 'priority' in data: |
| post_data["priorityCode"] = data['priority'] |
| if 'src-ip-address' in data: |
| post_data["sourceIpAddress"] = data['src-ip-address'] |
| if 'dst-ip-address' in data: |
| post_data["destinationIpAddress"] = data['dst-ip-address'] |
| if 'protocol' in data: |
| post_data["ipv4Protocol"] = data['protocol'] |
| if 'tos' in data: |
| #Uncomment the line below once we support tos in the REST API |
| #post_data["tos"] = data['tos'] |
| pass |
| if 'src-port' in data: |
| post_data["ipL4SourcePort"] = data['src-port'] |
| if 'dst-port' in data: |
| post_data["ipL4DestinationPort"] = data['dst-port'] |
| |
| self.debug_msg("Post Data = %s" % post_data) |
| |
| url = "http://%s/rest/v1/realtimetest/network/explain-packet" % self.controller |
| |
| try: |
| jresponse = self.store.rest_post_request(url, post_data) |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, url) |
| yield(self.error_msg(self.rest_error_dict_to_message(errors))) |
| return |
| |
| response = json.loads(jresponse) |
| self.debug_msg("Response = %s" % response) |
| |
| |
| yield ("Input packet\n") |
| yield ("------------\n") |
| explPkt = response['explainPktParams'] |
| switch_alias = alias_lookup_with_foreign_key("switch-alias", explPkt['srcSwitchDpid']) |
| src_host_alias = alias_lookup_with_foreign_key("host-alias", explPkt['sourceMACAddress']) |
| dst_host_alias = alias_lookup_with_foreign_key("host-alias", explPkt['destinationMACAddress']) |
| |
| if (src_host_alias): |
| yield ("Source host : %s (%s), ip=%s\n" % |
| (explPkt['sourceMACAddress'], src_host_alias, explPkt['sourceIpAddress'])) |
| else: |
| yield ("Source host : %s, ip=%s\n" % (explPkt['sourceMACAddress'], explPkt['sourceIpAddress'])) |
| |
| if (dst_host_alias): |
| yield ("Destination host : %s (%s), ip=%s\n" % |
| (explPkt['destinationMACAddress'], dst_host_alias, explPkt['destinationIpAddress'])) |
| else: |
| yield "Destination host : %s, ip=%s\n" % (explPkt['destinationMACAddress'], explPkt['destinationIpAddress']) |
| if (explPkt['vlanID'] == '-1'): |
| yield "VLAN : %s (Untagged)\n" % explPkt['vlanID'] |
| else: |
| yield "VLAN : %s\n" % explPkt['vlanID'] |
| yield "802.1Q priority : %s\n" % explPkt['priorityCode'] |
| yield "Ether type : %s\n" % explPkt['etherType'] |
| if (switch_alias): |
| yield "Source switch/port : %s (%s)/%s\n" % (explPkt['srcSwitchDpid'], switch_alias, explPkt['srcSwitchInPort']) |
| else: |
| yield "Source switch/port : %s/%s\n" % (explPkt['srcSwitchDpid'], explPkt['srcSwitchInPort']) |
| yield "Protocol : %s\n" % explPkt['ipv4Protocol'] |
| yield "L4 source port : %s\n" % explPkt['ipL4SourcePort'] |
| yield "L4 destination port: %s\n" % explPkt['ipL4DestinationPort'] |
| |
| yield("\nForward path:") |
| yield("=============\n") |
| yield(self.test_pkt_in_result_display(response)) |
| |
| # Now send the reverse test packet |
| self.test_pkt_in_swap_src_dest_attributes(post_data) |
| try: |
| jresponse = self.store.rest_post_request(url, post_data) |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, url) |
| yield(self.error_msg(self.rest_error_dict_to_message(errors))) |
| return |
| |
| response = json.loads(jresponse) |
| self.debug_msg("Response = %s" % response) |
| yield("\nReverse path:") |
| yield("=============\n") |
| yield(self.test_pkt_in_result_display(response)) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # |
| def implement_test_path(self, data): |
| |
| # compute source |
| # |
| if 'src-switch' in data: |
| src_sw = data['src-switch'] |
| src_sw_port = data['src-switch-port'] |
| elif 'src-host' in data: |
| (result, src_sw, src_sw_port) = \ |
| self.get_host_attachment_switch_port({ 'mac' : data['src-host']}) |
| if not result: |
| return |
| elif 'src-ip' in data: |
| # look up the ip in the device api |
| (result, src_sw, src_sw_port) = \ |
| self.get_host_attachment_switch_port({ 'ipv4' : data['src-ip']}) |
| if not result: |
| return |
| else: |
| return self.error_msg('No source attachment point in request') |
| |
| # compute dest |
| if 'dst-switch' in data: |
| dst_sw = data['dst-switch'] |
| dst_sw_port = data['dst-switch-port'] |
| elif 'dst-host' in data: |
| (result, dst_sw, dst_sw_port) = \ |
| self.get_host_attachment_switch_port({ 'mac' : data['dst-host']}) |
| if not result: |
| return |
| elif 'dst-ip' in data: |
| # look up the ip in the device api |
| (result, dst_sw, dst_sw_port) = \ |
| self.get_host_attachment_switch_port({ 'ipv4' : data['dst-ip']}) |
| if not result: |
| return |
| else: |
| return self.error_msg('No dest attachment point in request') |
| |
| url = "http://%s/rest/v1/realtimetest/network/path" % self.controller |
| request = { |
| 'src-switch' : src_sw, |
| 'src-switch-port' : src_sw_port, |
| 'dst-switch' : dst_sw, |
| 'dst-switch-port' : dst_sw_port, |
| } |
| |
| try: |
| response = self.store.rest_post_request(url, request) |
| self.debug_msg("Response = %s" % response) |
| if response != '': |
| command.query_result = json.loads(response) |
| else: |
| command.query_result = None |
| |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, url) |
| return self.error_msg(self.rest_error_dict_to_message(errors)) |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # |
| def cp_help(self, words, text, completion_char): |
| """ |
| Completion for the help command must be done using the collection |
| of command descriptions; ie: help uses the command descriptions |
| to complete the help commands. |
| """ |
| if completion_char == ord('?'): |
| if len(words) > 1: |
| command.do_command_completion_help(words[1:], text) |
| else: |
| print self.help_splash([], text) |
| return |
| if len(words) == 1: |
| items = self.commands_for_current_mode_starting_with(text) |
| return utif.add_delim(items, ' ') |
| else: |
| return command.do_command_completion(words[1:], text) |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # command_short_help |
| # Associate short help strings with known commands |
| # These ought to be associated with the COMMAND_DESCRIPTIONs, |
| # but since not all command use that mechanism yet, provide |
| # these short descriptions here. |
| # |
| command_short_help = { |
| 'access-list' : 'Define access-list', |
| 'access-group' : 'Configure access-list to interface association', |
| 'boot' : "Configure boot configuration", |
| 'clock' : 'Configure timezone or set clock', |
| 'clear' : 'Reset counters', |
| 'connect' : "Connect to a controller's REST API via ip[:port]", |
| 'date' : 'Display current date and time', |
| 'debug' : 'Enter various debug modes', |
| 'echo' : 'Echo remaining arguments', |
| 'enable' : 'Move to enable mode', |
| 'exit' : 'Exit current mode', |
| 'feature' : 'Enable or disable features', |
| 'firewall' : 'Enable controller interfaces acls', |
| 'help' : 'Help on commands or topics', |
| 'history' : 'Display history of commands', |
| 'ip' : 'Configure various controller node ip values', |
| 'logout' : 'Exit from cli', |
| 'logging' : 'Configure logging/syslog', |
| 'ntp' : 'Configure ntp', |
| 'ping' : 'Ping request from controller to switch or ip address', |
| 'show' : 'Display configuration or settings', |
| 'test' : 'Test various behaviors', |
| 'trace' : 'Trace various streams', |
| 'traceroute' : 'Traceroute from controller to switch or ip address', |
| 'configure' : 'Enter configuration mode', |
| 'reload' : 'Reboot controller, reload configuration', |
| 'write' : 'Write configuraion', |
| 'no' : 'Delete or disable configuration parameters', |
| 'vns-definition' : 'Enter vns-definiton submode, describe membership', |
| 'vns' : 'Enter vns submode, manage access lists', |
| 'copy' : 'Copy configurations', |
| 'switchto' : 'Switch to another vns definition', |
| 'interface-rule' : 'VNS Membership rule', |
| 'end' : 'End all nested configuration modes', |
| 'interface' : 'Enter interface submode', |
| 'watch' : 'Iterate indicated command displaying results', |
| } |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # obj_type_short_help |
| # |
| obj_type_short_help = { |
| 'controller-node' : 'Configure specific controller nodes', |
| 'host' : 'Configure host details', |
| 'statd-config' : 'Statd Configuration', |
| 'statdropd-config' : 'Stat dropd configuration', |
| 'statdropd-progress-info' : 'Stat dropd progress configuration', |
| 'switch' : 'Switch Configuration', |
| |
| # -- debug only tables |
| 'vns-access-list' : 'VNS Access List object', |
| 'vns-access-list-entry' : 'VNS Access List Entry object', |
| 'vns-interface' : 'VNS Interface object', |
| 'vns-interface-access-list' : 'VNS Interface Access List object', |
| 'vns-interface-rule' : 'VNS Interface Rule object', |
| 'host-alias' : 'Host Alias object', |
| 'port-alias' : 'Port Alias object', |
| 'switch-alias' : 'Switch Alias object', |
| } |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # help_splash |
| # |
| def help_splash(self, words, text): |
| ret = "" |
| if not words: |
| if text == "": |
| ret += "For help on specific commands type help <topic>\n" |
| |
| count = 0 |
| longest_command = 0 |
| # this submode commands |
| s_ret = "" |
| mode = self.current_mode() |
| nested_mode_commands = [self.title_of(x) for x in |
| self.command_nested_dict.get(mode, [])] |
| possible_commands = [self.title_of(x) for x in |
| self.command_dict.get(mode, []) ] + \ |
| nested_mode_commands |
| available_commands = self.commands_feature_enabled(possible_commands) |
| submode_commands = sorted(utif.unique_list_from_list(available_commands)) |
| if len(submode_commands): |
| longest_command = len(max(submode_commands, key=len)) |
| for i in submode_commands: |
| if not i.startswith(text): |
| continue |
| count += 1 |
| short_help = command.get_command_short_help(i) |
| if not short_help: |
| short_help = self.command_short_help.get(i, None) |
| if short_help: |
| s_ret += " %s%s%s\n" % (i, |
| ' ' * (longest_command - len(i) + 1), |
| short_help) |
| else: |
| s_ret += " %s\n" % i |
| |
| # commands |
| c_ret = "" |
| upper_commands = [x for x in self.commands_for_current_mode_starting_with() |
| if not x in submode_commands] |
| commands = sorted(upper_commands) |
| if len(commands): |
| longest_command = max([len(x) for x in commands] + [longest_command]) |
| for i in commands: |
| if not i.startswith(text): |
| continue |
| count += 1 |
| short_help = command.get_command_short_help(i) |
| if not short_help: |
| short_help = self.command_short_help.get(i, None) |
| if short_help: |
| c_ret += " %s%s%s\n" % (i, |
| ' ' * (longest_command - len(i) + 1), |
| short_help) |
| else: |
| c_ret += " %s\n" % i |
| |
| # objects |
| o_ret = "" |
| if self.in_config_mode(): |
| obj_types = sorted(self.obj_types_for_config_mode_starting_with()) |
| if len(obj_types) > 0: |
| for i in obj_types: |
| longest_command = max([len(x) for x in commands] + |
| [longest_command]) |
| for i in obj_types: |
| if i in commands: |
| continue |
| if i.startswith(text): |
| count += 1 |
| short_help = self.obj_type_short_help.get(i, None) |
| if short_help: |
| o_ret += " %s%s%s\n" % (i, |
| ' ' * (longest_command - len(i) + 1), |
| short_help) |
| else: |
| o_ret += " %s\n" % i |
| |
| # fields |
| f_ret = "" |
| if self.in_config_submode(): |
| # try to get both the fields and the commands to line up |
| longest_field = longest_command |
| for i in self.fields_for_current_submode_starting_with(): |
| if i.startswith(text): |
| longest_field = max(longest_field, len(i)) |
| |
| f_count = 0 |
| # LOOK! could be getting the help text for each of these... |
| for i in sorted(self.fields_for_current_submode_starting_with()): |
| if not i.startswith(text): |
| continue |
| count += 1 |
| field_info = mi.obj_type_info_dict[ |
| self.get_current_mode_obj_type()]['fields'].\ |
| get(i, None) |
| if not field_info and self.debug: |
| print 'no field for %s:%s' % (self.get_current_mode_obj_type(), i) |
| if field_info and field_info.get('help_text', None): |
| f_ret += " %s%s%s\n" % (i, |
| ' ' * (longest_field - len(i) + 1), |
| field_info.get('help_text')) |
| else: |
| f_ret += " %s\n"% i |
| |
| if (text == "" or count > 1) and s_ret != "": |
| ret += "Commands:\n" |
| ret += s_ret |
| |
| if (text == "" or count > 1) and c_ret != "": |
| ret += "All Available commands:\n" |
| ret += c_ret |
| |
| if (text == "" or count > 1) and o_ret != "": |
| ret += "\nAvailable config submodes:\n" |
| ret += o_ret |
| |
| if (text == "" or count > 1) and f_ret != "": |
| ret += "\nAvailable fields for %s:\n" % self.get_current_mode_obj_type() |
| ret += f_ret |
| elif words[0] in ["show", "help", "copy", "watch" ]: |
| method = self.command_method_from_name(words[0]) |
| if method: |
| ret = method(None) |
| else: |
| #try: |
| #ret = command.get_command_syntax_help(words, 'Command syntax:') |
| #except: |
| #if self.debug or self.debug_backtrace: |
| #traceback.print_exc() |
| #ret = "No help available for command %s" % words[0] |
| |
| try: |
| ret = command.get_command_documentation(words) |
| except: |
| if self.debug or self.debug_backtrace: |
| traceback.print_exc() |
| ret = "No help available for command %s" % words[0] |
| else: |
| #try: |
| #ret = command.get_command_syntax_help(words, 'Command syntax:') |
| #except: |
| #if self.debug or self.debug_backtrace: |
| #traceback.print_exc() |
| #ret = "No help available for command %s" % words[0] |
| try: |
| ret = command.get_command_documentation(words) |
| except: |
| if self.debug or self.debug_backtrace: |
| traceback.print_exc() |
| ret = "No help available for command %s" % words[0] |
| return ret |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_help |
| # |
| def do_help(self, words): |
| return self.help_splash(words, "") |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_lint |
| # |
| def do_lint(self, words): |
| return command.lint_command(words) |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # cp_history |
| # |
| def cp_history(self, words, text, completion_char): |
| if len(words) == 1: |
| ret_val = " <num> - to display a specific number of commands (default:all)\n" |
| ret_val += " <cr>\n" |
| self.print_completion_help(ret_val) |
| else: |
| self.print_completion_help("<cr>") |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_history |
| # |
| def do_history(self, words = None): |
| ret_val = "" |
| how_many = num_commands = readline.get_current_history_length() |
| if words: |
| how_many = words[0] |
| for i in range(num_commands-int(how_many) + 1, num_commands): |
| yield "%s: %s\n" % (i, readline.get_history_item(i)) |
| return |
| |
| |
| debug_cmd_options = ["python", "bash", "cassandra-cli", "netconfig", "tcpdump", |
| "cli", "cli-backtrace", "cli-batch", "cli-interactive"] |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_debug |
| # |
| def implement_debug(self, words): |
| if len(words) < 1 or len([x for x in self.debug_cmd_options if x.startswith(words[0])]) < 1: |
| return "Syntax: debug < %s >" % " | ".join(self.debug_cmd_options) |
| def shell(args): |
| subprocess.call(["env", "SHELL=/bin/bash", "/bin/bash"] + list(args), cwd=os.environ.get("HOME")) |
| print |
| print "\n***** Warning: this is a debug command - use caution! *****" |
| if "python".startswith(words[0]): |
| print '***** Type "exit()" or Ctrl-D to return to the SDNOS CLI *****\n' |
| shell(["-l", "-c", "python"]) |
| elif "bash".startswith(words[0]): |
| print '***** Type "exit" or Ctrl-D to return to the SDNOS CLI *****\n' |
| shell(["-l", "-i"]) |
| elif "cassandra-cli".startswith(words[0]): |
| print '***** Type "exit" or Ctrl-D to return to the SDNOS CLI *****\n' |
| shell(["-l", "-c", "/opt/sdnplatform/db/bin/cassandra-cli --host localhost"]) |
| elif "netconfig".startswith(words[0]): |
| if not re.match("/dev/ttyS?[\d]+$", os.ttyname(0)): |
| print '***** You seem to be connected via SSH or another remote protocol;' |
| print '***** reconfiguring the network interface may disrupt the connection!' |
| print '\n(Press Control-C now to leave the network configuration unchanged)\n' |
| subprocess.call(["sudo", "env", "SHELL=/bin/bash", "/opt/sdnplatform/sys/bin/bscnetconfig", "eth0"], cwd=os.environ.get("HOME")) |
| elif "tcpdump".startswith(words[0]): |
| print '***** Type Ctrl-C to return to the SDNOS CLI *****\n' |
| try: |
| shell(["-l", "-c", "sudo /opt/openflow/sbin/tcpdump " + " ".join(words[1:])]) |
| except: |
| pass # really, we need to ignore this unconditionally! |
| time.sleep(0.2) |
| elif "cli".startswith(words[0]): |
| if self.debug: |
| self.debug = False |
| print "debug disabled" |
| else: |
| self.debug = True |
| print "debug enabled" |
| elif "cli-backtrace".startswith(words[0]): |
| # This feature is separated from the cli debug mode so that backtraces |
| # can be collected during cli qa |
| self.debug_backtrace = True |
| print '***** Enabled cli debug backtrace *****' |
| elif "cli-batch".startswith(words[0]): |
| self.batch = True |
| print '***** Enabled cli batch mode *****' |
| elif "cli-interactive".startswith(words[0]): |
| self.batch = False |
| print '***** Disabled cli batch mode *****' |
| else: |
| return self.error_msg("invoking debug") |
| # |
| # -------------------------------------------------------------------------------- |
| # do_echo |
| # |
| def do_echo(self, words): |
| print " ".join(words) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # cp_trace |
| # |
| trace_optional_params = { |
| 'screen': {'type': 'flag'}, |
| # 'port': {'type': 'string', |
| # 'syntax_help': "Enter <switch dpid> <physical port#>, where the packets traces are spanned."}, |
| 'file': {'type': 'string', |
| 'syntax_help': "Enter <filename> where the packets traces are captured."}, |
| } |
| |
| trace_cmds = ["detail", "oneline", "vns", "in", "out", "both", |
| "echo_reply", "echo_request", "features_rep", "flow_mod", "flow_removed", "get_config_rep", "hello", |
| "packet_in", "packet_out", "port_status", "set_config", "stats_reply", "stats_reques"] |
| |
| def cp_trace(self, words, text, completion_char): |
| vns_cache = objects_starting_with('vns-definition') |
| if len(words) == 1: |
| return [x for x in self.trace_cmds if x.startswith(text)] |
| elif len(words) == 2 and words[1].lower().startswith("vns"): |
| return vns_cache |
| elif len(words) == 2 and words[1].lower().startswith("output"): |
| return self.complete_optional_parameters(self.trace_optional_params, |
| words[len(words):], text) |
| elif (len(words) == 3 and |
| words[1].lower().startswith("vns") and |
| words[2].lower() in vns_cache): |
| return [x for x in ["in", "out", "both", "output"] if x.startswith(text)] |
| elif (len(words) == 4 and |
| words[1].lower().startswith("vns") and |
| words[2].lower() in objects_starting_with('vns-definition') and |
| words[3].lower() in ["in", "out", "both"]): |
| return [x for x in ["output"] if x.startswith(text)] |
| elif len(words) == 2 and words[1].lower()in ["in", "out", "both"]: |
| return [x for x in ["output"] if x.startswith(text)] |
| elif (len(words) > 2 and |
| (words[1].lower().startswith("vns") or |
| words[1].lower().startswith("output") or |
| words[2].lower().startswith("output"))) : |
| offset = len(words) |
| if words[len(words)-1].lower() == 'screen': |
| self.print_completion_help("<cr>") |
| return [] |
| elif words[len(words)-1].lower() in ['file']: |
| offset = len(words) - 1 |
| return self.complete_optional_parameters(self.trace_optional_params, words[offset:], text) |
| |
| self.print_completion_help("<cr>") |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_trace |
| # |
| def do_trace(self, *args): |
| dpid = None |
| if len(args) > 0: |
| args = args[0] # first element is the array of 'real' args |
| if len(args) > 1 and args[0].lower() in ['vns', 'in', 'out', 'both', 'output']: |
| try : |
| return self.do_sdnplatform_trace(args) |
| except KeyboardInterrupt, e: |
| return |
| |
| if len(args) > 1 and args[0].lower() == 'trace': |
| dpid = args[1] |
| args = args[2:] # And in this case, args are after the first 2 parameters |
| return self.do_tcpdump_trace(dpid, args) |
| |
| def do_sdnplatform_trace(self, args): |
| vns = None |
| direction = 'both' |
| output = 'screen' |
| dpid = None |
| port = None |
| file_name = None |
| sessionId = None |
| error = 'Error: Invalid command syntax.\n' + \ |
| 'trace [vns <vnsId>] [in|out|both] output [screen]' |
| # 'trace [vns <vnsId>] [in|out|both] output [screen|port <dpid> <physical port>|file <filename>]' |
| |
| if args[0].lower() == 'vns': |
| if len(args) > 1: |
| vns = args[1] |
| args = args[2:] |
| else: |
| return error |
| |
| while args: |
| if args[0].lower() == 'output': |
| args = args[1:] |
| continue |
| if args[0].lower() in ['in', 'out', 'both']: |
| direction = args[0].lower() |
| args = args[1:] |
| continue |
| if args[0].lower() == 'screen': |
| output = 'screen' |
| break |
| if args[0].lower() == 'port': |
| output = 'port' |
| if len(args) > 2: |
| dpid = args[1] |
| port = args[2:] |
| break |
| else: |
| return error |
| if args[0].lower() == 'file': |
| output = 'file' |
| if len(args) > 1: |
| file_name = args[1] |
| break |
| else: |
| return error |
| |
| filter_request = {FT_DIRECTION:direction, FT_OUTPUT:output, FT_PERIOD:FT_PERIOD_DEFAULT} |
| if vns : |
| filter_request[FT_VNS] = vns |
| if dpid and port : |
| filter_request[FT_PORT] = {FT_PORT_DPID:dpid, FT_PORT_PORT:port} |
| |
| |
| post_data = json.dumps(filter_request) |
| |
| while 1: |
| response_text = None |
| sessionId = None |
| url = 'http://%s/rest/v1/packettrace/' % cli.controller |
| request = urllib2.Request(url, post_data, {'Content-Type':'application/json'}) |
| try: |
| response = urllib2.urlopen(request) |
| response_text = response.read() |
| response_text = json.loads(response_text) |
| except Exception, e: |
| return "Error: failed to get a response for a trace request. %s" % e |
| |
| if response_text and SESSIONID in response_text: |
| sessionId = response_text[SESSIONID] |
| else : |
| return "Error: failed to get a trace session. %s" % response_text |
| |
| # Span to a physical port |
| # No data is shown on cli. ^C to stop the trace |
| if output.lower() == 'port': |
| while 1: |
| try : |
| time.sleep(1000) |
| except KeyboardInterrupt, e: |
| self.terminateTrace(sessionId) |
| return |
| |
| lpurl = None |
| |
| if sessionId: |
| lpurl = 'http://%s/poll/packets/%s' % (cli.controller, sessionId) |
| else: |
| return "Error: failed to start a trace session. %s" % response_text |
| |
| FILE = None |
| if file_name: |
| FILE = open(file_name, "w") |
| |
| flagTimeout = False |
| while 1: |
| try: |
| response_text = urllib2.urlopen(lpurl).read() |
| data = json.loads(response_text) |
| if output.lower() == "screen": |
| flagTimeout = self.dumpPktToScreen(data) |
| elif output.lower() == 'file' and FILE != None: |
| self.dumpPktToFile(FILE, data) |
| except KeyboardInterrupt, e: |
| self. terminateTrace(sessionId) |
| return |
| except Exception, e: |
| return "Error: failed to start a trace session. %s" % e |
| |
| if flagTimeout: |
| continueString = raw_input("Do you want to continue tracing? [y/n]: ") |
| if 'y' in continueString.lower(): |
| break |
| else: |
| return |
| |
| |
| @staticmethod |
| def dumpPktToScreen(data): |
| for entry in data: |
| if FT_TIMEOUT in entry: |
| return True |
| else: |
| print entry |
| return False |
| |
| @staticmethod |
| def dumpPktToFile(FILE, data): |
| FILE.writelines(data) |
| |
| @staticmethod |
| def terminateTrace(sid): |
| post_data = json.dumps({SESSIONID:sid, FT_PERIOD:-1}) |
| url = 'http://%s/rest/v1/packettrace/' % cli.controller |
| request = urllib2.Request(url, post_data, {'Content-Type':'application/json'}) |
| try: |
| response = urllib2.urlopen(request) |
| response_text = response.read() |
| except Exception, e: |
| # Sdnplatform may not be running, but we don't want that to be a fatal |
| # error, so we just ignore the exception in that case. |
| pass |
| |
| def do_tcpdump_trace(self, dpid, args): |
| ret = '' |
| bsc_port = '6633' # LOOK: listen addr/port are wrong in the controller-node, so hard code 6633 for now |
| trace_cmd = 'sudo /opt/sdnplatform/cli/bin/trace --filter \'%s\' %s' |
| trace_rule = '((tcp) and (port %s))' % (bsc_port,) |
| single_session = False |
| |
| trace_args = '--alias' |
| if 'detail' in args: |
| trace_args += ' --detail' |
| args.remove('detail') |
| if 'oneline' in args: |
| trace_args += ' --oneline' |
| args.remove('oneline') |
| if 'single_session' in args: |
| single_session = True |
| args.remove('single_session') |
| |
| if dpid: |
| query_dict = { 'dpid' : dpid } |
| row = rest_to_model.get_model_from_url('switches', query_dict) |
| if len(row) >= 1: |
| if not 'ip-address' in row[0] or row[0].get('ip-address') == '': |
| return self.error_msg("switch %s currently not connected " % dpid) |
| addr = row[0]['ip-address'] |
| port = row[0]['tcp-port'] |
| if single_session: |
| trace_rule = '((tcp) and (port %s) and (host %s) and (port %s))' % (bsc_port, addr, port) |
| else: |
| trace_rule = '((tcp) and (port %s) and (host %s))' % (bsc_port, addr) |
| if len(args) > 0: |
| trace_args += (' ' + ' '.join(args)) |
| try: |
| # print args, dpid, trace_cmd % (trace_rule, trace_args) |
| process = subprocess.Popen(trace_cmd % (trace_rule, trace_args), shell=True) |
| status = os.waitpid(process.pid, 0)[1] |
| if status != 0: |
| ret = 'Errno: %d' % (status, ) |
| except: |
| pass # really, we need to ignore this unconditionally! |
| |
| time.sleep(0.2) |
| return ret |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # get_auth_token_file_path |
| # |
| @staticmethod |
| def get_auth_token_file_path(): |
| auth_token_path = os.environ.get('BSC_AUTH_TOKEN_PATH') |
| if auth_token_path is None: |
| auth_token_path = '/opt/sdnplatform' |
| auth_token_path += '/auth_token' |
| return auth_token_path |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # play_config_lines |
| # |
| def play_config_lines(self, src_text): |
| """ |
| The complete collection of lines to replay are in src_text. |
| In the future, this would provide for a way to first identify |
| what needs to be removed, then apply what needs to be added. |
| |
| Some smart diffing from the exiting running-config would |
| be needed to support that. The stanza groups would need to |
| be collected, the diff'd |
| """ |
| print "Updating running-config ..." |
| |
| num_lines_played = 0 |
| # scan the lines looking for the version number. |
| # |
| version = None |
| for line in src_text.splitlines(): |
| m = re.match("(\s*)(\w+)\s*", line) # only match non-blank lines |
| if m: |
| if m.group(0).startswith('version '): |
| line_parts = line.split() |
| if line_parts > 1: |
| version = line_parts[1] |
| break |
| |
| # construct the command to run. |
| command = ['env'] |
| if version: |
| command.append('CLI_COMMAND_VERSION=%s' % version) |
| command.append('CLI_STARTING_MODE=config') |
| command.append('CLI_SUPPRESS_WARNING=True') |
| |
| if os.path.exists('/opt/sdnplatform/cli/bin/cli'): |
| # controller VM |
| command.append('/opt/sdnplatform/cli/bin/cli') |
| command.append('--init') |
| else: |
| # developer setup |
| base = os.path.dirname(__file__) |
| command.append(os.path.join(base, 'cli.py')) |
| command.append('--init') |
| |
| # create a subprocess for the configuration, then push |
| # each of the lines into that subprocess. |
| p = subprocess.Popen(command, |
| stdin=subprocess.PIPE, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT, |
| bufsize=1) |
| postprocesslines=[] |
| for line in src_text.splitlines(1): |
| m = re.match("(\s*)(\w+)\s*", line) # only match non-blank lines |
| if m: |
| if 'tenant' or 'router' in line: #special handling for routing rule commands VNS-226 |
| postprocesslines.append(line) |
| p.stdin.write(line) |
| num_lines_played += 1 |
| for line in postprocesslines: |
| p.stdin.write(line) |
| num_lines_played += 1 |
| p.stdin.close() |
| output = p.stdout.read() |
| if output and (self.debug or self.debug_backtrace or self.description): |
| print output |
| p.stdout.close() |
| return "Num lines applied: %s\n" % num_lines_played |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # path_concat |
| # Concatenate a suffix path to a prefix, making the guarantee that the |
| # suffix will always be "under" the prefix. the suffix is normalized, |
| # then any prefix up-dirs ("..") are removed. |
| # |
| def path_concat(self, prefix, path): |
| suffix = posixpath.normpath(path.lstrip('/').rstrip('/')) |
| (head, tail) = os.path.split(suffix) |
| tails = [tail] |
| while head: |
| (head, tail) = os.path.split(head) |
| tails.insert(0, tail) |
| drop = '' |
| while tails[0] == '..': |
| drop = os.path.join(drop, tails.pop(0)) |
| if drop != '': |
| self.note("Ignoring %s prefix" % drop) |
| for t in tails: |
| prefix = os.path.join(prefix, t) |
| return prefix |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # implement_copy |
| # startup-config and update-config are simply abbreviations for |
| # config://startup-config and config://update_config |
| # |
| def implement_copy(self, words): |
| if not words or len(words) > 3: |
| print "Syntax: copy <src> <dst>" |
| print "where <src> and <dst> can be" |
| print " running-config startup-config update-config" |
| print " config://<name-of-config-in-db> - A-Za-z0-9_-.@ " |
| print " <url> - starting with either tftp://, ftp://, http:// or file://" |
| return |
| if len(words) == 1: |
| src = words[0] |
| dst = "terminal" |
| elif len(words) == 2: |
| (src, dst) = (words[0], words[1]) |
| |
| # Complete as required. |
| # NOTE: No completion for upgrade-config as only our scripts should be using it |
| if "running-config".startswith(src): |
| src = "running-config" |
| elif "startup-config".startswith(src): |
| src = "startup-config" |
| if "running-config".startswith(dst): |
| dst = "running-config" |
| elif "startup-config".startswith(dst): |
| dst = "startup-config" |
| elif "terminal".startswith(dst): |
| dst = "terminal" |
| |
| if src == dst: |
| print "src and dst for copy command cannot be equal" |
| return |
| |
| # get from the source first |
| src_text = "" |
| |
| # map any abbreviations |
| abbreviations = {'startup-config': 'config://startup-config', |
| 'upgrade-config': 'config://upgrade-config' |
| } |
| src = abbreviations.get(src, src) |
| data = None |
| |
| if src == "running-config": # get running |
| src_text = run_config.implement_show_running_config([]) + "\n" |
| elif src.startswith("config://"): # get local file |
| m = re.search(self.local_name_pattern, src) |
| if m: |
| data = self.store.get_user_data_table(m.group(1), "latest") |
| if data and len(data) > 0: |
| src_text = self.store.get_user_data_file(data[0]['full_name']) |
| else: |
| print self.error_msg("src name does not match %s" % self.local_name_pattern) |
| return |
| elif src.startswith("ftp") or src.startswith("http"): # get URL |
| src_text = self.store.get_text_from_url(src) |
| elif src.startswith("file://"): |
| #src_file = self.saved_configs_dirname + posixpath.normpath(src[7:]).lstrip('/').rstrip('/') |
| src_file = self.path_concat(self.saved_configs_dirname, src[7:]) |
| if src_file == "": |
| return self.error_msg("src %s could not be " |
| "interpreted as a valid source for a file" % src) |
| try: |
| src_text = self.store.get_text_from_url("file://" + src_file) |
| except Exception, e: |
| return self.error_msg(src + ":" + str(e)) |
| else: |
| return self.error_msg("Unknown configuration") |
| |
| message = "" |
| if len(words) > 2: |
| message = " ".join(words[2:]).strip('"') |
| |
| # |
| # map any abbreviations |
| dst = abbreviations.get(dst, dst) |
| |
| # now copy to dest |
| if dst == "running-config": |
| return self.play_config_lines(src_text) |
| elif dst == "terminal": |
| return src_text # returning src_text here allow '|' to work |
| elif dst.startswith("config://"): |
| m = re.search(self.local_name_pattern, dst) |
| if m: |
| store_result = self.store.set_user_data_file(m.group(1), src_text) |
| if store_result: |
| result = json.loads(store_result) |
| else: |
| return self.error_msg("rest store result not json format") |
| if 'status' in result and result['status'] == 'success': |
| return None |
| elif 'message' not in result: |
| return self.error_msg("rest store result doesn't contain error message") |
| else: |
| return self.error_msg(result['message']) |
| else: |
| return self.error_msg("dst name does not match %s" % self.local_name_pattern) |
| elif dst.startswith("ftp") or dst.startswith("http"): |
| self.store.copy_text_to_url(dst, src_text, message) |
| elif dst.startswith("file://"): |
| dst_file = self.path_concat(self.saved_configs_dirname, dst[7:]) |
| dst_dir = '/'.join(dst_file.split('/')[:-1]) |
| if not os.path.exists(dst_dir): |
| try: |
| os.makedirs(dst_dir) |
| except Exception as e: |
| return self.error_msg("in creating destination directory: %s [%s]" |
| % (str(e), dst_dir)) |
| if not os.path.isdir(dst_dir): |
| return self.error_msg("destination directory is not a " |
| "valid directory: %s" % dst_dir) |
| with open(dst_file, 'w') as f: |
| try: |
| f.write(src_text) |
| except Exception as e: |
| return self.error_msg("in saving config file: %s [%s]" % |
| (str(e), dst_file)) |
| elif dst == 'trash': |
| if data: |
| result = self.store.delete_user_data_file(data[0]['full_name']) |
| if 'status' in result and result['status'] == 'success': |
| return None |
| elif 'message' not in result: |
| return self.error_msg("rest store result doesn't contain error message") |
| else: |
| return self.error_msg(result['message']) |
| else: |
| print self.error_msg("source file retrieved," |
| "but could not copy to destination %s" % dst) |
| return |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # cp_conf_object_type |
| # Completion function for a named object, returns names of |
| # available obj_types |
| # |
| def cp_conf_object_type(self, words, text): |
| if words[0] in mi.obj_types and len(words) == 1: |
| return utif.add_delim(objects_starting_with(words[0], text), ' ') |
| self.print_completion_help("<cr>") |
| return [] |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # cp_conf_field |
| # Handle if we've got a bunch of fields being concatenated, |
| # e.g., priority 42 src-port 80 |
| # |
| def cp_conf_field(self, obj_type, words, text): |
| if len(words) % 2 == 1: |
| latest_field_name = words[-1] |
| |
| field_info = mi.obj_type_info_dict[obj_type]['fields'].get(latest_field_name, None) |
| # if there was no shallow match of text, do deep match of xrefs if any |
| if field_info is None: |
| if obj_type in mi.alias_obj_type_xref: |
| if latest_field_name in mi.alias_obj_type_xref[obj_type]: |
| field_info = mi.obj_type_info_dict[latest_field_name]['fields']['id'] |
| |
| if field_info: |
| completion_text = "< %s >: " % field_info['verbose-name'] |
| completion_text += field_info.get('help_text', "") |
| self.print_completion_help(completion_text) |
| |
| |
| # return choices based on the types |
| if mi.is_field_boolean(obj_type, latest_field_name): |
| return utif.add_delib([x for x in ["True", "False"] if x.startswith(text)], |
| ' ') |
| |
| # Use the 'Values:' prefix in the help_text as a way |
| # of enumerating the different choices |
| if field_info.get('help_text', "").startswith('Values:'): |
| values = field_info.get('help_text').replace("Values:", "") |
| return utif.add_delim([x for x in values.replace(" ","").split(',') |
| if x.startswith(text)], ' ') |
| |
| # |
| # special cases: |
| if obj_type == 'vns-interface-rule' and latest_field_name == 'mac': |
| return utif.add_delim(objects_starting_with('host', text), ' ') |
| elif obj_type == 'vns-interface-rule' and latest_field_name == 'switch': |
| return utif.add_delim(objects_starting_with('switch', text), ' ') |
| else: |
| # we've got an even number of field-value pairs, so just give the list |
| # of fields for this obj_type |
| return utif.add_fields( |
| self.fields_for_current_submode_starting_with(text), |
| ' ') |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # vns methods. |
| # |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # vns_obj_type |
| # return True for an obj_type associated with vns, intended to identify |
| # tables which are or aren't available in debug mode |
| # |
| @staticmethod |
| def vns_obj_type(obj_type): |
| if obj_type in [ 'vns-interface', 'vns-access-list-entry', 'vns-access-list', |
| 'vns-interface-access-list', 'vns-interface-rule', |
| 'vns-access-group', 'vns-interfaces', 'host-vns-interface']: |
| return True |
| return False |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # vns_debug_obj_type |
| # return True if the obj_type is available in the non-debug mode |
| # |
| def vns_debug_obj_type(self, obj_type): |
| if self.debug: |
| return True |
| if self.in_config_submode("config-vns-") and obj_type == 'vns-access-list-entry': |
| return False |
| return not self.vns_obj_type(obj_type) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # vns_debug_show_commands |
| # return True if the show command vns option is availabe in the current debug mode |
| # |
| def vns_debug_show_commands(self, show_command): |
| if self.debug: |
| return True |
| if self.in_config_submode() and show_command == 'do_show_vns_interface_rule': |
| return True |
| if self.in_config_vns_if_mode() and show_command == 'do_show_vns_access_group': |
| return True |
| if show_command == 'do_show_vns': |
| return True |
| return not self.vns_obj_type(show_command.replace('_', '-').replace('do-show-', '', 1)) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # vns_find_name |
| # Return True when found, False otherwise |
| # |
| def vns_find_name(self, name): |
| try: |
| self.get_object_from_store("vns-definition", name) |
| return True |
| except Exception, e: |
| pass |
| return False |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # vns_find_and_set_name |
| # Return True when the name is known, as a side effect, set self.vns_name |
| # Invalid names are managed by the model, see VnsNameValidator |
| # |
| def vns_find_and_set_name(self, words): |
| if len(words) == 0: |
| print self.error_msg("vns <name> required") |
| return False |
| elif len(words) > 1: |
| print self.error_msg("Additional text after vns name ignored") |
| name = words[0] |
| if self.vns_find_name(name): |
| return True |
| return False |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # vns_find_and_set_existing_name |
| # Return True when the name is known, as a side effect, set self.vns_name |
| # And display an error message concerning the failure. |
| # |
| def vns_find_and_set_existing_name(self, words): |
| if self.vns_find_and_set_name(words): |
| return True |
| if len(words) > 0: |
| print self.error_msg("Not Found: vns '%s'" % words[0]) |
| return False |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # vns_find_or_add_name |
| # Return True when either the vns name currently exists, or of a create |
| # for the vns name succeeded. |
| # |
| def vns_find_or_add_name(self, words): |
| if self.vns_find_and_set_name(words): |
| return True |
| name = words[0] |
| errors = None |
| ident = {mi.pk("vns-definition"): name } |
| try: |
| self.rest_create_object("vns-definition", ident) |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, 'vns-definition') |
| print self.rest_error_dict_to_message(errors) |
| return False |
| |
| return True |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # vns_all_ids |
| # |
| def vns_all_ids(self): |
| return [x['id'] for x in self.get_table_from_store("vns-definition")] |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # vns_convert_table_id |
| # return a list of two entries, the first is the vns name, the second is the |
| # updated column value |
| # |
| def vns_convert_table_id(self, obj_type, row, id): |
| fields = row[id].split('|') |
| if len(fields) == 1: |
| # Just in case there's an error in the table |
| return [fields[0], "<error>"] |
| if len(fields) == 2: |
| return fields |
| elif len(fields) == 3: |
| if obj_type == 'host-vns-interface': |
| return [fields[1], fields[2]] |
| elif obj_type == 'vns-access-list-entry': |
| row['vns-access-list'] = fields[1] |
| return [fields[0], fields[2]] |
| else: |
| print self.error_msg("3: vns_convert_table_id case %s" % obj_type) |
| elif len(fields) == 4: |
| if obj_type == 'vns-interface-access-list': |
| # XXX ought to validate |
| if row['vns-access-list'].split('|')[1] != fields[2]: |
| print self.error_msg('vns-interface-access-list inconsistent %s and id %d' % \ |
| (row['vns-access-list'], row[id])) |
| row['vns-access-list'] = fields[2] |
| row['vns-interface'] = fields[1] |
| return [fields[0], fields[2]] |
| else: |
| print self.error_msg("4: vns_convert_table_id case %s" % obj_type) |
| else: |
| print self.error_msg("vns_convert_table_id: length %d not managed" % len(fields)) |
| # |
| # -------------------------------------------------------------------------------- |
| # vns_table_id_to_vns_column |
| # |
| # vns related tables in the model have unique id's created by concatenating the |
| # vns name with other fields. For those tables, separate the vns id's from the |
| # associated fields, and transform the vns association into its own column. |
| # |
| # obj_type is the name of the table. |
| # |
| # for the vns_interface, the vns name is extracted from several fields, |
| # validated, updated to exclude the vns name, then a new column is added |
| # |
| def vns_table_id_to_vns_column(self, obj_type, entries): |
| id = mi.pk(obj_type) |
| for row in entries: |
| if row[id]: |
| type_info = mi.obj_type_info_dict[obj_type]['fields'][id]['type'] |
| if type_info == 'compound-key': |
| mi.split_compound_into_dict(obj_type, id, row) |
| else: |
| convert_id = self.vns_convert_table_id(obj_type, row, id) |
| row['vns'] = convert_id[0] |
| row[id] = convert_id[1] |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # vns_foreign_key_to_base_name |
| # For foreign key's which vns wants to 'show', convert the |
| # foriegn key to the last entry for the field, and display |
| # that entry |
| # |
| def vns_foreign_key_to_base_name(self, obj_type, entries): |
| foreign_keys = mi.obj_type_foreign_keys(obj_type) |
| for row in entries: |
| # convert the rule id into the rule suffix |
| for foreign_key in foreign_keys: |
| if foreign_key in row and row[foreign_key].find('|'): |
| fields = row[foreign_key].split('|') |
| row[foreign_key] = fields[-1] |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # vns_join_host_fields |
| # For vns-interfaces new fields are joined from the host table. |
| # |
| def vns_join_host_fields(self, obj_type, entries): |
| if obj_type == 'vns-interface': |
| # |
| host_dap_dict = self.create_attachment_dict() |
| host_ip_dict = self.create_ip_dict() |
| # |
| vns_interface_key = mi.pk('vns-interface') |
| # |
| # for vns-interface, currently vlan is shown in the output, |
| # and this must be joined from the host's table. Again, it |
| # is better to collect the complee host table, then create |
| # a host dictionary |
| |
| hosts_rows_dict = create_obj_type_dict('host', 'mac') |
| |
| # |
| # re-index host_rows_dict based not on mac, but on attachment point, |
| # which is the sum of the switch+port. the objective here is to |
| # identify the number of items on a specific-switch port. |
| # |
| ap_dict = {} # indexed by dpid|port |
| for (mac, values) in host_dap_dict.items(): |
| for value in values: |
| key = "%s|%s" % (value['switch'], value['ingress-port']) |
| if not key in ap_dict: |
| ap_dict[key] = 0 |
| ap_dict[key] += 1 |
| # |
| # the ap_dict counts the number of mac's associated with an ap. |
| # sort the host_rows_dict based on the lowest ap in each group. |
| # |
| for (mac, value) in host_dap_dict.items(): |
| new_value = sorted(value, |
| key=lambda k:ap_dict["%s|%s" % ( |
| k['switch'], |
| k['ingress-port'])], |
| cmp=lambda x,y: int(x) - int(y)) |
| # add a tag to the dictionary of the first list item |
| # when the ap_dict's entry's count is 1. |
| first = new_value[0] |
| if ap_dict['%s|%s' % (first['switch'], first['ingress-port'])] == 1: |
| new_value[0]['prime'] = True |
| host_dap_dict[mac] = new_value |
| |
| for row in entries: |
| fields = row['id'].split('/') |
| if not 'rule' in row: |
| row['rule'] = fields[0] |
| if len(fields) != 2: |
| continue # silently ignore any id's not containing hosts |
| host = fields[1] |
| |
| row['mac'] = host |
| host_dict = {'mac' : host} |
| row['attachment-points'] = self.get_attachment_points( |
| host_dict, |
| host_dap_dict) |
| row['ips'] = self.get_ip_addresses(host_dict, host_ip_dict) |
| |
| host_row = hosts_rows_dict.get(host, None) |
| if host_row and len(host_row) == 1 and 'vlan' in host_row[0]: |
| row['vlan'] = host_row[0]['vlan'] |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # vns_join_switch_fields |
| # For vns-interfaces with entries arising from switch interface rules, |
| # join in the switch and port. |
| # |
| def vns_join_switch_fields(self, vns_name, entries): |
| # |
| # store all the rules associated with a specific vns, |
| # index them by the primary key. |
| key = mi.pk('vns-interface-rule') |
| if vns_name == None: |
| # vns_name == None, this means 'all' |
| vns_ifr_dict = create_obj_type_dict('vns-interface-rule', |
| key) |
| else: |
| search_key = self.unique_key_from_non_unique([vns_name]) |
| vns_ifr_dict = create_obj_type_dict('vns-interface-rule', |
| key, |
| key, |
| search_key) |
| for entry in entries: |
| if 'rule' in entry: |
| rule = entry['rule'] |
| if rule in vns_ifr_dict: |
| entry['switch'] = vns_ifr_dict[rule][0].get('switch', "") |
| entry['ports'] = vns_ifr_dict[rule][0].get('ports', "") |
| |
| # |
| # end of vns methods. |
| # -------------------------------------------------------------------------------- |
| # |
| |
| # |
| # |
| # -------------------------------------------------------------------------------- |
| # debug_obj_type |
| # Return True when the object is available, in self.debug mode, more objects |
| # are available. |
| # |
| def debug_obj_type(self, obj_type): |
| if self.debug: |
| return True |
| if mi.is_obj_type_source_debug_only(obj_type): |
| return False |
| if self.vns_debug_obj_type(obj_type): |
| if not obj_type in mi.alias_obj_types: |
| return True |
| return False |
| |
| # |
| # generic parsing routines |
| # |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # completion_reset |
| # |
| def completion_reset(self): |
| self.last_line = None |
| self.last_options = None |
| self.completion_cache = True |
| self.last_completion_char = readline.get_completion_type() |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # completer |
| # This is the main function that is called in order to complete user input |
| # |
| def completer(self, text, state): |
| question_mark = ord('?') |
| if readline.get_completion_type() == question_mark: |
| if len(readline.get_line_buffer()) == 0: |
| # |
| # manage printing of help text during command completion |
| help_text = self.help_splash(None, text) |
| if help_text != "": |
| self.print_completion_help(help_text) |
| return |
| |
| try: |
| origline = readline.get_line_buffer() |
| # See if we have a cached reply already |
| if (self.completion_cache and origline == self.last_line and |
| self.last_completion_char == readline.get_completion_type() and |
| self.last_options): |
| |
| if state < len(self.last_options): |
| return self.last_options[state] |
| else: |
| # apparently, for the linux VM choice don't print |
| if self.last_options and \ |
| len(self.last_options) > 1 and \ |
| self.last_completion_char == ord('\t'): |
| choices_text = self.choices_text_builder(self.last_options) |
| self.print_completion_help(choices_text) |
| |
| if self.completion_skip: |
| self.completion_cache = False |
| self.completion_skip = False |
| return None |
| |
| self.completion_reset() |
| |
| # parse what user has typed so far |
| |
| begin = readline.get_begidx() |
| end = readline.get_endidx() |
| |
| # Find which command we're in for a semicolon-separated list of single commands |
| # LOOK! This doesn't handle cases where an earlier command in the line changed |
| # the mode so the completion for later commands in the line should be different. |
| # For example, if you typed "enable; conf" it won't detect that it should be |
| # able to complete "conf" to "configure" because the enable command has not been |
| # executed yet, so you're not in enable mode yet. Handling that case would be |
| # non-trivial I think, at least with the current CLI framework. |
| command_begin = 0 |
| command_end = 0 |
| while True: |
| command_end = self.find_with_quoting(origline, ';', start_index=command_begin) |
| if command_end < 0: |
| command_end = len(origline) |
| break |
| if begin >= command_begin and end <= command_end: |
| break |
| command_begin = command_end + 1 |
| |
| # Skip past any leading whitespace in the command |
| while command_begin < begin and origline[command_begin].isspace(): |
| command_begin += 1 |
| |
| words = origline[command_begin:end].split() |
| |
| # remove last term if it is the one being matched |
| if begin != end: |
| words.pop() |
| |
| # LOOK! there are at least three places that try to parse the valid options: |
| # 1. When actually handling a command |
| # 2. When trying to show completions (here) |
| # 3. When displaying help |
| |
| # complete the first word in a command line |
| if not words or begin == command_begin: |
| options = self.commands_for_current_mode_starting_with(text, completion = True) |
| if self.in_config_mode(): |
| for item in self.obj_types_for_config_mode_starting_with(text): |
| self.append_when_missing(options, item) |
| if self.in_config_submode(): |
| for item in self.fields_for_current_submode_starting_with(text): |
| self.append_when_missing(options, item) |
| options = [x if x.endswith(' ') else x + ' ' for x in sorted(options)] |
| # Complete the 2nd word or later |
| else: |
| commands = self.commands_for_current_mode_starting_with(words[0]) |
| obj_types = self.obj_types_for_config_mode_starting_with(words[0]) if self.in_config_mode() else [] |
| fields = self.fields_for_current_submode_starting_with(words[0]) if self.in_config_submode() else [] |
| if len(commands) + len(obj_types) + len(fields) > 1: |
| if len(fields) > 1: |
| fields = [words[0]] if words[0] in fields else [] |
| if len(commands) > 1: |
| commands = [words[0]] if words[0] in commands else [] |
| if len(obj_types) > 1: |
| obj_types = [words[0]] if words[0] in obj_types else [] |
| |
| if len(fields) == 1: |
| options = self.cp_conf_field(self.get_current_mode_obj_type(), words, text) |
| elif len(obj_types) == 1: |
| options = self.cp_conf_object_type(obj_types + words[1:], text) |
| elif len(commands) == 1: |
| try: |
| # options[0] is expanded while words[0] is not |
| method = self.completion_method_from_name(commands[0]) |
| if method: |
| options = method(words, text, readline.get_completion_type()) |
| if not options: |
| # no match |
| return None |
| else: |
| if readline.get_completion_type() == question_mark: |
| options = command.do_command_completion_help(words, text) |
| else: |
| options = command.do_command_completion(words, text) |
| #else: |
| #if options: |
| #print syntax_help |
| #else: |
| #pass |
| #self.print_completion_help(syntax_help) |
| |
| except AttributeError: |
| if self.debug or self.debug_backtrace: |
| traceback.print_exc() |
| return None |
| |
| else: |
| options = None |
| |
| except Exception, e: |
| if self.debug or self.debug_backtrace: |
| traceback.print_exc() |
| # errors in connect are caught silently, complain here |
| # TODO - Maybe we should log this in a file we can review |
| # at a later date? |
| |
| try: |
| if options: |
| self.last_line = origline |
| self.last_options = options |
| self.last_completion_char = readline.get_completion_type() |
| return options[state] |
| except IndexError: |
| return None |
| |
| return None |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # handle_alias |
| # Manage alias creation for an obj_type, only manage one alias for each of the |
| # targets, although that requirement exists so that a single alias |
| # can be displayed for output from (show) commands |
| # |
| # Note that setting the alias to its current value currently causes the |
| # alias to be created (ie: there's no specific search to find the item) |
| # |
| def handle_alias(self, obj_type, alias_obj_type, alias_value): |
| |
| if alias_value in self.reserved_words: |
| return self.error_msg("alias value %s is a reserved word (%s)" % |
| (alias_value, ', '.join(self.reserved_words))) |
| # |
| # allow the use of 'alias' when only one alias entry |
| # exists for the obj_type. only-one ought to be typical. |
| # this may need to be removed after a compatability period. |
| if alias_obj_type == 'alias': |
| # there should be only one alias table for this obj_type |
| aliases = mi.alias_obj_type_xref[obj_type] |
| if len(aliases) != 1: |
| print self.error_msg("Internal more than one alias choice") |
| alias_obj_type = aliases[0] |
| |
| obj = self.get_current_mode_obj() |
| alias_key = mi.pk(alias_obj_type) |
| |
| # |
| # create row for table with foreign key |
| # find the name of the field which is the foreign key... |
| foreign_field = mi.alias_obj_type_field(alias_obj_type) |
| if not foreign_field: |
| print self.error_msg("internal handle_alias: alias_obj_type_field") |
| return None |
| |
| # |
| # delete the current alias if it exists |
| try: |
| exists = self.get_object_from_store(alias_obj_type, alias_value) |
| if len(exists): |
| self.rest_delete_object(alias_obj_type, alias_value) |
| except: |
| pass |
| |
| try: |
| create_dict = { alias_key : alias_value, foreign_field : obj } |
| self.rest_create_object(alias_obj_type, create_dict) |
| errors = None |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, alias_obj_type) |
| |
| if errors: |
| return self.error_msg("could not create %s for %s (%s): %s" % |
| (alias_obj_type, alias_value, obj_type, |
| self.rest_error_dict_to_message(errors))) |
| # |
| # remove other existing alias for the same foreign key |
| # find any other alias for this config object, then remove them |
| # |
| try: |
| rows = self.get_table_from_store(alias_obj_type, |
| foreign_field, |
| obj, |
| "exact") |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, alias_obj_type) |
| print self.rest_error_dict_to_message(errors) |
| rows = [] |
| # |
| # |
| for row in rows: |
| # |
| # skip the entry which was just inserted |
| if row[alias_key] == alias_value and row[foreign_field] == obj: |
| continue |
| try: |
| self.rest_delete_object(alias_obj_type, row[alias_key]) |
| self.warning("removed other alias '%s' for %s '%s'" % |
| (row[alias_key], foreign_field, row[foreign_field])) |
| except: |
| pass |
| |
| return None |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # update_foreign_key |
| # Given an obj_type, its primary key name, and the old and new |
| # id values, find any associated tables which have foreign keys |
| # associated with this table, and update the foreign key so that |
| # the table again points to a valid id. |
| # |
| # its unlikely, although not impossible, for the foreign key |
| # update to need to cascade. |
| # |
| def update_foreign_key(self, obj_type, obj_key, old_id, new_id): |
| if obj_type in mi.foreign_key_xref and \ |
| obj_key in mi.foreign_key_xref[obj_type]: |
| for (fk_obj_type, fk_name) in mi.foreign_key_xref[obj_type][obj_key]: |
| # |
| # find any affected rows |
| try: |
| rows = self.get_table_from_store(fk_obj_type, |
| fk_name, |
| old_id, |
| "exact") |
| except: |
| rows = [] |
| |
| key = mi.pk(fk_obj_type) |
| for row in rows: |
| self.warning("updating %s key %s field %s to %s" % \ |
| (fk_obj_type, row[key], fk_name, new_id)) |
| try: |
| self.rest_update_object(fk_obj_type, |
| key, |
| row[key], |
| { fk_name : new_id }) |
| errors = None |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, fk_obj_type) |
| |
| if errors: |
| print self.rest_error_dict_to_message(errors) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # handle_field |
| # |
| # handle_field calls itself recursively to parse fields and build |
| # a field dict. It then passes the field dict to one rest call. |
| # |
| def handle_field(self, words, accumulated_data=None): |
| if accumulated_data is None: |
| accumulated_data = {} |
| if len(words) < 2: |
| return self.error_msg("incorrect number of args (must be <field> <value>)") |
| obj_type = self.get_current_mode_obj_type() |
| obj = self.get_current_mode_obj() |
| last_word = 2 |
| field = words[0] |
| value = words[1] |
| |
| # complete the field if needed |
| field_choices = self.fields_for_current_submode_starting_with(words[0]) |
| # LOOK!: robv: Fix to work with a field names which is a prefix of another |
| if len(field_choices) > 1: |
| for field_choice in field_choices: |
| if field_choice == words[0]: |
| field_choices = [field_choice] |
| break |
| if len(field_choices) > 1: |
| return "Multiple matches for field name %s." % field |
| if len(field_choices) == 0: |
| return self.error_msg("%s has no field named %s." % (obj_type, field)) |
| field = field_choices[0] |
| |
| # |
| # handle special case alias's which aren't understood via xref |
| # (this is because vns-interface-rule doesn't foreignKey neigther |
| # switch or mac references to the switch/host tables) |
| # |
| if obj_type == 'vns-interface-rule' and field == 'switch': |
| if not self.DPID_RE.match(value): |
| alias_value = alias_lookup('switch-alias', value) |
| if alias_value: |
| value = alias_value |
| else: |
| return "Syntax: Unknown switch alias '%s'; Specify switch as DPID or alias" % value |
| if obj_type == 'vns-interface-rule' and field == 'mac': |
| if not self.MAC_RE.match(value): |
| alias_value = alias_lookup('host-alias', value) |
| if alias_value: |
| value = alias_value |
| else: |
| return "Syntax: Unknown host alias '%s'; Specify host as MAC or alias" % value |
| |
| # |
| # Replace common type values with expected types, for example true or false |
| # for True/False. |
| if mi.is_field_boolean(self.get_current_mode_obj_type(), field): |
| if value.lower() == 'false': |
| value = 'False' |
| elif value.lower() == 'true': |
| value = 'True' |
| # |
| elif mi.is_hex_allowed(obj_type, field) and self.HEX_RE.match(value): |
| value = str(int(value, 16)) |
| # |
| # Look up any validation or value changing callouts... |
| validate = mi.field_validation(self.get_current_mode_obj_type(), field) |
| if validate: |
| validate_error = validate(obj_type, field, value) |
| if validate_error: |
| return validate_error |
| |
| accumulated_data[field] = value |
| |
| ret_val = None |
| |
| if len(words) > last_word: |
| # more to munch |
| ret_val = self.handle_field(words[last_word:], accumulated_data) |
| else: |
| # munched everything, now update |
| obj_key = mi.pk(obj_type) |
| |
| prepare_update = mi.obj_type_prepare_row_update(obj_type) |
| if prepare_update: |
| accumulated_data = prepare_update(obj_type, obj_key, obj, |
| accumulated_data) |
| # in case the obj_type's key is updated |
| obj = accumulated_data[obj_key] |
| |
| # |
| # Note: if all the fields for a row are all manually reverted |
| # to a default value, perhaps the row ought to be deleted. |
| errors = None |
| try: |
| self.rest_update_object(obj_type, obj_key, obj, accumulated_data) |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, obj_type) |
| |
| if errors: |
| ret_val = self.rest_error_dict_to_message(errors) |
| else: |
| # |
| # if the primary key was part of the update. find relatred |
| # tables and update the foreign key. XXX perhaps this is |
| # the rest api's problem? |
| # |
| if obj_key in accumulated_data and \ |
| accumulated_data[obj_key] != obj: |
| |
| self.update_foreign_key(obj_type, obj_key, obj, |
| accumulated_data[obj_key]) |
| # |
| # delete the old key. |
| self.rest_delete_object(obj_type, obj) |
| |
| # |
| # move the the current id |
| self.set_current_mode_obj(accumulated_data[obj_key]) |
| |
| return ret_val |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # convert_hex_to_decimal |
| # Review the contents of all the values, looking for hex |
| # valued fields. When a hex value is identified, change |
| # it to decimal when 'is_hex_allowed' is true. |
| # |
| def convert_hex_to_decimal(self, obj_type, field_dict): |
| for key in field_dict.keys(): |
| if mi.is_hex_allowed(obj_type, key) and \ |
| self.HEX_RE.match(field_dict[key]): |
| field_dict[key] = int(field_dict[key], 16) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # handle_obj_type |
| # In config mode, this handles if first word is an object-type |
| # (ex: "flow-entry catch-web") |
| # |
| def handle_obj_type(self, words): |
| if len(words) < 2: |
| return "Syntax: %s <key>: where key is an id of %s" % (words[0], words[0]) |
| |
| (obj_type, obj_name) = (words[0], words[1]) |
| |
| obj_name = convert_alias_to_object_key(obj_type, obj_name) |
| # First check if this is something we can actually configure |
| if obj_type not in mi.obj_types: |
| return self.error_msg("unknown command or object.") |
| |
| # Next, find the object |
| # Deal with any changes to the lookup name based on the 'concatenation' |
| # of the config mode name to the named identifer. |
| # |
| found_obj = self.get_obj_of_type(obj_type, obj_name) |
| |
| if found_obj == None: |
| # need to create object, obj_data is the rest api dictionary used |
| # to create the new row. |
| obj_data = { mi.pk(obj_type): obj_name } |
| |
| if self.in_config_submode(): |
| # add in all the key value from modes in the stack |
| # LOOK! this is brittle as it depends on the name of |
| # the mode matching the name of the attribute. |
| # |
| obj_data = self.mode_stack_to_rest_dict(obj_data) |
| |
| # |
| self.convert_hex_to_decimal(obj_type, obj_data) |
| |
| # make sure all the foreign keys are populated |
| # |
| for key in mi.obj_type_foreign_keys(obj_type): |
| if not key in obj_data: |
| print self.error_msg("foreign key '%s' missing from mode stack %s" % |
| (key, obj_data)) |
| |
| prepare_update = mi.obj_type_prepare_row_update(obj_type) |
| if prepare_update: |
| obj_data = prepare_update(obj_type, |
| mi.pk(obj_type), obj_name, |
| obj_data) |
| |
| # create the object |
| errors = None |
| try: |
| self.rest_create_object(obj_type, obj_data) |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, obj_type) |
| |
| if errors: |
| return self.rest_error_dict_to_message(errors) |
| |
| # get it back, populating found_obj |
| found_obj = self.get_obj_of_type(obj_type, obj_name) |
| |
| # push on the submode for the object |
| # LOOK! assumes all object commands drop you into a submode... |
| if found_obj: |
| sub_mode = "config-%s" % obj_type |
| self.push_mode(sub_mode, obj_type, obj_name) |
| |
| # hand off the rest of the line to handle_field |
| if len(words) > 2: |
| return self.handle_field(words[2:]) |
| else: |
| return self.error_msg("No %s with %s = %s" % |
| (obj_type, mi.pk(obj_type), obj_name)) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # print_completion_help |
| # |
| def print_completion_help(self, completion_help_text): |
| origline = readline.get_line_buffer() |
| end = readline.get_endidx() |
| cur_command = origline[0:end] |
| |
| help_text = "\n%s\n%s%s" % ( completion_help_text, |
| self.prompt, |
| cur_command) |
| self.completion_skip = True |
| sys.stdout.write(help_text) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # get_ip_from_switch_dpid_or_alias |
| # Returns the IP associated with the switch if it exists |
| # or the original string if it's not a dpid or alias. |
| # |
| def get_ip_from_switch_dpid_or_alias(self, alias): |
| dpid = convert_alias_to_object_key("switches", alias) |
| if self.DPID_RE.match(dpid): |
| query_dict = { 'dpid' : dpid } |
| row = rest_to_model.get_model_from_url('switches', query_dict) |
| if len(row) >= 1: |
| if not 'ip-address' in row[0] or row[0].get('ip-address') == '': |
| return self.error_msg("switch %s currently not connected " |
| % alias) |
| return row[0].get('ip-address') |
| return alias |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # implement_write |
| # |
| def implement_write(self, words): |
| if len(words) == 1: |
| if "memory".startswith(words[0]): |
| return self.implement_copy(["running-config", "startup-config"]) |
| elif "erase".startswith(words[0]): |
| print "This will clear the startup-config and set it to be empty." |
| resp = raw_input("Are you sure that want to proceed? [n]") |
| if resp and "yes".startswith(resp.lower()): |
| print "Erasing startup config ..." |
| result = self.store.delete_user_data_file("startup-config/time/len/version") |
| if 'status' in result and result['status'] == 'success': |
| return None |
| elif 'message' not in result: |
| return self.error_msg("rest store result doesn't contain error message") |
| else: |
| return self.error_msg(result['message']) |
| else: |
| print "Command aborted by user: write erase" |
| return |
| elif "terminal".startswith(words[0]): |
| return self.implement_copy(["running-config", "terminal"]) |
| return "Syntax: write < terminal | memory | erase>" |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # |
| |
| @staticmethod |
| def set_clock_action(data): |
| time_values = data['time'].split(':') |
| if len(time_values) != 3: |
| raise error.CommandError('Invalid time; must be HH:MM:SS') |
| hour = int(time_values[0]) |
| minute = int(time_values[1]) |
| second = int(time_values[2]) |
| |
| MONTH_NAMES = ('January', 'February', 'March', 'April', 'May', 'June', |
| 'July', 'August', 'September', 'October', 'November', 'December') |
| |
| day_of_month = int(data['day-of-month']) |
| month_name = data['month'] |
| if month_name not in MONTH_NAMES: |
| raise error.CommandError('Invalid month name (e.g. January, May, July)') |
| month = MONTH_NAMES.index(month_name) + 1 |
| year = int(data['year']) |
| |
| date_time_info = { |
| 'year': year, |
| 'month': month, |
| 'day': day_of_month, |
| 'hour': hour, |
| 'minute': minute, |
| 'second': second |
| } |
| url = 'http://%s/rest/v1/system/clock/local' % cli.controller |
| result = cli.store.rest_post_request(url, date_time_info) |
| date_time_info = json.loads(result) |
| clock_string = SDNSh.get_clock_string(date_time_info, False) |
| return clock_string |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # get_misconfigured_default_gateway |
| # |
| @staticmethod |
| def get_misconfigured_default_gateway(): |
| """ |
| Determine if the controller is configured with a default gateway |
| setting that doesn't match (i.e. is not on the same subnet) |
| any of the interfaces configured with static IP addresses. |
| This is used by the begin_default_gateway_check_action and |
| end_default_gateway_check_action actions that check if a command |
| results in a misconfigured default gateway setting and, if so, |
| emit an error message. |
| If the default gateway is misconfigured, the return value is the |
| default gateway value. Otherwise, the return value is None. |
| """ |
| # First figure out which controller-node we're working with. |
| # This is a little kludgy right now. We check the mode stack |
| # looking for a controller node element. |
| controller_id = cli.get_nested_mode_obj('controller-node') |
| if not controller_id: |
| raise error.CommandDescriptionError('check_default_gateway_action must be called from a (possibly nested) controller node config mode') |
| |
| controller = cli.get_object_from_store('controller-node', controller_id) |
| if not controller: |
| # This shouldn't really happen unless someone/thing has mucked with the |
| # controller object out from under this instance of the CLI |
| # but just to be safe... |
| raise error.CommandInvocationError('Current controller settings have been deleted.') |
| |
| # Check if the controller node is configured with a default gateway. |
| # If not, then there's no possible misconfiguration and we're done. |
| default_gateway = controller.get('default-gateway') |
| if default_gateway == '': |
| return |
| |
| # There is a default gateway configured, so we need to check if there's |
| # an interface that matches (i.e. is on the same subnet as) the default |
| # gateway |
| interfaces = cli.rest_query_objects('controller-interface', {'controller': controller_id}) |
| for interface in interfaces: |
| mode = interface.get('mode') |
| if mode == 'static': |
| ip = interface.get('ip') |
| netmask = interface.get('netmask') |
| if (ip != '') and (netmask != '') and same_subnet(default_gateway, ip, netmask): |
| return None |
| |
| return default_gateway |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # check_default_gateway_action |
| # |
| @staticmethod |
| def begin_default_gateway_check_action(): |
| """ |
| This is an action proc for the data-driven command module that is used |
| in conjunction with the end_default_gateway_check_action to check if a |
| CLI command results in an improperly configured default gateway setting. |
| Currently the backend code that maps the network/interface settings |
| for the controller to the /etc/network/interfaces file will only |
| apply the default gateway setting if it's on the same subnet as an |
| interface that's configured with a static IP address/netmask. |
| So if there's no interface that matches the default gateway (either |
| because it's configured to use DHCP or the subnet doesn't match) |
| then the default gateway setting will be ignored and we want to warn |
| the user about the misconfiguration. The check should be performed |
| on any configuration change that could affect this check. This includes: |
| |
| 1) setting the default gateway |
| 2) setting the mode of an interface to DHCP |
| 3) setting/resetting the ip/netmask of an interface |
| 4) deleting an interface |
| |
| We only want to emit the warning if the default gateway was previously |
| not misconfigured and the current command resulted in a misconfiguration, |
| so we want to check for a misconfiguration before applying the command |
| and then check again after the settings have been updated. The |
| begin_default_gateway_check_action is the action that is performed before |
| the settings are updated and the corresponding end_default_gateway_check_action |
| action is the one that is performed after the settings are updated. |
| |
| LOOK!: Ideally we should be doing this check in the sdncon code instead |
| of here in the CLI code. That way the warning would also be returned |
| for clients that are changing these settings directly via the REST |
| API instead of via the CLI. But there isn't a really good mechanism |
| currently for the REST API to return a warning but still apply the |
| config change. |
| """ |
| # Stash away if the gateway setting was already misconfigured at the |
| # beginning of the current command. |
| # LOOK! This is a bit kludgy to stash this away in the CLI object. |
| # When the command module action API is changed to allow access to the |
| # context, this code should be updated to stash the value in the context |
| # instead of the CLI. |
| cli.begin_misconfigured_default_gateway = SDNSh.get_misconfigured_default_gateway() |
| |
| @staticmethod |
| def end_default_gateway_check_action(): |
| """ |
| This is an action proc for the data-driven command module that is used |
| in conjunction with the begin_default_gateway_check_action to check if a |
| CLI command results in an improperly configured default gateway setting. |
| Check the doc comments for begin_default_gateway_check_action for more |
| details about how the two actions are intended to be used. |
| """ |
| end_misconfigured_default_gateway = SDNSh.get_misconfigured_default_gateway() |
| if (end_misconfigured_default_gateway and |
| (end_misconfigured_default_gateway != cli.begin_misconfigured_default_gateway)): |
| return ('The controller is configured with a default gateway setting (%s), that\n' |
| 'is not on the same subnet as any of the interfaces configured to use a\n' |
| 'static IP address. The default gateway setting will be ignored.' % |
| end_misconfigured_default_gateway) |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # load_time_zone_list |
| # Use the rest api to collect all the known time zones |
| # |
| @staticmethod |
| def load_time_zone_list(): |
| if not cli.time_zone_list: |
| url = "http://%s/rest/v1/system/timezones/all" % cli.controller |
| cli.time_zone_list = cli.rest_simple_request_to_dict(url) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # time_zone_completion |
| # |
| @staticmethod |
| def time_zone_completion(words, text, completions): |
| SDNSh.load_time_zone_list() |
| completion_dict = {} |
| tz_completions = [] |
| for tz in cli.time_zone_list: |
| if tz.lower().startswith(text.lower()): |
| tz_completions.append(tz) |
| for index in range(len(text), len(tz)): |
| if tz[index] == '/': |
| tz = tz[:index+1] |
| break |
| completion_dict[tz] = 'Timezone Choice' |
| |
| if len(completion_dict) >= 1: |
| completions.update(completion_dict) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # time_zone_validation |
| # |
| @staticmethod |
| def time_zone_validation(typedef, value): |
| SDNSh.load_time_zone_list() |
| if value not in cli.time_zone_list: |
| raise error.ArgumentValidationError('Invalid time zone') |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # cp_clock |
| # |
| @staticmethod |
| def cp_clock(words, text, completion_char): |
| return command.do_command_completion(words, text) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_clock |
| # |
| @staticmethod |
| def do_clock(words): |
| return command.do_command(['clock'] + words) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_show_clock |
| # |
| def cp_show_clock(self, words, text, completion_char): |
| if len(words) == 1 and 'detail'.startswith(text.lower()): |
| return ['detail'] |
| |
| |
| @staticmethod |
| def get_clock_string(time_info, detail = None): |
| # The tz item contains the abbreviated time zone string (e.g. PST, EST) |
| # This is different from the full time zone string (e.g. America/Los_Angeles) |
| # which we get from the controller-node object below when we're showing |
| # detail info. |
| tz = time_info['tz'] |
| del time_info['tz'] |
| dt = datetime.datetime(**time_info) |
| if detail: |
| time_info['tz'] = tz |
| |
| # Arista-style format |
| #show_result = dt.strftime('%c') |
| |
| # Cisco-style format |
| clock_string = dt.strftime('%H:%M:%S '+ tz + ' %a %b %d %Y') |
| |
| return clock_string |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_show_clock |
| # |
| @staticmethod |
| def do_show_clock(words): |
| syntax_help = "Syntax: show clock [detail]" |
| |
| if len(words) == 0: |
| detail = False |
| elif len(words) == 1: |
| if not 'detail'.startswith(words[0].lower()): |
| return syntax_help |
| detail = True |
| else: |
| return syntax_help |
| |
| url = "http://%s/rest/v1/system/clock/local/" % cli.controller |
| time_info = cli.rest_simple_request_to_dict(url) |
| clock_string = SDNSh.get_clock_string(time_info, detail) |
| return clock_string |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # cp_show_vns |
| # |
| def cp_show_vns(self, words, text, completion_char): |
| if len(words) == 1: |
| return objects_starting_with('vns-definition', text) |
| elif len(words) == 2: |
| return [x for x in |
| [ 'interface', 'mac-address-table', 'interface-rules', \ |
| 'access-lists', 'running-config', 'switch', 'flow' ] |
| if x.startswith(text)] |
| elif len(words) == 3 and 'switch'.startswith(words[2]): |
| return objects_starting_with("switch", text) |
| elif (len(words) >= 3) and (words[2] == 'flow'): |
| return self.cp_show_vns_flow(words, text) |
| else: |
| self.print_completion_help("<cr>") |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # show_vns_definition_running_config |
| # Display the vns-definition -- the interface rules for the vns. |
| # |
| def show_vns_definition_running_config(self, config, vns_name,tenant=None,indent=0): |
| if tenant==None and vns_name!='all': |
| vns_id='default|'+vns_name |
| else: |
| vns_id=tenant +'|'+vns_name |
| try: |
| vns = self.get_object_from_store('vns-definition', vns_id) |
| except: |
| return self.error_msg('no vns named %s' % vns_name) |
| |
| config.append(' ' *2*indent + "vns-definition %s\n" % vns_name) |
| run_config.running_config_vns_details(config, vns, indent+1) |
| |
| vns_rules = None |
| try: |
| vns_rules = self.get_table_from_store('vns-interface-rule', |
| 'vns', vns_id, "exact") |
| except Exception: |
| pass |
| |
| if vns_rules: |
| run_config.running_config_vns_if_rule(config, |
| vns_rules,indent+1) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # show_vns_running_config |
| # In the larger 'running-config' for vns, the complete vns tables are read |
| # since the complete 'running-config' will display all the data from the |
| # fields. Here, the tables are searched only for specific vns entries. |
| # |
| # The procedure's name is choosen carefully, since its not intended to |
| # part of any of show command, completion, or command processing |
| # |
| def show_vns_running_config(self, vns_name, tenant=None, indent=0): |
| config=[] |
| preconfig=[' '*2*indent +"vns %s\n" % vns_name] |
| if tenant==None and vns_name!='all': |
| vns_name='default|'+vns_name |
| if tenant!=None: |
| vns_name=tenant+'|'+vns_name |
| try: |
| self.get_object_from_store('vns-definition', vns_name) |
| except: |
| return self.error_msg('no vns named %s' % vns_name) |
| vns_acls = [] |
| try: |
| vns_acls = self.get_table_from_store('vns-access-list', |
| 'vns', vns_name, 'exact') |
| except Exception, e: |
| pass |
| |
| for acl in vns_acls: |
| # note use of key initialized above |
| |
| vns_acl_entries = None |
| try: |
| vns_acl_entries = self.get_table_from_store('vns-access-list-entry', |
| 'vns-access-list', |
| acl['id'], |
| 'exact') |
| except Exception: |
| pass |
| run_config.running_config_vns_acl(config, vns_name, acl, vns_acl_entries,indent+1) |
| |
| vns_interface_acl = None |
| try: |
| vns_interface_acl = self.get_table_from_store('vns-interface-access-list') |
| except Exception: |
| pass |
| |
| for vns_if in run_config.running_config_active_vns_interfaces(vns_name, vns_interface_acl): |
| config.append(' ' *2*(indent+1) + "interface %s\n" % vns_if) |
| run_config.running_config_vns_if_and_access_group(config, |
| vns_name, |
| vns_if, |
| vns_interface_acl,indent+2) |
| if len(config) > 0: |
| config = preconfig + config |
| return ''.join(config) |
| |
| #generate tenant xxx running config |
| def show_tenant_running_config(self, config, tenant_name): |
| try: |
| tenants = self.get_object_from_store('tenant', tenant_name) |
| except: |
| return self.error_msg('no tenant named %s' % tenant_name) |
| |
| config.append("!\ntenant %s\n" % tenant_name) |
| run_config.running_config_tenant_details(config,tenants) |
| |
| try: |
| vnses = self.get_table_from_store('vns-definition','tenant',tenant_name,"exact") |
| except Exception: |
| vnses = {} |
| pass |
| try: |
| virtual_routers = self.get_table_from_store('virtualrouter','tenant', tenant_name, "exact") |
| except Exception: |
| virtual_routers = {} |
| pass |
| |
| for vns in vnses: |
| vns_name=vns['vnsname'] |
| self.show_vns_definition_running_config(config, vns_name, tenant=tenant_name,indent=1) |
| config += self.show_vns_running_config(vns_name, tenant=tenant_name,indent=1) |
| |
| for virtual_router in virtual_routers: |
| virtual_router_name=virtual_router['vrname'] |
| virtual_router_id=virtual_router['id'] |
| config.append(" router %s\n" % virtual_router_name) |
| run_config.running_config_tenant_router_details(config,virtual_router,indent=1) |
| try: |
| vr_interfaces = self.get_table_from_store('virtualrouter-interface','virtual-router', virtual_router_id, "exact") |
| except Exception: |
| vr_interfaces = {} |
| pass |
| try: |
| vr_routes = self.get_table_from_store('virtualrouter-routingrule','virtual-router', virtual_router_id, "exact") |
| except Exception: |
| vr_routes = {} |
| pass |
| try: |
| vr_gwpools = self.get_table_from_store('virtualrouter-gwpool','virtual-router', virtual_router_id, "exact") |
| except Exception: |
| vr_gwpools = {} |
| pass |
| |
| for vr_interface in vr_interfaces: |
| run_config.running_config_router_interface_details(config,vr_interface,indent=2) |
| for vr_route in vr_routes: |
| run_config.running_config_router_rule_details(config,vr_route,indent=2) |
| for vr_gwpool in vr_gwpools: |
| run_config.running_config_router_gwpool_details(config,vr_gwpool,indent=2) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # switch_port_match |
| # Return True when the port matches a port specirfication "pattern" |
| # |
| # The port specification of an interface rule is a command separated list, |
| # which can contain a tail of "-<d>". That tail describes a range. |
| # With a port range, determine whether or not the 'port' parameter |
| # is a match. |
| # |
| def switch_port_match(self, port, pattern): |
| # XXX validate port is something which ends in a number? |
| for field in pattern.split(','): |
| m = re.match(r'^([A-Za-z0-9-]*?)(\d+)-(\d+)$', pattern) |
| if not m: |
| if field == port: |
| return True |
| continue |
| prefix = m.group(1) |
| startport = int(m.group(2)) |
| endport = int(m.group(3)) |
| if not port.startswith(prefix): |
| continue |
| tail = port[len(prefix):] |
| if not self.DIGITS_RE.match(tail): |
| print self.error_msg("port name tail %s must be all digits" % tail) |
| continue |
| m = re.search(r'(\d+)$', port) |
| if not m: |
| continue |
| port = m.group(1) |
| if int(port) >= int(startport) and int(port) <= int(endport): |
| return True |
| else: |
| print self.error_msg("port %s out of port-spec range %s-%s" % |
| (port, startport, endport)) |
| return False |
| |
| # |
| # --------------------------------------------------------------------------- |
| # cp_show_event_history |
| # Command completion for the following command to see |
| # last <n> events of <name> events |
| # Used in "show event-history <name> [ last <n> ]" |
| # |
| def cp_show_event_history(self, words, text, completion_char): |
| options = ['attachment-point', |
| 'packet-in', |
| 'topology-link', |
| 'topology-switch', |
| 'topology-cluster', |
| # Add more event history options above this line |
| # Add the same item in do_show_event_history() below also |
| ] |
| completion_help = '' |
| for op in options: |
| ev_help = '%s show %s events\n' % (op, op) |
| completion_help = completion_help + ev_help |
| |
| # syntax: show event-history <name> [ last <n> ] |
| if (len(words) == 1) and text=='': |
| self.print_completion_help(completion_help) |
| return options |
| elif (len(words) == 1): |
| return [op for op in options if op.startswith(text)] |
| elif (len(words) == 2): |
| if text == '': |
| self.print_completion_help("last Show lastest <n> events\n" + |
| "<cr> Enter") |
| return ['last', '<cr>'] |
| elif "last".startswith(text): |
| return ["last"] |
| elif (len(words) == 3): |
| self.print_completion_help("<number> Enter number of latest events to display (1-10000)") |
| return range(0, 10000) |
| else: |
| self.print_completion_help('<cr>') |
| |
| # |
| # -------------------------------------------------------------------------- |
| # do_show_event_history |
| # |
| # Show the event history of the specified events |
| # Syntax: show event-history <event-history-name> [last <count>] |
| # <event-history-name> is words[0], <count> is words[2] |
| # |
| def do_show_event_history(self, words): |
| self.debug_msg("do-show-event-history words: %s" % words) |
| |
| # Check syntax of the command |
| syntax = self.syntax_msg('show event-history <name> [ last <n> ]') |
| options = ['attachment-point', |
| 'packet-in', |
| 'topology-link', |
| 'topology-switch', |
| 'topology-cluster', |
| # Add more event history options above this line |
| # Add the same item in do_cp_event_history() above also |
| ] |
| if (words[0] not in options): |
| yield("Event history name %s not found" % words[0]) |
| return |
| if (len(words) >= 3) and (words[1] != 'last'): |
| yield(syntax) |
| return |
| if (len(words) == 2) or (len(words) > 4): |
| yield(syntax) |
| return |
| if (len(words) >= 3) and (not words[2].isdigit()): |
| yield(syntax) |
| return |
| if (len(words) >= 3) and (words[2].isdigit()): |
| count = int(words[2]) |
| if (count < 1) or (count > 10000): |
| yield("Number of events must be between 1 and 10000") |
| return |
| |
| last_n_count = 1024 # default |
| if (len(words) >= 3): |
| last_n_count = words[2] |
| ev_hist_name = words[0] |
| ev_hist_field_name = 'ev-hist-' + ev_hist_name |
| url = 'http://%s/rest/v1/event-history/%s/%s' % \ |
| (self.controller, ev_hist_name, last_n_count) |
| ev_hist = self.rest_simple_request_to_dict(url) |
| #ev_hist - json.dumps(ev_hist) |
| events = ev_hist['events'] |
| tableData = [] |
| for ev in events: |
| info = ev['info'] |
| base_info = ev['base_info'] |
| info.update(base_info) |
| tableData.append(info) |
| # Prepare the date for CLI display |
| tableData = self.pp.format_table(tableData, ev_hist_field_name) |
| yield("\n") |
| yield(tableData) |
| |
| # |
| # ------------------------------------------------------------------------------- |
| # cp_show_vns |
| # Command completion for the following command |
| # Used in "show vns { <vns-name> | all } flow [ brief | full-detail | detail | summary ] |
| # |
| def cp_show_vns_flow(self, words, text, completion_char): |
| cmd1 = 'brief show flow information in brief format' |
| cmd2 = 'detail show detailed flow information' |
| cmd3 = 'full-detail show full details of flow information' |
| cmd4 = 'summary show summarized flow information' |
| |
| syntax = self.syntax_msg('show vns {<vns-name> | all} flow [ brief | detail | full-detail | summary ]') |
| if (len(words) == 3) and text=='': |
| self.print_completion_help(cmd1+'\n'+cmd2+'\n'+cmd3+'\n'+cmd4) |
| return ['brief', 'detail', 'full-detail', 'summary', '<cr>' ] |
| elif (len(words) == 3) and ('detail'.startswith(text)): |
| return ['detail'] |
| elif (len(words) == 3) and ('full-detail'.startswith(text)): |
| return ['full-detail'] |
| elif (len(words) == 3) and ('summary'.startswith(text)): |
| return ['summary'] |
| elif (len(words) == 3) and ('brief'.startswith(text)): |
| return ['brief'] |
| elif (len(words) == 4) and (words[3] not in ['brief', 'detail', 'full-detail', 'summary']): |
| self.print_completion_help(syntax) |
| elif (len(words) >= 3) and (text != ''): |
| self.print_completion_help(syntax) |
| else: |
| self.print_completion_help('<cr>') |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # show_vns_flow_annotated |
| # |
| # For a given vns-name, show all the active flows in that vns |
| # or |
| # for show active-flows in all vnses, categorized by vns |
| # Used in "show vns { <vns-name> | all } flow [ brief | full-detail | detail | summary ] |
| # flow records are annotated in the sdnplatform with vns name |
| # |
| def show_vns_flow_annotated(self, words): |
| |
| syntax = self.syntax_msg('show vns {<vns-name> | all} flow [ brief | detail | full-detail |summary ]') |
| if (len(words) == 3) and (words[2] not in ['brief', 'details', 'full-detail', 'summary']): |
| print syntax |
| return |
| |
| if (len(words) > 3): |
| print syntax |
| return |
| |
| # all = False is this used anywhere |
| |
| vns = words[0] |
| |
| option = 'none' |
| if (len(words) == 3): |
| option = words[2] |
| |
| # Get all the flows in the network |
| annotated_flow_data = {} |
| url = 'http://%s/rest/v1/vns/realtimestats/flow/%s' % (self.controller, vns) |
| annotated_flow_data = self.rest_simple_request_to_dict(url) |
| #print "Data=%s" % json.dumps(annotated_flow_data, sort_keys=True, indent=4) |
| # annotated_flow_data response is in the following format: |
| # Data={ |
| # "vnsCount": 1, |
| # "vnsFlowMap": { |
| # "two": { |
| # "flowCount": 10, |
| # "flowList": [ |
| # { |
| # "dpid": "00:00:00:00:00:00:00:0f", |
| # "flowEntry": { |
| # "actions": [ |
| # { |
| |
| # vnsCount = annotated_flow_data["vnsCount"] |
| vnsFlowMap = annotated_flow_data["vnsFlowMap"] |
| vnsAddressSpaceMap = annotated_flow_data["vnsAddressSpaceMap"] |
| |
| if (option == "brief"): |
| table_field_ordering = "vns_flow" |
| elif (option == "detail"): |
| table_field_ordering = "details" |
| elif (option == "full-detail"): |
| table_field_ordering = "undefined" # prints everything! |
| elif (option == "summary"): |
| table_field_ordering = "summary" |
| else: |
| table_field_ordering = "default" |
| |
| summaryTable = [] # used for holding the data for "summary" option |
| for vnsName in vnsFlowMap: |
| if (option == 'brief') or (option == "summary"): |
| flow_tuple_list = [] |
| briefFlowCnt = 0 |
| # Table Data will hold all the flow entries, one list element per row of output |
| # It is reinitilized for each VNS |
| tableData = [] |
| vnsFlowCnt = vnsFlowMap[vnsName]["flowCount"] |
| vnsFlowList = vnsFlowMap[vnsName]["flowList"] |
| for vnsFlowEntry in vnsFlowList: |
| flowEntry = vnsFlowEntry["flowEntry"] |
| dpid = vnsFlowEntry["dpid"] |
| if (option == "brief") or (option == "summary"): |
| src_mac = flowEntry['match']['dataLayerDestination'] |
| dst_mac = flowEntry['match']['dataLayerSource'] |
| vlan = flowEntry['match']['dataLayerVirtualLan'] |
| etherType = flowEntry['match']['dataLayerType'] |
| flowList = [] |
| flowList.append(src_mac) |
| flowList.append(dst_mac) |
| flowList.append(vlan) |
| flowList.append(etherType) |
| if flowList in flow_tuple_list: |
| # duplicate flow (due to same flow entry on multiple switches along the path, |
| # skip it if the option if brief or summary |
| continue |
| else: |
| flow_tuple_list.append(flowList) |
| briefFlowCnt += 1 |
| #print "DPID = %s" % dpid |
| #print " New Data = %s" % json.dumps(flowEntry, indent=4) |
| if (option != "summary"): |
| tableEntry = {} |
| tableEntry[dpid] = [] |
| tableEntry[dpid].append(flowEntry) |
| tableEntry = self.add_dpid_to_data(tableEntry) |
| tableEntry = self.fix_realtime_flows(tableEntry) |
| #print "Table Entry : %s" % json.dumps(tableEntry, indent=4) |
| tableData.append(tableEntry[0]) |
| #print "*** Table Data: %s" % json.dumps(tableData[0], indent=4) |
| #printf tenant + vnsname |
| names=vnsName.split('|') |
| tenant=names[0] |
| vns=names[1] |
| if (option == "brief"): |
| yield("\nTenant %s VNS %s (address-space %s) flows (count: %s)" % (tenant, vns, vnsAddressSpaceMap[vnsName], briefFlowCnt)) |
| elif (option != "summary"): |
| yield("\nTenant %s VNS %s (address-space %s) flow entries (count: %s)" % (tenant, vns, vnsAddressSpaceMap[vnsName], vnsFlowCnt)) |
| # Print flow data for one vns, unless option is summary |
| if (option == "summary"): |
| summaryData = dict() |
| # summaryData['tenant'] =tenant |
| summaryData["vnsName"] = vns |
| summaryData["flowEntryCnt"] = vnsFlowCnt |
| summaryData["vnsFlowCnt"] = briefFlowCnt |
| summaryTable.append(summaryData) |
| else: |
| yield("\n") |
| # print "*** TD = %s" % tableData |
| tableData = self.pp.format_table(tableData, "realtime_flow", table_field_ordering) |
| yield(tableData) |
| |
| if (option == "summary"): |
| # print "*** ST %s" % summaryTable |
| yield("\n") |
| summaryTable = self.pp.format_table(summaryTable, "realtime_flow", table_field_ordering) |
| yield(summaryTable) |
| print(" ") |
| return |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # show_vns_switch_ports |
| # For some vns-name, collect all the interfaces, and from that, |
| # collect all the attachment points. The objective here is to |
| # name all the switch's and ports associated with a vns |
| # |
| def show_vns_switch_ports(self, words): |
| if len(words) < 1: |
| return |
| |
| # words: [ vns_name, 'switch', switch_name ] |
| vns_name = words[0] |
| switch = None |
| if len(words) > 2: |
| switch = convert_alias_to_object_key('switch', words[2]) |
| port = None |
| if len(words) > 3: |
| port = words[3] |
| |
| vns_interface_key = mi.pk('vns-interface') |
| if vns_name == 'all': |
| vns_ifs = create_obj_type_dict('vns-interface', |
| vns_interface_key, |
| 'id') |
| vns_rules = create_obj_type_dict('vns-interface-rule', |
| mi.pk('vns-interface-rule'), |
| 'id',) |
| else: |
| vns_ifs = create_obj_type_dict('vns-interface', |
| vns_interface_key, |
| 'vns', |
| vns_name) |
| vns_rules = create_obj_type_dict('vns-interface-rule', |
| mi.pk('vns-interface-rule'), |
| 'vns', |
| vns_name) |
| host_dap_dict = self.create_attachment_dict() |
| |
| bsp = {} # _b_vs _s_witch _p_ort |
| for ifs in vns_ifs: |
| fields = ifs.split('|') |
| # all these fields must have two parts. |
| if len(fields) != 3: |
| continue |
| # choose between switch based rules and mac/ip based rules |
| rule = 'default' |
| vns_name = vns_ifs[ifs][0]['vns'] |
| switch_rule = None |
| if fields[1] != 'default': |
| rule = vns_ifs[ifs][0]['rule'] |
| if not rule in vns_rules: |
| continue |
| if_rule = vns_rules[rule][0] |
| if 'switch' in if_rule: |
| switch_rule = if_rule['switch'] |
| else: |
| if_rule = { 'mac' : 'defaultLie' } # for default rules |
| |
| # There's two major classes of rules: host and switch |
| # Use existance of a 'switch' to differentiate between the two. |
| if switch_rule: |
| # if_rule/switch are both set, part after the '/' is the port |
| if fields[2].find('/') < 0: |
| # skip Eth<n> general interface |
| continue |
| # |
| # Prefix this port with a plus sign, which will be recognized |
| # in the printing code as already describing an openflow port name |
| port = '+' + fields[2].split('/')[1] |
| tenant_vns=vns_name.split('|') |
| tenant_name=tenant_vns[0] |
| vns_real_name=tenant_vns[1] |
| key = "%s|%s|%s|%s" % (tenant_name,vns_real_name, switch, port) |
| |
| if not key in bsp: |
| bsp[key] = { |
| 'tenant' : tenant_name, |
| 'vns' : vns_real_name, |
| 'switch' : switch_rule, |
| 'port' : port, |
| 'reason' : ["Rule:%s" % rule.split('|')[-1], fields[1]], |
| } |
| else: |
| pass # should only be one. |
| else: |
| # the second part (fields[1]) is the interface-long name, |
| items = fields[2].split('/') |
| if len(items) == 1: |
| if items[0].startswith('VEth'): |
| if not 'mac' in rule: |
| continue |
| mac = if_rule['mac'] |
| elif items[0].startswith('Eth'): |
| self.debug_msg('should have been a switch in if_rule') |
| continue |
| else: |
| self.debug_msg('item length of 1') |
| continue |
| else: |
| mac = items[1] |
| if not re.match(r'^(([A-Fa-f\d]){2}:?){5}[A-Fa-f\d]{2}$', mac): |
| print 'Not a mac address: %s' % mac |
| # currently just use the mac to find the attachment points. |
| if mac in host_dap_dict: |
| for attachment_point in host_dap_dict[mac]: |
| # if switch is named, skip any which don't match |
| if switch and attachment_point['switch'] != switch: |
| continue |
| if port and attachment_point['ingress-port'] != port: |
| continue |
| |
| tenant_vns=vns_name.split('|') |
| tenant_name=tenant_vns[0] |
| vns_real_name=tenant_vns[1] |
| key = "%s|%s|%s|%s" % (tenant_name, |
| vns_real_name, |
| attachment_point['switch'], |
| attachment_point['ingress-port']) |
| if not key in bsp: |
| bsp[key] = { |
| 'tenant' : tenant_name, |
| 'vns' : vns_real_name, |
| 'switch' : attachment_point['switch'], |
| 'port' : utif.try_int(attachment_point['ingress-port']), |
| 'reason' : ["Rule:%s" % rule.split('|')[-1], mac] |
| } |
| else: |
| self.append_when_missing(bsp[key]['reason'], |
| "Rule:%s" % rule.split('|')[-1]) |
| self.append_when_missing(bsp[key]['reason'], mac) |
| |
| sort = [bsp[x] for x in sorted(bsp.keys(), |
| cmp=lambda a,b: cmp(utif.try_int(a.split('|')[2]), |
| utif.try_int(b.split('|')[2])) |
| if a.split('|')[1] == b.split('|')[1] |
| else cmp(b.split('|')[1],a.split('|')[1]))] |
| |
| return self.display_obj_type_rows('vns-switch-ports', sort) |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # show_switch_ports_vns |
| # |
| def show_switch_ports_vns(self, words): |
| switch = None |
| if len(words) > 1 and words[0] != 'all': |
| switch = words[0] |
| # |
| # switch alias conversion |
| value = alias_lookup('switch-alias', switch) |
| if value: |
| switch = value |
| |
| # |
| # dictionary from the vns side, indexed by host. |
| vns_ifs_dict = create_obj_type_dict('vns-interface', |
| mi.pk('vns-interface')) |
| # |
| # dictionary of the interface-rules, by rule |
| vns_rules = create_obj_type_dict('vns-interface-rule', |
| mi.pk('vns-interface-rule')) |
| # |
| # from the list of hosts, identify the attachment points |
| host_dap_dict = self.create_attachment_dict() |
| |
| # |
| # there can be multiple attachment points for each of the |
| # hosts. iterate over the hosts, find all the attachment points, |
| # manage an association for the entries |
| spb = {} # _s_witch _p_ort _b_vs |
| for ifs in vns_ifs_dict: |
| fields = vns_ifs_dict[ifs][0]['id'].split('|') |
| # all these fields must have two parts. |
| if len(fields) != 2: |
| continue |
| # id parts are vns|interface |
| vns_name = fields[0] |
| |
| # choose between switch based rules and mac/ip based rules |
| rule = 'default' |
| |
| switch_rule = None |
| mac_rule = None |
| rule_fields = [rule] |
| if vns_name == 'default': |
| if fields[1].find('/') >= 0: |
| mac_rule = fields[1].split('/')[1] |
| else: |
| if not 'rule' in vns_ifs_dict[ifs][0]: |
| continue |
| rule = vns_ifs_dict[ifs][0]['rule'] |
| rule_fields = rule.split('|') |
| if_rule = vns_rules[rule][0] |
| if 'switch' in if_rule: |
| if switch and if_rule['switch'] != switch: |
| continue |
| switch_rule = if_rule['switch'] # switch may be None. |
| elif 'mac' in if_rule: |
| mac_rule = if_rule['mac'] |
| elif 'ip-subnet' in if_rule: |
| mac_rule = if_rule['ip-subnet'] |
| elif 'tags' in if_rule: |
| mac_rule = if_rule['tags'] |
| elif 'vlans' in if_rule: |
| mac_rule = if_rule['vlans'] |
| if mac_rule: |
| if not mac_rule in host_dap_dict: |
| self.debug_msg("Unknown attachment point for %s" % mac_rule) |
| continue |
| |
| for attachment_point in host_dap_dict[mac_rule]: |
| key = "%s|%s" % (attachment_point['switch'], |
| attachment_point['ingress-port']) |
| if switch and attachment_point['switch'] != switch: |
| continue |
| |
| if not key in spb: |
| spb[key] = { |
| 'switch' : attachment_point['switch'], |
| 'port' : utif.try_int(attachment_point['ingress-port']), |
| 'vns' : {vns_name : 1}, |
| 'reason' : ["Rule:%s" % rule_fields[-1], mac_rule] |
| } |
| else: |
| if vns_name in spb[key]['vns']: |
| spb[key]['vns'][vns_name] += 1 |
| else: |
| spb[key]['vns'][vns_name] = 1 |
| self.append_when_missing(spb[key]['reason'], |
| "Rule:%s" % rule_fields[-1]) |
| self.append_when_missing(spb[key]['reason'], mac_rule) |
| if switch_rule: |
| if fields[1].find('/') >= 0: |
| # |
| # Prefix this port with a plus sign, which will be recognized |
| # in the printing code as already describing an openflow port name |
| port = '+' + fields[1].split('/')[1] |
| key = "%s|%s" % (switch_rule, port) |
| |
| if not key in spb: |
| spb[key] = { |
| 'switch' : switch_rule, |
| 'port' : port, |
| 'vns' : {vns_name : 1}, |
| 'reason' : ["Rule:%s" % rule_fields[-1], fields[1]] |
| } |
| |
| sort = [spb[x] for x in sorted(spb.keys(), |
| cmp=lambda a,b: cmp(utif.try_int(a.split('|')[1]), |
| utif.try_int(b.split('|')[1])) |
| if a.split('|')[0] == b.split('|')[0] |
| else cmp(b.split('|')[0],a.split('|')[0]))] |
| |
| return self.display_obj_type_rows('switch-ports-vns', sort) |
| # [spb[x] for x in sorted(spb.keys())]) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_show_vns |
| # |
| def do_show_vns(self, words): |
| choices = ['interface', 'mac-address-table', |
| 'interface-rules', 'access-lists', 'running-config', 'switch', 'flow' ] |
| if words == None or len(words) > 2: |
| if words[1] not in ['switch', 'flow']: |
| return self.syntax_msg('show vns <vns-name> [ %s ] ' % |
| ' | '.join(choices)) |
| if len(words) in [3, 4]: |
| if words[1] == 'switch': |
| return self.show_vns_switch_ports(words) |
| elif words[1] == 'flow': |
| return self.show_vns_flow_annotated(words) |
| else: |
| return self.syntax_msg('show vns <vns-name> [ %s ] ' % |
| ' | '.join(choices)) |
| elif len(words) == 2: |
| # words[0] ought to an existing vns |
| # Allow show vns all flow [detail] |
| if (not self.vns_find_name(words[0])) and (words[1] != 'flow'): |
| return self.error_msg("show vns '%s': vns Not Found" % words[0]) |
| |
| vns_key = self.prefix_search_key([words[0]]) |
| selection = utif.full_word_from_choices(words[1], choices) |
| if not selection: |
| return self.syntax_msg('show vns <vns-name> [ %s ' % |
| ' | '.join(choices)) |
| if selection == 'interface': |
| return self.find_and_display_vns_interface(words[0], words[2:]) |
| elif selection == 'mac-address-table': |
| # the id for the host-vns-interface table has the host as the 'prefix', |
| # preventing searching based on the prefix. the vns is not even a foreign |
| # key, which means the complete table must be read, then the vns association |
| # must be determined, and then matched |
| return self.display_vns_mac_address_table(words[0], words[2:]) |
| elif selection == 'interface-rules': |
| return self.do_show_object(['vns-interface-rule', vns_key], "<no_key>") |
| elif selection == 'access-lists': |
| return self.do_show_object(['vns-access-list-entry', vns_key], "<no_key>") |
| elif selection == 'running-config': |
| return self.show_vns_running_config(words[0]) |
| elif selection == 'switch': |
| return self.show_vns_switch_ports(words) |
| elif selection == 'flow': |
| return self.show_vns_flow_annotated(words) |
| elif len(words) == 1: |
| return self.do_show_object(["vns-definition", words[0]]) |
| else: |
| return self.do_show_object(["vns-definition"]) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_show_vns_interface_rule |
| # do_show_object is used to construct the output for the table search. |
| # However, since a vns prefix key search is used to display the table, |
| # and since do_show_object behaves differently when displaying tables |
| # depending on whether the table is a key search or not, the 'with_search_key' |
| # parameter is set to "with_key" when the caller included an additional |
| # prefix for (key) for the rule, and otherwise '<no_key>' is used when |
| # the caller wants the complete interface-rule table for a single vns |
| # (which obvioulsy also implies a key search for 'vns|' prefied rules. |
| # |
| def do_show_vns_interface_rule(self, words): |
| with_search_key = "<no_key>" |
| if self.vns_name() is None: |
| if len(words) > 0: |
| search_object = ["vns-interface-rule", words[0]] |
| else: |
| search_object = ["vns-interface-rule"] |
| elif len(words) > 0: |
| with_search_key = '-'.join(words) |
| words.insert(0, self.vns_name()) |
| search_object = ["vns-interface-rule", |
| self.unique_key_from_non_unique(words)] |
| else: |
| search_object = ["vns-interface-rule", |
| self.prefix_search_key([self.vns_name()]) ] |
| obj_type = 'vns-interface-rule' |
| |
| # --- this ought to be promoted to be used more .. |
| s_dict = {} |
| # for all foreign keys in this obj_type |
| mode_dict = self.mode_stack_to_rest_dict({}) |
| |
| # for fk in mi.obj_type_foreign_keys(obj_type): |
| for kf in mi.compound_key_fields(obj_type, mi.pk(obj_type)): |
| if mi.is_foreign_key(obj_type, kf): |
| (ref_ot, ref_fn) = mi.foreign_key_references(obj_type, kf) |
| if ref_ot in mode_dict: |
| s_dict[kf] = mode_dict[ref_ot] |
| |
| # for (n,v) in self.mode_stack_to_rest_dict({}).items(): |
| # for (fk_ot, fk_fn) in mi.foreign_key_xref[n][mi.pk(n)]: |
| # print n, fk_ot, fk_fn |
| # if fk_ot == obj_type: |
| # s_dict[fk_fn] = v |
| entries = self.store.rest_query_objects(obj_type, s_dict) |
| return self.display_obj_type_rows(obj_type, entries, with_search_key) |
| return self.do_show_object(search_object, with_search_key) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # cp_is_alias |
| # Used in cp_no to determine if the particular request is an alias completion |
| # |
| def cp_is_alias(self, words, text): |
| if not self.in_config_submode() or len(words) != 2: |
| return False |
| obj_type = self.get_current_mode_obj_type() |
| # LOOK! robv: Tweaked this from the commented out version below |
| return obj_type in mi.alias_obj_type_xref |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # cascade_delete |
| # Delete interior objects' rows related via foreign keys |
| # |
| def cascade_delete(self, obj_type, id_value): |
| global modi |
| |
| obj_key = mi.pk(obj_type) |
| if obj_type in mi.foreign_key_xref and \ |
| obj_key in mi.foreign_key_xref[obj_type]: |
| for (fk_obj_type, fk_name) in mi.foreign_key_xref[obj_type][obj_key]: |
| if mi.is_cascade_delete_enabled(fk_obj_type): |
| # |
| # find any affected rows |
| try: |
| rows = self.get_table_from_store(fk_obj_type, |
| fk_name, |
| id_value, |
| "exact") |
| except Exception, e: |
| if self.debug or self.debug_backtrace: |
| errors = self.rest_error_to_dict(e, fk_obj_type) |
| print self.rest_error_dict_to_message(errors) |
| rows = [] |
| |
| # determine whether the foreign key can have a null |
| # value, in which case the row doesn't have to be deleted, |
| # just updated the foreign key to `None'. |
| if mi.is_force_delete_enabled(fk_obj_type): |
| delete = True |
| elif mi.is_null_allowed(fk_obj_type, fk_name): |
| delete = False |
| else: |
| delete = True |
| |
| key = mi.pk(fk_obj_type) |
| for row in rows: |
| try: |
| if delete: |
| self.rest_delete_object(fk_obj_type, row[key]) |
| else: # update |
| self.rest_update_object(fk_obj_type, key, row[key], |
| { fk_name : None } ) |
| errors = None |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, |
| fk_obj_type + " " |
| + row[key]) |
| if errors: |
| return self.rest_error_dict_to_message(errors) |
| self.debug_msg("cascade delete: %s: %s" % \ |
| (fk_obj_type, ', '.join(row[key].split('|')))) |
| if delete: |
| self.cascade_delete(fk_obj_type, row[key]) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_no |
| # Delete rows from a table, or update a field. For fields which |
| # have defaults, 'no' referts the field to its default value. |
| # |
| def do_no(self, words): |
| # format is no < obj_type > < id > |
| if not self.in_config_mode(): |
| if len(words) < 1 or words[0] != 'debug': |
| return self.error_msg("'no' command only valid in config mode") |
| obj_type = self.get_current_mode_obj_type() |
| option = words[0] |
| # |
| # manage deletion of complete rows in obj_types' |
| # |
| if option in self.obj_types_for_config_mode_starting_with(): |
| if not len(words) > 1: |
| return self.error_msg("<id> must be specified for the object to delete") |
| item = convert_alias_to_object_key(option, words[1]) |
| |
| try: |
| self.rest_delete_object(option, item) |
| errors = None |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, option + " " + item) |
| # |
| # cascade delete? |
| if not errors: |
| self.cascade_delete(option, item) |
| # |
| # manage removal/set-to-default of values in fields of an obj_type |
| # |
| elif option in self.fields_for_current_submode_starting_with(option): |
| # |
| # replace the obj_type with the alias obj_type when for |
| # fields called 'alias' when the obj_type has one alias |
| if obj_type in mi.alias_obj_type_xref: |
| aliases = mi.alias_obj_type_xref[obj_type] |
| if option in aliases: |
| obj_type = option |
| if len(aliases) == 1 and option == 'alias': |
| obj_type = aliases[0] |
| if len(aliases) != 1: |
| print self.error_msg("Internal 'no' more than one alias choice") |
| if len(words) < 2: |
| return "Syntax: no %s <value>" % option |
| item = words[1] |
| try: |
| self.rest_delete_object(obj_type, item) |
| errors = None |
| except Exception, e: |
| errors = self.rest_error_to_dict(e, obj_type + " " + item) |
| return self.rest_error_dict_to_message(errors) |
| |
| key = mi.pk(obj_type) |
| # |
| # note: field_default_value returns None when no default is |
| # provided. its not clear whether default values are always |
| # provided for fields which don't accept null values. |
| # |
| default_value = mi.field_default_value(obj_type, option) |
| |
| if mi.is_null_allowed(obj_type, option) and default_value: |
| self.warning("'%s' accepts null and had a default " |
| "value; %s is set to the default value '%s" % |
| (obj_type, option, default_value)) |
| |
| errors = self.rest_update_object(obj_type, |
| key, |
| self.get_current_mode_obj(), |
| {option:default_value}) |
| # fall through to return |
| elif self.in_config_vns_acl_mode() and self.ACL_RE.match(option): |
| return self.do_vns_no(['access-list-entry'] + words) |
| #elif self.in_config_controller_interface_mode() and 'firewall'.startswith(option): |
| # return self.do_no_firewall(words) |
| else: |
| try: |
| command_words = ['no'] + words |
| # Just try to execute the command. It will either succeed or |
| # throw an exception |
| return command.do_command(command_words) |
| except urllib2.HTTPError, e: |
| raise e |
| except Exception, e: |
| if self.debug or self.debug_backtrace: |
| traceback.print_exc() |
| return self.error_msg("'%s' must be either a valid object type or a field" % |
| option) |
| |
| # when errors == None, rest_error_dict_to_message returns None. |
| return self.rest_error_dict_to_message(errors) |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # implement_ping |
| # Performs a traceroute to a switch or host. |
| # Input can either be a DPID, switch alias, or host (IP or domain). |
| def implement_ping(self, data): |
| |
| count = '-c %d ' % data.get('count', 5) |
| if not 'ip-address' in data: |
| yield('Can\'t determine ping target') |
| return |
| |
| ip_host = data['ip-address'] |
| if self.DPID_RE.match(ip_host): |
| ip_host = self.get_ip_from_switch_dpid_or_alias(ip_host) |
| |
| cmd = 'ping %s%s' % (count, self.shell_escape(ip_host)) |
| for item in self.generate_subprocess_output(cmd): |
| yield item |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # implement_traceroute |
| # Performs a traceroute to a switch or host. |
| # Input can either be a DPID, switch alias, or host (IP or domain). |
| def implement_traceroute(self, data): |
| |
| if not 'ip-address' in data: |
| yield('Can\'t determine traceroute target') |
| return |
| |
| ip_host = data['ip-address'] |
| if self.DPID_RE.match(ip_host): |
| ip_host = self.get_ip_from_switch_dpid_or_alias(ip_host) |
| |
| cmd = 'traceroute %s' % self.shell_escape(ip_host) |
| for item in self.generate_subprocess_output(cmd): |
| yield item |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_no_firewall |
| # |
| #def do_no_firewall(self, words): |
| # return self.do_firewall(words[1:], True) |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # firewall_rule_add_rule_to_entries |
| # |
| def firewall_rule_add_rule_to_entries(self, entries): |
| for entry in entries: |
| entry['rule'] = run_config.firewall_rule(entry) |
| return entries |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # get_firewall_rules |
| # Returns a list of strings describing the firewall rules for a particular |
| # controller-node name. |
| # |
| def get_firewall_rules(self, node): |
| key = mi.pk('firewall-rule') |
| key_value = self.unique_key_from_non_unique([node]) |
| entries = self.get_table_from_store('firewall-rule', key, key_value) |
| |
| return self.firewall_rule_add_rule_to_entries(entries) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_show_firewall |
| # Depends on get_firewall_rules to add 'rule' to entries so that |
| # the 'firewall-rule' table can display the rule's value in a |
| # easily identified syntax. |
| # |
| def do_show_firewall(self, words): |
| return self.display_obj_type_rows('firewall-rule', |
| self.get_firewall_rules(self.get_current_mode_obj())) |
| |
| # -------------------------------------------------------------------------------- |
| # handle_command |
| # |
| def handle_command(self, command_word, words): |
| if type(command_word) == str: |
| method = self.command_method_from_name(command_word) |
| if method: |
| return method(words) |
| # XXX It would be better to only call do_command if it |
| # was clear that this command actually existed. |
| return command.do_command([command_word] + words) |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # find_with_quoting |
| # |
| # Assumes start_index is not inside a quoted string. |
| # |
| @staticmethod |
| def find_with_quoting(line, find_char, reverse=False, start_index=0): |
| in_quoted_arg = False |
| line_length = len(line) |
| i = start_index |
| found_index = -1; |
| while i < line_length: |
| c = line[i] |
| if c in "\"'": |
| if not in_quoted_arg: |
| quote_char = c |
| in_quoted_arg = True |
| elif c == quote_char: |
| in_quoted_arg = False |
| # otherwise leave in_quoted_arg True |
| elif c == "\\" and in_quoted_arg: |
| i += 1 |
| elif (c == find_char) and not in_quoted_arg: |
| found_index = i |
| if not reverse: |
| break |
| i += 1 |
| |
| return found_index |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # split_with_quoting |
| # |
| @staticmethod |
| def split_with_quoting(line, separators=" \t"): |
| word_list = [] |
| current_word = "" |
| in_quoted_arg = False |
| line_length = len(line) |
| i = 0 |
| while i < line_length: |
| c = line[i] |
| i += 1 |
| if c in "\"'": |
| if not in_quoted_arg: |
| in_quoted_arg = True |
| quote_char = c |
| elif c == quote_char: |
| in_quoted_arg = False |
| word_list.append(current_word) |
| current_word = "" |
| else: |
| current_word += c |
| elif c == "\\" and in_quoted_arg: |
| if i < line_length: |
| c = line[i] |
| current_word += c |
| i += 1 |
| elif (c in separators) and not in_quoted_arg: |
| if current_word: |
| word_list.append(current_word) |
| current_word = "" |
| else: |
| current_word += c |
| |
| if current_word: |
| word_list.append(current_word) |
| |
| return word_list |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # quote_item |
| # Some of the new model columns available as choices to select have the '|' |
| # character as a separator. For these choices to word, they need to be |
| # quoted |
| # |
| @staticmethod |
| def quote_item(obj_type, item): |
| if item.find("|") >= 0: |
| return '"' + str(item) + '"' |
| else: |
| return str(item) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # |
| def replay(self, file, verbose = True, command_replay = False): |
| # Only replay the STR values, since the code ought to |
| # stuff back the JSON values. |
| play = open(file) |
| rest_line_format = re.compile(r'^REST ([^ ]*)( *)([^ ]*)( *)(.*)$') |
| cmd_line_format = re.compile(r'^COMMAND (.*)$') |
| skip_command = True |
| for line in play.read().split('\n'): |
| # the format ought to be url<space>[STR|JSON]<space> ... |
| match = rest_line_format.match(line) |
| if match: |
| if match.group(3) == 'STR': |
| if verbose: |
| print 'REST STR', match.group(1) |
| url_cache.save_url(match.group(1), match.group(5), 1000000) |
| elif match.group(3) == 'JSON': |
| if verbose: |
| print 'REST JSON', match.group(1) |
| entries = json.loads(match.group(5)) |
| url_cache.save_url(match.group(1), entries, 1000000) |
| else: |
| print 'REST REPLAY NOT STR|JSON' |
| elif len(line): |
| match = cmd_line_format.match(line) |
| if command_replay and match: |
| # skip the first command since it ought to be the replay enablement |
| if skip_command: |
| if verbose: |
| print 'SKIP COMMAND %s' % match.group(1) |
| skip_command = False |
| else: |
| line = self.split_with_quoting(match.group(1)) |
| if verbose: |
| print 'COMMAND %s' % line |
| output = self.handle_multipart_line(line[0]) |
| if output != None: |
| print output |
| else: |
| print 'no MATCH +%s+' % line |
| play.close() |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # handle_single_line |
| # |
| def handle_single_line(self, line): |
| ret_val = None |
| if len(line) > 0 and line[0]=="!": # skip comments |
| return |
| words = self.split_with_quoting(line) |
| if not words: |
| return |
| # |
| self.completion_reset() |
| |
| # Look for the replay keyword, use the first two tokens if the replay |
| # keyword is in the first part of the command. |
| if self.debug and len(words) >= 2: |
| if words[0] == 'replay': |
| # replay the file, remove the first two keywords |
| self.replay(words[1], command_replay = len(words) == 2) |
| if len(words) == 2: |
| return |
| words = words[2:] |
| |
| # the first word of a line is either: |
| # - a command - dependent on mode (show anywhere but configure only in enable) |
| # - an object type - if we're in a config mode (either config or config submode) |
| # - a field for an object - if we're in a config submode |
| matches = [(x, "command") for x in self.commands_for_current_mode_starting_with(words[0])] |
| matches.extend([(x, "config object") for x in self.obj_types_for_config_mode_starting_with(words[0])]) |
| matches.extend([(x, "field") for x in self.fields_for_current_submode_starting_with(words[0])]) |
| # LOOK!: robv Fix to work with field names where one name is a prefix of another |
| if len(matches) > 1: |
| for match_tuple in matches: |
| if match_tuple[0] == words[0]: |
| matches = [match_tuple] |
| break |
| if len(matches) == 1: |
| match = matches[0] |
| # Replace the (possibly) abbreviated argument with the full name. |
| # This is so that the handlers don't need to all handle abbreviations. |
| if type(match[0]) == str: |
| words[0] = match[0] |
| |
| if match[1] == "field": |
| ret_val = self.handle_field(words) |
| elif match[1] == "config object": |
| ret_val = self.handle_obj_type(words) |
| else: |
| ret_val = self.handle_command(words[0], words[1:]) |
| #ret_val = self.handle_command(match[0], words[1:]) |
| elif len(matches) > 1: |
| ret_val = self.error_msg("%s is ambiguous\n" % words[0]) |
| for m in matches: |
| ret_val += "%s (%s)\n" % m |
| else: |
| ret_val = self.error_msg("Unknown command: %s\n" % words[0]) |
| |
| url_cache.command_finished(words) |
| return ret_val |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # generate_pipe_output |
| # |
| def generate_pipe_output(self, p, output): |
| fl = fcntl.fcntl(p.stdout, fcntl.F_GETFL) |
| fcntl.fcntl(p.stdout, fcntl.F_SETFL, fl | os.O_NONBLOCK) |
| |
| for item in output: |
| try: |
| p.stdin.write(item) |
| except IOError: |
| break |
| |
| try: |
| out_item = p.stdout.read() |
| yield out_item |
| except IOError: |
| pass |
| |
| p.stdin.close() |
| |
| fcntl.fcntl(p.stdout, fcntl.F_SETFL, fl) |
| while True: |
| out_item = p.stdout.read() |
| if (out_item): |
| yield out_item |
| else: |
| p.stdout.close() |
| break |
| p.wait() |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # write_to_pipe |
| # |
| def write_to_pipe(self, p, output): |
| for item in output: |
| try: |
| p.stdin.write(item) |
| except IOError: |
| break |
| p.stdin.close() |
| p.wait() |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # shell_escape |
| # Return a string, quoting the complete string, and correctly prefix any |
| # quotes within the string. |
| # |
| def shell_escape(self, arg): |
| return "'" + arg.replace("'", "'\\''") + "'" |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # handle_pipe_and_redirect |
| # |
| def handle_pipe_and_redirect(self, pipe_cmds, redirect_target, output): |
| # if redirect target is tftp/ftp/http/file, then we should actually stick |
| # curl at the end of the pipe_cmds so it gets handled below |
| if redirect_target: |
| if redirect_target.startswith("tftp") or redirect_target.startswith("ftp") or \ |
| redirect_target.startswith("http") or redirect_target.startswith("file"): |
| redirect_target = self.shell_escape(redirect_target) |
| # add so it can be used below |
| if pipe_cmds == None: |
| pipe_cmds = "" |
| else: |
| pipe_cmds += " | " |
| |
| if redirect_target.startswith("ftp"): # shell_escape added quote |
| pipe_cmds += " curl -T - %s" % self.shell_escape(redirect_target) |
| else: |
| pipe_cmds += " curl -X PUT -d @- %s" % self.shell_escape(redirect_target) |
| |
| if pipe_cmds: |
| new_pipe_cmd_list = [] |
| for pipe_cmd in [x.strip() for x in pipe_cmds.split('|')]: |
| # doing it this way let us handles spaces in the patterns |
| # as opposed to using split/join which would compress space |
| new_pipe_cmd = pipe_cmd |
| m = re.search('^(\w+)(.*)$', pipe_cmd) |
| if m: |
| first_tok = m.group(1) |
| rest_of_cmd = m.group(2).strip() |
| if first_tok.startswith("in"): |
| new_pipe_cmd = "grep -e " + rest_of_cmd |
| elif first_tok.startswith("ex"): |
| new_pipe_cmd = "grep -v -e" + rest_of_cmd |
| elif first_tok.startswith("begin"): |
| new_pipe_cmd = "awk '/%s/,0'" % rest_of_cmd |
| new_pipe_cmd_list.append(new_pipe_cmd) |
| |
| new_pipe_cmds = "|".join(new_pipe_cmd_list) |
| if new_pipe_cmds: |
| if redirect_target: |
| p = subprocess.Popen(new_pipe_cmds, |
| shell=True, |
| stdin=subprocess.PIPE, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT) |
| output = self.generate_pipe_output(p, output) |
| else: |
| p = subprocess.Popen(new_pipe_cmds, |
| shell=True, |
| stdin=subprocess.PIPE) |
| self.write_to_pipe(p, output) |
| output = None |
| |
| # only handle local file here as http/ftp were handled above via pipe |
| if redirect_target: |
| if redirect_target.startswith("config://"): |
| m = re.search(self.local_name_pattern, redirect_target) |
| if m: |
| join_output = ''.join(iter(output)) |
| store_result = self.store.set_user_data_file(m.group(1), join_output) |
| if store_result: |
| result = json.loads(store_result) |
| else: |
| return self.error_msg("rest store result not json format") |
| if 'status' in result and result['status'] == 'success': |
| return None |
| elif 'message' not in result: |
| return self.error_msg("rest store result doesn't contain error message") |
| else: |
| return self.error_msg(result['message']) |
| else: |
| print self.error_msg("invalid name-in-db (%s)\n" % redirect_target) |
| else: |
| return output |
| |
| return None |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # generate_command_output |
| # |
| @staticmethod |
| def generate_command_output(ret_val): |
| if (isinstance(ret_val, str) or \ |
| isinstance(ret_val, buffer) or \ |
| isinstance(ret_val, bytearray) or \ |
| isinstance(ret_val, unicode)): |
| |
| # yield ret_val |
| if len(ret_val) and ret_val[-1] == '\n': |
| ret_val = ret_val[:-1] |
| for line in ret_val.split('\n'): |
| yield line + '\n' |
| elif ret_val != None: |
| for item in ret_val: |
| yield item |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # generate_line_output |
| # |
| # This is a generator that will generate the output of the |
| # command either as a string, or by iterating over a returned |
| # iterable. This way, a subcommand can return an iterable to |
| # lazily evaluate large amounts of output |
| # |
| def generate_line_output(self, line, dont_ask): |
| while line: |
| subline_index = self.find_with_quoting(line, ';') |
| if subline_index < 0: |
| subline_index = len(line) |
| subline = line[:subline_index] |
| line = line[subline_index+1:] |
| ret_val = self.handle_single_line(subline) |
| cnt = 1 |
| total_cnt = 0 |
| |
| (col_width, screen_length) = self.pp.get_terminal_size() |
| if type(self.length) == int: |
| screen_length = self.length |
| |
| for item in self.generate_command_output(ret_val): |
| if not dont_ask: |
| incr = 1 + (max((len(item.rstrip()) - 1), 0) / col_width) |
| if screen_length and cnt + incr >= screen_length: |
| raw_input('-- hit return to continue, %s) --' % total_cnt) |
| cnt = 0 |
| cnt += incr |
| total_cnt += incr |
| yield item |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # handle_multipart_line |
| # |
| # this is the outermost handler that should print |
| # |
| def handle_multipart_line(self, line): |
| pipe_cmds = None |
| redirect_target = None |
| output = None |
| |
| # pattern is: |
| # single line [; single line]* [| ...] [> {conf|ftp|http}] |
| |
| # first take off the potential redirect part then the pipe cmds |
| redirect_index = self.find_with_quoting(line, '>', True) |
| if redirect_index >= 0: |
| redirect_target = line[redirect_index+1:].strip() |
| line = line[:redirect_index].strip() |
| pipe_index = self.find_with_quoting(line, '|') |
| if pipe_index >= 0: |
| pipe_cmds = line[pipe_index+1:].strip() |
| line = line[:pipe_index].strip() |
| # remaining text is single lines separated by ';' - handle them |
| output = self.generate_line_output(line, pipe_cmds or redirect_target) |
| |
| # now output everything |
| if pipe_cmds or redirect_target: |
| output = self.handle_pipe_and_redirect(pipe_cmds, redirect_target, output) |
| |
| if output != None: |
| for line in output: |
| print line.rstrip() |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # cp_watch |
| # |
| def cp_watch(self, words, text, completion_char): |
| if completion_char == ord('?'): |
| if len(words) > 1: |
| command.do_command_completion_help(words[1:], text) |
| else: |
| self.print_completion_help(self.help_splash([], text)) |
| return |
| if len(words) == 1: |
| items = self.commands_for_current_mode_starting_with(text) |
| return utif.add_delim(items, ' ') |
| else: |
| return command.do_command_completion(words[1:], text) |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # do_watch |
| # only called to display help |
| # |
| def do_watch(self, words): |
| return 'watch: repeat indicated command after watch keyword' |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # handle_watch_command |
| # |
| # handle this here because this is a CLI-only command |
| # LOOK! This could be using curses, but that has some complications with |
| # potentially messing up the terminal. This is cheap but downside |
| # is it uses up the scrollbuffer... |
| # |
| def handle_watch_command(self, line): |
| # |
| words = line.split() |
| if len(words) == 0: |
| return self.syntax_msg('watch: command to watch missing') |
| |
| if len(words) and words[0] == 'watch': |
| return self.error_msg('watch command not supported for watch') |
| |
| while True: # must control-C to get out of this |
| output = self.handle_multipart_line(line) |
| if output: |
| os.system("clear") |
| print "Executing %s " % line |
| print output |
| else: |
| print "Executing %s " % line |
| time.sleep(2) |
| |
| # |
| # |
| # -------------------------------------------------------------------------------- |
| # loop |
| # this is just dispatching the command and handling errors |
| # |
| def loop(self): |
| command.action_invoke('wait-for-controller', (5)) |
| |
| if self.controller: |
| try: |
| version_url = 'http://%s/rest/v1/system/version' % self.controller |
| version = self.rest_simple_request_to_dict(version_url) |
| except Exception, e: |
| version = [{'controller' : 'REST API FAILURE\n'}] |
| |
| print "default controller: %s, %s" % (self.controller, |
| version[0]['controller']) |
| # |
| # vns feature enablement. |
| # when vns is enabled, call a init procedure |
| # |
| if onos == 0: |
| self.netvirt_feature_enabled() |
| |
| while self.run: |
| # Get command line - this will use the command completion above |
| try: |
| #rest_to_model.validate_switch() |
| url_cache.clear_cached_urls() |
| line = raw_input(self.prompt) |
| if self.batch: |
| print line |
| m = re.search('^watch (.*)$', line) |
| if m: |
| self.handle_watch_command(m.group(1)) |
| else: |
| self.handle_multipart_line(line) |
| except KeyboardInterrupt: |
| self.completion_reset() |
| print "\nInterrupt." |
| except EOFError: |
| print "\nExiting." |
| return |
| except urllib2.HTTPError, e: |
| errors = self.rest_error_to_dict(e) |
| print self.error_msg("%s" % self.rest_error_dict_to_message(errors)) |
| except urllib2.URLError, e: |
| print self.error_msg("communicating with REST API server on %s " |
| "- Network error: %s" % |
| (self.controller, e.reason)) |
| except Exception, e: |
| print "\nError running command '%s'.\n" % line |
| if self.debug or self.debug_backtrace: |
| traceback.print_exc() |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # Initialization crazyness to make it work across platforms. Many platforms don't include |
| # GNU readline (e.g. mac os x) and we need to compensate for this |
| |
| try: |
| import readline |
| except ImportError: |
| try: |
| import pyreadline as readline |
| except ImportError: |
| print "Can't find any readline equivalent - aborting." |
| else: |
| if 'libedit' in readline.__doc__: |
| # needed for Mac, please fix Apple |
| readline.parse_and_bind ("bind ^I rl_complete") |
| else: |
| readline.parse_and_bind("tab: complete") |
| readline.parse_and_bind("?: possible-completions") |
| |
| |
| # |
| # -------------------------------------------------------------------------------- |
| # Run the shell |
| |
| def main(): |
| global cli |
| # Uncomment the next two lines to enable remote debugging with PyDev |
| # LOOK! Should have logic here that enables/disables the pydevd stuff |
| # automatically without requiring uncommenting (and, more importantly, |
| # remembering to recomment before committing). |
| # (e.g. checking environment variable or something like that) |
| #python_path = os.environ.get('PYTHONPATH', '') |
| #if 'pydev.debug' in python_path: |
| try: |
| import pydevd |
| pydevd.settrace() |
| except Exception, e: |
| pass |
| |
| # Process '--init' argument since it gates progress to rest of processing |
| # as this is a special argument, it is required to be first argument (if used) |
| check_ready_file = True |
| if len(sys.argv) > 1: |
| if sys.argv[1] == '--init': |
| check_ready_file = False |
| |
| # Do not start CLI till the systemn is ready |
| # (allow user to ^C and get to SDNSh for debug etc) |
| not_ready_file = '/opt/sdnplatform/run/starting' |
| if check_ready_file: |
| try: |
| while True: |
| if os.path.exists(not_ready_file): |
| with open(not_ready_file, "r") as f: |
| message = f.read() |
| if len(message): |
| print message, |
| else: |
| print 'Controller not yet ready ... waiting 5 sec' |
| time.sleep(5) |
| else: |
| break |
| except: |
| if os.path.exists(not_ready_file): |
| resp = raw_input('Controller is not yet ready. Do you still want to continue to the CLI? [n]') |
| if resp and "yes".startswith(resp.lower()): |
| print 'Continuing with CLI despite initialization error ...' |
| else: |
| print 'Aborting Controller CLI login.' |
| time.sleep(1) |
| return |
| |
| # Start CLI |
| cli = SDNSh() |
| cli.init() |
| cli.loop() |
| |
| if __name__ == '__main__': |
| main() |