410 lines
15 KiB
Python
410 lines
15 KiB
Python
|
# SPDX-License-Identifier: ISC
|
||
|
#
|
||
|
# Modified work Copyright (c) 2019 by VMware, Inc. ("VMware")
|
||
|
# Original work Copyright (c) 2018 by Network Device Education
|
||
|
# Foundation, Inc. ("NetDEF")
|
||
|
#
|
||
|
|
||
|
import json
|
||
|
import ipaddress
|
||
|
import os
|
||
|
from collections import OrderedDict
|
||
|
from copy import deepcopy
|
||
|
from re import search as re_search
|
||
|
|
||
|
import pytest
|
||
|
|
||
|
from lib.bgp import create_router_bgp
|
||
|
from lib.common_config import (
|
||
|
create_bgp_community_lists,
|
||
|
create_interfaces_cfg,
|
||
|
create_prefix_lists,
|
||
|
create_route_maps,
|
||
|
create_static_routes,
|
||
|
create_vrf_cfg,
|
||
|
load_config_to_routers,
|
||
|
start_topology,
|
||
|
topo_daemons,
|
||
|
number_to_column,
|
||
|
)
|
||
|
from lib.ospf import create_router_ospf
|
||
|
from lib.pim import (
|
||
|
create_igmp_config,
|
||
|
create_pim_config,
|
||
|
create_mld_config,
|
||
|
)
|
||
|
from lib.topolog import logger
|
||
|
|
||
|
|
||
|
def build_topo_from_json(tgen, topo=None):
|
||
|
"""
|
||
|
Reads configuration from JSON file. Adds routers, creates interface
|
||
|
names dynamically and link routers as defined in JSON to create
|
||
|
topology. Assigns IPs dynamically to all interfaces of each router.
|
||
|
* `tgen`: Topogen object
|
||
|
* `topo`: json file data, or use tgen.json_topo if None
|
||
|
"""
|
||
|
if topo is None:
|
||
|
topo = tgen.json_topo
|
||
|
|
||
|
router_list = sorted(
|
||
|
topo["routers"].keys(), key=lambda x: int(re_search(r"\d+", x).group(0))
|
||
|
)
|
||
|
|
||
|
switch_list = []
|
||
|
if "switches" in topo:
|
||
|
switch_list = sorted(
|
||
|
topo["switches"].keys(), key=lambda x: int(re_search(r"\d+", x).group(0))
|
||
|
)
|
||
|
|
||
|
listRouters = sorted(router_list[:])
|
||
|
listSwitches = sorted(switch_list[:])
|
||
|
listAllRouters = deepcopy(listRouters)
|
||
|
dictSwitches = {}
|
||
|
|
||
|
for routerN in router_list:
|
||
|
logger.info("Topo: Add router {}".format(routerN))
|
||
|
tgen.add_router(routerN)
|
||
|
|
||
|
for switchN in switch_list:
|
||
|
logger.info("Topo: Add switch {}".format(switchN))
|
||
|
dictSwitches[switchN] = tgen.add_switch(switchN)
|
||
|
|
||
|
if "ipv4base" in topo:
|
||
|
ipv4Next = ipaddress.IPv4Address(topo["link_ip_start"]["ipv4"])
|
||
|
ipv4Step = 2 ** (32 - topo["link_ip_start"]["v4mask"])
|
||
|
if topo["link_ip_start"]["v4mask"] < 32:
|
||
|
ipv4Next += 1
|
||
|
if "ipv6base" in topo:
|
||
|
ipv6Next = ipaddress.IPv6Address(topo["link_ip_start"]["ipv6"])
|
||
|
ipv6Step = 2 ** (128 - topo["link_ip_start"]["v6mask"])
|
||
|
if topo["link_ip_start"]["v6mask"] < 127:
|
||
|
ipv6Next += 1
|
||
|
for router in listRouters:
|
||
|
topo["routers"][router]["nextIfname"] = 0
|
||
|
|
||
|
router_count = 0
|
||
|
while listRouters != []:
|
||
|
curRouter = listRouters.pop(0)
|
||
|
# Physical Interfaces
|
||
|
if "links" in topo["routers"][curRouter]:
|
||
|
for destRouterLink, data in sorted(
|
||
|
topo["routers"][curRouter]["links"].items()
|
||
|
):
|
||
|
currRouter_lo_json = topo["routers"][curRouter]["links"][destRouterLink]
|
||
|
# Loopback interfaces
|
||
|
if "type" in data and data["type"] == "loopback":
|
||
|
router_count += 1
|
||
|
if (
|
||
|
"ipv4" in currRouter_lo_json
|
||
|
and currRouter_lo_json["ipv4"] == "auto"
|
||
|
):
|
||
|
currRouter_lo_json["ipv4"] = "{}{}.{}/{}".format(
|
||
|
topo["lo_prefix"]["ipv4"],
|
||
|
router_count,
|
||
|
number_to_column(curRouter),
|
||
|
topo["lo_prefix"]["v4mask"],
|
||
|
)
|
||
|
if (
|
||
|
"ipv6" in currRouter_lo_json
|
||
|
and currRouter_lo_json["ipv6"] == "auto"
|
||
|
):
|
||
|
currRouter_lo_json["ipv6"] = "{}{}:{}/{}".format(
|
||
|
topo["lo_prefix"]["ipv6"],
|
||
|
router_count,
|
||
|
number_to_column(curRouter),
|
||
|
topo["lo_prefix"]["v6mask"],
|
||
|
)
|
||
|
|
||
|
if "-" in destRouterLink:
|
||
|
# Spliting and storing destRouterLink data in tempList
|
||
|
tempList = destRouterLink.split("-")
|
||
|
|
||
|
# destRouter
|
||
|
destRouter = tempList.pop(0)
|
||
|
|
||
|
# Current Router Link
|
||
|
tempList.insert(0, curRouter)
|
||
|
curRouterLink = "-".join(tempList)
|
||
|
else:
|
||
|
destRouter = destRouterLink
|
||
|
curRouterLink = curRouter
|
||
|
|
||
|
if destRouter in listRouters:
|
||
|
currRouter_link_json = topo["routers"][curRouter]["links"][
|
||
|
destRouterLink
|
||
|
]
|
||
|
destRouter_link_json = topo["routers"][destRouter]["links"][
|
||
|
curRouterLink
|
||
|
]
|
||
|
|
||
|
# Assigning name to interfaces
|
||
|
currRouter_link_json["interface"] = "{}-{}-eth{}".format(
|
||
|
curRouter, destRouter, topo["routers"][curRouter]["nextIfname"]
|
||
|
)
|
||
|
destRouter_link_json["interface"] = "{}-{}-eth{}".format(
|
||
|
destRouter, curRouter, topo["routers"][destRouter]["nextIfname"]
|
||
|
)
|
||
|
|
||
|
# add link interface
|
||
|
destRouter_link_json["peer-interface"] = "{}-{}-eth{}".format(
|
||
|
curRouter, destRouter, topo["routers"][curRouter]["nextIfname"]
|
||
|
)
|
||
|
currRouter_link_json["peer-interface"] = "{}-{}-eth{}".format(
|
||
|
destRouter, curRouter, topo["routers"][destRouter]["nextIfname"]
|
||
|
)
|
||
|
|
||
|
topo["routers"][curRouter]["nextIfname"] += 1
|
||
|
topo["routers"][destRouter]["nextIfname"] += 1
|
||
|
|
||
|
# Linking routers to each other as defined in JSON file
|
||
|
tgen.gears[curRouter].add_link(
|
||
|
tgen.gears[destRouter],
|
||
|
topo["routers"][curRouter]["links"][destRouterLink][
|
||
|
"interface"
|
||
|
],
|
||
|
topo["routers"][destRouter]["links"][curRouterLink][
|
||
|
"interface"
|
||
|
],
|
||
|
)
|
||
|
|
||
|
# IPv4
|
||
|
if "ipv4" in currRouter_link_json:
|
||
|
if currRouter_link_json["ipv4"] == "auto":
|
||
|
currRouter_link_json["ipv4"] = "{}/{}".format(
|
||
|
ipv4Next, topo["link_ip_start"]["v4mask"]
|
||
|
)
|
||
|
destRouter_link_json["ipv4"] = "{}/{}".format(
|
||
|
ipv4Next + 1, topo["link_ip_start"]["v4mask"]
|
||
|
)
|
||
|
ipv4Next += ipv4Step
|
||
|
# IPv6
|
||
|
if "ipv6" in currRouter_link_json:
|
||
|
if currRouter_link_json["ipv6"] == "auto":
|
||
|
currRouter_link_json["ipv6"] = "{}/{}".format(
|
||
|
ipv6Next, topo["link_ip_start"]["v6mask"]
|
||
|
)
|
||
|
destRouter_link_json["ipv6"] = "{}/{}".format(
|
||
|
ipv6Next + 1, topo["link_ip_start"]["v6mask"]
|
||
|
)
|
||
|
ipv6Next = ipaddress.IPv6Address(int(ipv6Next) + ipv6Step)
|
||
|
|
||
|
logger.debug(
|
||
|
"Generated link data for router: %s\n%s",
|
||
|
curRouter,
|
||
|
json.dumps(
|
||
|
topo["routers"][curRouter]["links"], indent=4, sort_keys=True
|
||
|
),
|
||
|
)
|
||
|
|
||
|
switch_count = 0
|
||
|
add_switch_to_topo = []
|
||
|
while listSwitches != []:
|
||
|
curSwitch = listSwitches.pop(0)
|
||
|
# Physical Interfaces
|
||
|
if "links" in topo["switches"][curSwitch]:
|
||
|
for destRouterLink, data in sorted(
|
||
|
topo["switches"][curSwitch]["links"].items()
|
||
|
):
|
||
|
# Loopback interfaces
|
||
|
if "dst_node" in data:
|
||
|
destRouter = data["dst_node"]
|
||
|
|
||
|
elif "-" in destRouterLink:
|
||
|
# Spliting and storing destRouterLink data in tempList
|
||
|
tempList = destRouterLink.split("-")
|
||
|
# destRouter
|
||
|
destRouter = tempList.pop(0)
|
||
|
else:
|
||
|
destRouter = destRouterLink
|
||
|
|
||
|
if destRouter in listAllRouters:
|
||
|
topo["routers"][destRouter]["links"][curSwitch] = deepcopy(
|
||
|
topo["switches"][curSwitch]["links"][destRouterLink]
|
||
|
)
|
||
|
|
||
|
# Assigning name to interfaces
|
||
|
topo["routers"][destRouter]["links"][curSwitch][
|
||
|
"interface"
|
||
|
] = "{}-{}-eth{}".format(
|
||
|
destRouter,
|
||
|
curSwitch,
|
||
|
topo["routers"][destRouter]["nextIfname"],
|
||
|
)
|
||
|
|
||
|
topo["switches"][curSwitch]["links"][destRouter][
|
||
|
"interface"
|
||
|
] = "{}-{}-eth{}".format(
|
||
|
curSwitch,
|
||
|
destRouter,
|
||
|
topo["routers"][destRouter]["nextIfname"],
|
||
|
)
|
||
|
|
||
|
topo["routers"][destRouter]["nextIfname"] += 1
|
||
|
|
||
|
# Add links
|
||
|
dictSwitches[curSwitch].add_link(
|
||
|
tgen.gears[destRouter],
|
||
|
topo["switches"][curSwitch]["links"][destRouter]["interface"],
|
||
|
topo["routers"][destRouter]["links"][curSwitch]["interface"],
|
||
|
)
|
||
|
|
||
|
# IPv4
|
||
|
if "ipv4" in topo["routers"][destRouter]["links"][curSwitch]:
|
||
|
if (
|
||
|
topo["routers"][destRouter]["links"][curSwitch]["ipv4"]
|
||
|
== "auto"
|
||
|
):
|
||
|
topo["routers"][destRouter]["links"][curSwitch][
|
||
|
"ipv4"
|
||
|
] = "{}/{}".format(
|
||
|
ipv4Next, topo["link_ip_start"]["v4mask"]
|
||
|
)
|
||
|
ipv4Next += 1
|
||
|
# IPv6
|
||
|
if "ipv6" in topo["routers"][destRouter]["links"][curSwitch]:
|
||
|
if (
|
||
|
topo["routers"][destRouter]["links"][curSwitch]["ipv6"]
|
||
|
== "auto"
|
||
|
):
|
||
|
topo["routers"][destRouter]["links"][curSwitch][
|
||
|
"ipv6"
|
||
|
] = "{}/{}".format(
|
||
|
ipv6Next, topo["link_ip_start"]["v6mask"]
|
||
|
)
|
||
|
ipv6Next = ipaddress.IPv6Address(int(ipv6Next) + ipv6Step)
|
||
|
|
||
|
logger.debug(
|
||
|
"Generated link data for router: %s\n%s",
|
||
|
curRouter,
|
||
|
json.dumps(
|
||
|
topo["routers"][curRouter]["links"], indent=4, sort_keys=True
|
||
|
),
|
||
|
)
|
||
|
|
||
|
|
||
|
def linux_intf_config_from_json(tgen, topo=None):
|
||
|
"""Configure interfaces from linux based on topo."""
|
||
|
if topo is None:
|
||
|
topo = tgen.json_topo
|
||
|
|
||
|
routers = topo["routers"]
|
||
|
for rname in routers:
|
||
|
router = tgen.net[rname]
|
||
|
links = routers[rname]["links"]
|
||
|
for rrname in links:
|
||
|
link = links[rrname]
|
||
|
if rrname == "lo":
|
||
|
lname = "lo"
|
||
|
else:
|
||
|
lname = link["interface"]
|
||
|
if "ipv4" in link:
|
||
|
router.cmd_raises("ip addr add {} dev {}".format(link["ipv4"], lname))
|
||
|
if "ipv6" in link:
|
||
|
router.cmd_raises(
|
||
|
"ip -6 addr add {} dev {}".format(link["ipv6"], lname)
|
||
|
)
|
||
|
|
||
|
|
||
|
def build_config_from_json(tgen, topo=None, save_bkup=True):
|
||
|
"""
|
||
|
Reads initial configuraiton from JSON for each router, builds
|
||
|
configuration and loads its to router.
|
||
|
|
||
|
* `tgen`: Topogen object
|
||
|
* `topo`: json file data, or use tgen.json_topo if None
|
||
|
"""
|
||
|
|
||
|
func_dict = OrderedDict(
|
||
|
[
|
||
|
("vrfs", create_vrf_cfg),
|
||
|
("ospf", create_router_ospf),
|
||
|
("links", create_interfaces_cfg),
|
||
|
("static_routes", create_static_routes),
|
||
|
("prefix_lists", create_prefix_lists),
|
||
|
("bgp_community_list", create_bgp_community_lists),
|
||
|
("route_maps", create_route_maps),
|
||
|
("pim", create_pim_config),
|
||
|
("igmp", create_igmp_config),
|
||
|
("mld", create_mld_config),
|
||
|
("bgp", create_router_bgp),
|
||
|
]
|
||
|
)
|
||
|
|
||
|
if topo is None:
|
||
|
topo = tgen.json_topo
|
||
|
|
||
|
data = topo["routers"]
|
||
|
for func_type in func_dict.keys():
|
||
|
logger.info("Checking for {} configuration in input data".format(func_type))
|
||
|
|
||
|
func_dict.get(func_type)(tgen, data, build=True)
|
||
|
|
||
|
routers = sorted(topo["routers"].keys())
|
||
|
result = load_config_to_routers(tgen, routers, save_bkup)
|
||
|
if not result:
|
||
|
logger.info("build_config_from_json: failed to configure topology")
|
||
|
assert False
|
||
|
|
||
|
logger.info(
|
||
|
"Built config now clearing ospf neighbors as that router-id might not be what is used"
|
||
|
)
|
||
|
for ospf in ["ospf", "ospf6"]:
|
||
|
for router in data:
|
||
|
if ospf not in data[router]:
|
||
|
continue
|
||
|
|
||
|
r = tgen.gears[router]
|
||
|
if ospf == "ospf":
|
||
|
r.vtysh_cmd("clear ip ospf process")
|
||
|
else:
|
||
|
r.vtysh_cmd("clear ipv6 ospf6 process")
|
||
|
|
||
|
|
||
|
def create_tgen_from_json(testfile, json_file=None):
|
||
|
"""Create a topogen object given a testfile.
|
||
|
|
||
|
- `testfile` : The path to the testfile.
|
||
|
- `json_file` : The path to the json config file. If None the pathname is derived
|
||
|
from the `testfile` first by trying to replace `.py` by `.json` and if that isn't
|
||
|
present then by removing `test_` prefix as well.
|
||
|
"""
|
||
|
from lib.topogen import Topogen # Topogen imports this module too
|
||
|
|
||
|
thisdir = os.path.dirname(os.path.realpath(testfile))
|
||
|
basename = os.path.basename(testfile)
|
||
|
logger.debug("starting standard JSON based module setup for %s", basename)
|
||
|
|
||
|
assert basename.startswith("test_")
|
||
|
assert basename.endswith(".py")
|
||
|
json_file = os.path.join(thisdir, basename[:-3] + ".json")
|
||
|
if not os.path.exists(json_file):
|
||
|
json_file = os.path.join(thisdir, basename[5:-3] + ".json")
|
||
|
assert os.path.exists(json_file)
|
||
|
with open(json_file, "r") as topof:
|
||
|
topo = json.load(topof)
|
||
|
|
||
|
# Create topology
|
||
|
tgen = Topogen(lambda tgen: build_topo_from_json(tgen, topo), basename[:-3])
|
||
|
tgen.json_topo = topo
|
||
|
return tgen
|
||
|
|
||
|
|
||
|
def setup_module_from_json(testfile, json_file=None):
|
||
|
"""Do the standard module setup for JSON based test.
|
||
|
|
||
|
* `testfile` : The path to the testfile. The name is used to derive the json config
|
||
|
file name as well (removing `test_` prefix and replacing `.py` suffix with `.json`
|
||
|
"""
|
||
|
# Create topology object
|
||
|
tgen = create_tgen_from_json(testfile, json_file)
|
||
|
|
||
|
# Start routers (and their daemons)
|
||
|
start_topology(tgen)
|
||
|
|
||
|
# Configure routers
|
||
|
build_config_from_json(tgen)
|
||
|
assert not tgen.routers_have_failure()
|
||
|
|
||
|
return tgen
|