Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev #18

Merged
merged 6 commits into from
Jul 16, 2024
Merged

Dev #18

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
@@ -1,4 +1,5 @@
venv/*
__pycache__
test/*
launch.json
launch.json
lab-examples/**
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

The `clab-io-draw` project unifies two tools, `clab2drawio` and `drawio2clab`. These tools facilitate the conversion between [Containerlab](https://github.com/srl-labs/containerlab) YAML files and Draw.io diagrams, making it easier for network engineers and architects to visualize, document, and share their network topologies.

![Drawio Example](docs/img/drawio1.png)
![Drawio Example](docs/img/modern_dark.png)

## clab2drawio

Expand All @@ -18,6 +18,12 @@ For more details on `drawio2clab`, including features, constraints for drawing,

## Quick Usage

### Running with Containerlab
```bash
containerlab graph --drawio -t topo.yml
containerlab graph --drawio -t topo.drawio
```

### Running with Docker

To simplify dependency management and execution, the tools can be run inside a Docker container. Follow these instructions to build and run the tool using Docker.
Expand Down Expand Up @@ -92,7 +98,6 @@ python clab2drawio.py -i <input_file.yaml>
```

`-i, --input`: Specifies the path to your input YAML file.
`-o, --output`: Specifies the path for the output drawio file.
Make sure to replace `<input_file.yaml>` with the path to your .drawio file

For more comprehensive guidance, including additional command-line options, please see the Usage section in [clab2drawio.md](docs/clab2drawio.md#usage)
168 changes: 126 additions & 42 deletions clab2drawio.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,12 @@ def add_ports(diagram, styles, verbose=True):
spacing = styles["node_width"] / (num_links + 1)
for i, link in enumerate(sorted_links):
port_x = (
node.pos_x
+ (i + 1) * spacing
- styles["connector_width"] / 2
node.pos_x + (i + 1) * spacing - styles["port_width"] / 2
)
port_y = (
node.pos_y
+ styles["node_height"]
- styles["connector_height"] / 2
- styles["port_height"] / 2
)
link.port_pos = (port_x, port_y)
elif direction == "upstream":
Expand All @@ -58,11 +56,9 @@ def add_ports(diagram, styles, verbose=True):
spacing = styles["node_width"] / (num_links + 1)
for i, link in enumerate(sorted_links):
port_x = (
node.pos_x
+ (i + 1) * spacing
- styles["connector_width"] / 2
node.pos_x + (i + 1) * spacing - styles["port_width"] / 2
)
port_y = node.pos_y - styles["connector_height"] / 2
port_y = node.pos_y - styles["port_height"] / 2
link.port_pos = (port_x, port_y)
else:
# Sort lateral links by y position of source and target
Expand Down Expand Up @@ -143,8 +139,8 @@ def add_ports(diagram, styles, verbose=True):
source_cID = f"{link.source.name}:{link.source_intf}:{link.target.name}:{link.target_intf}"
source_label = re.findall(r"\d+", link.source_intf)[-1]
source_connector_pos = link.port_pos
connector_width = styles["connector_width"]
connector_height = styles["connector_height"]
port_width = styles["port_width"]
port_height = styles["port_height"]

# Add the source connector ID to the source connector dictionary
if link.source.name not in connector_dict:
Expand Down Expand Up @@ -209,8 +205,8 @@ def add_ports(diagram, styles, verbose=True):
label=source_label,
x_pos=source_connector_pos[0],
y_pos=source_connector_pos[1],
width=connector_width,
height=connector_height,
width=port_width,
height=port_height,
style=styles["port_style"],
)

Expand All @@ -219,19 +215,19 @@ def add_ports(diagram, styles, verbose=True):
label=target_label,
x_pos=target_connector_pos[0],
y_pos=target_connector_pos[1],
width=connector_width,
height=connector_height,
width=port_width,
height=port_height,
style=styles["port_style"],
)

# Calculate center positions
source_center = (
source_connector_pos[0] + connector_width / 2,
source_connector_pos[1] + connector_height / 2,
source_connector_pos[0] + port_width / 2,
source_connector_pos[1] + port_height / 2,
)
target_center = (
target_connector_pos[0] + connector_width / 2,
target_connector_pos[1] + connector_height / 2,
target_connector_pos[0] + port_width / 2,
target_connector_pos[1] + port_height / 2,
)

# Calculate the real middle between the centers for the midpoint connector
Expand Down Expand Up @@ -272,8 +268,8 @@ def add_ports(diagram, styles, verbose=True):
label="\u200b",
x_pos=midpoint_top_left_x,
y_pos=midpoint_top_left_y,
width=4,
height=4,
width=styles["connector_width"],
height=styles["connector_height"],
style=styles["connector_style"],
)

Expand Down Expand Up @@ -303,16 +299,28 @@ def add_ports(diagram, styles, verbose=True):

def add_links(diagram, styles):
nodes = diagram.nodes
global_seen_links = set()

for node in nodes.values():
downstream_links = node.get_downstream_links()
lateral_links = node.get_lateral_links()

links = downstream_links + lateral_links

# Filter links globally
filtered_links = []
for link in links:
source_id = f"{link.source.name}:{link.source_intf}"
target_id = f"{link.target.name}:{link.target_intf}"
link_pair = tuple(sorted([source_id, target_id]))

if link_pair not in global_seen_links:
global_seen_links.add(link_pair)
filtered_links.append(link)

# Group links by their target
target_groups = {}
for link in links:
for link in filtered_links:
target = link.target
if target not in target_groups:
target_groups[target] = []
Expand Down Expand Up @@ -353,15 +361,56 @@ def add_links(diagram, styles):
entryY = exitY = step
style = f"{styles['link_style']}entryY={entryY};exitY={exitY};entryX={entryX};exitX={exitX};"

diagram.add_link(
source=link.source.name,
target=link.target.name,
src_label=link.source_intf,
trgt_label=link.target_intf,
src_label_style=styles["src_label_style"],
trgt_label_style=styles["trgt_label_style"],
style=style,
)
# Create label nodes for source and target interfaces
source_label_id = f"label:{link.source.name}:{link.source_intf}"
target_label_id = f"label:{link.target.name}:{link.target_intf}"

if not styles["default_labels"]:
# Calculate label positions using the get_label_positions method
(
(source_label_x, source_label_y),
(target_label_x, target_label_y),
) = link.get_label_positions(entryX, entryY, exitX, exitY, styles)

diagram.add_link(
link_id=f"link:{link.source.name}:{link.source_intf}:{link.target.name}:{link.target_intf}",
source=link.source.name,
target=link.target.name,
style=style,
)

# Add label nodes
diagram.add_node(
id=source_label_id,
# label should node name + interface name
label=f"{link.source_intf}",
x_pos=source_label_x,
y_pos=source_label_y,
width=styles["label_width"],
height=styles["label_height"],
style=styles["src_label_style"],
)

diagram.add_node(
id=target_label_id,
label=f"{link.target_intf}",
x_pos=target_label_x,
y_pos=target_label_y,
width=styles["label_width"],
height=styles["label_height"],
style=styles["trgt_label_style"],
)
else:
diagram.add_link(
link_id=f"link:{link.source.name}:{link.source_intf}:{link.target.name}:{link.target_intf}",
source=link.source.name,
target=link.target.name,
src_label=link.source_intf,
trgt_label=link.target_intf,
src_label_style=styles["src_label_style"],
trgt_label_style=styles["trgt_label_style"],
style=style,
)


def add_nodes(diagram, nodes, styles):
Expand Down Expand Up @@ -787,7 +836,13 @@ def load_styles_from_config(config_path):


def interactive_mode(
nodes, icon_to_group_mapping, containerlab_data, output_file, processor, prefix, lab_name
nodes,
icon_to_group_mapping,
containerlab_data,
output_file,
processor,
prefix,
lab_name,
):
# Initialize previous summary with existing node labels
previous_summary = {"Levels": {}, "Icons": {}}
Expand Down Expand Up @@ -836,8 +891,13 @@ def interactive_mode(
unformatted_node_name = node_name.replace(f"{prefix}-{lab_name}-", "")

# Check if 'labels' section exists, create it if necessary
if "labels" not in containerlab_data["topology"]["nodes"][unformatted_node_name]:
containerlab_data["topology"]["nodes"][unformatted_node_name]["labels"] = {}
if (
"labels"
not in containerlab_data["topology"]["nodes"][unformatted_node_name]
):
containerlab_data["topology"]["nodes"][unformatted_node_name][
"labels"
] = {}

# Update containerlab_data with graph-level
containerlab_data["topology"]["nodes"][unformatted_node_name]["labels"][
Expand Down Expand Up @@ -876,8 +936,13 @@ def interactive_mode(
unformatted_node_name = node_name.replace(f"{prefix}-{lab_name}-", "")

# Check if 'labels' section exists, create it if necessary
if "labels" not in containerlab_data["topology"]["nodes"][unformatted_node_name]:
containerlab_data["topology"]["nodes"][unformatted_node_name]["labels"] = {}
if (
"labels"
not in containerlab_data["topology"]["nodes"][unformatted_node_name]
):
containerlab_data["topology"]["nodes"][unformatted_node_name][
"labels"
] = {}

# Update containerlab_data with graph-icon
containerlab_data["topology"]["nodes"][unformatted_node_name]["labels"][
Expand Down Expand Up @@ -965,14 +1030,30 @@ def main(
print(error_message)
exit()

if theme in ["nokia_bright", "nokia_dark", "grafana_dark"]:
config_path = os.path.join(script_dir, f"styles/{theme}.yaml")
else:
# Assume the user has provided a custom path
config_path = theme
# Load the theme file
try:
if os.path.isabs(theme):
theme_path = theme
else:
theme_path = os.path.join(script_dir, "styles", f"{theme}.yaml")

# Check if the theme file exists
if not os.path.exists(theme_path):
raise FileNotFoundError(
f"The specified theme file '{theme_path}' does not exist."
)

except FileNotFoundError as e:
error_message = str(e)
print(error_message)
exit()
except Exception as e:
error_message = f"An error occurred while loading the theme: {e}"
print(error_message)
exit()

# Load styles
styles = load_styles_from_config(config_path)
styles = load_styles_from_config(theme_path)

diagram = CustomDrawioDiagram()
diagram.layout = layout
Expand Down Expand Up @@ -1087,7 +1168,7 @@ def main(
input_file,
processor,
prefix,
lab_name
lab_name,
)

assign_graphlevels(diagram, verbose=False)
Expand Down Expand Up @@ -1127,6 +1208,9 @@ def main(
add_nodes(diagram, diagram.nodes, styles)

if grafana:
styles["ports"] = True

if styles["ports"]:
add_ports(diagram, styles)
if not output_file:
grafana_output_file = os.path.splitext(input_file)[0] + ".grafana.json"
Expand Down
45 changes: 44 additions & 1 deletion docs/clab2drawio.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Using graph-level helps manage the vertical alignment of nodes in the generated

- `--layout`: Specifies the layout of the topology diagram (either `vertical` or `horizontal`). The default layout is `vertical`.

- `--theme`: Specifies the theme for the diagram (`nokia_bright` or `nokia_dark`) or the path to a custom style config file. By default, the `bright` theme is used. Users can also create their own style file and place it in any directory, specifying its path with this option. Feel free to contribute your own styles.
- `--theme`: Specifies the theme for the diagram (`nokia_bright`, `nokia_dark`, or ... ) or the path to a custom style config file. By default, the `bright` theme is used. Users can also create their own style file and place it in any directory, specifying its path with this option. Feel free to contribute your own styles.

```bash
python clab2drawio.py --theme nokia_dark -i <path_to_your_yaml_file>
Expand All @@ -98,6 +98,49 @@ Using graph-level helps manage the vertical alignment of nodes in the generated
## Customization
The tool allows for customization of node and link styles within the generated diagrams, making it possible to adjust the appearance to fit specific requirements or preferences.

Below are some example images of the available custom styles:

<table>
<tr>
<td style="text-align: center;">
<a href="img/nokia_bright.png" target="_blank">
<img src="img/nokia_bright.png" alt="Nokia Bright Mode" style="width: 200px;">
</a>
<p>nokia_bright</p>
</td>
<td style="text-align: center;">
<a href="img/nokia_dark.png" target="_blank">
<img src="img/nokia_dark.png" alt="Nokia Dark Mode" style="width: 200px;">
</a>
<p>nokia_dark</p>
</td>
</tr>
<tr>
<td style="text-align: center;">
<a href="img/modern_bright.png" target="_blank">
<img src="img/modern_bright.png" alt="Modern Bright Mode" style="width: 200px;">
</a>
<p>nokia_modern_bright</p>
</td>
<td style="text-align: center;">
<a href="img/modern_dark.png" target="_blank">
<img src="img/modern_dark.png" alt="Modern Dark Mode" style="width: 200px;">
</a>
<p>nokia_modern_dark</p>
</td>
</tr>
<tr>
<td style="text-align: center;">
<a href="img/grafana_dark.png" target="_blank">
<img src="img/grafana_dark.png" alt="Grafana Dark Mode" style="width: 200px;">
</a>
<p>grafana_dark</p>
</td>
</tr>
</table>

**_NOTE:_** drawio diagrams created with default_labels: true, cannot be used by drawio2clab

### Custom Styles
To customize styles, you can edit or copy the `nokia_bright.yaml` configuration file. This file defines the base style, link style, source and target label styles, and custom styles for different types of nodes based on their roles (e.g., routers, switches, servers).

Expand Down
2 changes: 1 addition & 1 deletion docs/drawio2clab.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,6 @@ python drawio2clab.py -i input_file.xml -o output_file.yaml --diagram-name "Diag

- `-i`, `--input`: Input `.drawio` XML file.
- `-o`, `--output`: Output YAML file.
- `--style`: YAML style (`block` or `flow`). Default is `block`.
- `--style`: YAML style (`block` or `flow`). Default is `flow`.
- `--diagram-name`: Name of the diagram to parse.
- `--default-kind`: The default kind for nodes. Default is 'nokia_srlinux'
Binary file added docs/img/grafana_dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/modern_bright.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/modern_dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/nokia_bright.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/nokia_dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading