Skip to content
Open
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
2 changes: 2 additions & 0 deletions .azure-pipelines/meta_validator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,12 @@ validators:
exclude_devices:
- sonic-s6100-dut1 # E3002 Device has no console connection
- sonic-s6100-dut2 # E3002 Device has no console connection
- vnut-.* # E3002 Virtual devices have no console connection
- name: pdu
enabled: true
config:
exclude_devices:
- sonic-s6100-dut1 # E4002 Device has no PDU connection
- sonic-s6100-dut2 # E4002 Device has no PDU connection
- vnut-.* # E4002 Virtual devices have no PDU connection
issue_severities: {}
4 changes: 4 additions & 0 deletions ansible/files/sonic_lab_devices.csv
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ management-1,192.168.10.3/23,Sonic,MgmtTsToRRouter,,sonic
switch01,192.168.0.100/24,SKU,DevSonic,,sonic
switch01-bmc,192.168.0.101/24,bmc,DevSonic,,sonic
switch01-bmc-con,192.168.0.101/24,bmc,ConsoleServer,ssh,sonic
vnut-t0-01,10.250.0.210/24,Force10-S6000,DevSonic,,sonic,
vnut-t0-02,10.250.0.211/24,Force10-S6000,DevSonic,,sonic,
vnut-t1-01,10.250.0.212/24,Force10-S6000,DevSonic,,sonic,
vnut-tg-01,10.250.0.220/24,IxiaChassis,DevIxiaChassis,,ixia,
4 changes: 4 additions & 0 deletions ansible/files/sonic_lab_links.csv
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ str-msn2700-01,Ethernet120,str-7260-10,Ethernet31,40000,1711,Access,on
str-msn2700-01,Ethernet124,str-7260-10,Ethernet32,40000,1712,Access,on
str-7260-11,Ethernet19,str-acs-serv-01,p4p1,40000,,Trunk,on
str-7260-11,Ethernet30,str-7260-10,Ethernet64,40000,1681-1712,Trunk,on
vnut-t0-01,Ethernet0,vnut-tg-01,Ethernet0,10000,,,
vnut-t0-01,Ethernet4,vnut-t1-01,Ethernet0,10000,,,
vnut-t0-02,Ethernet0,vnut-tg-01,Ethernet4,10000,,,
vnut-t0-02,Ethernet4,vnut-t1-01,Ethernet4,10000,,,
27 changes: 27 additions & 0 deletions ansible/lab
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ all:
sonic_msft_sup:
sonic_msft_lc_100G:
sonic_cisco_vs:
sonic_vnut:
fanout:
hosts:
str-7260-10:
Expand Down Expand Up @@ -53,6 +54,8 @@ all:
ansible_ssh_user: root
ansible_ssh_pass: root
hosts:
vnut-tg-01:
ansible_host: 10.250.0.220
ptf_ptf1:
ansible_host: 10.255.0.188
ansible_hostv6: 2001:db8:1::1/64
Expand Down Expand Up @@ -284,3 +287,27 @@ sonic_cisco_vs:
ansible_user: admin
ansible_ssh_user: admin
ansible_altpassword: admin

sonic_vnut:
vars:
hwsku: Force10-S6000
iface_speed: 10000
hosts:
vnut-t0-01:
ansible_host: 10.250.0.210
type: kvm
hwsku: Force10-S6000
serial_port: 9100
num_asics: 1
vnut-t0-02:
ansible_host: 10.250.0.211
type: kvm
hwsku: Force10-S6000
serial_port: 9101
num_asics: 1
vnut-t1-01:
ansible_host: 10.250.0.212
type: kvm
hwsku: Force10-S6000
serial_port: 9102
num_asics: 1
204 changes: 204 additions & 0 deletions ansible/library/vnut_network.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
#!/usr/bin/python3
"""
Ansible module to manage virtual network links (veth pairs) for NUT virtual testbed.

Usage in Ansible:
- name: Connect container to management bridge
vnut_network:
action: connect_mgmt
device: "switch-t0-1"
mgmt_ip: "10.0.0.100/24"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale docstring: the module docstring (lines 6-38) still documents create_link and cleanup actions that were removed. Update to only show connect_mgmt and create_bridge usage examples.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — updated module docstring to only document connect_mgmt and create_bridge actions.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed fixed — docstring updated.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — docstring now documents only connect_mgmt and create_bridge. No reference to create_link.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Confirmed fixed — docstring now documents only the two remaining actions.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed: the docstring now documents only the two current actions (connect_mgmt and create_bridge). The stale create_link/cleanup references have been removed.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed fixed — docstring now documents only connect_mgmt and create_bridge.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed: docstring updated to document only the remaining actions.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed — docstring now documents connect_mgmt and create_bridge actions only.

mgmt_gateway: "10.0.0.1"
mgmt_bridge: "br-mgmt"
testbed_name: "nut-ci-1"
container_prefix: "net"

- name: Create management bridge
vnut_network:
action: create_bridge
bridge_name: "br-mgmt"
bridge_ip: "10.0.0.1/24"
"""

import hashlib
import subprocess

from ansible.module_utils.basic import AnsibleModule


def run_cmd(cmd_args, check=True):
"""Run a command and return (rc, stdout, stderr).

Args:
cmd_args: List of command arguments (e.g. ["ip", "link", "show", "eth0"]).
check: If True, raise RuntimeError on non-zero exit code.
"""
result = subprocess.run(cmd_args, capture_output=True, text=True, timeout=60)
if check and result.returncode != 0:
raise RuntimeError(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No timeout on subprocess.run(). If a command hangs (e.g., docker inspect on a stuck container), the Ansible task blocks indefinitely. Consider adding timeout=60 (or a configurable parameter) to prevent hung tasks.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added timeout=60 to subprocess.run() in run_cmd.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — timeout=60 has been added to subprocess.run().

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Confirmed fixed — timeout=60 added to subprocess.run().

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — timeout=60 added.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed: timeout=60 has been added to subprocess.run() in run_cmd().

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed: timeout=60 added to subprocess.run().

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed — timeout=60 added to subprocess.run().

"Command failed: {}\nstdout: {}\nstderr: {}".format(
" ".join(cmd_args), result.stdout.strip(), result.stderr.strip()
)
)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: run_cmd silently discards stderr on success (rc=0). Consider logging stderr via module.warn() when non-empty, as commands like ip can emit warnings even on success (e.g. deprecated options).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open — run_cmd still silently discards stderr on success. Low priority nit.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — will address stderr handling in a follow-up.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open (minor) — stderr is still discarded on success. Low priority.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open — stderr discarded on success. Minor nit, low priority.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open — minor nit, non-blocking.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open (nit): run_cmd still discards stderr on success. Logging non-empty stderr via module.warn() would help debugging.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open (nit): stderr discarded on success. Non-blocking.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open (nit): stderr discarded on success in run_cmd. Non-blocking.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed — run_cmd now raises RuntimeError including both stdout and stderr on failure.

return result.returncode, result.stdout.strip(), result.stderr.strip()


def link_exists_on_host(link_name):
"""Check if a network link exists on the host."""
rc, _, _ = run_cmd(["ip", "link", "show", link_name], check=False)
return rc == 0


def bridge_exists(bridge_name):
"""Check if a bridge exists on the host."""
rc, _, _ = run_cmd(
["ip", "link", "show", "type", "bridge", "dev", bridge_name], check=False
)
return rc == 0


def get_container_pid(container_name):
"""Get the PID of a running Docker container."""
rc, stdout, stderr = run_cmd(
["docker", "inspect", "-f", "{{.State.Pid}}", container_name], check=False
)
if rc != 0:
raise RuntimeError(
"Container '{}' not found or not running: {}".format(container_name, stderr)
)
pid = stdout.strip()
if pid == "0":
raise RuntimeError("Container '{}' is not running (PID=0)".format(container_name))
return pid


def container_name(prefix, testbed, device):
"""Build the Docker container name from components."""
return "{}_{}_{}".format(prefix, testbed, device)


def interface_exists_in_ns(pid, iface_name):
"""Check if an interface exists inside a container network namespace."""
rc, _, _ = run_cmd(
["nsenter", "-t", pid, "-n", "ip", "link", "show", iface_name], check=False
)
return rc == 0


def action_connect_mgmt(module):
"""Connect a container to a management bridge."""
p = module.params
device = p["device"]
mgmt_ip = p["mgmt_ip"]
mgmt_gw = p["mgmt_gateway"]
bridge = p["mgmt_bridge"]
testbed_name = p["testbed_name"]
prefix = p["container_prefix"]

cname = container_name(prefix, testbed_name, device)
pid = get_container_pid(cname)

# Idempotency: if eth0 already exists inside container, skip
if interface_exists_in_ns(pid, "eth0"):
module.exit_json(
changed=False,
msg="Management interface eth0 already exists in {}".format(cname),
)

short_id = hashlib.md5(cname.encode()).hexdigest()[:8]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 8-character MD5 prefix (short_id) gives only 32 bits of uniqueness. While collision probability is low for small testbed counts, consider using a deterministic scheme like testbed[:4]_device[:4] that's also human-readable and debuggable.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open — 8-char MD5 prefix used. Acceptable for current scale, noting for future reference.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — 8-char MD5 prefix is sufficient for current scale.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open but acceptable risk for the expected scale. The 8-char MD5 prefix is fine for typical testbed sizes.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open — 8-char MD5 prefix is acceptable for expected scale.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acceptable for expected scale. Non-blocking.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open (nit): 8-char MD5 prefix gives 32 bits — acceptable for small testbeds, but worth a comment noting the limitation.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acceptable for expected scale. Non-blocking.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open (nit): 8-char MD5 prefix for interface naming. Acceptable at current scale.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — 8-char MD5 prefix is acceptable for this use case (per-testbed container naming, not security-critical). Low collision risk in practice.

veth_a = "vm{}a".format(short_id) # 12 chars, well under 15
veth_b = "vm{}b".format(short_id) # 12 chars, well under 15

# Clean up if host-side veth exists
if link_exists_on_host(veth_a):
run_cmd(["ip", "link", "delete", veth_a])

# Create veth pair and move into container; clean up on failure
run_cmd(["ip", "link", "add", veth_a, "type", "veth", "peer", "name", veth_b])
try:
# Move one end into container as eth0
run_cmd(["ip", "link", "set", veth_a, "netns", pid])
run_cmd(["nsenter", "-t", pid, "-n", "ip", "link", "set", veth_a, "name", "eth0"])
run_cmd(["nsenter", "-t", pid, "-n", "ip", "addr", "add", mgmt_ip, "dev", "eth0"])
run_cmd(["nsenter", "-t", pid, "-n", "ip", "link", "set", "eth0", "up"])
run_cmd(["nsenter", "-t", pid, "-n", "ip", "route", "add", "default", "via", mgmt_gw])

# Attach host end to bridge
run_cmd(["ip", "link", "set", veth_b, "master", bridge])
run_cmd(["ip", "link", "set", veth_b, "up"])
except Exception:
# Clean up the veth pair to avoid dangling interfaces
for iface in (veth_a, veth_b):
try:
run_cmd(["ip", "link", "delete", iface])
except Exception:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inner except Exception: pass here will silently swallow cleanup failures. While this is in a cleanup path and you don't want to lose the original exception, adding a brief comment explaining the intent (e.g., # Best-effort cleanup; original exception re-raised below) would address the code-scanning alert and help future maintainers understand why the pass is intentional.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added comment explaining intentional best-effort cleanup.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed fixed — the comment clarifies intentional best-effort cleanup behavior.

pass # Best-effort cleanup; re-raise original exception below
raise

module.exit_json(
changed=True,
msg="Connected {} to bridge {} with IP {}".format(cname, bridge, mgmt_ip),
)


def action_create_bridge(module):
"""Create a Linux bridge with an IP address."""
p = module.params
bridge_name = p["bridge_name"]
bridge_ip = p["bridge_ip"]

if bridge_exists(bridge_name):
module.exit_json(changed=False, msg="Bridge {} already exists".format(bridge_name))

run_cmd(["ip", "link", "add", bridge_name, "type", "bridge"])
run_cmd(["ip", "addr", "add", bridge_ip, "dev", bridge_name])
run_cmd(["ip", "link", "set", bridge_name, "up"])

module.exit_json(
changed=True,
msg="Created bridge {} with IP {}".format(bridge_name, bridge_ip),
)


def main():
module = AnsibleModule(
argument_spec=dict(
action=dict(
type="str",
required=True,
choices=["connect_mgmt", "create_bridge"],
),
# connect_mgmt params
device=dict(type="str"),
mgmt_ip=dict(type="str"),
mgmt_gateway=dict(type="str"),
mgmt_bridge=dict(type="str", default="br-mgmt"),
# create_bridge params
bridge_name=dict(type="str"),
bridge_ip=dict(type="str"),
# common params
testbed_name=dict(type="str"),
container_prefix=dict(type="str", default="net"),
),
required_if=[
("action", "connect_mgmt", ["device", "mgmt_ip", "mgmt_gateway", "testbed_name"]),
("action", "create_bridge", ["bridge_name", "bridge_ip"]),
],
supports_check_mode=False,
)

action = module.params["action"]

try:
if action == "connect_mgmt":
action_connect_mgmt(module)
elif action == "create_bridge":
action_create_bridge(module)
except RuntimeError as e:
module.fail_json(msg=str(e))
except Exception as e:
module.fail_json(msg="Unexpected error: {}".format(e))


if __name__ == "__main__":
main()
22 changes: 22 additions & 0 deletions ansible/roles/testbed/nut-vtopo-create/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
# Default Docker images (TG container only)
vnut_ptf_image: "docker-ptf:latest"

# Naming prefix for TG Docker containers
vnut_container_prefix: "net"

# KVM VM settings
vnut_vm_memory_gb: 4
vnut_vm_vcpus: 2
vnut_sonic_vm_storage: "{{ ansible_env.HOME }}/sonic-vm"
vnut_sonic_vs_image: "{{ vnut_sonic_vm_storage }}/images/sonic-vs.img"
vnut_vm_disk_dir: "{{ vnut_sonic_vm_storage }}/disks"
vnut_vm_serial_port_base: 9100

# Front-panel MTU
vnut_fp_mtu: 9100

# Management network
mgmt_bridge: "vnut_mgmt"
mgmt_gw: "10.250.0.1"
mgmt_prefixlen: "24"
64 changes: 64 additions & 0 deletions ansible/roles/testbed/nut-vtopo-create/tasks/connect_tg_links.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
# connect_tg_links.yml — Connect TG container interfaces to front-panel link bridges
# For each link involving a TG device, create a veth pair with one end in the
# TG container namespace and the other end attached to the link bridge.

- name: Identify TG links
set_fact:
vnut_tg_links: >-
{{ (vnut_links | selectattr('StartDevice', 'in', testbed_tg_names) | list
+ vnut_links | selectattr('EndDevice', 'in', testbed_tg_names) | list) | unique }}

- name: Connect TG containers to link bridges
shell: |
{% for link in vnut_links %}
{% if link.StartDevice in testbed_tg_names %}
{# TG is the start device #}
BRIDGE="vbr_{{ testbed_name[:8] }}_{{ loop.index0 }}"
CONTAINER="{{ vnut_container_prefix }}_{{ testbed_name }}_{{ link.StartDevice }}"
PORT="{{ link.StartPort }}"
VETH_A="vtg{{ testbed_name[:6] }}_{{ loop.index0 }}a"
VETH_B="vtg{{ testbed_name[:6] }}_{{ loop.index0 }}b"
PID=$(docker inspect -f '{% raw %}{{.State.Pid}}{% endraw %}' "$CONTAINER")

# Check if port already exists in container
if nsenter -t "$PID" -n ip link show "$PORT" >/dev/null 2>&1; then
echo "Port $PORT already exists in $CONTAINER"
else
ip link show "$VETH_A" 2>/dev/null || ip link add "$VETH_A" type veth peer name "$VETH_B"
ip link set "$VETH_A" master "$BRIDGE"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ip link add ... 2>/dev/null || true swallows all errors, including legitimate failures (e.g. out of memory, permission denied). Consider checking if the link already exists first (ip link show "$VETH_A") and only suppressing the "already exists" case.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open — ip link add ... 2>/dev/null || true still swallows all errors. See new inline comment about the interaction with set -e.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — will improve error handling in connect_tg_links in a follow-up.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open — ip link show ... 2>/dev/null || ip link add ... still swallows errors on the ip link show check. The logic is correct for the idempotency pattern but could mask failures.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open — error swallowing pattern unchanged.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open — the idempotency check with ip link show mitigates the risk. Non-blocking.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open: ip link show ... 2>/dev/null || ip link add ... still swallows errors from ip link add failures. Consider capturing the exit code and logging a warning on unexpected failures.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open: error swallowing pattern unchanged. Non-blocking but worth improving.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open: ip link add ... || true error swallowing. Non-blocking.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed — the pattern now checks existence first (ip link show ... || ip link add ...) rather than blindly swallowing errors.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ip link show ... 2>/dev/null || ip link add ... pattern makes the veth creation idempotent, but ip link add failures (e.g. out of memory, permission denied, exceeding max interfaces) are silently swallowed. Consider:

if ! ip link show "$VETH_A" 2>/dev/null; then
  ip link add "$VETH_A" type veth peer name "$VETH_B"
fi

This way, legitimate ip link add failures will surface as task errors.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open: same concern as thread 88 — ip link add failure errors are swallowed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — idempotency checks prevent silent failures for the common case.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged by maintainer — idempotency pattern acceptable.

ip link set "$VETH_A" up
ip link set "$VETH_B" netns "$PID"
nsenter -t "$PID" -n ip link set "$VETH_B" name "$PORT"
nsenter -t "$PID" -n ip link set "$PORT" up
echo "Connected $CONTAINER:$PORT to $BRIDGE"
fi

{% elif link.EndDevice in testbed_tg_names %}
{# TG is the end device #}
BRIDGE="vbr_{{ testbed_name[:8] }}_{{ loop.index0 }}"
CONTAINER="{{ vnut_container_prefix }}_{{ testbed_name }}_{{ link.EndDevice }}"
PORT="{{ link.EndPort }}"
VETH_A="vtg{{ testbed_name[:6] }}_{{ loop.index0 }}a"
VETH_B="vtg{{ testbed_name[:6] }}_{{ loop.index0 }}b"
PID=$(docker inspect -f '{% raw %}{{.State.Pid}}{% endraw %}' "$CONTAINER")

if nsenter -t "$PID" -n ip link show "$PORT" >/dev/null 2>&1; then
echo "Port $PORT already exists in $CONTAINER"
else
ip link show "$VETH_A" 2>/dev/null || ip link add "$VETH_A" type veth peer name "$VETH_B"
ip link set "$VETH_A" master "$BRIDGE"
ip link set "$VETH_A" up
ip link set "$VETH_B" netns "$PID"
nsenter -t "$PID" -n ip link set "$VETH_B" name "$PORT"
nsenter -t "$PID" -n ip link set "$PORT" up
echo "Connected $CONTAINER:$PORT to $BRIDGE"
fi

{% endif %}
{% endfor %}
args:
executable: /bin/bash
register: connect_tg_result
changed_when: "'Connected' in connect_tg_result.stdout"
when: vnut_tg_links | length > 0
25 changes: 25 additions & 0 deletions ansible/roles/testbed/nut-vtopo-create/tasks/create_links.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
# create_links.yml — Create Linux bridges for each front-panel link.
# KVM VM interfaces and TG container interfaces will attach to these bridges.
# Bridge naming: vbr_<testbed>_<idx> (max 15 chars for Linux interface name)

- name: Create front-panel link bridges
shell: |
BRIDGE="vbr_{{ testbed_name[:8] }}_{{ idx }}"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: vbr_{{ testbed_name[:8] }}_{{ idx }} can exceed the Linux 15-char interface name limit if idx >= 100 (e.g. vbr_abcdefgh_100 = 16 chars). Consider shortening the testbed prefix to 6 chars or adding a validation task. Unlikely in practice with current topologies but worth noting.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open (nit): vbr_{{ testbed_name[:8] }}_{{ idx }} can exceed 15 chars when idx >= 100 (e.g. vbr_vnut-2ti_100 = 16 chars). For the sample testbed with only 4 links this is fine, but should be validated for larger topologies.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — bridge name length is safe for current scale. Will add validation in a follow-up if needed.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged by maintainer — bridge name length safe for current scale.

if ip link show "$BRIDGE" >/dev/null 2>&1; then
echo "Bridge $BRIDGE already exists"
else
ip link add "$BRIDGE" type bridge
ip link set "$BRIDGE" up
echo "Created bridge $BRIDGE"
fi
loop: "{{ vnut_links }}"
loop_control:
index_var: idx
label: "{{ item.StartDevice }}:{{ item.StartPort }} <-> {{ item.EndDevice }}:{{ item.EndPort }}"
register: bridge_results
changed_when: "'Created' in bridge_results.stdout"

- name: Display link bridge creation summary
debug:
msg: "Created {{ vnut_links | length }} front-panel link bridges"
Loading
Loading