Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ clab-*
**/__pycache__
*.pyc
.venv/*
*.tar**
*.tar**
.idea/
15 changes: 15 additions & 0 deletions builders/builder2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from abc import ABC, abstractmethod
from topology import Topology


class TopologyBuilder(ABC):
def __init__(self, topology: Topology):
self.topology = topology

@abstractmethod
def build_topology(self):
pass

@abstractmethod
def destroy_topology(self):
pass
110 changes: 110 additions & 0 deletions builders/clab_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from builders.builder2 import TopologyBuilder
from topology import Topology
import yaml
import subprocess
import logging


class TopologyDumper:
def __init__(self, topology: Topology):
self.topology = topology

def __to_dict(self):
return {
"name": self.topology.name,
"topology": {
"nodes": {
node.name: {
"kind": node.kind,
"image": f"{node.kind}:latest",
}
for node in self.topology.nodes
},
"links": [
{
"endpoints": [
f"{link.name_from}:{link.interface_from}",
f"{link.name_to}:{link.interface_to}",
]
}
for link in self.topology.links
],
},
}

def dump(self) -> str:
data = self.__to_dict()
return yaml.safe_dump(data, default_flow_style=False, sort_keys=False)


class ClabBuilder(TopologyBuilder):
def __init__(self, topology: Topology):
super().__init__(topology)
self.logger = logging.getLogger(__name__)

def build_topology(self):
topology_spec = TopologyDumper(self.topology).dump()
self.logger.info(
f"Building topology {self.topology.name} with Containerlab..."
)
try:
proc = subprocess.Popen(
"clab deploy --topo -",
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
stdout, stderr = proc.communicate(topology_spec)
if proc.returncode != 0:
self.logger.info(
f"Error creating topology {self.topology.name}: {stderr}"
)
raise RuntimeError(
f"Topology deployment failed. "
f"Code: {proc.returncode}: {stderr}"
)

self.logger.info(
f"Successfully built topology {self.topology.name}"
)

except FileNotFoundError:
self.logger.info(
"Containerlab not installed. Aborting topology creation"
)
raise RuntimeError(
"Containerlab is required to use the clab builder."
)

def destroy_topology(self):
topology_spec = TopologyDumper(self.topology).dump()
self.logger.info(
f"Destroying Containerlab topology {self.topology.name}..."
)
try:
proc = subprocess.Popen(
"clab destroy --cleanup --topo -",
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
stdout, stderr = proc.communicate(topology_spec)
if proc.returncode != 0:
self.logger.info(
f"Error destroying topology {self.topology.name}: {stderr}"
)
raise RuntimeError(
f"Topology destruction failed. "
f"Code: {proc.returncode}: {stderr}"
)

self.logger.info(
f"Successfully removed topology {self.topology.name}"
)

except FileNotFoundError:
self.logger.info(
"Containerlab not installed. Aborting topology destruction"
)
2 changes: 1 addition & 1 deletion config/cli.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import argparse


__all__ = ['ArgParser']
__all__ = ["ArgParser"]


class ArgParser:
Expand Down
28 changes: 20 additions & 8 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,26 @@
import yaml


__all__ = ['TopologyType', 'TopologyAdjustment', 'KafkaSettings',
'RabbitSettings', 'TopologyAdjustmentAdd',
'TopologyAdjustmentRemove', 'TopologyAdjustmentRemoveLink',
'TopologyAdjustmentAddLink', 'InterfaceSettings',
'InterfaceCredentials', 'RealnetSettings',
'ControllerSettings', 'SiblingSettings',
'BuilderSettings', 'read_config', 'validate_config',
'AppSettings', 'Settings']
__all__ = [
"TopologyType",
"TopologyAdjustment",
"KafkaSettings",
"RabbitSettings",
"TopologyAdjustmentAdd",
"TopologyAdjustmentRemove",
"TopologyAdjustmentRemoveLink",
"TopologyAdjustmentAddLink",
"InterfaceSettings",
"InterfaceCredentials",
"RealnetSettings",
"ControllerSettings",
"SiblingSettings",
"BuilderSettings",
"read_config",
"validate_config",
"AppSettings",
"Settings",
]


class TopologyType(BaseModel):
Expand Down
2 changes: 0 additions & 2 deletions tests/example/test_example.py

This file was deleted.

File renamed without changes.
40 changes: 40 additions & 0 deletions tests/topology/test_topology.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import unittest
from ...topology import TopologyBuilder


class TopologyTests(unittest.TestCase):

def setUp(self):
self.b = TopologyBuilder()

def test_topology_builder(self):
self.b = TopologyBuilder()
self.b.add_node("srl1", "nokia_srlinux")
self.b.add_node("srl2", "nokia_srlinux")
self.b.add_link("srl1", "srl2", "e1-1", "e1-1")

topo = self.b.build()
self.assertEqual(len(topo.nodes), 2)
self.assertEqual(len(topo.links), 1)

self.assertEqual(topo.nodes[0].name, "srl1")
self.assertEqual(topo.nodes[1].name, "srl2")
self.assertEqual(topo.links[0].name_from, "srl1")
self.assertEqual(topo.links[0].name_to, "srl2")
self.assertEqual(topo.links[0].interface_to, "e1-1")
self.assertEqual(topo.links[0].interface_from, "e1-1")

def test_topology_faulty_link(self):
self.b.add_node("srl1", "nokia_srlinux")
with self.assertRaises(AssertionError):
self.b.add_link("srl1", "srl2", "e1-1", "e1-1")

def test_topology_clear(self):
self.b.add_node("srl1", "nokia_srlinux")
self.b.add_node("srl2", "nokia_srlinux")
self.b.add_link("srl1", "srl2", "e1-1", "e1-1")
self.b.clear()
topo = self.b.build()

self.assertEqual(len(topo.nodes), 0)
self.assertEqual(len(topo.links), 0)
3 changes: 3 additions & 0 deletions topology/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .link import * # noqa: F403, F401
from .node import * # noqa: F403, F401
from .topology import * # noqa: F403, F401
11 changes: 11 additions & 0 deletions topology/link.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from dataclasses import dataclass

__all__ = ["Link"]


@dataclass
class Link:
name_from: str
name_to: str
interface_from: str
interface_to: str
12 changes: 12 additions & 0 deletions topology/node.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from dataclasses import dataclass
from typing import List
from .link import Link

__all__ = ["Node"]


@dataclass
class Node:
name: str
kind: str
links: List[Link]
49 changes: 49 additions & 0 deletions topology/topology.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from .link import Link
from .node import Node
from typing import List

__all__ = ["Topology", "TopologyBuilder"]


class Topology:
def __init__(self):
self.name = ""
self.nodes: List[Node] = []
self.links: List[Link] = []

@staticmethod
def builder():
return TopologyBuilder()


class TopologyBuilder:
def __init__(self) -> None:
self._topo = Topology()

def add_node(self, name: str, kind: str) -> None:
self._topo.nodes.append(Node(name, kind, []))

def add_link(
self,
node_from: str,
node_to: str,
interface_from: str,
interface_to: str,
) -> None:
# assert
assert any(node.name == node_from for node in self._topo.nodes)
assert any(node.name == node_to for node in self._topo.nodes)
self._topo.links.append(
Link(node_from, node_to, interface_from, interface_to)
)

def name(self, name: str) -> None:
self._topo.name = name

def clear(self) -> None:
self._topo.name = ""
self._topo.nodes = []
self._topo.links = []

def build(self) -> Topology:
return self._topo