Skip to content

Commit 2d17ece

Browse files
authored
Encap type support for edge and isls (#120)
* update ignore * encap type args for edge and isl * ruff lint
1 parent 0737354 commit 2d17ece

File tree

13 files changed

+166
-21
lines changed

13 files changed

+166
-21
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,6 @@ example-topologies/*/
166166

167167
.envrc
168168
CLAUDE.md
169-
AGENTS.md
169+
AGENTS.md
170+
license.txt
171+
patch.sh

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ clab-connector integrate \
148148
| `--log-file`, `-f` | No | None | Optional log file path |
149149
| `--verify` | No | False | Enable certificate verification for EDA |
150150
| `--skip-edge-intfs` | No | False | Skip creation of edge links and their interfaces |
151+
| `--edge-encapsulation` | No | None | Encapsulation for generated edge interfaces (`dot1q`) |
152+
| `--isl-encapsulation` | No | None | Encapsulation for inter-switch interfaces (`dot1q`) |
151153
152154
153155
> [!NOTE]
@@ -253,6 +255,8 @@ clab-connector generate-crs \
253255
| `--log-level`, `-l` | No | INFO | Logging level (DEBUG/INFO/WARNING/ERROR/CRITICAL) |
254256
| `--log-file`, `-f` | No | None | Optional log file path |
255257
| `--skip-edge-intfs` | No | False | Skip creation of edge links and their interfaces |
258+
| `--edge-encapsulation` | No | None | Encapsulation for generated edge interfaces (`dot1q`) |
259+
| `--isl-encapsulation` | No | None | Encapsulation for inter-switch interfaces (`dot1q`) |
256260
| `--namespace`, `-n` | No | None | Namespace to use instead of deriving from the topology |
257261
258262

clab_connector/cli/main.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ class LogLevel(str, Enum):
3535
CRITICAL = "CRITICAL"
3636

3737

38+
class InterfaceEncapsulation(str, Enum):
39+
UNTAGGED = "untagged"
40+
DOT1Q = "dot1q"
41+
42+
3843
app = typer.Typer(
3944
name="clab-connector",
4045
help="Integrate or remove an existing containerlab topology with EDA (Event-Driven Automation)",
@@ -55,6 +60,20 @@ class LogLevel(str, Enum):
5560
help="Optional log file path",
5661
)
5762

63+
edge_encapsulation_option = typer.Option(
64+
None,
65+
"--edge-encapsulation",
66+
help="Encapsulation type for generated edge interfaces",
67+
case_sensitive=False,
68+
)
69+
70+
isl_encapsulation_option = typer.Option(
71+
None,
72+
"--isl-encapsulation",
73+
help="Encapsulation type for generated inter-switch link interfaces",
74+
case_sensitive=False,
75+
)
76+
5877

5978
def complete_json_files(
6079
_ctx: typer.Context, _param: typer.Option, incomplete: str
@@ -81,7 +100,7 @@ def complete_eda_url(
81100

82101

83102
@app.command(name="integrate", help="Integrate containerlab with EDA")
84-
def integrate_cmd(
103+
def integrate_cmd( # noqa: PLR0913
85104
topology_data: Annotated[
86105
Path,
87106
typer.Option(
@@ -147,6 +166,8 @@ def integrate_cmd(
147166
"--sync-timeout",
148167
help="Timeout for node synchronization check in seconds",
149168
),
169+
edge_encapsulation: InterfaceEncapsulation | None = edge_encapsulation_option,
170+
isl_encapsulation: InterfaceEncapsulation | None = isl_encapsulation_option,
150171
):
151172
"""CLI command to integrate a containerlab topology with EDA."""
152173

@@ -172,6 +193,14 @@ class Args:
172193
args.skip_edge_intfs = skip_edge_intfs
173194
args.enable_sync_check = enable_sync_check
174195
args.sync_timeout = sync_timeout
196+
if edge_encapsulation and edge_encapsulation != InterfaceEncapsulation.UNTAGGED:
197+
args.edge_encapsulation = edge_encapsulation.value
198+
else:
199+
args.edge_encapsulation = None
200+
if isl_encapsulation and isl_encapsulation != InterfaceEncapsulation.UNTAGGED:
201+
args.isl_encapsulation = isl_encapsulation.value
202+
else:
203+
args.isl_encapsulation = None
175204

176205
def execute_integration(a):
177206
eda_client = create_eda_client(
@@ -193,6 +222,8 @@ def execute_integration(a):
193222
topology_file=a.topology_data,
194223
skip_edge_intfs=a.skip_edge_intfs,
195224
namespace_override=a.namespace_override,
225+
edge_encapsulation=a.edge_encapsulation,
226+
isl_encapsulation=a.isl_encapsulation,
196227
)
197228

198229
try:
@@ -364,6 +395,8 @@ def generate_crs_cmd(
364395
"--skip-edge-intfs",
365396
help="Skip creation of edge links and their interfaces",
366397
),
398+
edge_encapsulation: InterfaceEncapsulation | None = edge_encapsulation_option,
399+
isl_encapsulation: InterfaceEncapsulation | None = isl_encapsulation_option,
367400
):
368401
"""
369402
Generate the CR YAML manifests (artifacts, init, node security profile,
@@ -383,6 +416,18 @@ def generate_crs_cmd(
383416
separate=separate,
384417
skip_edge_intfs=skip_edge_intfs,
385418
namespace=namespace_override,
419+
edge_encapsulation=(
420+
edge_encapsulation.value
421+
if edge_encapsulation
422+
and edge_encapsulation != InterfaceEncapsulation.UNTAGGED
423+
else None
424+
),
425+
isl_encapsulation=(
426+
isl_encapsulation.value
427+
if isl_encapsulation
428+
and isl_encapsulation != InterfaceEncapsulation.UNTAGGED
429+
else None
430+
),
386431
)
387432
generator.generate()
388433
generator.output_manifests()

clab_connector/models/node/arista_ceos.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,23 +162,40 @@ def get_topolink_interface_name(self, topology, ifname):
162162
# No longer strip out the 'ethernet-' prefix to maintain consistency with SR Linux
163163
return f"{node_name}-{eda_ifname}"
164164

165-
def get_topolink_interface(self, topology, ifname, other_node):
165+
def get_topolink_interface(
166+
self,
167+
topology,
168+
ifname,
169+
other_node,
170+
edge_encapsulation: str | None = None,
171+
isl_encapsulation: str | None = None,
172+
):
166173
"""
167174
Render the Interface CR YAML for an cEOS link endpoint.
168175
"""
169176
logger.debug(f"{SUBSTEP_INDENT}Creating topolink interface for {self.name}")
170177
role = "interSwitch"
171178
if other_node is None or not other_node.is_eda_supported():
172179
role = "edge"
180+
peer_name = (
181+
other_node.get_node_name(topology)
182+
if other_node is not None
183+
else "external-endpoint"
184+
)
185+
if role == "edge":
186+
encap_type = "dot1q" if edge_encapsulation == "dot1q" else None
187+
else:
188+
encap_type = "dot1q" if isl_encapsulation == "dot1q" else None
189+
173190
data = {
174191
"namespace": topology.namespace,
175192
"interface_name": self.get_topolink_interface_name(topology, ifname),
176193
"label_key": "eda.nokia.com/role",
177194
"label_value": role,
178-
"encap_type": "'null'",
195+
"encap_type": encap_type,
179196
"node_name": self.get_node_name(topology),
180197
"interface": self.get_interface_name_for_kind(ifname),
181-
"description": f"{role} link to {other_node.get_node_name(topology)}",
198+
"description": f"{role} link to {peer_name}",
182199
}
183200
return helpers.render_template("interface.j2", data)
184201

clab_connector/models/node/base.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,14 @@ def get_topolink_interface_name(self, topology, ifname):
214214
f"{self.get_node_name(topology)}-{self.get_interface_name_for_kind(ifname)}"
215215
)
216216

217-
def get_topolink_interface(self, _topology, _ifname, _other_node):
217+
def get_topolink_interface(
218+
self,
219+
_topology,
220+
_ifname,
221+
_other_node,
222+
_edge_encapsulation: str | None = None,
223+
_isl_encapsulation: str | None = None,
224+
):
218225
"""
219226
Render and return the interface resource YAML (Interface CR) for a link endpoint.
220227

clab_connector/models/node/nokia_srl.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,14 @@ def get_interface_name_for_kind(self, ifname):
253253
return f"ethernet-{match.group(1)}-{match.group(2)}"
254254
return ifname
255255

256-
def get_topolink_interface(self, topology, ifname, other_node):
256+
def get_topolink_interface(
257+
self,
258+
topology,
259+
ifname,
260+
other_node,
261+
edge_encapsulation: str | None = None,
262+
isl_encapsulation: str | None = None,
263+
):
257264
"""
258265
Render the Interface CR YAML for an SR Linux link endpoint.
259266
@@ -275,15 +282,25 @@ def get_topolink_interface(self, topology, ifname, other_node):
275282
role = "interSwitch"
276283
if other_node is None or not other_node.is_eda_supported():
277284
role = "edge"
285+
peer_name = (
286+
other_node.get_node_name(topology)
287+
if other_node is not None
288+
else "external-endpoint"
289+
)
290+
if role == "edge":
291+
encap_type = "dot1q" if edge_encapsulation == "dot1q" else None
292+
else:
293+
encap_type = "dot1q" if isl_encapsulation == "dot1q" else None
294+
278295
data = {
279296
"namespace": topology.namespace,
280297
"interface_name": self.get_topolink_interface_name(topology, ifname),
281298
"label_key": "eda.nokia.com/role",
282299
"label_value": role,
283-
"encap_type": "'null'",
300+
"encap_type": encap_type,
284301
"node_name": self.get_node_name(topology),
285302
"interface": self.get_interface_name_for_kind(ifname),
286-
"description": f"{role} link to {other_node.get_node_name(topology)}",
303+
"description": f"{role} link to {peer_name}",
287304
}
288305
return helpers.render_template("interface.j2", data)
289306

clab_connector/models/node/nokia_sros.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -338,23 +338,40 @@ def get_topolink_interface_name(self, topology, ifname):
338338
# No longer strip out the 'ethernet-' prefix to maintain consistency with SR Linux
339339
return f"{node_name}-{eda_ifname}"
340340

341-
def get_topolink_interface(self, topology, ifname, other_node):
341+
def get_topolink_interface(
342+
self,
343+
topology,
344+
ifname,
345+
other_node,
346+
edge_encapsulation: str | None = None,
347+
isl_encapsulation: str | None = None,
348+
):
342349
"""
343350
Render the Interface CR YAML for an SROS link endpoint.
344351
"""
345352
logger.debug(f"{SUBSTEP_INDENT}Creating topolink interface for {self.name}")
346353
role = "interSwitch"
347354
if other_node is None or not other_node.is_eda_supported():
348355
role = "edge"
356+
peer_name = (
357+
other_node.get_node_name(topology)
358+
if other_node is not None
359+
else "external-endpoint"
360+
)
361+
if role == "edge":
362+
encap_type = "dot1q" if edge_encapsulation == "dot1q" else None
363+
else:
364+
encap_type = "dot1q" if isl_encapsulation == "dot1q" else None
365+
349366
data = {
350367
"namespace": topology.namespace,
351368
"interface_name": self.get_topolink_interface_name(topology, ifname),
352369
"label_key": "eda.nokia.com/role",
353370
"label_value": role,
354-
"encap_type": "'null'",
371+
"encap_type": encap_type,
355372
"node_name": self.get_node_name(topology),
356373
"interface": self.get_interface_name_for_kind(ifname),
357-
"description": f"{role} link to {other_node.get_node_name(topology)}",
374+
"description": f"{role} link to {peer_name}",
358375
}
359376
return helpers.render_template("interface.j2", data)
360377

clab_connector/models/topology.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,12 @@ def get_topolinks(self, skip_edge_links: bool = False):
175175
links.append(link_yaml)
176176
return links
177177

178-
def get_topolink_interfaces(self, skip_edge_link_interfaces: bool = False):
178+
def get_topolink_interfaces(
179+
self,
180+
skip_edge_link_interfaces: bool = False,
181+
edge_encapsulation: str | None = None,
182+
isl_encapsulation: str | None = None,
183+
):
179184
"""
180185
Generate Interface YAML for each link endpoint (if EDA-supported).
181186
@@ -205,7 +210,13 @@ def get_topolink_interfaces(self, skip_edge_link_interfaces: bool = False):
205210
and (peer is None or not peer.is_eda_supported())
206211
):
207212
continue
208-
intf_yaml = node.get_topolink_interface(self, ifname, peer)
213+
intf_yaml = node.get_topolink_interface(
214+
self,
215+
ifname,
216+
peer,
217+
edge_encapsulation=edge_encapsulation,
218+
isl_encapsulation=isl_encapsulation,
219+
)
209220
if intf_yaml:
210221
interfaces.append(intf_yaml)
211222
return interfaces

clab_connector/services/integration/topology_integrator.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,16 @@ def __init__(
4141
self.topology = None
4242
self.enable_sync_checking = enable_sync_checking
4343
self.sync_timeout = sync_timeout
44+
self.edge_encapsulation: str | None = None
45+
self.isl_encapsulation: str | None = None
4446

4547
def run(
4648
self,
4749
topology_file,
4850
skip_edge_intfs: bool = False,
4951
namespace_override: str | None = None,
52+
edge_encapsulation: str | None = None,
53+
isl_encapsulation: str | None = None,
5054
):
5155
"""
5256
Parse the topology, run connectivity checks, and create EDA resources.
@@ -73,6 +77,8 @@ def run(
7377
If any resource fails validation.
7478
"""
7579
logger.info("Parsing topology for integration")
80+
self.edge_encapsulation = edge_encapsulation
81+
self.isl_encapsulation = isl_encapsulation
7682
self.topology = parse_topology_file(
7783
str(topology_file), namespace=namespace_override
7884
)
@@ -119,7 +125,11 @@ def run(
119125
# Nodes are committed in batches within create_toponodes method
120126

121127
logger.info("== Adding topolink interfaces ==")
122-
self.create_topolink_interfaces(skip_edge_intfs)
128+
self.create_topolink_interfaces(
129+
skip_edge_intfs,
130+
edge_encapsulation=self.edge_encapsulation,
131+
isl_encapsulation=self.isl_encapsulation,
132+
)
123133
# Only commit if there are transactions
124134
if self.eda_client.transactions:
125135
self.commit_transaction("create topolink interfaces")
@@ -402,12 +412,19 @@ def create_toponodes(self):
402412
)
403413
time.sleep(batch_delay)
404414

405-
def create_topolink_interfaces(self, skip_edge_intfs: bool = False):
415+
def create_topolink_interfaces(
416+
self,
417+
skip_edge_intfs: bool = False,
418+
edge_encapsulation: str | None = None,
419+
isl_encapsulation: str | None = None,
420+
):
406421
"""
407422
Create Interface resources for each link endpoint in the topology.
408423
"""
409424
interfaces = self.topology.get_topolink_interfaces(
410-
skip_edge_link_interfaces=skip_edge_intfs
425+
skip_edge_link_interfaces=skip_edge_intfs,
426+
edge_encapsulation=edge_encapsulation,
427+
isl_encapsulation=isl_encapsulation,
411428
)
412429
for intf_yaml in interfaces:
413430
item = self.eda_client.add_replace_to_transaction(intf_yaml)

clab_connector/services/manifest/manifest_generator.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ def __init__(
3535
separate: bool = False,
3636
skip_edge_intfs: bool = False,
3737
namespace: str | None = None,
38+
edge_encapsulation: str | None = None,
39+
isl_encapsulation: str | None = None,
3840
):
3941
"""
4042
Parameters
@@ -59,6 +61,8 @@ def __init__(
5961
self.separate = separate
6062
self.skip_edge_intfs = skip_edge_intfs
6163
self.namespace_override = namespace
64+
self.edge_encapsulation = edge_encapsulation
65+
self.isl_encapsulation = isl_encapsulation
6266
self.topology = None
6367
# Dictionary mapping category name to a list of YAML document strings.
6468
self.cr_groups = {}
@@ -185,7 +189,9 @@ def generate(self):
185189

186190
# --- Topolink Interfaces
187191
intfs = self.topology.get_topolink_interfaces(
188-
skip_edge_link_interfaces=self.skip_edge_intfs
192+
skip_edge_link_interfaces=self.skip_edge_intfs,
193+
edge_encapsulation=self.edge_encapsulation,
194+
isl_encapsulation=self.isl_encapsulation,
189195
)
190196
if intfs:
191197
self.cr_groups["topolink-interfaces"] = list(intfs)

0 commit comments

Comments
 (0)