Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[config] Add Initial draft of Breakout Mode subcommand #766

Merged
merged 17 commits into from
Jul 23, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
273 changes: 273 additions & 0 deletions config/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,48 @@
import ipaddress
from swsssdk import ConfigDBConnector, SonicV2Connector, SonicDBConfig
from minigraph import parse_device_desc_xml
from config_mgmt import ConfigMgmtDPB

import aaa
import mlnx
import nat

# import port config file path
from sfputil.main import get_platform_and_hwsku
samaity marked this conversation as resolved.
Show resolved Hide resolved
from portconfig import get_port_config_file_name
from portconfig import get_child_ports
samaity marked this conversation as resolved.
Show resolved Hide resolved

CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help', '-?'])

SONIC_GENERATED_SERVICE_PATH = '/etc/sonic/generated_services.conf'
SONIC_CFGGEN_PATH = '/usr/local/bin/sonic-cfggen'
SYSLOG_IDENTIFIER = "config"
PORT_STR = "Ethernet"
INTF_KEY = "interfaces"

(platform, hwsku) = get_platform_and_hwsku()
samaity marked this conversation as resolved.
Show resolved Hide resolved
BREAKOUT_CFG_FILE = get_port_config_file_name(hwsku, platform)

VLAN_SUB_INTERFACE_SEPARATOR = '.'
ASIC_CONF_FILENAME = 'asic.conf'
DEFAULT_CONFIG_DB_FILE = '/etc/sonic/config_db.json'
NAMESPACE_PREFIX = 'asic'


INIT_CFG_FILE = '/etc/sonic/init_cfg.json'

SYSTEMCTL_ACTION_STOP="stop"
SYSTEMCTL_ACTION_RESTART="restart"
SYSTEMCTL_ACTION_RESET_FAILED="reset-failed"

DEFAULT_NAMESPACE = ''

CFG_LOOPBACK_PREFIX = "Loopback"
CFG_LOOPBACK_PREFIX_LEN = len(CFG_LOOPBACK_PREFIX)
CFG_LOOPBACK_NAME_TOTAL_LEN_MAX = 11
CFG_LOOPBACK_ID_MAX_VAL = 999
CFG_LOOPBACK_NO="<0-999>"

# ========================== Syslog wrappers ==========================

def log_debug(msg):
Expand Down Expand Up @@ -116,6 +131,140 @@ def get_command(self, ctx, cmd_name):
except KeyError, TypeError:
raise click.Abort()

#
# Breakout Mode Helper functions
#

# Read given JSON file
def readJsonFile(fileName):
try:
with open(fileName) as f:
result = json.load(f)
except Exception as e:
raise Exception(str(e))
return result

def _get_option(ctx,args,incomplete):
""" Provides dynamic mode option as per user argument i.e. interface name """
global all_mode_options
samaity marked this conversation as resolved.
Show resolved Hide resolved
interface_name = args[-1]

if not os.path.isfile(BREAKOUT_CFG_FILE) or not BREAKOUT_CFG_FILE.endswith('.json'):
return []
else:
breakout_file_input = readJsonFile(BREAKOUT_CFG_FILE)
if interface_name in breakout_file_input[INTF_KEY]:
breakout_mode_list = [v["breakout_modes"] for i ,v in breakout_file_input[INTF_KEY].items() if i == interface_name][0]
breakout_mode_options = []
for i in breakout_mode_list.split(','):
breakout_mode_options.append(i)
all_mode_options = [str(c) for c in breakout_mode_options if incomplete in c]
return all_mode_options


def shutdown_interfaces(ctx, del_intf_dict):
""" shut down all the interfaces before deletion """
for intf in del_intf_dict.keys():
config_db = ctx.obj['config_db']
if get_interface_naming_mode() == "alias":
interface_name = interface_alias_to_name(intf)
if interface_name is None:
click.echo("[ERROR] interface name is None!")
return False
if interface_name_is_valid(intf) is False:
click.echo("[ERROR] Interface name is invalid. Please enter a valid interface name!!")
return False
if intf.startswith(PORT_STR):
samaity marked this conversation as resolved.
Show resolved Hide resolved
config_db.mod_entry("PORT", intf, {"admin_status": "down"})
else:
click.secho("[ERROR] Could not get the correct interface name, exiting", fg='red')
return False
return True


def _validate_interface_mode(ctx, BREAKOUT_CFG_FILE, interface_name, target_brkout_mode, cur_brkout_mode):
""" Validate Parent interface and user selected mode before starting deletetion or addition process """
breakout_file_input = readJsonFile(BREAKOUT_CFG_FILE)["interfaces"]

if interface_name not in breakout_file_input:
click.secho("[ERROR] {} is not a Parent port. So, Breakout Mode is not available on this port".format(interface_name), fg='red')
return False

# Check whether target breakout mode is available for the user-selected interface or not
if target_brkout_mode not in breakout_file_input[interface_name]["breakout_modes"]:
click.secho('[ERROR] Target mode {} is not available for the port {}'. format(target_brkout_mode, interface_name), fg='red')
return False

# Get config db context
config_db = ctx.obj['config_db']
port_dict = config_db.get_table('PORT')

# Check whether there is any port in config db.
if not port_dict:
click.echo("port_dict is None!")
return False

# Check whether the user-selected interface is part of 'port' table in config db.
if interface_name not in port_dict.keys():
click.secho("[ERROR] {} is not in port_dict".format(interface_name))
return False
click.echo("\nRunning Breakout Mode : {} \nTarget Breakout Mode : {}".format(cur_brkout_mode, target_brkout_mode))
if (cur_brkout_mode == target_brkout_mode):
click.secho("[WARNING] No action will be taken as current and desired Breakout Mode are same.", fg='magenta')
sys.exit(0)
return True

def load_ConfigMgmt(verbose):
""" Load config for the commands which are capable of change in config DB. """
try:
cm = ConfigMgmtDPB(debug=verbose)
return cm
except Exception as e:
raise Exception("Failed to load the config. Error: {}".format(str(e)))

"""
Funtion to warn user about extra tables while Dynamic Port Breakout(DPB).
confirm: re-confirm from user to proceed.
Config Tables Without Yang model considered extra tables.
cm = instance of config MGMT class.
"""
def breakout_warnUser_extraTables(cm, final_delPorts, confirm=True):

jleveque marked this conversation as resolved.
Show resolved Hide resolved
try:
# check if any extra tables exist
eTables = cm.tablesWithOutYang()
if len(eTables):
# find relavent tables in extra tables, i.e. one which can have deleted
# ports
tables = cm.configWithKeys(configIn=eTables, keys=final_delPorts)
click.secho("Below Config can not be verified, It may cause harm "\
"to the system\n {}".format(json.dumps(tables, indent=2)))
click.confirm('Do you wish to Continue?', abort=True)
except Exception as e:
raise Exception("Failed in breakout_warnUser_extraTables. Error: {}".format(str(e)))

return

def breakout_Ports(cm, delPorts=list(), addPorts=list(), portJson=dict(), \
force=False, loadDefConfig=False, verbose=False):

deps, ret = cm.breakOutPort(delPorts=delPorts, addPorts = addPorts, \
samaity marked this conversation as resolved.
Show resolved Hide resolved
portJson=portJson, force=force, loadDefConfig=loadDefConfig)
# check if DPB failed
if ret == False:
if not force and deps:
print("Dependecies Exist. No further action will be taken")
print("*** Printing dependecies ***")
for dep in deps:
print(dep)
sys.exit(0)
else:
print("[ERROR] Port breakout Failed!!! Opting Out")
samaity marked this conversation as resolved.
Show resolved Hide resolved
raise click.Abort()

return


#
# Helper functions
#
Expand Down Expand Up @@ -1906,6 +2055,129 @@ def speed(ctx, interface_name, interface_speed, verbose):
command += " -vv"
run_command(command, display_cmd=verbose)



#
# 'breakout' subcommand
#
@interface.command()
@click.argument('interface_name', metavar='<interface_name>', required=True)
@click.argument('mode', required=True, type=click.STRING, autocompletion=_get_option)
@click.option('-f', '--force-remove-dependencies', is_flag=True, help='Clear all depenedecies internally first.')
@click.option('-l', '--load-predefined-config', is_flag=True, help='load predefied user configuration (alias, lanes, speed etc) first.')
@click.option('-y', '--yes', is_flag=True, callback=_abort_if_false, expose_value=False, prompt='Do you want to Breakout the port, continue?')
@click.option('-v', '--verbose', is_flag=True, help="Enable verbose output")
@click.pass_context
def breakout(ctx, interface_name, mode, verbose, force_remove_dependencies, load_predefined_config):

jleveque marked this conversation as resolved.
Show resolved Hide resolved
""" Set interface breakout mode """

if not os.path.isfile(BREAKOUT_CFG_FILE) or not BREAKOUT_CFG_FILE.endswith('.json'):
click.secho("[ERROR] Breakout feature is not available without platform.json file", fg='red')
raise click.Abort()

# Connect to config db and get the context
config_db = ConfigDBConnector()
config_db.connect()
ctx.obj['config_db'] = config_db

target_brkout_mode = mode

# Get current breakout mode
cur_brkout_dict = config_db.get_table('BREAKOUT_CFG')
cur_brkout_mode = cur_brkout_dict[interface_name]["brkout_mode"]

# Validate Interface and Breakout mode
if not _validate_interface_mode(ctx, BREAKOUT_CFG_FILE, interface_name, mode, cur_brkout_mode):
raise click.Abort()

""" Interface Deletion Logic """
# Get list of interfaces to be deleted
del_ports = get_child_ports(interface_name, cur_brkout_mode, BREAKOUT_CFG_FILE)
del_intf_dict = {intf: del_ports[intf]["speed"] for intf in del_ports}

if del_intf_dict:
""" shut down all the interface before deletion """
ret = shutdown_interfaces(ctx, del_intf_dict)
if not ret:
raise click.Abort()
click.echo("\nPorts to be deleted : \n {}".format(json.dumps(del_intf_dict, indent=4)))

else:
click.secho("[ERROR] del_intf_dict is None! No interfaces are there to be deleted", fg='red')
raise click.Abort()


""" Interface Addition Logic """
# Get list of interfaces to be added
add_ports = get_child_ports(interface_name, target_brkout_mode, BREAKOUT_CFG_FILE)
add_intf_dict = {intf: add_ports[intf]["speed"] for intf in add_ports}

if add_intf_dict:
click.echo("Ports to be added : \n {}".format(json.dumps(add_intf_dict, indent=4)))
else:
click.secho("[ERROR] port_dict is None!", fg='red')
raise click.Abort()

""" Special Case: Dont delete those ports where the current mode and speed of the parent port
remains unchanged to limit the traffic impact """

click.secho("\nAfter running Logic to limit the impact", fg="cyan", underline=True)
matched_item = [intf for intf, speed in del_intf_dict.items() if intf in add_intf_dict.keys() and speed == add_intf_dict[intf]]

# Remove the interface which remains unchanged from both del_intf_dict and add_intf_dict
map(del_intf_dict.pop, matched_item)
map(add_intf_dict.pop, matched_item)

click.secho("\nFinal list of ports to be deleted : \n {} \nFinal list of ports to be added : \n {}".format(json.dumps(del_intf_dict, indent=4), json.dumps(add_intf_dict, indent=4), fg='green', blink=True))
if len(add_intf_dict.keys()) == 0:
click.secho("[ERROR] add_intf_dict is None! No interfaces are there to be added", fg='red')
raise click.Abort()

port_dict = {}
for intf in add_intf_dict:
if intf in add_ports.keys():
port_dict[intf] = add_ports[intf]

# writing JSON object
with open('new_port_config.json', 'w') as f:
json.dump(port_dict, f, indent=4)

# Start Interation with Dy Port BreakOut Config Mgmt
try:
""" Load config for the commands which are capable of change in config DB """
cm = load_ConfigMgmt(verbose)

""" Delete all ports if forced else print dependencies using ConfigMgmt API """
final_delPorts = [intf for intf in del_intf_dict.keys()]
""" Warn user if tables without yang models exist and have final_delPorts """
breakout_warnUser_extraTables(cm, final_delPorts, confirm=True)
# prompt
""" Add ports with its attributes using ConfigMgmt API """
final_addPorts = [intf for intf in port_dict.keys()]
portJson = dict(); portJson['PORT'] = port_dict
# breakout_Ports will abort operation on failure, So no need to check return
breakout_Ports(cm, delPorts=final_delPorts, addPorts = final_addPorts, \
portJson=portJson, force=force_remove_dependencies, \
loadDefConfig=load_predefined_config, verbose=verbose)

# Set Current Breakout mode in config DB
brkout_cfg_keys = config_db.get_keys('BREAKOUT_CFG')
if interface_name.decode("utf-8") not in brkout_cfg_keys:
click.secho("[ERROR] {} is not present in 'BREAKOUT_CFG' Table!".\
format(interface_name), fg='red')
raise click.Abort()
config_db.set_entry("BREAKOUT_CFG", interface_name,\
{'brkout_mode': target_brkout_mode})
click.secho("Breakout process got successfully completed.".\
format(interface_name), fg="cyan", underline=True)

except Exception as e:
click.secho("Failed to break out Port. Error: {}".format(str(e)), \
fg='magenta')
sys.exit(0)


def _get_all_mgmtinterface_keys():
"""Returns list of strings containing mgmt interface keys
"""
Expand All @@ -1928,6 +2200,7 @@ def mgmt_ip_restart_services():
cmd="systemctl restart ntp-config"
os.system (cmd)


#
# 'mtu' subcommand
#
Expand Down
4 changes: 4 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,12 @@
# - sonic-config-engine
# - swsssdk
# - tabulate
# - click
install_requires=[
'click-default-group',
samaity marked this conversation as resolved.
Show resolved Hide resolved

samaity marked this conversation as resolved.
Show resolved Hide resolved
'click',

samaity marked this conversation as resolved.
Show resolved Hide resolved
'natsort'
],
setup_requires= [
Expand Down