blob: 71622a25757c6967c2b92c1fc4d46dd8735eac9b [file] [log] [blame]
#
# Copyright (c) 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.
#
import re
import numbers
import collections
import traceback
import types
import json
import time
import sys
import datetime
import os
import subprocess
import socket
import urllib2 # exception, dump_log()
import modi
import error
import command
import run_config
import rest_to_model
import url_cache
from midw import *
from vnsw import *
#from html5lib.constants import DataLossWarning
onos=1
#
# ACTION PROCS
#Format actions for stats per table
def remove_unicodes(actions):
if actions:
#TODO: Check:- Why I have to remove last two character from string
#instead of 1 character to get rid of comma from last aciton
a=''
b=''
newActions=''
isRemoved_u = False
for ch in actions:
if ch =='u':
a= 'u'
if ch =='\'':
b= '\''
if isRemoved_u:
isRemoved_u=False
continue
if (a+b) == 'u\'':
newActions = newActions[:-1]
a= ''
isRemoved_u = True
else:
newActions += ch
return newActions
else:
actions
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)
tunnel_id=None
tunnel_dict={}
def tunnel_create(data=None):
global tunnel_id,tunnel_dict
if sdnsh.description: # description debugging
print "tunnel_create:" , data
if not tunnel_dict:
tunnel_id=data['tunnel-id']
tunnel_dict[tunnel_id]=[]
if sdnsh.description: # description debugging
print "tunnel_create:" , tunnel_id, tunnel_dict
if data.has_key('node-label'):
tunnel_dict[tunnel_id].append(data['node-label'])
if data.has_key('adjacency-label'):
tunnel_dict[tunnel_id].append(data['adjacency-label'])
def tunnel_config_exit():
global tunnel_id,tunnel_dict
if sdnsh.description: # description debugging
print "tunnel_config_exit entered", tunnel_dict
if tunnel_dict:
url_str = ""
entries = tunnel_dict[tunnel_id]
url_str = "http://%s/rest/v1/tunnel/" % (sdnsh.controller)
obj_data = {}
obj_data['tunnel_id']=tunnel_id
obj_data['label_path']=entries
data = sdnsh.store.rest_post_request(url_str,obj_data)
# LOOK! successful stuff should be returned in json too.
if data != "success":
result = json.loads(data)
return result
else:
print "empty command"
#Clear the transit information
tunnel_dict = {}
def tunnel_remove(data=None):
if sdnsh.description: # description debugging
print "tunnel_remove:" , data
tunnel_id=data['tunnel-id']
url_str = "http://%s/rest/v1/tunnel/" % (sdnsh.controller)
obj_data = {}
obj_data['tunnel-id']=data['tunnel_id']
data = sdnsh.store.rest_post_request(url_str,obj_data,'DELETE')
if data != "deleted":
result = json.loads(data)
return result
policy_obj_data = {}
flow = {}
def policy_create(data=None):
global policy_obj_data
if sdnsh.description: # description debugging
print "policy_create:" , data
if data.has_key('policy-id'):
policy_obj_data['policy_id'] = data['policy-id']
policy_obj_data['policy_type'] = data['policy-type']
if data.has_key('src_ip'):
for key in data:
policy_obj_data[key] = data[key]
if data.has_key('priority'):
policy_obj_data['priority'] = data['priority']
if data.has_key('tunnel-id'):
policy_obj_data['tunnel_id'] = data['tunnel-id']
if sdnsh.description: # description debugging
print policy_obj_data
def policy_config_exit():
global policy_obj_data
if sdnsh.description: # description debugging
print "policy_config_exit entered", policy_obj_data
if policy_obj_data:
url_str = "http://%s/rest/v1/policy/" % (sdnsh.controller)
data = sdnsh.store.rest_post_request(url_str,policy_obj_data)
# LOOK! successful stuff should be returned in json too.
if data != "success":
result = json.loads(data)
return result
else:
print "empty command"
#Clear the transit information
policy_obj_data = {}
def policy_remove(data=None):
if sdnsh.description: # description debugging
print "policy_remove:" , data
policy_id=data['policy-id']
url_str = "http://%s/rest/v1/policy/" % (sdnsh.controller)
obj_data = {}
obj_data['policy-id']=data['policy-id']
data = sdnsh.store.rest_post_request(url_str,obj_data,'DELETE')
if data != "deleted":
result = json.loads(data)
return result
def write_fields(obj_type, obj_id, data):
"""
Typical action to update fields of a row in the model
@param obj_type a string, the name of the db table to update
@param obj_id a string, the value of the primary key in for the table
@param data a dict, the name:value pairs of data to update in the table
"""
if sdnsh.description: # description debugging
print "write_fields:", obj_type, obj_id, data
pk_name = mi.pk(obj_type)
if not pk_name:
raise error.CommandDescriptionError("Can't find primary key name for type: %s" % obj_type)
if sdnsh.description: # description debugging
print "write_fields:", obj_type, pk_name, obj_id, data
for fk in mi.obj_type_foreign_keys(obj_type):
if fk in data and mi.is_null_allowed(obj_type, fk):
if data[fk] == 'default': # XXX much too magic, seems an option here would be good
data[fk] = None
result = sdnsh.rest_update_object(obj_type, pk_name, obj_id, data)
check_rest_result(result)
def verify_row_includes(obj_type, pk_value, data, verify):
"""
Intended to raise an exception when a user enters 'no field value',
and the field isn't currently set to value, for example:
'address-space as1 ; no address-space as2', should complain
that the 'address-space' field isn't currently set to 'as2'.
@param obj_type a string, identifies the db table
@param pk_value a string, identifies the value for the primary key
@param data is a dict, collecting the name:value pairs from the description
@verify the string or list of field names to be verified
"""
if sdnsh.description: # description debugging
print "validate_row_includes:", obj_type, pk_value, data, verify
if type(verify) == str:
verify = [verify] # if not a list, make it a list
try:
row = sdnsh.get_object_from_store(obj_type, pk_value)
except Exception, e:
if sdnsh.debug or sdnsh.debug_backtrace:
print 'Failed lookup of %s:%s:%s', (obj_type, pk_value, e)
traceback.print_exc()
raise error.ArgumentValidationError("%s: '%s' doesn't exist" %
(obj_type, pk_value))
return
if sdnsh.description: # description debugging
print "validate_includes: ", row
for field in [x for x in verify if x in data and x in row]:
if row[field] != data[field]:
raise error.ArgumentValidationError("%s: %s found '%s' current value '%s'" %
(obj_type, field, data[field], row[field]))
def reset_fields(obj_type, arg_data,
obj_id = None, fields = None, match_for_no = None):
"""
For an obj_type, revert fields back to their default value.
This is the typical action for 'no' commands.
When verify is set, this is a string or list of fields who's values
must match in the table for the primary key associated with the reset.
This allows command descriptions to identify any fields which need to
be checked against, when they are explicidly named in the 'no' command,
so that 'no XXX value' will verify that 'value' matches the current
row's value before allowing the reset to continue
@param obj_type a string, identifies the db table
@param obj_id a string, identifies the value for the primary key of the row in the table,
possibly unset, the key is looked for in the arg_data in that case.
@param arg_data a dict, collection of name:value pairs from the description
@param fields a list, collection of fields to update in the table
@param match_for_no a string or list, list of fields to check for matched values in arg_data
"""
if obj_type == None:
raise error.CommandDescriptionError("No object to reset (missing obj-type)")
pk_name = mi.pk(obj_type)
# If the fields aren't specified explicitly, then derive from the arg_data
if fields is None:
fields = []
for field in arg_data.keys():
# Only add arguments that correspond to valid fields in the object
if mi.obj_type_has_field(obj_type, field):
if field != pk_name: # don't reset primary keys
fields.append(field)
if len(fields) == 0:
raise error.CommandDescriptionError("No fields to reset: type: %s" % obj_type)
# Get the primary key name
if not pk_name:
raise error.CommandDescriptionError("Can't find primary key name for type: %s" % obj_type)
if obj_id == None:
if pk_name in arg_data:
obj_id = arg_data[pk_name]
elif mi.field_default_value(obj_type, pk_name):
# unusual, but not impossible for singletons
obj_id = mi.field_default_value(obj_type, pk_name)
else:
raise error.CommandDescriptionError("Can't find id value name for type: %s"
" field %s" % (obj_type, pk_name))
if match_for_no:
verify_row_includes(obj_type, obj_id, arg_data, match_for_no)
# Get the default values of the specified field from CLI model info
data = {}
for field in fields:
if field == pk_name:
continue
type_info = mi.cli_model_info.get_field_info(obj_type, field)
if type_info == None:
raise error.CommandDescriptionError("Can't find field details for "
"field %s in type %s" % (field, obj_type))
data[field] = type_info.get('default')
if data[field] == None and type_info.get('type') == 'BooleanField':
data[field] = False
# why does boolean not respect the default in the model?!?
# data[field] = type_info.get('default') if type_info.get('type') != 'BooleanField' else False
if sdnsh.description: # description debugging
print "reset_fields:", obj_type, pk_name, obj_id, data, match_for_no
# Invoke the REST API to set the default values
try:
result = sdnsh.rest_update_object(obj_type, pk_name, obj_id, data)
except Exception, e:
errors = sdnsh.rest_error_to_dict(e, obj_type)
raise error.CommandError('REST', sdnsh.rest_error_dict_to_message(errors))
def obj_type_fields_have_default_value(obj_type, row, data):
"""
Return True when all the fields have a default value,
row is the queried data from the store,
data is the data to be updated.
The goal is to determine whether to delete or update
the row in the store.
"""
ckf = []
if mi.is_compound_key(obj_type, mi.pk(obj_type)):
# XXX primitive compound keys' too?
ckf = mi.compound_key_fields(obj_type, mi.pk(obj_type))
for field in mi.obj_type_fields(obj_type):
if mi.is_primary_key(obj_type, field):
continue
if mi.is_foreign_key(obj_type, field):
# perhaps only allow a single foreign key?
continue
# also any fields which are used to compound the ident.
if field in ckf:
continue
# Needs a better way to identify non-model-fields
if field == 'Idx':
continue
if mi.is_null_allowed(obj_type, field):
# does this need to be more complex?
if field in data and data[field] != None:
return False
continue # next field
default_value = mi.field_default_value(obj_type, field)
if default_value == None:
if sdnsh.description: # description debugging
print 'default_value: no default: %s %s' % (obj_type, field)
return False
# check to see if the updated value would be the default
if field in data and data[field] != default_value:
if sdnsh.description: # description debugging
print 'default_value: not default %s %s %s' % \
(field, data[field], default_value)
return False
elif row.get(field, default_value) != default_value:
if field in data and data[field] == default_value:
if sdnsh.description: # description debugging
print 'default_value: db not default %s %s %s' \
' new value in data %s is default' % \
(field, row[field], default_value, data[field])
continue
if sdnsh.description: # description debugging
print 'default_value: db not default %s %s %s' % \
(field, row[field], default_value)
return False
return True
def update_config(obj_type, obj_id, data, no_command):
"""
update_config is intended to write a row when the described data
is different from the default values of the fields of the row.
When the data described in the call updates the field's values
to all default values, the row associated with the obj_id is
deleted.
This is intended to be used for models which contain configuration
row data, and that every field has a default value,
so that when the config data is transitioned to the default
state, the row is intended to be removed. For these sorts of
command descriptions, updating a field to some default value
may result in the row getting deleted.
"""
c_data = dict(data) # make a local copy
if sdnsh.description: # description debugging
print "update_config: ", obj_type, obj_id, c_data, no_command
if not mi.obj_type_exists(obj_type):
raise error.CommandDescriptionError("Unknown obj-type: %s" % obj_type)
# collect any dict.key names which aren't fields in the object
for unknown_field in [x for x in c_data.keys() if not mi.obj_type_has_field(obj_type, x)]:
del c_data[unknown_field]
# if its a no command, set the value to 'None' if it's allowed,
# of to its default value otherwise
if no_command:
for field in c_data.keys():
if mi.is_null_allowed(obj_type, field):
c_data[field] = None
else:
# required to have a default value
c_data[field] = mi.field_default_value(obj_type, field)
# Get the primary key name
pk_name = mi.pk(obj_type)
if not pk_name:
raise error.CommandDescriptionError("Can't find primary key name for type: %s" % obj_type)
pk_value = obj_id
if pk_name in data:
pk_value = data[pk_name]
if pk_name in c_data:
del c_data[pk_name]
# Query for the row, if it doesn't exist, create the item if any item isn't default
if sdnsh.description: # description debugging
print "update_config: query:", obj_type, pk_value
result = sdnsh.rest_query_objects(obj_type, { pk_name : pk_value })
check_rest_result(result)
if len(result) == 0:
# result[0] -> dictionary of field:value pairs
# check to ensure c_data isn't just default row values
if not obj_type_fields_have_default_value(obj_type, {}, c_data):
if sdnsh.description: # description debugging
print "update_config: create:", obj_type, c_data
# populate the create dictionary
create_dict = dict(c_data)
create_dict[pk_name] = pk_value
result = sdnsh.rest_create_object(obj_type, create_dict)
check_rest_result(result)
else:
if sdnsh.description: # description debugging
print "update_config: no current row"
return
else:
if sdnsh.description: # description debugging
print "update_config: found row", result[0]
if len(result) > 1:
raise error.CommandInternalError("Multiple rows for obj-type: %s: pk %s" %
(obj_type, pk_value))
# See if the complete row needs to be deleted.
# For each of the current fields, if a field's default doesn't exist,
# skip the row delete, or if any field has a non-default value, update
# the requested fields instead of deleting the row.
if obj_type_fields_have_default_value(obj_type, result[0], c_data):
# if the table has foreign keys, check no children refer to this table.
no_foreign_keys_active = True
if obj_type in mi.foreign_key_xref:
for (fk_obj_type, fk_fn) in mi.foreign_key_xref[obj_type][mi.pk(obj_type)]:
try:
rows = sdnsh.get_table_from_store(fk_obj_type, fk_fn,
pk_value, "exact")
except Exception, e:
rows = []
if len(rows):
if sdnsh.description: # description debugging
print "update_config: foreign key active:", \
fk_obj_type, fk_fn, pk_value
no_foreign_keys_active = False
break
if no_foreign_keys_active:
if sdnsh.description: # description debugging
print "update_config: delete:", obj_type, pk_value
try:
delete_result = sdnsh.rest_delete_objects(obj_type, { pk_name : pk_value })
check_rest_result(delete_result)
except Exception, e:
errors = sdnsh.rest_error_to_dict(e)
raise error.CommandInvocationError(sdnsh.rest_error_dict_to_message(errors))
return
# XXX if a row from some table is removed, and that table is using
# foreign keys, then the table which is refered to ought to be
# reviewed, to see if all the entries of the row which this table
# refer's to are default, and if that parent table is a config-style
# table, with all default values for every field, there's a good
# argument that the row ought to be removed.
# See if any of the c_data items in the matching row are different
# (ie: is this update really necessary?)
update_necessary = False
for (name, value) in c_data.items():
if name in result[0]:
if value != result[0][name]:
update_necessary = True
if sdnsh.description: # description debugging
print "update_config: update necessary:", name, result[0][name], value
else:
update_necessary = True
if not update_necessary:
if sdnsh.description: # description debugging
print "update_config: no update needed", obj_type, pk_name, pk_value
return
if sdnsh.description: # description debugging
print "update_config: update:", obj_type, pk_name, pk_value, c_data
# Invoke the REST API to set the default values
result = sdnsh.rest_update_object(obj_type, pk_name, pk_value, c_data)
check_rest_result(result)
def delete_objects(obj_type, data, parent_field=None, parent_id=None):
"""
Delete a row in the table.
@param obj_type a string, the name of the table to update
@param data a dictionary, name:value pairs to describe the delete
@param parent_field a string, the name of a field in the obj_type,
identifying a relationship between this table, and another table
@param parent_id a string, the value of the parent_field, to identify
another row in the other table identified by a field in this table
"""
pk_name = mi.pk(obj_type)
if not pk_name:
raise error.CommandDescriptionError("Can't find primary key name for type: %s" % obj_type)
query_data = dict(data)
if parent_field:
query_data[parent_field] = parent_id
# case conversion
for field in data:
if mi.obj_type_has_field(obj_type, field):
case = mi.get_obj_type_field_case_sensitive(obj_type, field)
if case:
if sdnsh.description: # description debugging
print 'delete_objects: case convert %s:%s to %s' % \
(obj_type, field, case)
data[field] = utif.convert_case(case, data[field])
query_result = sdnsh.rest_query_objects(obj_type, query_data)
check_rest_result(query_result)
#
# if there were no results, try to delete by removing any
# items which have "None" values
if len(query_result) == 0:
for key in query_data.keys():
if query_data[key] == None:
del query_data[key]
query_result = sdnsh.rest_query_objects(obj_type, query_data)
check_rest_result(query_result)
if sdnsh.description: # description debugging
print "delete_objects:", obj_type, query_data
delete_result = sdnsh.rest_delete_objects(obj_type, query_data)
check_rest_result(delete_result)
for item in query_result:
key = item[pk_name]
sdnsh.cascade_delete(obj_type, key)
def set_data(data, key, value):
"""
Action to associate a new name:value pair with 'data', the dictionary used
to pass to REST API's. Allows the action to describe a value for a field
which wasn't directly named in the description.
"""
if sdnsh.description: # description debugging
print "set_data:", data, key, value
data[key] = value
def write_object(obj_type, data, parent_field=None, parent_id=None):
"""
Write a new row into a specific table.
"""
# If we're pushing a config submode with an object, then we need to extend the
# argument data that was entered explicitly in the command with the information
# about the parent object (by default obtained by looking at the obj info on
# the mode stack -- see default arguments for this action when it is added).
if sdnsh.description: # description debugging
print 'write_object: params ', obj_type, data, parent_field, parent_id
data = dict(data) # data is overwriten in various situations below
if parent_field:
if not parent_id:
raise error.CommandDescriptionError('Invalid command description;'
'improperly configured parent info for create-object')
data[parent_field] = parent_id
pk_name = mi.pk(obj_type)
if not pk_name:
raise error.CommandDescriptionError("Can't find primary key name for type: %s" % obj_type)
# look for unpopulated foreign keys.
fks = mi.obj_type_foreign_keys(obj_type)
if fks:
for fk in fks:
(fk_obj, fk_nm) = mi.foreign_key_references(obj_type, fk)
if not fk in data or \
(mi.is_compound_key(fk_obj, fk_nm) and data[fk].find('|') == -1):
# use various techniques to populate the foreign key
# - if the foreign key is for class which has a compound key, see if all the
# parts of the compound key are present
if mi.is_compound_key(fk_obj, fk_nm):
kfs = mi.deep_compound_key_fields(fk_obj, fk_nm)
missing = [x for x in kfs if not x in data]
if len(missing) == 0:
# remove the entries, build the compound key for the foreign key reference
new_value = mi.compound_key_separator(fk_obj, fk_nm).\
join([data[x] for x in kfs])
# verify the foreign key exists, if not complain and return,
# preventing a error during the create request
query_result = sdnsh.rest_query_objects( fk_obj, { fk_nm : new_value })
check_rest_result(query_result)
if len(query_result) == 0:
joinable_name = ["%s: %s" % (x, data[x]) for x in kfs]
raise error.CommandSemanticError("Reference to non-existant object: %s " %
', '.join(joinable_name))
for rfn in kfs: # remove field name
del data[rfn]
data[fk] = new_value
else:
qr = sdnsh.rest_query_objects(fk_obj, data)
if len(qr) == 1:
data[fk] = qr[0][mi.pk(fk_obj)]
if pk_name in data:
if sdnsh.description: # description debugging
print command._line(), 'write_object: query pk_name ', obj_type, pk_name, data
case = mi.get_obj_type_field_case_sensitive(obj_type, pk_name)
if case:
data[pk_name] = utif.convert_case(case, data[pk_name])
query_result = sdnsh.rest_query_objects(obj_type, { pk_name : data[pk_name]})
else:
query_data = dict([[n,v] for (n,v) in data.items() if v != None])
if sdnsh.description: # description debugging
print command._line(), 'write_object: query ', obj_type, query_data
query_result = sdnsh.rest_query_objects(obj_type, query_data)
check_rest_result(query_result)
# Consider checking to see if all the fields listed here
# already match a queried result, if so, no write is needed
if (len(query_result) > 0) and (pk_name in data):
if sdnsh.description: # description debugging
print "write_object: update object", obj_type, pk_name, data
result = sdnsh.rest_update_object(obj_type, pk_name, data[pk_name], data)
else:
if sdnsh.description: # description debugging
print "write_object: create_object", obj_type, data
result = sdnsh.rest_create_object(obj_type, data)
check_rest_result(result)
for item in query_result:
key = item[pk_name]
sdnsh.cascade_delete(obj_type, key)
def delete_object(obj_type, data, parent_field=None, parent_id=None):
global sdnsh
data = dict(data)
if parent_field:
if not parent_id:
raise error.CommandDescriptionError('Invalid command description;'
'improperly configured parent info for delete-object')
data[parent_field] = parent_id
# case conversion
for field in data:
if mi.obj_type_has_field(obj_type, field):
case = mi.get_obj_type_field_case_sensitive(obj_type, field)
if case:
if sdnsh.description: # description debugging
print 'delete_object: case convert %s:%s to %s' % \
(obj_type, field, case)
data[field] = utif.convert_case(case, data[field])
if sdnsh.description: # description debugging
print "delete_object: ", obj_type, data
result = sdnsh.rest_delete_objects(obj_type, data)
check_rest_result(result)
def push_mode_stack(mode_name, obj_type, data, parent_field = None, parent_id = None, create=True):
"""
Push a submode on the config stack.
"""
global sdnsh, modi
# Some few minor validations: enable only in login, config only in enable,
# and additional config modes must also have the same prefix as the
# current mode.
current_mode = sdnsh.current_mode()
if sdnsh.description: # description debugging
print "push_mode: ", mode_name, obj_type, data, parent_field, parent_id
# See if this is a nested submode, or whether some current modes
# need to be popped.
if (mode_name.startswith('config-') and
(not mode_name.startswith(current_mode) or (mode_name == current_mode))):
sdnsh.pop_mode()
current_mode = sdnsh.current_mode()
# pop until it it matches
while not mode_name.startswith(current_mode):
if len(sdnsh.mode_stack) == 0:
raise error.CommandSemanticError('%s not valid within %s mode' %
(mode_name, current_mode))
sdnsh.pop_mode()
current_mode = sdnsh.current_mode()
# if there's a parent id, it is typically the parent, and audit
# ought to be done to verify this
if parent_field:
data = dict(data)
data[parent_field] = sdnsh.get_current_mode_obj()
elif mode_name in ['config', 'enable', 'login']:
# see if the mode is in the stack
if mode_name in [x['mode_name'] for x in sdnsh.mode_stack]:
if sdnsh.description: # description debugging
print 'push_mode: popping stack for', mode_name
current_mode = sdnsh.current_mode()
while current_mode != mode_name:
sdnsh.pop_mode()
current_mode = sdnsh.current_mode()
return
# If we're pushing a config submode with an object, then we need to extend the
# argument data that was entered explicitly in the command with the information
# about the parent object (by default obtained by looking at the obj info on
# the mode stack -- see default arguments for this action when it is added).
elif parent_field:
if not parent_id:
raise error.CommandDescriptionError('Invalid command description; '
'improperly configured parent info for push-mode-stack')
data = dict(data)
data[parent_field] = parent_id
key = None
if obj_type:
for field in data:
if mi.obj_type_has_field(obj_type, field):
case = mi.get_obj_type_field_case_sensitive(obj_type, field)
if case:
if sdnsh.description: # description debugging
print 'push_mode: case convert %s:%s to %s' % \
(obj_type, field, case)
data[field] = utif.convert_case(case, data[field])
# Query for the object both to see if it exists and also to determine
# the pk value we're going to push on the stack. We need to do
# the query in the case where the model uses compound keys and we're
# specifying the individual fields that compose the compound key.
result = sdnsh.rest_query_objects(obj_type, data)
check_rest_result(result)
if len(result) == 0 and create:
#
# For vns-interface, the association of 'rule' with the data dict
# is difficult to explain via the command description. This is
# obviously a poor method of dealing with the issue, but until
# a better one arises (possibly REST api create? possibly
# model validation code?), this solution works.
if obj_type == 'vns-interface':
data = associate_foreign_key_for_vns_interface(data)
# Create the object and re-query to get the id/pk value
# FIXME: Could probably optimize here if the data already
# contains the pk value.
if sdnsh.description: # description debugging
print "push_mode: create ", obj_type, data
result = sdnsh.rest_create_object(obj_type, data)
check_rest_result(result)
result = sdnsh.rest_query_objects(obj_type, data)
check_rest_result(result)
else:
if sdnsh.description: # description debugging
print "push_mode: object found", obj_type, result
# Check (again) to make sure that we have an object
if len(result) == 0:
raise error.CommandSemanticError('Object not found; type = %s' % obj_type)
# Check to make sure there aren't multiple matching objects. If there
# are that would indicate a problem in the command description.
if len(result) > 1:
raise error.CommandDescriptionError('Push mode info must identify a single object;'
'type = %s; data = %s' %
(obj_type, str(data)))
# Get the id/pk value from the object info
pk_name = mi.pk(obj_type)
if not pk_name:
raise error.CommandDescriptionError("Can't find primary key name for type: %s" % obj_type)
key = result[0][pk_name]
else:
pk_name = '<none>'
if sdnsh.description: # description debugging
print "push_mode: ", mode_name, obj_type, pk_name, key
exitCallback = None
if (mode_name == 'config-tunnel'):
exitCallback = tunnel_config_exit
if (mode_name == 'config-policy'):
exitCallback = policy_config_exit
sdnsh.push_mode(mode_name, obj_type, key, exitCallback)
def pop_mode_stack():
global sdnsh
if sdnsh.description: # description debugging
print "pop_mode: "
sdnsh.pop_mode()
def confirm_request(prompt):
global sdnsh
if sdnsh.batch:
return
result = raw_input(prompt)
if result.lower() == 'y' or result.lower() == 'yes':
return
raise error.ArgumentValidationError("Expected y or yes, command: ")
import c_data_handlers
def convert_vns_access_list(obj_type, key, data):
"""
For vns-access-group's, the access list which is the first parameter
needs to be converted into a vns-access-list foreign key. This is
possible since the vns name is part of the current object id.
"""
global sdnsh, modi
key_parts = key.split('|')
if len(key_parts) != 3:
raise error.ArgumentValidationError("invalid id")
if not 'vns-access-list' in data:
raise error.ArgumentValidationError("missing vns-access-list")
try:
key_parts.pop()
vnskey='|'.join(key_parts)
entry = sdnsh.rest_query_objects('vns-access-list',
{ 'vns' : vnskey,
'name' : data['vns-access-list']
})
except Exception, _e:
entry = []
if len(entry) != 1:
raise error.ArgumentValidationError("unknown acl %s" % data['vns-access-list'])
data['vns-access-list'] = entry[0]['id']
def command_query_object(obj_type, data, scoped, sort):
"""
Return model entries (db rows) via the REST API. Try to be
very smart about using parameters and the model definition to
figure out how to query for the entries.
"""
if sdnsh.description:
print 'command_query_object: ', obj_type, data, scoped, sort
skipforeignsearch=False
if (obj_type=='virtualrouter-routingrule' or obj_type=='virtualrouter-interface'):
skipforeignsearch=True
# big_search describes a related search which must be done to
# satisfy this request, see the relationship of tag-mapping to tag
# as an example.
big_search = []
key = mi.pk(obj_type)
#
if mi.is_compound_key(obj_type, key):
if sdnsh.description: # description debugging
print "command_query_object: %s compound %s" % (obj_type, key)
#
# collect compound key names, look for these in the data,
# if any of the values are 'all', remove the item from
# the group of data.
#
# XXX needs work: we ought to check to see if the
# compound key is part of some other key.
#
if scoped:
obj_d = { key : sdnsh.get_current_mode_obj() }
mi.split_compound_into_dict(obj_type, key, obj_d, is_prefix = True)
for (k,v) in obj_d.items():
if k != key and not k in data:
data[k] = v
new_data = {}
dckfs = mi.deep_compound_key_fields(obj_type, key)
if key in data:
mi.split_compound_into_dict(obj_type, key, data, is_prefix = True)
foreign_obj_type_search = {}
for kf in dckfs:
if mi.obj_type_has_field(obj_type, kf) and kf in data and data[kf] != 'all':
new_data[kf] = data[kf]
elif not mi.obj_type_has_field(obj_type, kf):
# deep_compound_keys returns references via foreign keys.
# if the field is missing in obj_type, its likely from
# some related fk.
for fk in mi.obj_type_foreign_keys(obj_type):
(_fk_obj_type, fk_name) = mi.foreign_key_references(obj_type,
fk)
if kf == fk_name:
# print "FOUND MATCH ", kf, _fk_obj_type, fk_name
continue
elif not mi.is_compound_key( _fk_obj_type, fk_name):
continue
for fkcf in mi.compound_key_fields(_fk_obj_type, fk_name):
if fkcf in data and data[fkcf] != 'all':
# assume all models use COMPOUND_KEY_FIELDS
if _fk_obj_type not in foreign_obj_type_search:
foreign_obj_type_search[_fk_obj_type] = {}
foreign_obj_type_search[_fk_obj_type][fkcf] = data[fkcf]
pass
# see if foreign key fields are indirectly named
elif mi.is_foreign_key(obj_type, kf):
(_fk_obj_type, fk_name) = mi.foreign_key_references(obj_type,
kf)
if fk_name in data and data[fk_name] != 'all':
new_data[kf] = data[fk_name]
if (not skipforeignsearch): #skip foreign key search for routingrule type
if len(foreign_obj_type_search):
# This means to collect the entries, a search though a
# related obj_type (through foreign key) will need to be done
# a single query isn't enough, unless all entries are collected
# consider the relationship between tag-mapping and tags
#
# This code seems to handle single indirected foreign key
# lookup, but if deep_compound_key_fields() found more than
# three layers deep (the obj-type has a fk reference to a
# table, which had a fk reference to another table, which
# had a value to search with), this won't do the trick.
# at that point some sort of recursive building of the
# foreign keys would be needed to collect up the required
# final seraches
for (_fk_obj_type, search) in foreign_obj_type_search.items():
fk_entries = sdnsh.rest_query_objects(_fk_obj_type, search)
# need to identify the name associated foreign key in this model
for fk in mi.obj_type_foreign_keys(obj_type):
(fk_obj, fk_name) = mi.foreign_key_references(obj_type, fk)
if fk_obj == _fk_obj_type:
obj_type_field = fk
break
else:
raise error.CommandSemanticError("bigsearch: can't find fk reference"
"for %s for obj-type %s" %
(fk, obj_type))
big_search += [{obj_type_field:
x[mi.pk(_fk_obj_type)]} for x in fk_entries]
# big_search would return id's for the _fk_obj_type,
# which can be used to search this obj_type
# look for fields which are set in new_data, which aren't in data.
for (field, value) in data.items():
if field not in new_data:
if mi.is_marked_searchable(obj_type, field) and value!='all':
new_data[field] = value
data = new_data
else:
# Only allow fields which are searchable (XXX need a prediate)
# only save primary key's and foreigh keys.
new_data = {}
if key in data and mi.is_primary_key(obj_type, key):
new_data[key] = data[key]
for fk in mi.obj_type_foreign_keys(obj_type):
if fk in data:
new_data[fk] = data[fk]
(_fk_obj, fk_fn) = mi.foreign_key_references(obj_type, fk)
if fk_fn in data:
new_data[fk_fn] = data[fk_fn]
for f in mi.obj_type_fields(obj_type):
if f in data and f not in new_data:
new_data[f] = data[f]
data = new_data
if scoped:
data[key] = sdnsh.get_current_mode_obj()
if key in data and (data[key]=='all' or data[key]==None):
del data[key]
#
# Now that the fields have been disassembled as much as possible, see
# if some of the entries need to be cobbled back together.
fks = mi.obj_type_foreign_keys(obj_type)
if sdnsh.description: # description debugging
print "command_query_object: %s foreign-key %s" % (obj_type, fks)
if fks:
for fk in fks:
(fk_obj, fk_nm) = mi.foreign_key_references(obj_type, fk)
if not fk in data or \
(mi.is_compound_key(fk_obj, fk_nm) and data[fk].find('|') == -1):
# use various techniques to populate the foreign key
# - if the foreign key is for class which has a compound key, see if all the
# parts of the compound key are present
if mi.is_compound_key(fk_obj, fk_nm):
kfs = mi.deep_compound_key_fields(fk_obj, fk_nm)
missing = [x for x in kfs if not x in data]
if len(missing) == 0:
# remove the entries, build the compound key for the foreign key reference
new_value = mi.compound_key_separator(fk_obj, fk_nm).\
join([data[x] for x in kfs])
# verify the foreign key exists, if not complain and return,
# preventing a error during the create request
query_result = sdnsh.rest_query_objects( fk_obj, { fk_nm : new_value })
check_rest_result(query_result)
if len(query_result) == 0:
joinable_name = ["%s: %s" % (x, data[x]) for x in kfs]
raise error.CommandSemanticError("Reference to non-existant object: %s " %
', '.join(joinable_name))
for rfn in kfs: # remove field name
del data[rfn]
data[fk] = new_value
if sdnsh.description: # description debugging
print "command_query_object: %s foreign key construction " % obj_type, data
#
# Do something for alias displays, for obj_types which sdnsh says
# are aliases, find the foreign reference in the alias obj_type,
# and use that to determine the field name (fk_fn) in the parent.
# Do lookups based on either the alias field name, or the parent's
# fk_fn when set in data{}
if obj_type in mi.alias_obj_types:
field = mi.alias_obj_type_field(obj_type)
(_fk_obj, fk_fn) = mi.foreign_key_references(obj_type, field)
new_data = {}
if fk_fn in data and data[fk_fn] != 'all':
new_data[field] = data[fk_fn]
elif field in data and data[field] != 'all':
new_data[field] = data[field]
data = new_data
#
# The sort value ought to be a command separated list of fields within the model
#
if sort:
data['orderby'] = sort
if not mi.obj_type_has_model(obj_type):
return rest_to_model.get_model_from_url(obj_type, data)
if sdnsh.description: # description debugging
print "command_query_object: ", obj_type, data
if len(big_search):
entries = []
if sdnsh.description: # description debugging
print "command_query_object: big search", big_search
for bs in big_search:
search = dict(list(bs.items()) + list(data.items()))
entries += sdnsh.rest_query_objects(obj_type, search)
# XXX needs to be re-sorted
return entries
return sdnsh.rest_query_objects(obj_type, data)
def command_display_table_join_entries(obj_type, data, entries, detail):
"""
"""
if obj_type == 'tag-mapping':
# lift persist from the parent tag
if len(entries) == 1:
entry = entries[0]
tag = sdnsh.rest_query_objects('tag', { mi.pk('tag') : entry['tag']})
entry['persist'] = tag[0]['persist']
else:
# key? value? for the _dict?
tags = create_obj_type_dict('tag', mi.pk('tag'))
for entry in entries:
entry['persist'] = tags[entry['tag']][0]['persist']
if obj_type == 'controller-node':
# This is a big odd, since the current node needs to be asked
# which controller node it is
url = "http://%s/rest/v1/system/controller" % sdnsh.controller
result = sdnsh.store.rest_simple_request(url)
check_rest_result(result)
iam = json.loads(result)
cluster_url = ("http://%s/rest/v1/system/ha/clustername"
% sdnsh.controller)
result = sdnsh.store.rest_simple_request(cluster_url)
check_rest_result(result)
# perhaps ought to assert on lenresult) == 1
clustername = json.loads(result)[0]['clustername']
for entry in entries:
controller = None
if entry['id'] == iam['id']:
controller = sdnsh.controller
else:
# find interfaces which hacve a firewall rule open for
# tcp/80. ie: ip for the interface with rest-api role
ips = local_interfaces_firewall_open("tcp", 80, entry)
# controller-interfaces needs to be examined to determine
# if there's an ip address to use to discover the ha-role
if len(ips) == 1:
# Not even certain if this is reachable
if ips[0]['discovered-ip'] != '':
controller = ips[0]['discovered-ip']
elif ips[0]['ip'] != '':
controller = ips[0]['ip']
else:
entry['ha-role'] = 'no-ip'
entry['errors'] = 'No IP Address'
else:
entry['errors'] = 'No IP Address'
if controller == None:
entry['errors'] = 'No ip address configured'
entry['ha-role'] = 'unknown'
continue
try:
url = "http://%s/rest/v1/system/ha/role" % controller
result = sdnsh.store.rest_simple_request(url, timeout = 2)
check_rest_result(result)
ha_role = json.loads(result)
entry['ha-role'] = ha_role['role']
if not 'clustername' in ha_role:
entry['errors'] = 'no clustername in ha-role rest api'
entry['ha-role'] = 'Untrusted: %s' % ha_role['role']
elif ha_role['clustername'] != clustername:
entry['errors'] = 'Not in HA Cluster, requires decomission'
entry['ha-role'] = 'External Cluster: %s' % ha_role['role']
if 'change-date-time' in ha_role:
entry['change-date-time'] = ha_role['change-date-time']
if 'change-description' in ha_role:
entry['change-description'] = ha_role['change-description']
except urllib2.HTTPError, e: # timeout?
entry['errors'] = e.reason
entry['ha-role'] = 'unknown'
continue
except urllib2.URLError, e: # timeout?
entry['errors'] = '%s: %s' % (controller, e.reason)
entry['ha-role'] = 'unknown'
continue # dontt try the uptime, it will fail too
except Exception, e:
entry['errors'] = str(e)
entry['ha-role'] = 'unknown'
url = "http://%s/rest/v1/system/uptime" % controller
try:
result = sdnsh.store.rest_simple_request(url)
check_rest_result(result)
uptime = json.loads(result)
entry['uptime'] = uptime['systemUptimeMsec']
except Exception, e:
pass
return detail
def command_display_table(obj_type, data, detail = 'default',
table_format = None, title = None, scoped = None, sort = None):
"""
Display entries from a obj_type, with some filtering done via data,
and the output format described by table_format, with the devel of detail in detail
@param obj_type string name of the object type
@param data dictionary of configured data items from the description
@param table_format string describing table format to use for output
@param detail string describing the detail-flavor for format
@param scoped string, when not null, indicates the submode level is used to filter query request
@param sort string, describes sort to append to the query request
"""
if not mi.obj_type_exists(obj_type):
raise error.CommandDescriptionError("Unknown obj-type: %s" % obj_type)
if sdnsh.description: # description debugging
print "command_display_table:", obj_type, data, table_format, detail, scoped, sort
if 'detail' in data:
detail = data['detail']
if not table_format:
if 'format' in data:
table_format = data['format']
else:
table_format = obj_type
if 'scoped' in data:
scoped=data['scoped']
del data['scoped']
entries = command_query_object(obj_type, data, scoped, sort)
if sdnsh.description: # description debugging
print "command_display_table: %d entries found, using %s" % (len(entries), data)
# update any of the pretty-printer tables based on the obj_type
obj_type_show_alias_update(obj_type)
# with_key manages whether a 'detail' or table is displayed.
with_key = '<with_key>' if detail == 'details' and len(entries) > 0 else '<no_key>'
# pick foreign keys which are compound keys, explode these into fields
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, True)
#
detail = command_display_table_join_entries(obj_type, data, entries, detail)
# use display_obj_type_rows since it (currently) joins fields for obj_types.
display = sdnsh.display_obj_type_rows(table_format, entries, with_key, detail)
if title:
return title + display
return display
def command_display_rest_join_entries(table_format, data, entries, detail):
"""
@param table_format string, identifying the final table output
@param data dict, used to query the rest api output
@param entries list of dicts, ready to be displayed
@return string replacing detail
"""
if sdnsh.description: # description debugging
print "command_display_rest_join_entries: ", table_format, data, detail
if table_format == 'controller-interface':
# join firewall rules for these interfaces
for intf in entries:
rules = [x['rule'] for x in sdnsh.get_firewall_rules(intf['id'])]
intf['firewall'] = ', '.join(rules)
if table_format == 'system-clock':
# join the 'time' string, possibly remove 'tz' from entries[0]
entries[0]['time'] = sdnsh.get_clock_string(entries[0], data.get('detail'))
return 'details' # force table format
return detail
def command_display_rest_type_converter(table_format, rest_type, data, entries):
"""
the expected display table_format is a list of dictionaries
each dictionary has the field : value pairs. Many rest api's
return a dictionary of different layers, the description
provides a rest-type, which is used to describe the form
of the value returned from the rest api.
"""
if sdnsh.description: # description debugging
print "command_display_rest_type_converter: ", table_format, rest_type
if rest_type.startswith('dict-of-list-of-'):
# entries look like { row_name : [value, ...], ... more-row-value-pairs }
#
# dict-of-list-of: a dict with key's which are given
# the name of the first token, then the dict's value is
# a list which can be given an associated name.
# for example 'dict-of-list-of-cluster-id|[switches]'
#
# 'dict-of-list-of-switch' is a dict with key : value's
# where the value is a list. The member's of the list
# are dictionaries. the key of the outer dict is added to
# each of the dicts, and this interior dict is added to
# the final output list.
# identify the added key from the rest_type
key = rest_type.replace('dict-of-list-of-','')
parts = key.split('|')
names = None
build_list = False
if len(parts) > 0:
key = parts[0]
names = parts[1:] # should only be one name
if len(names) > 0 and names[0][0] == '[':
build_list = True
formatted_list = []
for (row_name, rows) in entries.items():
if not rows:
continue
# use the names as ways of describing each of the list's items
if type(rows) == list and build_list:
# name[0] looks like '[switches]', requesting that this
# list become switches : [rows]
formatted_list.append({key : row_name, names[0][1:-1] : rows})
elif type(rows) == list:
for row in rows:
add_dict = {key : row_name}
if type(row) == str or type(row) == unicode:
add_dict[names[0]] = row
elif type(row) == dict:
# addition names make no difference
add_dict.update(row)
formatted_list.append(add_dict)
elif type(rows) == dict:
do_append = True
new_row = { key : row_name }
for name in [x for x in names.keys() if x in row]:
item = row[name]
if type(item) == str or type(item) == unicode:
new_row[name] = item
if type(item) == dict:
new_row[name].update(item)
if type(item) == list:
do_append = False
for i_row in item:
new_row.update(i_row)
formatted_list.append(new_row)
new_row = { key : row_name }
if do_append:
formatted_list.append(new_row)
entries = formatted_list
elif rest_type.startswith('dict-of-dict-of-'):
# entries looks like { row_name : { [ { }, ... ] } }
# ^
# want this |
# ie: dict with a value which is a dict, whose
# 'dict-of-dict-of-switch|ports' The dict has key : values
# where the value is a dict. That dict has the 'switch' : key
# added, and it becomes the final output dict.
#
# if a second name is included, then the outer dict is
# examined to find these values (ie: values[names]), and these
# get added to the final output dict.
#
# identify the added key from the rest_type
key = rest_type.replace('dict-of-dict-of-','')
parts = key.split('|')
name = None
if len(parts) > 0:
names = parts[1:]
key = parts[0]
formatted_list = []
for (row_name, row) in entries.items():
row[key] = row_name
do_append = False
if names:
new_row = {}
for name in names:
if name in row:
item = row[name]
if type(item) == str or type(item) == unicode:
new_row[name] = item
do_append = True
elif type(item) == dict:
if name == row_name:
do_append = True
elif type(item) == list:
for i_row in item:
row_items = {}
row_items[key] = row_name
row_items.update(i_row)
formatted_list.append(row_items)
if do_append:
formatted_list.append(row)
else:
formatted_list.append(row)
entries = formatted_list
elif rest_type.startswith('dict-with-'):
# rest result looks like: { k : v, k : { } }
# ^
# want this |
# dict-with: typically used for dict returns which have
# nested dict's who's values are promoted to a single
# list with a dict with these values.
#
# identify the added key from the rest_type
key = rest_type.replace('dict-with-','')
names = key.split('|')
collect_row = {}
formatted_list = []
for name in names:
if name in entries:
item = entries[name]
if type(item) == str or type(item) == unicode or \
type(item) == int or type(item) == long: # XXX float?
collect_row[name] = item
elif type(item) == list:
for i_row in item:
row_items = {}
formatted_list.append(i_row)
elif type(item) == dict:
collect_row.update(item)
if len(collect_row) == 0:
entries = formatted_list
else:
entries = [collect_row] + formatted_list
elif rest_type == 'dict':
entries = [entries]
else:
raise error.CommandDescriptionError("Unknown rest-type: %s" % rest_type)
return entries
def missing_part(key_parts, entry, key_case = False):
"""
Return the name of the missing field of one of the strings
in the key_parts list when it doesn't appear in the 'entry' dictionary.
Return None otherwise.
This is used to identify rows which don't have all the
parts needed to constrcut a join key, or a db-table or
query "key" to support addition of two different tables.
@key_parts list of strings,
@entry dictionary, needs to contains each string in key_parts
@key_case True when all key_parts may contain a leading '~' to
denote the field needs to be lower cased for joining
"""
for kn in key_parts:
if not kn in entry:
if key_case == False:
return kn
if kn[0] != '~':
return kn
if kn[1:] not in entry:
return kn[1:]
return None
def case_cvt(fn, f_dict):
"""
For join operations, the fields in the partial result can no longer
be associated with any obj-type, which means its impossible to determine
whether the associated field is case sensitive.
One approach to this problem is to case-normalize the obj-type's
field values when they're first added to the row. That doesn't
help for rest-api's, which means it can only be a partial solution.
In addition, it makes sense to preserve these values when possible,
but still join based on case-normalization.
"""
if fn[0] == '~':
return str(f_dict.get(fn[1:], '').lower())
return str(f_dict.get(fn, ''))
def obj_type_field_case(data, obj_type, field):
"""
For objects where the case-normalization is identifed,
manage conversion of the value associated with the field
"""
case = mi.get_obj_type_field_case_sensitive(obj_type, field)
return str(data[field]) if not case else str(utif.convert_case(case, data[field]))
def add_fields(dest, src):
"""
These should both be dictionaries, leave the original entries in place
when the 'dest' entries are populated from 'src'. This operation is
handy since the original 'dest' entries may differ from the 'src' due
to case normalization. Since having consistent names is a plus, by
not updating the value with the 'src' entries, 'dest' retains its original
values.
"""
for (n,v) in src.items():
if n not in dest:
dest[n] = v
elif str(dest[n]).lower() == str(v).lower:
# should have better controls for when the case matters
if sdnsh.description:
print 'ADD %s skipping updating %s <-> %s' % (n, dest[n], v)
else:
dest[n] = v
def command_query_table(obj_type, data,
clear = True,
key = None, append = None, scoped = None, sort = None, crack = None):
"""
Leave the result in command's global query_result, which can
be used by other c_action steps
'key' is one or more fields which are concatenated together to form
the display-pipeline's version of a primary key. It could be the
actual primary key of the table, or it could be some fields which
appear in all the rows. Once the 'key' is constructed, it used to
determine how results are added to the command.query_result.
If the existing entries are to be 'cleared', then te primary key's
are simply added to the table. When the entries aren't cleared, then
the computed primary key is used to join against existing items.
Finally, the dict field name for the primary key is a single character: '@'
This name was picked since its not possible for the database to ever
use that name.
"""
if not mi.obj_type_exists(obj_type):
raise error.CommandDescriptionError("Unknown obj-type: %s" % obj_type)
if sdnsh.description: # description debugging
print "command_query_table:", obj_type, data, clear, key, append, scoped, sort, crack
if 'scoped' in data:
scoped=data['scoped']
del data['scoped']
result = command_query_object(obj_type, data, scoped, sort)
if sdnsh.description: # description debugging
print "command_query_table: %d entries found, using %s" % \
(len(result), data)
if crack:
if crack == True:
crack = mi.pk(obj_type)
for entry in result:
mi.split_compound_into_dict(obj_type, crack, entry, True)
if append:
for entry in result:
if type(append) == dict:
entry.update(append)
elif type(append) == list:
entry.update(dict(append))
else:
entry[append] = True
# all the field from all the rows need to be present.
if key:
fields = key.split('|')
if clear:
command.query_result = result
if key:
for r in result:
missing = missing_part(fields, r)
if missing:
if sdnsh.description:
print "command_query_table: ' \
' missing field in row %s (%s) " % (missing, obj_type)
continue
r['@'] = '|'.join([obj_type_field_case(r, obj_type, f) for f in fields])
else:
if key == None:
if command.query_resuls != None:
command.query_result += result
else:
command.query_result = result
else:
r_dict = {}
for r in result:
missing = missing_part(fields, r)
if missing:
if sdnsh.description:
print "command_query_table: ' \
' missing field in row %s (%s) " % (missing, obj_type)
continue
pk = '|'.join([r[f] for f in fields])
r_dict[pk] = r
if hasattr(command, 'query_result') and command.query_result:
for qr in command.query_result:
if '@' in qr and qr['@'] in r_dict:
add_fields(qr, r_dict[qr['@']])
del r_dict[qr['@']]
command.query_result += r_dict.values()
else:
for (r, value) in r_dict.items():
value['@'] = '|'.join([value[f] for f in fields])
command.query_result = r_dict.values()
def command_query_rest(data,
url = None, path = None, clear = True,
key = None, rest_type = None, scoped = None, sort = None, append = None):
"""
Leave the result in command's global query_result, which can
be used by other c_action steps (query-table, join-table, join-rest, display)
'key' is one or more fields which are concatenated together to form
the display-pipeline's version of a primary key. It could be the
actual primary key of the table, or it could be some fields which
appear in all the rows. Once the 'key' is constructed, it used to
determine how results are added to the command.query_result.
If the existing entries are to be 'cleared', then te primary key's
are simply added to the table. When the entries aren't cleared, then
the computed primary key is used to join against existing items.
Finally, the dict field name for the primary key is a single character: '@'
This name was picked since its not possible for the database to ever
use that name.
"""
if sdnsh.description: # description debugging
print "command_query_rest:", url, path, rest_type, data, scoped, sort, append
if url == None and path == None:
raise error.CommandDescriptionError("missing url or path")
if path:
schema = sdnsh.sdndb.schema_detail(path)
if schema:
result = sdnsh.sdndb.data_rest_request(path)
if key:
# create a key dictionary, with the key values, pointing to
# a psth in the schema.
pass
print 'PATH', path, result
else:
# if url is a list, pick the first one which can be build from the data
if type(url) == list:
select_url = url
else:
select_url = [url]
use_url = None
for u in select_url:
try:
use_url = (u % data)
break
except:
pass
if use_url == None:
if sdnsh.description: # description debugging
print "command_query_rest: no url found"
return
query_url = "http://%s/rest/v1/" % sdnsh.controller + use_url
if sdnsh.description: # description debugging
print "command_query_rest: query ", query_url
try:
result = sdnsh.store.rest_simple_request(query_url)
check_rest_result(result)
entries = json.loads(result)
except Exception, e:
if sdnsh.description or sdnsh.debug:
print 'command_query_rest: ERROR url %s %s' % (url, e)
entries = []
if entries == None or len(entries) == 0:
if sdnsh.description: # description debugging
print "command_query_rest: no new entries ", query_url
if clear:
command.query_result = None
return
# It certainly seems possible to map from url's to the type associated,
# with the result, but it also makes sense to encode that type information
# into the description
if rest_type:
result = command_display_rest_type_converter(None,
rest_type,
data,
entries)
if sdnsh.description: # description debugging
print "command_query_rest: %s #entries %d " % (url, len(entries))
print result
else:
result = []
if (onos == 1) and (url == 'links'):
for entry in entries:
src = entry.get('src')
dst = entry.get('dst')
for tempEntry in entries:
if cmp(src, tempEntry.get('dst')) == 0:
if cmp(dst, tempEntry.get('src')) == 0:
entries.remove(tempEntry)
result.append({
'src-switch' : entry['src']['dpid'],
'src-port' : entry['src']['portNumber'],
'src-port-state' : 0,
'dst-switch' : entry['dst']['dpid'],
'dst-port' : entry['dst']['portNumber'],
'dst-port-state' : 0,
'type' : entry['type'],
})
else:
result = entries
if append:
for entry in result:
if type(append) == dict:
entry.update(append)
elif type(append) == list:
entry.update(dict(append))
else:
entry[append] = True
if key:
fields = key.split('|')
if clear:
command.query_result = result
if key:
for r in result:
r['@'] = '|'.join([r[f] for f in fields])
else:
if key == None:
if command.query_result != None:
command.query_result += result
else:
command.query_result = result
else:
r_dict = {}
for r in result:
missing = missing_part(fields, r, key_case = True)
if missing:
if sdnsh.description:
print "command_query_rest: missing field %s in row %s" % (missing, r)
continue
pk = '|'.join([case_cvt(f, r) for f in fields])
r_dict[pk] = r
for qr in command.query_result:
if '@' in qr and qr['@'] in r_dict:
add_fields(qr, r_dict[qr['@']])
def command_join_rest(url, data, key, join_field,
add_field = None, rest_type = None, crack = None, url_key = None):
"""
url-key allows single row results to have a name:value added to the
entry in situations where a single dictionary is computed after the
rest-type conversion. this allows simple results from the url to
have a keyword added to allow joins.
"""
if not hasattr(command, 'query_result'):
if sdnsh.description: # description debugging
print "command_join_rest: no entries found"
return
if command.query_result == None:
if sdnsh.description: # description debugging
print "command_join_rest: query_result: None"
return
if sdnsh.description: # description debugging
print "command_join_rest: %d entries found, using %s, url %s" % \
(len(command.query_result), data, url)
print "command_join_rest:", data, key, join_field
if url == None:
return
if join_field == None:
return
if key == None:
return
# Collect all the queries, removing any duplicates
queries = {}
for entry in command.query_result:
# if url is a list, pick the first one which can be build from the data
if type(url) == list:
select_url = url
else:
select_url = [url]
use_url = None
for u in select_url:
try:
use_url = (u % entry)
break
except:
pass
if use_url == None:
if sdnsh.description: # description debugging
print "command_join_rest: no url found", url
continue
query_url = "http://%s/rest/v1/" % sdnsh.controller + use_url
if sdnsh.description: # description debugging
print "command_join_rest: query ", query_url, entry
if query_url in queries:
continue
try:
result = sdnsh.store.rest_simple_request(query_url)
check_rest_result(result)
entries = json.loads(result)
except Exception, e:
entries = []
if entries == None or len(entries) == 0:
continue
# It certainly seems possible to map from url's to the type associated,
# with the result, but it also makes sense to encode that type information
# into the description
if rest_type:
queries[query_url] = command_display_rest_type_converter(None,
rest_type,
data,
entries)
#
# url_key allows the addition of a key for joining for single results
if url_key and len(queries[query_url]) == 1:
queries[query_url][0][url_key] = entry.get(url_key)
if sdnsh.description: # description debugging
print "command_join_rest: %s #entries %d #result %s" % \
(url, len(entries), len(queries[query_url]))
else:
queries[query_url] = entries
# From the query results, generate the dictionary to join through
key_parts = key.split('|') # all the fields needed to make a key
key_dict = {} # resulting key dictionary
for (url, value) in queries.items():
for entry in value:
# see if all the key parts are in the entry
missing = missing_part(key_parts, entry)
if missing:
if sdnsh.description:
print 'command_join_rest: missing field %s in %s' % (missing, entry)
continue
new_key = '|'.join([str(entry[kn]) for kn in key_parts])
if sdnsh.description: # description debugging
print 'command_join_rest: new-key', new_key
key_dict[new_key] = entry
# Using the key-dictinoary, look for matches from the original entries
if add_field:
parts = add_field.split('|')
from_fields = None
if len(parts):
add_field = parts[0]
from_fields = parts[1:]
join_parts = join_field.split('|')
for entry in command.query_result:
if len(join_parts):
missing = missing_part(join_parts, entry, key_case = True)
if missing:
if sdnsh.description: # description debugging
print "command_join_rest: missing field %s in %s" % (missing, entry)
continue
joiner = '|'.join([case_cvt(kn, entry) for kn in join_parts])
else:
if sdnsh.description: # description debugging
print "command_join_rest: joining ", entry, join_field, entry.get(join_field)
if not join_field in entry:
continue
joiner = case_cvt(join_field, entry)
if sdnsh.description: # description debugging
print "command_join_rest: joining ", entry, joiner, key_dict.get(joiner)
if joiner in key_dict:
# add all the entries from the key_dict
if sdnsh.description: # description debugging
print 'command_join_rest: ADD', key_dict[joiner]
if add_field == None:
add_fields(entry, key_dict[joiner])
elif from_fields:
if len(from_fields) == 1:
# add a single field
if from_fields[0] in key_dict[joiner]:
entry[add_field] = key_dict[joiner][from_fields[0]]
else:
# add a dictionary
entry[add_field] = dict([[ff, key_dict[joiner][ff]]
for ff in from_fields])
else:
entry[add_field] = key_dict[joiner]
if sdnsh.description: # description debugging
print "command_join_rest: ", command.query_result
def command_join_table(obj_type, data, key, join_field,
key_value = None, add_field = None, crack = None):
"""
Add fieds to the current command.query_result by looking up the entry in
the db/store. key represents the value of the index to use from
the entries read from the database. The key can be composed of
multiple fields within the entry. The join_field is the name
of the field within the command.query_result to use as the value to match
against the key field.
When key_value is None, the matched entry from the join_field's is
treated as a dictionary, and all the pair of name:values are added
directly to the new entry.
When key_value is a field name, the joined entries are collected
as a list, and added to the new entry a the key_value name.
(see the use of tag-mapping as an example)
"""
if not hasattr(command, 'query_result'):
if sdnsh.description: # description debugging
print "command_join_table: no entries found"
return
if command.query_result == None:
if sdnsh.description: # description debugging
print "command_join_table: query_result: None"
return
if sdnsh.description: # description debugging
print "command_join_table: %d entries found, using %s, obj_type %s %s %s" % \
(len(command.query_result), data, obj_type, key, join_field)
print "command_join_table:", data, key, join_field
if join_field == None:
return
if key == None:
return
if not mi.obj_type_exists(obj_type):
raise error.CommandDescriptionError("Unknown obj-type: %s" % obj_type)
# build the join_dict, which will have keys for the items to
# add into the entries
if not mi.obj_type_has_model(obj_type):
entries = rest_to_model.get_model_from_url(obj_type, data)
else:
entries = sdnsh.get_table_from_store(obj_type)
# determine whether specific field names are added
if add_field:
parts = add_field.split('|')
from_fields = None
if len(parts):
add_field = parts[0]
from_fields = parts[1:]
# constuct the join key for each row from the db table
key_parts = key.split('|') # all the fields needed to make a key
key_dict = {} # resulting key dictionary
for entry in entries:
# see if all the key parts are in the entry
missing = missing_part(key_parts, entry)
if missing:
if sdnsh.description: # description debugging
print "command_join_table: missing field %s in %s" % (missing, entry)
continue
new_key = '|'.join([obj_type_field_case(entry, obj_type, kn) for kn in key_parts])
if sdnsh.description: # description debugging
print 'command_join_table: new-key', new_key, key_value
if key_value:
if not new_key in key_dict:
key_dict[new_key] = [entry]
else:
key_dict[new_key].append(entry)
else:
key_dict[new_key] = entry
# let 'crack' contain the field's name, not a boolean.
if crack and crack == True:
crack = mi.pk(obj_type)
# Using the key-dictinoary, look for matches from the original entries
join_parts = join_field.split('|')
for entry in command.query_result:
if len(join_parts):
missing = missing_part(join_parts, entry, key_case = True)
if missing:
if sdnsh.description: # description debugging
print "command_join_table: missing field %s in %s" % (missing, entry)
continue
joiner = '|'.join([case_cvt(kn, entry) for kn in join_parts])
else:
if sdnsh.description: # description debugging
print "command_join_table: joining ", entry, join_field, entry.get(join_field)
if not join_field in entry:
continue
joiner = case_cvt(join_field, entry)
if joiner in key_dict:
if crack:
if not crack in key_dict[entry[joiner]]:
if sdnsh.description: # description debugging
print "command_join_table: field %s not in entry" % crack, key_dict[joiner]
else:
mi.split_compound_into_dict(obj_type, crack, key_dict[joiner], True)
# add all the entries from the key_dict
if sdnsh.description: # description debugging
print 'command_join_table: ADD %s as %s ' % (key_dict[joiner], add_field)
if add_field == None:
if key_value:
entry[key_value] = key_dict[joiner]
else:
add_fields(entry, key_dict[joiner])
elif from_fields:
if len(from_fields) == 1:
# add a single field
if type(key_dict[joiner]) == list:
entry[add_field] = [x[from_fields[0]] for x in key_dict[joiner]]
else:
entry[add_field] = key_dict[joiner][from_fields[0]]
else:
# add a dictionary with named fields
if type(key_dict[joiner]) == list:
for item in key_dict[joiner]:
entry[add_field] = dict([[ff, item[ff]]
for ff in from_fields])
else:
entry[add_field] = dict([[ff, key_dict[joiner][ff]]
for ff in from_fields])
else:
entry[add_field] = key_dict[joiner]
if sdnsh.description: # description debugging
print "command_join_table: ", command.query_result
def command_display_rest(data, url = None, sort = None, rest_type = None,
table_format = None, title = None, detail = None):
"""
Perform a call to the rest api, and format the result.
When sort isn't None, it names a field whose's value are sorted on.
"""
if sdnsh.description: # description debugging
print "command_display_rest: ", data, url, rest_type, table_format, detail
if not url:
url = data.get('url')
if not table_format:
table_format = data.get('format')
check_single_entry = True
# if url is a list, pick the first one which can be build from the data
select_url = url
if url and type(url) == list:
for u in url:
try:
select_url = (u % data)
select_url = u # select this url from the list
break
except:
pass
if not detail:
detail = data.get('detail', 'default')
url = "http://%s/rest/v1/" % sdnsh.controller + (select_url % data)
result = sdnsh.store.rest_simple_request(url)
check_rest_result(result)
if sdnsh.description: # description debugging
print "command_display_rest: result ", result
entries = json.loads(result)
#rest_type = None
#raise error.ArgumentValidationError('\n\n\n %s' % (attributes))
#if 'realtimestats' in data and data['realtimestats'] == 'group':
entries2 = None
if 'realtimestats' in data and data['realtimestats'] == 'group':
url2 = "http://%s/rest/v1/" % sdnsh.controller + ("realtimestats/groupdesc/%(dpid)s/" % data)
result2 = sdnsh.store.rest_simple_request(url2)
check_rest_result(result2)
if sdnsh.description: # description debugging
print "command_display_rest: groupdesc result ", result2
entries2 = json.loads(result2)
# It certainly seems possible to map from url's to the type associated,
# with the result, but it also makes sense to encode that type information
# into the description
if rest_type:
entries = command_display_rest_type_converter(table_format,
rest_type,
data,
entries)
if 'realtimestats' in data and data['realtimestats'] == 'group':
if entries2 is not None:
entries2 = command_display_rest_type_converter(table_format,
rest_type,
data,
entries2)
if 'router' in data and data['router'] == 'router':
combResult = []
for entry in entries:
attributes = entry.get('stringAttributes')
#raise error.ArgumentValidationError('\n\n\n %s' % (attributes))
combResult.append({
'dpid' : entry.get('dpid'),
'routerIP' : attributes['routerIp'],
'name' : attributes['name'],
'isEdgeRouter' : attributes['isEdgeRouter'],
'routerMac' : attributes['routerMac'],
'nodeSId' : attributes['nodeSid'],
},)
entries = combResult
#raise error.ArgumentValidationError('\n\n\n %s' % (entries))
if 'routerrealtimestats' in data and data['routerrealtimestats'] == 'port':
#raise error.ArgumentValidationError('\n\n\n %s' % (data))
combResult = []
portList = entries
for port in portList:
portData = port.get("port")
name = portData.get("stringAttributes").get('name')
portNo = portData.get("portNumber")
subnetIp = port.get("subnetIp")
combResult.append({
'name' :name,
'portNo' : portNo,
'subnetIp' : subnetIp,
})
entries = combResult
#raise error.ArgumentValidationError('\n\n\n %s' % (data))
if 'showtunnel' in data and (data['showtunnel'] == 'tunnel' or data['detail'] == 'details'):
#eraise error.ArgumentValidationError('\n\n\n %s' % (entries))
combResult = []
tunnelList = entries
for tunnel in tunnelList:
labelStack = str(tunnel.get('labelStack'))
labelStack = remove_unicodes(labelStack)
tunnelId = tunnel.get('tunnelId')
dpidGroup = str(tunnel.get('dpidGroup'))
dpidGroup= remove_unicodes(dpidGroup)
policies = tunnel.get('policies')
#nodes = ''
#for node in tunnel.get("nodes"):
# nodes += (node.get('value') + ',')
#if nodes != '':
# nodes = nodes[:-1]
combResult.append({
'tunnelId' : tunnelId,
#'nodes' : nodes,
'labelStack' :labelStack,
'dpidGroup' :dpidGroup,
'policies' : policies,
})
entries = combResult
if 'showpolicy' in data and data['showpolicy'] == 'policy':
#raise error.ArgumentValidationError('\n\n\n %s' % (data))
combResult = []
portList = entries
for policy in portList:
policyId = policy.get("policyId")
policyType = policy.get("policyType")
priority = policy.get("priority")
match = policy.get("match")
dstIpAddress = match.get('dstIpAddress')['value'] if match.get('dstIpAddress') else '*'
dstMacAddress = match.get('dstMacAddress')['value'] if match.get('dstMacAddress') else '*'
dstTcpPortNumber = match.get('dstTcpPortNumber') if match.get('dstTcpPortNumber') else '*'
etherType = match.get('etherType') if match.get('etherType') else '*'
ipProtocolNumber = match.get('ipProtocolNumber') if match.get('ipProtocolNumber') else '*'
srcIpAddress = match.get('srcIpAddress')['value'] if match.get('srcIpAddress') else '*'
srcMacAddress = match.get('srcMacAddress')['value'] if match.get('srcMacAddress') else '*'
srcTcpPortNumber = match.get('srcTcpPortNumber') if match.get('srcTcpPortNumber') else '*'
combResult.append({
'policyId' : policyId,
'policyType' : policyType,
'priority' : priority,
'dstIpAddress' : dstIpAddress,
'dstMacAddress' : dstMacAddress,
'dstTcpPortNumber': dstTcpPortNumber,
'etherType' : etherType,
'ipProtocolNumber': ipProtocolNumber,
'srcIpAddress' : srcIpAddress,
'srcMacAddress' : srcMacAddress,
'srcTcpPortNumber': srcTcpPortNumber,
})
entries = combResult
if 'realtimestats' in data and 'tabletype' in data and data['realtimestats'] == 'table':
combResult = []
if data['tabletype'] == 'ip':
for ipTableEntry in entries:
match = ipTableEntry['match']
networkDestination = '*'
if match :
networkDestination = match.get('networkDestination') if match.get('networkDestination') else '*'
#raise error.ArgumentValidationError('\n\n\n %s' % json.tool(entries))
instructions = ipTableEntry['instructions']
actions = str(instructions[0]) if instructions[0] else None
actions = remove_unicodes(actions)
combResult.append({
'switch' : ipTableEntry['switch'],
'byteCount' : ipTableEntry['byteCount'],
'packetCount' : ipTableEntry['packetCount'],
'priority' : ipTableEntry['priority'],
'cookie' : ipTableEntry['cookie'],
'durationSeconds' : ipTableEntry['durationSec'],
'networkDestination' : networkDestination,
'actions' : actions,
})
elif data['tabletype'] == 'mpls':
for ipTableEntry in entries:
match = ipTableEntry['match']
mplsTc = '*'
mplsLabel = '*'
mplsBos = '*'
if match :
mplsTc = match.get('mplsTc') if match.get('mplsTc') else '*'
mplsLabel = match.get('mplsLabel') if match.get('mplsLabel') else '*'
mplsBos = match.get('mplsBos') if match.get('mplsBos') else '*'
instructions = ipTableEntry['instructions']
#raise error.ArgumentValidationError('\n\n\n %s' %len(actions))
actions = str(instructions[0])if instructions[0] else None
actions = remove_unicodes(actions)
combResult.append({
'switch' : ipTableEntry['switch'],
'byteCount' : ipTableEntry['byteCount'],
'packetCount' : ipTableEntry['packetCount'],
'cookie' : ipTableEntry['cookie'],
'priority' : ipTableEntry['priority'],
'mplsTc' : mplsTc,
'mplsLabel' : mplsLabel,
'mplsBos' : mplsBos,
'durationSeconds' : ipTableEntry['durationSec'],
'actions' : actions
})
elif data['tabletype'] == 'acl':
for ipTableEntry in entries:
match = ipTableEntry['match']
networkDestination ='*'
networkProtocol = '*'
networkSource = '*'
mplsTc = '*'
mplsLabel = '*'
mplsBos = '*'
transportDestination = '*'
inputPort = '*'
transportSource = '*'
dataLayerSource = '*'
dataLayerDestination = '*'
dataLayerType = '*'
if match :
networkDestination = match.get('networkDestination') if match.get('networkDestination') else '*'
networkProtocol = match.get('networkProtocol') if match.get('networkProtocol') else '*'
networkSource = match.get('networkSource') if match.get('networkSource') else '*'
mplsTc = match.get('mplsTc') if match.get('mplsTc') else '*'
mplsLabel = match.get('mplsLabel')if match.get('mplsLabel') else '*'
transportDestination = match.get('transportDestination') if match.get('transportDestination') else '*'
transportSource = match.get('transportSource') if match.get('transportSource') else '*'
inputPort = match.get('inputPort') if match.get('inputPort') else '*'
dataLayerSource = match.get('dataLayerSource') if match.get('dataLayerSource') else '*'
dataLayerDestination = match.get('dataLayerDestination') if match.get('dataLayerDestination') else '*'
dataLayerType= match.get('dataLayerType') if match.get('dataLayerType') else '*'
mplsBos = match.get('mplsBos') if match.get('mplsBos') else '*'
instructions = ipTableEntry['instructions']
actions = str(instructions[0])if instructions[0] else None
actions = remove_unicodes(actions)
combResult.append({
'switch' : ipTableEntry['switch'],
'byteCount' : ipTableEntry['byteCount'],
'packetCount' : ipTableEntry['packetCount'],
'cookie' : ipTableEntry['cookie'],
'priority' : ipTableEntry['priority'],
'inputPort' : inputPort,
'durationSeconds' : ipTableEntry['durationSec'],
'networkSource' : networkSource,
'networkDestination' : networkDestination,
'networkProtocol' : networkProtocol,
'dataLayerType' : dataLayerType,
'dataLayerSource' : dataLayerSource,
'dataLayerDestination' : dataLayerDestination,
'mplsTc' : mplsTc,
'mplsLabel' : mplsLabel,
'mplsBos' : mplsBos,
'transportDestination' : transportDestination,
'transportSource' : transportSource,
'actions' : actions
})
entries = combResult
if 'realtimestats' in data and data['realtimestats'] == 'group':
combResult = []
for groupStatEntry in entries:
groupId = groupStatEntry["groupId"]
groupDescEntry = None
for entry in entries2:
if groupId == entry["groupId"]:
groupDescEntry = entry
break
if groupDescEntry is None:
print "command_display_rest: missing group desc for group id %s" % (groupId)
continue
for bucketId in range(len(groupStatEntry['bucketStats'])):
setsrcmac = '*'
if 'SET_DL_SRC' in groupDescEntry['bucketsActions'][bucketId]:
setsrcmac = groupDescEntry['bucketsActions'][bucketId]['SET_DL_SRC']
setdstmac = '*'
if 'SET_DL_DST' in groupDescEntry['bucketsActions'][bucketId]:
setdstmac = groupDescEntry['bucketsActions'][bucketId]['SET_DL_DST']
pushmpls = '*'
if 'PUSH_MPLS_LABEL' in groupDescEntry['bucketsActions'][bucketId]:
pushmpls = groupDescEntry['bucketsActions'][bucketId]['PUSH_MPLS_LABEL']
popmpls = '*'
if 'POP_MPLS' in groupDescEntry['bucketsActions'][bucketId]:
popmpls = groupDescEntry['bucketsActions'][bucketId]['POP_MPLS']
outport = '*'
if 'OUTPPUT' in groupDescEntry['bucketsActions'][bucketId]:
outport = groupDescEntry['bucketsActions'][bucketId]['OUTPPUT']
goToGroup = '*'
if 'goToGroup' in groupDescEntry['bucketsActions'][bucketId]:
goToGroup = groupDescEntry['bucketsActions'][bucketId]['goToGroup']
setBos= None
if 'PUSH_MPLS_BOS' in groupDescEntry['bucketsActions'][bucketId]:
setBos = groupDescEntry['bucketsActions'][bucketId]['PUSH_MPLS_BOS']
combResult.append({
'groupid' : groupId,
'grouptype' : groupDescEntry['groupType'],
'totalpktcnt' : groupStatEntry['packetCount'],
'totalbytecnt' : groupStatEntry['byteCount'],
'bucketpktcnt' : groupStatEntry['bucketStats'][bucketId]['pktCount'],
'bucketbytecnt' : groupStatEntry['bucketStats'][bucketId]['byteCount'],
'setsrcmac' : setsrcmac,
'setdstmac' : setdstmac,
'pushMplsLabel' : pushmpls,
'popmpls' : popmpls,
'outport' : outport,
'goToGroup' : goToGroup,
'setBos' : setBos,
})
entries = combResult
#
if format:
#
detail = command_display_rest_join_entries(table_format, data, entries, detail)
#if 'realtimestats' in data and data['realtimestats'] == 'flow':
# entries = sdnsh.fix_realtime_flows(entries)
# check_single_entry = False
if 'realtimestats' in data and data['realtimestats'] == 'features':
for entry in entries:
entry['stp-state'] = entry['state']
# update any of the pretty-printer tables based on the table_format (obj_type)
obj_type_show_alias_update(table_format % data)
if check_single_entry and entries and len(entries) == 1 and detail == 'details':
return sdnsh.pp.format_entry(entries[0],
table_format % data,
detail,
sdnsh.debug)
#
if sort:
def sort_cmp(x,y):
for f in sort:
if f in x:
c = cmp(x.get(f), y.get(f))
if c != 0:
return c
return 0
entries = sorted(entries, cmp=sort_cmp)
display = sdnsh.pp.format_table(entries, table_format % data, detail)
else:
display = entries
if title:
return title + display
return display
def command_crack(field):
"""
Part of the show pipeline, split is typically used with rest api's
not associated with the model (database), since the cli has enough
details of the relationships between model fields to understand
which of the fields has a compound key value, and has options to
crack those into component parts.
The operation is called 'crack' (not split), since the other
options for some of the actions is called 'crack'
The field identifies the name of the field in the entry to
split into parts, and the remaining '|' separated fields list
the labels to associate in the result from each of the
split components. Currently, the 'crack' character is '|',
although this could be parameterized.
"""
if sdnsh.description: # description debugging
print "command_split: ", field
if hasattr(command, 'query_result'):
entries = command.query_result
if command.query_result == None:
entries = []
else:
if sdnsh.description: # description debugging
print "command_join_table: no entries found"
entries = []
parts = field.split('|')
if len(parts) == 0:
if sdnsh.description: # description debugging
print "command_join_table: field doesn't contain labels" \
" use field|label1|label2|..."
return
field = parts[0]
label = parts[1:]
many = len(label)
for entry in entries:
if field in entry:
parts = entry[field].split('|')
if len(parts) and many >= len(parts) :
# use enumerate to create a tuple for each item in parts,
# assocaiting an index, which can be used to identify the
# label to use for each of the elements; from that create
# a dictionay, which is then used to update the entry
entry.update(dict([[label[n],p] for (n,p) in enumerate(parts)]))
def command_display(data, table_format, detail = 'default', sort = None, title = None):
if sdnsh.description: # description debugging
print "command_display: ", data, table_format, detail
if 'detail' in data:
detail = data['detail']
if hasattr(command, 'query_result'):
entries = command.query_result
if command.query_result == None:
entries = []
else:
if sdnsh.description: # description debugging
print "command_join_table: no entries found"
entries = []
if sdnsh.description: # description debugging
print "command_display: #entries ", len(entries)
# XXX controller-node has an odd url, join-rest needs to be able to
# be handed a complete url, and replace the ip address with the controller's
# ip address.
detail = command_display_table_join_entries(table_format, data, entries, detail)
# update any of the pretty-printer tables based on the table_format (obj_type)
obj_type_show_alias_update(table_format)
# with_key manages whether a 'detail' or table is displayed.
with_key = '<with_key>' if detail == 'details' and len(entries) > 0 else '<no_key>'
#
if sort:
def sort_cmp(x,y):
for f in sort:
if f in x:
c = utif.trailing_integer_cmp(x.get(f),y.get(f))
if c:
return c
return 0
entries = sorted(entries, cmp=sort_cmp)
# use display_obj_type_rows since it (currently) joins fields for obj_types.
display = sdnsh.display_obj_type_rows(table_format, entries, with_key, detail)
if title:
return title + display
return display
def command_legacy_cli(obj_type, data, detail = 'default', scoped = None, sort = None):
"""
Unfortunatly, the command descriptions don't have enough different
detail to describe how to join specific distinct fields. In the future,
there will be rest api's for each of the cli requests; that should cause
this trampoline code to become obsolete.
"""
if sdnsh.description: # description debugging
print "command_legacy_cli: ", obj_type, data, detail, scoped, sort
# update any of the pretty-printer tables based on the obj_type
obj_type_show_alias_update(obj_type)
#
#
# Various show command 'join' data to create a table not
# directly available in the REST API, someday in the future,
# these joins will be directly implemented in the REST API,
# but these special cases still exist:
#
if 'running-config' in data:
result = sdnsh.error_msg("No running-config choice")
words = []
if 'word' in data and data['word'] != 'all':
words = [data['word']]
if data['running-config'] == 'running-config':
# 'show vns XXX running-config'
if 'vnsname' in data and data['vnsname'] != 'all':
return sdnsh.show_vns_running_config(data['vnsname'],data['tenant'])
elif 'vns' in data and data['vns']=='all':
data['running-config'] = 'vns'
elif 'tenant' in data:
data['running-config']='tenant'
words=[data['tenant']]
if data['running-config'] in run_config.registry_items_enabled():
result = run_config.perform_running_config(data['running-config'], sdnsh, config, words)
if result:
return result
return ''.join(config)
if obj_type == 'running-config':
return run_config.implement_show_running_config([])
if obj_type == 'vns-interface':
if scoped:
# should check for missing 'id' in data
data['vns'] = sdnsh.get_current_mode_obj()
if 'vns' in data:
if data['vns'] == 'all':
return sdnsh.display_vns_interface(None, {}, '<no_key>')
vns_name=data['vns']
return sdnsh.display_vns_interface(vns_name, {'vns': vns_name },
'<no_key>', detail = 'scoped')
if obj_type == 'vns-switch-ports':
if 'vns' in data:
return sdnsh.show_vns_switch_ports([data['vns']])
return sdnsh.show_vns_switch_ports([])
if obj_type == 'switch-ports-vns':
if 'dpid' in data:
return sdnsh.show_switch_ports_vns([data['dpid']])
return sdnsh.show_switch_ports_vns([])
if obj_type == 'switch-interfaces':
key = mi.pk(obj_type)
if scoped:
data['dpid'] = sdnsh.get_current_mode_obj()
# in legacy_cli to join the switch-interfaces with port stats
port_obj = 'port'
entries = sdnsh.show_sort_obj_type(obj_type,
command_query_object(port_obj, data, scoped, sort))
# switch-interfaces is really class Port, and the primary key
# is '#|switch|number, not name.
entries_dict = dict([['%s|%s' % (x['switch'], x['name']), x] for x in entries])
# collect switch-interface-config
sic = 'switch-interface-config'
if 'dpid' in data and data['dpid'] != 'all':
sic_dict = create_obj_type_dict(sic, mi.pk(sic), mi.pk(sic), data['dpid'])
else:
sic_dict = create_obj_type_dict(sic, mi.pk(sic))
# add switch-interface-config names when missing
for (sic_id, sic_value) in sic_dict.items():
if not sic_id in entries_dict:
# add 'state' to this item for prettyprinting column width computation
for sv in sic_value:
sv['state'] = ''
entries += sic_value
# collect the stats for the interfaces
stats_url = 'realtimestats/port/%(dpid)s/' % data
url = "http://%s/rest/v1/" % sdnsh.controller + stats_url
try:
result = sdnsh.store.rest_simple_request(url)
check_rest_result(result)
stats = json.loads(result)
except Exception, e:
stats = {}
# join realtimestats
for entry in entries:
if 'state' in entry:
entry['stp-state'] = entry['state']
stats_list = stats.get(entry['switch'])
# Note, 'number' may be missing from entry if the switch
# matches for switch-interface-config but the interface name
# doesn't show up.
if stats_list and 'number' in entry:
ifn = entry['number']
# Notice that the realtime stat's use a int for the 2^16 value here
# The & 0xffff converts the "-x" to a positive 2^16 value
item = [x for x in stats_list if (x['portNumber'] & 0xffff) == ifn]
if len(item) == 1:
entry.update(item[0])
if entry['id'] in sic_dict:
entry.update(sic_dict[entry['id']][0])
# Update the alias mappings for display
obj_type_show_alias_update(obj_type)
return sdnsh.pp.format_table(entries, obj_type, detail)
if obj_type == 'tunnel-interfaces':
# Use the active tunnels to identify the interfaces on the
# switches which are the tunneling interfaces, with that
# collect to port -> if_name mappings from 'port', then
# find all the switches interfaces, convert those port numbers to
# if names, to collect only tunneling interfaces. Now collect
# realtimestats for the switch's ports, and associate those
# stats with any filtered interfaces, finally display the result
tunnel_url = "tunnel-manager/%(dpid)s" % data
url = "http://%s/rest/v1/" % sdnsh.controller + tunnel_url
result = sdnsh.store.rest_simple_request(url)
check_rest_result(result)
tunnels = json.loads(result)
# use the active tunnels to
# collect dpid's, convert the remote ip's to interface names.
tunnel_ports = {}
for t in tunnels:
quad = t['tunnelPorts'].split('.')
if_name = "vta%03d%03d%03d%03d" % (int(quad[0]), int(quad[1]),
int(quad[2]), int(quad[3]))
key = "%s|%s" % (t['dpid'], if_name)
if not key in tunnel_ports:
tunnel_ports[key] = {t['dpid']: t['tunnelPorts']}
# Collect interfaces on associated switch
port_obj = 'port'
entries = sdnsh.show_sort_obj_type(port_obj,
command_query_object(port_obj, data, scoped, sort))
# Associate port names with interface names
port_to_if_name = {}
try:
ports = sdnsh.get_table_from_store("port")
except Exception, e:
port = []
for port in ports:
key_string = '%s|%s' % (port['switch'], port['number'])
port_to_if_name[key_string] = port['name']
# Filter elements, 'filtered' only contains active tunnel interfaces
filtered = []
for e in entries:
e['ifname'] = port_to_if_name[e['id']]
key = '%s|%s' % (e['switch'], e['ifname'])
if sdnsh.description: # description debugging
print command._line(), key
if key in tunnel_ports:
if sdnsh.description: # description debugging
print command._line(), "Found ", e['id']
filtered.append(e)
entries = filtered
# collect switch-interface-config
sic = 'switch-interface-config'
if 'dpid' in data:
sic_dict = create_obj_type_dict(sic, mi.pk(sic), mi.pk(sic), data['dpid'])
else:
sic_dict = create_obj_type_dict(sic, mi.pk(sic))
# collect the stats for the interfaces
stats_url = 'realtimestats/port/%(dpid)s/' % data
url = "http://%s/rest/v1/" % sdnsh.controller + stats_url
try:
result = sdnsh.store.rest_simple_request(url)
check_rest_result(result)
stats = json.loads(result)
except Exception, e:
stats = {}
# join realtimestats
for entry in entries:
if 'state' in entry:
entry['stp-state'] = entry['state']
stats_list = stats.get(entry['switch'])
if stats_list and 'number' in entry:
ifn = entry['number']
# Notice that the realtime stat's use a int for the 2^16 value here
# The & 0xffff converts the "-x" to a positive 2^16 value
item = [x for x in stats_list if (x['portNumber'] & 0xffff) == ifn]
if len(item) == 1:
entry.update(item[0])
if entry['id'] in sic_dict:
entry.update(sic_dict[entry['id']][0])
obj_type_show_alias_update('switch-interfaces')
return sdnsh.pp.format_table(entries, 'switch-interfaces', detail)
if obj_type == 'host-vns-interface-vns':
words = []
for w in []: # list of options to display_vns_mac_address_table
if w in data:
words[w] = data[w]
return sdnsh.display_vns_mac_address_table(data['vns'], words)
if obj_type == 'config':
if 'config' in data:
if 'version' in data:
return sdnsh.implement_show_config([data['config'],data['version']])
return sdnsh.implement_show_config([data['config']])
if 'config-diff' in data:
if 'version' in data:
return sdnsh.implement_show_config([ data['first'],
'diff',
data['second'],
data['version']])
return sdnsh.implement_show_config([data['first'],
'diff',
data['second'], ])
return sdnsh.implement_show_config([])
if obj_type == 'vns-flow':
if 'detail' in data:
return sdnsh.show_vns_flow_annotated([data['vns'],
'flow',
data['detail']])
return sdnsh.show_vns_flow_annotated([data['vns'], 'flow'])
if obj_type == 'tech-support':
return sdnsh.do_show_tech_support([])
if obj_type == 'config-file':
if 'file' in data:
return sdnsh.implement_show_config_file(['config-file', data['config']])
return sdnsh.implement_show_config_file(['config-file', ])
if obj_type == 'logging':
if 'log-name' in data:
return sdnsh.implement_show_logging([data['log-name']])
return sdnsh.implement_show_logging([])
if obj_type == 'event-history':
if 'count' in data:
return sdnsh.do_show_event_history([data['event'],
'last',
str(data['count'])])
return sdnsh.do_show_event_history([data['event']])
if obj_type == 'flow-cache':
words = []
if 'counters' in data:
words.append('counters')
elif 'application' in data:
words.append('app')
words.append(data['application'])
words.append('app-instance')
words.append(data['instance'])
return sdnsh.do_show_flow_cache(words)
if obj_type in ['controller-stats', 'switch-stats']:
#
# data['id'] is the name of the controller
helper_item = obj_type.replace('-stats','')
if helper_item == 'controller':
helper_item = 'controller-node'
key = mi.pk(helper_item)
words = [helper_item, data[key], 'stats']
if 'stats-type' in data:
words.append(data['stats-type'])
for (n,v) in data.items():
if not n in [key, 'stats', 'stats-type']:
words.append(n)
words.append(v)
return sdnsh.helper_show_object_stats(words)
if obj_type == 'switch-tcpdump':
words = ['trace', data['dpid']]
for (n,v) in data.items():
if not n in ['tcpdump', 'dpid']:
words.append(n)
return sdnsh.do_trace(words)
if obj_type == 'copy':
words = [data['source']]
if 'dest' in data:
words.append(data['dest'])
return sdnsh.implement_copy(words)
if obj_type == 'write':
return sdnsh.implement_write([data['target']])
if obj_type == 'this':
obj_type = sdnsh.get_current_mode_obj_type()
show_this = mi.obj_type_show_this(obj_type)
if not show_this:
return sdnsh.do_show_object(['this'])
result = []
for show in show_this:
if type(show) is list and len(show) >= 3:
# [ object, format, detail ]
if len(result) > 0:
result.append(mi.obj_type_show_title(show[0]))
sort = None
if len(show) > 3:
sort = show[3]
result.append(command_display_table(show[0], {},
table_format = show[1],
detail = show[2],
sort = sort,
scoped = True))
elif type(show) is list and len(show) == 2:
# [ object, detail ]
if len(result) > 0:
result.append(mi.obj_type_show_title(show[0]))
result.append(command_display_table(show[0], {}, detail = show[1], scoped = True))
else:
result.append(sdnsh.do_show_object([show]))
return '\n'.join(result)
if obj_type == 'version':
return sdnsh.do_show_version([])
if obj_type == 'reload':
return sdnsh.implement_reload()
if obj_type == 'test-command':
if data['test-type'] == 'packet-in':
return sdnsh.implement_test_packet_in(data)
if data['test-type'] == 'path':
return sdnsh.implement_test_path(data)
print 'command_legacy_cli: obj-type unknown: ', obj_type
def command_legacy_cli_no(obj_type, data, detail = 'default', scoped = None, sort = None):
"""
Implement no command for trampoline code back to the original code
"""
if obj_type == 'tag-mapping':
return sdnsh.implement_no_tag(['tag', data['tag']])
def command_version(data):
"""
The version command will later manage changing the syntax to match
the requested version.
"""
new_version = data.get('version')
if new_version == None:
return
version = new_version # save for error message
new_version = sdnsh.desc_version_to_path_elem(new_version)
# skip version change is this is the current version.
if sdnsh.desc_version == new_version:
return
# see if the requested version exists
if not sdnsh.command_packages_exists(new_version):
print 'No command description group for version %s' % version
return
# run 'env [envriron_vars] ... cli.py'
command = ['env']
command.append('CLI_COMMAND_VERSION=%s' % version)
command.append('CLI_STARTING_MODE=config')
if os.path.exists('/opt/sdnplatform/cli/bin/cli'):
# controller VM
command.append('/opt/sdnplatform/cli/bin/cli --init')
else:
# developer setup
base = os.path.dirname(__file__)
command.append(os.path.join(base, 'cli.py'))
command.append('--init')
# dump the command descriptions, and read a new set.
# open a subshell with a new command version
subprocess.call(command, cwd=os.environ.get("HOME"))
return
def command_clearterm():
"""
Print reset characters to the screen to clear the console
"""
subprocess.call("reset")
def command_display_cli(data):
"""
Display various cli details
(this may need to be re-factored into some general "internal" state show
"""
debug = []
if sdnsh.debug:
debug.append('debug')
if sdnsh.debug_backtrace:
debug.append('backtrace')
modes = sdnsh.command_dict.keys() + sdnsh.command_nested_dict.keys()
entry = {
'version' : ', '.join(command.command_syntax_version.keys()),
'desc' : ', '.join(sorted(command.command_added_modules.keys())),
'format' : ', '.join(sorted(sdnsh.pp.format_added_modules.keys())),
'modes' : ', '.join(sorted(utif.unique_list_from_list(modes))),
'debug' : ', '.join(debug),
}
basic = sdnsh.pp.format_entry(entry, 'cli')
mode_entries = command.command_submode_dictionary(modes)
mode_table = sdnsh.pp.format_table(mode_entries, 'cli-modes')
return basic + '\n\nCommand Submode Transition\n' + mode_table
return
def delete_alias_by_id(alias_obj_type, alias_value):
"""
Common delete operation for alias, based on primary key
@param alias_obj_type string, name of table where single entry is removed
@param alias_value string, value of primary key to delete
"""
xref = mi.foreign_key_xref.get(alias_obj_type)
if xref:
# look for any referecnes to this alias_value. Since this
# is an alias table, only the pk ought to exist in the xref.
# When the alias is getting removed, any references to it
# via foreign keys must also get removed.
if len(xref) > 1 or not mi.pk(alias_obj_type) in xref:
print 'Internal Inconsistency'
else:
for (fk_obj_type, fk_field) in xref[mi.pk(alias_obj_type)]:
rows = sdnsh.get_table_from_store(fk_obj_type,
fk_field,
alias_value,
'exact')
for row in rows:
sdnsh.rest_delete_object(fk_obj_type, row[mi.pk(fk_obj_type)])
sdnsh.rest_delete_object(alias_obj_type, alias_value)
def delete_alias_by_fk(alias_obj_type, foreign_key):
"""
Common delete operation for alias, by foreign key
@param alias_obj_type string, name of table where single entry is removed
@param alias_value string, value of primary key to delete
"""
# find all the id's based on the foreign key, then delete them all.
# note: see similar midw alias_lookup_with_foreign_key()
foreign_field = mi.alias_obj_type_field(alias_obj_type)
try:
rows = sdnsh.get_table_from_store(alias_obj_type,
foreign_field,
foreign_key,
"exact")
except Exception, e:
raise error.CommandInternalError("Can't fetch %s:%s" %
(foreign_field, foreign_key))
pk = mi.pk(alias_obj_type)
for row in rows:
delete_alias_by_id(alias_obj_type, row[pk])
def command_delete_alias(obj_type, data):
"""
Action for delete-alias
A single row is deleted from an alias table.
Current alias tables include host-alias, switch-alias, port-alias
@param obj_type string, name of alias table to manage
@param data dict, collection of field:value's from command description
"""
if sdnsh.description: # description debugging
print "command_delete_alias: ", obj_type, data
parent_id = sdnsh.get_current_mode_obj()
key = mi.pk(obj_type)
if key not in data:
delete_alias_by_fk(obj_type, parent_id)
else:
delete_alias_by_id(obj_type, data[key])
def command_create_alias(obj_type, data, reserved = None, fail_if_exists = False):
"""
Action for create-alias
Current alias tables include host-alias, switch-alias, port-alias
@param obj_type string, name of alias table to manage
@param data dict, collection of field:value's from the command description
"""
if sdnsh.description: # description debugging
print "command_create_alias: ", obj_type, data, reserved, fail_if_exists
parent_obj_type = sdnsh.get_current_mode_obj_type()
parent_id = sdnsh.get_current_mode_obj()
key = mi.pk(obj_type)
if key not in data:
raise error.CommandInternalError("Alias table '%s': description "
"doesn't populate correct '%s' field as data" %
(obj_type, key))
alias_value = data[key]
#
# Determine if the alias name is allowed.
if alias_value in sdnsh.reserved_words:
raise error.ArgumentValidationError('reserved name "%s" in "%s"'
% (alias_value, ','.join(sdnsh.reserved_words)))
if reserved and type(reserved) != list:
reserved = [reserved]
if reserved and alias_value in reserved:
raise error.ArgumentValidationError('reserved name "%s" in "%s"'
% (alias_value, ','.join(reserved)))
# Walk the foreign key's in the (alias) obj-type, looking
# for the parent reference.
alias_fk = None
obj_type_foreign_keys = mi.obj_type_foreign_keys(obj_type)
if len(obj_type_foreign_keys) == 1:
alias_fk = obj_type_foreign_keys[0]
else:
for alias_fn in obj_type_foreign_keys:
(fk_ot, fk_fn) = mi.foreign_key_references(obj_type, alias_fn)
if fk_ot == parent_obj_type:
alias_fk = alias_fn
if not alias_fk:
raise error.CommandInternalError("Alias table '%s' has no foreign key to '%s'" %
(obj_type, parent_obj_type))
try:
sdnsh.get_object_from_store(obj_type, alias_value)
if sdnsh.description: # description debugging
print "command_create_alias: delete ", obj_type, alias_value
if fail_if_exists:
raise error.ArgumentValidationError("Interface name '%s' already in use - cannot reassign" %(alias_value))
delete_alias_by_id(obj_type, alias_value)
except:
pass
# Remove other existing alias for the same foreign key
# (ie: only one alias per each item, this could be relaxed)
# XXX improve method of managing errors here
try:
rows = sdnsh.get_table_from_store(obj_type,
alias_fk,
parent_id,
"exact")
except Exception, e:
errors = sdnsh.rest_error_to_dict(e)
print sdnsh.rest_error_dict_to_message(errors)
rows = []
for row in rows:
try:
delete_alias_by_id(obj_type, row[key])
if row[alias_fk] != parent_id:
sdnsh.warning("Removed additional alias '%s'"
", also refers to %s '%s'" %
(row[key], parent_obj_type, parent_id))
except:
if sdnsh.debug or sdnsh.debug_backtrace:
traceback.print_exc()
# This set's the foreign key to allow the create to succeed
c_dict = {
key : alias_value,
alias_fk : parent_id,
}
if sdnsh.description: # description debugging
print "command_create_alias: create ", obj_type, c_dict
result = sdnsh.rest_create_object(obj_type, c_dict)
check_rest_result(result)
result = sdnsh.rest_query_objects(obj_type, c_dict)
check_rest_result(result)
return None
def command_create_tag(obj_type, data):
"""
obj_type needs to be one of the objects which implements
a relationship to 'tag', for example: tag-mac-mapping
"""
item = sdnsh.get_current_mode_obj_type()
fks = mi.obj_type_foreign_keys(obj_type)
for fk in fks:
(fk_obj, fk_name) = mi.foreign_key_references(obj_type, fk)
if fk_obj == item:
break
else:
raise error.CommandSemanticError( "type mapping %s doesn't have "
"relationship to the current object %s" %
(obj_type, item))
if sdnsh.description: # description debugging
print "command_create_tag: create ", obj_type, data
tag_and_value = data['tag'].split('=')
if len(tag_and_value) != 2:
# deal with tag_and_value's 'va=vb=vc...'
raise error.CommandSemanticError("tag <[tag-namespace.]name>=<value> "
": associate tag with host")
tag_parts = tag_and_value[0].split('.')
if len(tag_parts) == 0:
raise error.CommandSemanticError("tag <[tag-namespace.]name>"
": must have a name")
elif len(tag_parts) == 1:
tag_namespace = "default"
tag_name = tag_parts[0]
elif len(tag_parts) >= 2:
# the tag_name is not allowed to have '.'
# collect all the '.'s together into the namespace
tag_namespace = '.'.join(tag_parts[:-1])
tag_name = tag_parts[-1]
tag_value = tag_and_value[1]
# first manage the tag ...
tag_dict = {
'namespace' : tag_namespace,
'name' : tag_name,
'value' : tag_value,
}
query = sdnsh.rest_query_objects('tag', tag_dict)
sdnsh.check_rest_result(query)
tag_dict['persist'] = True
if len(query) == 0:
result = sdnsh.rest_create_object('tag', tag_dict)
sdnsh.check_rest_result(result)
elif len(query) == 1:
update = sdnsh.rest_update_object('tag',
mi.pk('tag'),
query[0][mi.pk('tag')],
tag_dict)
sdnsh.check_rest_result(update)
del tag_dict['persist']
query = sdnsh.rest_query_objects('tag', tag_dict)
sdnsh.check_rest_result(query)
tag_id = query[0][mi.pk('tag')]
# now create the tag-mapping
tag_dict = {
fk : sdnsh.get_current_mode_obj(), # fk from early for loop
'tag' : tag_id,
}
query = sdnsh.rest_query_objects(obj_type, tag_dict)
sdnsh.check_rest_result(query)
if len(query) == 0:
result = sdnsh.rest_create_object(obj_type, tag_dict)
sdnsh.check_rest_result(result)
def command_delete_tag(obj_type, data):
"""
obj_type describes the tag-XXX-mapping which is getting
managed, data has the tag 'string' to delete.
"""
item = sdnsh.get_current_mode_obj_type()
fks = mi.obj_type_foreign_keys(obj_type)
for fk in fks:
(fk_obj, fk_name) = mi.foreign_key_references(obj_type, fk)
if fk_obj == item:
break
else:
raise error.CommandSemanticError( "type mapping %s doesn't have "
"relationship to the current object %s" %
(obj_type, item))
if 'tag' not in data:
raise error.CommandSemanticError('Tag value missing')
tag = data['tag']
name_and_value = tag.split('=')
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]
pk_value = sdnsh.unique_key_from_non_unique([namespace,
name,
value,
sdnsh.get_current_mode_obj()])
try:
sdnsh.get_object_from_store(obj_type, pk_value)
except Exception:
raise error.CommandSemanticError('%s No such tag %s' % (obj_type, tag))
sdnsh.rest_delete_object(obj_type, pk_value)
# with that entry removed, check to see if any other
# foreign keys assocaited with class Tag exist.
fk_value = sdnsh.unique_key_from_non_unique([namespace,
name,
value])
for tag_fields in mi.foreign_key_xref['tag']:
for (fk_obj_type, fk_name) in mi.foreign_key_xref['tag'][tag_fields]:
try:
sdnsh.get_table_from_store(fk_obj_type, fk_name, fk_value)
break
except Exception, e:
pass
else:
continue
break
else:
try:
sdnsh.rest_delete_object('tag', fk_value)
except Exception, e:
raise error.CommandSemanticError('base tag missing' % fk_value)
def command_rest_post_data(path, data=None, verb='PUT'):
"""
"""
url = 'http://%s/rest/v1/%s' % (sdnsh.controller, path)
result = sdnsh.rest_post_request(url, data, verb)
check_rest_result(result)
return None
def command_cli_variables_set(variable, value, data):
global sdnsh
if variable == 'debug':
print '***** %s cli debug *****' % \
('Enabled' if value else 'Disabled')
sdnsh.debug = value
elif variable == 'cli-backtrace':
print '***** %s cli debug backtrace *****' % \
('Enabled' if value else 'Disabled')
sdnsh.debug_backtrace = value
elif variable == 'cli-batch':
print '***** %s cli batch mode *****' % \
('Enabled' if value else 'Disabled')
sdnsh.batch = value
elif variable == 'description':
print '***** %s command description mode *****' % \
('Enabled' if value else 'Disabled')
sdnsh.description = value
elif variable == 'rest':
if 'record' in data and value:
print '***** Eanbled rest record mode %s *****' % \
(data['record'])
url_cache.record(data['record'])
return
print '***** %s display rest mode *****' % \
('Enabled' if value else 'Disabled')
if 'detail' in data and data['detail'] == 'details':
if value == True:
sdnsh.disply_rest_detail = value
sdnsh.store.display_reply_mode(value)
sdnsh.display_rest = value
sdnsh.store.display_mode(value)
if value == False:
sdnsh.disply_rest_detail = value
sdnsh.store.display_reply_mode(value)
url_cache.record(None)
elif variable == 'set':
if 'length' in data:
sdnsh.length = utif.try_int(data['length'])
def command_cli_set(variable, data):
command_cli_variables_set(variable, True, data)
def command_cli_unset(variable, data):
command_cli_variables_set(variable, False, data)
def command_shell_command(script):
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 script == 'bash':
print '***** Type "exit" or Ctrl-D to return to the CLI *****\n'
shell(["-l", "-i"])
elif script == 'python':
print '***** Type "exit()" or Ctrl-D to return to the CLI *****\n'
shell(["-l", "-c", "python"])
elif script == 'cassandra-cli':
print '***** Type "exit" or Ctrl-D to return to the CLI *****\n'
shell(["-l", "-c", "/opt/sdnplatform/db/bin/cassandra-cli --host localhost"])
elif script == 'netconfig':
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"))
else:
# XXX possibly run the script directly?
print "Unknown debug choice %s" % script
def command_prompt_update():
"""
Action to recompute the prompt, used when there's some possibility
the prompt has changes after some other action (hostname update, for example)
"""
sdnsh.set_controller_for_prompt()
sdnsh.update_prompt()
def command_controller_decommission(data):
"""
Decommission the controller using the REST API
"""
id = data.get('id')
confirm_request("Decommission controller '%s'?\n(yes to continue) " % id)
while True:
url = 'http://%s/rest/v1/system/ha/decommission' % (sdnsh.controller)
result = sdnsh.rest_post_request(url, {"id": id}, 'PUT')
status = json.loads(result)
if (status['status'] == 'OK') and status['description'].endswith('is already decommissioned') == True:
print 'Decommission finished'
print
break
else:
print 'Decommission in progress'
time.sleep(10)
def command_controller_upgrade(data = None):
"""
Upgrade the controller using the REST API
"""
force = 'force' in data
details = 'details' in data
if force:
print "WARNING: Ignoring any validation errors during upgrade"
url = "http://%s/rest/v1/system/upgrade/image-name" % sdnsh.controller
result = sdnsh.store.rest_simple_request(url)
check_rest_result(result)
iname = json.loads(result)
if (iname['file'] is None or iname['file'] == ""):
print "Error: No upgrade image present."
print ""
print """To perform upgrade, an upgrade image package needs to be uploaded (with scp) to the controller's \"images\" user."""
print """Upgrade image package is a file with name of format \"upgrade-YYYY.MM.DD.XXXX.pkg\"."""
print ""
print "Following is an example to prepare upgrade for controller with IP address 192.168.67.141:"
print "scp $path/upgrade-2013.02.13.0921.pkg images@192.168.67.141:"
print ""
return
confirm_request("Upgrade controller from image '%s'?\n(yes to continue) "
% iname['file'])
url = "http://%s/rest/v1/system/upgrade/extract-image-manifest" % sdnsh.controller
result = sdnsh.store.rest_simple_request(url)
check_rest_result(result)
manifest = json.loads(result)
print "Executing upgrade..."
for step in manifest:
print "%s - %s" % (step['step'], step['description'])
url = 'http://%s/rest/v1/system/upgrade/execute-upgrade-step' % \
(sdnsh.controller)
result = sdnsh.rest_post_request(url, {"step": step['step'],
"imageName": iname['file'],
"force": force},
'PUT')
check_rest_result(result)
status = json.loads(result)
if (status['status'] == "OK"):
print " Succeeded"
if details:
print "\nDetailed output:"
print status['description']
print
else:
print " Failed to execute upgrade step %d" % step['step']
print "\nDetailed output:"
print status['description']
print
return
print """Controller node upgrade complete.
Upgrade will not take effect until system is rebooted. Use 'reload' to
reboot this controller node. To revert, select the appropriate image
from the boot menu"""
def command_cluster_config_rollback(data):
path = ''
if data.get('dir') == 'images://':
path += '/home/images/'
elif data.get('dir') == 'saved-configs://':
path += '/opt/sdnplatform/run/saved-configs/'
path += data.get('file')
url = "http://%s/rest/v1/system/ha/role" % sdnsh.controller
result = sdnsh.store.rest_simple_request(url, use_cache = False)
ha_role = json.loads(result)
if ha_role['role'] != 'MASTER':
print "Command can only be run on Master"
return
command_legacy_cli('copy', {'dest': 'file://running-config-copy', 'source': 'running-config'})
print "INFO: Checking config '%s'" % path
url = "http://%s/rest/v1/system/rollback/diffconfig" % sdnsh.controller
result = sdnsh.rest_post_request(url, {"config-1": "/opt/sdnplatform/run/saved-configs/running-config-copy", "config-2": path}, 'PUT')
check_rest_result(result)
if json.loads(result)['out'].startswith('Found differences'):
print json.loads(result)['out']
print "Rollback aborted"
return
url = "http://%s/rest/v1/system/controller" % sdnsh.controller
result = sdnsh.store.rest_simple_request(url, use_cache = False)
controller_id = json.loads(result)['id']
url = "http://%s/rest/v1/model/controller-interface?controller=%s" % (sdnsh.controller, controller_id)
result = sdnsh.store.rest_simple_request(url)
local_iface = json.loads(result)[0]['discovered-ip']
url = "http://%s/rest/v1/model/controller-interface" % sdnsh.controller
result = sdnsh.store.rest_simple_request(url)
check_rest_result(result)
ifaces = json.loads(result)
nodeCount = len(ifaces)
cutover = nodeCount/2
if nodeCount%2 == 1:
cutover = cutover + 1
rollbackedNodes = []
# remove and add object for local node at the end of the list
for index, iface in enumerate(ifaces):
if iface['discovered-ip'] == local_iface:
break
del ifaces[index]
ifaces.append(iface)
config=open(path, 'r').read()
url = 'http://%s/rest/v1/system/upload-data' % ifaces[0]['discovered-ip']
result = sdnsh.rest_post_request(url, {"data": config, "dst" : "/tmp/rollback.conf"}, 'PUT')
check_rest_result(result)
while len(ifaces) > 0:
if sdnsh.batch == False:
while True:
confirm = raw_input("Rollback controller at '%s'. [yes/no] ?" % ifaces[0]['discovered-ip'])
if confirm.lower() == 'n' or confirm.lower() == 'no':
if len(rollbackedNodes) == 0:
print "INFO: Rollback aborted"
return
print "INFO: Undoing Rollback on previously rollbacked nodes"
for node in rollbackedNodes:
print "INFO: Resetting database on '%s'" % node['discovered-ip']
url = 'http://%s/rest/v1/system/resetbsc' % (node['discovered-ip'])
result = sdnsh.rest_post_request(url, {}, 'PUT')
check_rest_result(result)
print "INFO: Rebooting '%s'" % node['discovered-ip']
url = 'http://%s/rest/v1/system/reload' % (node['discovered-ip'])
result = sdnsh.rest_post_request(url, {}, 'GET')
check_rest_result(result)
if len(rollbackedNodes) >= cutover:
# delete the REJECT rules
url="http://localhost/rest/v1/model/firewall-rule?port=6633"
result = sdnsh.rest_post_request(url, {}, 'DELETE')
# enable allow openflow on all controllers not rollbacked.
url="http://localhost/rest/v1/model/firewall-rule"
for iface in ifaces:
pk_id = '%s|Ethernet|0' % iface['controller']
data = {
'action': 'allow',
'interface': pk_id,
'src-ip': '',
'port': '6633',
'proto': 'tcp',
'vrrp-ip': '',
}
print "INFO: re-allow openflow on %s" % iface['discovered-ip']
result = sdnsh.rest_post_request(url, data, 'PUT')
check_rest_result(result)
print "Rollback aborted"
return
elif confirm.lower() == 'y' or confirm.lower() == 'yes':
break
url = 'http://%s/rest/v1/system/rollback/config' % (ifaces[0]['discovered-ip'])
result = sdnsh.rest_post_request(url, {"path": "/tmp/rollback.conf"}, 'PUT')
check_rest_result(result)
time.sleep(10)
print "INFO: Rebooting ", ifaces[0]['discovered-ip']
url = "http://%s/rest/v1/system/reload" % ifaces[0]['discovered-ip']
result = sdnsh.store.rest_simple_request(url)
if ifaces[0]['discovered-ip'] == local_iface:
break
print "INFO: Waiting for %s to come back up" % ifaces[0]['discovered-ip']
url = "http://%s/rest/v1/system/ha/role" % ifaces[0]['discovered-ip']
while True:
time.sleep(30)
try:
result = sdnsh.store.rest_simple_request(url, use_cache = False)
status = json.loads(result)
if status['role'] == 'SLAVE' or status['role'] == 'MASTER':
print "INFO: Rollback complete on '%s'" % ifaces[0]['discovered-ip']
break
print "INFO: Waiting for 30 seconds"
except:
print "INFO: Waiting for 30 seconds"
iface = ifaces.pop(0)
rollbackedNodes.append(iface)
print "Rollback completed"
def command_wait_for_controller(delay = None, sdnplatform_check = False,
within_command = False):
"""
For various commands, it makes sense for the command to verify that
the controller restart has been completed. In the situation where
a single controller is configured, it also makes sense to verify the
controller is now configured as MASTER.
This is especially true for command which are known to cause the
controller to restart, for exampe the 'feature' command.
The procedure is also used during CLI startup (see cli.py)
to verify that the controller is in MASTER mode. Its normal
for the HA role to transition from SLAVE to master during
system startup.
"""
# if the CLI was started with --init, skip the wait, the
# controller isn't running.
if sdnsh.options.init:
return
def is_ready(sdnsh, verbose, duration):
"""
Be loud-as-_ean when the duration is greater then 15 seconds.
Display the gory details for all to know.
"""
too_long = 90
try:
url = "http://%s/rest/v1/system/ha/role" % sdnsh.controller
result = sdnsh.store.rest_simple_request(url, use_cache = False)
ha_role = json.loads(result)
if duration > too_long:
print 'Long delay: reason', \
', '.join(['%s: %s' % (n,v) for (n,v) in ha_role.items()
if v != ''])
if (ha_role['role'] == 'MASTER' or
sdnsh.find_master()['master'] is not None):
if verbose:
print 'Current role is MASTER'
return True
return False
except error.CommandRestError,e:
print "REST error whileUnable to determine controller HA role."
errors = self.rest_error_to_dict(e, obj_type)
print self.rest_error_dict_to_message(errors)
return True
except Exception, e:
if duration > too_long:
print 'MASTER Transition Failure: ', e
traceback.print_exc()
return True
return False
# if this isn't a typical environment (ie: running remotely)
# don't bother trying to determine the role
if not os.path.exists('/opt/sdnplatform/current_role'):
return
# now vadalidate the rest api port is working
ip_and_port = sdnsh.controller.split(':')
if len(ip_and_port) == 2:
# first ensure the REST API is answering
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.connect((ip_and_port[0], int(ip_and_port[1])))
s.close()
except Exception, e:
print 'REST API not running, emergency CLI access'
if sdnsh.debug: # enable debug to see messages
print 'Exception:', e
return
# issue a REST API request directed at the model.
try:
entry = sdnsh.get_table_from_store('feature')
except Exception, e:
print 'REST API/Database not responding, emergency CLI access'
if sdnsh.debug: # enable debug to see messages
print 'Exception:', e
return
if sdnplatform_check:
# the REST API request for ha-role will return UNAVAILABLE
# when sdnplatform isn't running.
url = "http://%s/rest/v1/system/ha/role" % sdnsh.controller
result = sdnsh.store.rest_simple_request(url, use_cache = False)
ha_role = json.loads(result)
if ha_role['role'] == 'UNAVAILABLE':
print 'REST API/SDN platform not responding, emergency CLI access'
return
if delay == None:
delay = 1
delay_str = 'a sec' if delay == 1 else '%d seconds' % delay
duration = 0
while True:
try:
verbose = False
while not is_ready(sdnsh, verbose, duration):
if within_command:
print 'Waiting %s to complete command execution, ' \
'Hit Ctrl-C to exit early' % delay_str
verbose = False
else:
print 'Waiting %s while current role is SLAVE mode, ' \
'Hit Ctrl-C to exit early' % delay_str
verbose = True
time.sleep(delay)
duration += delay
return
except:
if is_ready(sdnsh, True, duration):
if duration > 15:
print 'MASTER Transition: %s sec' % duration
return
try:
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 ...'
return
except KeyboardInterrupt:
return
def command_factory_default():
print "Re-setting controller to factory defaults ..."
os.system("sudo /opt/sdnplatform/sys/bin/resetbsc")
return
def command_dump_log(data):
controller = data.get('controller-node') # can be None.
controller_dict = { 'id' : controller }
for ip_port in controller_ip_and_port(controller_dict):
log_name = data['log-name']
if log_name == 'all':
url = log_url(ip_and_port = ip_port)
log_names = command.sdnsh.rest_simple_request_to_dict(url)
for log in log_names:
yield '*' * 40 + ip_port + ' ' + log['log'] + '\n'
for item in command_dump_log({ 'log-name' : log['log'] }):
yield item
return
# use a streaming method so the complete log is not in memory
url = log_url(ip_and_port = ip_port, log = log_name)
request = urllib2.urlopen(url)
for line in request:
yield line
request.close()
#
# Initialize action functions
#
#
def init_actions(bs, modi):
global sdnsh, mi
sdnsh = bs
mi = modi
command.add_action('create-tunnel',
tunnel_create,
{'kwargs': {'data' : '$data',}})
command.add_action('remove-tunnel',
tunnel_remove,
{'kwargs': {'data' : '$data',}})
command.add_action('create-policy',
policy_create,
{'kwargs': {'data' : '$data',}})
command.add_action('remove-policy',
policy_remove,
{'kwargs': {'data' : '$data',}})
command.add_action('write-fields', write_fields,
{'kwargs': {'obj_type': '$current-mode-obj-type',
'obj_id': '$current-mode-obj-id',
'data': '$data'}})
command.add_action('reset-fields', reset_fields,
{'kwargs': {'obj_type' : '$current-mode-obj-type',
'obj_id' : '$current-mode-obj-id',
'arg_data' : '$data',
'match_for_no' : '$match-for-no',
'fields' : '$fields'}})
command.add_action('write-fields-explicit', write_fields,
{'kwargs': {'obj_type' : '$obj-type',
'obj_id' : '$obj-id',
'data' : '$data'}})
command.add_action('reset-fields-explicit', reset_fields,
{'kwargs': {'obj_type' : '$obj-type',
'obj_id' : '$obj-id',
'arg_data' : '$data',
'match_for_no' : '$match-for-no',
'fields' : '$fields'}})
command.add_action('update-config', update_config,
{'kwargs': {'obj_type' : '$obj-type',
'obj_id' : '$current-mode-obj-id',
'data' : '$data',
'no_command' : '$is-no-command', }})
command.add_action('delete-objects', delete_objects,
{'kwargs': {'obj_type': '$obj-type',
'data': '$data',
'parent_field': '$parent-field',
'parent_id': '$current-mode-obj-id'}})
command.add_action('write-object', write_object,
{'kwargs': {'obj_type': '$obj-type',
'data': '$data',
'parent_field': '$parent-field',
'parent_id': '$current-mode-obj-id'}})
command.add_action('set-data', set_data,
{'kwargs': {'data': '$data',
'key': '$key',
'value': '$value'}})
command.add_action('push-mode-stack', push_mode_stack,
{'kwargs': {'mode_name': '$submode-name',
'obj_type': '$obj-type',
'parent_field': '$parent-field',
'parent_id': '$current-mode-obj-id',
'data': '$data',
'create': '$create'}})
command.add_action('pop-mode-stack', pop_mode_stack)
command.add_action('confirm', confirm_request,
{'kwargs': {'prompt': '$prompt'}})
command.add_action('convert-vns-access-list', convert_vns_access_list,
{'kwargs': {'obj_type': '$obj-type',
'key' : '$current-mode-obj-id',
'data' : '$data'}})
command.add_action('display-table', command_display_table,
{'kwargs': {'obj_type' : '$obj-type',
'data' : '$data',
'table_format' : '$format',
'title' : '$title',
'detail' : '$detail',
'scoped' : '$scoped',
'sort' : '$sort',
}})
command.add_action('display-rest', command_display_rest,
{'kwargs': { 'data' : '$data',
'url' : '$url',
'path' : '$path',
'rest_type' : '$rest-type',
'sort' : '$sort',
'title' : '$title',
'table_format' : '$format',
'detail' : '$detail',
}})
command.add_action('query-table', command_query_table,
{'kwargs': {'obj_type' : '$obj-type',
'data' : '$data',
'key' : '$key',
'scoped' : '$scoped',
'sort' : '$sort',
'crack' : '$crack',
'append' : '$append',
'clear' : True,
}})
command.add_action('query-table-append', command_query_table,
{'kwargs': {'obj_type' : '$obj-type',
'data' : '$data',
'key' : '$key',
'scoped' : '$scoped',
'sort' : '$sort',
'crack' : '$crack',
'append' : '$append',
'clear' : False,
}})
command.add_action('query-rest', command_query_rest,
{'kwargs': {'url' : '$url',
'path' : '$path',
'rest_type' : '$rest-type',
'data' : '$data',
'key' : '$key',
'scoped' : '$scoped',
'sort' : '$sort',
'append' : '$append',
'clear' : True,
}})
command.add_action('query-rest-append', command_query_rest,
{'kwargs': {'url' : '$url',
'path' : '$path',
'rest_type' : '$rest-type',
'data' : '$data',
'key' : '$key',
'scoped' : '$scoped',
'sort' : '$sort',
'crack' : '$crack',
'append' : '$append',
'clear' : False,
}})
command.add_action('join-rest', command_join_rest,
{'kwargs': {'url' : '$url',
'key' : '$key',
'join_field' : '$join-field',
'rest_type' : '$rest-type',
'add_field' : '$add-field',
'data' : '$data',
'crack' : '$crack',
'url_key' : '$url-key',
}})
command.add_action('join-table', command_join_table,
{'kwargs': {'obj_type' : '$obj-type',
'data' : '$data',
'key' : '$key',
'key_value' : '$key-value',
'add_field' : '$add-field',
'join_field' : '$join-field',
'crack' : '$crack',
}})
command.add_action('crack', command_crack,
{'kwargs': {
'field' : '$field',
}})
command.add_action('display', command_display,
{'kwargs': {'data' : '$data',
'table_format' : '$format',
'sort' : '$sort',
'detail' : '$detail',
'title' : '$title',
}})
command.add_action('legacy-cli', command_legacy_cli,
{'kwargs': {'obj_type' : '$obj-type',
'data' : '$data',
'detail' : '$detail',
'sort' : '$sort',
'scoped' : '$scoped',
}})
command.add_action('legacy-cli-no', command_legacy_cli_no,
{'kwargs': {'obj_type' : '$obj-type',
'data' : '$data',
'detail' : '$detail',
'sort' : '$sort',
'scoped' : '$scoped',
}})
command.add_action('version', command_version,
{'kwargs': {'data' : '$data',
}})
command.add_action('clearterm', command_clearterm)
command.add_action('display-cli', command_display_cli,
{'kwargs': {'data' : '$data',
'detail' : '$detail',
}})
command.add_action('create-alias', command_create_alias,
{'kwargs': {'obj_type' : '$obj-type',
'data' : '$data',
'reserved' : '$reserved',
'fail_if_exists' : '$fail-if-exists',
}})
command.add_action('delete-alias', command_delete_alias,
{'kwargs': {'obj_type' : '$obj-type',
'data' : '$data',
}})
command.add_action('create-tag', command_create_tag,
{'kwargs': {'obj_type' : '$obj-type',
'data' : '$data',
}})
command.add_action('delete-tag', command_delete_tag,
{'kwargs': {'obj_type' : '$obj-type',
'data' : '$data',
}})
command.add_action('cli-set', command_cli_set,
{'kwargs': {'variable' : '$variable',
'data' : '$data',
}})
command.add_action('cli-unset', command_cli_unset,
{'kwargs': {'variable' : '$variable',
'data' : '$data',
}})
command.add_action('shell-command', command_shell_command,
{'kwargs': {'script' : '$command',
}})
command.add_action('rest-post-data', command_rest_post_data,
{'kwargs': {'path': '$path',
'data': '$data',
'verb': '$verb'
}})
command.add_action('prompt-update', command_prompt_update,)
command.add_action('controller-upgrade', command_controller_upgrade,
{'kwargs': {'data': '$data'}})
command.add_action('controller-config-rollback', command_cluster_config_rollback,
{'kwargs': {'data': '$data'}})
command.add_action('controller-decommission', command_controller_decommission,
{'kwargs': {'data': '$data'}})
command.add_action('wait-for-controller', command_wait_for_controller,
{'kwargs': {'within_command': True}})
command.add_action('factory-default', command_factory_default)
command.add_action('dump-log', command_dump_log,
{'kwargs' : { 'data' : '$data', }})