blob: 0f807ce77c15a1814961a03c43b575110701dc9b [file] [log] [blame]
#!/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()