diff --git a/docs/plugins.md b/docs/plugins.md index 1aa12a9071..0f802e51ea 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -18,6 +18,7 @@ plugins/mlag.vtep.md plugins/multilab.md plugins/node.clone.md + plugins/ospf.stub.md plugins/vrrp.version.md plugins/firewall.zonebased.md ``` diff --git a/docs/plugins/ospf.stub.md b/docs/plugins/ospf.stub.md new file mode 100644 index 0000000000..1847960dbd --- /dev/null +++ b/docs/plugins/ospf.stub.md @@ -0,0 +1,42 @@ +(plugin-ospf-stub)= +# Fine-grained Control Over OSPF Area Route Propagation + +The **ospf.stub** plugin extends OSPF topology modeling by adding native support for stub, totally stubby, NSSA, +and totally NSSA areas. These specialized OSPF area types are commonly used to simplify routing in enterprise networks +by reducing the number of LSAs exchanged and minimizing routing table size in remote or branch locations. With this +plugin, Netlab will automatically generate the correct configurations on all routers in the area, including the +appropriate ABR settings. This makes it easy to model and validate optimized OSPF designs in your lab topologies. + +```eval_rst +.. contents:: Table of Contents + :depth: 2 + :local: + :backlinks: none +``` + +## Platform support + +| Operating system | Stub areas | +|---------------------|:----------:| +| Cumulus 5.x NVUE | ✅ | +| Dell OS10 | ✅ | +| FRR | ✅ | + +## Using the Plugin + +To use the plugin, add it to the **plugin** list in the lab topology: + +``` +plugin: [ ...., ospf.stub ] +``` + +This enables support for ospf.areas: + +``` +module: [ ospf ] + +ospf.areas: + 1: + kind: stub + no_summarize: True +``` diff --git a/netsim/extra/ospf.stub/cumulus_nvue.j2 b/netsim/extra/ospf.stub/cumulus_nvue.j2 new file mode 100644 index 0000000000..d7b0ef9f3e --- /dev/null +++ b/netsim/extra/ospf.stub/cumulus_nvue.j2 @@ -0,0 +1,10 @@ +{% for id,area in ospf.areas.items() %} +- set: + vrf: + default: + router: + ospf: + area: + {{ id | ipv4 }}: + type: {{ 'totally-' if area.get('no_summary') else '' }}{{ area.kind }} +{% endfor %} diff --git a/netsim/extra/ospf.stub/defaults.yml b/netsim/extra/ospf.stub/defaults.yml new file mode 100644 index 0000000000..c964d8fc85 --- /dev/null +++ b/netsim/extra/ospf.stub/defaults.yml @@ -0,0 +1,24 @@ +--- +devices: + cumulus_nvue.features.ospf: + stub: True + dellos10.features.ospf: + stub: True + frr.features.ospf: + stub: True + none.features.ospf: + stub: True + +ospf: + attributes: + global: + areas: + type: dict + _requires: [ ospf ] + _keytype: int + _subtype: + kind: { 'type': str, 'valid_values': [ stub, nssa, normal ] } + no_summary: bool # Only applied at ABR + range: { 'type': list, _subtype: { type: ipv4, use: subnet_prefix } } # Only for 'normal' areas + node: + areas: diff --git a/netsim/extra/ospf.stub/dellos10.j2 b/netsim/extra/ospf.stub/dellos10.j2 new file mode 100644 index 0000000000..ed4ef64889 --- /dev/null +++ b/netsim/extra/ospf.stub/dellos10.j2 @@ -0,0 +1,6 @@ +{% set pid = ospf.process|default(1) %} + +router ospf {{ pid }} +{% for id,area in ospf.areas.items() %} + area {{ id }} {{ area.kind }}{{ ' no-summary' if area.get('no_summary') else '' }} +{% endfor %} diff --git a/netsim/extra/ospf.stub/frr.j2 b/netsim/extra/ospf.stub/frr.j2 new file mode 100644 index 0000000000..86086f2790 --- /dev/null +++ b/netsim/extra/ospf.stub/frr.j2 @@ -0,0 +1,8 @@ +router ospf +{% for id,area in ospf.areas.items() %} +{% if area.kind != 'normal' %} + area {{ id }} {{ area.kind }}{{ ' no-summary' if area.get('no_summary') else '' }} +{% elif 'range' in area %} + area {{ id }} range {{ area.range }} +{% endif %} +{% endfor %} diff --git a/netsim/extra/ospf.stub/plugin.py b/netsim/extra/ospf.stub/plugin.py new file mode 100644 index 0000000000..b8e65d12b7 --- /dev/null +++ b/netsim/extra/ospf.stub/plugin.py @@ -0,0 +1,39 @@ +import typing +from box import Box +from netsim import api +from netsim.augment import devices +from netsim.utils import log +from ipaddress import IPv4Address + +_config_name = "ospf.stub" +_require = [ "ospf" ] + +def validate_area( id: int, area: Box ) -> None: + if area.get('kind','normal') != 'normal' and 'range' in area: + log.error(f"OSPF area {id} is of type {area.kind} and cannot support inter-area 'range' summarization") + +def post_transform(topology: Box) -> None: + for ndata in topology.nodes.values(): + if not 'ospf' in ndata.get('module',[]): + continue + ospf_areas = ndata.get('ospf.areas',{}) + if not ospf_areas: + continue + features = devices.get_device_features(ndata,topology.defaults) + if 'stub' not in features.ospf: + log.error(f"Node {ndata.name} (device {ndata.device}) not supported by the ospf.stub plugin") + continue + for k,v in ospf_areas.items(): + validate_area(k,v) # TODO once for entire topology, not per node + if ndata.get('ospf.area','0.0.0.0') != '0.0.0.0': # Check if node is not an ABR + areas = { intf.get('ospf.area') for intf in ndata.get('interfaces',[]) } + updated_areas = {} + for id,area in ospf_areas.items(): + if str(IPv4Address(id)) not in areas: + continue + for att in [ 'no_summary', 'range' ]: # Only applied at ABR + area.pop(att,None) + updated_areas[ id ] = area + ndata.ospf.areas = updated_areas + global _config_name + api.node_config(ndata,_config_name) diff --git a/tests/integration/ospf.stub/01-ospf-stub.yml b/tests/integration/ospf.stub/01-ospf-stub.yml new file mode 100644 index 0000000000..98e42c03f7 --- /dev/null +++ b/tests/integration/ospf.stub/01-ospf-stub.yml @@ -0,0 +1,25 @@ +--- +plugin: [ ospf.stub ] + +module: [ ospf ] + +ospf.areas: + 0: + kind: normal + range: 10.0.0.0/8 + 1: + kind: stub + no_summary: True + +nodes: + r1: + r2: + ospf.area: 1 + r3: + +links: +- r1: + r2: + ospf.area: 1 + +- r1-r3