From 1b5679c5800103857a81e3cd4cb9c4554aff6022 Mon Sep 17 00:00:00 2001 From: Tyler Li Date: Wed, 27 Nov 2019 07:49:22 +0800 Subject: [PATCH] [VRF]: submit vrf CLI #392 (#558) * Support VRF --- config/main.py | 361 +++++++++++++++++++++++++++++++++++++++++-------- show/main.py | 91 ++++++++++--- 2 files changed, 379 insertions(+), 73 deletions(-) diff --git a/config/main.py b/config/main.py index 6fcc0f46f839..ed046fe9c583 100755 --- a/config/main.py +++ b/config/main.py @@ -8,6 +8,7 @@ import netaddr import re import syslog +import time import netifaces import sonic_device_util @@ -156,6 +157,61 @@ def interface_name_to_alias(interface_name): return None +def get_interface_table_name(interface_name): + """Get table name by interface_name prefix + """ + if interface_name.startswith("Ethernet"): + if VLAN_SUB_INTERFACE_SEPARATOR in interface_name: + return "VLAN_SUB_INTERFACE" + return "INTERFACE" + elif interface_name.startswith("PortChannel"): + if VLAN_SUB_INTERFACE_SEPARATOR in interface_name: + return "VLAN_SUB_INTERFACE" + return "PORTCHANNEL_INTERFACE" + elif interface_name.startswith("Vlan"): + return "VLAN_INTERFACE" + elif interface_name.startswith("Loopback"): + return "LOOPBACK_INTERFACE" + else: + return "" + +def interface_ipaddr_dependent_on_interface(config_db, interface_name): + """Get table keys including ipaddress + """ + data = [] + table_name = get_interface_table_name(interface_name) + if table_name == "": + return data + keys = config_db.get_keys(table_name) + for key in keys: + if interface_name in key and len(key) == 2: + data.append(key) + return data + +def is_interface_bind_to_vrf(config_db, interface_name): + """Get interface if bind to vrf or not + """ + table_name = get_interface_table_name(interface_name) + if table_name == "": + return False + entry = config_db.get_entry(table_name, interface_name) + if entry and entry.get("vrf_name"): + return True + return False + +def del_interface_bind_to_vrf(config_db, vrf_name): + """del interface bind to vrf + """ + tables = ['INTERFACE', 'PORTCHANNEL_INTERFACE', 'VLAN_INTERFACE', 'LOOPBACK_INTERFACE'] + for table_name in tables: + interface_dict = config_db.get_table(table_name) + if interface_dict: + for interface_name in interface_dict.keys(): + if interface_dict[interface_name].has_key('vrf_name') and vrf_name == interface_dict[interface_name]['vrf_name']: + interface_dependent = interface_ipaddr_dependent_on_interface(config_db, interface_name) + for interface_del in interface_dependent: + config_db.set_entry(table_name, interface_del, None) + config_db.set_entry(table_name, interface_name, None) def set_interface_naming_mode(mode): """Modify SONIC_CLI_IFACE_MODE env variable in user .bashrc @@ -1373,14 +1429,8 @@ def add(ctx, interface_name, ip_addr, gw): try: ipaddress.ip_network(unicode(ip_addr), strict=False) - if interface_name.startswith("Ethernet"): - if VLAN_SUB_INTERFACE_SEPARATOR in interface_name: - config_db.set_entry("VLAN_SUB_INTERFACE", interface_name, {"admin_status": "up"}) - config_db.set_entry("VLAN_SUB_INTERFACE", (interface_name, ip_addr), {"NULL": "NULL"}) - else: - config_db.set_entry("INTERFACE", (interface_name, ip_addr), {"NULL": "NULL"}) - config_db.set_entry("INTERFACE", interface_name, {"NULL": "NULL"}) - elif interface_name == 'eth0': + + if interface_name == 'eth0': # Configuring more than 1 IPv4 or more than 1 IPv6 address fails. # Allow only one IPv4 and only one IPv6 address to be configured for IPv6. @@ -1403,20 +1453,18 @@ def add(ctx, interface_name, ip_addr, gw): config_db.set_entry("MGMT_INTERFACE", (interface_name, ip_addr), {"gwaddr": gw}) mgmt_ip_restart_services() - elif interface_name.startswith("PortChannel"): - if VLAN_SUB_INTERFACE_SEPARATOR in interface_name: - config_db.set_entry("VLAN_SUB_INTERFACE", interface_name, {"admin_status": "up"}) - config_db.set_entry("VLAN_SUB_INTERFACE", (interface_name, ip_addr), {"NULL": "NULL"}) - else: - config_db.set_entry("PORTCHANNEL_INTERFACE", (interface_name, ip_addr), {"NULL": "NULL"}) - config_db.set_entry("PORTCHANNEL_INTERFACE", interface_name, {"NULL": "NULL"}) - elif interface_name.startswith("Vlan"): - config_db.set_entry("VLAN_INTERFACE", (interface_name, ip_addr), {"NULL": "NULL"}) - config_db.set_entry("VLAN_INTERFACE", interface_name, {"NULL": "NULL"}) - elif interface_name.startswith("Loopback"): - config_db.set_entry("LOOPBACK_INTERFACE", (interface_name, ip_addr), {"NULL": "NULL"}) - else: + return + + table_name = get_interface_table_name(interface_name) + if table_name == "": ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan/Loopback]") + interface_entry = config_db.get_entry(table_name, interface_name) + if len(interface_entry) == 0: + if table_name == "VLAN_SUB_INTERFACE": + config_db.set_entry(table_name, interface_name, {"admin_status": "up"}) + else: + config_db.set_entry(table_name, interface_name, {"NULL": "NULL"}) + config_db.set_entry(table_name, (interface_name, ip_addr), {"NULL": "NULL"}) except ValueError: ctx.fail("'ip_addr' is not valid.") @@ -1436,51 +1484,252 @@ def remove(ctx, interface_name, ip_addr): if interface_name is None: ctx.fail("'interface_name' is None!") - if_table = "" try: ipaddress.ip_network(unicode(ip_addr), strict=False) - if interface_name.startswith("Ethernet"): - if VLAN_SUB_INTERFACE_SEPARATOR in interface_name: - config_db.set_entry("VLAN_SUB_INTERFACE", (interface_name, ip_addr), None) - if_table = "VLAN_SUB_INTERFACE" - else: - config_db.set_entry("INTERFACE", (interface_name, ip_addr), None) - if_table = "INTERFACE" - elif interface_name == 'eth0': + + if interface_name == 'eth0': config_db.set_entry("MGMT_INTERFACE", (interface_name, ip_addr), None) mgmt_ip_restart_services() - elif interface_name.startswith("PortChannel"): - if VLAN_SUB_INTERFACE_SEPARATOR in interface_name: - config_db.set_entry("VLAN_SUB_INTERFACE", (interface_name, ip_addr), None) - if_table = "VLAN_SUB_INTERFACE" - else: - config_db.set_entry("PORTCHANNEL_INTERFACE", (interface_name, ip_addr), None) - if_table = "PORTCHANNEL_INTERFACE" - elif interface_name.startswith("Vlan"): - config_db.set_entry("VLAN_INTERFACE", (interface_name, ip_addr), None) - if_table = "VLAN_INTERFACE" - elif interface_name.startswith("Loopback"): - config_db.set_entry("LOOPBACK_INTERFACE", (interface_name, ip_addr), None) - else: + return + + table_name = get_interface_table_name(interface_name) + if table_name == "": ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan/Loopback]") + config_db.set_entry(table_name, (interface_name, ip_addr), None) + interface_dependent = interface_ipaddr_dependent_on_interface(config_db, interface_name) + if len(interface_dependent) == 0 and is_interface_bind_to_vrf(config_db, interface_name) is False: + config_db.set_entry(table_name, interface_name, None) - command = "ip neigh flush {}".format(ip_addr) + command = "ip neigh flush dev {} {}".format(interface_name, ip_addr) run_command(command) except ValueError: ctx.fail("'ip_addr' is not valid.") - exists = False - if if_table: - interfaces = config_db.get_table(if_table) - for key in interfaces.keys(): - if not isinstance(key, tuple): - continue - if interface_name in key: - exists = True - break - - if not exists: - config_db.set_entry(if_table, interface_name, None) +# +# 'vrf' subgroup ('config interface vrf ...') +# + + +@interface.group() +@click.pass_context +def vrf(ctx): + """Bind or unbind VRF""" + pass + +# +# 'bind' subcommand +# +@vrf.command() +@click.argument('interface_name', metavar='', required=True) +@click.argument('vrf_name', metavar='', required=True) +@click.pass_context +def bind(ctx, interface_name, vrf_name): + """Bind the interface to VRF""" + config_db = ctx.obj["config_db"] + if get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(interface_name) + if interface_name is None: + ctx.fail("'interface_name' is None!") + + table_name = get_interface_table_name(interface_name) + if table_name == "": + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan/Loopback]") + if is_interface_bind_to_vrf(config_db, interface_name) is True and \ + config_db.get_entry(table_name, interface_name).get('vrf_name') == vrf_name: + return + # Clean ip addresses if interface configured + interface_dependent = interface_ipaddr_dependent_on_interface(config_db, interface_name) + for interface_del in interface_dependent: + config_db.set_entry(table_name, interface_del, None) + config_db.set_entry(table_name, interface_name, None) + # When config_db del entry and then add entry with same key, the DEL will lost. + state_db = SonicV2Connector(host='127.0.0.1') + state_db.connect(state_db.STATE_DB, False) + _hash = '{}{}'.format('INTERFACE_TABLE|', interface_name) + while state_db.get(state_db.STATE_DB, _hash, "state") == "ok": + time.sleep(0.01) + state_db.close(state_db.STATE_DB) + config_db.set_entry(table_name, interface_name, {"vrf_name": vrf_name}) + +# +# 'unbind' subcommand +# + +@vrf.command() +@click.argument('interface_name', metavar='', required=True) +@click.pass_context +def unbind(ctx, interface_name): + """Unbind the interface to VRF""" + config_db = ctx.obj["config_db"] + if get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(interface_name) + if interface_name is None: + ctx.fail("interface is None!") + + table_name = get_interface_table_name(interface_name) + if table_name == "": + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan/Loopback]") + if is_interface_bind_to_vrf(config_db, interface_name) is False: + return + interface_dependent = interface_ipaddr_dependent_on_interface(config_db, interface_name) + for interface_del in interface_dependent: + config_db.set_entry(table_name, interface_del, None) + config_db.set_entry(table_name, interface_name, None) + + +# +# 'vrf' group ('config vrf ...') +# + +@config.group() +@click.pass_context +def vrf(ctx): + """VRF-related configuration tasks""" + config_db = ConfigDBConnector() + config_db.connect() + ctx.obj = {} + ctx.obj['config_db'] = config_db + pass + +@vrf.command('add') +@click.argument('vrf_name', metavar='', required=True) +@click.pass_context +def add_vrf(ctx, vrf_name): + """Add vrf""" + config_db = ctx.obj['config_db'] + if not vrf_name.startswith("Vrf"): + ctx.fail("'vrf_name' is not start with Vrf!") + if len(vrf_name) > 15: + ctx.fail("'vrf_name' is too long!") + config_db.set_entry('VRF', vrf_name, {"NULL": "NULL"}) + +@vrf.command('del') +@click.argument('vrf_name', metavar='', required=True) +@click.pass_context +def del_vrf(ctx, vrf_name): + """Del vrf""" + config_db = ctx.obj['config_db'] + if not vrf_name.startswith("Vrf"): + ctx.fail("'vrf_name' is not start with Vrf!") + if len(vrf_name) > 15: + ctx.fail("'vrf_name' is too long!") + del_interface_bind_to_vrf(config_db, vrf_name) + config_db.set_entry('VRF', vrf_name, None) + + +# +# 'route' group ('config route ...') +# + +@config.group() +@click.pass_context +def route(ctx): + """route-related configuration tasks""" + pass + +@route.command('add',context_settings={"ignore_unknown_options":True}) +@click.argument('command_str', metavar='prefix [vrf ] nexthop <[vrf ] >|>', nargs=-1, type=click.Path()) +@click.pass_context +def add_route(ctx, command_str): + """Add route command""" + if len(command_str) < 4 or len(command_str) > 9: + ctx.fail("argument is not in pattern prefix [vrf ] nexthop <[vrf ] >|>!") + if "prefix" not in command_str: + ctx.fail("argument is incomplete, prefix not found!") + if "nexthop" not in command_str: + ctx.fail("argument is incomplete, nexthop not found!") + for i in range(0,len(command_str)): + if "nexthop" == command_str[i]: + prefix_str = command_str[:i] + nexthop_str = command_str[i:] + vrf_name = "" + cmd = 'sudo vtysh -c "configure terminal" -c "ip route' + if prefix_str: + if len(prefix_str) == 2: + prefix_mask = prefix_str[1] + cmd += ' {}'.format(prefix_mask) + elif len(prefix_str) == 4: + vrf_name = prefix_str[2] + prefix_mask = prefix_str[3] + cmd += ' {}'.format(prefix_mask) + else: + ctx.fail("prefix is not in pattern!") + if nexthop_str: + if len(nexthop_str) == 2: + ip = nexthop_str[1] + if vrf_name == "": + cmd += ' {}'.format(ip) + else: + cmd += ' {} vrf {}'.format(ip, vrf_name) + elif len(nexthop_str) == 3: + dev_name = nexthop_str[2] + if vrf_name == "": + cmd += ' {}'.format(dev_name) + else: + cmd += ' {} vrf {}'.format(dev_name, vrf_name) + elif len(nexthop_str) == 4: + vrf_name_dst = nexthop_str[2] + ip = nexthop_str[3] + if vrf_name == "": + cmd += ' {} nexthop-vrf {}'.format(ip, vrf_name_dst) + else: + cmd += ' {} vrf {} nexthop-vrf {}'.format(ip, vrf_name, vrf_name_dst) + else: + ctx.fail("nexthop is not in pattern!") + cmd += '"' + run_command(cmd) + +@route.command('del',context_settings={"ignore_unknown_options":True}) +@click.argument('command_str', metavar='prefix [vrf ] nexthop <[vrf ] >|>', nargs=-1, type=click.Path()) +@click.pass_context +def del_route(ctx, command_str): + """Del route command""" + if len(command_str) < 4 or len(command_str) > 9: + ctx.fail("argument is not in pattern prefix [vrf ] nexthop <[vrf ] >|>!") + if "prefix" not in command_str: + ctx.fail("argument is incomplete, prefix not found!") + if "nexthop" not in command_str: + ctx.fail("argument is incomplete, nexthop not found!") + for i in range(0,len(command_str)): + if "nexthop" == command_str[i]: + prefix_str = command_str[:i] + nexthop_str = command_str[i:] + vrf_name = "" + cmd = 'sudo vtysh -c "configure terminal" -c "no ip route' + if prefix_str: + if len(prefix_str) == 2: + prefix_mask = prefix_str[1] + cmd += ' {}'.format(prefix_mask) + elif len(prefix_str) == 4: + vrf_name = prefix_str[2] + prefix_mask = prefix_str[3] + cmd += ' {}'.format(prefix_mask) + else: + ctx.fail("prefix is not in pattern!") + if nexthop_str: + if len(nexthop_str) == 2: + ip = nexthop_str[1] + if vrf_name == "": + cmd += ' {}'.format(ip) + else: + cmd += ' {} vrf {}'.format(ip, vrf_name) + elif len(nexthop_str) == 3: + dev_name = nexthop_str[2] + if vrf_name == "": + cmd += ' {}'.format(dev_name) + else: + cmd += ' {} vrf {}'.format(dev_name, vrf_name) + elif len(nexthop_str) == 4: + vrf_name_dst = nexthop_str[2] + ip = nexthop_str[3] + if vrf_name == "": + cmd += ' {} nexthop-vrf {}'.format(ip, vrf_name_dst) + else: + cmd += ' {} vrf {} nexthop-vrf {}'.format(ip, vrf_name, vrf_name_dst) + else: + ctx.fail("nexthop is not in pattern!") + cmd += '"' + run_command(cmd) # # 'acl' group ('config acl ...') diff --git a/show/main.py b/show/main.py index bb2556403334..d452bc1d842e 100755 --- a/show/main.py +++ b/show/main.py @@ -406,6 +406,47 @@ def cli(): """SONiC command line - 'show' command""" pass +# +# 'vrf' command ("show vrf") +# + +def get_interface_bind_to_vrf(config_db, vrf_name): + """Get interfaces belong to vrf + """ + tables = ['INTERFACE', 'PORTCHANNEL_INTERFACE', 'VLAN_INTERFACE', 'LOOPBACK_INTERFACE'] + data = [] + for table_name in tables: + interface_dict = config_db.get_table(table_name) + if interface_dict: + for interface in interface_dict.keys(): + if interface_dict[interface].has_key('vrf_name') and vrf_name == interface_dict[interface]['vrf_name']: + data.append(interface) + return data + +@cli.command() +@click.argument('vrf_name', required=False) +def vrf(vrf_name): + """Show vrf config""" + config_db = ConfigDBConnector() + config_db.connect() + header = ['VRF', 'Interfaces'] + body = [] + vrf_dict = config_db.get_table('VRF') + if vrf_dict: + vrfs = [] + if vrf_name is None: + vrfs = vrf_dict.keys() + elif vrf_name in vrf_dict.keys(): + vrfs = [vrf_name] + for vrf in vrfs: + intfs = get_interface_bind_to_vrf(config_db, vrf) + if len(intfs) == 0: + body.append([vrf, ""]) + else: + body.append([vrf, intfs[0]]) + for intf in intfs[1:]: + body.append(["", intf]) + click.echo(tabulate(body, header)) # # 'arp' command ("show arp") @@ -1117,17 +1158,32 @@ def get_if_oper_state(iface): return "down" +# +# get_if_master +# +# Given an interface name, return its master reported by the kernel. +# +def get_if_master(iface): + oper_file = "/sys/class/net/{0}/master" + + if os.path.exists(oper_file.format(iface)): + real_path = os.path.realpath(oper_file.format(iface)) + return os.path.basename(real_path) + else: + return "" + + # # 'show ip interfaces' command # -# Display all interfaces with an IPv4 address, admin/oper states, their BGP neighbor name and peer ip. +# Display all interfaces with master, an IPv4 address, admin/oper states, their BGP neighbor name and peer ip. # Addresses from all scopes are included. Interfaces with no addresses are # excluded. # @ip.command() def interfaces(): """Show interfaces IPv4 address""" - header = ['Interface', 'IPv4 address/mask', 'Admin/Oper', 'BGP Neighbor', 'Neighbor IP'] + header = ['Interface', 'Master', 'IPv4 address/mask', 'Admin/Oper', 'BGP Neighbor', 'Neighbor IP'] data = [] bgp_peer = get_bgp_peer() @@ -1156,14 +1212,14 @@ def interfaces(): oper = get_if_oper_state(iface) else: oper = "down" - + master = get_if_master(iface) if get_interface_mode() == "alias": iface = iface_alias_converter.name_to_alias(iface) - data.append([iface, ifaddresses[0][1], admin + "/" + oper, neighbor_name, neighbor_ip]) + data.append([iface, master, ifaddresses[0][1], admin + "/" + oper, neighbor_name, neighbor_ip]) for ifaddr in ifaddresses[1:]: - data.append(["", ifaddr[1], ""]) + data.append(["", "", ifaddr[1], ""]) print tabulate(data, header, tablefmt="simple", stralign='left', missingval="") @@ -1192,14 +1248,14 @@ def get_bgp_peer(): # @ip.command() -@click.argument('ipaddress', required=False) +@click.argument('args', metavar='[IPADDRESS] [vrf ] [...]', nargs=-1, required=False) @click.option('--verbose', is_flag=True, help="Enable verbose output") -def route(ipaddress, verbose): +def route(args, verbose): """Show IP (IPv4) routing table""" cmd = 'sudo vtysh -c "show ip route' - if ipaddress is not None: - cmd += ' {}'.format(ipaddress) + for arg in args: + cmd += " " + str(arg) cmd += '"' @@ -1260,14 +1316,14 @@ def prefix_list(prefix_list_name, verbose): # # 'show ipv6 interfaces' command # -# Display all interfaces with an IPv6 address, admin/oper states, their BGP neighbor name and peer ip. +# Display all interfaces with master, an IPv6 address, admin/oper states, their BGP neighbor name and peer ip. # Addresses from all scopes are included. Interfaces with no addresses are # excluded. # @ipv6.command() def interfaces(): """Show interfaces IPv6 address""" - header = ['Interface', 'IPv6 address/mask', 'Admin/Oper', 'BGP Neighbor', 'Neighbor IP'] + header = ['Interface', 'Master', 'IPv6 address/mask', 'Admin/Oper', 'BGP Neighbor', 'Neighbor IP'] data = [] bgp_peer = get_bgp_peer() @@ -1296,11 +1352,12 @@ def interfaces(): oper = get_if_oper_state(iface) else: oper = "down" + master = get_if_master(iface) if get_interface_mode() == "alias": iface = iface_alias_converter.name_to_alias(iface) - data.append([iface, ifaddresses[0][1], admin + "/" + oper, neighbor_name, neighbor_ip]) + data.append([iface, master, ifaddresses[0][1], admin + "/" + oper, neighbor_name, neighbor_ip]) for ifaddr in ifaddresses[1:]: - data.append(["", ifaddr[1], ""]) + data.append(["", "", ifaddr[1], ""]) print tabulate(data, header, tablefmt="simple", stralign='left', missingval="") @@ -1310,14 +1367,14 @@ def interfaces(): # @ipv6.command() -@click.argument('ipaddress', required=False) +@click.argument('args', metavar='[IPADDRESS] [vrf ] [...]', nargs=-1, required=False) @click.option('--verbose', is_flag=True, help="Enable verbose output") -def route(ipaddress, verbose): +def route(args, verbose): """Show IPv6 routing table""" cmd = 'sudo vtysh -c "show ipv6 route' - if ipaddress is not None: - cmd += ' {}'.format(ipaddress) + for arg in args: + cmd += " " + str(arg) cmd += '"'