Skip to content

Commit

Permalink
viosl2: add scrapli and new docker image (#323)
Browse files Browse the repository at this point in the history
  • Loading branch information
tjbalzer authored Feb 13, 2025
1 parent f2ee1ab commit cd5b013
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 105 deletions.
8 changes: 4 additions & 4 deletions viosl2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ Tested with versions:

The router supports up to 16 GigabitEthernet interfaces.

- The first interface `GigaEthernet0/0` is used as the management interface (it is placed in separated VRF) and is
- The first interface `GigabitEthernet0/0` is used as the management interface (it is placed in separated VRF) and is
mapped to the docker container interface `eth0`.
- The rest of the interfaces are numbered from `GigaEthernet0/1` and are used as data interfaces.
- The rest of the interfaces are numbered from `GigabitEthernet0/1` and are used as data interfaces.
They are mapped to the docker container interfaces `eth1`, `eth2`, etc.
- The interfaces are used in groups of four, e.g. `GigaEthernet0/0` to `GigaEthernet0/3`, `GigaEthernet1/0-3` to
`GigaEthernet1/3`, etc.
- The interfaces are used in groups of four, e.g. `GigabitEthernet0/0` to `GigabitEthernet0/3`, `GigabitEthernet1/0` to
`GigabitEthernet1/3`, etc.

## Management plane

Expand Down
17 changes: 1 addition & 16 deletions viosl2/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,19 +1,4 @@
FROM public.ecr.aws/docker/library/debian:bookworm-slim
LABEL org.opencontainers.image.authors="xtothj@gmail.com"

ARG DEBIAN_FRONTEND=noninteractive

RUN apt-get update -qy \
&& apt-get install -y --no-install-recommends \
bridge-utils \
iproute2 \
python3 \
socat \
qemu-kvm \
qemu-utils \
tcpdump \
procps \
&& rm -rf /var/lib/apt/lists/*
FROM ghcr.io/srl-labs/vrnetlab-base:0.1.0

ARG IMAGE
COPY $IMAGE* /
Expand Down
176 changes: 91 additions & 85 deletions viosl2/docker/launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import sys

import vrnetlab
from scrapli.driver.core import IOSXEDriver

STARTUP_CONFIG_FILE = "/config/startup-config.cfg"

Expand Down Expand Up @@ -52,6 +53,7 @@ def __init__(self, hostname: str, username: str, password: str, conn_mode: str):
smp="1",
ram=768,
driveif="virtio",
use_scrapli=True,
)

self.hostname = hostname
Expand All @@ -68,13 +70,12 @@ def bootstrap_spin(self):
self.start()
return

(ridx, match, res) = self.tn.expect(
(ridx, match, res) = self.con_expect(
[
rb"Would you like to enter the initial configuration dialog\? \[yes/no\]:",
b"Press RETURN to get started!",
b"Switch>",
],
1,
)

if match:
Expand All @@ -86,107 +87,112 @@ def bootstrap_spin(self):
for _ in range(3):
self.wait_write("\r", wait=None)
elif ridx == 2:
self._enter_config_mode()
self._bootstrap_config()
self._load_startup_config()
self._save_config()
self.apply_config()

# close telnet connection
self.tn.close()
# startup time
startup_time = datetime.datetime.now() - self.start_time
self.logger.info(f"Startup complete in: {startup_time}")
# mark as running
self.running = True
return

# no match, if we saw some output from the router it's probably
# booting, so let's give it some more time
if res != b"":
self.logger.trace(f"OUTPUT: {res.decode()}")
self.write_to_stdout(res)
# reset spins if we saw some output
self.spins = 0

self.spins += 1
return

def _enter_config_mode(self):
self.logger.info("Entering configuration mode")

self.wait_write("enable", wait=None)
self.wait_write("configure terminal")

def _bootstrap_config(self):
self.logger.info("Applying initial configuration")

self.wait_write(f"hostname {self.hostname}")
self.wait_write(f"ip domain-name {self.hostname}.clab")
self.wait_write("no ip domain-lookup")

# Explicitly enable IPv6
self.wait_write("ipv6 unicast-routing")

self.wait_write(f"username {self.username} privilege 15 secret {self.password}")

self.wait_write("line con 0")
self.wait_write("logging synchronous")
self.wait_write("exec-timeout 0 0")
self.wait_write("login local")
self.wait_write("exit")

self.wait_write("line vty 0 4")
self.wait_write("logging synchronous")
self.wait_write("exec-timeout 0 0")
self.wait_write("transport input ssh")
self.wait_write("login local")
self.wait_write("exit")

self.wait_write("vrf definition clab-mgmt")
self.wait_write("description Containerlab management VRF (DO NOT DELETE)")
self.wait_write("address-family ipv4")
self.wait_write("exit")
self.wait_write("address-family ipv6")
self.wait_write("exit")
self.wait_write("exit")

v4_mgmt_address = vrnetlab.cidr_to_ddn(self.mgmt_address_ipv4)

self.wait_write("interface GigabitEthernet0/0")
self.wait_write("no switchport")
self.wait_write("vrf forwarding clab-mgmt")
self.wait_write(f"ip address {v4_mgmt_address[0]} {v4_mgmt_address[1]}")
self.wait_write(f"ipv6 address {self.mgmt_address_ipv6}")
self.wait_write("no shutdown")
self.wait_write("exit")

self.wait_write(f"ip route vrf clab-mgmt 0.0.0.0 0.0.0.0 {self.mgmt_gw_ipv4}")
self.wait_write(f"ipv6 route vrf clab-mgmt ::/0 {self.mgmt_gw_ipv6}")

self.wait_write("crypto key generate rsa modulus 2048")
self.wait_write("ip ssh version 2")

self.wait_write("netconf ssh")
self.wait_write("netconf max-sessions 16")
self.wait_write("snmp-server community public rw")

self.wait_write("no banner exec")
self.wait_write("no banner login")
self.wait_write("no banner incoming")

def _load_startup_config(self):
if not os.path.exists(STARTUP_CONFIG_FILE):
self.logger.trace(f"Startup config file {STARTUP_CONFIG_FILE} not found")
return
def apply_config(self):
scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", vrnetlab.DEFAULT_SCRAPLI_TIMEOUT)
self.logger.info(
f"Scrapli timeout is {scrapli_timeout}s (default {vrnetlab.DEFAULT_SCRAPLI_TIMEOUT}s)"
)

self.logger.trace(f"Loading startup config file {STARTUP_CONFIG_FILE}")
with open(STARTUP_CONFIG_FILE) as file:
for line in (line.rstrip() for line in file):
self.wait_write(line)
# init scrapli
vios_scrapli_dev = {
"host": "127.0.0.1",
"auth_bypass": True,
"auth_strict_key": False,
"timeout_socket": scrapli_timeout,
"timeout_transport": scrapli_timeout,
"timeout_ops": scrapli_timeout,
}

def _save_config(self):
self.logger.info("Saving configuration")
v4_mgmt_address = vrnetlab.cidr_to_ddn(self.mgmt_address_ipv4)

self.wait_write("end")
self.wait_write("write memory")
vios_config = f"""hostname {self.hostname}
username {self.username} privilege 15 password {self.password}
ip domain-name example.com
no ip domain-lookup
!
line con 0
logging synchronous
exec timeout 0 0
!
line vty 0 4
logging synchronous
login local
transport input all
exec timeout 0 0
!
ipv6 unicast-routing
!
vrf definition clab-mgmt
description Containerlab management VRF (DO NOT DELETE)
address-family ipv4
exit
address-family ipv6
exit
exit
!
ip route vrf clab-mgmt 0.0.0.0 0.0.0.0 {self.mgmt_gw_ipv4}
ipv6 route vrf clab-mgmt ::/0 {self.mgmt_gw_ipv6}
!
interface GigabitEthernet0/0
description Containerlab management interface
no switchport
vrf forwarding clab-mgmt
ip address {v4_mgmt_address[0]} {v4_mgmt_address[1]}
ipv6 address {self.mgmt_address_ipv6}
no shut
exit
!
crypto key generate rsa modulus 2048
ip ssh version 2
!
netconf ssh
netconf max-sessions 16
snmp-server community public rw
!
no banner exec
no banner login
no banner incoming
!
"""

con = IOSXEDriver(**vios_scrapli_dev)
con.commandeer(conn=self.scrapli_tn)

if os.path.exists(STARTUP_CONFIG_FILE):
self.logger.info("Startup configuration file found")
with open(STARTUP_CONFIG_FILE, "r") as config:
vios_config += config.read()
else:
self.logger.warning("User provided startup configuration is not found.")

res = con.send_configs(vios_config.splitlines())
res += con.send_commands(["write memory"])

for response in res:
self.logger.info(f"CONFIG:{response.channel_input}")
self.logger.info(f"RESULT:{response.result}")

# close the scrapli connection
con.close()


class VIOS(vrnetlab.VR):
Expand All @@ -212,7 +218,7 @@ def __init__(self, hostname: str, username: str, password: str, conn_mode: str):
"--password", help="Password", default=os.getenv("PASSWORD", "admin")
)
parser.add_argument(
"--hostname", help="Router hostname", default=os.getenv("HOSTNAME", "viosl2")
"--hostname", help="Switch hostname", default=os.getenv("HOSTNAME", "viosl2")
)
parser.add_argument(
"--connection-mode",
Expand Down

0 comments on commit cd5b013

Please sign in to comment.