From fef31290db3a3cbd670f1b3c6c5731a6fb0f7482 Mon Sep 17 00:00:00 2001 From: Stefano Sasso <852093+ssasso@users.noreply.github.com> Date: Mon, 16 Dec 2024 10:11:47 +0100 Subject: [PATCH 01/50] AOS-CX: Update CPU and MEM values for newer versions (#294) Co-authored-by: Stefano Sasso --- aoscx/README.md | 6 ++++-- aoscx/docker/launch.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/aoscx/README.md b/aoscx/README.md index 5e7a3afc..23a21b3f 100644 --- a/aoscx/README.md +++ b/aoscx/README.md @@ -16,11 +16,13 @@ docker tag vrnetlab/vr-aoscx:20210610000730 vrnetlab/vr-aoscx:10.07.0010 Tested booting and responding to SSH: * `ArubaOS-CX_10_12_0006.ova` (`arubaoscx-disk-image-genericx86-p4-20230531220439.vmdk`) +* `ArubaOS-CX_10_13_0005.ova` (`arubaoscx-disk-image-genericx86-p4-20231110145644.vmdk`) +* `ArubaOS-CX_10_14_1000.ova` (`arubaoscx-disk-image-genericx86-p4-20240731173624.vmdk`) ## System requirements -CPU: 2 core +CPU: 4 core -RAM: 4GB +RAM: 8GB Disk: <1GB diff --git a/aoscx/docker/launch.py b/aoscx/docker/launch.py index 04f9a0ef..c3bf63af 100755 --- a/aoscx/docker/launch.py +++ b/aoscx/docker/launch.py @@ -47,7 +47,7 @@ def __init__(self, hostname, username, password, conn_mode): logging.getLogger().info("Disk image was not found") exit(1) super(AOSCX_vm, self).__init__( - username, password, disk_image=disk_image, ram=4096, cpu="host,level=9", smp="2" + username, password, disk_image=disk_image, ram=8192, cpu="host,level=9", smp="4" ) self.hostname = hostname self.conn_mode = conn_mode From e11a7bf9d244f500fd835ddcab084d6580989514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Machado?= <63718541+jcpvdm@users.noreply.github.com> Date: Mon, 16 Dec 2024 09:17:23 +0000 Subject: [PATCH 02/50] backdoor to reset VR or specific VMs (#285) * backdoor to reset VR * option to reset specific VMs --- common/vrnetlab.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/common/vrnetlab.py b/common/vrnetlab.py index b683e413..9b415385 100644 --- a/common/vrnetlab.py +++ b/common/vrnetlab.py @@ -825,6 +825,24 @@ def start(self): else: self.update_health(1, "starting") + #file-based signalling backdoor to trigger a system reset (via qemu-monitor) on all or specific VMs. + #if file is empty: reset whole VR (all VMs) + #if file is non-empty: reset only specified VMs (comma separated list) + if os.path.exists('/reset'): + with open('/reset','rt') as f: + fcontent=f.read().strip() + vm_num_list=fcontent.split(',') + for vm in self.vms: + if (str(vm.num) in vm_num_list) or not fcontent: + try: + vm.qm.write("system_reset\r".encode()) + self.logger.debug(f"Sent qemu-monitor system_reset to VM num {vm.num} ") + except Exception as e: + self.logger.debug(f"Failed to send qemu-monitor system_reset to VM num {vm.num} ({e})") + try: + os.remove('/reset') + except Exception as e: + self.logger.debug(f"Failed to cleanup /reset file({e}). qemu-monitor system_reset will likely be triggered again on VMs") class QemuBroken(Exception): """Our Qemu instance is somehow broken""" From a9601990e3d04d5699f0751551386431f8d8d556 Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Mon, 16 Dec 2024 10:21:55 +0100 Subject: [PATCH 03/50] give ocnos some time to boot in the login routine (#295) --- ocnos/docker/launch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ocnos/docker/launch.py b/ocnos/docker/launch.py index 18168a28..37f386b1 100755 --- a/ocnos/docker/launch.py +++ b/ocnos/docker/launch.py @@ -70,6 +70,7 @@ def bootstrap_spin(self): if ridx == 0: # login self.logger.debug("matched login prompt") self.logger.debug("trying to log in with 'ocnos'") + time.sleep(15) self.wait_write("ocnos", wait=None) self.wait_write("ocnos", wait="Password:") From 5665736e8e7fcae0153d1b058b56d4231ed7577c Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Sun, 15 Dec 2024 21:17:42 +1300 Subject: [PATCH 04/50] Add vrnetlab base image --- build-base-image.sh | 13 +++++++++++++ vrnetlab-base.dockerfile | 28 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100755 build-base-image.sh create mode 100644 vrnetlab-base.dockerfile diff --git a/build-base-image.sh b/build-base-image.sh new file mode 100755 index 00000000..0ff3fd0b --- /dev/null +++ b/build-base-image.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# this script builds the vrnetlab base container image +# that is used in the dockerfiles of the NOS images + +set -e + +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +sudo docker build -t ghcr.io/srl-labs/vrnetlab-base:$1 \ + -f vrnetlab-base.dockerfile . \ No newline at end of file diff --git a/vrnetlab-base.dockerfile b/vrnetlab-base.dockerfile new file mode 100644 index 00000000..7b56e24a --- /dev/null +++ b/vrnetlab-base.dockerfile @@ -0,0 +1,28 @@ +FROM public.ecr.aws/docker/library/debian:bookworm-slim +LABEL org.opencontainers.image.authors="roman@dodin.dev" + +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 \ + tftpd-hpa \ + ssh \ + inetutils-ping \ + dnsutils \ + iptables \ + nftables \ + telnet \ + python3-pip \ + python3-passlib \ + dosfstools \ + genisoimage \ + && rm -rf /var/lib/apt/lists/* + +RUN pip install https://github.com/carlmontanari/scrapli/archive/refs/tags/2024.07.30.post1.zip \ + https://github.com/scrapli/scrapli_community/archive/refs/tags/2024.07.30.zip --break-system-packages From 9984c66f816bac98e6ae7bd62306c0a49e9d6bbe Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Thu, 19 Dec 2024 08:06:22 +1300 Subject: [PATCH 05/50] Add `cidfile` to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8ec320e9..e7461652 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ cisco*.bin *.xz *.vmdk *.iso +*cidfile .DS_Store */.DS_Store From f420953670a545809b7ce00839c67aff93e0a476 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Thu, 19 Dec 2024 08:13:53 +1300 Subject: [PATCH 06/50] Implement Scrapli - Implement scrapli for telnet console and qemu monitor - Add scrapli for core funcs (wait_write, read_until, expect) - Add conditional use of scrapli via 'use_scrapli' var. Default is disabled - Add colours to logging - Log env vars - Log if transparent mgmt intf is in use - Log if scrapli is in use - Log overlay image creation - Log defined SMP and RAM --- common/vrnetlab.py | 200 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 192 insertions(+), 8 deletions(-) diff --git a/common/vrnetlab.py b/common/vrnetlab.py index 9b415385..63ddd0d5 100644 --- a/common/vrnetlab.py +++ b/common/vrnetlab.py @@ -12,6 +12,13 @@ import telnetlib import time from pathlib import Path +import sys + +try: + from scrapli import Driver + from scrapli.logging import enable_basic_logging +except ImportError: + pass MAX_RETRIES = 60 @@ -80,8 +87,59 @@ def __init__( smp="1", mgmt_passthrough=False, min_dp_nics=0, + use_scrapli=False, ): + + self.use_scrapli = use_scrapli + + # configure logging self.logger = logging.getLogger() + + # set fancy logging colours + logging.addLevelName( logging.INFO, f"\x1B[1;32m\t{logging.getLevelName(logging.INFO)}\x1B[0m") + logging.addLevelName( logging.WARN, f"\x1B[1;38;5;220m\t{logging.getLevelName(logging.WARN)}\x1B[0m") + logging.addLevelName( logging.DEBUG, f"\x1B[1;94m\t{logging.getLevelName(logging.DEBUG)}\x1B[0m") + logging.addLevelName( logging.ERROR, f"\x1B[1;91m\t{logging.getLevelName(logging.ERROR)}\x1B[0m") + logging.addLevelName( logging.CRITICAL, f"\x1B[1;91m\t{logging.getLevelName(logging.CRITICAL)}\x1B[0m") + + """ + Configure Scrapli logger to only be INFO level. + Scrapli uses 'scrapli' logger by default, and + will write all channel i/o as DEBUG log level. + """ + self.scrapli_logger = logging.getLogger("scrapli") + self.scrapli_logger.setLevel(logging.INFO) + + # configure scrapli + if self.use_scrapli: + # init scrapli -- main telnet device + scrapli_tn_dev = { + "host": "127.0.0.1", + "port": 5000 + num, + "auth_bypass": True, + "auth_strict_key": False, + "transport": "telnet", + "timeout_socket": 3600, + "timeout_transport": 3600, + "timeout_ops": 3600, + } + + self.scrapli_tn = Driver(**scrapli_tn_dev) + + # init scrapli -- qemu monitor device + scrapli_qm_dev = { + "host": "127.0.0.1", + "port": 4000 + num, + "auth_bypass": True, + "auth_strict_key": False, + "transport": "telnet", + "timeout_socket": 3600, + "timeout_transport": 3600, + "timeout_ops": 3600, + } + + self.scrapli_qm = Driver(**scrapli_qm_dev) + # username / password to configure self.username = username @@ -170,6 +228,7 @@ def __init__( overlay_disk_image = ".".join(tokens) if not os.path.exists(overlay_disk_image): + self.logger.debug(f"class: {self.__class__.__name__}, disk_image: {disk_image}, overlay: {overlay_disk_image}") self.logger.debug("Creating overlay disk image") run_command( [ @@ -214,7 +273,21 @@ def __init__( self.qemu_args.insert(1, "-enable-kvm") def start(self): - self.logger.info("Starting %s" % self.__class__.__name__) + # self.logger.info("Starting %s" % self.__class__.__name__) + self.logger.info("START ENVIRONMENT VARIABLES".center(60, "-")) + for var, value in sorted(os.environ.items()): + self.logger.info(f"{var}: {value}") + self.logger.info("END ENVIRONMENT VARIABLES".center(60, "-")) + + self.logger.info(f"Launching {self.__class__.__name__} with {self.smp} SMP/VCPU and {self.ram} M of RAM") + + # give nice colours. Red if disabled, Green if enabled + mgmt_passthrough_coloured = f"\x1B[32mEnabled\x1B[0m" if self.mgmt_passthrough else f"\x1B[31mDisabled\x1B[0m" + use_scrapli_coloured = f"\x1B[32mEnabled\x1B[0m" if self.use_scrapli else f"\x1B[31mDisabled\x1B[0m" + + self.logger.info(f"Scrapli: {use_scrapli_coloured}") + self.logger.info(f"Transparent mgmt interface: {mgmt_passthrough_coloured}") + self.start_time = datetime.datetime.now() cmd = list(self.qemu_args) @@ -263,10 +336,13 @@ def start(self): self.logger.info("STDERR: %s" % errs) except: pass - + for i in range(1, MAX_RETRIES + 1): try: - self.qm = telnetlib.Telnet("127.0.0.1", 4000 + self.num) + if self.use_scrapli: + self.scrapli_qm.open() + else: + self.qm = telnetlib.Telnet("127.0.0.1", 4000 + self.num) break except: self.logger.info( @@ -284,7 +360,10 @@ def start(self): for i in range(1, MAX_RETRIES + 1): try: - self.tn = telnetlib.Telnet("127.0.0.1", 5000 + self.num) + if self.use_scrapli: + self.scrapli_tn.open() + else: + self.tn = telnetlib.Telnet("127.0.0.1", 5000 + self.num) break except: self.logger.info( @@ -649,6 +728,10 @@ def wait_write( Defaults to using self.tn as connection but this can be overridden by passing a telnetlib.Telnet object in the con argument. """ + + if self.use_scrapli: + return self.wait_write_scrapli(cmd, wait) + con_name = "custom con" if con is None: con = self.tn @@ -662,11 +745,11 @@ def wait_write( # use class default wait pattern if none was explicitly specified if wait == "__defaultpattern__": wait = self.wait_pattern - self.logger.trace(f"waiting for '{wait}' on {con_name}") + self.logger.info(f"waiting for '{wait}' on {con_name}") res = con.read_until(wait.encode()) while hold and (hold in res.decode()): - self.logger.trace( + self.logger.info( f"Holding pattern '{hold}' detected: {res.decode()}, retrying in 10s..." ) con.write("\r".encode()) @@ -677,13 +760,114 @@ def wait_write( (con.read_very_eager()) if clean_buffer else None ) # Clear any remaining characters in buffer - self.logger.trace(f"read from {con_name}: '{res.decode()}'") + self.logger.info(f"read from {con_name}: '{res.decode()}'") # log the cleaned buffer if it's not empty if cleaned_buf: - self.logger.trace(f"cleaned buffer: '{cleaned_buf.decode()}'") + self.logger.info(f"cleaned buffer: '{cleaned_buf.decode()}'") self.logger.debug(f"writing to {con_name}: '{cmd}'") con.write("{}\r".format(cmd).encode()) + + def wait_write_scrapli(self, cmd, wait="__defaultpattern__"): + """ + Wait for something on the serial port and then send command using Scrapli telnet channel + + Arguments are: + - cmd: command to send (string) + - wait: prompt to wait for before sending command, defaults to # (string) + """ + if wait: + # use class default wait pattern if none was explicitly specified + if wait == "__defaultpattern__": + wait = self.wait_pattern + + self.logger.info(f"Waiting on console for: '{wait}'") + + self.con_read_until(wait) + + time.sleep(0.1) # don't write to the console too fast + + self.write_to_stdout(b"\n") + + self.logger.info(f"Writing to console: '{cmd}'") + self.scrapli_tn.channel.write(f"{cmd}\r") + + def con_expect(self, regex_list, timeout=None): + """ + Implements telnetlib expect() functionality, for usage with scrapli driver. + Wait for something on the console. + + Takes list of byte strings and an optional timeout (block) time (float) as arguments. + + Returns tuple of: + - index of matched object from regex. + - match object. + - buffer of cosole read until match, or function exit. + """ + + buf = b"" + + if timeout: + t_end = time.time() + timeout + while time.time() < t_end: + buf += self.scrapli_tn.channel.read() + else: + buf = self.scrapli_tn.channel.read() + + for i, obj in enumerate(regex_list): + match = re.search(obj.decode(), buf.decode()) + if match: + return i, match, buf + + return -1, None, buf + + def con_read_until(self, match_str, timeout=None): + """ + Implements telnetlib read_until() functionality, for usage with scrapli driver. + + Read until a given string is encountered or until timeout. + + When no match is found, return whatever is available instead, + possibly the empty string. + + Arguments: + - match_str: string to match on (string) + - timeout: timeout in seconds, defaults to None (float) + """ + buf = b"" + + if timeout: + t_end = time.time() + timeout + + while True: + current_buf = self.scrapli_tn.channel.read() + buf += current_buf + + match = re.search(match_str, current_buf.decode()) + + # for reliability purposes, doublecheck the entire buffer + # maybe the current buffer only has partial output + if match is None: + match = re.search(match_str, buf.decode()) + + self.write_to_stdout(current_buf) + + if match: + break + if timeout and time.time() > t_end: + break + + return buf + + def write_to_stdout(self, bytes): + """ + Quick and dirty way to write to stdout (docker logs) instead of + using the python logger which poorly formats the output. + + Mainly for printing console to docker logs + """ + sys.stdout.buffer.write(bytes) + sys.stdout.buffer.flush() def work(self): self.check_qemu() From 92a154c2fb7bed5bd64d485eac5825d1df00cfc5 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Thu, 19 Dec 2024 08:15:37 +1300 Subject: [PATCH 07/50] cat8kv: Migrate to Scrapli - Use Scrapli IOSXEDriver for config - Update install VM var name to 'cat8kv' from 'csr' - Fix installer class init so overlay image is only created once --- c8000v/docker/Dockerfile | 18 +--- c8000v/docker/launch.py | 186 +++++++++++++++++++-------------------- 2 files changed, 94 insertions(+), 110 deletions(-) diff --git a/c8000v/docker/Dockerfile b/c8000v/docker/Dockerfile index 0550287f..2fab6fd7 100644 --- a/c8000v/docker/Dockerfile +++ b/c8000v/docker/Dockerfile @@ -1,20 +1,4 @@ -FROM public.ecr.aws/docker/library/debian:bookworm-slim - -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update -qy \ - && apt-get install -y \ - bridge-utils \ - iproute2 \ - socat \ - qemu-kvm \ - tcpdump \ - inetutils-ping \ - ssh \ - telnet \ - procps \ - genisoimage \ - && rm -rf /var/lib/apt/lists/* +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 ARG VERSION ENV VERSION=${VERSION} diff --git a/c8000v/docker/launch.py b/c8000v/docker/launch.py index 71454769..c2056048 100755 --- a/c8000v/docker/launch.py +++ b/c8000v/docker/launch.py @@ -9,9 +9,10 @@ import sys import vrnetlab +from scrapli.driver.core import IOSXEDriver STARTUP_CONFIG_FILE = "/config/startup-config.cfg" - +DEFAULT_SCRAPLI_TIMEOUT = 900 def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -52,7 +53,7 @@ def __init__(self, hostname, username, password, conn_mode, install_mode=False): logger.info("License found") self.license = True - super().__init__(username, password, disk_image=disk_image, ram=4096) + super().__init__(username, password, disk_image=disk_image, ram=4096, use_scrapli=True) self.install_mode = install_mode self.hostname = hostname self.conn_mode = conn_mode @@ -60,7 +61,7 @@ def __init__(self, hostname, username, password, conn_mode, install_mode=False): self.nic_type = "virtio-net-pci" if self.install_mode: - logger.trace("install mode") + self.logger.debug("Install mode") self.image_name = "config.iso" self.create_boot_image() @@ -104,28 +105,27 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect( - [b"Press RETURN to get started!", b"IOSXEBOOT-4-FACTORY_RESET"], 1 + (ridx, match, res) = self.con_expect( + [b"Press RETURN to get started!", b"IOSXEBOOT-4-FACTORY_RESET"] ) if match: # got a match! if ridx == 0: # login - self.logger.debug("matched, Press RETURN to get started.") + self.logger.info("matched, Press RETURN to get started.") if self.install_mode: - self.logger.debug("Now we wait for the device to reload") + self.logger.info("Now we wait for the device to reload") else: self.wait_write("", wait=None) - - # run main config! - self.bootstrap_config() - # add startup config if present - self.startup_config() + + self.apply_config() + # close telnet connection - self.tn.close() + self.scrapli_tn.close() + # startup time? startup_time = datetime.datetime.now() - self.start_time self.logger.info("Startup complete in: %s", startup_time) - # mark as running self.running = True + return elif ridx == 1: # IOSXEBOOT-4-FACTORY_RESET if self.install_mode: @@ -139,7 +139,7 @@ def bootstrap_spin(self): # 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("OUTPUT: %s", res.decode()) + self.write_to_stdout(res) # reset spins if we saw some output self.spins = 0 @@ -147,83 +147,83 @@ def bootstrap_spin(self): return - def bootstrap_config(self): - """Do the actual bootstrap config""" - self.logger.info("applying bootstrap configuration") - + def apply_config(self): + + scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) + self.logger.info(f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)") + + # init scrapli + cat8kv_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, + } + v4_mgmt_address = vrnetlab.cidr_to_ddn(self.mgmt_address_ipv4) - - self.wait_write("", None) - self.wait_write("enable", wait=">") - self.wait_write("configure terminal", wait=">") - - self.wait_write(f"hostname {self.hostname}") - self.wait_write( - "username %s privilege 15 password %s" % (self.username, self.password) - ) - if int(self.version.split(".")[0]) >= 16: - self.wait_write("ip domain name example.com") + + cat8kv_config = f"""hostname {self.hostname} +username {self.username} privilege 15 password {self.password} +ip domain name example.com +! +crypto key generate rsa modulus 2048 +! +line con 0 +logging synchronous +! +line vty 0 4 +logging synchronous +login local +transport input all +! +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 GigabitEthernet 1 +description Containerlab management interface +vrf forwarding clab-mgmt +ip address {v4_mgmt_address[0]} {v4_mgmt_address[1]} +ipv6 address {self.mgmt_address_ipv6} +no shut +exit +! +restconf +netconf-yang +netconf max-sessions 16 +netconf detailed-error +! +ip ssh server algorithm mac hmac-sha2-512 +ip ssh maxstartups 128 +! +""" + + con = IOSXEDriver(**cat8kv_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: + cat8kv_config += config.read() else: - self.wait_write("ip domain-name example.com") - self.wait_write("crypto key generate rsa modulus 2048") - - self.wait_write("ipv6 unicast-routing") - - 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") - - 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("interface GigabitEthernet1") - 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 shut") - self.wait_write("exit") - self.wait_write("restconf") - self.wait_write("netconf-yang") - self.wait_write("netconf max-sessions 16") - # I did not find any documentation about this, but is seems like a good idea!? - self.wait_write("netconf detailed-error") - self.wait_write("ip ssh server algorithm mac hmac-sha2-512") - self.wait_write("ip ssh maxstartups 128") - - self.wait_write("line vty 0 4") - self.wait_write("login local") - self.wait_write("transport input all") - self.wait_write("end") - self.wait_write("copy running-config startup-config") - self.wait_write("\r", "Destination") - - def startup_config(self): - """Load additional config provided by user.""" - - if not os.path.exists(STARTUP_CONFIG_FILE): - self.logger.trace(f"Startup config file {STARTUP_CONFIG_FILE} is not found") - return + self.logger.warning(f"User provided startup configuration is not found.") - self.logger.trace(f"Startup config file {STARTUP_CONFIG_FILE} exists") - with open(STARTUP_CONFIG_FILE) as file: - config_lines = file.readlines() - config_lines = [line.rstrip() for line in config_lines] - self.logger.trace(f"Parsed startup config file {STARTUP_CONFIG_FILE}") + res = con.send_configs(cat8kv_config.splitlines()) - self.logger.info(f"Writing lines from {STARTUP_CONFIG_FILE}") - - self.wait_write("configure terminal") - # Apply lines from file - for line in config_lines: - self.wait_write(line) - # End and Save - self.wait_write("end") - self.wait_write("copy running-config startup-config") - self.wait_write("\r", "Destination") + for response in res: + self.logger.info(f"CONFIG:{response.channel_input}") + self.logger.info(f"RESULT:{response.result}") class C8000v(vrnetlab.VR): @@ -240,17 +240,17 @@ class C8000v_installer(C8000v): """ def __init__(self, hostname, username, password, conn_mode): - super(C8000v_installer, self).__init__(hostname, username, password, conn_mode) + super(C8000v, self).__init__(username, password) self.vms = [ C8000v_vm(hostname, username, password, conn_mode, install_mode=True) ] def install(self): self.logger.info("Installing C8000v") - csr = self.vms[0] - while not csr.running: - csr.work() - csr.stop() + cat8kv = self.vms[0] + while not cat8kv.running: + cat8kv.work() + cat8kv.stop() self.logger.info("Installation complete") From 29b1d64c5bd6608a67f7d5db203fae72ca470eb3 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Thu, 19 Dec 2024 08:17:36 +1300 Subject: [PATCH 08/50] cat9kv: Migrate to Scrapli - Remove license check - Send bootstrap config via day0/CVAC config (mounted file to cdrom) - Send startup config via Scrapli IOSXEDriver --- cat9kv/docker/Dockerfile | 20 +---- cat9kv/docker/launch.py | 165 ++++++++++++++++++--------------------- 2 files changed, 75 insertions(+), 110 deletions(-) diff --git a/cat9kv/docker/Dockerfile b/cat9kv/docker/Dockerfile index 335c7dd1..8f79f39d 100644 --- a/cat9kv/docker/Dockerfile +++ b/cat9kv/docker/Dockerfile @@ -1,22 +1,4 @@ -FROM public.ecr.aws/docker/library/debian:bookworm-slim - -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update -qy \ - && apt-get install -y --no-install-recommends \ - bridge-utils \ - iproute2 \ - socat \ - qemu-kvm \ - qemu-utils \ - python3 \ - tcpdump \ - inetutils-ping \ - ssh \ - telnet \ - procps \ - genisoimage \ - && rm -rf /var/lib/apt/lists/* +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 ARG VERSION ENV VERSION=${VERSION} diff --git a/cat9kv/docker/launch.py b/cat9kv/docker/launch.py index acccb2c2..e5da270c 100755 --- a/cat9kv/docker/launch.py +++ b/cat9kv/docker/launch.py @@ -9,9 +9,10 @@ import sys import vrnetlab +from scrapli.driver.core import IOSXEDriver STARTUP_CONFIG_FILE = "/config/startup-config.cfg" - +DEFAULT_SCRAPLI_TIMEOUT = 900 def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -44,13 +45,6 @@ def __init__(self, hostname, username, password, conn_mode, vcpu, ram): for e in sorted(os.listdir("/")): if not disk_image and re.search(".qcow2$", e): disk_image = "/" + e - if re.search(r"\.license$", e): - os.rename("/" + e, "/tftpboot/license.lic") - - self.license = False - if os.path.isfile("/tftpboot/license.lic"): - logger.info("License found") - self.license = True super().__init__( username, @@ -59,6 +53,7 @@ def __init__(self, hostname, username, password, conn_mode, vcpu, ram): smp=f"cores={vcpu},threads=1,sockets=1", ram=ram, min_dp_nics=8, + use_scrapli=True ) self.hostname = hostname self.conn_mode = conn_mode @@ -91,9 +86,46 @@ def create_boot_image(self): except: self.logger.debug("No vswitch.xml file provided.") + v4_mgmt_address = vrnetlab.cidr_to_ddn(self.mgmt_address_ipv4) + + cat9kv_config = f"""hostname {self.hostname} +username {self.username} privilege 15 password {self.password} +ip domain name example.com +no ip domain lookup +! +crypto key generate rsa modulus 2048 +! +line con 0 +logging synchronous +! +line vty 0 4 +logging synchronous +login local +transport input all +! +ipv6 unicast-routing +! +ip route vrf Mgmt-vrf 0.0.0.0 0.0.0.0 {self.mgmt_gw_ipv4} +ipv6 route vrf Mgmt-vrf ::/0 {self.mgmt_gw_ipv6} +! +interface GigabitEthernet0/0 +description Containerlab management interface +ip address {v4_mgmt_address[0]} {v4_mgmt_address[1]} +ipv6 address {self.mgmt_address_ipv6} +no shut +exit +! +restconf +netconf-yang +netconf max-sessions 16 +netconf detailed-error +! +ip ssh server algorithm mac hmac-sha2-512 +! +""" + with open("/img_dir/iosxe_config.txt", "w") as cfg_file: - cfg_file.write(f"hostname {self.hostname}\r\n") - cfg_file.write("end\r\n") + cfg_file.write(cat9kv_config) genisoimage_args = [ "genisoimage", @@ -115,25 +147,25 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect( + (ridx, match, res) = self.con_expect( [ b"Press RETURN to get started!", b"IOSXEBOOT-4-FACTORY_RESET", ], - 1, ) if match: # got a match! if ridx == 0: # login self.logger.debug("matched, Press RETURN to get started.") - - self.wait_write("", wait=None) - - # run main config! - self.bootstrap_config() - # add startup config if present - self.startup_config() + + if os.path.exists(STARTUP_CONFIG_FILE): + self.logger.info("Startup configuration file found. Applying startup config.") + self.wait_write("", wait=None) + self.apply_startup_config() + else: + self.logger.warning(f"User provided startup configuration is not found.") + # close telnet connection - self.tn.close() + self.scrapli_tn.close() # startup time? startup_time = datetime.datetime.now() - self.start_time self.logger.info("Startup complete in: %s", startup_time) @@ -146,7 +178,7 @@ def bootstrap_spin(self): # 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("OUTPUT: %s", res.decode()) + self.write_to_stdout(res) # reset spins if we saw some output self.spins = 0 @@ -154,78 +186,29 @@ def bootstrap_spin(self): return - def bootstrap_config(self): - """Do the actual bootstrap config""" - self.logger.info("applying bootstrap configuration") + def apply_startup_config(self): - v4_mgmt_address = vrnetlab.cidr_to_ddn(self.mgmt_address_ipv4) - - self.wait_write("", None) - self.wait_write("enable", wait=">") - self.wait_write("configure terminal", wait=">") - - self.wait_write(f"hostname {self.hostname}") - self.wait_write( - "username %s privilege 15 password %s" % (self.username, self.password) - ) - if int(self.version.split(".")[0]) >= 16: - self.wait_write("ip domain name example.com") - else: - self.wait_write("ip domain-name example.com") - self.wait_write("crypto key generate rsa modulus 2048") - - self.wait_write("no ip domain lookup") + scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) + self.logger.info(f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)") - self.wait_write("ipv6 unicast-routing") - - # add mgmt vrf static route - 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("interface GigabitEthernet0/0") - 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 shut") - self.wait_write("exit") - - self.wait_write("restconf") - self.wait_write("netconf-yang") - self.wait_write("netconf max-sessions 16") - # I did not find any documentation about this, but is seems like a good idea!? - self.wait_write("netconf detailed-error") - self.wait_write("ip ssh server algorithm mac hmac-sha2-512") - self.wait_write("ip ssh maxstartups 128") - - self.wait_write("line vty 0 4") - self.wait_write("login local") - self.wait_write("transport input all") - self.wait_write("end") - self.wait_write("copy running-config startup-config") - self.wait_write("\r", "Destination") - - def startup_config(self): - """Load additional config provided by user.""" - - if not os.path.exists(STARTUP_CONFIG_FILE): - self.logger.trace(f"Startup config file {STARTUP_CONFIG_FILE} is not found") - return - - self.logger.trace(f"Startup config file {STARTUP_CONFIG_FILE} exists") - with open(STARTUP_CONFIG_FILE) as file: - config_lines = file.readlines() - config_lines = [line.rstrip() for line in config_lines] - self.logger.trace(f"Parsed startup config file {STARTUP_CONFIG_FILE}") - - self.logger.info(f"Writing lines from {STARTUP_CONFIG_FILE}") - - self.wait_write("configure terminal") - # Apply lines from file - for line in config_lines: - self.wait_write(line) - # End and Save - self.wait_write("end") - self.wait_write("copy running-config startup-config") - self.wait_write("\r", "Destination") + # init scrapli + cat9kv_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, + } + + con = IOSXEDriver(**cat9kv_scrapli_dev) + con.commandeer(conn=self.scrapli_tn) + + res = con.send_configs_from_file(STARTUP_CONFIG_FILE) + + for response in res: + self.logger.info(f"CONFIG:{response.channel_input}") + self.logger.info(f"RESULT:{response.result}") class cat9kv(vrnetlab.VR): From 9438bcc98c0a7a7f717029a6a4ecf4fc9b754309 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Thu, 19 Dec 2024 08:18:29 +1300 Subject: [PATCH 09/50] csr1kv: Migrate to Scrapli - Use Scrapli IOSXEDriver for sending bootstrap and startup configs --- csr/docker/Dockerfile | 22 +----- csr/docker/launch.py | 161 ++++++++++++++++++++++-------------------- 2 files changed, 85 insertions(+), 98 deletions(-) diff --git a/csr/docker/Dockerfile b/csr/docker/Dockerfile index 9cf2e68b..2fab6fd7 100644 --- a/csr/docker/Dockerfile +++ b/csr/docker/Dockerfile @@ -1,24 +1,4 @@ -FROM public.ecr.aws/docker/library/debian:bookworm-slim -MAINTAINER Kristian Larsson -MAINTAINER Denis Pointer - -ARG DEBIAN_FRONTEND=noninteractive - -RUN apt-get update -qy \ - && apt-get upgrade -qy \ - && apt-get install -y \ - bridge-utils \ - iproute2 \ - python3-ipy \ - socat \ - qemu-kvm \ - tcpdump \ - ssh \ - inetutils-ping \ - dnsutils \ - telnet \ - genisoimage \ - && rm -rf /var/lib/apt/lists/* +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 ARG VERSION ENV VERSION=${VERSION} diff --git a/csr/docker/launch.py b/csr/docker/launch.py index c4b3283d..68a208b8 100755 --- a/csr/docker/launch.py +++ b/csr/docker/launch.py @@ -10,9 +10,10 @@ import time import vrnetlab +from scrapli.driver.core import IOSXEDriver STARTUP_CONFIG_FILE = "/config/startup-config.cfg" - +DEFAULT_SCRAPLI_TIMEOUT = 900 def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -55,7 +56,7 @@ def __init__( logger.info("License found") self.license = True - super(CSR_vm, self).__init__(username, password, disk_image=disk_image) + super(CSR_vm, self).__init__(username, password, disk_image=disk_image, use_scrapli=True) self.install_mode = install_mode self.num_nics = nics @@ -64,7 +65,7 @@ def __init__( self.nic_type = "virtio-net-pci" if self.install_mode: - logger.trace("install mode") + self.logger.debug("install mode") self.image_name = "config.iso" self.create_boot_image() @@ -109,7 +110,7 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect([b"Press RETURN to get started!"], 1) + (ridx, match, res) = self.con_expect([b"Press RETURN to get started!"], 1) if match: # got a match! if ridx == 0: # login if self.install_mode: @@ -117,23 +118,25 @@ def bootstrap_spin(self): return self.logger.debug("matched, Press RETURN to get started.") + self.wait_write("", wait=None) - # run main config! - self.bootstrap_config() - self.startup_config() - self.running = True - # close telnet connection - self.tn.close() + self.apply_config() + + # close the telnet connection + self.scrapli_tn.close() + # startup time? startup_time = datetime.datetime.now() - self.start_time self.logger.info("Startup complete in: %s" % startup_time) + 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("OUTPUT: %s" % res.decode()) + self.write_to_stdout(res) # reset spins if we saw some output self.spins = 0 @@ -141,77 +144,81 @@ def bootstrap_spin(self): return - def bootstrap_config(self): - """Do the actual bootstrap config""" - self.logger.info("applying bootstrap configuration") + + def apply_config(self): + + scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) + self.logger.info(f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)") + + # init scrapli + csr_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, + } v4_mgmt_address = vrnetlab.cidr_to_ddn(self.mgmt_address_ipv4) - - self.wait_write("", None) - self.wait_write("enable", wait=">") - self.wait_write("configure terminal", wait=">") - - self.wait_write("hostname %s" % (self.hostname)) - self.wait_write( - "username %s privilege 15 password %s" % (self.username, self.password) - ) - if int(self.version.split('.')[0]) >= 16: - self.wait_write("ip domain name example.com") - else: - self.wait_write("ip domain-name example.com") - self.wait_write("crypto key generate rsa modulus 2048") - self.wait_write("ipv6 unicast-routing") + ip_domain_name = "ip domain name example.com" if int(self.version.split('.')[0]) >= 16 else "ip domain-name example.com" + + csr_config = f"""hostname {self.hostname} +username {self.username} privilege 15 password {self.password} +{ip_domain_name} +! +crypto key generate rsa modulus 2048 +! +line con 0 +logging synchronous +! +line vty 0 4 +logging synchronous +login local +transport input all +! +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 GigabitEthernet 1 +description Containerlab management interface +vrf forwarding clab-mgmt +ip address {v4_mgmt_address[0]} {v4_mgmt_address[1]} +ipv6 address {self.mgmt_address_ipv6} +no shut +exit +! +restconf +netconf-yang +! +""" + + con = IOSXEDriver(**csr_scrapli_dev) + con.commandeer(conn=self.scrapli_tn) - 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") - - 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("interface GigabitEthernet1") - 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 shut") - self.wait_write("exit") - self.wait_write("restconf") - self.wait_write("netconf-yang") - - self.wait_write("line vty 0 4") - self.wait_write("login local") - self.wait_write("transport input all") - self.wait_write("end") - self.wait_write("copy running-config startup-config") - self.wait_write("\r", None) - - def startup_config(self): - """Load additional config provided by user.""" - - if not os.path.exists(STARTUP_CONFIG_FILE): - self.logger.trace(f"Startup config file {STARTUP_CONFIG_FILE} is not found") - return + if os.path.exists(STARTUP_CONFIG_FILE): + self.logger.info("Startup configuration file found") + with open(STARTUP_CONFIG_FILE, "r") as config: + csr_config += config.read() + else: + self.logger.warning(f"User provided startup configuration is not found.") - self.logger.trace(f"Startup config file {STARTUP_CONFIG_FILE} exists") - with open(STARTUP_CONFIG_FILE) as file: - config_lines = file.readlines() - config_lines = [line.rstrip() for line in config_lines] - self.logger.trace(f"Parsed startup config file {STARTUP_CONFIG_FILE}") - - self.logger.info(f"Writing lines from {STARTUP_CONFIG_FILE}") - - self.wait_write("configure terminal") - # Apply lines from file - for line in config_lines: - self.wait_write(line) - # End and Save - self.wait_write("end") - self.wait_write("copy running-config startup-config") + res = con.send_configs(csr_config.splitlines()) + + for response in res: + self.logger.info(f"CONFIG:{response.channel_input}") + self.logger.info(f"RESULT:{response.result}") class CSR(vrnetlab.VR): From a011fa60be4e6e538220728dd17c9e6b5ebd51b3 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Thu, 19 Dec 2024 08:19:34 +1300 Subject: [PATCH 10/50] xrv: Migrate to Scrapli - Use Scrapli IOSXRDriver to send bootstrap and startup configs --- xrv/docker/Dockerfile | 18 +--- xrv/docker/launch.py | 201 ++++++++++++++++++------------------------ 2 files changed, 85 insertions(+), 134 deletions(-) diff --git a/xrv/docker/Dockerfile b/xrv/docker/Dockerfile index a5cbc2ac..34aa6673 100644 --- a/xrv/docker/Dockerfile +++ b/xrv/docker/Dockerfile @@ -1,20 +1,4 @@ -FROM public.ecr.aws/docker/library/debian:bookworm-slim -LABEL org.opencontainers.image.authors="roman@dodin.dev" - -ARG DEBIAN_FRONTEND=noninteractive - -RUN apt-get update -qy \ - && apt-get upgrade -qy \ - && apt-get install -y \ - bridge-utils \ - iproute2 \ - python3-ipy \ - socat \ - qemu-kvm \ - tcpdump \ - procps \ - openvswitch-switch \ - && rm -rf /var/lib/apt/lists/* +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 ARG IMAGE COPY $IMAGE* / diff --git a/xrv/docker/launch.py b/xrv/docker/launch.py index 662214d3..ae4e8c09 100755 --- a/xrv/docker/launch.py +++ b/xrv/docker/launch.py @@ -11,9 +11,10 @@ import time import vrnetlab +from scrapli.driver.core import IOSXRDriver STARTUP_CONFIG_FILE = "/config/startup-config.cfg" - +DEFAULT_SCRAPLI_TIMEOUT = 900 def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -46,12 +47,11 @@ def __init__(self, hostname, username, password, conn_mode): if re.search(".vmdk", e): disk_image = "/" + e super(XRV_vm, self).__init__( - username, password, disk_image=disk_image, ram=3072 - ) + username, password, disk_image=disk_image, ram=3072, use_scrapli=True) self.hostname = hostname self.conn_mode = conn_mode self.num_nics = 128 - self.credentials = [["admin", "admin"]] + self.credentials = [] self.xr_ready = False @@ -64,7 +64,7 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect( + (ridx, match, res) = self.con_expect( [ b"Press RETURN to get started", b"SYSTEM CONFIGURATION COMPLETE", @@ -72,7 +72,6 @@ def bootstrap_spin(self): b"Username:", b"^[^ ]+#", ], - 1, ) if match: # got a match! if ridx == 0: # press return to get started, so we press return! @@ -92,23 +91,22 @@ def bootstrap_spin(self): self.wait_write(self.password, wait="Enter secret again:") self.credentials.insert(0, [self.username, self.password]) if ridx == 3: # matched login prompt, so should login - self.logger.debug("matched login prompt") + self.logger.info("matched login prompt") try: username, password = self.credentials.pop(0) except IndexError as exc: self.logger.error("no more credentials to try") return - self.logger.debug( + self.logger.info( "trying to log in with %s / %s" % (username, password) ) self.wait_write(username, wait=None) self.wait_write(password, wait="Password:") if self.xr_ready == True and ridx == 4: # run main config! - self.bootstrap_config() - self.startup_config() + self.apply_config() # close telnet connection - self.tn.close() + self.scrapli_tn.close() # startup time? startup_time = datetime.datetime.now() - self.start_time self.logger.info("Startup complete in: %s" % startup_time) @@ -119,7 +117,7 @@ def bootstrap_spin(self): # 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("OUTPUT: %s" % res.decode()) + self.write_to_stdout(res) # reset spins if we saw some output self.spins = 0 @@ -127,115 +125,84 @@ def bootstrap_spin(self): return - def bootstrap_config(self): - """Do the actual bootstrap config""" - self.logger.info("applying bootstrap configuration") - self.wait_write("", None) - - self.wait_write("terminal length 0") - - self.wait_write("crypto key generate rsa") - # check if we are prompted to overwrite current keys - (ridx, match, res) = self.tn.expect( - [ - b"How many bits in the modulus", - b"Do you really want to replace them", - b"^[^ ]+#", - ], - 10, - ) - if match: # got a match! - if ridx == 0: - self.wait_write("2048", None) - elif ridx == 1: # press return to get started, so we press return! - self.wait_write("no", None) - - # make sure we get our prompt back - self.wait_write("") - - if self.username and self.password: - self.wait_write("admin") - self.wait_write("configure") - self.wait_write("username %s group root-system" % (self.username)) - self.wait_write("username %s group cisco-support" % (self.username)) - self.wait_write("username %s secret %s" % (self.username, self.password)) - self.wait_write("commit") - self.wait_write("exit") - self.wait_write("exit") - - self.wait_write("show interface description") - self.wait_write("configure") - self.wait_write("hostname {}".format(self.hostname)) - - # configure management vrf - self.wait_write("vrf clab-mgmt") - self.wait_write("description Containerlab management VRF (DO NOT DELETE)") - self.wait_write("address-family ipv4 unicast") - self.wait_write("exit") - self.wait_write("address-family ipv6 unicast") - self.wait_write("exit") - self.wait_write("exit") - - # add static route for management - self.wait_write("router static") - self.wait_write("vrf clab-mgmt") - self.wait_write("address-family ipv4 unicast") - self.wait_write(f"0.0.0.0/0 {self.mgmt_gw_ipv4}") - self.wait_write("exit") - self.wait_write("address-family ipv6 unicast") - self.wait_write(f"::/0 {self.mgmt_gw_ipv6}") - self.wait_write("exit") - self.wait_write("exit") - self.wait_write("exit") - - # configure ssh & netconf w/ vrf - self.wait_write("ssh server v2") - self.wait_write("ssh server vrf clab-mgmt") - self.wait_write("ssh server netconf port 830") # for 5.1.1 - self.wait_write("ssh server netconf vrf clab-mgmt") # for 5.3.3 - self.wait_write("netconf agent ssh") # for 5.1.1 - self.wait_write("netconf-yang agent ssh") # for 5.3.3 + def apply_config(self): - # configure gNMI - self.wait_write("grpc port 57400") - self.wait_write("grpc vrf clab-mgmt") - self.wait_write("grpc no-tls") + scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) + self.logger.info(f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)") - # configure xml agent - self.wait_write("xml agent tty") + # init scrapli + xrv_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, + } + + xrv_config = f"""hostname {self.hostname} +vrf clab-mgmt +description Containerlab management VRF (DO NOT DELETE) +address-family ipv4 unicast +exit +address-family ipv6 unicast +root +! +router static +vrf clab-mgmt +address-family ipv4 unicast +0.0.0.0/0 {self.mgmt_gw_ipv4} +address-family ipv6 unicast +::/0 {self.mgmt_gw_ipv6} +root +! +interface MgmtEth 0/0/CPU0/0 +description Containerlab management interface +vrf clab-mgmt +ipv4 address {self.mgmt_address_ipv4} +ipv6 address {self.mgmt_address_ipv6} +no shut +exit +! +ssh server v2 +ssh server vrf clab-mgmt +ssh server netconf port 830 +ssh server netconf vrf clab-mgmt +netconf agent ssh +netconf-yang agent ssh +! +grpc port 57400 +grpc vrf clab-mgmt +grpc no-tls +! +xml agent tty +! +""" + + con = IOSXRDriver(**xrv_scrapli_dev) + con.commandeer(conn=self.scrapli_tn) - # configure mgmt interface - self.wait_write("interface MgmtEth 0/0/CPU0/0") - self.wait_write("vrf clab-mgmt") - self.wait_write("no shutdown") - self.wait_write(f"ipv4 address {self.mgmt_address_ipv4}") - self.wait_write(f"ipv6 address {self.mgmt_address_ipv6}") - self.wait_write("exit") - self.wait_write("commit") - self.wait_write("exit") - - def startup_config(self): - """Load additional config provided by user.""" - - if not os.path.exists(STARTUP_CONFIG_FILE): - self.logger.trace(f"Startup config file {STARTUP_CONFIG_FILE} is not found") - return + if os.path.exists(STARTUP_CONFIG_FILE): + self.logger.info("Startup configuration file found") + with open(STARTUP_CONFIG_FILE, "r") as config: + xrv_config += config.read() + else: + self.logger.warning(f"User provided startup configuration is not found.") + + # configure SSH keys + con.send_interactive( + [ + ("crypto key generate rsa", "How many bits in the modulus [2048]", False), + ("2048", "#", True), + ] + ) - self.logger.trace(f"Startup config file {STARTUP_CONFIG_FILE} exists") - with open(STARTUP_CONFIG_FILE) as file: - config_lines = file.readlines() - config_lines = [line.rstrip() for line in config_lines] - self.logger.trace(f"Parsed startup config file {STARTUP_CONFIG_FILE}") - - self.logger.info(f"Writing lines from {STARTUP_CONFIG_FILE}") - - self.wait_write("configure") - # Apply lines from file - for line in config_lines: - self.wait_write(line) - # Commit and GTFO - self.wait_write("commit") - self.wait_write("exit") + res = con.send_configs(xrv_config.splitlines()) + con.send_configs(["commit best-effort label CLAB_BOOTSTRAP", "end"], eager=True) + + for response in res: + self.logger.info(f"CONFIG:{response.channel_input}") + self.logger.info(f"RESULT:{response.result}") class XRV(vrnetlab.VR): From 2958e525f2fd3c421423b8101e32ccc48f5f9b81 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Thu, 19 Dec 2024 08:20:47 +1300 Subject: [PATCH 11/50] xrv: Add convert-image target in Makefile - Converts the qcow2 image into required vmdk format for vrnetlab via qemu-img. --- xrv/Makefile | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/xrv/Makefile b/xrv/Makefile index 59c24901..d5305e1d 100644 --- a/xrv/Makefile +++ b/xrv/Makefile @@ -2,6 +2,7 @@ VENDOR=Cisco NAME=XRv IMAGE_FORMAT=vmdk IMAGE_GLOB=*vmdk* +QCOW=$(shell ls *qcow2* 2>/dev/null) # match versions like: # iosxrv-k9-demo-5.3.3.51U.vmdk @@ -13,3 +14,14 @@ VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+[^0-9]\([0-9]\+\.[0-9]\+\.[0-9]\+\( -include ../makefile-sanity.include -include ../makefile.include + +convert-image: + @if [ -z "$QCOW" ]; then echo "\033[1;31mERROR:\033[0m No .qcow2 image found"; exit 1; fi + @printf "\033[1;32mFound image $(QCOW)\033[0m.\n" +ifeq (, $(shell which qemu-img)) + @printf "\033[1;31mERROR\033[0m: qemu-img not found. Please install 'qemu-img' or 'qemu-utils'.\n"; exit 1; +endif + $(eval FILE_NAME := $(shell basename $(QCOW) .qcow2)) + qemu-img convert -cpf qcow2 -O vmdk $(QCOW) $(FILE_NAME).vmdk + + From d8ca50dbd109fefd116d5b9b92e32ba2c2d64be4 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Thu, 19 Dec 2024 08:25:24 +1300 Subject: [PATCH 12/50] xrv9k: Migrate to Scrapli - Use Scrapli IOSXRDriver for bootstrap and startup configs - Change class names to 'XRv9k' instead of 'XRv' - Explicitly wait for SDR baking to complete in install process - Remove call home/LC check --- xrv9k/docker/Dockerfile | 21 +-- xrv9k/docker/launch.py | 294 ++++++++++++++-------------------------- 2 files changed, 101 insertions(+), 214 deletions(-) diff --git a/xrv9k/docker/Dockerfile b/xrv9k/docker/Dockerfile index 73e3a8f5..8c6a929e 100644 --- a/xrv9k/docker/Dockerfile +++ b/xrv9k/docker/Dockerfile @@ -1,23 +1,4 @@ -FROM public.ecr.aws/docker/library/debian:bookworm-slim -LABEL org.opencontainers.image.authors="roman@dodin.dev" - -ARG DEBIAN_FRONTEND=noninteractive - -RUN apt-get update -qy \ - && apt-get upgrade -qy \ - && apt-get install -y \ - bridge-utils \ - iproute2 \ - python3-ipy \ - socat \ - qemu-kvm \ - tcpdump \ - ssh \ - telnet \ - inetutils-ping \ - dnsutils \ - openvswitch-switch \ - && rm -rf /var/lib/apt/lists/* +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 ARG IMAGE COPY $IMAGE* / diff --git a/xrv9k/docker/launch.py b/xrv9k/docker/launch.py index 50ba30c5..b37e1de3 100755 --- a/xrv9k/docker/launch.py +++ b/xrv9k/docker/launch.py @@ -10,9 +10,10 @@ import vrnetlab +from scrapli.driver.core import IOSXRDriver STARTUP_CONFIG_FILE = "/config/startup-config.cfg" - +DEFAULT_SCRAPLI_TIMEOUT = 900 def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -39,13 +40,13 @@ def trace(self, message, *args, **kws): logging.Logger.trace = trace -class XRV_vm(vrnetlab.VM): +class XRv9k_vm(vrnetlab.VM): def __init__(self, hostname, username, password, nics, conn_mode, vcpu, ram, install=False): disk_image = None for e in sorted(os.listdir("/")): if not disk_image and re.search(".qcow2", e): disk_image = "/" + e - super(XRV_vm, self).__init__(username, password, disk_image=disk_image, ram=ram, smp=f"cores={vcpu},threads=1,sockets=1") + super(XRv9k_vm, self).__init__(username, password, disk_image=disk_image, ram=ram, smp=f"cores={vcpu},threads=1,sockets=1", use_scrapli=True) self.hostname = hostname self.conn_mode = conn_mode self.num_nics = nics @@ -66,9 +67,6 @@ def __init__(self, hostname, username, password, nics, conn_mode, vcpu, ram, ins "telnet:0.0.0.0:50%02d,server,nowait" % (self.num + 3), ] ) - self.credentials = [] - - self.xr_ready = False def gen_mgmt(self): """Generate qemu args for the mgmt interface(s)""" @@ -110,82 +108,44 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect( + (ridx, match, res) = self.con_expect( [ b"Press RETURN to get started", - b"Not settable: Success", # no SYSTEM CONFIGURATION COMPLETE in xrv9k? b"Enter root-system [U|u]sername", - b"Username:", + b"XR partition preparation completed successfully", ], - 1, ) - xr_login = False # whether we are logged into the shell or not - if match: # got a match! if ridx == 0: # press return to get started, so we press return! - self.logger.debug("got 'press return to get started...'") + self.logger.info("got 'press return to get started...'") self.wait_write("", wait=None) - if ridx == 1: # system configuration complete - self.logger.info( - "IOS XR system configuration is complete, should be able to proceed with bootstrap configuration" - ) - self.wait_write("", wait=None) - self.xr_ready = True - if ridx == 2: # initial user config - # if we are installing and we reach this point, we are finished and don't need to bootstrap - if self.install_mode: - self.running = True - return - self.logger.info("Creating initial user") + if ridx == 1 and not self.install_mode: # initial user config + self.logger.info("Caught user creation prompt. Creating initial user") self.wait_write(self.username, wait=None) self.wait_write(self.password, wait="Enter secret:") self.wait_write(self.password, wait="Enter secret again:") - self.credentials.insert(0, [self.username, self.password]) - if ridx == 3: # matched login prompt, so should login - self.logger.debug("matched login prompt") - - try: - username, password = self.credentials[0] - except IndexError: - self.logger.error("no credentials populated") - return - - self.logger.debug( - "trying to log in with %s / %s" % (username, password) - ) - self.wait_write(username, wait=None) - self.wait_write(password, wait="Password:") - - _, match, res = self.tn.expect([b"ios#"], 3) - if match: - self.logger.debug("logged in with %s / %s successfully" % (username, password)) - xr_login = True - else: - self.logger.error("could not login with %s / %s" % (username, password)) - - if self.xr_ready is True and xr_login is True: - # run main config! - if not self.bootstrap_config(): - # main config failed :/ - self.logger.debug("bootstrap_config failed, restarting device") - self.stop() - self.start() - return - self.startup_config() - # close telnet connection - self.tn.close() + self.write_to_stdout(self.scrapli_tn.channel.read()) + + self.apply_config() + # startup time? startup_time = datetime.datetime.now() - self.start_time self.logger.info("Startup complete in: %s" % startup_time) # mark as running self.running = True return + if ridx == 2 and self.install_mode: + # SDR/XR image bake is complete, install finished + install_time = datetime.datetime.now() - self.start_time + self.logger.info("Install complete in: %s", install_time) + 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("OUTPUT: %s" % res.decode()) + self.write_to_stdout(res) # reset spins if we saw some output self.spins = 0 @@ -193,155 +153,102 @@ def bootstrap_spin(self): return - def bootstrap_config(self): - """Do the actual bootstrap config""" - self.logger.info("applying bootstrap configuration") - self.wait_write("", None) - - self.wait_write("terminal length 0") - - self.wait_write("crypto key generate rsa") - # check if we are prompted to overwrite current keys - (ridx, match, res) = self.tn.expect( - [ - b"How many bits in the modulus", - b"Do you really want to replace them", - b"^[^ ]+#", - ], - 10, - ) - if match: # got a match! - if ridx == 0: - self.wait_write("2048", None) - elif ridx == 1: # press return to get started, so we press return! - self.wait_write("no", None) - - # make sure we get our prompt back - self.wait_write("") - - # wait for linecard to show up - if not self._wait_config("show platform | in LC", "IOS XR RUN"): - return False - - self.wait_write("configure") - self.wait_write(f"hostname {self.hostname}") + def apply_config(self): - # configure management vrf - self.wait_write("vrf clab-mgmt") - self.wait_write("description Containerlab management VRF (DO NOT DELETE)") - self.wait_write("address-family ipv4 unicast") - self.wait_write("exit") - self.wait_write("address-family ipv6 unicast") - self.wait_write("exit") - self.wait_write("exit") + scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) + self.logger.info(f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)") - # add static route for management - self.wait_write("router static") - self.wait_write("vrf clab-mgmt") - self.wait_write("address-family ipv4 unicast") - self.wait_write(f"0.0.0.0/0 {self.mgmt_gw_ipv4}") - self.wait_write("exit") - self.wait_write("address-family ipv6 unicast") - self.wait_write(f"::/0 {self.mgmt_gw_ipv6}") - self.wait_write("exit") - self.wait_write("exit") - self.wait_write("exit") - - # configure ssh & netconf w/ vrf - self.wait_write("ssh server v2") - self.wait_write("ssh server vrf clab-mgmt") - self.wait_write("ssh server netconf port 830") # for 5.1.1 - self.wait_write("ssh server netconf vrf clab-mgmt") # for 5.3.3 - self.wait_write("netconf agent ssh") # for 5.1.1 - self.wait_write("netconf-yang agent ssh") # for 5.3.3 - # configure gNMI - self.wait_write("grpc port 57400") - self.wait_write("grpc vrf clab-mgmt") - self.wait_write("grpc no-tls") - - # configure xml agent - self.wait_write("xml agent tty") + # init scrapli + xrv9k_scrapli_dev = { + "host": "127.0.0.1", + "port": 5000 + self.num, + "auth_username": self.username, + "auth_password": self.password, + "auth_strict_key": False, + "transport": "telnet", + "timeout_socket": scrapli_timeout, + "timeout_transport": scrapli_timeout, + "timeout_ops": scrapli_timeout, + } + + xrv9k_config = f"""hostname {self.hostname} +vrf clab-mgmt +description Containerlab management VRF (DO NOT DELETE) +address-family ipv4 unicast +exit +address-family ipv6 unicast +root +! +router static +vrf clab-mgmt +address-family ipv4 unicast +0.0.0.0/0 {self.mgmt_gw_ipv4} +address-family ipv6 unicast +::/0 {self.mgmt_gw_ipv6} +root +! +interface MgmtEth 0/RP0/CPU0/0 +description Containerlab management interface +vrf clab-mgmt +ipv4 address {self.mgmt_address_ipv4} +ipv6 address {self.mgmt_address_ipv6} +no shutdown +exit +! +ssh server v2 +ssh server vrf clab-mgmt +ssh server netconf +! +grpc port 57400 +grpc vrf clab-mgmt +grpc no-tls +! +xml agent tty +! +""" - # configure mgmt interface - self.wait_write("interface MgmtEth0/RP0/CPU0/0") - self.wait_write("vrf clab-mgmt") - self.wait_write("no shutdown") - self.wait_write(f"ipv4 address {self.mgmt_address_ipv4}") - self.wait_write(f"ipv6 address {self.mgmt_address_ipv6}") - self.wait_write("commit") - self.wait_write("end") - - return True - - def startup_config(self): - """Load additional config provided by user.""" - - if not os.path.exists(STARTUP_CONFIG_FILE): - self.logger.trace(f"Startup config file {STARTUP_CONFIG_FILE} is not found") - return + if os.path.exists(STARTUP_CONFIG_FILE): + self.logger.info("Startup configuration file found") + with open(STARTUP_CONFIG_FILE, "r") as config: + xrv9k_config += config.read() + else: + self.logger.warning(f"User provided startup configuration is not found.") + + self.scrapli_tn.close() + + with IOSXRDriver(**xrv9k_scrapli_dev) as con: + res = con.send_configs(xrv9k_config.splitlines()) + con.send_configs(["commit best-effort label CLAB_BOOTSTRAP", "end"], eager=True) + + for response in res: + self.logger.info(f"CONFIG:{response.channel_input}") + self.logger.info(f"RESULT:{response.result}") + - self.logger.trace(f"Startup config file {STARTUP_CONFIG_FILE} exists") - with open(STARTUP_CONFIG_FILE) as file: - config_lines = file.readlines() - config_lines = [line.rstrip() for line in config_lines] - self.logger.trace(f"Parsed startup config file {STARTUP_CONFIG_FILE}") - - self.logger.info(f"Writing lines from {STARTUP_CONFIG_FILE}") - - self.wait_write("configure") - # Apply lines from file - for line in config_lines: - self.wait_write(line) - # Commit and GTFO - self.wait_write("commit") - self.wait_write("exit") - - - def _wait_config(self, show_cmd, expect): - """Some configuration takes some time to "show up". - To make sure the device is really ready, wait here. - """ - self.logger.debug("waiting for {} to appear in {}".format(expect, show_cmd)) - wait_spins = 0 - # 10s * 90 = 900s = 15min timeout - while wait_spins < 90: - self.wait_write(show_cmd, wait=None) - _, match, data = self.tn.expect([expect.encode("UTF-8")], timeout=10) - self.logger.trace(data.decode("UTF-8")) - if match: - self.logger.debug("a wild {} has appeared!".format(expect)) - return True - wait_spins += 1 - self.logger.error("{} not found in {}".format(expect, show_cmd)) - return False - - -class XRV(vrnetlab.VR): +class XRv9k(vrnetlab.VR): def __init__(self, hostname, username, password, nics, conn_mode, vcpu, ram): - super(XRV, self).__init__(username, password) - self.vms = [XRV_vm(hostname, username, password, nics, conn_mode, vcpu, ram)] + super(XRv9k, self).__init__(username, password) + self.vms = [XRv9k_vm(hostname, username, password, nics, conn_mode, vcpu, ram)] -class XRV_Installer(XRV): - """ XRV installer - Will start the XRV and then shut it down. Booting the XRV for the - first time requires the XRV itself to install internal packages +class XRv9k_Installer(XRv9k): + """ XRv9k installer + Will start the XRv9k and then shut it down. Booting the XRv9k for the + first time requires the XRv9k itself to install internal packages then it will restart. Subsequent boots will not require this restart. By running this "install" when building the docker image we can - decrease the normal startup time of the XRV. + decrease the normal startup time of the XRv9k. """ def __init__(self, hostname, username, password, nics, conn_mode, vcpu, ram): - super(XRV, self).__init__(username, password) - self.vms = [XRV_vm(hostname, username, password, nics, conn_mode, vcpu, ram, install=True)] + super(XRv9k, self).__init__(username, password) + self.vms = [XRv9k_vm(hostname, username, password, nics, conn_mode, vcpu, ram, install=True)] def install(self): self.logger.info("Installing XRv9k") xrv = self.vms[0] while not xrv.running: xrv.work() - time.sleep(30) xrv.stop() - self.logger.info("Installation complete") if __name__ == "__main__": @@ -377,11 +284,10 @@ def install(self): if args.trace: logger.setLevel(1) - logger.debug(f"Environment variables: {os.environ}") vrnetlab.boot_delay() if args.install: - vr = XRV_Installer( + vr = XRv9k_Installer( args.hostname, args.username, args.password, @@ -392,7 +298,7 @@ def install(self): ) vr.install() else: - vr = XRV( + vr = XRv9k( args.hostname, args.username, args.password, From 8bdfba396c4de9e08463a0e361fc80164d7e0721 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Thu, 19 Dec 2024 21:59:56 +1300 Subject: [PATCH 13/50] xrv: Remove env var printing --- xrv/docker/launch.py | 1 - 1 file changed, 1 deletion(-) diff --git a/xrv/docker/launch.py b/xrv/docker/launch.py index ae4e8c09..62ccbb31 100755 --- a/xrv/docker/launch.py +++ b/xrv/docker/launch.py @@ -242,7 +242,6 @@ def __init__(self, hostname, username, password, conn_mode): ) ) - logger.debug(f"Environment variables: {os.environ}") vrnetlab.boot_delay() vr = XRV(args.hostname, args.username, args.password, args.connection_mode) From eb427d3a72672bd569d2caae85a2b2089c3538da Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Thu, 19 Dec 2024 22:12:25 +1300 Subject: [PATCH 14/50] n9kv: Migrate to Scrapli - Use NXOSDriver for bootstrap and startup configs --- n9kv/docker/Dockerfile | 22 +------- n9kv/docker/launch.py | 122 ++++++++++++++++++++--------------------- 2 files changed, 60 insertions(+), 84 deletions(-) diff --git a/n9kv/docker/Dockerfile b/n9kv/docker/Dockerfile index f0814d73..a72c601e 100644 --- a/n9kv/docker/Dockerfile +++ b/n9kv/docker/Dockerfile @@ -1,24 +1,4 @@ -FROM public.ecr.aws/docker/library/debian:bookworm-slim -LABEL maintainer="Roman Dodin , Kaelem Chandra " - -ARG DEBIAN_FRONTEND=noninteractive -RUN apt-get update -qy \ - && apt-get install -y --no-install-recommends \ - bridge-utils \ - iproute2 \ - python3-ipy \ - socat \ - qemu-kvm \ - tcpdump \ - tftpd-hpa \ - ssh \ - inetutils-ping \ - dnsutils \ - openvswitch-switch \ - iptables \ - nftables \ - telnet \ - && rm -rf /var/lib/apt/lists/* +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 ARG IMAGE COPY $IMAGE* / diff --git a/n9kv/docker/launch.py b/n9kv/docker/launch.py index ab36a422..584f5af3 100755 --- a/n9kv/docker/launch.py +++ b/n9kv/docker/launch.py @@ -9,9 +9,10 @@ import time import vrnetlab +from scrapli.driver.core import NXOSDriver STARTUP_CONFIG_FILE = "/config/startup-config.cfg" - +DEFAULT_SCRAPLI_TIMEOUT = 900 def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -49,7 +50,7 @@ def __init__(self, hostname, username, password, conn_mode): exit(1) super(N9KV_vm, self).__init__( username, password, disk_image=disk_image, ram=10240, - smp=4, cpu="host,level=9" + smp=4, cpu="host,level=9", use_scrapli=True ) self.hostname = hostname self.conn_mode = conn_mode @@ -88,7 +89,7 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect([b"\(yes\/skip\/no\)\[no\]:",b"\(yes\/no\)\[n\]:", b"\(yes\/no\)\[no\]:", b"login:"], 1) + (ridx, match, res) = self.con_expect([b"\(yes\/skip\/no\)\[no\]:",b"\(yes\/no\)\[n\]:", b"\(yes\/no\)\[no\]:", b"login:"]) if match: # got a match! if ridx in (0, 1, 2): self.logger.debug("matched poap prompt") @@ -108,10 +109,9 @@ def bootstrap_spin(self): self.wait_write(self.password, wait="Password:") # run main config! - self.bootstrap_config() - self.startup_config() + self.apply_config() # close telnet connection - self.tn.close() + self.scrapli_tn.close() # startup time? startup_time = datetime.datetime.now() - self.start_time self.logger.info("Startup complete in: %s" % startup_time) @@ -122,7 +122,7 @@ def bootstrap_spin(self): # 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("OUTPUT: %s" % res.decode()) + self.write_to_stdout(res) # reset spins if we saw some output self.spins = 0 @@ -130,64 +130,61 @@ def bootstrap_spin(self): return - def bootstrap_config(self): - """Do the actual bootstrap config""" - self.logger.info("applying bootstrap configuration") - self.wait_write("", None) - self.wait_write("configure") - self.wait_write(f"hostname {self.hostname}") - self.wait_write( - f"username {self.username} password 0 {self.password} role network-admin" - ) + def apply_config(self): - # configure management vrf - self.wait_write("vrf context management") - self.wait_write(f"ip route 0.0.0.0/0 {self.mgmt_gw_ipv4}") - self.wait_write(f"ipv6 route ::/0 {self.mgmt_gw_ipv6}") - self.wait_write("exit") - - # configure mgmt interface - self.wait_write("interface mgmt0") - self.wait_write(f"ip address {self.mgmt_address_ipv4}") - self.wait_write(f"ipv6 address {self.mgmt_address_ipv6}") - self.wait_write("exit") + scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) + self.logger.info(f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)") - # configure longer ssh keys - self.wait_write("ssh key rsa 2048 force") - self.wait_write("feature ssh") - - # setup nxapi/scp server - self.wait_write("feature scp-server") - self.wait_write("feature nxapi") - self.wait_write("feature telnet") - self.wait_write("feature netconf") - self.wait_write("feature grpc") - self.wait_write("exit") - self.wait_write("copy running-config startup-config") - self.wait_write("! Bootstrap Config for ContainerLab Complete.", wait="Copy complete.") - - def startup_config(self): - """Load additional config provided by user.""" - - if not os.path.exists(STARTUP_CONFIG_FILE): - self.logger.trace(f"Startup config file {STARTUP_CONFIG_FILE} is not found") - return - - self.logger.trace(f"Startup config file {STARTUP_CONFIG_FILE} exists") - with open(STARTUP_CONFIG_FILE) as file: - config_lines = file.readlines() - config_lines = [line.rstrip() for line in config_lines] - self.logger.trace(f"Parsed startup config file {STARTUP_CONFIG_FILE}") - - self.logger.info(f"Writing lines from {STARTUP_CONFIG_FILE}") + # init scrapli + n9kv_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, + } + + n9kv_config = f"""hostname {self.hostname} +username {self.username} password 0 {self.password} role network-admin +! +vrf context management +ip route 0.0.0.0/0 {self.mgmt_gw_ipv4} +ipv6 route ::/0 {self.mgmt_gw_ipv6} +exit +! +interface mgmt0 +ip address {self.mgmt_address_ipv4} +ipv6 address {self.mgmt_address_ipv6} +exit +! +ssh key rsa 2048 force +feature ssh +! +feature scp-server +feature nxapi +feature telnet +feature netconf +feature grpc +! +""" + + con = NXOSDriver(**n9kv_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: + n9kv_config += config.read() + else: + self.logger.warning(f"User provided startup configuration is not found.") - self.wait_write("configure terminal") - # Apply lines from file - for line in config_lines: - self.wait_write(line) - # End and Save - self.wait_write("end") - self.wait_write("copy running-config startup-config") + res = con.send_configs(n9kv_config.splitlines()) + con.send_config("copy running-config startup-config", eager=True) + + for response in res: + self.logger.info(f"CONFIG:{response.channel_input}") + self.logger.info(f"RESULT:{response.result}") class N9KV(vrnetlab.VR): @@ -221,7 +218,6 @@ def __init__(self, hostname, username, password, conn_mode): if args.trace: logger.setLevel(1) - logger.debug(f"Environment variables: {os.environ}") vrnetlab.boot_delay() vr = N9KV(args.hostname, args.username, args.password, args.connection_mode) From d95f4626011eeb333230f4e75806e0cd7878d036 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Thu, 19 Dec 2024 22:13:19 +1300 Subject: [PATCH 15/50] nxos: Migrate to Scrapli - Use NXOSDriver for bootstrap and startup configs --- nxos/docker/Dockerfile | 18 +------ nxos/docker/launch.py | 113 ++++++++++++++++++++--------------------- 2 files changed, 56 insertions(+), 75 deletions(-) diff --git a/nxos/docker/Dockerfile b/nxos/docker/Dockerfile index 6ec3ca93..1527c896 100644 --- a/nxos/docker/Dockerfile +++ b/nxos/docker/Dockerfile @@ -1,20 +1,4 @@ -FROM public.ecr.aws/docker/library/debian:bookworm-slim -LABEL org.opencontainers.image.authors="roman@dodin.dev" - -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update -qy \ - && apt-get upgrade -qy \ - && apt-get install -y \ - bridge-utils \ - iproute2 \ - python3-ipy \ - socat \ - qemu-kvm \ - tcpdump \ - procps \ - openvswitch-switch \ - && rm -rf /var/lib/apt/lists/* +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 ARG IMAGE COPY $IMAGE* / diff --git a/nxos/docker/launch.py b/nxos/docker/launch.py index 26aa2a72..54f56e35 100755 --- a/nxos/docker/launch.py +++ b/nxos/docker/launch.py @@ -9,9 +9,10 @@ import time import vrnetlab +from scrapli.driver.core import NXOSDriver STARTUP_CONFIG_FILE = "/config/startup-config.cfg" - +DEFAULT_SCRAPLI_TIMEOUT = 900 def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -44,7 +45,7 @@ def __init__(self, hostname, username, password, conn_mode): if re.search(".qcow2$", e): disk_image = "/" + e super(NXOS_vm, self).__init__( - username, password, disk_image=disk_image, ram=4096, smp="2" + username, password, disk_image=disk_image, ram=4096, smp="2", use_scrapli=True ) self.credentials = [["admin", "admin"]] self.hostname = hostname @@ -59,7 +60,7 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect([b"login:"], 1) + (ridx, match, res) = self.con_expect([b"login:"]) if match: # got a match! if ridx == 0: # login self.logger.debug("matched login prompt") @@ -75,10 +76,9 @@ def bootstrap_spin(self): self.wait_write(password, wait="Password:") # run main config! - self.bootstrap_config() - self.startup_config() + self.apply_config() # close telnet connection - self.tn.close() + self.scrapli_tn.close() # startup time? startup_time = datetime.datetime.now() - self.start_time self.logger.info("Startup complete in: %s" % startup_time) @@ -89,7 +89,7 @@ def bootstrap_spin(self): # 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("OUTPUT: %s" % res.decode()) + self.write_to_stdout(res) # reset spins if we saw some output self.spins = 0 @@ -97,59 +97,56 @@ def bootstrap_spin(self): return - def bootstrap_config(self): - """Do the actual bootstrap config""" - self.logger.info("applying bootstrap configuration") - self.wait_write("", None) - self.wait_write("configure") - self.wait_write( - "username %s password 0 %s role network-admin" - % (self.username, self.password) - ) - self.wait_write("hostname %s" % (self.hostname)) - - # configure management vrf - self.wait_write("vrf context management") - self.wait_write(f"ip route 0.0.0.0/0 {self.mgmt_gw_ipv4}") - self.wait_write(f"ipv6 route ::/0 {self.mgmt_gw_ipv6}") - self.wait_write("exit") - - # configure mgmt interface - self.wait_write("interface mgmt0") - self.wait_write(f"ip address {self.mgmt_address_ipv4}") - self.wait_write(f"ipv6 address {self.mgmt_address_ipv6}") - self.wait_write("exit") + def apply_config(self): - # configure longer ssh keys - self.wait_write("no feature ssh") - self.wait_write("ssh key rsa 2048 force") - self.wait_write("feature ssh") + scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) + self.logger.info(f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)") - self.wait_write("exit") - self.wait_write("copy running-config startup-config") - - def startup_config(self): - """Load additional config provided by user.""" - - if not os.path.exists(STARTUP_CONFIG_FILE): - self.logger.trace(f"Startup config file {STARTUP_CONFIG_FILE} is not found") - return - - self.logger.trace(f"Startup config file {STARTUP_CONFIG_FILE} exists") - with open(STARTUP_CONFIG_FILE) as file: - config_lines = file.readlines() - config_lines = [line.rstrip() for line in config_lines] - self.logger.trace(f"Parsed startup config file {STARTUP_CONFIG_FILE}") - - self.logger.info(f"Writing lines from {STARTUP_CONFIG_FILE}") - - self.wait_write("configure terminal") - # Apply lines from file - for line in config_lines: - self.wait_write(line) - # End and Save - self.wait_write("end") - self.wait_write("copy running-config startup-config") + # init scrapli + nxos_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, + } + + nxos_config = f"""hostname {self.hostname} +username {self.username} password 0 {self.password} role network-admin +! +vrf context management +ip route 0.0.0.0/0 {self.mgmt_gw_ipv4} +ipv6 route ::/0 {self.mgmt_gw_ipv6} +exit +! +interface mgmt0 +ip address {self.mgmt_address_ipv4} +ipv6 address {self.mgmt_address_ipv6} +exit +! +no feature ssh +ssh key rsa 2048 force +feature ssh +! +""" + + con = NXOSDriver(**nxos_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: + nxos_config += config.read() + else: + self.logger.warning(f"User provided startup configuration is not found.") + + res = con.send_configs(nxos_config.splitlines()) + con.send_config("copy running-config startup-config", eager=True) + + for response in res: + self.logger.info(f"CONFIG:{response.channel_input}") + self.logger.info(f"RESULT:{response.result}") class NXOS(vrnetlab.VR): From 17df8f54d37159ff56a921a331b86fec62588a3d Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Thu, 19 Dec 2024 22:18:45 +1300 Subject: [PATCH 16/50] vios: Migrate to Scrapli - Use IOSXEDriver for bootstrap and startup configs --- vios/docker/Dockerfile | 16 +--- vios/docker/launch.py | 166 +++++++++++++++++++++-------------------- 2 files changed, 86 insertions(+), 96 deletions(-) diff --git a/vios/docker/Dockerfile b/vios/docker/Dockerfile index d9e8aada..6427f83f 100644 --- a/vios/docker/Dockerfile +++ b/vios/docker/Dockerfile @@ -1,18 +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 \ - && rm -rf /var/lib/apt/lists/* +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 ARG IMAGE COPY $IMAGE* / diff --git a/vios/docker/launch.py b/vios/docker/launch.py index be55ced2..23f5ab0b 100755 --- a/vios/docker/launch.py +++ b/vios/docker/launch.py @@ -7,9 +7,10 @@ import sys import vrnetlab +from scrapli.driver.core import IOSXEDriver STARTUP_CONFIG_FILE = "/config/startup-config.cfg" - +DEFAULT_SCRAPLI_TIMEOUT = 900 def handle_SIGCHLD(_signal, _frame): os.waitpid(-1, os.WNOHANG) @@ -52,6 +53,7 @@ def __init__(self, hostname: str, username: str, password: str, conn_mode: str): smp="1", ram=512, driveif="virtio", + use_scrapli=True ) self.hostname = hostname @@ -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"Router>", ], - 1, ) if match: @@ -86,106 +87,109 @@ 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() + self.scrapli_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") + def apply_config(self): + + scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) + self.logger.info(f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)") - # 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") + # 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, + } v4_mgmt_address = vrnetlab.cidr_to_ddn(self.mgmt_address_ipv4) - - self.wait_write("interface GigabitEthernet0/0") - 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") + + 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 +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) - 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 - - 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) - - def _save_config(self): - self.logger.info("Saving configuration") - - self.wait_write("end") - self.wait_write("write memory") + 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(f"User provided startup configuration is not found.") + + res = con.send_configs(vios_config.splitlines()) + + for response in res: + self.logger.info(f"CONFIG:{response.channel_input}") + self.logger.info(f"RESULT:{response.result}") class VIOS(vrnetlab.VR): From a7e076cebfcec4e0d5e39c38db083e6eaf3fb2ea Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Thu, 19 Dec 2024 22:34:16 +1300 Subject: [PATCH 17/50] vrnetlab: Support scrapli qemu monitor option for VM reset --- common/vrnetlab.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/common/vrnetlab.py b/common/vrnetlab.py index 63ddd0d5..95e2d7f6 100644 --- a/common/vrnetlab.py +++ b/common/vrnetlab.py @@ -1019,14 +1019,17 @@ def start(self): for vm in self.vms: if (str(vm.num) in vm_num_list) or not fcontent: try: - vm.qm.write("system_reset\r".encode()) + if vm.use_scrapli: + vm.scrapli_qm.channel.write("system_reset\r") + else: + vm.qm.write("system_reset\r".encode()) self.logger.debug(f"Sent qemu-monitor system_reset to VM num {vm.num} ") except Exception as e: - self.logger.debug(f"Failed to send qemu-monitor system_reset to VM num {vm.num} ({e})") + self.logger.error(f"Failed to send qemu-monitor system_reset to VM num {vm.num} ({e})") try: os.remove('/reset') except Exception as e: - self.logger.debug(f"Failed to cleanup /reset file({e}). qemu-monitor system_reset will likely be triggered again on VMs") + self.logger.error(f"Failed to cleanup /reset file({e}). qemu-monitor system_reset will likely be triggered again on VMs") class QemuBroken(Exception): """Our Qemu instance is somehow broken""" From b246f4f5c84538935d27882af6c84546805a84e1 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Fri, 20 Dec 2024 00:10:08 +1300 Subject: [PATCH 18/50] vrnetlab: Move logging colour config outside of class init method --- common/vrnetlab.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/common/vrnetlab.py b/common/vrnetlab.py index 95e2d7f6..91190ab8 100644 --- a/common/vrnetlab.py +++ b/common/vrnetlab.py @@ -22,6 +22,13 @@ MAX_RETRIES = 60 +# set fancy logging colours +logging.addLevelName( logging.INFO, f"\x1B[1;32m\t{logging.getLevelName(logging.INFO)}\x1B[0m") +logging.addLevelName( logging.WARN, f"\x1B[1;38;5;220m\t{logging.getLevelName(logging.WARN)}\x1B[0m") +logging.addLevelName( logging.DEBUG, f"\x1B[1;94m\t{logging.getLevelName(logging.DEBUG)}\x1B[0m") +logging.addLevelName( logging.ERROR, f"\x1B[1;91m\t{logging.getLevelName(logging.ERROR)}\x1B[0m") +logging.addLevelName( logging.CRITICAL, f"\x1B[1;91m\t{logging.getLevelName(logging.CRITICAL)}\x1B[0m") + def gen_mac(last_octet=None): """Generate a random MAC address that is in recognizable (0C:00) OUI space @@ -95,13 +102,6 @@ def __init__( # configure logging self.logger = logging.getLogger() - # set fancy logging colours - logging.addLevelName( logging.INFO, f"\x1B[1;32m\t{logging.getLevelName(logging.INFO)}\x1B[0m") - logging.addLevelName( logging.WARN, f"\x1B[1;38;5;220m\t{logging.getLevelName(logging.WARN)}\x1B[0m") - logging.addLevelName( logging.DEBUG, f"\x1B[1;94m\t{logging.getLevelName(logging.DEBUG)}\x1B[0m") - logging.addLevelName( logging.ERROR, f"\x1B[1;91m\t{logging.getLevelName(logging.ERROR)}\x1B[0m") - logging.addLevelName( logging.CRITICAL, f"\x1B[1;91m\t{logging.getLevelName(logging.CRITICAL)}\x1B[0m") - """ Configure Scrapli logger to only be INFO level. Scrapli uses 'scrapli' logger by default, and From be0db66e67d9cd4986d4872855301976cf415983 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Fri, 20 Dec 2024 08:17:05 +1300 Subject: [PATCH 19/50] cat8kv: Fix logger warning (log.warning -> logger.warning) --- c8000v/docker/launch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/c8000v/docker/launch.py b/c8000v/docker/launch.py index c2056048..be670dae 100755 --- a/c8000v/docker/launch.py +++ b/c8000v/docker/launch.py @@ -134,7 +134,7 @@ def bootstrap_spin(self): self.running = True return else: - self.log.warning("Unexpected reload while running") + self.logger.warning("Unexpected reload while running") # no match, if we saw some output from the router it's probably # booting, so let's give it some more time From 1a725d52923569d64b89ca063a6c743e0775c65a Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Fri, 20 Dec 2024 08:20:02 +1300 Subject: [PATCH 20/50] vrnetlab: Remove scrpali logging import --- common/vrnetlab.py | 1 - 1 file changed, 1 deletion(-) diff --git a/common/vrnetlab.py b/common/vrnetlab.py index 91190ab8..f60b4488 100644 --- a/common/vrnetlab.py +++ b/common/vrnetlab.py @@ -16,7 +16,6 @@ try: from scrapli import Driver - from scrapli.logging import enable_basic_logging except ImportError: pass From 1f37955b690cf942d2be9f10a73dccc50d4b681b Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Fri, 20 Dec 2024 21:21:41 +1300 Subject: [PATCH 21/50] Cisco devices: Add/tweak configuration saving: - vios, csr, cat8kv, cat9kv -- add configuration saving - XRv, XRv9k -- log configuration saving --- c8000v/docker/launch.py | 1 + cat9kv/docker/launch.py | 1 + csr/docker/launch.py | 1 + vios/docker/launch.py | 1 + xrv/docker/launch.py | 2 +- xrv9k/docker/launch.py | 2 +- 6 files changed, 6 insertions(+), 2 deletions(-) diff --git a/c8000v/docker/launch.py b/c8000v/docker/launch.py index be670dae..f969fd7b 100755 --- a/c8000v/docker/launch.py +++ b/c8000v/docker/launch.py @@ -220,6 +220,7 @@ def apply_config(self): self.logger.warning(f"User provided startup configuration is not found.") res = con.send_configs(cat8kv_config.splitlines()) + res += con.send_commands(["write memory"], eager=True) for response in res: self.logger.info(f"CONFIG:{response.channel_input}") diff --git a/cat9kv/docker/launch.py b/cat9kv/docker/launch.py index e5da270c..2ee58eed 100755 --- a/cat9kv/docker/launch.py +++ b/cat9kv/docker/launch.py @@ -205,6 +205,7 @@ def apply_startup_config(self): con.commandeer(conn=self.scrapli_tn) res = con.send_configs_from_file(STARTUP_CONFIG_FILE) + res += con.send_commands(["write memory"], eager=True) for response in res: self.logger.info(f"CONFIG:{response.channel_input}") diff --git a/csr/docker/launch.py b/csr/docker/launch.py index 68a208b8..af0fdc83 100755 --- a/csr/docker/launch.py +++ b/csr/docker/launch.py @@ -215,6 +215,7 @@ def apply_config(self): self.logger.warning(f"User provided startup configuration is not found.") res = con.send_configs(csr_config.splitlines()) + res += con.send_commands(["write memory"], eager=True) for response in res: self.logger.info(f"CONFIG:{response.channel_input}") diff --git a/vios/docker/launch.py b/vios/docker/launch.py index 23f5ab0b..66f2f00d 100755 --- a/vios/docker/launch.py +++ b/vios/docker/launch.py @@ -186,6 +186,7 @@ def apply_config(self): self.logger.warning(f"User provided startup configuration is not found.") res = con.send_configs(vios_config.splitlines()) + res += con.send_commands(["write memory"], eager=True) for response in res: self.logger.info(f"CONFIG:{response.channel_input}") diff --git a/xrv/docker/launch.py b/xrv/docker/launch.py index 62ccbb31..49266a63 100755 --- a/xrv/docker/launch.py +++ b/xrv/docker/launch.py @@ -198,7 +198,7 @@ def apply_config(self): ) res = con.send_configs(xrv_config.splitlines()) - con.send_configs(["commit best-effort label CLAB_BOOTSTRAP", "end"], eager=True) + res += con.send_configs(["commit best-effort label CLAB_BOOTSTRAP", "end"], eager=True) for response in res: self.logger.info(f"CONFIG:{response.channel_input}") diff --git a/xrv9k/docker/launch.py b/xrv9k/docker/launch.py index b37e1de3..bada97ea 100755 --- a/xrv9k/docker/launch.py +++ b/xrv9k/docker/launch.py @@ -218,7 +218,7 @@ def apply_config(self): with IOSXRDriver(**xrv9k_scrapli_dev) as con: res = con.send_configs(xrv9k_config.splitlines()) - con.send_configs(["commit best-effort label CLAB_BOOTSTRAP", "end"], eager=True) + res += con.send_configs(["commit best-effort label CLAB_BOOTSTRAP", "end"], eager=True) for response in res: self.logger.info(f"CONFIG:{response.channel_input}") From 4915f934a2d27e7fc3bf0bf6b17753280bc8e3ef Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Fri, 20 Dec 2024 21:23:50 +1300 Subject: [PATCH 22/50] xrv, xrv9k: Return to root at end of bootstrap cfg --- xrv/docker/launch.py | 2 +- xrv9k/docker/launch.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/xrv/docker/launch.py b/xrv/docker/launch.py index 49266a63..3ad1fd14 100755 --- a/xrv/docker/launch.py +++ b/xrv/docker/launch.py @@ -176,7 +176,7 @@ def apply_config(self): grpc no-tls ! xml agent tty -! +root """ con = IOSXRDriver(**xrv_scrapli_dev) diff --git a/xrv9k/docker/launch.py b/xrv9k/docker/launch.py index bada97ea..5602d6c8 100755 --- a/xrv9k/docker/launch.py +++ b/xrv9k/docker/launch.py @@ -204,7 +204,7 @@ def apply_config(self): grpc no-tls ! xml agent tty -! +root """ if os.path.exists(STARTUP_CONFIG_FILE): From b4a99f55942f282ff1acb046e25bcc6ae89d54d6 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Sat, 21 Dec 2024 08:59:00 +1300 Subject: [PATCH 23/50] vrnetlab: add bool formatter func --- common/vrnetlab.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/common/vrnetlab.py b/common/vrnetlab.py index f60b4488..7f9e3c0a 100644 --- a/common/vrnetlab.py +++ b/common/vrnetlab.py @@ -281,8 +281,8 @@ def start(self): self.logger.info(f"Launching {self.__class__.__name__} with {self.smp} SMP/VCPU and {self.ram} M of RAM") # give nice colours. Red if disabled, Green if enabled - mgmt_passthrough_coloured = f"\x1B[32mEnabled\x1B[0m" if self.mgmt_passthrough else f"\x1B[31mDisabled\x1B[0m" - use_scrapli_coloured = f"\x1B[32mEnabled\x1B[0m" if self.use_scrapli else f"\x1B[31mDisabled\x1B[0m" + mgmt_passthrough_coloured = format_bool_color(self.mgmt_passthrough, "Enabled", "Disabled") + use_scrapli_coloured = format_bool_color(self.use_scrapli, "Enabled", "Disabled") self.logger.info(f"Scrapli: {use_scrapli_coloured}") self.logger.info(f"Transparent mgmt interface: {mgmt_passthrough_coloured}") @@ -1056,3 +1056,14 @@ def cidr_to_ddn(prefix: str) -> list[str]: network = ipaddress.IPv4Interface(prefix) return [str(network.ip), str(network.netmask)] + +def format_bool_color(bool_var: bool, text_if_true: str, text_if_false: str) -> str: + """ + Generate a ANSI escape code colored string based on a boolean. + + Args: + bool_var: Boolean to be evaluated + text_if_true: Text returned if bool_var is true -- ANSI Formatted in green color + text_if_false: Text returned if bool_var is false -- ANSI Formatted in red color + """ + return f"\x1B[32m{text_if_true}\x1B[0m" if bool_var else f"\x1B[31m{text_if_false}\x1B[0m" \ No newline at end of file From 71de18bd23a509ec44f3a098ee40d71d081c3ee1 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Sat, 21 Dec 2024 09:12:15 +1300 Subject: [PATCH 24/50] sros: Migrate to Scrapli - Use scrapli community 'nokia_sros' platform - Remove wait_write clean_buffer override - Check if tftpboot conifg exists *before* opening Scrapli connection - Log command outputs with 'DEBUG_SCRAPLI' env var (defaults to false) --- sros/docker/Dockerfile | 22 +----- sros/docker/launch.py | 147 ++++++++++++++++++++++++++--------------- 2 files changed, 93 insertions(+), 76 deletions(-) diff --git a/sros/docker/Dockerfile b/sros/docker/Dockerfile index c18f8741..8c6a929e 100644 --- a/sros/docker/Dockerfile +++ b/sros/docker/Dockerfile @@ -1,24 +1,4 @@ -FROM public.ecr.aws/docker/library/debian:bookworm-slim -LABEL org.opencontainers.image.authors="roman@dodin.dev" - -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 \ - tftpd-hpa \ - ssh \ - inetutils-ping \ - dnsutils \ - iptables \ - nftables \ - telnet \ - && rm -rf /var/lib/apt/lists/* +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 ARG IMAGE COPY $IMAGE* / diff --git a/sros/docker/launch.py b/sros/docker/launch.py index 13a5d702..f814bc4d 100755 --- a/sros/docker/launch.py +++ b/sros/docker/launch.py @@ -11,7 +11,10 @@ from typing import Dict import vrnetlab +from scrapli import Scrapli +DEFAULT_SCRAPLI_TIMEOUT = 900 +DEBUG_SCRAPLI = True if os.getenv("DEBUG_SCRAPLI", "false").lower() == "true" else False def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -911,6 +914,7 @@ def __init__(self, username, password, ram, conn_mode, cpu=2, num=0): ram=ram, driveif="virtio", smp=f"{cpu}", + use_scrapli=True ) self.nic_type = "virtio-net-pci" @@ -994,12 +998,12 @@ def attach_cf(self, slot, cfname, size): path = f"/tftpboot/{cfname}_{slot}.qcow2" if not os.path.exists(path): - logger.debug( + self.logger.debug( f"Slot {slot}: creating {cfname} disk with size {size} -> {path}" ) vrnetlab.run_command(["qemu-img", "create", "-f", "qcow2", path, size]) else: - logger.debug( + self.logger.debug( f"Slot {slot}: bypassed creation of {cfname} disk because it already exist -> {path}. " ) @@ -1009,14 +1013,10 @@ def attach_cf(self, slot, cfname, size): self.qemu_args.extend(["-drive", f"if=virtio,index={disk_idx},file={path}"]) - # override wait_write clean_buffer parameter default - def wait_write(self, cmd, wait="__defaultpattern__", con=None, clean_buffer=True): - super().wait_write(cmd, wait, con, clean_buffer) - def bootstrap_spin(self): """This function should be called periodically to do work.""" - (ridx, match, res) = self.tn.expect([b"Login:", b"^[^ ]+#"], 1) + (ridx, match, res) = self.con_expect([b"Login:", b"^[^ ]+#"]) if match: # got a match! if ridx == 0: # matched login prompt, so should login self.logger.debug("matched login prompt") @@ -1025,7 +1025,7 @@ def bootstrap_spin(self): # run main config! self.bootstrap_config() # close telnet connection - self.tn.close() + self.scrapli_tn.close() # calc startup time startup_time = datetime.datetime.now() - self.start_time self.logger.info("Startup complete in: %s" % startup_time) @@ -1035,7 +1035,7 @@ def bootstrap_spin(self): # 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("OUTPUT: %s" % res.decode()) + self.write_to_stdout(res) # reset spins if we saw some output self.spins = 0 @@ -1094,74 +1094,81 @@ def configure_power(self, power_cfg): power_path = "system" for s in range(1, shelves + 1): - self.wait_write( - f"/configure {power_path} power-shelf {s} power-shelf-type {power_shelf_type}" - ) + res = self.sros_con.send_configs([f"/configure {power_path} power-shelf {s} power-shelf-type {power_shelf_type}"], strip_prompt=False) + self.log_scrapli_cmd_res(res) for m in range(1, modules + 1): - self.wait_write( - f"/configure {power_path} power-shelf {s} power-module {m} power-module-type {module_type}" - ) + res = self.sros_con.send_configs([f"/configure {power_path} power-shelf {s} power-module {m} power-module-type {module_type}"], strip_prompt=False) + self.log_scrapli_cmd_res(res) def enterConfig(self): """Enter configuration mode. No-op for SR OS version <= 22""" if SROS_VERSION.major <= 22 or SROS_VERSION.magc: return - self.wait_write("edit-config exclusive") + self.sros_con.acquire_priv("configuration") def enterBofConfig(self): """Enter bof configuration mode. No-op for SR OS version <= 22""" if SROS_VERSION.major <= 22 or SROS_VERSION.magc: return - self.wait_write("edit-config bof exclusive") + res = self.sros_con.send_commands(["edit-config bof exclusive"], strip_prompt=False) + self.log_scrapli_cmd_res(res) def commitConfig(self): """Commit configuration. No-op for SR OS version <= 22""" if SROS_VERSION.major <= 22 or SROS_VERSION.magc: return - self.wait_write("commit") - self.wait_write("/") - self.wait_write("quit-config") + res = self.sros_con.send_configs([ + "commit", + "/" + ], strip_prompt=False) + self.log_scrapli_cmd_res(res) def commitBofConfig(self): """Commit configuration. No-op for SR OS version <= 22""" if SROS_VERSION.major <= 22 or SROS_VERSION.magc: return - self.wait_write("commit") - self.wait_write("/") - self.wait_write("quit-config") + res = self.sros_con.send_configs([ + "commit", + "/" + ], strip_prompt=False) + self.log_scrapli_cmd_res(res) def configureCards(self): """Configure cards""" # integrated vsims have `card_config` in the variant definition if "card_config" in self.variant: - for line in iter(self.variant["card_config"].splitlines()): - self.wait_write(line) + res = self.sros_con.send_configs(self.variant["card_config"].splitlines(), strip_prompt=False) + self.log_scrapli_cmd_res(res) # else this might be a distributed chassis elif self.variant.get("lcs") is not None: for lc in self.variant["lcs"]: if "card_config" in lc: - for line in iter(lc["card_config"].splitlines()): - self.wait_write(line) + res = self.sros_con.send_configs(lc["card_config"].splitlines(), strip_prompt=False) + self.log_scrapli_cmd_res(res) def persistBofAndConfig(self): """ "Persist bof and config""" if SROS_VERSION.magc: - self.wait_write("/bof save cf3:") - self.wait_write("/admin save") + cmds = ["/bof save cf3:"] elif SROS_VERSION.major <= 22: - self.wait_write("/bof save") - self.wait_write("/admin save") + cmds = ["/bof save"] else: - self.wait_write("/admin save bof") - self.wait_write("/admin save") + cmds = ["/admin save bof"] + + cmds.append("/admin save") + res = self.sros_con.send_commands(cmds, strip_prompt=False) + self.log_scrapli_cmd_res(res) def switchConfigEngine(self): """Switch configuration engine""" if SROS_VERSION.major <= 22 or SROS_VERSION.magc: # for SR OS version <= 22, we enforce MD-CLI by switching to it - self.wait_write( - f"/configure system management-interface configuration-mode {self.mode}" + res = self.sros_con.send_confgs( + [ + f"/configure system management-interface configuration-mode {self.mode}" + ], strip_prompt=False ) + self.log_scrapli_cmd_res(res) def gen_bof_config(self): """generate bof configuration commands based on env vars and SR OS version""" @@ -1217,6 +1224,13 @@ def gen_bof_config(self): # if "docker-net-v6-addr" in m: # cmds.append(f"/bof static-route {m[docker-net-v6-addr]} next-hop {BRIDGE_ADDR}") return cmds + + def log_scrapli_cmd_res(self, res: list): + if not DEBUG_SCRAPLI: + return + for response in res: + self.logger.debug(f"CHANNEL INPUT: {response.channel_input}") + self.logger.debug(f"OUTPUT:\n{response.result}") def bootstrap_config(self): """Common function used to push initial configuration for bof and config to @@ -1225,24 +1239,54 @@ def bootstrap_config(self): # configure bof before we check if config file was provided # since bof statements are not part of the config file # thus it must be applied unconditionally + + # init scrapli sros driver + scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) + self.logger.info(f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)") + + # check if config was provided + config_exists = os.path.isfile("/tftpboot/config.txt") + fmt_config_exists = vrnetlab.format_bool_color(config_exists, "exists", "does not exist") + self.logger.debug(f"Configuration file {fmt_config_exists}") + + # init scrapli + sros_scrapli_dev = { + "platform": "nokia_sros", + "host": "127.0.0.1", + "auth_bypass": True, + "auth_strict_key": False, + "timeout_socket": scrapli_timeout, + "timeout_transport": scrapli_timeout, + "timeout_ops": scrapli_timeout, + } + + if SROS_VERSION.major <= 22 or SROS_VERSION.magc: + sros_scrapli_dev["variant"] = "cassic" + + # self.scrapli_logger.setLevel(logging.DEBUG) + self.sros_con = Scrapli(**sros_scrapli_dev) + self.sros_con.commandeer(conn=self.scrapli_tn) + + # configure BOF self.enterBofConfig() - for line in iter(self.gen_bof_config()): - self.wait_write(line) + + # send and log BOF config + res = self.sros_con.send_configs(self.gen_bof_config(), strip_prompt=False) + self.log_scrapli_cmd_res(res) + self.commitBofConfig() # save bof config on disk self.persistBofAndConfig() # apply common configuration if config file was not provided - if not os.path.isfile("/tftpboot/config.txt"): - self.logger.info("Applying basic SR OS configuration...") + if not config_exists: + self.logger.debug("Applying basic SR OS configuration...") # enter config mode, no-op for sros <=22 self.enterConfig() - - for line in iter( - getDefaultConfig().format(name=self.hostname).splitlines() - ): - self.wait_write(line) + + res = self.sros_con.send_configs(getDefaultConfig().format(name=self.hostname).splitlines(), strip_prompt=False) + self.log_scrapli_cmd_res(res) # configure card/mda of a given variant self.configureCards() @@ -1254,9 +1298,7 @@ def bootstrap_config(self): self.commitConfig() self.switchConfigEngine() - - # logout at the end of execution - self.wait_write("/logout") + @property def ram(self): @@ -1359,7 +1401,7 @@ def gen_mgmt(self): "chassis=ixr-e2c", ] ): - logger.debug( + self.logger.debug( "detected ixr-r6/ixr-ec/ixr-e2/ixr-e2c chassis, creating a dummy network device for SFM connection" ) res.append(f"-device virtio-net-pci,netdev=dummy,mac={vrnetlab.gen_mac(0)}") @@ -1526,7 +1568,7 @@ def gen_mgmt(self): def bootstrap_spin(self): """We have nothing to do for VSR-SIM line cards""" self.running = True - self.tn.close() + self.scrapli_tn.close() return @@ -1785,7 +1827,6 @@ def getDefaultConfig() -> str: mgmt_passthrough = False if os.getenv("CLAB_MGMT_PASSTHROUGH", "").lower() == "true": mgmt_passthrough = True - logger.debug("Management passthrough mode is ON") # In host-forwarded mode the container runs a tftp server in the root namespace of the container. if not mgmt_passthrough: @@ -1894,11 +1935,6 @@ def getDefaultConfig() -> str: "/tftpboot", ] ) - logger.debug( - f"acting flags: username '{args.username}', password '{args.password}', connection-mode '{args.connection_mode}', variant '{args.variant}'" - ) - - logger.debug(f"Environment variables: {os.environ}") vrnetlab.boot_delay() @@ -1911,4 +1947,5 @@ def getDefaultConfig() -> str: conn_mode=args.connection_mode, mgmt_passthrough=mgmt_passthrough, ) + ia.logger.debug(f"acting flags: username '{args.username}', password '{args.password}', connection-mode '{args.connection_mode}', variant '{args.variant}'") ia.start() From 481244268618c86276ddbc1260c145722ca12571 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Sat, 21 Dec 2024 10:48:17 +1300 Subject: [PATCH 25/50] Use kaelemc/scrapli_community in base image --- vrnetlab-base.dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/vrnetlab-base.dockerfile b/vrnetlab-base.dockerfile index 7b56e24a..121c6344 100644 --- a/vrnetlab-base.dockerfile +++ b/vrnetlab-base.dockerfile @@ -20,9 +20,10 @@ RUN apt-get update -qy \ telnet \ python3-pip \ python3-passlib \ + git \ dosfstools \ genisoimage \ && rm -rf /var/lib/apt/lists/* -RUN pip install https://github.com/carlmontanari/scrapli/archive/refs/tags/2024.07.30.post1.zip \ - https://github.com/scrapli/scrapli_community/archive/refs/tags/2024.07.30.zip --break-system-packages +RUN pip install https://github.com/carlmontanari/scrapli/archive/refs/tags/2024.07.30.post1.zip --break-system-packages +RUN pip install -e git+https://github.com/kaelemc/scrapli_community.git@sros_regex_fix#egg=scrapli_community --break-system-packages \ No newline at end of file From 3b83a7e790469fa6d1e40a7319d2d707da78be1c Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Sun, 22 Dec 2024 06:02:20 +1300 Subject: [PATCH 26/50] Disable eager mode for config saving on Cisco devices --- c8000v/docker/launch.py | 2 +- cat9kv/docker/launch.py | 2 +- csr/docker/launch.py | 2 +- n9kv/docker/launch.py | 2 +- nxos/docker/launch.py | 2 +- vios/docker/launch.py | 2 +- xrv/docker/launch.py | 2 +- xrv9k/docker/launch.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/c8000v/docker/launch.py b/c8000v/docker/launch.py index f969fd7b..42981e4d 100755 --- a/c8000v/docker/launch.py +++ b/c8000v/docker/launch.py @@ -220,7 +220,7 @@ def apply_config(self): self.logger.warning(f"User provided startup configuration is not found.") res = con.send_configs(cat8kv_config.splitlines()) - res += con.send_commands(["write memory"], eager=True) + res += con.send_commands(["write memory"]) for response in res: self.logger.info(f"CONFIG:{response.channel_input}") diff --git a/cat9kv/docker/launch.py b/cat9kv/docker/launch.py index 2ee58eed..0aa693c7 100755 --- a/cat9kv/docker/launch.py +++ b/cat9kv/docker/launch.py @@ -205,7 +205,7 @@ def apply_startup_config(self): con.commandeer(conn=self.scrapli_tn) res = con.send_configs_from_file(STARTUP_CONFIG_FILE) - res += con.send_commands(["write memory"], eager=True) + res += con.send_commands(["write memory"]) for response in res: self.logger.info(f"CONFIG:{response.channel_input}") diff --git a/csr/docker/launch.py b/csr/docker/launch.py index af0fdc83..29feaf1f 100755 --- a/csr/docker/launch.py +++ b/csr/docker/launch.py @@ -215,7 +215,7 @@ def apply_config(self): self.logger.warning(f"User provided startup configuration is not found.") res = con.send_configs(csr_config.splitlines()) - res += con.send_commands(["write memory"], eager=True) + res += con.send_commands(["write memory"]) for response in res: self.logger.info(f"CONFIG:{response.channel_input}") diff --git a/n9kv/docker/launch.py b/n9kv/docker/launch.py index 584f5af3..f51142f5 100755 --- a/n9kv/docker/launch.py +++ b/n9kv/docker/launch.py @@ -180,7 +180,7 @@ def apply_config(self): self.logger.warning(f"User provided startup configuration is not found.") res = con.send_configs(n9kv_config.splitlines()) - con.send_config("copy running-config startup-config", eager=True) + con.send_config("copy running-config startup-config") for response in res: self.logger.info(f"CONFIG:{response.channel_input}") diff --git a/nxos/docker/launch.py b/nxos/docker/launch.py index 54f56e35..ee7dfcd5 100755 --- a/nxos/docker/launch.py +++ b/nxos/docker/launch.py @@ -142,7 +142,7 @@ def apply_config(self): self.logger.warning(f"User provided startup configuration is not found.") res = con.send_configs(nxos_config.splitlines()) - con.send_config("copy running-config startup-config", eager=True) + con.send_config("copy running-config startup-config") for response in res: self.logger.info(f"CONFIG:{response.channel_input}") diff --git a/vios/docker/launch.py b/vios/docker/launch.py index 66f2f00d..8a3eaac1 100755 --- a/vios/docker/launch.py +++ b/vios/docker/launch.py @@ -186,7 +186,7 @@ def apply_config(self): self.logger.warning(f"User provided startup configuration is not found.") res = con.send_configs(vios_config.splitlines()) - res += con.send_commands(["write memory"], eager=True) + res += con.send_commands(["write memory"]) for response in res: self.logger.info(f"CONFIG:{response.channel_input}") diff --git a/xrv/docker/launch.py b/xrv/docker/launch.py index 3ad1fd14..955b6012 100755 --- a/xrv/docker/launch.py +++ b/xrv/docker/launch.py @@ -198,7 +198,7 @@ def apply_config(self): ) res = con.send_configs(xrv_config.splitlines()) - res += con.send_configs(["commit best-effort label CLAB_BOOTSTRAP", "end"], eager=True) + res += con.send_configs(["commit best-effort label CLAB_BOOTSTRAP", "end"]) for response in res: self.logger.info(f"CONFIG:{response.channel_input}") diff --git a/xrv9k/docker/launch.py b/xrv9k/docker/launch.py index 5602d6c8..50a8096e 100755 --- a/xrv9k/docker/launch.py +++ b/xrv9k/docker/launch.py @@ -218,7 +218,7 @@ def apply_config(self): with IOSXRDriver(**xrv9k_scrapli_dev) as con: res = con.send_configs(xrv9k_config.splitlines()) - res += con.send_configs(["commit best-effort label CLAB_BOOTSTRAP", "end"], eager=True) + res += con.send_configs(["commit best-effort label CLAB_BOOTSTRAP", "end"]) for response in res: self.logger.info(f"CONFIG:{response.channel_input}") From 9340d3f7729d46ea524cecac1d15eab954114343 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Mon, 23 Dec 2024 16:11:44 +1300 Subject: [PATCH 27/50] cat8kv: Migrate to CVAC configuration --- c8000v/docker/launch.py | 211 +++++++++++++++++++--------------------- 1 file changed, 99 insertions(+), 112 deletions(-) diff --git a/c8000v/docker/launch.py b/c8000v/docker/launch.py index 42981e4d..8aeeee10 100755 --- a/c8000v/docker/launch.py +++ b/c8000v/docker/launch.py @@ -9,7 +9,6 @@ import sys import vrnetlab -from scrapli.driver.core import IOSXEDriver STARTUP_CONFIG_FILE = "/config/startup-config.cfg" DEFAULT_SCRAPLI_TIMEOUT = 900 @@ -59,112 +58,59 @@ def __init__(self, hostname, username, password, conn_mode, install_mode=False): self.conn_mode = conn_mode self.num_nics = 9 self.nic_type = "virtio-net-pci" + self.image_name = "config.iso" if self.install_mode: self.logger.debug("Install mode") - self.image_name = "config.iso" - self.create_boot_image() - - self.qemu_args.extend(["-cdrom", "/" + self.image_name]) - - def create_boot_image(self): - """Creates a iso image with a bootstrap configuration""" - - with open("/iosxe_config.txt", "w") as cfg_file: - if self.license: - cfg_file.write("do clock set 13:33:37 1 Jan 2010\r\n") - cfg_file.write("interface GigabitEthernet1\r\n") - cfg_file.write("ip address 10.0.0.15 255.255.255.0\r\n") - cfg_file.write("no shut\r\n") - cfg_file.write("exit\r\n") - cfg_file.write("license accept end user agreement\r\n") - cfg_file.write("yes\r\n") - cfg_file.write("do license install tftp://10.0.0.2/license.lic\r\n\r\n") - cfg_file.write("license boot level network-premier addon dna-premier\r\n") - cfg_file.write("platform console serial\r\n\r\n") - cfg_file.write("do clear platform software vnic-if nvtable\r\n") - cfg_file.write("do wr\r\n") - cfg_file.write("do reload\r\n") - - genisoimage_args = [ - "genisoimage", - "-l", - "-o", - "/" + self.image_name, - "/iosxe_config.txt", - ] - - subprocess.Popen(genisoimage_args) - - def bootstrap_spin(self): - """This function should be called periodically to do work.""" - - if self.spins > 300: - # too many spins with no result -> give up - self.stop() - self.start() - return - - (ridx, match, res) = self.con_expect( - [b"Press RETURN to get started!", b"IOSXEBOOT-4-FACTORY_RESET"] - ) - if match: # got a match! - if ridx == 0: # login - self.logger.info("matched, Press RETURN to get started.") - if self.install_mode: - self.logger.info("Now we wait for the device to reload") - else: - self.wait_write("", wait=None) - - self.apply_config() - - # close telnet connection - self.scrapli_tn.close() - - # startup time? - startup_time = datetime.datetime.now() - self.start_time - self.logger.info("Startup complete in: %s", startup_time) - self.running = True - - return - elif ridx == 1: # IOSXEBOOT-4-FACTORY_RESET - if self.install_mode: - install_time = datetime.datetime.now() - self.start_time - self.logger.info("Install complete in: %s", install_time) - self.running = True - return - else: - self.logger.warning("Unexpected reload while running") - - # 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.write_to_stdout(res) - # reset spins if we saw some output - self.spins = 0 - - self.spins += 1 - - return - - def apply_config(self): + self.create_config_image(self.gen_install_config()) + else: + cfg = self.gen_bootstrap_config() + if os.path.exists(STARTUP_CONFIG_FILE): + self.logger.info("Startup configuration file found") + with open (STARTUP_CONFIG_FILE, "r") as startup_config: + cfg += startup_config.read() + else: + self.logger.warning(f"User provided startup configuration is not found.") + self.create_config_image(cfg) + + self.qemu_args.extend(["-cdrom", "/" + self.image_name]) + + def gen_install_config(self) -> str: + """ + Returns the configuration to load in install mode + """ - scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) - self.logger.info(f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)") + config = "" - # init scrapli - cat8kv_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, - } + if self.license: + config += """do clock set 13:33:37 1 Jan 2010 +interface GigabitEthernet1 +ip address 10.0.0.15 255.255.255.0 +no shut +exit +license accept end user agreement +yes +do license install tftp://10.0.0.2/license.lic +""" + + config += """ +license boot level network-premier addon dna-premier +platform console serial +do clear platform software vnic-if nvtable +do wr +do reload +""" + + return config + + def gen_bootstrap_config(self) -> str: + """ + Returns the system bootstrap configuration + """ v4_mgmt_address = vrnetlab.cidr_to_ddn(self.mgmt_address_ipv4) - cat8kv_config = f"""hostname {self.hostname} + return f"""hostname {self.hostname} username {self.username} privilege 15 password {self.password} ip domain name example.com ! @@ -209,23 +155,64 @@ def apply_config(self): ! """ - con = IOSXEDriver(**cat8kv_scrapli_dev) - con.commandeer(conn=self.scrapli_tn) + def create_config_image(self, config): + """Creates a iso image with a installation configuration""" + + with open("/iosxe_config.txt", "w") as cfg: + cfg.write(config) + + genisoimage_args = [ + "genisoimage", + "-l", + "-o", + "/" + self.image_name, + "/iosxe_config.txt", + ] + + subprocess.Popen(genisoimage_args) + + def bootstrap_spin(self): + """This function should be called periodically to do work.""" + + if self.spins > 300: + # too many spins with no result -> give up + self.stop() + self.start() + return - if os.path.exists(STARTUP_CONFIG_FILE): - self.logger.info("Startup configuration file found") - with open(STARTUP_CONFIG_FILE, "r") as config: - cat8kv_config += config.read() - else: - self.logger.warning(f"User provided startup configuration is not found.") + (ridx, match, res) = self.con_expect( + [b"CVAC-4-CONFIG_DONE", b"IOSXEBOOT-4-FACTORY_RESET"] + ) + if match: # got a match! + if ridx == 0 and not self.install_mode: # configuration applied + self.logger.info("CVAC Configuration has been applied.") + # close telnet connection + self.scrapli_tn.close() + # startup time? + startup_time = datetime.datetime.now() - self.start_time + self.logger.info("Startup complete in: %s", startup_time) + # mark as running + self.running = True + return + elif ridx == 1: # IOSXEBOOT-4-FACTORY_RESET + if self.install_mode: + install_time = datetime.datetime.now() - self.start_time + self.logger.info("Install complete in: %s", install_time) + self.running = True + return + else: + self.logger.warning("Unexpected reload while running") - res = con.send_configs(cat8kv_config.splitlines()) - res += con.send_commands(["write memory"]) + # 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.write_to_stdout(res) + # reset spins if we saw some output + self.spins = 0 - for response in res: - self.logger.info(f"CONFIG:{response.channel_input}") - self.logger.info(f"RESULT:{response.result}") + self.spins += 1 + return class C8000v(vrnetlab.VR): def __init__(self, hostname, username, password, conn_mode): From c42eed26f66be4e5b1b3393b2c041a0aeae018f5 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Mon, 23 Dec 2024 16:56:29 +1300 Subject: [PATCH 28/50] cat8kv: add log message and block while generating cfg ISO --- c8000v/docker/launch.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/c8000v/docker/launch.py b/c8000v/docker/launch.py index 8aeeee10..d7352a01 100755 --- a/c8000v/docker/launch.py +++ b/c8000v/docker/launch.py @@ -168,8 +168,9 @@ def create_config_image(self, config): "/" + self.image_name, "/iosxe_config.txt", ] - - subprocess.Popen(genisoimage_args) + + self.logger.debug("Generating boot ISO") + subprocess.Popen(genisoimage_args).wait() def bootstrap_spin(self): """This function should be called periodically to do work.""" From acbf0ab7e994f5854ea5565d584f5de7549cd624 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Mon, 23 Dec 2024 16:56:41 +1300 Subject: [PATCH 29/50] cat9kv: Migrate startup config to CVAC --- cat9kv/docker/launch.py | 52 +++++++++------------------------------ csr/src/scrapli-community | 1 + 2 files changed, 12 insertions(+), 41 deletions(-) create mode 160000 csr/src/scrapli-community diff --git a/cat9kv/docker/launch.py b/cat9kv/docker/launch.py index 0aa693c7..e562efaa 100755 --- a/cat9kv/docker/launch.py +++ b/cat9kv/docker/launch.py @@ -9,7 +9,6 @@ import sys import vrnetlab -from scrapli.driver.core import IOSXEDriver STARTUP_CONFIG_FILE = "/config/startup-config.cfg" DEFAULT_SCRAPLI_TIMEOUT = 900 @@ -103,8 +102,6 @@ def create_boot_image(self): login local transport input all ! -ipv6 unicast-routing -! ip route vrf Mgmt-vrf 0.0.0.0 0.0.0.0 {self.mgmt_gw_ipv4} ipv6 route vrf Mgmt-vrf ::/0 {self.mgmt_gw_ipv6} ! @@ -124,6 +121,13 @@ def create_boot_image(self): ! """ + if os.path.exists(STARTUP_CONFIG_FILE): + self.logger.info("Startup configuration file found") + with open (STARTUP_CONFIG_FILE, "r") as startup_config: + cat9kv_config += startup_config.read() + else: + self.logger.warning(f"User provided startup configuration is not found.") + with open("/img_dir/iosxe_config.txt", "w") as cfg_file: cfg_file.write(cat9kv_config) @@ -136,7 +140,7 @@ def create_boot_image(self): ] self.logger.debug("Generating boot ISO") - subprocess.Popen(genisoimage_args) + subprocess.Popen(genisoimage_args).wait() def bootstrap_spin(self): """This function should be called periodically to do work.""" @@ -149,21 +153,13 @@ def bootstrap_spin(self): (ridx, match, res) = self.con_expect( [ - b"Press RETURN to get started!", + b"CVAC-4-CONFIG_DONE", b"IOSXEBOOT-4-FACTORY_RESET", ], ) if match: # got a match! - if ridx == 0: # login - self.logger.debug("matched, Press RETURN to get started.") - - if os.path.exists(STARTUP_CONFIG_FILE): - self.logger.info("Startup configuration file found. Applying startup config.") - self.wait_write("", wait=None) - self.apply_startup_config() - else: - self.logger.warning(f"User provided startup configuration is not found.") - + if ridx == 0: # configuration applied + self.logger.info("CVAC Configuration has been applied.") # close telnet connection self.scrapli_tn.close() # startup time? @@ -186,32 +182,6 @@ def bootstrap_spin(self): return - def apply_startup_config(self): - - scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) - self.logger.info(f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)") - - # init scrapli - cat9kv_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, - } - - con = IOSXEDriver(**cat9kv_scrapli_dev) - con.commandeer(conn=self.scrapli_tn) - - res = con.send_configs_from_file(STARTUP_CONFIG_FILE) - 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}") - - class cat9kv(vrnetlab.VR): def __init__(self, hostname, username, password, conn_mode, vcpu, ram): super(cat9kv, self).__init__(username, password) diff --git a/csr/src/scrapli-community b/csr/src/scrapli-community new file mode 160000 index 00000000..756e4d03 --- /dev/null +++ b/csr/src/scrapli-community @@ -0,0 +1 @@ +Subproject commit 756e4d0330de74940c6bd72d7c1202fe838c2399 From 157b8a057c2979368cc3914531fdbb580b017140 Mon Sep 17 00:00:00 2001 From: kaelemc <62122480+kaelemc@users.noreply.github.com.> Date: Wed, 25 Dec 2024 11:59:22 +1300 Subject: [PATCH 30/50] Remove erroneous Scrapli Community submodule --- csr/src/scrapli-community | 1 - 1 file changed, 1 deletion(-) delete mode 160000 csr/src/scrapli-community diff --git a/csr/src/scrapli-community b/csr/src/scrapli-community deleted file mode 160000 index 756e4d03..00000000 --- a/csr/src/scrapli-community +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 756e4d0330de74940c6bd72d7c1202fe838c2399 From 31263c56880d7d1878547b8a62c788aea2e81fff Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Sun, 5 Jan 2025 11:27:07 +1300 Subject: [PATCH 31/50] sros: fix typos for classic CLI --- sros/docker/launch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sros/docker/launch.py b/sros/docker/launch.py index f814bc4d..985a40ca 100755 --- a/sros/docker/launch.py +++ b/sros/docker/launch.py @@ -1163,7 +1163,7 @@ def switchConfigEngine(self): """Switch configuration engine""" if SROS_VERSION.major <= 22 or SROS_VERSION.magc: # for SR OS version <= 22, we enforce MD-CLI by switching to it - res = self.sros_con.send_confgs( + res = self.sros_con.send_configs( [ f"/configure system management-interface configuration-mode {self.mode}" ], strip_prompt=False @@ -1261,7 +1261,7 @@ def bootstrap_config(self): } if SROS_VERSION.major <= 22 or SROS_VERSION.magc: - sros_scrapli_dev["variant"] = "cassic" + sros_scrapli_dev["variant"] = "classic" # self.scrapli_logger.setLevel(logging.DEBUG) self.sros_con = Scrapli(**sros_scrapli_dev) From fdbe6bf9efc8bb3632c8752f7efd875b5190cc02 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Sun, 5 Jan 2025 13:49:13 +1300 Subject: [PATCH 32/50] csr: Migrate to CVAC --- csr/docker/launch.py | 212 +++++++++++++++++++++---------------------- 1 file changed, 101 insertions(+), 111 deletions(-) diff --git a/csr/docker/launch.py b/csr/docker/launch.py index 29feaf1f..2e74f36a 100755 --- a/csr/docker/launch.py +++ b/csr/docker/launch.py @@ -7,13 +7,11 @@ import signal import subprocess import sys -import time +from time import sleep import vrnetlab -from scrapli.driver.core import IOSXEDriver STARTUP_CONFIG_FILE = "/config/startup-config.cfg" -DEFAULT_SCRAPLI_TIMEOUT = 900 def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -63,108 +61,60 @@ def __init__( self.hostname = hostname self.conn_mode = conn_mode self.nic_type = "virtio-net-pci" + self.image_name = "config.iso" if self.install_mode: - self.logger.debug("install mode") - self.image_name = "config.iso" - self.create_boot_image() - - self.qemu_args.extend(["-cdrom", "/" + self.image_name]) - - def create_boot_image(self): - """Creates a iso image with a bootstrap configuration""" - - cfg_file = open("/iosxe_config.txt", "w") + self.logger.debug("Install mode") + self.create_config_image(self.gen_install_config()) + else: + cfg = self.gen_bootstrap_config() + if os.path.exists(STARTUP_CONFIG_FILE): + self.logger.info("Startup configuration file found") + with open (STARTUP_CONFIG_FILE, "r") as startup_config: + cfg += startup_config.read() + else: + self.logger.warning(f"User provided startup configuration is not found.") + self.create_config_image(cfg) + + self.qemu_args.extend(["-cdrom", "/" + self.image_name]) + + def gen_install_config(self) -> str: + """ + Returns the configuration to load in install mode + """ + + config = "" + if self.license: - cfg_file.write("do clock set 13:33:37 1 Jan 2010\r\n") - cfg_file.write("interface GigabitEthernet1\r\n") - cfg_file.write("ip address 10.0.0.15 255.255.255.0\r\n") - cfg_file.write("no shut\r\n") - cfg_file.write("exit\r\n") - cfg_file.write("license accept end user agreement\r\n") - cfg_file.write("yes\r\n") - cfg_file.write("do license install tftp://10.0.0.2/license.lic\r\n\r\n") - - cfg_file.write("platform console serial\r\n\r\n") - cfg_file.write("do clear platform software vnic-if nvtable\r\n\r\n") - cfg_file.write("do wr\r\n") - cfg_file.write("do reload\r\n") - cfg_file.close() - - genisoimage_args = [ - "genisoimage", - "-l", - "-o", - "/" + self.image_name, - "/iosxe_config.txt", - ] - - subprocess.Popen(genisoimage_args) - - def bootstrap_spin(self): - """This function should be called periodically to do work.""" - - if self.spins > 600: - # too many spins with no result -> give up - self.stop() - self.start() - return - - (ridx, match, res) = self.con_expect([b"Press RETURN to get started!"], 1) - if match: # got a match! - if ridx == 0: # login - if self.install_mode: - self.running = True - return - - self.logger.debug("matched, Press RETURN to get started.") - - self.wait_write("", wait=None) - - self.apply_config() - - # close the telnet connection - self.scrapli_tn.close() - - # startup time? - startup_time = datetime.datetime.now() - self.start_time - self.logger.info("Startup complete in: %s" % startup_time) - 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.write_to_stdout(res) - # reset spins if we saw some output - self.spins = 0 - - self.spins += 1 - - return + config += """do clock set 13:33:37 1 Jan 2010 +interface GigabitEthernet1 +ip address 10.0.0.15 255.255.255.0 +no shut +exit +license accept end user agreement +yes +do license install tftp://10.0.0.2/license.lic +""" + + config += """ +platform console serial +do clear platform software vnic-if nvtable +do wr +do reload +""" + return config - def apply_config(self): - - scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) - self.logger.info(f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)") - - # init scrapli - csr_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 gen_bootstrap_config(self) -> str: + """ + Returns the system bootstrap configuration + """ v4_mgmt_address = vrnetlab.cidr_to_ddn(self.mgmt_address_ipv4) ip_domain_name = "ip domain name example.com" if int(self.version.split('.')[0]) >= 16 else "ip domain-name example.com" - csr_config = f"""hostname {self.hostname} + return f"""hostname {self.hostname} username {self.username} privilege 15 password {self.password} {ip_domain_name} ! @@ -204,22 +154,63 @@ def apply_config(self): ! """ - con = IOSXEDriver(**csr_scrapli_dev) - con.commandeer(conn=self.scrapli_tn) + def create_config_image(self, config): + """Creates a iso image with a installation configuration""" + + with open("/iosxe_config.txt", "w") as cfg: + cfg.write(config) + + genisoimage_args = [ + "genisoimage", + "-l", + "-o", + "/" + self.image_name, + "/iosxe_config.txt", + ] - if os.path.exists(STARTUP_CONFIG_FILE): - self.logger.info("Startup configuration file found") - with open(STARTUP_CONFIG_FILE, "r") as config: - csr_config += config.read() - else: - self.logger.warning(f"User provided startup configuration is not found.") + self.logger.debug("Generating boot ISO") + subprocess.Popen(genisoimage_args).wait() - res = con.send_configs(csr_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}") + def bootstrap_spin(self): + """This function should be called periodically to do work.""" + + if self.spins > 600: + # too many spins with no result -> give up + self.stop() + self.start() + return + + (ridx, match, res) = self.con_expect( + [b"CVAC-4-CONFIG_DONE", b"Press RETURN to get started!"] + ) + if match: # got a match! + if ridx == 0 and not self.install_mode: # configuration applied + self.logger.info("CVAC Configuration has been applied.") + # close telnet connection + self.scrapli_tn.close() + # startup time? + startup_time = datetime.datetime.now() - self.start_time + self.logger.info("Startup complete in: %s", startup_time) + # mark as running + self.running = True + return + elif ridx == 1: # IOSXEBOOT-4-FACTORY_RESET + if self.install_mode: + install_time = datetime.datetime.now() - self.start_time + self.logger.info("Install complete in: %s", install_time) + 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.write_to_stdout(res) + # reset spins if we saw some output + self.spins = 0 + + self.spins += 1 + + return class CSR(vrnetlab.VR): @@ -253,11 +244,10 @@ def install(self): csr = self.vms[0] while not csr.running: csr.work() - time.sleep(30) + sleep(30) csr.stop() self.logger.info("Installation complete") - if __name__ == "__main__": import argparse From 532a2699b7f21d1e1b52d3408964ae54ab5783f8 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Tue, 7 Jan 2025 14:35:33 +1300 Subject: [PATCH 33/50] Switch back to scrapli/scrapli-community --- vrnetlab-base.dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vrnetlab-base.dockerfile b/vrnetlab-base.dockerfile index 121c6344..ad111214 100644 --- a/vrnetlab-base.dockerfile +++ b/vrnetlab-base.dockerfile @@ -26,4 +26,4 @@ RUN apt-get update -qy \ && rm -rf /var/lib/apt/lists/* RUN pip install https://github.com/carlmontanari/scrapli/archive/refs/tags/2024.07.30.post1.zip --break-system-packages -RUN pip install -e git+https://github.com/kaelemc/scrapli_community.git@sros_regex_fix#egg=scrapli_community --break-system-packages \ No newline at end of file +RUN pip install git+https://github.com/scrapli/scrapli_community --break-system-packages \ No newline at end of file From 8d4608f81b6539b2d169940bbaeeca4a8cd9e17a Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Thu, 9 Jan 2025 15:29:32 +0100 Subject: [PATCH 34/50] added uv lock/venv and env file for pylance resolve sequence (#303) --- .env | 1 + dev-notes.md | 4 +++- pyproject.toml | 7 +++++++ sros/docker/launch.py | 8 ++++---- uv.lock | 7 +++++++ 5 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 .env create mode 100644 pyproject.toml create mode 100644 uv.lock diff --git a/.env b/.env new file mode 100644 index 00000000..3621af53 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +PYTHONPATH=".:./common" \ No newline at end of file diff --git a/dev-notes.md b/dev-notes.md index dd8bd1d0..14b30fb3 100644 --- a/dev-notes.md +++ b/dev-notes.md @@ -2,7 +2,9 @@ ## vrnetlab module and vscode pylance -since vrnetlab module is in the `common` dir the pylance extension in vscode will not be able to find the module reference in `launch.py` files. To fix this, add the following to the `settings.json` file in vscode: +We added `.env` file in the root of the repo to add `common` dir to the python path so that the pylance extension can find the module. + +However, if this doesn't work for you, add the following to the `settings.json` file in vscode: ```json { diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..3681b7a9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +[project] +name = "vrnetlab" +version = "0.1.0" +description = "vrnetlab fork for Containerlab integration" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [] diff --git a/sros/docker/launch.py b/sros/docker/launch.py index 985a40ca..f289e806 100755 --- a/sros/docker/launch.py +++ b/sros/docker/launch.py @@ -50,15 +50,15 @@ def getMem(vmMode: str, ram: int) -> int: if vmMode == "integrated": # Integrated VM can use both MEMORY and CP_MEMORY env vars if "MEMORY" in os.environ: - return 1024 * get_digits(os.getenv("MEMORY")) + return 1024 * vrnetlab.get_digits(os.getenv("MEMORY")) if "CP_MEMORY" in os.environ: - return 1024 * get_digits(os.getenv("CP_MEMORY")) + return 1024 * vrnetlab.get_digits(os.getenv("CP_MEMORY")) if vmMode == "cp": if "CP_MEMORY" in os.environ: - return 1024 * get_digits(os.getenv("CP_MEMORY")) + return 1024 * vrnetlab.get_digits(os.getenv("CP_MEMORY")) if vmMode == "lc": if "LC_MEMORY" in os.environ: - return 1024 * get_digits(os.getenv("LC_MEMORY")) + return 1024 * vrnetlab.get_digits(os.getenv("LC_MEMORY")) return 1024 * int(ram) diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..d31c8bfc --- /dev/null +++ b/uv.lock @@ -0,0 +1,7 @@ +version = 1 +requires-python = ">=3.11" + +[[package]] +name = "vrnetlab" +version = "0.1.0" +source = { virtual = "." } From 8d1b4afbf515d2d1fbd93068762ca623bc866839 Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Thu, 9 Jan 2025 16:45:47 +0200 Subject: [PATCH 35/50] added uv dep for scrapli --- pyproject.toml | 4 +++- uv.lock | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3681b7a9..48c726f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,4 +4,6 @@ version = "0.1.0" description = "vrnetlab fork for Containerlab integration" readme = "README.md" requires-python = ">=3.11" -dependencies = [] +dependencies = [ + "scrapli>=2024.7.30.post1", +] diff --git a/uv.lock b/uv.lock index d31c8bfc..943ee4ac 100644 --- a/uv.lock +++ b/uv.lock @@ -1,7 +1,22 @@ version = 1 requires-python = ">=3.11" +[[package]] +name = "scrapli" +version = "2024.7.30.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/1b/27dd7f93b72e49222fc26bde23f720c24e3779bc6946b46d7977150b67a2/scrapli-2024.7.30.post1.tar.gz", hash = "sha256:4a7b862ff66c1fabba5f0c5673cc5c46a46e24e0ebf19c37a56c398cbc3ccfde", size = 104341 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/4d/af60aaf236f738dae6bf7daec613b9efa241a56d0822afde25f0e1463a5a/scrapli-2024.7.30.post1-py3-none-any.whl", hash = "sha256:59b96836f38d27498b141f6153ae0e169a5c806480f5e1f24cbb37ea74021e6f", size = 145809 }, +] + [[package]] name = "vrnetlab" version = "0.1.0" source = { virtual = "." } +dependencies = [ + { name = "scrapli" }, +] + +[package.metadata] +requires-dist = [{ name = "scrapli", specifier = ">=2024.7.30.post1" }] From d97359a5b26bdc9670fc8021714e7ed876281fb5 Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Sun, 12 Jan 2025 16:42:42 +0200 Subject: [PATCH 36/50] use ruff formatting --- common/vrnetlab.py | 150 +++++++++++++++++++++++++----------------- sros/docker/launch.py | 92 ++++++++++++++++---------- 2 files changed, 145 insertions(+), 97 deletions(-) diff --git a/common/vrnetlab.py b/common/vrnetlab.py index 7f9e3c0a..24c98032 100644 --- a/common/vrnetlab.py +++ b/common/vrnetlab.py @@ -9,10 +9,10 @@ import random import re import subprocess +import sys import telnetlib import time from pathlib import Path -import sys try: from scrapli import Driver @@ -22,11 +22,21 @@ MAX_RETRIES = 60 # set fancy logging colours -logging.addLevelName( logging.INFO, f"\x1B[1;32m\t{logging.getLevelName(logging.INFO)}\x1B[0m") -logging.addLevelName( logging.WARN, f"\x1B[1;38;5;220m\t{logging.getLevelName(logging.WARN)}\x1B[0m") -logging.addLevelName( logging.DEBUG, f"\x1B[1;94m\t{logging.getLevelName(logging.DEBUG)}\x1B[0m") -logging.addLevelName( logging.ERROR, f"\x1B[1;91m\t{logging.getLevelName(logging.ERROR)}\x1B[0m") -logging.addLevelName( logging.CRITICAL, f"\x1B[1;91m\t{logging.getLevelName(logging.CRITICAL)}\x1B[0m") +logging.addLevelName( + logging.INFO, f"\x1b[1;32m\t{logging.getLevelName(logging.INFO)}\x1b[0m" +) +logging.addLevelName( + logging.WARN, f"\x1b[1;38;5;220m\t{logging.getLevelName(logging.WARN)}\x1b[0m" +) +logging.addLevelName( + logging.DEBUG, f"\x1b[1;94m\t{logging.getLevelName(logging.DEBUG)}\x1b[0m" +) +logging.addLevelName( + logging.ERROR, f"\x1b[1;91m\t{logging.getLevelName(logging.ERROR)}\x1b[0m" +) +logging.addLevelName( + logging.CRITICAL, f"\x1b[1;91m\t{logging.getLevelName(logging.CRITICAL)}\x1b[0m" +) def gen_mac(last_octet=None): @@ -95,12 +105,11 @@ def __init__( min_dp_nics=0, use_scrapli=False, ): - self.use_scrapli = use_scrapli - + # configure logging self.logger = logging.getLogger() - + """ Configure Scrapli logger to only be INFO level. Scrapli uses 'scrapli' logger by default, and @@ -108,7 +117,7 @@ def __init__( """ self.scrapli_logger = logging.getLogger("scrapli") self.scrapli_logger.setLevel(logging.INFO) - + # configure scrapli if self.use_scrapli: # init scrapli -- main telnet device @@ -122,9 +131,9 @@ def __init__( "timeout_transport": 3600, "timeout_ops": 3600, } - + self.scrapli_tn = Driver(**scrapli_tn_dev) - + # init scrapli -- qemu monitor device scrapli_qm_dev = { "host": "127.0.0.1", @@ -136,9 +145,8 @@ def __init__( "timeout_transport": 3600, "timeout_ops": 3600, } - - self.scrapli_qm = Driver(**scrapli_qm_dev) + self.scrapli_qm = Driver(**scrapli_qm_dev) # username / password to configure self.username = username @@ -227,7 +235,9 @@ def __init__( overlay_disk_image = ".".join(tokens) if not os.path.exists(overlay_disk_image): - self.logger.debug(f"class: {self.__class__.__name__}, disk_image: {disk_image}, overlay: {overlay_disk_image}") + self.logger.debug( + f"class: {self.__class__.__name__}, disk_image: {disk_image}, overlay: {overlay_disk_image}" + ) self.logger.debug("Creating overlay disk image") run_command( [ @@ -278,11 +288,17 @@ def start(self): self.logger.info(f"{var}: {value}") self.logger.info("END ENVIRONMENT VARIABLES".center(60, "-")) - self.logger.info(f"Launching {self.__class__.__name__} with {self.smp} SMP/VCPU and {self.ram} M of RAM") - + self.logger.info( + f"Launching {self.__class__.__name__} with {self.smp} SMP/VCPU and {self.ram} M of RAM" + ) + # give nice colours. Red if disabled, Green if enabled - mgmt_passthrough_coloured = format_bool_color(self.mgmt_passthrough, "Enabled", "Disabled") - use_scrapli_coloured = format_bool_color(self.use_scrapli, "Enabled", "Disabled") + mgmt_passthrough_coloured = format_bool_color( + self.mgmt_passthrough, "Enabled", "Disabled" + ) + use_scrapli_coloured = format_bool_color( + self.use_scrapli, "Enabled", "Disabled" + ) self.logger.info(f"Scrapli: {use_scrapli_coloured}") self.logger.info(f"Transparent mgmt interface: {mgmt_passthrough_coloured}") @@ -335,7 +351,7 @@ def start(self): self.logger.info("STDERR: %s" % errs) except: pass - + for i in range(1, MAX_RETRIES + 1): try: if self.use_scrapli: @@ -603,7 +619,7 @@ def gen_dummy_nics(self): for i in range(0, nics): # dummy interface naming - interface_name = f"dummy{str(i+self.num_provisioned_nics)}" + interface_name = f"dummy{str(i + self.num_provisioned_nics)}" # PCI bus counter is to ensure pci bus index starts from 1 # and continuing in sequence regardles the eth index @@ -727,10 +743,10 @@ def wait_write( Defaults to using self.tn as connection but this can be overridden by passing a telnetlib.Telnet object in the con argument. """ - + if self.use_scrapli: return self.wait_write_scrapli(cmd, wait) - + con_name = "custom con" if con is None: con = self.tn @@ -766,11 +782,11 @@ def wait_write( self.logger.debug(f"writing to {con_name}: '{cmd}'") con.write("{}\r".format(cmd).encode()) - + def wait_write_scrapli(self, cmd, wait="__defaultpattern__"): """ Wait for something on the serial port and then send command using Scrapli telnet channel - + Arguments are: - cmd: command to send (string) - wait: prompt to wait for before sending command, defaults to # (string) @@ -779,23 +795,23 @@ def wait_write_scrapli(self, cmd, wait="__defaultpattern__"): # use class default wait pattern if none was explicitly specified if wait == "__defaultpattern__": wait = self.wait_pattern - + self.logger.info(f"Waiting on console for: '{wait}'") - + self.con_read_until(wait) - time.sleep(0.1) # don't write to the console too fast - + time.sleep(0.1) # don't write to the console too fast + self.write_to_stdout(b"\n") - + self.logger.info(f"Writing to console: '{cmd}'") self.scrapli_tn.channel.write(f"{cmd}\r") - + def con_expect(self, regex_list, timeout=None): """ Implements telnetlib expect() functionality, for usage with scrapli driver. Wait for something on the console. - + Takes list of byte strings and an optional timeout (block) time (float) as arguments. Returns tuple of: @@ -803,66 +819,66 @@ def con_expect(self, regex_list, timeout=None): - match object. - buffer of cosole read until match, or function exit. """ - + buf = b"" - + if timeout: t_end = time.time() + timeout while time.time() < t_end: buf += self.scrapli_tn.channel.read() else: buf = self.scrapli_tn.channel.read() - + for i, obj in enumerate(regex_list): match = re.search(obj.decode(), buf.decode()) if match: return i, match, buf - + return -1, None, buf def con_read_until(self, match_str, timeout=None): """ Implements telnetlib read_until() functionality, for usage with scrapli driver. - + Read until a given string is encountered or until timeout. When no match is found, return whatever is available instead, possibly the empty string. - + Arguments: - match_str: string to match on (string) - timeout: timeout in seconds, defaults to None (float) """ buf = b"" - + if timeout: t_end = time.time() + timeout - + while True: current_buf = self.scrapli_tn.channel.read() buf += current_buf - + match = re.search(match_str, current_buf.decode()) - # for reliability purposes, doublecheck the entire buffer + # for reliability purposes, doublecheck the entire buffer # maybe the current buffer only has partial output if match is None: match = re.search(match_str, buf.decode()) - + self.write_to_stdout(current_buf) - + if match: break if timeout and time.time() > t_end: break - + return buf - + def write_to_stdout(self, bytes): """ - Quick and dirty way to write to stdout (docker logs) instead of + Quick and dirty way to write to stdout (docker logs) instead of using the python logger which poorly formats the output. - + Mainly for printing console to docker logs """ sys.stdout.buffer.write(bytes) @@ -1008,13 +1024,13 @@ def start(self): else: self.update_health(1, "starting") - #file-based signalling backdoor to trigger a system reset (via qemu-monitor) on all or specific VMs. - #if file is empty: reset whole VR (all VMs) - #if file is non-empty: reset only specified VMs (comma separated list) - if os.path.exists('/reset'): - with open('/reset','rt') as f: - fcontent=f.read().strip() - vm_num_list=fcontent.split(',') + # file-based signalling backdoor to trigger a system reset (via qemu-monitor) on all or specific VMs. + # if file is empty: reset whole VR (all VMs) + # if file is non-empty: reset only specified VMs (comma separated list) + if os.path.exists("/reset"): + with open("/reset", "rt") as f: + fcontent = f.read().strip() + vm_num_list = fcontent.split(",") for vm in self.vms: if (str(vm.num) in vm_num_list) or not fcontent: try: @@ -1022,13 +1038,20 @@ def start(self): vm.scrapli_qm.channel.write("system_reset\r") else: vm.qm.write("system_reset\r".encode()) - self.logger.debug(f"Sent qemu-monitor system_reset to VM num {vm.num} ") + self.logger.debug( + f"Sent qemu-monitor system_reset to VM num {vm.num} " + ) except Exception as e: - self.logger.error(f"Failed to send qemu-monitor system_reset to VM num {vm.num} ({e})") + self.logger.error( + f"Failed to send qemu-monitor system_reset to VM num {vm.num} ({e})" + ) try: - os.remove('/reset') + os.remove("/reset") except Exception as e: - self.logger.error(f"Failed to cleanup /reset file({e}). qemu-monitor system_reset will likely be triggered again on VMs") + self.logger.error( + f"Failed to cleanup /reset file({e}). qemu-monitor system_reset will likely be triggered again on VMs" + ) + class QemuBroken(Exception): """Our Qemu instance is somehow broken""" @@ -1057,13 +1080,18 @@ def cidr_to_ddn(prefix: str) -> list[str]: network = ipaddress.IPv4Interface(prefix) return [str(network.ip), str(network.netmask)] + def format_bool_color(bool_var: bool, text_if_true: str, text_if_false: str) -> str: """ Generate a ANSI escape code colored string based on a boolean. - + Args: bool_var: Boolean to be evaluated text_if_true: Text returned if bool_var is true -- ANSI Formatted in green color text_if_false: Text returned if bool_var is false -- ANSI Formatted in red color """ - return f"\x1B[32m{text_if_true}\x1B[0m" if bool_var else f"\x1B[31m{text_if_false}\x1B[0m" \ No newline at end of file + return ( + f"\x1b[32m{text_if_true}\x1b[0m" + if bool_var + else f"\x1b[31m{text_if_false}\x1b[0m" + ) diff --git a/sros/docker/launch.py b/sros/docker/launch.py index f289e806..28407f08 100755 --- a/sros/docker/launch.py +++ b/sros/docker/launch.py @@ -16,6 +16,7 @@ DEFAULT_SCRAPLI_TIMEOUT = 900 DEBUG_SCRAPLI = True if os.getenv("DEBUG_SCRAPLI", "false").lower() == "true" else False + def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -914,7 +915,7 @@ def __init__(self, username, password, ram, conn_mode, cpu=2, num=0): ram=ram, driveif="virtio", smp=f"{cpu}", - use_scrapli=True + use_scrapli=True, ) self.nic_type = "virtio-net-pci" @@ -1094,10 +1095,20 @@ def configure_power(self, power_cfg): power_path = "system" for s in range(1, shelves + 1): - res = self.sros_con.send_configs([f"/configure {power_path} power-shelf {s} power-shelf-type {power_shelf_type}"], strip_prompt=False) + res = self.sros_con.send_configs( + [ + f"/configure {power_path} power-shelf {s} power-shelf-type {power_shelf_type}" + ], + strip_prompt=False, + ) self.log_scrapli_cmd_res(res) for m in range(1, modules + 1): - res = self.sros_con.send_configs([f"/configure {power_path} power-shelf {s} power-module {m} power-module-type {module_type}"], strip_prompt=False) + res = self.sros_con.send_configs( + [ + f"/configure {power_path} power-shelf {s} power-module {m} power-module-type {module_type}" + ], + strip_prompt=False, + ) self.log_scrapli_cmd_res(res) def enterConfig(self): @@ -1110,40 +1121,40 @@ def enterBofConfig(self): """Enter bof configuration mode. No-op for SR OS version <= 22""" if SROS_VERSION.major <= 22 or SROS_VERSION.magc: return - res = self.sros_con.send_commands(["edit-config bof exclusive"], strip_prompt=False) + res = self.sros_con.send_commands( + ["edit-config bof exclusive"], strip_prompt=False + ) self.log_scrapli_cmd_res(res) def commitConfig(self): """Commit configuration. No-op for SR OS version <= 22""" if SROS_VERSION.major <= 22 or SROS_VERSION.magc: return - res = self.sros_con.send_configs([ - "commit", - "/" - ], strip_prompt=False) + res = self.sros_con.send_configs(["commit", "/"], strip_prompt=False) self.log_scrapli_cmd_res(res) def commitBofConfig(self): """Commit configuration. No-op for SR OS version <= 22""" if SROS_VERSION.major <= 22 or SROS_VERSION.magc: return - res = self.sros_con.send_configs([ - "commit", - "/" - ], strip_prompt=False) + res = self.sros_con.send_configs(["commit", "/"], strip_prompt=False) self.log_scrapli_cmd_res(res) def configureCards(self): """Configure cards""" # integrated vsims have `card_config` in the variant definition if "card_config" in self.variant: - res = self.sros_con.send_configs(self.variant["card_config"].splitlines(), strip_prompt=False) + res = self.sros_con.send_configs( + self.variant["card_config"].splitlines(), strip_prompt=False + ) self.log_scrapli_cmd_res(res) # else this might be a distributed chassis elif self.variant.get("lcs") is not None: for lc in self.variant["lcs"]: if "card_config" in lc: - res = self.sros_con.send_configs(lc["card_config"].splitlines(), strip_prompt=False) + res = self.sros_con.send_configs( + lc["card_config"].splitlines(), strip_prompt=False + ) self.log_scrapli_cmd_res(res) def persistBofAndConfig(self): @@ -1166,7 +1177,8 @@ def switchConfigEngine(self): res = self.sros_con.send_configs( [ f"/configure system management-interface configuration-mode {self.mode}" - ], strip_prompt=False + ], + strip_prompt=False, ) self.log_scrapli_cmd_res(res) @@ -1205,26 +1217,26 @@ def gen_bof_config(self): else: if SROS_VERSION.major >= 23 and not SROS_VERSION.magc: cmds.append( - f'/bof router static-routes route {os.getenv("DOCKER_NET_V4_ADDR")} next-hop {BRIDGE_V4_ADDR}' + f"/bof router static-routes route {os.getenv('DOCKER_NET_V4_ADDR')} next-hop {BRIDGE_V4_ADDR}" ) else: cmds.append( - f'/bof static-route {os.getenv("DOCKER_NET_V4_ADDR")} next-hop {BRIDGE_V4_ADDR}' + f"/bof static-route {os.getenv('DOCKER_NET_V4_ADDR')} next-hop {BRIDGE_V4_ADDR}" ) if "DOCKER_NET_V6_ADDR" in os.environ and os.getenv("DOCKER_NET_V6_ADDR") != "": if not self.mgmt_passthrough: if SROS_VERSION.major >= 23 and not SROS_VERSION.magc: cmds.append( - f'/bof router static-routes route {os.getenv("DOCKER_NET_V6_ADDR")} next-hop {BRIDGE_V6_ADDR}' + f"/bof router static-routes route {os.getenv('DOCKER_NET_V6_ADDR')} next-hop {BRIDGE_V6_ADDR}" ) else: cmds.append( - f'/bof static-route {os.getenv("DOCKER_NET_V6_ADDR")} next-hop {BRIDGE_V6_ADDR}' + f"/bof static-route {os.getenv('DOCKER_NET_V6_ADDR')} next-hop {BRIDGE_V6_ADDR}" ) # if "docker-net-v6-addr" in m: # cmds.append(f"/bof static-route {m[docker-net-v6-addr]} next-hop {BRIDGE_ADDR}") return cmds - + def log_scrapli_cmd_res(self, res: list): if not DEBUG_SCRAPLI: return @@ -1239,16 +1251,20 @@ def bootstrap_config(self): # configure bof before we check if config file was provided # since bof statements are not part of the config file # thus it must be applied unconditionally - + # init scrapli sros driver scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) - self.logger.info(f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)") - - # check if config was provided + self.logger.info( + f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)" + ) + + # check if config was provided config_exists = os.path.isfile("/tftpboot/config.txt") - fmt_config_exists = vrnetlab.format_bool_color(config_exists, "exists", "does not exist") + fmt_config_exists = vrnetlab.format_bool_color( + config_exists, "exists", "does not exist" + ) self.logger.debug(f"Configuration file {fmt_config_exists}") - + # init scrapli sros_scrapli_dev = { "platform": "nokia_sros", @@ -1259,21 +1275,21 @@ def bootstrap_config(self): "timeout_transport": scrapli_timeout, "timeout_ops": scrapli_timeout, } - + if SROS_VERSION.major <= 22 or SROS_VERSION.magc: sros_scrapli_dev["variant"] = "classic" - - # self.scrapli_logger.setLevel(logging.DEBUG) + + # self.scrapli_logger.setLevel(logging.DEBUG) self.sros_con = Scrapli(**sros_scrapli_dev) self.sros_con.commandeer(conn=self.scrapli_tn) - + # configure BOF self.enterBofConfig() - + # send and log BOF config res = self.sros_con.send_configs(self.gen_bof_config(), strip_prompt=False) self.log_scrapli_cmd_res(res) - + self.commitBofConfig() # save bof config on disk self.persistBofAndConfig() @@ -1284,8 +1300,11 @@ def bootstrap_config(self): # enter config mode, no-op for sros <=22 self.enterConfig() - - res = self.sros_con.send_configs(getDefaultConfig().format(name=self.hostname).splitlines(), strip_prompt=False) + + res = self.sros_con.send_configs( + getDefaultConfig().format(name=self.hostname).splitlines(), + strip_prompt=False, + ) self.log_scrapli_cmd_res(res) # configure card/mda of a given variant @@ -1298,7 +1317,6 @@ def bootstrap_config(self): self.commitConfig() self.switchConfigEngine() - @property def ram(self): @@ -1947,5 +1965,7 @@ def getDefaultConfig() -> str: conn_mode=args.connection_mode, mgmt_passthrough=mgmt_passthrough, ) - ia.logger.debug(f"acting flags: username '{args.username}', password '{args.password}', connection-mode '{args.connection_mode}', variant '{args.variant}'") + ia.logger.debug( + f"acting flags: username '{args.username}', password '{args.password}', connection-mode '{args.connection_mode}', variant '{args.variant}'" + ) ia.start() From 70c688d1308eae052553c46d4a679e3ba747a4e3 Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Sun, 12 Jan 2025 18:15:28 +0200 Subject: [PATCH 37/50] update base image with pinned scrapli community --- c8000v/docker/Dockerfile | 2 +- cat9kv/docker/Dockerfile | 2 +- csr/docker/Dockerfile | 2 +- n9kv/docker/Dockerfile | 2 +- nxos/docker/Dockerfile | 2 +- sros/docker/Dockerfile | 2 +- vios/docker/Dockerfile | 2 +- vrnetlab-base.dockerfile | 6 +++--- xrv/docker/Dockerfile | 2 +- xrv9k/docker/Dockerfile | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/c8000v/docker/Dockerfile b/c8000v/docker/Dockerfile index 2fab6fd7..05210d93 100644 --- a/c8000v/docker/Dockerfile +++ b/c8000v/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 +FROM ghcr.io/srl-labs/vrnetlab-base:0.1.0 ARG VERSION ENV VERSION=${VERSION} diff --git a/cat9kv/docker/Dockerfile b/cat9kv/docker/Dockerfile index 8f79f39d..a641794a 100644 --- a/cat9kv/docker/Dockerfile +++ b/cat9kv/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 +FROM ghcr.io/srl-labs/vrnetlab-base:0.1.0 ARG VERSION ENV VERSION=${VERSION} diff --git a/csr/docker/Dockerfile b/csr/docker/Dockerfile index 2fab6fd7..05210d93 100644 --- a/csr/docker/Dockerfile +++ b/csr/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 +FROM ghcr.io/srl-labs/vrnetlab-base:0.1.0 ARG VERSION ENV VERSION=${VERSION} diff --git a/n9kv/docker/Dockerfile b/n9kv/docker/Dockerfile index a72c601e..7f1b2530 100644 --- a/n9kv/docker/Dockerfile +++ b/n9kv/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 +FROM ghcr.io/srl-labs/vrnetlab-base:0.1.0 ARG IMAGE COPY $IMAGE* / diff --git a/nxos/docker/Dockerfile b/nxos/docker/Dockerfile index 1527c896..bcadab64 100644 --- a/nxos/docker/Dockerfile +++ b/nxos/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 +FROM ghcr.io/srl-labs/vrnetlab-base:0.1.0 ARG IMAGE COPY $IMAGE* / diff --git a/sros/docker/Dockerfile b/sros/docker/Dockerfile index 8c6a929e..b9b73491 100644 --- a/sros/docker/Dockerfile +++ b/sros/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 +FROM ghcr.io/srl-labs/vrnetlab-base:0.1.0 ARG IMAGE COPY $IMAGE* / diff --git a/vios/docker/Dockerfile b/vios/docker/Dockerfile index 6427f83f..dfe9a778 100644 --- a/vios/docker/Dockerfile +++ b/vios/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 +FROM ghcr.io/srl-labs/vrnetlab-base:0.1.0 ARG IMAGE COPY $IMAGE* / diff --git a/vrnetlab-base.dockerfile b/vrnetlab-base.dockerfile index ad111214..6258c3f2 100644 --- a/vrnetlab-base.dockerfile +++ b/vrnetlab-base.dockerfile @@ -7,6 +7,8 @@ RUN apt-get update -qy \ bridge-utils \ iproute2 \ python3 \ + python3-pip \ + python3-passlib \ socat \ qemu-kvm \ qemu-utils \ @@ -18,12 +20,10 @@ RUN apt-get update -qy \ iptables \ nftables \ telnet \ - python3-pip \ - python3-passlib \ git \ dosfstools \ genisoimage \ && rm -rf /var/lib/apt/lists/* RUN pip install https://github.com/carlmontanari/scrapli/archive/refs/tags/2024.07.30.post1.zip --break-system-packages -RUN pip install git+https://github.com/scrapli/scrapli_community --break-system-packages \ No newline at end of file +RUN pip install git+https://github.com/scrapli/scrapli_community@d862833 --break-system-packages \ No newline at end of file diff --git a/xrv/docker/Dockerfile b/xrv/docker/Dockerfile index 34aa6673..f58cc2b0 100644 --- a/xrv/docker/Dockerfile +++ b/xrv/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 +FROM ghcr.io/srl-labs/vrnetlab-base:0.1.0 ARG IMAGE COPY $IMAGE* / diff --git a/xrv9k/docker/Dockerfile b/xrv9k/docker/Dockerfile index 8c6a929e..b9b73491 100644 --- a/xrv9k/docker/Dockerfile +++ b/xrv9k/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 +FROM ghcr.io/srl-labs/vrnetlab-base:0.1.0 ARG IMAGE COPY $IMAGE* / From 7867c398158cf8496cdf7beffeda9e2906b8f0a6 Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Sun, 12 Jan 2025 19:06:03 +0200 Subject: [PATCH 38/50] close sros driver connection to invoke on_exit commands (quit-config) --- common/vrnetlab.py | 4 ++-- sros/docker/launch.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/common/vrnetlab.py b/common/vrnetlab.py index 24c98032..e121139f 100644 --- a/common/vrnetlab.py +++ b/common/vrnetlab.py @@ -120,7 +120,7 @@ def __init__( # configure scrapli if self.use_scrapli: - # init scrapli -- main telnet device + # init scrapli_tn -- main telnet device scrapli_tn_dev = { "host": "127.0.0.1", "port": 5000 + num, @@ -134,7 +134,7 @@ def __init__( self.scrapli_tn = Driver(**scrapli_tn_dev) - # init scrapli -- qemu monitor device + # init scrapli_qm_dev -- qemu monitor device scrapli_qm_dev = { "host": "127.0.0.1", "port": 4000 + num, diff --git a/sros/docker/launch.py b/sros/docker/launch.py index 28407f08..f35da2ca 100755 --- a/sros/docker/launch.py +++ b/sros/docker/launch.py @@ -1318,6 +1318,9 @@ def bootstrap_config(self): self.switchConfigEngine() + # close scrapli device driver + self.sros_con.close() + @property def ram(self): """Ignore environment variables here, since getMem function is used""" From f49d21fcf46c745aa3841f06625938937df11ff0 Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Sun, 12 Jan 2025 19:51:43 +0200 Subject: [PATCH 39/50] added local deps --- pyproject.toml | 15 ++++++++++----- uv.lock | 25 ++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 48c726f3..6267f4e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,14 @@ [project] +dependencies = [ + "passlib>=1.7.4", + "scrapli>=2024.7.30.post1", + "scrapli-community", +] +description = "Building containers for VM-based Network OSes for Containerlab" name = "vrnetlab" -version = "0.1.0" -description = "vrnetlab fork for Containerlab integration" readme = "README.md" requires-python = ">=3.11" -dependencies = [ - "scrapli>=2024.7.30.post1", -] +version = "0.1.0" + +[tool.uv.sources] +scrapli-community = {git = "https://github.com/scrapli/scrapli_community", rev = "d862833"} diff --git a/uv.lock b/uv.lock index 943ee4ac..113feed4 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,15 @@ version = 1 requires-python = ">=3.11" +[[package]] +name = "passlib" +version = "1.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554 }, +] + [[package]] name = "scrapli" version = "2024.7.30.post1" @@ -10,13 +19,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/4d/af60aaf236f738dae6bf7daec613b9efa241a56d0822afde25f0e1463a5a/scrapli-2024.7.30.post1-py3-none-any.whl", hash = "sha256:59b96836f38d27498b141f6153ae0e169a5c806480f5e1f24cbb37ea74021e6f", size = 145809 }, ] +[[package]] +name = "scrapli-community" +version = "2024.7.30" +source = { git = "https://github.com/scrapli/scrapli_community?rev=d862833#d86283316eee66799d43a7ef62bf0e20565d31a8" } +dependencies = [ + { name = "scrapli" }, +] + [[package]] name = "vrnetlab" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "passlib" }, { name = "scrapli" }, + { name = "scrapli-community" }, ] [package.metadata] -requires-dist = [{ name = "scrapli", specifier = ">=2024.7.30.post1" }] +requires-dist = [ + { name = "passlib", specifier = ">=1.7.4" }, + { name = "scrapli", specifier = ">=2024.7.30.post1" }, + { name = "scrapli-community", git = "https://github.com/scrapli/scrapli_community?rev=d862833" }, +] From fa2f50d0a78507f3eeea1926ca4d5949415c29b0 Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Sun, 12 Jan 2025 22:27:38 +0200 Subject: [PATCH 40/50] use uv in the base image --- common/healthcheck.py | 2 -- pyproject.toml | 3 ++- sros/docker/Dockerfile | 7 ++----- uv.lock | 30 +++++++++++++++++++++++++++++- vrnetlab-base.dockerfile | 19 ++++++++++++++----- 5 files changed, 47 insertions(+), 14 deletions(-) diff --git a/common/healthcheck.py b/common/healthcheck.py index 49053b63..35518e86 100755 --- a/common/healthcheck.py +++ b/common/healthcheck.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - import sys try: diff --git a/pyproject.toml b/pyproject.toml index 6267f4e8..6461b3a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,14 @@ [project] dependencies = [ "passlib>=1.7.4", + "pyyaml>=6.0.2", "scrapli>=2024.7.30.post1", "scrapli-community", ] description = "Building containers for VM-based Network OSes for Containerlab" name = "vrnetlab" readme = "README.md" -requires-python = ">=3.11" +requires-python = ">=3.11,<3.13" version = "0.1.0" [tool.uv.sources] diff --git a/sros/docker/Dockerfile b/sros/docker/Dockerfile index b9b73491..4fc2db93 100644 --- a/sros/docker/Dockerfile +++ b/sros/docker/Dockerfile @@ -1,9 +1,6 @@ -FROM ghcr.io/srl-labs/vrnetlab-base:0.1.0 +# base image dockerfile is defined in https://github.com/hellt/vrnetlab +FROM ghcr.io/srl-labs/vrnetlab-base:0.2.0 ARG IMAGE COPY $IMAGE* / COPY *.py / - -EXPOSE 22 80 443 161/udp 830 5000 10000-10099 57400 -HEALTHCHECK CMD ["/healthcheck.py"] -ENTRYPOINT ["/launch.py"] diff --git a/uv.lock b/uv.lock index 113feed4..965cdd9d 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -requires-python = ">=3.11" +requires-python = ">=3.11, <3.13" [[package]] name = "passlib" @@ -10,6 +10,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554 }, ] +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, +] + [[package]] name = "scrapli" version = "2024.7.30.post1" @@ -33,6 +59,7 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "passlib" }, + { name = "pyyaml" }, { name = "scrapli" }, { name = "scrapli-community" }, ] @@ -40,6 +67,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "passlib", specifier = ">=1.7.4" }, + { name = "pyyaml", specifier = ">=6.0.2" }, { name = "scrapli", specifier = ">=2024.7.30.post1" }, { name = "scrapli-community", git = "https://github.com/scrapli/scrapli_community?rev=d862833" }, ] diff --git a/vrnetlab-base.dockerfile b/vrnetlab-base.dockerfile index 6258c3f2..8203331f 100644 --- a/vrnetlab-base.dockerfile +++ b/vrnetlab-base.dockerfile @@ -1,14 +1,15 @@ FROM public.ecr.aws/docker/library/debian:bookworm-slim LABEL org.opencontainers.image.authors="roman@dodin.dev" +COPY --from=ghcr.io/astral-sh/uv:0.5.18 /uv /uvx /bin/ + ARG DEBIAN_FRONTEND=noninteractive + RUN apt-get update -qy \ && apt-get install -y --no-install-recommends \ + ca-certificates \ bridge-utils \ iproute2 \ - python3 \ - python3-pip \ - python3-passlib \ socat \ qemu-kvm \ qemu-utils \ @@ -25,5 +26,13 @@ RUN apt-get update -qy \ genisoimage \ && rm -rf /var/lib/apt/lists/* -RUN pip install https://github.com/carlmontanari/scrapli/archive/refs/tags/2024.07.30.post1.zip --break-system-packages -RUN pip install git+https://github.com/scrapli/scrapli_community@d862833 --break-system-packages \ No newline at end of file +# copying the uv project +COPY pyproject.toml /pyproject.toml +COPY uv.lock /uv.lock +RUN /bin/uv sync --frozen + +# copy core vrnetlab scripts +COPY ./common/healthcheck.py ./common/vrnetlab.py / + +HEALTHCHECK CMD ["uv", "run", "/healthcheck.py"] +ENTRYPOINT ["uv", "run", "/launch.py"] \ No newline at end of file From d3d167ed0607b6b49aa4515b96cfb219f35d05a6 Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Sun, 12 Jan 2025 22:42:57 +0200 Subject: [PATCH 41/50] ruff formatting --- c8000v/docker/launch.py | 26 ++++++++++-------- cat9kv/docker/launch.py | 10 ++++--- n9kv/docker/launch.py | 43 +++++++++++++++++++---------- nxos/docker/launch.py | 23 ++++++++++------ vios/docker/launch.py | 21 +++++++------- xrv/docker/launch.py | 31 ++++++++++++--------- xrv9k/docker/launch.py | 61 +++++++++++++++++++++++++---------------- 7 files changed, 131 insertions(+), 84 deletions(-) diff --git a/c8000v/docker/launch.py b/c8000v/docker/launch.py index d7352a01..0bcfeed9 100755 --- a/c8000v/docker/launch.py +++ b/c8000v/docker/launch.py @@ -13,6 +13,7 @@ STARTUP_CONFIG_FILE = "/config/startup-config.cfg" DEFAULT_SCRAPLI_TIMEOUT = 900 + def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -52,7 +53,9 @@ def __init__(self, hostname, username, password, conn_mode, install_mode=False): logger.info("License found") self.license = True - super().__init__(username, password, disk_image=disk_image, ram=4096, use_scrapli=True) + super().__init__( + username, password, disk_image=disk_image, ram=4096, use_scrapli=True + ) self.install_mode = install_mode self.hostname = hostname self.conn_mode = conn_mode @@ -67,10 +70,10 @@ def __init__(self, hostname, username, password, conn_mode, install_mode=False): cfg = self.gen_bootstrap_config() if os.path.exists(STARTUP_CONFIG_FILE): self.logger.info("Startup configuration file found") - with open (STARTUP_CONFIG_FILE, "r") as startup_config: + with open(STARTUP_CONFIG_FILE, "r") as startup_config: cfg += startup_config.read() else: - self.logger.warning(f"User provided startup configuration is not found.") + self.logger.warning("User provided startup configuration is not found.") self.create_config_image(cfg) self.qemu_args.extend(["-cdrom", "/" + self.image_name]) @@ -79,9 +82,9 @@ def gen_install_config(self) -> str: """ Returns the configuration to load in install mode """ - + config = "" - + if self.license: config += """do clock set 13:33:37 1 Jan 2010 interface GigabitEthernet1 @@ -92,7 +95,7 @@ def gen_install_config(self) -> str: yes do license install tftp://10.0.0.2/license.lic """ - + config += """ license boot level network-premier addon dna-premier platform console serial @@ -107,9 +110,9 @@ def gen_bootstrap_config(self) -> str: """ Returns the system bootstrap configuration """ - + v4_mgmt_address = vrnetlab.cidr_to_ddn(self.mgmt_address_ipv4) - + return f"""hostname {self.hostname} username {self.username} privilege 15 password {self.password} ip domain name example.com @@ -168,7 +171,7 @@ def create_config_image(self, config): "/" + self.image_name, "/iosxe_config.txt", ] - + self.logger.debug("Generating boot ISO") subprocess.Popen(genisoimage_args).wait() @@ -180,12 +183,12 @@ def bootstrap_spin(self): self.stop() self.start() return - + (ridx, match, res) = self.con_expect( [b"CVAC-4-CONFIG_DONE", b"IOSXEBOOT-4-FACTORY_RESET"] ) if match: # got a match! - if ridx == 0 and not self.install_mode: # configuration applied + if ridx == 0 and not self.install_mode: # configuration applied self.logger.info("CVAC Configuration has been applied.") # close telnet connection self.scrapli_tn.close() @@ -215,6 +218,7 @@ def bootstrap_spin(self): return + class C8000v(vrnetlab.VR): def __init__(self, hostname, username, password, conn_mode): super(C8000v, self).__init__(username, password) diff --git a/cat9kv/docker/launch.py b/cat9kv/docker/launch.py index e562efaa..46789e5f 100755 --- a/cat9kv/docker/launch.py +++ b/cat9kv/docker/launch.py @@ -13,6 +13,7 @@ STARTUP_CONFIG_FILE = "/config/startup-config.cfg" DEFAULT_SCRAPLI_TIMEOUT = 900 + def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -52,7 +53,7 @@ def __init__(self, hostname, username, password, conn_mode, vcpu, ram): smp=f"cores={vcpu},threads=1,sockets=1", ram=ram, min_dp_nics=8, - use_scrapli=True + use_scrapli=True, ) self.hostname = hostname self.conn_mode = conn_mode @@ -86,7 +87,7 @@ def create_boot_image(self): self.logger.debug("No vswitch.xml file provided.") v4_mgmt_address = vrnetlab.cidr_to_ddn(self.mgmt_address_ipv4) - + cat9kv_config = f"""hostname {self.hostname} username {self.username} privilege 15 password {self.password} ip domain name example.com @@ -123,7 +124,7 @@ def create_boot_image(self): if os.path.exists(STARTUP_CONFIG_FILE): self.logger.info("Startup configuration file found") - with open (STARTUP_CONFIG_FILE, "r") as startup_config: + with open(STARTUP_CONFIG_FILE, "r") as startup_config: cat9kv_config += startup_config.read() else: self.logger.warning(f"User provided startup configuration is not found.") @@ -158,7 +159,7 @@ def bootstrap_spin(self): ], ) if match: # got a match! - if ridx == 0: # configuration applied + if ridx == 0: # configuration applied self.logger.info("CVAC Configuration has been applied.") # close telnet connection self.scrapli_tn.close() @@ -182,6 +183,7 @@ def bootstrap_spin(self): return + class cat9kv(vrnetlab.VR): def __init__(self, hostname, username, password, conn_mode, vcpu, ram): super(cat9kv, self).__init__(username, password) diff --git a/n9kv/docker/launch.py b/n9kv/docker/launch.py index f51142f5..8ca462f0 100755 --- a/n9kv/docker/launch.py +++ b/n9kv/docker/launch.py @@ -14,6 +14,7 @@ STARTUP_CONFIG_FILE = "/config/startup-config.cfg" DEFAULT_SCRAPLI_TIMEOUT = 900 + def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -49,8 +50,13 @@ def __init__(self, hostname, username, password, conn_mode): logging.getLogger().info("Disk image was not found") exit(1) super(N9KV_vm, self).__init__( - username, password, disk_image=disk_image, ram=10240, - smp=4, cpu="host,level=9", use_scrapli=True + username, + password, + disk_image=disk_image, + ram=10240, + smp=4, + cpu="host,level=9", + use_scrapli=True, ) self.hostname = hostname self.conn_mode = conn_mode @@ -67,10 +73,10 @@ def __init__(self, hostname, username, password, conn_mode): replace_index = self.qemu_args.index( "if=ide,file={}".format(overlay_disk_image) ) - self.qemu_args[ - replace_index - ] = "file={},if=none,id=drive-sata-disk0,format=qcow2".format( - overlay_disk_image + self.qemu_args[replace_index] = ( + "file={},if=none,id=drive-sata-disk0,format=qcow2".format( + overlay_disk_image + ) ) self.qemu_args.extend(["-device", "ahci,id=ahci0,bus=pci.0"]) self.qemu_args.extend( @@ -80,7 +86,6 @@ def __init__(self, hostname, username, password, conn_mode): ] ) - def bootstrap_spin(self): """This function should be called periodically to do work.""" if self.spins > 300: @@ -89,7 +94,14 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.con_expect([b"\(yes\/skip\/no\)\[no\]:",b"\(yes\/no\)\[n\]:", b"\(yes\/no\)\[no\]:", b"login:"]) + (ridx, match, res) = self.con_expect( + [ + b"\(yes\/skip\/no\)\[no\]:", + b"\(yes\/no\)\[n\]:", + b"\(yes\/no\)\[no\]:", + b"login:", + ] + ) if match: # got a match! if ridx in (0, 1, 2): self.logger.debug("matched poap prompt") @@ -130,11 +142,12 @@ def bootstrap_spin(self): return - def apply_config(self): - + def apply_config(self): scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) - self.logger.info(f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)") - + self.logger.info( + f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)" + ) + # init scrapli n9kv_scrapli_dev = { "host": "127.0.0.1", @@ -144,7 +157,7 @@ def apply_config(self): "timeout_transport": scrapli_timeout, "timeout_ops": scrapli_timeout, } - + n9kv_config = f"""hostname {self.hostname} username {self.username} password 0 {self.password} role network-admin ! @@ -171,7 +184,7 @@ def apply_config(self): con = NXOSDriver(**n9kv_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: @@ -181,7 +194,7 @@ def apply_config(self): res = con.send_configs(n9kv_config.splitlines()) con.send_config("copy running-config startup-config") - + for response in res: self.logger.info(f"CONFIG:{response.channel_input}") self.logger.info(f"RESULT:{response.result}") diff --git a/nxos/docker/launch.py b/nxos/docker/launch.py index ee7dfcd5..632f91de 100755 --- a/nxos/docker/launch.py +++ b/nxos/docker/launch.py @@ -14,6 +14,7 @@ STARTUP_CONFIG_FILE = "/config/startup-config.cfg" DEFAULT_SCRAPLI_TIMEOUT = 900 + def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -45,7 +46,12 @@ def __init__(self, hostname, username, password, conn_mode): if re.search(".qcow2$", e): disk_image = "/" + e super(NXOS_vm, self).__init__( - username, password, disk_image=disk_image, ram=4096, smp="2", use_scrapli=True + username, + password, + disk_image=disk_image, + ram=4096, + smp="2", + use_scrapli=True, ) self.credentials = [["admin", "admin"]] self.hostname = hostname @@ -97,11 +103,12 @@ def bootstrap_spin(self): return - def apply_config(self): - + def apply_config(self): scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) - self.logger.info(f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)") - + self.logger.info( + f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)" + ) + # init scrapli nxos_scrapli_dev = { "host": "127.0.0.1", @@ -111,7 +118,7 @@ def apply_config(self): "timeout_transport": scrapli_timeout, "timeout_ops": scrapli_timeout, } - + nxos_config = f"""hostname {self.hostname} username {self.username} password 0 {self.password} role network-admin ! @@ -133,7 +140,7 @@ def apply_config(self): con = NXOSDriver(**nxos_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: @@ -143,7 +150,7 @@ def apply_config(self): res = con.send_configs(nxos_config.splitlines()) con.send_config("copy running-config startup-config") - + for response in res: self.logger.info(f"CONFIG:{response.channel_input}") self.logger.info(f"RESULT:{response.result}") diff --git a/vios/docker/launch.py b/vios/docker/launch.py index 8a3eaac1..6b7d1ff1 100755 --- a/vios/docker/launch.py +++ b/vios/docker/launch.py @@ -12,6 +12,7 @@ STARTUP_CONFIG_FILE = "/config/startup-config.cfg" DEFAULT_SCRAPLI_TIMEOUT = 900 + def handle_SIGCHLD(_signal, _frame): os.waitpid(-1, os.WNOHANG) @@ -53,7 +54,7 @@ def __init__(self, hostname: str, username: str, password: str, conn_mode: str): smp="1", ram=512, driveif="virtio", - use_scrapli=True + use_scrapli=True, ) self.hostname = hostname @@ -108,12 +109,12 @@ def bootstrap_spin(self): self.spins += 1 return - - def apply_config(self): - + def apply_config(self): scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) - self.logger.info(f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)") - + self.logger.info( + f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)" + ) + # init scrapli vios_scrapli_dev = { "host": "127.0.0.1", @@ -123,9 +124,9 @@ def apply_config(self): "timeout_transport": scrapli_timeout, "timeout_ops": scrapli_timeout, } - + v4_mgmt_address = vrnetlab.cidr_to_ddn(self.mgmt_address_ipv4) - + vios_config = f"""hostname {self.hostname} username {self.username} privilege 15 password {self.password} ip domain-name example.com @@ -177,7 +178,7 @@ def apply_config(self): 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: @@ -187,7 +188,7 @@ def apply_config(self): 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}") diff --git a/xrv/docker/launch.py b/xrv/docker/launch.py index 955b6012..2e87f25f 100755 --- a/xrv/docker/launch.py +++ b/xrv/docker/launch.py @@ -16,6 +16,7 @@ STARTUP_CONFIG_FILE = "/config/startup-config.cfg" DEFAULT_SCRAPLI_TIMEOUT = 900 + def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -47,7 +48,8 @@ def __init__(self, hostname, username, password, conn_mode): if re.search(".vmdk", e): disk_image = "/" + e super(XRV_vm, self).__init__( - username, password, disk_image=disk_image, ram=3072, use_scrapli=True) + username, password, disk_image=disk_image, ram=3072, use_scrapli=True + ) self.hostname = hostname self.conn_mode = conn_mode self.num_nics = 128 @@ -97,9 +99,7 @@ def bootstrap_spin(self): except IndexError as exc: self.logger.error("no more credentials to try") return - self.logger.info( - "trying to log in with %s / %s" % (username, password) - ) + self.logger.info("trying to log in with %s / %s" % (username, password)) self.wait_write(username, wait=None) self.wait_write(password, wait="Password:") if self.xr_ready == True and ridx == 4: @@ -125,11 +125,12 @@ def bootstrap_spin(self): return - def apply_config(self): - + def apply_config(self): scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) - self.logger.info(f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)") - + self.logger.info( + f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)" + ) + # init scrapli xrv_scrapli_dev = { "host": "127.0.0.1", @@ -139,7 +140,7 @@ def apply_config(self): "timeout_transport": scrapli_timeout, "timeout_ops": scrapli_timeout, } - + xrv_config = f"""hostname {self.hostname} vrf clab-mgmt description Containerlab management VRF (DO NOT DELETE) @@ -181,25 +182,29 @@ def apply_config(self): con = IOSXRDriver(**xrv_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: xrv_config += config.read() else: self.logger.warning(f"User provided startup configuration is not found.") - + # configure SSH keys con.send_interactive( [ - ("crypto key generate rsa", "How many bits in the modulus [2048]", False), + ( + "crypto key generate rsa", + "How many bits in the modulus [2048]", + False, + ), ("2048", "#", True), ] ) res = con.send_configs(xrv_config.splitlines()) res += con.send_configs(["commit best-effort label CLAB_BOOTSTRAP", "end"]) - + for response in res: self.logger.info(f"CONFIG:{response.channel_input}") self.logger.info(f"RESULT:{response.result}") diff --git a/xrv9k/docker/launch.py b/xrv9k/docker/launch.py index 50a8096e..a4da5909 100755 --- a/xrv9k/docker/launch.py +++ b/xrv9k/docker/launch.py @@ -8,13 +8,13 @@ import sys import time - import vrnetlab from scrapli.driver.core import IOSXRDriver STARTUP_CONFIG_FILE = "/config/startup-config.cfg" DEFAULT_SCRAPLI_TIMEOUT = 900 + def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -41,12 +41,21 @@ def trace(self, message, *args, **kws): class XRv9k_vm(vrnetlab.VM): - def __init__(self, hostname, username, password, nics, conn_mode, vcpu, ram, install=False): + def __init__( + self, hostname, username, password, nics, conn_mode, vcpu, ram, install=False + ): disk_image = None for e in sorted(os.listdir("/")): if not disk_image and re.search(".qcow2", e): disk_image = "/" + e - super(XRv9k_vm, self).__init__(username, password, disk_image=disk_image, ram=ram, smp=f"cores={vcpu},threads=1,sockets=1", use_scrapli=True) + super(XRv9k_vm, self).__init__( + username, + password, + disk_image=disk_image, + ram=ram, + smp=f"cores={vcpu},threads=1,sockets=1", + use_scrapli=True, + ) self.hostname = hostname self.conn_mode = conn_mode self.num_nics = nics @@ -70,9 +79,9 @@ def __init__(self, hostname, username, password, nics, conn_mode, vcpu, ram, ins def gen_mgmt(self): """Generate qemu args for the mgmt interface(s)""" - + res = super().gen_mgmt() - + # dummy interface for xrv9k ctrl interface res.extend( [ @@ -115,7 +124,7 @@ def bootstrap_spin(self): b"XR partition preparation completed successfully", ], ) - + if match: # got a match! if ridx == 0: # press return to get started, so we press return! self.logger.info("got 'press return to get started...'") @@ -154,10 +163,11 @@ def bootstrap_spin(self): return def apply_config(self): - scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) - self.logger.info(f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)") - + self.logger.info( + f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)" + ) + # init scrapli xrv9k_scrapli_dev = { "host": "127.0.0.1", @@ -170,7 +180,7 @@ def apply_config(self): "timeout_transport": scrapli_timeout, "timeout_ops": scrapli_timeout, } - + xrv9k_config = f"""hostname {self.hostname} vrf clab-mgmt description Containerlab management VRF (DO NOT DELETE) @@ -206,20 +216,20 @@ def apply_config(self): xml agent tty root """ - + if os.path.exists(STARTUP_CONFIG_FILE): self.logger.info("Startup configuration file found") with open(STARTUP_CONFIG_FILE, "r") as config: xrv9k_config += config.read() else: self.logger.warning(f"User provided startup configuration is not found.") - + self.scrapli_tn.close() - + with IOSXRDriver(**xrv9k_scrapli_dev) as con: res = con.send_configs(xrv9k_config.splitlines()) res += con.send_configs(["commit best-effort label CLAB_BOOTSTRAP", "end"]) - + for response in res: self.logger.info(f"CONFIG:{response.channel_input}") self.logger.info(f"RESULT:{response.result}") @@ -232,17 +242,22 @@ def __init__(self, hostname, username, password, nics, conn_mode, vcpu, ram): class XRv9k_Installer(XRv9k): - """ XRv9k installer - Will start the XRv9k and then shut it down. Booting the XRv9k for the - first time requires the XRv9k itself to install internal packages - then it will restart. Subsequent boots will not require this restart. - By running this "install" when building the docker image we can - decrease the normal startup time of the XRv9k. + """XRv9k installer + Will start the XRv9k and then shut it down. Booting the XRv9k for the + first time requires the XRv9k itself to install internal packages + then it will restart. Subsequent boots will not require this restart. + By running this "install" when building the docker image we can + decrease the normal startup time of the XRv9k. """ + def __init__(self, hostname, username, password, nics, conn_mode, vcpu, ram): super(XRv9k, self).__init__(username, password) - self.vms = [XRv9k_vm(hostname, username, password, nics, conn_mode, vcpu, ram, install=True)] - + self.vms = [ + XRv9k_vm( + hostname, username, password, nics, conn_mode, vcpu, ram, install=True + ) + ] + def install(self): self.logger.info("Installing XRv9k") xrv = self.vms[0] @@ -262,7 +277,7 @@ def install(self): parser.add_argument("--username", default="vrnetlab", help="Username") parser.add_argument("--password", default="VR-netlab9", help="Password") parser.add_argument("--nics", type=int, default=128, help="Number of NICS") - parser.add_argument('--install', action="store_true", help="Pre-install image") + parser.add_argument("--install", action="store_true", help="Pre-install image") parser.add_argument( "--vcpu", type=int, default=4, help="Number of cpu cores to use" ) From dbe76ca1a778dd23e3fb1f95de50dd00bfbe8276 Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Mon, 13 Jan 2025 00:29:16 +0200 Subject: [PATCH 42/50] use single const for scrapli timeout --- c8000v/docker/launch.py | 1 - cat9kv/docker/launch.py | 1 - common/vrnetlab.py | 2 ++ n9kv/docker/launch.py | 6 ++---- nxos/docker/launch.py | 5 ++--- sros/docker/launch.py | 5 ++--- vios/docker/launch.py | 1 - xrv/docker/launch.py | 5 ++--- xrv9k/docker/launch.py | 5 ++--- 9 files changed, 12 insertions(+), 19 deletions(-) diff --git a/c8000v/docker/launch.py b/c8000v/docker/launch.py index 0bcfeed9..822d6437 100755 --- a/c8000v/docker/launch.py +++ b/c8000v/docker/launch.py @@ -11,7 +11,6 @@ import vrnetlab STARTUP_CONFIG_FILE = "/config/startup-config.cfg" -DEFAULT_SCRAPLI_TIMEOUT = 900 def handle_SIGCHLD(signal, frame): diff --git a/cat9kv/docker/launch.py b/cat9kv/docker/launch.py index 46789e5f..15478c62 100755 --- a/cat9kv/docker/launch.py +++ b/cat9kv/docker/launch.py @@ -11,7 +11,6 @@ import vrnetlab STARTUP_CONFIG_FILE = "/config/startup-config.cfg" -DEFAULT_SCRAPLI_TIMEOUT = 900 def handle_SIGCHLD(signal, frame): diff --git a/common/vrnetlab.py b/common/vrnetlab.py index e121139f..a8692587 100644 --- a/common/vrnetlab.py +++ b/common/vrnetlab.py @@ -21,6 +21,8 @@ MAX_RETRIES = 60 +DEFAULT_SCRAPLI_TIMEOUT = 900 + # set fancy logging colours logging.addLevelName( logging.INFO, f"\x1b[1;32m\t{logging.getLevelName(logging.INFO)}\x1b[0m" diff --git a/n9kv/docker/launch.py b/n9kv/docker/launch.py index 8ca462f0..58df6fa7 100755 --- a/n9kv/docker/launch.py +++ b/n9kv/docker/launch.py @@ -6,13 +6,11 @@ import re import signal import sys -import time import vrnetlab from scrapli.driver.core import NXOSDriver STARTUP_CONFIG_FILE = "/config/startup-config.cfg" -DEFAULT_SCRAPLI_TIMEOUT = 900 def handle_SIGCHLD(signal, frame): @@ -143,9 +141,9 @@ def bootstrap_spin(self): return def apply_config(self): - scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) + scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", vrnetlab.DEFAULT_SCRAPLI_TIMEOUT) self.logger.info( - f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)" + f"Scrapli timeout is {scrapli_timeout}s (default {vrnetlab.DEFAULT_SCRAPLI_TIMEOUT}s)" ) # init scrapli diff --git a/nxos/docker/launch.py b/nxos/docker/launch.py index 632f91de..1e48d2d9 100755 --- a/nxos/docker/launch.py +++ b/nxos/docker/launch.py @@ -12,7 +12,6 @@ from scrapli.driver.core import NXOSDriver STARTUP_CONFIG_FILE = "/config/startup-config.cfg" -DEFAULT_SCRAPLI_TIMEOUT = 900 def handle_SIGCHLD(signal, frame): @@ -104,9 +103,9 @@ def bootstrap_spin(self): return def apply_config(self): - scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) + scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", vrnetlab.DEFAULT_SCRAPLI_TIMEOUT) self.logger.info( - f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)" + f"Scrapli timeout is {scrapli_timeout}s (default {vrnetlab.DEFAULT_SCRAPLI_TIMEOUT}s)" ) # init scrapli diff --git a/sros/docker/launch.py b/sros/docker/launch.py index f35da2ca..a24c1ed0 100755 --- a/sros/docker/launch.py +++ b/sros/docker/launch.py @@ -13,7 +13,6 @@ import vrnetlab from scrapli import Scrapli -DEFAULT_SCRAPLI_TIMEOUT = 900 DEBUG_SCRAPLI = True if os.getenv("DEBUG_SCRAPLI", "false").lower() == "true" else False @@ -1253,9 +1252,9 @@ def bootstrap_config(self): # thus it must be applied unconditionally # init scrapli sros driver - scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) + scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", vrnetlab.DEFAULT_SCRAPLI_TIMEOUT) self.logger.info( - f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)" + f"Scrapli timeout is {scrapli_timeout}s (default {vrnetlab.DEFAULT_SCRAPLI_TIMEOUT}s)" ) # check if config was provided diff --git a/vios/docker/launch.py b/vios/docker/launch.py index 6b7d1ff1..f5e0a81d 100755 --- a/vios/docker/launch.py +++ b/vios/docker/launch.py @@ -10,7 +10,6 @@ from scrapli.driver.core import IOSXEDriver STARTUP_CONFIG_FILE = "/config/startup-config.cfg" -DEFAULT_SCRAPLI_TIMEOUT = 900 def handle_SIGCHLD(_signal, _frame): diff --git a/xrv/docker/launch.py b/xrv/docker/launch.py index 2e87f25f..dd9a1471 100755 --- a/xrv/docker/launch.py +++ b/xrv/docker/launch.py @@ -14,7 +14,6 @@ from scrapli.driver.core import IOSXRDriver STARTUP_CONFIG_FILE = "/config/startup-config.cfg" -DEFAULT_SCRAPLI_TIMEOUT = 900 def handle_SIGCHLD(signal, frame): @@ -126,9 +125,9 @@ def bootstrap_spin(self): return def apply_config(self): - scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) + scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", vrnetlab.DEFAULT_SCRAPLI_TIMEOUT) self.logger.info( - f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)" + f"Scrapli timeout is {scrapli_timeout}s (default {vrnetlab.DEFAULT_SCRAPLI_TIMEOUT}s)" ) # init scrapli diff --git a/xrv9k/docker/launch.py b/xrv9k/docker/launch.py index a4da5909..781ddbd8 100755 --- a/xrv9k/docker/launch.py +++ b/xrv9k/docker/launch.py @@ -12,7 +12,6 @@ from scrapli.driver.core import IOSXRDriver STARTUP_CONFIG_FILE = "/config/startup-config.cfg" -DEFAULT_SCRAPLI_TIMEOUT = 900 def handle_SIGCHLD(signal, frame): @@ -163,9 +162,9 @@ def bootstrap_spin(self): return def apply_config(self): - scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) + scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", vrnetlab.DEFAULT_SCRAPLI_TIMEOUT) self.logger.info( - f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)" + f"Scrapli timeout is {scrapli_timeout}s (default {vrnetlab.DEFAULT_SCRAPLI_TIMEOUT}s)" ) # init scrapli From 3da1b40cf38486663c0e755ea341ff0c13fa76a6 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Mon, 13 Jan 2025 18:00:05 +1300 Subject: [PATCH 43/50] Close the commandeered connection so the on close actions are run --- n9kv/docker/launch.py | 9 +++++---- nxos/docker/launch.py | 7 ++++--- vios/docker/launch.py | 11 ++++++----- xrv/docker/launch.py | 12 +++++------- xrv9k/docker/launch.py | 2 +- 5 files changed, 21 insertions(+), 20 deletions(-) diff --git a/n9kv/docker/launch.py b/n9kv/docker/launch.py index 58df6fa7..a88e1eed 100755 --- a/n9kv/docker/launch.py +++ b/n9kv/docker/launch.py @@ -53,7 +53,7 @@ def __init__(self, hostname, username, password, conn_mode): disk_image=disk_image, ram=10240, smp=4, - cpu="host,level=9", + cpu="host", use_scrapli=True, ) self.hostname = hostname @@ -120,8 +120,7 @@ def bootstrap_spin(self): # run main config! self.apply_config() - # close telnet connection - self.scrapli_tn.close() + # startup time? startup_time = datetime.datetime.now() - self.start_time self.logger.info("Startup complete in: %s" % startup_time) @@ -188,7 +187,7 @@ def apply_config(self): with open(STARTUP_CONFIG_FILE, "r") as config: n9kv_config += config.read() else: - self.logger.warning(f"User provided startup configuration is not found.") + self.logger.warning("User provided startup configuration is not found.") res = con.send_configs(n9kv_config.splitlines()) con.send_config("copy running-config startup-config") @@ -197,6 +196,8 @@ def apply_config(self): self.logger.info(f"CONFIG:{response.channel_input}") self.logger.info(f"RESULT:{response.result}") + con.close() + class N9KV(vrnetlab.VR): def __init__(self, hostname, username, password, conn_mode): diff --git a/nxos/docker/launch.py b/nxos/docker/launch.py index 1e48d2d9..9f778d01 100755 --- a/nxos/docker/launch.py +++ b/nxos/docker/launch.py @@ -82,8 +82,7 @@ def bootstrap_spin(self): # run main config! self.apply_config() - # close telnet connection - self.scrapli_tn.close() + # startup time? startup_time = datetime.datetime.now() - self.start_time self.logger.info("Startup complete in: %s" % startup_time) @@ -145,7 +144,7 @@ def apply_config(self): with open(STARTUP_CONFIG_FILE, "r") as config: nxos_config += config.read() else: - self.logger.warning(f"User provided startup configuration is not found.") + self.logger.warning("User provided startup configuration is not found.") res = con.send_configs(nxos_config.splitlines()) con.send_config("copy running-config startup-config") @@ -154,6 +153,8 @@ def apply_config(self): self.logger.info(f"CONFIG:{response.channel_input}") self.logger.info(f"RESULT:{response.result}") + con.close() + class NXOS(vrnetlab.VR): def __init__(self, hostname, username, password, conn_mode): diff --git a/vios/docker/launch.py b/vios/docker/launch.py index f5e0a81d..b89a0826 100755 --- a/vios/docker/launch.py +++ b/vios/docker/launch.py @@ -89,8 +89,6 @@ def bootstrap_spin(self): elif ridx == 2: self.apply_config() - # close telnet connection - self.scrapli_tn.close() # startup time startup_time = datetime.datetime.now() - self.start_time self.logger.info(f"Startup complete in: {startup_time}") @@ -109,9 +107,9 @@ def bootstrap_spin(self): return def apply_config(self): - scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", DEFAULT_SCRAPLI_TIMEOUT) + scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", vrnetlab.DEFAULT_SCRAPLI_TIMEOUT) self.logger.info( - f"Scrapli timeout is {scrapli_timeout}s (default {DEFAULT_SCRAPLI_TIMEOUT}s)" + f"Scrapli timeout is {scrapli_timeout}s (default {vrnetlab.DEFAULT_SCRAPLI_TIMEOUT}s)" ) # init scrapli @@ -183,7 +181,7 @@ def apply_config(self): with open(STARTUP_CONFIG_FILE, "r") as config: vios_config += config.read() else: - self.logger.warning(f"User provided startup configuration is not found.") + self.logger.warning("User provided startup configuration is not found.") res = con.send_configs(vios_config.splitlines()) res += con.send_commands(["write memory"]) @@ -192,6 +190,9 @@ def apply_config(self): 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): def __init__(self, hostname: str, username: str, password: str, conn_mode: str): diff --git a/xrv/docker/launch.py b/xrv/docker/launch.py index dd9a1471..a2a1cf78 100755 --- a/xrv/docker/launch.py +++ b/xrv/docker/launch.py @@ -3,11 +3,9 @@ import datetime import logging import os -import random import re import signal import sys -import telnetlib import time import vrnetlab @@ -95,17 +93,15 @@ def bootstrap_spin(self): self.logger.info("matched login prompt") try: username, password = self.credentials.pop(0) - except IndexError as exc: + except IndexError: self.logger.error("no more credentials to try") return self.logger.info("trying to log in with %s / %s" % (username, password)) self.wait_write(username, wait=None) self.wait_write(password, wait="Password:") - if self.xr_ready == True and ridx == 4: + if self.xr_ready and ridx == 4: # run main config! self.apply_config() - # close telnet connection - self.scrapli_tn.close() # startup time? startup_time = datetime.datetime.now() - self.start_time self.logger.info("Startup complete in: %s" % startup_time) @@ -187,7 +183,7 @@ def apply_config(self): with open(STARTUP_CONFIG_FILE, "r") as config: xrv_config += config.read() else: - self.logger.warning(f"User provided startup configuration is not found.") + self.logger.warning("User provided startup configuration is not found.") # configure SSH keys con.send_interactive( @@ -208,6 +204,8 @@ def apply_config(self): self.logger.info(f"CONFIG:{response.channel_input}") self.logger.info(f"RESULT:{response.result}") + con.close() + class XRV(vrnetlab.VR): def __init__(self, hostname, username, password, conn_mode): diff --git a/xrv9k/docker/launch.py b/xrv9k/docker/launch.py index 781ddbd8..2bd50097 100755 --- a/xrv9k/docker/launch.py +++ b/xrv9k/docker/launch.py @@ -221,7 +221,7 @@ def apply_config(self): with open(STARTUP_CONFIG_FILE, "r") as config: xrv9k_config += config.read() else: - self.logger.warning(f"User provided startup configuration is not found.") + self.logger.warning("User provided startup configuration is not found.") self.scrapli_tn.close() From cffd0a85fefd21ae50046d55fd4bb7fe78036b93 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Mon, 13 Jan 2025 18:00:29 +1300 Subject: [PATCH 44/50] Connection error log type from info->error --- common/vrnetlab.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/vrnetlab.py b/common/vrnetlab.py index a8692587..387bbc67 100644 --- a/common/vrnetlab.py +++ b/common/vrnetlab.py @@ -362,7 +362,7 @@ def start(self): self.qm = telnetlib.Telnet("127.0.0.1", 4000 + self.num) break except: - self.logger.info( + self.logger.error( "Unable to connect to qemu monitor (port {}), retrying in a second (attempt {})".format( 4000 + self.num, i ) @@ -383,7 +383,7 @@ def start(self): self.tn = telnetlib.Telnet("127.0.0.1", 5000 + self.num) break except: - self.logger.info( + self.logger.error( "Unable to connect to qemu monitor (port {}), retrying in a second (attempt {})".format( 5000 + self.num, i ) From 1f80210590638d9fae71a727e0d9fd0d2ed1d9c2 Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Mon, 13 Jan 2025 12:28:45 +0200 Subject: [PATCH 45/50] extracting image edit --- sros/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sros/README.md b/sros/README.md index 1a9b729f..b997b6c9 100644 --- a/sros/README.md +++ b/sros/README.md @@ -109,7 +109,7 @@ NOTE: If only CF2 is provisioned, node will remap it to CF1. ## Usage with containerlab -Refer to containerlab documentation piece on [vrnetlab integration](https://containerlab.srlinux.dev/manual/vrnetlab/) and vr-sros. +Refer to containerlab documentation article on [vrnetlab integration](https://containerlab.dev/manual/vrnetlab/) and Containerlab. ## Extracting qcow2 disk image from a container image @@ -118,7 +118,7 @@ It is possible to extract the original qcow2 disk image from an existing contain The following script takes an image name and the qcow2 image name to copy out from the container image: ```bash -IMAGE=registry.srlinux.dev/pub/vr-sros:23.7.R1 +IMAGE=registry.srlinux.dev/pub/nokia_sros:24.10.R1 VERSION=$(cut -d ':' -f 2 <<< $IMAGE) docker create --name sros-copy $IMAGE docker cp sros-copy:sros-vm-$VERSION.qcow2 . From f42e138f646c6fed76c1f70239c13e1665393d3d Mon Sep 17 00:00:00 2001 From: Roman Dodin Date: Mon, 13 Jan 2025 14:34:32 +0200 Subject: [PATCH 46/50] persist bof and config after bootstrap config is applied and close sros con regardless if config was provided or not --- sros/docker/launch.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sros/docker/launch.py b/sros/docker/launch.py index a24c1ed0..04a6d168 100755 --- a/sros/docker/launch.py +++ b/sros/docker/launch.py @@ -1290,8 +1290,6 @@ def bootstrap_config(self): self.log_scrapli_cmd_res(res) self.commitBofConfig() - # save bof config on disk - self.persistBofAndConfig() # apply common configuration if config file was not provided if not config_exists: @@ -1317,8 +1315,10 @@ def bootstrap_config(self): self.switchConfigEngine() - # close scrapli device driver - self.sros_con.close() + self.persistBofAndConfig() + + # close scrapli device driver + self.sros_con.close() @property def ram(self): From 98f679d91bcdd547e847566614ca0798ba9b2846 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Tue, 14 Jan 2025 23:37:30 +1300 Subject: [PATCH 47/50] Configure scrapli variant if startup config is classic If the startup-configuration provided is classic then the default configuration engine will be set to classic mode. In this case the scrapli device variant should also be set to classic so the scrapli magic can do it's thing with the correct prompt matching. --- sros/docker/launch.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sros/docker/launch.py b/sros/docker/launch.py index 04a6d168..d52e2c8e 100755 --- a/sros/docker/launch.py +++ b/sros/docker/launch.py @@ -1277,8 +1277,15 @@ def bootstrap_config(self): if SROS_VERSION.major <= 22 or SROS_VERSION.magc: sros_scrapli_dev["variant"] = "classic" + elif config_exists: + with open("/tftpboot/config.txt") as startup_cfg: + for line in startup_cfg: + l_clean = line.strip() + if "configure" == l_clean and "{" not in l_clean: + self.logger.debug("Detected classic startup configuration") + sros_scrapli_dev["variant"] = "classic" + break - # self.scrapli_logger.setLevel(logging.DEBUG) self.sros_con = Scrapli(**sros_scrapli_dev) self.sros_con.commandeer(conn=self.scrapli_tn) From c03c10f3d01622c55d0c4991f4805f62f12f1105 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Wed, 15 Jan 2025 01:59:30 +1300 Subject: [PATCH 48/50] Use a global var to determine when to send classic configs As classic startup configurations are now supported for MD-CLI defaulting versions, the classic CLI will mean the default config engine is classic on node boot. In this commit all logic that determined when to send/not send config for classic versions is now replaced with a single 'classic_cfg' global variable. Most of the logic across the code had repeated statements checking if the version was <= 22 or magc. 'classic_cfg' is set to True in this case. Else it is False. --- sros/docker/launch.py | 50 ++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/sros/docker/launch.py b/sros/docker/launch.py index d52e2c8e..82c94917 100755 --- a/sros/docker/launch.py +++ b/sros/docker/launch.py @@ -1090,7 +1090,7 @@ def configure_power(self, power_cfg): # power_path sets the configuration path to access power shelf and module # it is different for SR OS version <= 22 power_path = "chassis router chassis-number 1" - if SROS_VERSION.major <= 22 or SROS_VERSION.magc: + if classic_cfg: power_path = "system" for s in range(1, shelves + 1): @@ -1111,14 +1111,14 @@ def configure_power(self, power_cfg): self.log_scrapli_cmd_res(res) def enterConfig(self): - """Enter configuration mode. No-op for SR OS version <= 22""" - if SROS_VERSION.major <= 22 or SROS_VERSION.magc: + """Enter configuration mode. No-op for SR OS version <= 22 (classic CLI)""" + if classic_cfg: return self.sros_con.acquire_priv("configuration") def enterBofConfig(self): - """Enter bof configuration mode. No-op for SR OS version <= 22""" - if SROS_VERSION.major <= 22 or SROS_VERSION.magc: + """Enter bof configuration mode. No-op for SR OS version <= 22 (classic CLI)""" + if classic_cfg: return res = self.sros_con.send_commands( ["edit-config bof exclusive"], strip_prompt=False @@ -1126,15 +1126,15 @@ def enterBofConfig(self): self.log_scrapli_cmd_res(res) def commitConfig(self): - """Commit configuration. No-op for SR OS version <= 22""" - if SROS_VERSION.major <= 22 or SROS_VERSION.magc: + """Commit configuration. No-op for SR OS version <= 22 (classic CLI)""" + if classic_cfg: return res = self.sros_con.send_configs(["commit", "/"], strip_prompt=False) self.log_scrapli_cmd_res(res) def commitBofConfig(self): - """Commit configuration. No-op for SR OS version <= 22""" - if SROS_VERSION.major <= 22 or SROS_VERSION.magc: + """Commit configuration. No-op for SR OS version <= 22 (classic CLI)""" + if classic_cfg: return res = self.sros_con.send_configs(["commit", "/"], strip_prompt=False) self.log_scrapli_cmd_res(res) @@ -1160,7 +1160,7 @@ def persistBofAndConfig(self): """ "Persist bof and config""" if SROS_VERSION.magc: cmds = ["/bof save cf3:"] - elif SROS_VERSION.major <= 22: + elif classic_cfg: cmds = ["/bof save"] else: cmds = ["/admin save bof"] @@ -1171,7 +1171,7 @@ def persistBofAndConfig(self): def switchConfigEngine(self): """Switch configuration engine""" - if SROS_VERSION.major <= 22 or SROS_VERSION.magc: + if classic_cfg: # for SR OS version <= 22, we enforce MD-CLI by switching to it res = self.sros_con.send_configs( [ @@ -1187,7 +1187,7 @@ def gen_bof_config(self): if "DOCKER_NET_V4_ADDR" in os.environ and os.getenv("DOCKER_NET_V4_ADDR") != "": if self.mgmt_passthrough: # in pass-trough mode we configure static routes for the IPv4 private space - if SROS_VERSION.major >= 23 and not SROS_VERSION.magc: + if SROS_VERSION.major >= 23 and not classic_cfg: cmds.append( f"/bof router static-routes route 100.64.0.0/10 next-hop {self.mgmt_gw_ipv4}" ) @@ -1214,7 +1214,7 @@ def gen_bof_config(self): f"/bof static-route 192.168.0.0/16 next-hop {self.mgmt_gw_ipv4}" ) else: - if SROS_VERSION.major >= 23 and not SROS_VERSION.magc: + if SROS_VERSION.major >= 23 and not classic_cfg: cmds.append( f"/bof router static-routes route {os.getenv('DOCKER_NET_V4_ADDR')} next-hop {BRIDGE_V4_ADDR}" ) @@ -1224,7 +1224,7 @@ def gen_bof_config(self): ) if "DOCKER_NET_V6_ADDR" in os.environ and os.getenv("DOCKER_NET_V6_ADDR") != "": if not self.mgmt_passthrough: - if SROS_VERSION.major >= 23 and not SROS_VERSION.magc: + if SROS_VERSION.major >= 23 and not classic_cfg: cmds.append( f"/bof router static-routes route {os.getenv('DOCKER_NET_V6_ADDR')} next-hop {BRIDGE_V6_ADDR}" ) @@ -1274,18 +1274,24 @@ def bootstrap_config(self): "timeout_transport": scrapli_timeout, "timeout_ops": scrapli_timeout, } + + # SROS <= 22 use classic configuration mode by defaults + # other functions rely on this variable to determine what cmds to send + global classic_cfg + classic_cfg = True if SROS_VERSION.major <= 22 or SROS_VERSION.magc else False - if SROS_VERSION.major <= 22 or SROS_VERSION.magc: - sros_scrapli_dev["variant"] = "classic" - elif config_exists: + if config_exists: with open("/tftpboot/config.txt") as startup_cfg: for line in startup_cfg: l_clean = line.strip() if "configure" == l_clean and "{" not in l_clean: self.logger.debug("Detected classic startup configuration") - sros_scrapli_dev["variant"] = "classic" + classic_cfg = True break + if classic_cfg: + sros_scrapli_dev["variant"] = "classic" + self.sros_con = Scrapli(**sros_scrapli_dev) self.sros_con.commandeer(conn=self.scrapli_tn) @@ -1297,6 +1303,8 @@ def bootstrap_config(self): self.log_scrapli_cmd_res(res) self.commitBofConfig() + + self.persistBofAndConfig() # apply common configuration if config file was not provided if not config_exists: @@ -1322,8 +1330,6 @@ def bootstrap_config(self): self.switchConfigEngine() - self.persistBofAndConfig() - # close scrapli device driver self.sros_con.close() @@ -1805,7 +1811,7 @@ def getDefaultConfig() -> str: """Returns the default configuration for the system based on the SR OS version. SR OS >=23 uses model-driven configuration, while SR OS <=22 uses classic configuration. """ - if SROS_VERSION.major <= 22 or SROS_VERSION.magc: + if classic_cfg: return SROS_CL_COMMON_CFG + get_version_specific_config(SROS_VERSION.major) return SROS_MD_COMMON_CFG + get_version_specific_config(SROS_VERSION.major) @@ -1934,7 +1940,7 @@ def getDefaultConfig() -> str: # configure a temporary ip address so the tftp server can start. # modified later in the startup process in the create_tc_tap_mgmt_ifup function vrnetlab.run_command( - f"ip netns exec fakehost ip addr add 169.254.254.254/16 dev FA".split() + "ip netns exec fakehost ip addr add 169.254.254.254/16 dev FA".split() ) # block arp responses in fakehost namespace so it doesn't interfere with root namespace vrnetlab.run_command( From 286b7b9cb312710460f013e0b09c35f3d5ed5007 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Wed, 15 Jan 2025 17:00:55 +1300 Subject: [PATCH 49/50] Use explicit `quit-config` and move persistBofAndConfig back to end of bootstrap. --- sros/docker/launch.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sros/docker/launch.py b/sros/docker/launch.py index 82c94917..d817042e 100755 --- a/sros/docker/launch.py +++ b/sros/docker/launch.py @@ -1129,14 +1129,14 @@ def commitConfig(self): """Commit configuration. No-op for SR OS version <= 22 (classic CLI)""" if classic_cfg: return - res = self.sros_con.send_configs(["commit", "/"], strip_prompt=False) + res = self.sros_con.send_configs(["commit", "quit-config"], strip_prompt=False) self.log_scrapli_cmd_res(res) def commitBofConfig(self): """Commit configuration. No-op for SR OS version <= 22 (classic CLI)""" if classic_cfg: return - res = self.sros_con.send_configs(["commit", "/"], strip_prompt=False) + res = self.sros_con.send_configs(["commit", "quit-config"], strip_prompt=False) self.log_scrapli_cmd_res(res) def configureCards(self): @@ -1274,10 +1274,10 @@ def bootstrap_config(self): "timeout_transport": scrapli_timeout, "timeout_ops": scrapli_timeout, } - + # SROS <= 22 use classic configuration mode by defaults # other functions rely on this variable to determine what cmds to send - global classic_cfg + global classic_cfg classic_cfg = True if SROS_VERSION.major <= 22 or SROS_VERSION.magc else False if config_exists: @@ -1303,8 +1303,6 @@ def bootstrap_config(self): self.log_scrapli_cmd_res(res) self.commitBofConfig() - - self.persistBofAndConfig() # apply common configuration if config file was not provided if not config_exists: @@ -1328,6 +1326,8 @@ def bootstrap_config(self): self.commitConfig() + self.persistBofAndConfig() + self.switchConfigEngine() # close scrapli device driver From 5be1626af21361ca76ee649b1514cd5b090e2b47 Mon Sep 17 00:00:00 2001 From: Kaelem Chandra Date: Fri, 17 Jan 2025 14:06:58 +1300 Subject: [PATCH 50/50] Don't enforce MD-CLI on versions older than 19.x --- sros/docker/launch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sros/docker/launch.py b/sros/docker/launch.py index d817042e..3a0df016 100755 --- a/sros/docker/launch.py +++ b/sros/docker/launch.py @@ -1171,8 +1171,8 @@ def persistBofAndConfig(self): def switchConfigEngine(self): """Switch configuration engine""" - if classic_cfg: - # for SR OS version <= 22, we enforce MD-CLI by switching to it + if (SROS_VERSION.major >= 19 and SROS_VERSION.major <= 22) or SROS_VERSION.magc: + # for SR OS versions 19-22 inclusive, we enforce MD-CLI by switching to it res = self.sros_con.send_configs( [ f"/configure system management-interface configuration-mode {self.mode}"