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 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 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/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..d7352a01 100755 --- a/c8000v/docker/launch.py +++ b/c8000v/docker/launch.py @@ -11,7 +11,7 @@ import vrnetlab STARTUP_CONFIG_FILE = "/config/startup-config.cfg" - +DEFAULT_SCRAPLI_TIMEOUT = 900 def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -52,38 +52,114 @@ 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 self.num_nics = 9 self.nic_type = "virtio-net-pci" + self.image_name = "config.iso" if self.install_mode: - logger.trace("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") + 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: + 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) + + return 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 +! +""" + + 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", @@ -92,8 +168,9 @@ def create_boot_image(self): "/" + 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.""" @@ -103,30 +180,21 @@ def bootstrap_spin(self): self.stop() 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"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 self.install_mode: - self.logger.debug("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() - # close telnet connection - self.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 + 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 @@ -134,12 +202,12 @@ 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 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,85 +215,6 @@ def bootstrap_spin(self): return - def bootstrap_config(self): - """Do the actual bootstrap config""" - self.logger.info("applying bootstrap configuration") - - 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("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.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") - - class C8000v(vrnetlab.VR): def __init__(self, hostname, username, password, conn_mode): super(C8000v, self).__init__(username, password) @@ -240,17 +229,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") 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..e562efaa 100755 --- a/cat9kv/docker/launch.py +++ b/cat9kv/docker/launch.py @@ -11,7 +11,7 @@ import vrnetlab STARTUP_CONFIG_FILE = "/config/startup-config.cfg" - +DEFAULT_SCRAPLI_TIMEOUT = 900 def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -44,13 +44,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 +52,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 +85,51 @@ 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 +! +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 +! +""" + + 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(f"hostname {self.hostname}\r\n") - cfg_file.write("end\r\n") + cfg_file.write(cat9kv_config) genisoimage_args = [ "genisoimage", @@ -104,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.""" @@ -115,25 +151,17 @@ 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"CVAC-4-CONFIG_DONE", 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 ridx == 0: # configuration applied + self.logger.info("CVAC Configuration has been applied.") # 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 +174,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,80 +182,6 @@ def bootstrap_spin(self): return - def bootstrap_config(self): - """Do the actual bootstrap config""" - self.logger.info("applying bootstrap configuration") - - 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") - - 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") - - class cat9kv(vrnetlab.VR): def __init__(self, hostname, username, password, conn_mode, vcpu, ram): super(cat9kv, self).__init__(username, password) diff --git a/common/vrnetlab.py b/common/vrnetlab.py index b683e413..7f9e3c0a 100644 --- a/common/vrnetlab.py +++ b/common/vrnetlab.py @@ -12,9 +12,22 @@ import telnetlib import time from pathlib import Path +import sys + +try: + from scrapli import Driver +except ImportError: + pass 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 @@ -80,8 +93,52 @@ 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() + + """ + 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 +227,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 +272,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 = 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}") + self.start_time = datetime.datetime.now() cmd = list(self.qemu_args) @@ -263,10 +335,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 +359,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 +727,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 +744,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 +759,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() @@ -825,6 +1008,27 @@ 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: + 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.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.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""" @@ -852,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 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..29feaf1f 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,82 @@ 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 - - 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") + 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.") + + 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}") class CSR(vrnetlab.VR): 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..f51142f5 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") + + 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) 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..ee7dfcd5 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") + + for response in res: + self.logger.info(f"CONFIG:{response.channel_input}") + self.logger.info(f"RESULT:{response.result}") class NXOS(vrnetlab.VR): 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:") 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() 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..8a3eaac1 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,110 @@ 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()) + 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 VIOS(vrnetlab.VR): diff --git a/vrnetlab-base.dockerfile b/vrnetlab-base.dockerfile new file mode 100644 index 00000000..121c6344 --- /dev/null +++ b/vrnetlab-base.dockerfile @@ -0,0 +1,29 @@ +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 \ + 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 -e git+https://github.com/kaelemc/scrapli_community.git@sros_regex_fix#egg=scrapli_community --break-system-packages \ No newline at end of file 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 + + 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..955b6012 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") + def apply_config(self): - # 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 + 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 gNMI - self.wait_write("grpc port 57400") - self.wait_write("grpc vrf clab-mgmt") - self.wait_write("grpc no-tls") + # 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 +root +""" + + con = IOSXRDriver(**xrv_scrapli_dev) + con.commandeer(conn=self.scrapli_tn) - # configure xml agent - self.wait_write("xml agent tty") - - # 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 - - 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}") + 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.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()) + 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}") class XRV(vrnetlab.VR): @@ -275,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) 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..50a8096e 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 +root +""" - # 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()) + 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}") + - 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,