diff --git a/.gitignore b/.gitignore index 8ec320e9..43fbb2f1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,10 @@ .idea venv *.qcow2 +*.qcow cisco*.bin *.gz +*.tgz *.xz *.vmdk *.iso @@ -24,4 +26,7 @@ cisco*.bin *.xml # ignore clab- dirs -clab-* \ No newline at end of file +clab-* + +# Ignore container cidfile +**cidfile \ No newline at end of file diff --git a/aoscx/docker/Dockerfile b/aoscx/docker/Dockerfile index 751b31ea..ad81d289 100644 --- a/aoscx/docker/Dockerfile +++ b/aoscx/docker/Dockerfile @@ -1,25 +1,4 @@ -FROM debian:bullseye -MAINTAINER Stefano Sasso - -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update -qy \ - && apt-get upgrade -qy \ - && apt-get install -y \ - bridge-utils \ - iproute2 \ - python3-ipy \ - socat \ - tcpdump \ - ssh \ - inetutils-ping \ - dnsutils \ - iptables \ - telnet \ - ftp \ - qemu-system-x86=1:5.2+dfsg-11+deb11u2 \ - qemu-utils=1:5.2+dfsg-11+deb11u2 \ - && rm -rf /var/lib/apt/lists/* +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 ARG IMAGE COPY $IMAGE* / diff --git a/aoscx/docker/launch.py b/aoscx/docker/launch.py index 04f9a0ef..77dc60ee 100755 --- a/aoscx/docker/launch.py +++ b/aoscx/docker/launch.py @@ -64,7 +64,7 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect([b"switch login:"], 1) + (ridx, match, res) = self.expect([b"switch login:"], 1) if match: # got a match! if ridx == 0: # login self.logger.debug("trying to log in with 'admin'") @@ -92,8 +92,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 diff --git a/asav/Makefile b/asav/Makefile deleted file mode 100644 index ec25d49e..00000000 --- a/asav/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -VENDOR=Cisco -NAME=ASAv -IMAGE_FORMAT=qcow2 -IMAGE_GLOB=*.qcow2 - -# match versions like: -# asav9-18-2.qcow2 -VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+[^0-9]\([0-9]\+\-[0-9]\+\-[0-9]\+[a-z]\?\)\([^0-9].*\|$$\)/\1/') - --include ../makefile-sanity.include --include ../makefile.include --include ../makefile-install.include \ No newline at end of file diff --git a/asav/docker/Dockerfile b/asav/docker/Dockerfile deleted file mode 100644 index 720080c4..00000000 --- a/asav/docker/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -FROM ubuntu:20.04 -MAINTAINER Kristian Larsson - -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 \ - ssh \ - inetutils-ping \ - dnsutils \ - telnet \ - genisoimage \ - && rm -rf /var/lib/apt/lists/* - -ARG VERSION -ENV VERSION=${VERSION} -ARG IMAGE -COPY $IMAGE* / -COPY *.py / - -EXPOSE 22 161/udp 830 5000 10000-10099 -HEALTHCHECK CMD ["/healthcheck.py"] -ENTRYPOINT ["/launch.py"] 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 deleted file mode 100644 index 0550287f..00000000 --- a/c8000v/docker/Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -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/* - -ARG VERSION -ENV VERSION=${VERSION} -ARG IMAGE -COPY $IMAGE* / -COPY *.py / - -EXPOSE 22 161/udp 830 5000 10000-10099 -HEALTHCHECK CMD ["/healthcheck.py"] -ENTRYPOINT ["/launch.py"] diff --git a/cat9kv/docker/Dockerfile b/cat9kv/docker/Dockerfile deleted file mode 100644 index 335c7dd1..00000000 --- a/cat9kv/docker/Dockerfile +++ /dev/null @@ -1,31 +0,0 @@ -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/* - -ARG VERSION -ENV VERSION=${VERSION} -ARG IMAGE -COPY $IMAGE* / -COPY *.py / -# for vSwitch.xml file (specifies ASIC emulation parameters), won't throw error if vswitch.xml isn't present -COPY vswitch.xm[l] /img_dir/conf/ - -EXPOSE 22 161/udp 830 5000 10000-10099 -HEALTHCHECK CMD ["/healthcheck.py"] -ENTRYPOINT ["/launch.py"] diff --git a/cisco/asav/Makefile b/cisco/asav/Makefile new file mode 100644 index 00000000..721ab989 --- /dev/null +++ b/cisco/asav/Makefile @@ -0,0 +1,21 @@ +VENDOR=cisco +NAME=asav +IMAGE_FORMAT=qcow2 +IMAGE_GLOB=*.qcow2 +VENDOR_SUBDIR=1 + +# match versions like: +# asav9-18-2.qcow2 +VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+[^0-9]\([0-9]\+\-[0-9]\+\-[0-9]\+[a-z]\?\)\([^0-9].*\|$$\)/\1/') + +-include ../../makefile-sanity.include +-include ../../makefile.include + +docker-pre-build: + -cat cidfile | xargs --no-run-if-empty docker rm -f + -rm cidfile + +docker-build: docker-build-common + docker run --cidfile cidfile --privileged $(REGISTRY)$(VENDOR)_$(NAME):$(VERSION) --trace --install + docker commit --change='ENTRYPOINT ["/launch.py"]' $$(cat cidfile) $(REGISTRY)$(VENDOR)_$(NAME):$(VERSION) + docker rm -f $$(cat cidfile) \ No newline at end of file diff --git a/asav/README.md b/cisco/asav/README.md similarity index 100% rename from asav/README.md rename to cisco/asav/README.md diff --git a/cisco/asav/docker/Dockerfile b/cisco/asav/docker/Dockerfile new file mode 100755 index 00000000..2fab6fd7 --- /dev/null +++ b/cisco/asav/docker/Dockerfile @@ -0,0 +1,11 @@ +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 + +ARG VERSION +ENV VERSION=${VERSION} +ARG IMAGE +COPY $IMAGE* / +COPY *.py / + +EXPOSE 22 161/udp 830 5000 10000-10099 +HEALTHCHECK CMD ["/healthcheck.py"] +ENTRYPOINT ["/launch.py"] diff --git a/asav/docker/launch.py b/cisco/asav/docker/launch.py old mode 100644 new mode 100755 similarity index 79% rename from asav/docker/launch.py rename to cisco/asav/docker/launch.py index 309becfc..04903837 --- a/asav/docker/launch.py +++ b/cisco/asav/docker/launch.py @@ -11,6 +11,9 @@ import vrnetlab +DEFAULT_SMP = 4 +DEFAULT_RAM = 8192 # in MB + def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -37,16 +40,17 @@ def trace(self, message, *args, **kws): class ASAv_vm(vrnetlab.VM): - def __init__(self, username, password, install_mode=False): + def __init__(self, username, password, conn_mode, install_mode=False): for e in os.listdir("/"): if re.search(".qcow2$", e): disk_image = "/" + e super(ASAv_vm, self).__init__( - username, password, disk_image=disk_image, ram=2048 + username, password, disk_image=disk_image, ram=DEFAULT_RAM, smp=f"cores={DEFAULT_SMP},threads=1,sockets=1" ) self.nic_type = "e1000" self.install_mode = install_mode + self.conn_mode = conn_mode self.num_nics = 8 def bootstrap_spin(self): @@ -58,18 +62,18 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect([b"ciscoasa>"], 1) + (ridx, match, res) = self.expect([b"ciscoasa>"], 1) if match: # got a match! if ridx == 0: # login if self.install_mode: - self.logger.debug("matched, ciscoasa>") + self.logger.info("matched, ciscoasa>") self.wait_write("", wait=None) self.wait_write("", None) self.wait_write("", wait="ciscoasa>") self.running = True return - self.logger.debug("matched, ciscoasa>") + self.logger.info("matched, ciscoasa>") self.wait_write("", wait=None) # run main config! @@ -85,8 +89,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 @@ -103,8 +107,7 @@ def bootstrap_config(self): self.wait_write("VR-netlab9", wait="Repeat Password:") self.wait_write("", wait="ciscoasa#") self.wait_write("configure terminal", wait="#") - self.wait_write("N", wait="[Y]es, [N]o, [A]sk later:") - self.wait_write("", wait="(config)#") + self.wait_write("N", wait="[Y]es, [N]o, [A]sk later:", timeout=15) self.wait_write("aaa authentication ssh console LOCAL") self.wait_write("aaa authentication enable console LOCAL") self.wait_write( @@ -114,6 +117,7 @@ def bootstrap_config(self): self.wait_write("nameif management") self.wait_write("ip address 10.0.0.15 255.255.255.0") self.wait_write("no shutdown") + self.wait_write("route management 0.0.0.0 0.0.0.0 10.0.0.2") self.wait_write("ssh 0.0.0.0 0.0.0.0 management") self.wait_write("ssh version 2") self.wait_write("ssh key-exchange group dh-group14-sha256") @@ -124,17 +128,17 @@ def bootstrap_config(self): class ASAv(vrnetlab.VR): - def __init__(self, username, password): + def __init__(self, username, password, conn_mode): super(ASAv, self).__init__(username, password) - self.vms = [ASAv_vm(username, password)] + self.vms = [ASAv_vm(username, password, conn_mode)] class ASAv_installer(ASAv): """ASAv installer""" - def __init__(self, username, password): + def __init__(self, username, password, conn_mode): super(ASAv, self).__init__(username, password) - self.vms = [ASAv_vm(username, password, install_mode=True)] + self.vms = [ASAv_vm(username, password, conn_mode, install_mode=True)] def install(self): self.logger.info("Installing ASAv") @@ -156,6 +160,11 @@ def install(self): parser.add_argument("--username", default="vrnetlab", help="Username") parser.add_argument("--password", default="VR-netlab9", help="Password") parser.add_argument("--install", action="store_true", help="Install ASAv") + parser.add_argument( + "--connection-mode", + default="vrxcon", + help="Connection mode to use in the datapath", + ) args = parser.parse_args() LOG_FORMAT = "%(asctime)s: %(module)-10s %(levelname)-8s %(message)s" @@ -167,8 +176,8 @@ def install(self): logger.setLevel(1) if args.install: - vr = ASAv_installer(args.username, args.password) + vr = ASAv_installer(args.username, args.password, args.connection_mode) vr.install() else: - vr = ASAv(args.username, args.password) + vr = ASAv(args.username, args.password, args.connection_mode) vr.start() diff --git a/c8000v/Makefile b/cisco/c8000v/Makefile similarity index 74% rename from c8000v/Makefile rename to cisco/c8000v/Makefile index daa054bd..bf76d17e 100644 --- a/c8000v/Makefile +++ b/cisco/c8000v/Makefile @@ -2,14 +2,18 @@ VENDOR=cisco NAME=c8000v IMAGE_FORMAT=qcow2 IMAGE_GLOB=*.qcow2 +VENDOR_SUBDIR=1 # match versions like: # c8000v-17.11.01a.qcow2 VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+[^0-9]\([0-9]\+\.[0-9]\+\.[0-9]\+[a-z]\?\)\([^0-9].*\|$$\)/\1/') --include ../makefile-sanity.include --include ../makefile.include --include ../makefile-install.include +-include ../../makefile-sanity.include +-include ../../makefile.include + +docker-pre-build: + -cat cidfile | xargs --no-run-if-empty docker rm -f + -rm cidfile docker-build: docker-build-common docker run --cidfile cidfile --privileged $(REGISTRY)$(VENDOR)_$(NAME):$(VERSION) --trace --install diff --git a/c8000v/README.md b/cisco/c8000v/README.md similarity index 100% rename from c8000v/README.md rename to cisco/c8000v/README.md diff --git a/cisco/c8000v/docker/Dockerfile b/cisco/c8000v/docker/Dockerfile new file mode 100644 index 00000000..2fab6fd7 --- /dev/null +++ b/cisco/c8000v/docker/Dockerfile @@ -0,0 +1,11 @@ +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 + +ARG VERSION +ENV VERSION=${VERSION} +ARG IMAGE +COPY $IMAGE* / +COPY *.py / + +EXPOSE 22 161/udp 830 5000 10000-10099 +HEALTHCHECK CMD ["/healthcheck.py"] +ENTRYPOINT ["/launch.py"] diff --git a/c8000v/docker/launch.py b/cisco/c8000v/docker/launch.py similarity index 64% rename from c8000v/docker/launch.py rename to cisco/c8000v/docker/launch.py index 9c129b5c..fc855557 100755 --- a/c8000v/docker/launch.py +++ b/cisco/c8000v/docker/launch.py @@ -9,9 +9,14 @@ import sys import vrnetlab +from scrapli.driver.core import IOSXEDriver STARTUP_CONFIG_FILE = "/config/startup-config.cfg" +# scrapli timeouts in seconds +DEFAULT_SCRAPLI_TIMEOUT = 2700 +DEFAULT_VCPU = 1 +DEFAULT_RAM = 4096 # in MB def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -52,7 +57,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=DEFAULT_RAM, smp=f"cores={DEFAULT_VCPU},threads=1,sockets=1") self.install_mode = install_mode self.hostname = hostname self.conn_mode = conn_mode @@ -104,23 +109,22 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect( + (ridx, match, res) = self.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.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() - # close telnet connection - self.tn.close() + self.logger.info("Applying configuration") + + # apply bootstrap and startup configuration + self.apply_config() + # startup time? startup_time = datetime.datetime.now() - self.start_time self.logger.info("Startup complete in: %s", startup_time) @@ -134,89 +138,79 @@ def bootstrap_spin(self): self.running = True return else: - self.log.warning("Unexpected reload while running") + self.log.error("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.print(res) # reset spins if we saw some output self.spins = 0 self.spins += 1 return - - def bootstrap_config(self): - """Do the actual bootstrap config""" - self.logger.info("applying bootstrap configuration") - - 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") + + def apply_config(self): - self.wait_write("vrf definition clab-mgmt") - self.wait_write("address-family ipv4") - self.wait_write("exit") - self.wait_write("description Containerlab management VRF (DO NOT DELETE)") - self.wait_write("exit") - - self.wait_write("ip route vrf clab-mgmt 0.0.0.0 0.0.0.0 10.0.0.2") - - self.wait_write("interface GigabitEthernet1") - self.wait_write("vrf forwarding clab-mgmt") - self.wait_write("ip address 10.0.0.15 255.255.255.0") - 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") - + self.tn.close() + + scrapli_timeout = os.getenv('SCRAPLI_TIMEOUT', DEFAULT_SCRAPLI_TIMEOUT) + self.logger.info(f"Scrapli timeout is {scrapli_timeout} seconds. (Default: {DEFAULT_SCRAPLI_TIMEOUT})") + + # init scrapli + cat8k_scrapli_dev = { + "host": "127.0.0.1", + "port": 5000 + self.num, + "auth_bypass": True, + "auth_strict_key": False, + "transport": "telnet", + "timeout_socket": scrapli_timeout, + "timeout_transport": scrapli_timeout, + "timeout_ops": scrapli_timeout, + } + + cat8k_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 +vrf definition clab-mgmt +description Containerlab management VRF (DO NOT DELETE) +address-family ipv4 +exit +ip route vrf clab-mgmt 0.0.0.0 0.0.0.0 10.0.0.2 +interface GigabitEthernet 1 +description Containerlab management interface +vrf forwarding clab-mgmt +ip address 10.0.0.15 255.255.255.0 +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 +line vty 0 4 +login local +transport input all +""" + + with IOSXEDriver(**cat8k_scrapli_dev) as con: + con.send_config(cat8k_config) + + if not os.path.exists(STARTUP_CONFIG_FILE): + self.logger.warning(f"User provided startup configuration is not found.") + return + + self.logger.info("Startup configuration file found") + # send startup config + res = con.send_configs_from_file(STARTUP_CONFIG_FILE) + # print startup config and result + for response in res: + self.logger.info(f"CONFIG: {response.channel_input}") + self.logger.info(f"CONFIG RESULT: {response.result}") class C8000v(vrnetlab.VR): def __init__(self, hostname, username, password, conn_mode): @@ -239,10 +233,10 @@ def __init__(self, hostname, username, password, conn_mode): 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/Makefile b/cisco/cat9kv/Makefile similarity index 72% rename from cat9kv/Makefile rename to cisco/cat9kv/Makefile index 4e4fd6d5..efd68101 100644 --- a/cat9kv/Makefile +++ b/cisco/cat9kv/Makefile @@ -1,12 +1,13 @@ -VENDOR=Cisco +VENDOR=cisco NAME=cat9kv IMAGE_FORMAT=qcow2 IMAGE_GLOB=*.qcow2 +VENDOR_SUBDIR=1 # match versions like: # csr1000v-universalk9.16.03.01a.qcow2 # csr1000v-universalk9.16.04.01.qcow2 VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+[^0-9]\([0-9]\+\.[0-9]\+\.[0-9]\+[a-z]\?\)\([^0-9].*\|$$\)/\1/') --include ../makefile-sanity.include --include ../makefile.include \ No newline at end of file +-include ../../makefile-sanity.include +-include ../../makefile.include \ No newline at end of file diff --git a/cat9kv/README.md b/cisco/cat9kv/README.md similarity index 100% rename from cat9kv/README.md rename to cisco/cat9kv/README.md diff --git a/cisco/cat9kv/docker/Dockerfile b/cisco/cat9kv/docker/Dockerfile new file mode 100644 index 00000000..8f79f39d --- /dev/null +++ b/cisco/cat9kv/docker/Dockerfile @@ -0,0 +1,13 @@ +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 + +ARG VERSION +ENV VERSION=${VERSION} +ARG IMAGE +COPY $IMAGE* / +COPY *.py / +# for vSwitch.xml file (specifies ASIC emulation parameters), won't throw error if vswitch.xml isn't present +COPY vswitch.xm[l] /img_dir/conf/ + +EXPOSE 22 161/udp 830 5000 10000-10099 +HEALTHCHECK CMD ["/healthcheck.py"] +ENTRYPOINT ["/launch.py"] diff --git a/cat9kv/docker/launch.py b/cisco/cat9kv/docker/launch.py similarity index 55% rename from cat9kv/docker/launch.py rename to cisco/cat9kv/docker/launch.py index 56e00fcc..1c095000 100755 --- a/cat9kv/docker/launch.py +++ b/cisco/cat9kv/docker/launch.py @@ -9,9 +9,12 @@ import sys import vrnetlab +from scrapli.driver.core import IOSXEDriver STARTUP_CONFIG_FILE = "/config/startup-config.cfg" +DEFAULT_SMP = 4 +DEFAULT_RAM = 18432 # in MB def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -39,25 +42,18 @@ def trace(self, message, *args, **kws): class cat9kv_vm(vrnetlab.VM): - def __init__(self, hostname, username, password, conn_mode, vcpu, ram): + def __init__(self, hostname, username, password, conn_mode): disk_image = None 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, password, disk_image=disk_image, - smp=f"cores={vcpu},threads=1,sockets=1", - ram=ram, + smp=f"cores={DEFAULT_SMP},threads=1,sockets=1", + ram=DEFAULT_RAM, min_dp_nics=8, ) self.hostname = hostname @@ -89,7 +85,7 @@ def create_boot_image(self): try: os.popen("cp /vswitch.xml /img_dir/conf/") except: - self.logger.debug("No vswitch.xml file provided.") + self.logger.warning("No vswitch.xml file provided.") with open("/img_dir/iosxe_config.txt", "w") as cfg_file: cfg_file.write(f"hostname {self.hostname}\r\n") @@ -103,7 +99,7 @@ def create_boot_image(self): "/img_dir", ] - self.logger.debug("Generating boot ISO") + self.logger.info("Generating boot ISO") subprocess.Popen(genisoimage_args) def bootstrap_spin(self): @@ -115,7 +111,7 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect( + (ridx, match, res) = self.expect( [ b"Press RETURN to get started!", b"IOSXEBOOT-4-FACTORY_RESET", @@ -124,16 +120,15 @@ def bootstrap_spin(self): ) 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.") 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() + self.logger.info("Applying configuration") + + # apply bootstrap and startup configuration + self.apply_config() + # startup time? startup_time = datetime.datetime.now() - self.start_time self.logger.info("Startup complete in: %s", startup_time) @@ -141,12 +136,12 @@ def bootstrap_spin(self): self.running = True return elif ridx == 1: # IOSXEBOOT-4-FACTORY_RESET - self.logger.warning("Unexpected reload while running") + self.logger.error("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.print(res) # reset spins if we saw some output self.spins = 0 @@ -154,78 +149,71 @@ 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("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") - - # add mgmt vrf static route - self.wait_write("ip route vrf Mgmt-vrf 0.0.0.0 0.0.0.0 10.0.0.2") - - self.wait_write("interface GigabitEthernet0/0") - self.wait_write("ip address 10.0.0.15 255.255.255.0") - 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") - + def apply_config(self): + + self.tn.close() + + # init scrapli + cat9k_scrapli_dev = { + "host": "127.0.0.1", + "port": 5000 + self.num, + "auth_bypass": True, + "auth_strict_key": False, + "transport": "telnet", + "timeout_socket": 600, + "timeout_transport": 600, + "timeout_ops": 600, + } + + # bootstrap configuration + cat9k_config = f"""hostname {self.hostname} +username {self.username} privilege 15 password {self.password} +ip domain name example.com +no ip domain lookup + +ip route vrf Mgmt-vrf 0.0.0.0 0.0.0.0 10.0.0.2 + +interface GigabitEthernet 0/0 +description Containerlab management interface +ip address 10.0.0.15 255.255.255.0 +no shut +exit + +crypto key generate rsa modulus 2048 + +ip ssh version 2 +ip ssh server algorithm mac hmac-sha2-512 +ip ssh maxstartups 128 + +restconf +netconf-yang +netconf detailed-error +netconf max-sessions 16 + +line vty 0 4 +login local +transport input all +""" + + with IOSXEDriver(**cat9k_scrapli_dev) as con: + con.send_config(cat9k_config) + + if not os.path.exists(STARTUP_CONFIG_FILE): + self.logger.warning(f"User provided startup configuration is not found.") + return + + self.logger.info("Startup configuration file found") + # send startup config + res = con.send_configs_from_file(STARTUP_CONFIG_FILE) + # print startup config and result + for response in res: + self.logger.info(f"CONFIG: {response.channel_input}") + self.logger.info(f"CONFIG RESULT: {response.result}") class cat9kv(vrnetlab.VR): - def __init__(self, hostname, username, password, conn_mode, vcpu, ram): + def __init__(self, hostname, username, password, conn_mode): super(cat9kv, self).__init__(username, password) - self.vms = [cat9kv_vm(hostname, username, password, conn_mode, vcpu, ram)] + self.vms = [cat9kv_vm(hostname, username, password, conn_mode)] if __name__ == "__main__": @@ -261,7 +249,5 @@ def __init__(self, hostname, username, password, conn_mode, vcpu, ram): args.username, args.password, args.connection_mode, - args.vcpu, - args.ram, ) vr.start() diff --git a/csr/Makefile b/cisco/csr/Makefile similarity index 87% rename from csr/Makefile rename to cisco/csr/Makefile index 4096c798..b45f8806 100644 --- a/csr/Makefile +++ b/cisco/csr/Makefile @@ -2,14 +2,16 @@ VENDOR=cisco NAME=csr1000v IMAGE_FORMAT=qcow2 IMAGE_GLOB=*.qcow2 +VENDOR_SUBDIR=1 + # match versions like: # csr1000v-universalk9.16.03.01a.qcow2 # csr1000v-universalk9.16.04.01.qcow2 VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+[^0-9]\([0-9]\+\.[0-9]\+\.[0-9]\+[sb]\?\?\)\([^0-9].*\|$$\)/\1/') --include ../makefile-sanity.include --include ../makefile.include +-include ../../makefile-sanity.include +-include ../../makefile.include docker-pre-build: -cat cidfile | xargs --no-run-if-empty docker rm -f diff --git a/csr/README.md b/cisco/csr/README.md similarity index 100% rename from csr/README.md rename to cisco/csr/README.md diff --git a/cisco/csr/docker/Dockerfile b/cisco/csr/docker/Dockerfile new file mode 100644 index 00000000..2fab6fd7 --- /dev/null +++ b/cisco/csr/docker/Dockerfile @@ -0,0 +1,11 @@ +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 + +ARG VERSION +ENV VERSION=${VERSION} +ARG IMAGE +COPY $IMAGE* / +COPY *.py / + +EXPOSE 22 161/udp 830 5000 10000-10099 +HEALTHCHECK CMD ["/healthcheck.py"] +ENTRYPOINT ["/launch.py"] diff --git a/csr/docker/launch.py b/cisco/csr/docker/launch.py similarity index 67% rename from csr/docker/launch.py rename to cisco/csr/docker/launch.py index a8598f3f..58d4e550 100755 --- a/csr/docker/launch.py +++ b/cisco/csr/docker/launch.py @@ -10,9 +10,13 @@ import time import vrnetlab +from scrapli.driver.core import IOSXEDriver + STARTUP_CONFIG_FILE = "/config/startup-config.cfg" +DEFAULT_SMP = 1 +DEFAULT_RAM = 4096 # in MB def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -55,7 +59,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, ram=DEFAULT_RAM, smp=f"cores={DEFAULT_SMP},threads=1,sockets=1") self.install_mode = install_mode self.num_nics = nics @@ -69,11 +73,12 @@ def __init__( 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") + if self.license: cfg_file.write("do clock set 13:33:37 1 Jan 2010\r\n") cfg_file.write("interface GigabitEthernet1\r\n") @@ -109,22 +114,23 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect([b"Press RETURN to get started!"], 1) + (ridx, match, res) = self.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.logger.info("matched, Press RETURN to get started.") self.wait_write("", wait=None) + + self.logger.info("Applying configuration") + + # apply bootstrap and startup configuration + self.apply_config() - # run main config! - self.bootstrap_config() - self.startup_config() self.running = True - # 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) @@ -133,78 +139,73 @@ 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.print(res) # reset spins if we saw some output self.spins = 0 self.spins += 1 - return + + def apply_config(self): + + self.tn.close() + + # init scrapli + csr_scrapli_dev = { + "host": "127.0.0.1", + "port": 5000 + self.num, + "auth_bypass": True, + "auth_strict_key": False, + "transport": "telnet", + "timeout_socket": scrapli_timeout, + "timeout_transport": scrapli_timeout, + "timeout_ops": scrapli_timeout, + } + + csr_config = f"""hostname {self.hostname} +username {self.username} privilege 15 password {self.password} +ip domain name example.com +no ip domain lookup - def bootstrap_config(self): - """Do the actual bootstrap config""" - self.logger.info("applying bootstrap configuration") +vrf definition clab-mgmt +description Containerlab management VRF (DO NOT DELETE) +address-family ipv4 +exit - self.wait_write("", None) - self.wait_write("enable", wait=">") - self.wait_write("configure terminal", wait=">") +ip route vrf clab-mgmt 0.0.0.0 0.0.0.0 10.0.0.2 - 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("vrf definition clab-mgmt") - self.wait_write("address-family ipv4") - self.wait_write("exit") - self.wait_write("description Containerlab management VRF (DO NOT DELETE)") - self.wait_write("exit") - - self.wait_write("ip route vrf clab-mgmt 0.0.0.0 0.0.0.0 10.0.0.2") - - self.wait_write("interface GigabitEthernet1") - self.wait_write("vrf forwarding clab-mgmt") - self.wait_write("ip address 10.0.0.15 255.255.255.0") - 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 +interface GigabitEthernet 1 +description Containerlab management interface +vrf forwarding clab-mgmt +ip address 10.0.0.15 255.255.255.0 +no shut +exit - 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}") +crypto key generate rsa modulus 2048 - self.logger.info(f"Writing lines from {STARTUP_CONFIG_FILE}") +ip ssh version 2 - 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") +restconf +line vty 0 4 +login local +transport input all +""" + + with IOSXEDriver(**csr_scrapli_dev) as con: + con.send_config(csr_config) + + if not os.path.exists(STARTUP_CONFIG_FILE): + self.logger.warning(f"User provided startup configuration is not found.") + return + + self.logger.info("Startup configuration file found") + # send startup config + res = con.send_configs_from_file(STARTUP_CONFIG_FILE) + # print startup config and result + for response in res: + self.logger.info(f"CONFIG: {response.channel_input}") + self.logger.info(f"CONFIG RESULT: {response.result}") class CSR(vrnetlab.VR): def __init__(self, hostname, username, password, nics, conn_mode): @@ -265,9 +266,11 @@ def install(self): logging.basicConfig(format=LOG_FORMAT) logger = logging.getLogger() - logger.setLevel(logging.DEBUG) + # logger.setLevel(logging.DEBUG) if args.trace: logger.setLevel(1) + + scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", 3600) if args.install: vr = CSR_installer( diff --git a/cisco/ftdv/Makefile b/cisco/ftdv/Makefile new file mode 100644 index 00000000..d0cf74a4 --- /dev/null +++ b/cisco/ftdv/Makefile @@ -0,0 +1,21 @@ +VENDOR=cisco +NAME=ftdv +IMAGE_FORMAT=qcow2 +IMAGE_GLOB=*.qcow2 +VENDOR_SUBDIR=1 + +# match versions like: +# Cisco_Secure_Firewall_Threat_Defense_Virtual-7.2.5-208.qcow2 +VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+-\([0-9]\+\.[0-9]\+\.[0-9]\+\)-[0-9]\+.*/\1/') + +-include ../../makefile-sanity.include +-include ../../makefile.include + +docker-pre-build: + -cat cidfile | xargs --no-run-if-empty docker rm -f + -rm cidfile + +docker-build: docker-build-common + docker run --cidfile cidfile --privileged $(REGISTRY)$(VENDOR)_$(NAME):$(VERSION) --trace --install + docker commit --change='ENTRYPOINT ["/launch.py"]' $$(cat cidfile) $(REGISTRY)$(VENDOR)_$(NAME):$(VERSION) + docker rm -f $$(cat cidfile) diff --git a/ftdv/README.md b/cisco/ftdv/README.md similarity index 100% rename from ftdv/README.md rename to cisco/ftdv/README.md diff --git a/cisco/ftdv/docker/Dockerfile b/cisco/ftdv/docker/Dockerfile new file mode 100644 index 00000000..b2121cf7 --- /dev/null +++ b/cisco/ftdv/docker/Dockerfile @@ -0,0 +1,9 @@ +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 + +ARG IMAGE +COPY $IMAGE* / +COPY *.py / + +EXPOSE 22 80 161/udp 443 5000 8305 10000-10099 +HEALTHCHECK CMD ["/healthcheck.py"] +ENTRYPOINT ["/launch.py"] diff --git a/ftdv/docker/launch.py b/cisco/ftdv/docker/launch.py similarity index 93% rename from ftdv/docker/launch.py rename to cisco/ftdv/docker/launch.py index 631708b2..af6b07c8 100755 --- a/ftdv/docker/launch.py +++ b/cisco/ftdv/docker/launch.py @@ -11,6 +11,8 @@ import vrnetlab +DEFAULT_SMP = 4 +DEFAULT_RAM = 8192 # in MB def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -50,7 +52,7 @@ def __init__( self.license = False super(FTDV_vm, self).__init__( - username, password, disk_image=disk_image, ram=8192, smp="4,sockets=1,cores=4,threads=1" + username, password, disk_image=disk_image, ram=8192, smp=f"cores={DEFAULT_SMP},threads=1,sockets=1" ) self.login_ready = False @@ -136,19 +138,19 @@ def bootstrap_spin(self): self.start() return - (_, l_match, l_res) = self.tn.expect([b"INFO: Power-On Self-Test"], 1) - if l_match: - self.logger.debug("LOGIN READY") - self.login_ready = True - # no match, if we saw some output from the router it's probably - # booting, so let's give it some more time - elif l_res != b"": - self.logger.trace("OUTPUT: %s" % l_res.decode()) - # reset spins if we saw some output - self.spins = 0 - - if self.login_ready or not self.install_mode: - (ridx, match, res) = self.tn.expect([b"login:"], 1) + if not self.login_ready: + (_, l_match, l_res) = self.expect([b"INFO: Power-On Self-Test"], 1) + if l_match: + self.logger.info("LOGIN READY") + self.login_ready = True + # no match, if we saw some output from the router it's probably + # booting, so let's give it some more time + elif l_res != "": + self.print(l_res) + # reset spins if we saw some output + self.spins = 0 + elif self.login_ready or not self.install_mode: + (ridx, match, _) = self.expect([b"login:"], 3) if match: # got a match! if ridx == 0: # login if self.install_mode: @@ -169,7 +171,7 @@ def bootstrap_spin(self): self.running = True return - self.logger.debug("matched, login:") + self.logger.info("matched, login:") self.wait_write("", wait=None) self.bootstrap_config() diff --git a/cisco/iol/docker/Dockerfile b/cisco/iol/docker/Dockerfile index 8fbab4b2..78473dcc 100644 --- a/cisco/iol/docker/Dockerfile +++ b/cisco/iol/docker/Dockerfile @@ -1,20 +1,8 @@ -FROM public.ecr.aws/docker/library/debian:bookworm-slim - -# Install dependencies -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - iproute2 \ - iputils-ping \ - net-tools \ - sudo \ - curl \ - ca-certificates \ - gnupg \ - && apt-get clean +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 # Add containerlab gemfury for custom IOUYAP RUN echo "deb [trusted=yes] https://netdevops.fury.site/apt/ /" | \ -sudo tee -a /etc/apt/sources.list.d/netdevops.list +tee -a /etc/apt/sources.list.d/netdevops.list # Update and install IOUYAP RUN apt-get update && \ diff --git a/n9kv/Makefile b/cisco/n9kv/Makefile similarity index 78% rename from n9kv/Makefile rename to cisco/n9kv/Makefile index b673c695..4d35293d 100644 --- a/n9kv/Makefile +++ b/cisco/n9kv/Makefile @@ -1,7 +1,8 @@ -VENDOR=Cisco -NAME=NXOS 9000v +VENDOR=cisco +NAME=nxos9000v IMAGE_FORMAT=qcow2 IMAGE_GLOB=*.qcow2 +VENDOR_SUBDIR=1 # Match versions similar to the following: # - nxosv-final.7.0.3.I7.5a.qcow2 @@ -13,5 +14,5 @@ IMAGE_GLOB=*.qcow2 # - nexus9300v64.10.2.2.F.qcow VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+\?\.\(\(7\.0\.3\.I[0-9]\.[0-9a-z]\+\)\|\([0-9]\+\.[0-9]\+\.[0-9]\+\)\)\(\..*\|$$\)/\1/') --include ../makefile-sanity.include --include ../makefile.include +-include ../../makefile-sanity.include +-include ../../makefile.include diff --git a/n9kv/README.md b/cisco/n9kv/README.md similarity index 100% rename from n9kv/README.md rename to cisco/n9kv/README.md diff --git a/cisco/n9kv/docker/Dockerfile b/cisco/n9kv/docker/Dockerfile new file mode 100644 index 00000000..a72c601e --- /dev/null +++ b/cisco/n9kv/docker/Dockerfile @@ -0,0 +1,10 @@ +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 + +ARG IMAGE +COPY $IMAGE* / +COPY OVMF.fd / +COPY *.py / + +EXPOSE 22 80 161/udp 443 830 5000 6030 10000-10099 57400 +HEALTHCHECK CMD ["/healthcheck.py"] +ENTRYPOINT ["/launch.py"] diff --git a/n9kv/docker/OVMF.fd b/cisco/n9kv/docker/OVMF.fd similarity index 100% rename from n9kv/docker/OVMF.fd rename to cisco/n9kv/docker/OVMF.fd diff --git a/n9kv/docker/launch.py b/cisco/n9kv/docker/launch.py similarity index 89% rename from n9kv/docker/launch.py rename to cisco/n9kv/docker/launch.py index 1115f595..4643b5d4 100755 --- a/n9kv/docker/launch.py +++ b/cisco/n9kv/docker/launch.py @@ -12,6 +12,8 @@ STARTUP_CONFIG_FILE = "/config/startup-config.cfg" +DEFAULT_SMP = 4 +DEFAULT_RAM = 10240 # in MB def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -48,8 +50,8 @@ 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" + username, password, disk_image=disk_image, ram=DEFAULT_RAM, + smp=DEFAULT_SMP, cpu="host,level=9" ) self.hostname = hostname self.conn_mode = conn_mode @@ -88,10 +90,10 @@ 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.expect([b"\(yes\/skip\/no\)\[no\]:",b"\(yes\/no\)\[n\]:", b"\(yes\/no\)\[no\]:", b"login:"], 1) if match: # got a match! if ridx in (0, 1, 2): - self.logger.debug("matched poap prompt") + self.logger.info("matched poap prompt") self.wait_write("yes", wait=None) self.wait_write( "no", wait="Do you want to enforce secure password standard" @@ -102,8 +104,8 @@ def bootstrap_spin(self): "no", wait="Would you like to enter the basic configuration dialog" ) elif ridx == 3: # login - self.logger.debug("matched login prompt") - self.logger.debug(f'trying to log in with "admin" / {self.password}') + self.logger.info("matched login prompt") + self.logger.info(f'trying to log in with "admin" / {self.password}') self.wait_write("admin", wait=None) self.wait_write(self.password, wait="Password:") @@ -121,8 +123,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 @@ -164,14 +166,14 @@ 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") + self.logger.warning(f"Startup config file {STARTUP_CONFIG_FILE} is not found") return - self.logger.trace(f"Startup config file {STARTUP_CONFIG_FILE} exists") + self.logger.info(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"Parsed startup config file {STARTUP_CONFIG_FILE}") self.logger.info(f"Writing lines from {STARTUP_CONFIG_FILE}") @@ -215,7 +217,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/Makefile b/cisco/nxos/Makefile similarity index 100% rename from nxos/Makefile rename to cisco/nxos/Makefile diff --git a/nxos/README.md b/cisco/nxos/README.md similarity index 100% rename from nxos/README.md rename to cisco/nxos/README.md diff --git a/cisco/nxos/docker/Dockerfile b/cisco/nxos/docker/Dockerfile new file mode 100644 index 00000000..1527c896 --- /dev/null +++ b/cisco/nxos/docker/Dockerfile @@ -0,0 +1,9 @@ +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 + +ARG IMAGE +COPY $IMAGE* / +COPY *.py / + +EXPOSE 22 161/udp 830 5000 10000-10099 +HEALTHCHECK CMD ["/healthcheck.py"] +ENTRYPOINT ["/launch.py"] diff --git a/nxos/docker/launch.py b/cisco/nxos/docker/launch.py similarity index 95% rename from nxos/docker/launch.py rename to cisco/nxos/docker/launch.py index a06eefe9..a82ad078 100755 --- a/nxos/docker/launch.py +++ b/cisco/nxos/docker/launch.py @@ -12,6 +12,8 @@ STARTUP_CONFIG_FILE = "/config/startup-config.cfg" +DEFAULT_SMP = 2 +DEFAULT_RAM = 4096 # in MB def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -44,7 +46,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=DEFAULT_RAM, smp=DEFAULT_SMP ) self.credentials = [["admin", "admin"]] self.hostname = hostname @@ -59,7 +61,7 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect([b"login:"], 1) + (ridx, match, res) = self.expect([b"login:"], 1) if match: # got a match! if ridx == 0: # login self.logger.debug("matched login prompt") @@ -88,8 +90,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 diff --git a/vios/Makefile b/cisco/vios/Makefile similarity index 67% rename from vios/Makefile rename to cisco/vios/Makefile index 5a28b837..edc3b874 100644 --- a/vios/Makefile +++ b/cisco/vios/Makefile @@ -1,12 +1,13 @@ -VENDOR=Cisco -NAME=vIOS +VENDOR=cisco +NAME=vios IMAGE_FORMAT=qcow2 IMAGE_GLOB=*.qcow2 +VENDOR_SUBDIR=1 # Match images like: # - cisco_vios-158-3.M2.qcow2 # Extract version cisco_vios and qcow2, for example: 158-3.M2 VERSION=$(shell echo $(IMAGE) | sed -e 's/cisco_vios-\(.*\)\.qcow2/\1/') --include ../makefile-sanity.include --include ../makefile.include +-include ../../makefile-sanity.include +-include ../../makefile.include diff --git a/vios/README.md b/cisco/vios/README.md similarity index 100% rename from vios/README.md rename to cisco/vios/README.md diff --git a/cisco/vios/docker/Dockerfile b/cisco/vios/docker/Dockerfile new file mode 100644 index 00000000..6427f83f --- /dev/null +++ b/cisco/vios/docker/Dockerfile @@ -0,0 +1,9 @@ +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 + +ARG IMAGE +COPY $IMAGE* / +COPY *.py / + +EXPOSE 22 161/udp 5000 10000-10099 +HEALTHCHECK CMD ["/healthcheck.py"] +ENTRYPOINT ["/launch.py"] diff --git a/vios/docker/launch.py b/cisco/vios/docker/launch.py similarity index 94% rename from vios/docker/launch.py rename to cisco/vios/docker/launch.py index a7b70339..c05cdcdc 100755 --- a/vios/docker/launch.py +++ b/cisco/vios/docker/launch.py @@ -10,6 +10,8 @@ STARTUP_CONFIG_FILE = "/config/startup-config.cfg" +DEFAULT_VCPU = 1 +DEFAULT_RAM = 512 # in MB def handle_SIGCHLD(_signal, _frame): os.waitpid(-1, os.WNOHANG) @@ -49,8 +51,8 @@ def __init__(self, hostname: str, username: str, password: str, conn_mode: str): username=username, password=password, disk_image=disk_image, - smp="1", - ram=512, + smp=DEFAULT_VCPU, + ram=DEFAULT_RAM, driveif="virtio", ) @@ -68,7 +70,7 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect( + (ridx, match, res) = self.expect( [ rb"Would you like to enter the initial configuration dialog\? \[yes/no\]:", b"Press RETURN to get started!", @@ -101,8 +103,8 @@ 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(f"OUTPUT: {res.decode()}") + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 @@ -163,10 +165,10 @@ def _bootstrap_config(self): 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") + self.logger.warning(f"Startup config file {STARTUP_CONFIG_FILE} not found") return - self.logger.trace(f"Loading startup config file {STARTUP_CONFIG_FILE}") + self.logger.info(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) diff --git a/xrv/Makefile b/cisco/xrv/Makefile similarity index 76% rename from xrv/Makefile rename to cisco/xrv/Makefile index 59c24901..ac38953a 100644 --- a/xrv/Makefile +++ b/cisco/xrv/Makefile @@ -1,7 +1,8 @@ -VENDOR=Cisco -NAME=XRv +VENDOR=cisco +NAME=xrv IMAGE_FORMAT=vmdk IMAGE_GLOB=*vmdk* +VENDOR_SUBDIR=1 # match versions like: # iosxrv-k9-demo-5.3.3.51U.vmdk @@ -11,5 +12,5 @@ IMAGE_GLOB=*vmdk* # iosxrv-k9-demo-6.2.2.22I.vmdk VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+[^0-9]\([0-9]\+\.[0-9]\+\.[0-9]\+\(\.[0-9A-Z]\+\)\?\)\([^0-9].*\|$$\)/\1/') --include ../makefile-sanity.include --include ../makefile.include +-include ../../makefile-sanity.include +-include ../../makefile.include diff --git a/xrv/README.md b/cisco/xrv/README.md similarity index 91% rename from xrv/README.md rename to cisco/xrv/README.md index 68af8dc7..37374d79 100644 --- a/xrv/README.md +++ b/cisco/xrv/README.md @@ -1,6 +1,6 @@ # vrnetlab / Cisco IOS XRv -This is the vrnetlab docker image for Cisco IOS XRv. +This is the vrnetlab docker image for Cisco IOS XRv. This image uses the vmdk image, if you have the Qemu image use the 'xrv_qemu' directory. > Originally developed by Kristian Larsson (@plajjan), adapted by @hellt to be integrated with [containerlab](https://containerlab.srlinux.dev) networking. diff --git a/cisco/xrv/docker/Dockerfile b/cisco/xrv/docker/Dockerfile new file mode 100644 index 00000000..34aa6673 --- /dev/null +++ b/cisco/xrv/docker/Dockerfile @@ -0,0 +1,9 @@ +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 + +ARG IMAGE +COPY $IMAGE* / +COPY *.py / + +EXPOSE 22 161/udp 830 5000 10000-10099 57400 +HEALTHCHECK CMD ["/healthcheck.py"] +ENTRYPOINT ["/launch.py"] diff --git a/xrv/docker/launch.py b/cisco/xrv/docker/launch.py similarity index 79% rename from xrv/docker/launch.py rename to cisco/xrv/docker/launch.py index 7c0557cd..1725b911 100755 --- a/xrv/docker/launch.py +++ b/cisco/xrv/docker/launch.py @@ -14,6 +14,8 @@ STARTUP_CONFIG_FILE = "/config/startup-config.cfg" +DEFAULT_SMP = 2 +DEFAULT_RAM = 4096 def handle_SIGCHLD(signal, frame): os.waitpid(-1, os.WNOHANG) @@ -46,12 +48,12 @@ 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=DEFAULT_RAM, smp=f"cores={DEFAULT_SMP},threads=1,sockets=1" ) self.hostname = hostname self.conn_mode = conn_mode self.num_nics = 128 - self.credentials = [["admin", "admin"]] + self.credentials = [] self.xr_ready = False @@ -64,19 +66,19 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect( + (ridx, match, res) = self.expect( [ b"Press RETURN to get started", b"SYSTEM CONFIGURATION COMPLETE", b"Enter root-system username", b"Username:", - b"^[^ ]+#", + b"#", ], 1, ) 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( @@ -92,13 +94,13 @@ 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) @@ -118,8 +120,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 @@ -135,35 +137,12 @@ def bootstrap_config(self): 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) + + self.wait_write("2048", wait=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)) @@ -212,14 +191,14 @@ 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") + self.logger.warning(f"Startup config file {STARTUP_CONFIG_FILE} is not found") return - self.logger.trace(f"Startup config file {STARTUP_CONFIG_FILE} exists") + self.logger.info(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"Parsed startup config file {STARTUP_CONFIG_FILE}") self.logger.info(f"Writing lines from {STARTUP_CONFIG_FILE}") @@ -263,14 +242,7 @@ def __init__(self, hostname, username, password, conn_mode): if args.trace: logger.setLevel(1) - logger.debug( - "acting flags: username '{}', password '{}', connection-mode '{}'".format( - args.username, args.password, args.connection_mode - ) - ) - - logger.debug(f"Environment variables: {os.environ}") vrnetlab.boot_delay() vr = XRV(args.hostname, args.username, args.password, args.connection_mode) - vr.start() + vr.start() \ No newline at end of file diff --git a/xrv9k/LICENSE b/cisco/xrv9k/LICENSE similarity index 100% rename from xrv9k/LICENSE rename to cisco/xrv9k/LICENSE diff --git a/cisco/xrv9k/Makefile b/cisco/xrv9k/Makefile new file mode 100644 index 00000000..863702f1 --- /dev/null +++ b/cisco/xrv9k/Makefile @@ -0,0 +1,31 @@ +VENDOR=cisco +NAME=xrv9k +IMAGE_FORMAT=qcow2 +IMAGE_GLOB=*.qcow2 +INSTALL=true +VENDOR_SUBDIR=1 + +# match versions like: +# TODO: add example file names here +# xrv9k-fullk9-x.vrr-6.1.3.qcow2 +# xrv9k-fullk9-x.vrr-6.2.1.qcow2 +# xrv9k-fullk9-x-7.10.1.qcow2 +VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+[^0-9]\([0-9]\+\.[0-9]\+\.[0-9]\+\(\.[0-9A-Z]\+\)\?\)\([^0-9].*\|$$\)/\1/') + +-include ../../makefile-sanity.include +-include ../../makefile.include + +ifeq ($(INSTALL),false) +$(info Install mode disabled) +else +$(info Install mode enabled) +docker-pre-build: + -cat cidfile | xargs --no-run-if-empty docker rm -f + -rm cidfile + +docker-build: docker-build-common + docker run --cidfile cidfile --privileged $(REGISTRY)$(VENDOR)_$(NAME):$(VERSION) --trace --install + docker commit --change='ENTRYPOINT ["/launch.py"]' $$(cat cidfile) $(REGISTRY)$(VENDOR)_$(NAME):$(VERSION) + docker rm -f $$(cat cidfile) +endif + diff --git a/xrv9k/README.md b/cisco/xrv9k/README.md similarity index 100% rename from xrv9k/README.md rename to cisco/xrv9k/README.md diff --git a/cisco/xrv9k/docker/Dockerfile b/cisco/xrv9k/docker/Dockerfile new file mode 100644 index 00000000..8c6a929e --- /dev/null +++ b/cisco/xrv9k/docker/Dockerfile @@ -0,0 +1,9 @@ +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 + +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/cisco/xrv9k/docker/launch.py b/cisco/xrv9k/docker/launch.py new file mode 100755 index 00000000..f7b0aab5 --- /dev/null +++ b/cisco/xrv9k/docker/launch.py @@ -0,0 +1,353 @@ +#!/usr/bin/env python3 + +import datetime +import logging +import os +import re +import signal +import sys +import time + + +import vrnetlab +from scrapli.driver.core import IOSXRDriver + +STARTUP_CONFIG_FILE = "/config/startup-config.cfg" + +DEFAULT_SCRAPLI_TIMEOUT = 2700 +DEFAULT_SMP = 4 +DEFAULT_RAM = 16384 # in MB + +def handle_SIGCHLD(signal, frame): + os.waitpid(-1, os.WNOHANG) + + +def handle_SIGTERM(signal, frame): + sys.exit(0) + + +signal.signal(signal.SIGINT, handle_SIGTERM) +signal.signal(signal.SIGTERM, handle_SIGTERM) +signal.signal(signal.SIGCHLD, handle_SIGCHLD) + +TRACE_LEVEL_NUM = 9 +logging.addLevelName(TRACE_LEVEL_NUM, "TRACE") + + +def trace(self, message, *args, **kws): + # Yes, logger takes its '*args' as 'args'. + if self.isEnabledFor(TRACE_LEVEL_NUM): + self._log(TRACE_LEVEL_NUM, message, args, **kws) + + +logging.Logger.trace = trace + + +class XRv9k_vm(vrnetlab.VM): + def __init__(self, hostname, username, password, nics, conn_mode, 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=DEFAULT_RAM, smp=f"cores={DEFAULT_SMP},threads=1,sockets=1") + + self.hostname = hostname + self.conn_mode = conn_mode + self.num_nics = nics + self.install_mode = install + + self.qemu_args.extend( + [ + "-machine", + "smm=off", + "-boot", + "order=c", + "-cpu", + "qemu64,+ssse3,+sse4.1,+sse4.2", + "-serial", + "telnet:0.0.0.0:50%02d,server,nowait" % (self.num + 1), + "-serial", + "telnet:0.0.0.0:50%02d,server,nowait" % (self.num + 2), + "-serial", + "telnet:0.0.0.0:50%02d,server,nowait" % (self.num + 3), + ] + ) + self.credentials = [] + + def gen_mgmt(self): + """Generate qemu args for the mgmt interface(s)""" + res = [] + # mgmt interface + res.extend( + ["-device", "e1000,netdev=mgmt,mac=%s" % vrnetlab.gen_mac(0)] + ) + res.extend( + [ + "-netdev", + "user,id=mgmt,net=10.0.0.0/24," + "tftp=/tftpboot," + "hostfwd=tcp:0.0.0.0:22-10.0.0.15:22," + "hostfwd=udp:0.0.0.0:161-10.0.0.15:161," + "hostfwd=tcp:0.0.0.0:830-10.0.0.15:830," + "hostfwd=tcp:0.0.0.0:57400-10.0.0.15:57400" + ] + ) + # dummy interface for xrv9k ctrl interface + res.extend( + [ + "-device", + "e1000,netdev=ctrl-dummy,id=ctrl-dummy,mac=%s" + % vrnetlab.gen_mac(0), + "-netdev", + "tap,ifname=ctrl-dummy,id=ctrl-dummy,script=no,downscript=no", + ] + ) + # dummy interface for xrv9k dev interface + res.extend( + [ + "-device", + "e1000,netdev=dev-dummy,id=dev-dummy,mac=%s" + % vrnetlab.gen_mac(0), + "-netdev", + "tap,ifname=dev-dummy,id=dev-dummy,script=no,downscript=no", + ] + ) + + return res + + def bootstrap_spin(self): + + if self.spins > 600: + # too many spins with no result -> give up + self.logger.error( + "node is failing to boot or we can't catch the right prompt. Restarting..." + ) + self.stop() + self.start() + return + + (ridx, match, res) = self.expect( + [ + b"Press RETURN to get started", + b"Enter root-system [U|u]sername", + b"SDR\/XR partition preparation completed successfully", + ], + 1, + ) + + 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...'") + self.wait_write("", wait=None) + if ridx == 1: # system configuration complete + if self.install_mode: + return + self.logger.info("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]) + + self.logger.info("Applying configuration") + + # apply bootstrap and startup configuration + 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: + self.logger.info(f"Installation completed, took {datetime.datetime.now() - self.start_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.print(res) + # reset spins if we saw some output + self.spins = 0 + + self.spins += 1 + + return + + def apply_config(self): + + self.tn.close() + + scrapli_timeout = os.getenv('SCRAPLI_TIMEOUT', DEFAULT_SCRAPLI_TIMEOUT) + self.logger.info(f"Scrapli timeout is {scrapli_timeout} seconds. (Default: {DEFAULT_SCRAPLI_TIMEOUT})") + + # 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 +! +router static +vrf clab-mgmt +address-family ipv4 unicast +0.0.0.0/0 10.0.0.2 +! +ssh server v2 +ssh server vrf clab-mgmt +ssh server netconf port 830 +ssh server netconf vrf clab-mgmt +netconf-yang agent ssh +! +grpc port 57400 +grpc no-tls +! +xml agent tty +! +interface MgmtEth0/RP0/CPU0/0 +vrf clab-mgmt +no shutdown +ipv4 address 10.0.0.15/24 +! +commit +""" + + with IOSXRDriver(**xrv9k_scrapli_dev) as con: + con.send_config(xrv9k_config) + + if not os.path.exists(STARTUP_CONFIG_FILE): + self.logger.warning(f"User provided startup configuration is not found.") + return + + self.logger.info("Startup configuration file found") + + # need to append 'commit' to end of startup config file + startup_cfg = [] + + with open(STARTUP_CONFIG_FILE, 'r') as cfg: + for line in cfg: + # remove trailing \n from each line + startup_cfg.append(line.strip()) + startup_cfg.append("commit") + + # send startup config + res = con.send_configs(startup_cfg) + # print startup config and result + for response in res: + self.logger.info(f"CONFIG: {response.channel_input}") + self.logger.info(f"CONFIG RESULT: {response.result}") + + if res.failed: + self.logger.error(f"Failed to load startup configuration.") + return + + @property + def smp(self): + """ + Read SMP parameter (e.g. number of CPU cores) from the QEMU_SMP environment variable. + If the QEMU_SMP parameter is not set, the default value is used. + Should be provided as a number, e.g. 2 + """ + + if "QEMU_SMP" in os.environ: + qemu_smp = os.getenv("QEMU_SMP") + # if the user has supplied only a core count set the correct smp + if re.match('^[0-9]+$', str(qemu_smp)): + return str(f'cores={qemu_smp},threads=1,sockets=1') + else: + return str(qemu_smp) + + return str(self._smp) + +class XRv9k(vrnetlab.VR): + def __init__(self, hostname, username, password, nics, conn_mode): + super(XRv9k, self).__init__(username, password) + self.vms = [XRv9k_vm(hostname, username, password, nics, conn_mode)] + + +class XRv9k_Installer(XRv9k): + """ 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 + 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. + """ + def __init__(self, hostname, username, password, nics, conn_mode): + super(XRv9k, self).__init__(username, password) + self.vms = [XRv9k_vm(hostname, username, password, nics, conn_mode, install=True)] + + def install(self): + self.logger.info("Installing XRv9k") + xrv = self.vms[0] + while not xrv.running: + xrv.work() + time.sleep(2) + xrv.stop() + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="") + parser.add_argument( + "--trace", action="store_true", help="enable trace level logging" + ) + parser.add_argument("--hostname", default="vr-xrv9k", help="Router hostname") + 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( + "--vcpu", type=int, default=4, help="Number of cpu cores to use" + ) + parser.add_argument( + "--ram", type=int, default=16384, help="Number RAM to use in MB" + ) + parser.add_argument( + "--connection-mode", + default="vrxcon", + help="Connection mode to use in the datapath", + ) + args = parser.parse_args() + + LOG_FORMAT = "%(asctime)s: %(module)-10s %(levelname)-8s %(message)s" + logging.basicConfig(format=LOG_FORMAT) + logger = logging.getLogger() + + logger.setLevel(logging.DEBUG) + if args.trace: + logger.setLevel(1) + + vrnetlab.boot_delay() + + if args.install: + vr = XRv9k_Installer( + args.hostname, + args.username, + args.password, + args.nics, + args.connection_mode, + ) + vr.install() + else: + vr = XRv9k( + args.hostname, + args.username, + args.password, + args.nics, + args.connection_mode, + ) + vr.start() diff --git a/cisco/xrv_qcow/Makefile b/cisco/xrv_qcow/Makefile new file mode 100644 index 00000000..3bc33253 --- /dev/null +++ b/cisco/xrv_qcow/Makefile @@ -0,0 +1,12 @@ +VENDOR=cisco +NAME=xrv +IMAGE_FORMAT=qcow2 +IMAGE_GLOB=*qcow2 +VENDOR_SUBDIR=1 + +# match versions like: +# iosxrv-k9-demo-6.3.1.qcow2 +VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+[^0-9]\([0-9]\+\.[0-9]\+\.[0-9]\+\(\.[0-9A-Z]\+\)\?\)\([^0-9].*\|$$\)/\1/') + +-include ../../makefile-sanity.include +-include ../../makefile.include diff --git a/cisco/xrv_qcow/README.md b/cisco/xrv_qcow/README.md new file mode 100644 index 00000000..1d1f585b --- /dev/null +++ b/cisco/xrv_qcow/README.md @@ -0,0 +1,30 @@ +# vrnetlab / Cisco IOS XRv + +This is the vrnetlab docker image for Cisco IOS XRv. Using the QCOW2 image, rather than the VMDK. You can obtain the QCOW2 from CML/VIRL. +- If you have the vmdk image for XRv use the 'xrv' directory instead. + +> Originally developed by Kristian Larsson (@plajjan), adapted by @hellt to be integrated with [containerlab](https://containerlab.srlinux.dev) networking. + +There are two flavours of virtual XR routers, XRv and XRv9000 where the latter +has a much more complete forwarding plane. This is for XRv, if you have the +XRv9k see the 'xrv9k' directory instead. + +It's not recommended to run XRv with less than 4GB of RAM. I have experienced +weird issues when trying to use less RAM. + +## Added in this fork + +* integration with containerlab as [vr-vmx](https://containerlab.srlinux.dev/manual/kinds/vr-vmx/) kind. +* docker networking using `--connection-mode` flag +* hostname, username and password configuration via flags +* added support for [boot delay](https://containerlab.srlinux.dev/manual/vrnetlab/#boot-delay) to allow for a smooth start of the big topologies +* enabled gNMI +* fixes for auto image upgrade disrupted node config +* base image updated to Ubuntu:20.04 + +## Building the docker image + +Obtain XRv qcow2 image and put the .qcow2 file in this directory and run `make docker-image`. The resulting image is called `vrnetlab/vr-xrv`. You can tag it with something else if you want, like `my-repo.example.com/vr-xrv` and then +push it to your repo. The tag is the same as the version of the XRv image, so if you have iosxrv-k9-demo-6.3.1.qcow2 your final docker image will be called `vrnetlab/cisco_xrv:6.3.1` + + * 6.1.2 diff --git a/cisco/xrv_qcow/docker/Dockerfile b/cisco/xrv_qcow/docker/Dockerfile new file mode 100644 index 00000000..34aa6673 --- /dev/null +++ b/cisco/xrv_qcow/docker/Dockerfile @@ -0,0 +1,9 @@ +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 + +ARG IMAGE +COPY $IMAGE* / +COPY *.py / + +EXPOSE 22 161/udp 830 5000 10000-10099 57400 +HEALTHCHECK CMD ["/healthcheck.py"] +ENTRYPOINT ["/launch.py"] diff --git a/cisco/xrv_qcow/docker/launch.py b/cisco/xrv_qcow/docker/launch.py new file mode 100755 index 00000000..da3c1f4c --- /dev/null +++ b/cisco/xrv_qcow/docker/launch.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 + +import datetime +import logging +import os +import re +import signal +import sys +import time + +import vrnetlab + +STARTUP_CONFIG_FILE = "/config/startup-config.cfg" + +DEFAULT_SMP = 2 +DEFAULT_RAM = 4096 + +def handle_SIGCHLD(signal, frame): + os.waitpid(-1, os.WNOHANG) + + +def handle_SIGTERM(signal, frame): + sys.exit(0) + + +signal.signal(signal.SIGINT, handle_SIGTERM) +signal.signal(signal.SIGTERM, handle_SIGTERM) +signal.signal(signal.SIGCHLD, handle_SIGCHLD) + +TRACE_LEVEL_NUM = 9 +logging.addLevelName(TRACE_LEVEL_NUM, "TRACE") + + +def trace(self, message, *args, **kws): + # Yes, logger takes its '*args' as 'args'. + if self.isEnabledFor(TRACE_LEVEL_NUM): + self._log(TRACE_LEVEL_NUM, message, args, **kws) + + +logging.Logger.trace = trace + +class XRV_vm(vrnetlab.VM): + def __init__(self, hostname, username, password, conn_mode): + for e in os.listdir("/"): + if re.search(".qcow2", e): + disk_image = "/" + e + super(XRV_vm, self).__init__( + username, password, disk_image=disk_image, ram=DEFAULT_RAM, smp=f"cores={DEFAULT_SMP},threads=1,sockets=1" + ) + self.hostname = hostname + self.conn_mode = conn_mode + self.num_nics = 128 + + self.xr_ready = False + self.credentials = [] + + def bootstrap_spin(self): + """""" + + if self.spins > 300: + # too many spins with no result -> give up + self.stop() + self.start() + return + + (ridx, match, res) = self.expect( + [ + b"Press RETURN to get started", + b"SYSTEM CONFIGURATION COMPLETE", + b"Enter root-system username", + b"Username:", + b"#", + ], + 1, + ) + 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...'") + 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 + self.logger.info("Creating initial user") + time.sleep(15) + 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.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.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() + # 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 + + # 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.print(res) + # reset spins if we saw some output + self.spins = 0 + + self.spins += 1 + + 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") + + self.wait_write("2048", wait=None) + + # make sure we get our prompt back + self.wait_write("") + + 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("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("0.0.0.0/0 10.0.0.2") + 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 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("ipv4 address 10.0.0.15/24") + 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.warning(f"Startup config file {STARTUP_CONFIG_FILE} is not found") + return + + self.logger.info(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.info(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") + +class XRV(vrnetlab.VR): + def __init__(self, hostname, username, password, conn_mode): + super(XRV, self).__init__(username, password) + self.vms = [XRV_vm(hostname, username, password, conn_mode)] + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description="") + parser.add_argument("--hostname", default="cisco_xrv", help="Router hostname") + parser.add_argument( + "--trace", action="store_true", help="enable trace level logging" + ) + parser.add_argument("--username", default="clab", help="Username") + parser.add_argument("--password", default="clab@123", help="Password") + parser.add_argument( + "--connection-mode", + default="vrxcon", + help="Connection mode to use in the datapath", + ) + args = parser.parse_args() + + LOG_FORMAT = "%(asctime)s: %(module)-10s %(levelname)-8s %(message)s" + logging.basicConfig(format=LOG_FORMAT) + logger = logging.getLogger() + + logger.setLevel(logging.DEBUG) + if args.trace: + logger.setLevel(1) + + vrnetlab.boot_delay() + + vr = XRV(args.hostname, args.username, args.password, args.connection_mode) + vr.start() \ No newline at end of file diff --git a/cmglinux/docker/Dockerfile b/cmglinux/docker/Dockerfile index 103cb747..e80fbe2f 100644 --- a/cmglinux/docker/Dockerfile +++ b/cmglinux/docker/Dockerfile @@ -1,26 +1,6 @@ -FROM public.ecr.aws/docker/library/debian:bookworm-slim +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 -ARG DEBIAN_FRONTEND=noninteractive ARG DISK_SIZE=4G - -RUN apt-get update -qy \ - && apt-get install -y --no-install-recommends \ - bridge-utils \ - iproute2 \ - socat \ - qemu-kvm \ - tcpdump \ - ssh \ - inetutils-ping \ - dnsutils \ - iptables \ - nftables \ - telnet \ - genisoimage \ - python3-yaml \ - sshpass \ - && rm -rf /var/lib/apt/lists/* - ARG IMAGE COPY $IMAGE* / COPY *.py / diff --git a/cmglinux/docker/launch.py b/cmglinux/docker/launch.py index a7a17d82..757135d8 100755 --- a/cmglinux/docker/launch.py +++ b/cmglinux/docker/launch.py @@ -202,7 +202,7 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect([b"login: "], 1) + (ridx, match, res) = self.expect([b"login: "], 1) # got am match and login if match and ridx == 0: self.logger.debug("matched, login: ") @@ -218,8 +218,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 diff --git a/common/vrnetlab.py b/common/vrnetlab.py index 26ef22fb..40b0a042 100644 --- a/common/vrnetlab.py +++ b/common/vrnetlab.py @@ -8,10 +8,13 @@ import random import re import subprocess -import telnetlib import time +import sys from pathlib import Path +from scrapli import Driver +from scrapli.logging import enable_basic_logging + MAX_RETRIES = 60 @@ -79,7 +82,39 @@ def __init__( smp="1", min_dp_nics=0, ): - self.logger = logging.getLogger() + + # set fancy logging colours + logging.addLevelName( logging.INFO, f"\033[1;92m{logging.getLevelName(logging.INFO)}\033[1;0m") + logging.addLevelName( logging.WARN, f"\033[38;5;220m{logging.getLevelName(logging.WARN)}\033[1;0m") + logging.addLevelName( logging.DEBUG, f"\033[1;91m{logging.getLevelName(logging.DEBUG)}\033[1;0m") + logging.addLevelName( logging.ERROR, f"\033[1;91m{logging.getLevelName(logging.ERROR)}\033[1;0m") + logging.addLevelName( logging.CRITICAL, f"\033[1;91m{logging.getLevelName(logging.CRITICAL)}\033[1;0m") + + # set default logger for vrnetlab VM class + self.logger = logging.getLogger("vrnetlab") + self.logger.setLevel(logging.DEBUG) + + """ + Configure Scrapli logger to only be INFO level. + Scrapli uses 'scrapli' logger by default, and + will write all channel i/o as DEBUG logs. + """ + scrapli_logger = logging.getLogger("scrapli") + scrapli_logger.setLevel(logging.INFO) + + # init scrapli + self.scrapli_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.tn = Driver(**self.scrapli_dev) # username / password to configure self.username = username @@ -91,7 +126,6 @@ def __init__( self.running = False self.spins = 0 self.p = None - self.tn = None self._ram = ram self._cpu = cpu @@ -132,7 +166,7 @@ def __init__( overlay_disk_image = ".".join(tokens) if not os.path.exists(overlay_disk_image): - self.logger.debug("Creating overlay disk image") + self.logger.info("Creating overlay disk image") run_command( [ "qemu-img", @@ -176,7 +210,12 @@ def __init__( self.qemu_args.insert(1, "-enable-kvm") def start(self): - self.logger.info("Starting %s" % self.__class__.__name__) + + self.logger.info("ENVIRONMENT VARIABLES") + for var, value in os.environ.items(): + self.logger.info(f"{var}: {value}") + + self.logger.info(f"Launching {self.__class__.__name__} with {self.smp} SMP/VCPU and {self.ram} M of RAM.") self.start_time = datetime.datetime.now() cmd = list(self.qemu_args) @@ -207,8 +246,8 @@ def start(self): # generate dummy NICs if self.insuffucient_nics: cmd.extend(self.gen_dummy_nics()) - - self.logger.debug("qemu cmd: {}".format(" ".join(cmd))) + + self.logger.info("qemu cmd: {}".format(" ".join(cmd))) self.p = subprocess.Popen( " ".join(cmd), @@ -228,30 +267,12 @@ def start(self): for i in range(1, MAX_RETRIES + 1): try: - self.qm = telnetlib.Telnet("127.0.0.1", 4000 + self.num) + self.tn.open() break - except: - self.logger.info( - "Unable to connect to qemu monitor (port {}), retrying in a second (attempt {})".format( - 4000 + self.num, i - ) - ) - time.sleep(1) - if i == MAX_RETRIES: - raise QemuBroken( - "Unable to connect to qemu monitor on port {}".format( - 4000 + self.num - ) - ) - - for i in range(1, MAX_RETRIES + 1): - try: - self.tn = telnetlib.Telnet("127.0.0.1", 5000 + self.num) - break - except: - self.logger.info( - "Unable to connect to qemu monitor (port {}), retrying in a second (attempt {})".format( - 5000 + self.num, i + except Exception as e: + self.logger.error( + "Unable to connect to qemu monitor (port {}), retrying in a second (attempt {})\nError: {}".format( + 5000 + self.num, i, e ) ) time.sleep(1) @@ -322,7 +343,7 @@ def gen_mgmt(self): return res def nic_provision_delay(self) -> None: - self.logger.debug( + self.logger.info( f"number of provisioned data plane interfaces is {self.num_provisioned_nics}" ) @@ -333,7 +354,7 @@ def nic_provision_delay(self) -> None: self.insuffucient_nics = True return - self.logger.debug("waiting for provisioned interfaces to appear...") + self.logger.info("waiting for provisioned interfaces to appear...") # start_eth means eth index for VM # particularly for multiple slot LC @@ -356,10 +377,10 @@ def nic_provision_delay(self) -> None: if nics: self.highest_provisioned_nic_num = max(nics) - self.logger.debug( + self.logger.info( f"highest allocated interface id determined to be: {self.highest_provisioned_nic_num}..." ) - self.logger.debug("interfaces provisioned, continuing...") + self.logger.info("interfaces provisioned, continuing...") break time.sleep(5) @@ -372,7 +393,7 @@ def gen_dummy_nics(self): # calculate required num of nics to generate nics = self.min_nics - self.num_provisioned_nics - self.logger.debug(f"Insuffucient NICs defined. Generating {nics} dummy nics") + self.logger.warning(f"Insuffucient NICs defined. Generating {nics} dummy nics") res = [] @@ -496,55 +517,121 @@ def restart(self): self.stop() self.start() - def wait_write(self, cmd, wait="__defaultpattern__", con=None, clean_buffer=False): - """Wait for something on the serial port and then send command - - Defaults to using self.tn as connection but this can be overridden - by passing a telnetlib.Telnet object in the con argument. + def work(self): + self.check_qemu() + if not self.running: + try: + self.bootstrap_spin() + except EOFError: + self.logger.error("Telnet session was disconnected, restarting") + self.restart() + + def read_until(self, match_str, timeout=None): + """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) """ - con_name = "custom con" - if con is None: - con = self.tn + buf = b"" + + if timeout: + t_end = time.time() + timeout + + while True: + current_buf = self.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.print(current_buf) + + if match: + break + if timeout and time.time() > t_end: + break + + return buf - if con == self.tn: - con_name = "serial console" - if con == self.qm: - con_name = "qemu monitor" + def wait_write(self, cmd, wait="__defaultpattern__", con=None, clean_buffer=None, timeout=None): + """ + Wait for something on the serial port and then send command + + Arguments are: + - cmd: command to send (string) + - wait: prompt to wait for before sending command, defaults to # (string) + - timeout: if prompt is not found after x amounts of seconds, send command anyways. Defaults to None (float) + """ + if con is not None: + raise ValueError("wait_write: con argument is no longer supported. Please raise GitHub issue on hellt/vrnetlab, or report in containerlab discord.") + if clean_buffer is not None: + raise ValueError("wait_write: clean_buffer argument is no longer supported. Please raise GitHub issue on hellt/vrnetlab, or report in containerlab discord.") if wait: # 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}") - res = con.read_until(wait.encode()) - - cleaned_buf = ( - (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()}'") - # log the cleaned buffer if it's not empty - if cleaned_buf: - self.logger.trace(f"cleaned buffer: '{cleaned_buf.decode()}'") + + self.print(b"\n") + self.logger.info(f"waiting for '{wait}' on console.") + + self.read_until(wait, timeout) + + time.sleep(0.1) # don't write to the console too fast + + self.print(b"\n") + self.logger.info(f"Writing to console: '{cmd}'") + self.tn.channel.write(f"{cmd}\r") + + def expect(self, regex_list, timeout=None): + """Wait for something on the serial port. + + Takes list of strings and timeout as arguments. + + Returns tuple of: + - index of matched object from regex. + - match object. + - buffer of cosole read until match, or function exit. + """ + buf = self.tn.channel.read() + + if timeout: + t_end = time.time() + timeout + + for i, obj in enumerate(regex_list): + match = re.search(obj.decode(), buf.decode()) + if match: + return i, match, buf + if timeout and time.time() > t_end: + break + + return -1, None, buf - self.logger.debug(f"writing to {con_name}: '{cmd}'") - con.write("{}\r".format(cmd).encode()) + def print(self, bytes): + """ + Quick and dirty way to write to stdout (docker logs) instead of + using the python logger which poorly formats the output. + + Useful for printing console to docker logs + """ + sys.stdout.buffer.write(bytes) + sys.stdout.buffer.flush() - def work(self): - self.check_qemu() - if not self.running: - try: - self.bootstrap_spin() - except EOFError: - self.logger.error("Telnet session was disconnected, restarting") - self.restart() def check_qemu(self): """Check health of qemu. This is mostly just seeing if there's error output on STDOUT from qemu which means we restart it. """ if self.p is None: - self.logger.debug("VM not started; starting!") + self.logger.warning("VM not started; starting!") self.start() # check for output @@ -556,7 +643,7 @@ def check_qemu(self): self.logger.info("STDERR: %s" % errs) if errs != "": - self.logger.debug("KVM error, restarting") + self.logger.error("KVM error, restarting") self.stop() self.start() @@ -640,8 +727,7 @@ def update_health(self, exit_status, message): def start(self): """Start the virtual router""" - self.logger.debug("Starting vrnetlab %s" % self.__class__.__name__) - self.logger.debug("VMs: %s" % self.vms) + self.logger.info("VMs: %s" % self.vms) started = False while True: diff --git a/csr/docker/Dockerfile b/csr/docker/Dockerfile deleted file mode 100644 index 7606384d..00000000 --- a/csr/docker/Dockerfile +++ /dev/null @@ -1,31 +0,0 @@ -FROM ubuntu:20.04 -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/* - -ARG VERSION -ENV VERSION=${VERSION} -ARG IMAGE -COPY $IMAGE* / -COPY *.py / - -EXPOSE 22 161/udp 830 5000 10000-10099 -HEALTHCHECK CMD ["/healthcheck.py"] -ENTRYPOINT ["/launch.py"] diff --git a/dell_sonic/docker/Dockerfile b/dell_sonic/docker/Dockerfile index 44c51ee9..fb02eed1 100644 --- a/dell_sonic/docker/Dockerfile +++ b/dell_sonic/docker/Dockerfile @@ -1,18 +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 \ - python3-ipy \ - qemu-kvm \ - qemu-utils \ - socat \ - ssh \ - sshpass \ - && rm -rf /var/lib/apt/lists/* +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 ARG IMAGE COPY $IMAGE* / diff --git a/dell_sonic/docker/launch.py b/dell_sonic/docker/launch.py index a5151e48..1211e2d2 100755 --- a/dell_sonic/docker/launch.py +++ b/dell_sonic/docker/launch.py @@ -65,7 +65,7 @@ def bootstrap_spin(self): self.start() return - ridx, match, res = self.tn.expect([b"login:"], 1) + ridx, match, res = self.expect([b"login:"], 1) if match and ridx == 0: # login self.logger.info("VM started") @@ -90,8 +90,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 diff --git a/fortigate/docker/Dockerfile b/fortigate/docker/Dockerfile index e04fbecf..42646fda 100644 --- a/fortigate/docker/Dockerfile +++ b/fortigate/docker/Dockerfile @@ -1,31 +1,6 @@ -FROM debian:bookworm-slim - -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 \ - tftpd-hpa \ - ssh \ - inetutils-ping \ - dnsutils \ - openvswitch-switch \ - iptables \ - telnet \ - genisoimage \ - dos2unix \ - vim \ - curl \ - && rm -rf /var/lib/apt/lists/* +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 ARG IMAGE - - COPY *.py / COPY $IMAGE / EXPOSE 22 161/udp 830 5000 10000-10099 3443 80 443 diff --git a/fortigate/docker/launch.py b/fortigate/docker/launch.py index f420e2d6..6cd53d76 100755 --- a/fortigate/docker/launch.py +++ b/fortigate/docker/launch.py @@ -86,7 +86,7 @@ def bootstrap_spin(self): self.spins = 0 return - (ridx, match, res) = self.tn.expect([b"login:", b"FortiGate-VM64-KVM #"], 1) + (ridx, match, res) = self.expect([b"login:", b"FortiGate-VM64-KVM #"], 1) if match: # got a match! if ridx == 0: # matched login prompt, so should login self.logger.debug("ridx == 0") @@ -115,8 +115,8 @@ def bootstrap_spin(self): else: # 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 FORTIGATE: {res.decode()}") + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 @@ -131,8 +131,8 @@ def _wait_reset(self): self.logger.debug("waiting for reset") wait_spins = 0 while wait_spins < 90: - _, match, data = self.tn.expect([b"login: "], timeout=10) - self.logger.trace(data.decode("UTF-8")) + _, match, data = self.expect([b"login: "], timeout=10) + self.print(data) if match: self.logger.debug("reset finished") return True diff --git a/freebsd/docker/Dockerfile b/freebsd/docker/Dockerfile index 75bed9cb..820c70f2 100644 --- a/freebsd/docker/Dockerfile +++ b/freebsd/docker/Dockerfile @@ -1,27 +1,6 @@ -FROM debian:bookworm-slim +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 -ARG DEBIAN_FRONTEND=noninteractive ARG DISK_SIZE=4G - -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 \ - iptables \ - nftables \ - telnet \ - cloud-utils \ - sshpass \ - && rm -rf /var/lib/apt/lists/* - ARG IMAGE COPY $IMAGE* / COPY *.py / diff --git a/freebsd/docker/launch.py b/freebsd/docker/launch.py index 6cfd180d..10b436fb 100755 --- a/freebsd/docker/launch.py +++ b/freebsd/docker/launch.py @@ -129,7 +129,7 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect([b"login: "], 1) + (ridx, match, res) = self.expect([b"login: "], 1) if match: # got a match! if ridx == 0: # login @@ -148,8 +148,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 diff --git a/ftdv/Makefile b/ftdv/Makefile deleted file mode 100644 index db28850c..00000000 --- a/ftdv/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -VENDOR=Cisco -NAME=FTDv -IMAGE_FORMAT=qcow2 -IMAGE_GLOB=*.qcow2 - -# match versions like: -# Cisco_Secure_Firewall_Threat_Defense_Virtual-7.2.5-208.qcow2 -VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+-\([0-9]\+\.[0-9]\+\.[0-9]\+\)-[0-9]\+.*/\1/') - --include ../makefile-sanity.include --include ../makefile.include --include ../makefile-install.include diff --git a/ftdv/docker/Dockerfile b/ftdv/docker/Dockerfile deleted file mode 100644 index 5d448346..00000000 --- a/ftdv/docker/Dockerfile +++ /dev/null @@ -1,29 +0,0 @@ -FROM debian:bookworm-slim - -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 \ - iptables \ - nftables \ - telnet \ - genisoimage \ - && rm -rf /var/lib/apt/lists/* - -ARG IMAGE -COPY $IMAGE* / -COPY *.py / - -EXPOSE 22 80 161/udp 443 5000 8305 10000-10099 -HEALTHCHECK CMD ["/healthcheck.py"] -ENTRYPOINT ["/launch.py"] diff --git a/ftosv/docker/Dockerfile b/ftosv/docker/Dockerfile index 7e407275..4819516e 100644 --- a/ftosv/docker/Dockerfile +++ b/ftosv/docker/Dockerfile @@ -1,24 +1,4 @@ -FROM public.ecr.aws/docker/library/debian:bookworm-slim -LABEL maintainer="Kristian Larsson " -LABEL maintainer="Roman Dodin " -LABEL maintainer="Stefano Sasso " - -ARG DEBIAN_FRONTEND=noninteractive -RUN apt-get update -qy \ - && apt-get install --no-install-recommends -y \ - dosfstools \ - bridge-utils \ - iproute2 \ - python3 \ - socat \ - ssh \ - tcpdump \ - qemu-kvm \ - qemu-utils \ - inetutils-ping \ - dnsutils \ - telnet \ - && rm -rf /var/lib/apt/lists/* +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 ARG IMAGE COPY $IMAGE* / diff --git a/ftosv/docker/launch.py b/ftosv/docker/launch.py index 86150dda..68ca2a09 100755 --- a/ftosv/docker/launch.py +++ b/ftosv/docker/launch.py @@ -102,7 +102,7 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect([b"login:"], 1) + (ridx, match, res) = self.expect([b"login:"], 1) if match: # got a match! if ridx == 0: # login self.logger.debug("matched login prompt") @@ -131,8 +131,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 diff --git a/huawei_vrp/docker/Dockerfile b/huawei_vrp/docker/Dockerfile index 210423ca..18c175a6 100644 --- a/huawei_vrp/docker/Dockerfile +++ b/huawei_vrp/docker/Dockerfile @@ -1,16 +1,4 @@ -FROM public.ecr.aws/docker/library/debian:bookworm-slim - -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update -qy \ - && apt-get install --no-install-recommends -y \ - iproute2 \ - python3 \ - socat \ - qemu-kvm \ - qemu-utils \ - telnet \ - && rm -rf /var/lib/apt/lists/* +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 ARG IMAGE COPY $IMAGE* / diff --git a/huawei_vrp/docker/launch.py b/huawei_vrp/docker/launch.py index d71f9242..46971b9e 100755 --- a/huawei_vrp/docker/launch.py +++ b/huawei_vrp/docker/launch.py @@ -73,7 +73,7 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect([b""], 1) + (ridx, match, res) = self.expect([b""], 1) if match and ridx == 0: # got a match! # run main config! @@ -94,8 +94,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 @@ -118,7 +118,7 @@ def bootstrap_mgmt_interface(self): # Error: The system is busy in building configuration. Please wait for a moment... while True: self.wait_write(cmd="clear configuration this", wait=None) - (idx, match, res) = self.tn.expect([rb"Error"], 1) + (idx, match, res) = self.expect([rb"Error"], 1) if match and idx == 0: time.sleep(5) else: diff --git a/vjunosevolved/Makefile b/juniper/vjunosevolved/Makefile similarity index 76% rename from vjunosevolved/Makefile rename to juniper/vjunosevolved/Makefile index a3bffb6a..6682f873 100644 --- a/vjunosevolved/Makefile +++ b/juniper/vjunosevolved/Makefile @@ -2,6 +2,7 @@ VENDOR=Juniper NAME=vJunosEvolved IMAGE_FORMAT=qcow IMAGE_GLOB=*.qcow2 +VENDOR_SUBDIR=1 # match versions like: # vJunosEvolved-23.1R1.8.qcow2 @@ -11,5 +12,5 @@ IMAGE_GLOB=*.qcow2 VERSION=$(shell echo $(IMAGE) | sed -e 's/vjunosevolved-//i' | sed -e 's/.qcow2//i') --include ../makefile-sanity.include --include ../makefile.include +-include ../../makefile-sanity.include +-include ../../makefile.include diff --git a/vjunosevolved/README.md b/juniper/vjunosevolved/README.md similarity index 100% rename from vjunosevolved/README.md rename to juniper/vjunosevolved/README.md diff --git a/juniper/vjunosevolved/docker/Dockerfile b/juniper/vjunosevolved/docker/Dockerfile new file mode 100644 index 00000000..4453c597 --- /dev/null +++ b/juniper/vjunosevolved/docker/Dockerfile @@ -0,0 +1,15 @@ +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 + +ARG IMAGE +COPY $IMAGE* / + +# copy conf file +COPY init.conf / +# copy config shell script +COPY make-config.sh / +# copy python scripts for launching VM +COPY *.py / + +EXPOSE 22 161/udp 830 5000 10000-10099 57400 +HEALTHCHECK CMD ["/healthcheck.py"] +ENTRYPOINT ["/launch.py"] diff --git a/vjunosevolved/docker/init.conf b/juniper/vjunosevolved/docker/init.conf similarity index 100% rename from vjunosevolved/docker/init.conf rename to juniper/vjunosevolved/docker/init.conf diff --git a/vjunosevolved/docker/launch.py b/juniper/vjunosevolved/docker/launch.py similarity index 98% rename from vjunosevolved/docker/launch.py rename to juniper/vjunosevolved/docker/launch.py index a2fbfd30..62ba158a 100755 --- a/vjunosevolved/docker/launch.py +++ b/juniper/vjunosevolved/docker/launch.py @@ -141,7 +141,7 @@ def bootstrap_spin(self): # lets wait for the OS/platform log to determine if VM is booted, # login prompt can get lost in boot logs - (ridx, match, res) = self.tn.expect([b"Juniper"], 1) + (ridx, match, res) = self.expect([b"Juniper"], 1) if match: # got a match! if ridx == 0: # login self.logger.info("VM started") @@ -164,8 +164,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 diff --git a/vjunosevolved/docker/make-config.sh b/juniper/vjunosevolved/docker/make-config.sh similarity index 100% rename from vjunosevolved/docker/make-config.sh rename to juniper/vjunosevolved/docker/make-config.sh diff --git a/vjunosrouter/Makefile b/juniper/vjunosrouter/Makefile similarity index 71% rename from vjunosrouter/Makefile rename to juniper/vjunosrouter/Makefile index dcf94171..06f8e9f1 100644 --- a/vjunosrouter/Makefile +++ b/juniper/vjunosrouter/Makefile @@ -2,11 +2,12 @@ VENDOR=Juniper NAME=vJunos-router IMAGE_FORMAT=qcow IMAGE_GLOB=*.qcow2 +VENDOR_SUBDIR=1 # match versions like: # vJunos-router-23.2R1.15.qcow2 # ... VERSION=$(shell echo $(IMAGE) | sed -e 's/vJunos-router-//i' | sed -e 's/.qcow2//i') --include ../makefile-sanity.include --include ../makefile.include \ No newline at end of file +-include ../../makefile-sanity.include +-include ../../makefile.include \ No newline at end of file diff --git a/vjunosrouter/README.md b/juniper/vjunosrouter/README.md similarity index 100% rename from vjunosrouter/README.md rename to juniper/vjunosrouter/README.md diff --git a/juniper/vjunosrouter/docker/Dockerfile b/juniper/vjunosrouter/docker/Dockerfile new file mode 100644 index 00000000..4453c597 --- /dev/null +++ b/juniper/vjunosrouter/docker/Dockerfile @@ -0,0 +1,15 @@ +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 + +ARG IMAGE +COPY $IMAGE* / + +# copy conf file +COPY init.conf / +# copy config shell script +COPY make-config.sh / +# copy python scripts for launching VM +COPY *.py / + +EXPOSE 22 161/udp 830 5000 10000-10099 57400 +HEALTHCHECK CMD ["/healthcheck.py"] +ENTRYPOINT ["/launch.py"] diff --git a/vjunosrouter/docker/init.conf b/juniper/vjunosrouter/docker/init.conf similarity index 100% rename from vjunosrouter/docker/init.conf rename to juniper/vjunosrouter/docker/init.conf diff --git a/vjunosrouter/docker/launch.py b/juniper/vjunosrouter/docker/launch.py similarity index 97% rename from vjunosrouter/docker/launch.py rename to juniper/vjunosrouter/docker/launch.py index 6c07034e..f1896d40 100755 --- a/vjunosrouter/docker/launch.py +++ b/juniper/vjunosrouter/docker/launch.py @@ -126,7 +126,7 @@ def bootstrap_spin(self): # lets wait for the OS/platform log to determine if VM is booted, # login prompt can get lost in boot logs - (ridx, match, res) = self.tn.expect([b"FreeBSD/amd64"], 1) + (ridx, match, res) = self.expect([b"FreeBSD/amd64"], 1) if match: # got a match! if ridx == 0: # login self.logger.info("VM started") @@ -149,8 +149,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 diff --git a/vjunosrouter/docker/make-config.sh b/juniper/vjunosrouter/docker/make-config.sh similarity index 100% rename from vjunosrouter/docker/make-config.sh rename to juniper/vjunosrouter/docker/make-config.sh diff --git a/vjunosswitch/Makefile b/juniper/vjunosswitch/Makefile similarity index 76% rename from vjunosswitch/Makefile rename to juniper/vjunosswitch/Makefile index 6cf35e7e..ed79054f 100644 --- a/vjunosswitch/Makefile +++ b/juniper/vjunosswitch/Makefile @@ -2,6 +2,7 @@ VENDOR=Juniper NAME=vJunos-switch IMAGE_FORMAT=qcow IMAGE_GLOB=*.qcow2 +VENDOR_SUBDIR=1 # match versions like: # vJunos-switch-23.1R1.8.qcow2 @@ -11,5 +12,5 @@ IMAGE_GLOB=*.qcow2 VERSION=$(shell echo $(IMAGE) | sed -e 's/vjunos-switch-//i' | sed -e 's/.qcow2//i') --include ../makefile-sanity.include --include ../makefile.include +-include ../../makefile-sanity.include +-include ../../makefile.include diff --git a/vjunosswitch/README.md b/juniper/vjunosswitch/README.md similarity index 100% rename from vjunosswitch/README.md rename to juniper/vjunosswitch/README.md diff --git a/juniper/vjunosswitch/docker/Dockerfile b/juniper/vjunosswitch/docker/Dockerfile new file mode 100644 index 00000000..4453c597 --- /dev/null +++ b/juniper/vjunosswitch/docker/Dockerfile @@ -0,0 +1,15 @@ +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 + +ARG IMAGE +COPY $IMAGE* / + +# copy conf file +COPY init.conf / +# copy config shell script +COPY make-config.sh / +# copy python scripts for launching VM +COPY *.py / + +EXPOSE 22 161/udp 830 5000 10000-10099 57400 +HEALTHCHECK CMD ["/healthcheck.py"] +ENTRYPOINT ["/launch.py"] diff --git a/vjunosswitch/docker/init.conf b/juniper/vjunosswitch/docker/init.conf similarity index 100% rename from vjunosswitch/docker/init.conf rename to juniper/vjunosswitch/docker/init.conf diff --git a/vjunosswitch/docker/launch.py b/juniper/vjunosswitch/docker/launch.py similarity index 97% rename from vjunosswitch/docker/launch.py rename to juniper/vjunosswitch/docker/launch.py index 7f23d13d..47b1d578 100755 --- a/vjunosswitch/docker/launch.py +++ b/juniper/vjunosswitch/docker/launch.py @@ -126,7 +126,7 @@ def bootstrap_spin(self): # lets wait for the OS/platform log to determine if VM is booted, # login prompt can get lost in boot logs - (ridx, match, res) = self.tn.expect([b"FreeBSD/amd64"], 1) + (ridx, match, res) = self.expect([b"FreeBSD/amd64"], 1) if match: # got a match! if ridx == 0: # login self.logger.info("VM started") @@ -149,8 +149,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 diff --git a/vjunosswitch/docker/make-config.sh b/juniper/vjunosswitch/docker/make-config.sh similarity index 100% rename from vjunosswitch/docker/make-config.sh rename to juniper/vjunosswitch/docker/make-config.sh diff --git a/vmx/Makefile b/juniper/vmx/Makefile similarity index 79% rename from vmx/Makefile rename to juniper/vmx/Makefile index bc12ce99..94ec859e 100644 --- a/vmx/Makefile +++ b/juniper/vmx/Makefile @@ -2,6 +2,7 @@ VENDOR=Juniper NAME=vMX IMAGE_FORMAT=tgz IMAGE_GLOB=*.tgz +VENDOR_SUBDIR=1 # match versions like: # vmx-14.1R6.4.tgz @@ -14,9 +15,9 @@ IMAGE_GLOB=*.tgz # vmx-bundle-17.1R1-S1.tgz VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+[^0-9]\([0-9][0-9]\.[0-9][A-Z][0-9]\+\(\.[0-9]\+\|-[SD][0-9]\+\(\.[0-9]\+\)\?\)\)[^0-9].*$$/\1/') --include ../makefile-sanity.include --include ../makefile.include --include ../makefile-install.include +-include ../../makefile-sanity.include +-include ../../makefile.include +-include ../../makefile-install.include docker-build-image-copy: ./vmx-extract.sh $(IMAGE) diff --git a/vmx/README.md b/juniper/vmx/README.md similarity index 100% rename from vmx/README.md rename to juniper/vmx/README.md diff --git a/juniper/vmx/docker/Dockerfile b/juniper/vmx/docker/Dockerfile new file mode 100644 index 00000000..be364af7 --- /dev/null +++ b/juniper/vmx/docker/Dockerfile @@ -0,0 +1,8 @@ +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 + +COPY vmx /vmx +COPY *.py / + +EXPOSE 22 161/udp 830 5000 10000-10099 57400 +HEALTHCHECK CMD ["/healthcheck.py"] +ENTRYPOINT ["/launch.py"] diff --git a/vmx/docker/launch.py b/juniper/vmx/docker/launch.py similarity index 97% rename from vmx/docker/launch.py rename to juniper/vmx/docker/launch.py index 72f72e87..60214476 100755 --- a/vmx/docker/launch.py +++ b/juniper/vmx/docker/launch.py @@ -116,7 +116,7 @@ def bootstrap_spin(self): self.spins = 0 return - (ridx, match, res) = self.tn.expect([b"login:", b"root@(%|:~ #)"], 1) + (ridx, match, res) = self.expect([b"login:", b"root@(%|:~ #)"], 1) if match: # got a match! if ridx == 0: # matched login prompt, so should login self.logger.info("matched login prompt") @@ -155,8 +155,8 @@ def bootstrap_spin(self): else: # 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 VCP: %s" % res.decode()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 @@ -237,7 +237,7 @@ def wait_write(self, cmd, wait="#", timeout=None): if wait: self.logger.trace("Waiting for {} before writing {}".format(wait, cmd)) while True: - (ridx, match, res) = self.tn.expect( + (ridx, match, res) = self.expect( [wait.encode(), b"Retry connection attempts"], timeout=timeout ) if match: @@ -245,7 +245,7 @@ def wait_write(self, cmd, wait="#", timeout=None): break if ridx == 1: self.tn.write("yes\r".encode()) - self.logger.trace("Read: %s" % res.decode()) + self.print(res) self.logger.debug("writing to serial console: %s" % cmd) self.tn.write("{}\r".format(cmd).encode()) @@ -308,7 +308,7 @@ def start(self): vrnetlab.run_command(["ip", "link", "set", "vfpc-int", "up"]) def bootstrap_spin(self): - (ridx, match, res) = self.tn.expect( + (ridx, match, res) = self.expect( [ b"localhost login", b"qemux86-64 login", @@ -423,9 +423,9 @@ def install(self): break try: - (ridx, match, res) = vcp.tn.expect([b"Powering system off"], 1) - if res != b"": - self.logger.trace("OUTPUT VCP: %s" % res.decode()) + (ridx, match, res) = vcp.expect([b"Powering system off"], 1) + if res != "": + self.print(res) except Exception as exc: # assume it's dead self.logger.info( diff --git a/vmx/vmx-extract.sh b/juniper/vmx/vmx-extract.sh similarity index 100% rename from vmx/vmx-extract.sh rename to juniper/vmx/vmx-extract.sh diff --git a/vqfx/Makefile b/juniper/vqfx/Makefile similarity index 94% rename from vqfx/Makefile rename to juniper/vqfx/Makefile index 1b185055..0c02e179 100644 --- a/vqfx/Makefile +++ b/juniper/vqfx/Makefile @@ -2,6 +2,7 @@ VENDOR=Juniper NAME=vQFX IMAGE_FORMAT=qcow2 IMAGE_GLOB=*.qcow2 +VENDOR_SUBDIR=1 # New vqfx are named: vqfx-19.4R1.10-re-qemu.qcow2 VERSION=$(shell echo $(IMAGE) | sed -e 's/^vqfx-//'|sed -e 's/-re-qemu.qcow2//' ) @@ -11,8 +12,8 @@ VMDK_VERSION:=$(shell ls *-re-*.vmdk | sed -re 's/vqfx10k-re-([^;]*)\.vmdk.*$$/\ PFE_BASE_VERSION=$(shell echo $VERSION | sed -e s'/.*//') PFE_IMAGE=$(shell ls vqfx-$(PFE_BASE_VERSION)*-pfe-qemu.qcow*) --include ../makefile-sanity.include --include ../makefile.include +-include ../../makefile-sanity.include +-include ../../makefile.include format-legacy-images: @if ls *.vmdk; then echo "VMDKs exist, converting them to qcow format"; qemu-img convert -f vmdk -O qcow2 *-re-*.vmdk vqfx-$(VMDK_VERSION)-re-qemu.qcow2 && qemu-img convert -f vmdk -O qcow *-pfe-*.vmdk vqfx-$(VMDK_VERSION)-pfe-qemu.qcow; echo "VMDKs have been converted"; fi @@ -30,6 +31,6 @@ docker-build-common: docker-clean-build docker-pre-build @if [ -z "$$IMAGE" ]; then echo "ERROR: No IMAGE specified"; exit 1; fi @if [ "$(IMAGE)" = "$(VERSION)" ]; then echo "ERROR: Incorrect version string ($(IMAGE)). The regexp for extracting version information is likely incorrect, check the regexp in the Makefile or open an issue at https://github.com/hellt/vrnetlab/issues/new including the image file name you are using."; exit 1; fi @echo "Building docker image using $(IMAGE) as $(REGISTRY)vr-$(VR_NAME):$(VERSION)" - cp ../common/* docker/ + cp ../../common/* docker/ $(MAKE) IMAGE=$$IMAGE docker-build-image-copy (cd docker; docker build --build-arg http_proxy=$(http_proxy) --build-arg https_proxy=$(https_proxy) --build-arg RE_IMAGE=$(IMAGE) --build-arg PFE_IMAGE=$(PFE_IMAGE) -t $(REGISTRY)vr-$(VR_NAME):$(VERSION) .) diff --git a/vqfx/README.md b/juniper/vqfx/README.md similarity index 100% rename from vqfx/README.md rename to juniper/vqfx/README.md diff --git a/juniper/vqfx/docker/Dockerfile b/juniper/vqfx/docker/Dockerfile new file mode 100644 index 00000000..25ed4b6e --- /dev/null +++ b/juniper/vqfx/docker/Dockerfile @@ -0,0 +1,15 @@ +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 + +ARG RE_IMAGE +ARG PFE_IMAGE + +COPY $RE_IMAGE / +COPY $PFE_IMAGE / + +COPY healthcheck.py / +COPY vrnetlab.py / +COPY launch.py / + +EXPOSE 22 161/udp 830 5000 10000-10099 +HEALTHCHECK CMD ["/healthcheck.py"] +ENTRYPOINT ["/launch.py"] diff --git a/vqfx/docker/launch.py b/juniper/vqfx/docker/launch.py similarity index 97% rename from vqfx/docker/launch.py rename to juniper/vqfx/docker/launch.py index 2436190e..58f1b726 100755 --- a/vqfx/docker/launch.py +++ b/juniper/vqfx/docker/launch.py @@ -99,7 +99,7 @@ def bootstrap_spin(self): if self._version["major"] < 20: logged_in_prompt = b"root@vqfx-re:RE:0%" - (ridx, match, res) = self.tn.expect([b"login:", logged_in_prompt], 1) + (ridx, match, res) = self.expect([b"login:", logged_in_prompt], 1) if match: # got a match! if ridx == 0: # matched login prompt, so should login self.logger.info("matched login prompt") @@ -122,8 +122,8 @@ def bootstrap_spin(self): else: # 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 VCP: %s" % res.decode()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 @@ -185,7 +185,7 @@ def wait_write(self, cmd, wait="#", timeout=None): if wait: self.logger.trace("Waiting for %s" % wait) while True: - (ridx, match, res) = self.tn.expect( + (ridx, match, res) = self.expect( [wait.encode(), b"Retry connection attempts"], timeout=timeout ) if match: @@ -193,7 +193,7 @@ def wait_write(self, cmd, wait="#", timeout=None): break if ridx == 1: self.tn.write("yes\r".encode()) - self.logger.trace("Read: %s" % res.decode()) + self.print(res) self.logger.debug("writing to serial console: %s" % cmd) self.tn.write("{}\r".format(cmd).encode()) diff --git a/vsrx/Makefile b/juniper/vsrx/Makefile similarity index 72% rename from vsrx/Makefile rename to juniper/vsrx/Makefile index 3f2978b8..e343228b 100644 --- a/vsrx/Makefile +++ b/juniper/vsrx/Makefile @@ -3,10 +3,11 @@ NAME=vSRX IMAGE_FORMAT=qcow IMAGE_GLOB=*.qcow2 IMAGE=ffp-12.1X47-D15.4-packetmode.qcow2 +VENDOR_SUBDIR=1 # match versions like: # 12.1X47-D15.4 VERSION=$(shell echo $(IMAGE) | sed -e 's/junos-vsrx3-x86-64-//' | sed -e 's/.qcow2//') --include ../makefile-sanity.include --include ../makefile.include +-include ../../makefile-sanity.include +-include ../../makefile.include diff --git a/vsrx/README.md b/juniper/vsrx/README.md similarity index 100% rename from vsrx/README.md rename to juniper/vsrx/README.md diff --git a/juniper/vsrx/docker/Dockerfile b/juniper/vsrx/docker/Dockerfile new file mode 100644 index 00000000..9a4a0234 --- /dev/null +++ b/juniper/vsrx/docker/Dockerfile @@ -0,0 +1,12 @@ +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 + +ARG IMAGE +COPY $IMAGE* / + +COPY init.conf / +COPY make-config-iso.sh / +COPY *.py / + +EXPOSE 22 161/udp 830 5000 10000-10099 +HEALTHCHECK CMD ["/healthcheck.py"] +ENTRYPOINT ["/launch.py"] diff --git a/vsrx/docker/init.conf b/juniper/vsrx/docker/init.conf similarity index 100% rename from vsrx/docker/init.conf rename to juniper/vsrx/docker/init.conf diff --git a/vsrx/docker/launch.py b/juniper/vsrx/docker/launch.py similarity index 97% rename from vsrx/docker/launch.py rename to juniper/vsrx/docker/launch.py index e6f886dc..86156c7b 100755 --- a/vsrx/docker/launch.py +++ b/juniper/vsrx/docker/launch.py @@ -110,7 +110,7 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect([b"login:"], 1) + (ridx, match, res) = self.expect([b"login:"], 1) if match: # got a match! if ridx == 0: # login self.logger.info("VM started") @@ -125,8 +125,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 diff --git a/vsrx/docker/make-config-iso.sh b/juniper/vsrx/docker/make-config-iso.sh similarity index 100% rename from vsrx/docker/make-config-iso.sh rename to juniper/vsrx/docker/make-config-iso.sh diff --git a/makefile-install.include b/makefile-install.include index 2f2bf9ae..bf681f86 100644 --- a/makefile-install.include +++ b/makefile-install.include @@ -31,18 +31,18 @@ docker-pre-build: -cat cidfile | xargs --no-run-if-empty docker rm -f -rm cidfile - -docker tag $(REGISTRY)vr-$(VR_NAME):$(VERSION) $(REGISTRY)vr-$(VR_NAME):$(VERSION)-previous-build + -docker tag $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION) $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION)-previous-build docker-build: docker-build-common - -docker inspect --format '{{.RootFS.Layers}}' $(REGISTRY)vr-$(VR_NAME):$(VERSION)-previous-build | tr -d '][' | awk '{ $$(NF)=""; print }' > built-image-sha-previous - docker inspect --format '{{.RootFS.Layers}}' $(REGISTRY)vr-$(VR_NAME):$(VERSION) | tr -d '][' > built-image-sha-current + -docker inspect --format '{{.RootFS.Layers}}' $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION)-previous-build | tr -d '][' | awk '{ $$(NF)=""; print }' > built-image-sha-previous + docker inspect --format '{{.RootFS.Layers}}' $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION) | tr -d '][' > built-image-sha-current if [ "$$(cat built-image-sha-previous | sed -e 's/[[:space:]]*$$//')" = "$$(cat built-image-sha-current)" ]; then echo "Previous image is the same as current, retagging!"; \ - docker tag $(REGISTRY)vr-$(VR_NAME):$(VERSION)-previous-build $(REGISTRY)vr-$(VR_NAME):$(VERSION) || true; \ + docker tag $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION)-previous-build $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION) || true; \ else \ echo "Current build differ from previous, running install!"; \ - docker run --cidfile cidfile --privileged $(REGISTRY)vr-$(VR_NAME):$(VERSION) --trace --install; \ - docker commit --change='ENTRYPOINT ["/launch.py"]' $$(cat cidfile) $(REGISTRY)vr-$(VR_NAME):$(VERSION); \ + docker run --cidfile cidfile --privileged $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION) --trace --install; \ + docker commit --change='ENTRYPOINT ["/launch.py"]' $$(cat cidfile) $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION); \ docker rm -f $$(cat cidfile); \ fi - docker rmi -f $(REGISTRY)vr-$(VR_NAME):$(VERSION)-previous-build || true + docker rmi -f $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION)-previous-build || true rm built-image-sha* diff --git a/makefile.include b/makefile.include index 574b7360..f2bb6d47 100644 --- a/makefile.include +++ b/makefile.include @@ -31,6 +31,9 @@ docker-build-common: docker-clean-build docker-pre-build @echo "Building docker image using $(IMAGE) as $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION)" ifeq ($(NOT_VM_IMAGE), 1) echo "ok" +else ifeq ($(VENDOR_SUBDIR), 1) + echo "Copying to vendor subdirectory" + cp ../../common/* docker/ else cp ../common/* docker/ endif diff --git a/n9kv/docker/Dockerfile b/n9kv/docker/Dockerfile deleted file mode 100644 index 53c78a09..00000000 --- a/n9kv/docker/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -FROM ubuntu:20.04 -LABEL maintainer="Kristian Larsson " -LABEL maintainer="Roman Dodin " - -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 \ - tftpd-hpa \ - ssh \ - inetutils-ping \ - dnsutils \ - openvswitch-switch \ - iptables \ - nftables \ - telnet \ - && rm -rf /var/lib/apt/lists/* - -ARG IMAGE -COPY $IMAGE* / -COPY OVMF.fd / -COPY *.py / - -EXPOSE 22 80 161/udp 443 830 5000 6030 10000-10099 57400 -HEALTHCHECK CMD ["/healthcheck.py"] -ENTRYPOINT ["/launch.py"] diff --git a/nxos/docker/Dockerfile b/nxos/docker/Dockerfile deleted file mode 100644 index 705006df..00000000 --- a/nxos/docker/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -FROM ubuntu:20.04 -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/* - -ARG IMAGE -COPY $IMAGE* / -COPY *.py / - -EXPOSE 22 161/udp 830 5000 10000-10099 -HEALTHCHECK CMD ["/healthcheck.py"] -ENTRYPOINT ["/launch.py"] diff --git a/ocnos/docker/Dockerfile b/ocnos/docker/Dockerfile index 572be609..32854cf0 100644 --- a/ocnos/docker/Dockerfile +++ b/ocnos/docker/Dockerfile @@ -1,24 +1,4 @@ -FROM ubuntu:20.04 - -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 \ - 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/ocnos/docker/launch.py b/ocnos/docker/launch.py index d3236f1b..3b083d53 100755 --- a/ocnos/docker/launch.py +++ b/ocnos/docker/launch.py @@ -65,7 +65,7 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect([b"OcNOS login:"], 1) + (ridx, match, res) = self.expect([b"OcNOS login:"], 1) if match: # got a match! if ridx == 0: # login self.logger.debug("matched login prompt") @@ -87,8 +87,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 diff --git a/openbsd/docker/Dockerfile b/openbsd/docker/Dockerfile index 75bed9cb..820c70f2 100644 --- a/openbsd/docker/Dockerfile +++ b/openbsd/docker/Dockerfile @@ -1,27 +1,6 @@ -FROM debian:bookworm-slim +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 -ARG DEBIAN_FRONTEND=noninteractive ARG DISK_SIZE=4G - -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 \ - iptables \ - nftables \ - telnet \ - cloud-utils \ - sshpass \ - && rm -rf /var/lib/apt/lists/* - ARG IMAGE COPY $IMAGE* / COPY *.py / diff --git a/openbsd/docker/launch.py b/openbsd/docker/launch.py index ffade909..3b5fa3a8 100755 --- a/openbsd/docker/launch.py +++ b/openbsd/docker/launch.py @@ -129,7 +129,7 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect([b"login: "], 1) + (ridx, match, res) = self.expect([b"login: "], 1) if match: # got a match! if ridx == 0: # login @@ -148,8 +148,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 diff --git a/openwrt/docker/Dockerfile b/openwrt/docker/Dockerfile index 14d5bbc7..7e15c4b7 100644 --- a/openwrt/docker/Dockerfile +++ b/openwrt/docker/Dockerfile @@ -1,18 +1,4 @@ -FROM alpine:3.18 -MAINTAINER Andreas Cymbal takalele@konnex.me - -RUN apk add --no-cache bash \ - qemu-system-x86_64 \ - qemu-img \ - socat \ - net-tools \ - iproute2 \ - bridge-utils \ - python3 \ - py3-click \ - nano \ - vim \ - py3-pip && ln -sf python3 /usr/bin/python +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 RUN pip3 install --no-cache --upgrade pip setuptools RUN pip install IPy diff --git a/openwrt/docker/launch.py b/openwrt/docker/launch.py index 6f8a8a46..d03d3036 100755 --- a/openwrt/docker/launch.py +++ b/openwrt/docker/launch.py @@ -51,7 +51,7 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect([b"br-lan"], 1) + (ridx, match, res) = self.expect([b"br-lan"], 1) if match: # got a match! if ridx == 0: # login self.logger.debug("VM started") @@ -68,8 +68,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 diff --git a/pan/docker/Dockerfile b/pan/docker/Dockerfile index 8ee0108a..4819516e 100644 --- a/pan/docker/Dockerfile +++ b/pan/docker/Dockerfile @@ -1,26 +1,4 @@ -FROM ubuntu:20.04 -LABEL maintainer="Kristian Larsson " -LABEL maintainer="Roman Dodin " - -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 \ - 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/pan/docker/launch.py b/pan/docker/launch.py index 8237dcf5..a570ba61 100755 --- a/pan/docker/launch.py +++ b/pan/docker/launch.py @@ -69,7 +69,7 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect( + (ridx, match, res) = self.expect( [ b"Login incorrect", b"vm login:", @@ -123,8 +123,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 @@ -145,7 +145,7 @@ def bootstrap_config(self): # whatever reason self.wait_write("", None) while True: - (ridx, match, res) = self.tn.expect([b"FIN", b"PEND"], 1) + (ridx, match, res) = self.expect([b"FIN", b"PEND"], 1) if match: if ridx == 0: # login self.logger.debug("auto commit complete, begin configuration") diff --git a/routeros/docker/Dockerfile b/routeros/docker/Dockerfile index 72da4f08..fe123020 100644 --- a/routeros/docker/Dockerfile +++ b/routeros/docker/Dockerfile @@ -1,25 +1,4 @@ -FROM ubuntu:20.04 -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 \ - inetutils-ping \ - dnsutils \ - iptables \ - nftables \ - telnet \ - ftp \ - && rm -rf /var/lib/apt/lists/* +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 ARG IMAGE COPY $IMAGE* / diff --git a/routeros/docker/launch.py b/routeros/docker/launch.py index 773c8f2d..e209fb32 100755 --- a/routeros/docker/launch.py +++ b/routeros/docker/launch.py @@ -100,7 +100,7 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect([b"MikroTik Login", b"RouterOS Login"], 1) + (ridx, match, res) = self.expect([b"MikroTik Login", b"RouterOS Login"], 1) if match: # got a match! if ridx in (0, 1): # login self.logger.debug("VM started") @@ -121,7 +121,7 @@ def bootstrap_spin(self): # ROSv7 requires changing the password right away. ROSv6 does not require changing the password - (ridx2, match2, _) = self.tn.expect([b"new password>"], 1) + (ridx2, match2, _) = self.expect([b"new password>"], 1) if match2 and ridx2 == 0: # got a match! login self.logger.debug("ROSv7 detected, setting admin password") self.wait_write(f"{self.password}", wait="new password>") @@ -147,8 +147,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 diff --git a/sonic/docker/Dockerfile b/sonic/docker/Dockerfile index 44c51ee9..fb02eed1 100644 --- a/sonic/docker/Dockerfile +++ b/sonic/docker/Dockerfile @@ -1,18 +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 \ - python3-ipy \ - qemu-kvm \ - qemu-utils \ - socat \ - ssh \ - sshpass \ - && rm -rf /var/lib/apt/lists/* +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 ARG IMAGE COPY $IMAGE* / diff --git a/sonic/docker/launch.py b/sonic/docker/launch.py index 778ad867..812976f9 100755 --- a/sonic/docker/launch.py +++ b/sonic/docker/launch.py @@ -65,7 +65,7 @@ def bootstrap_spin(self): self.start() return - ridx, match, res = self.tn.expect([b"login:"], 1) + ridx, match, res = self.expect([b"login:"], 1) if match and ridx == 0: # login self.logger.info("VM started") @@ -90,8 +90,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 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/healthcheck.py b/sros/docker/healthcheck.py deleted file mode 100755 index 49053b63..00000000 --- a/sros/docker/healthcheck.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 - -import sys - -try: - health_file = open("/health", "r") - health = health_file.read() - health_file.close() -except FileNotFoundError: - print("health status file not found") - sys.exit(2) - -exit_status, message = health.strip().split(" ", 1) - -if message != "": - print(message) - -sys.exit(int(exit_status)) diff --git a/sros/docker/launch.py b/sros/docker/launch.py index cfa92712..9b5c64a4 100755 --- a/sros/docker/launch.py +++ b/sros/docker/launch.py @@ -964,14 +964,14 @@ 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) + # # 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.expect([b"Login:", b"^[^ ]+#"], 1) if match: # got a match! if ridx == 0: # matched login prompt, so should login self.logger.debug("matched login prompt") @@ -989,8 +989,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 @@ -1679,8 +1679,6 @@ def getDefaultConfig() -> str: 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() ia = SROS( diff --git a/ubuntu/docker/Dockerfile b/ubuntu/docker/Dockerfile index 417aa776..00649405 100644 --- a/ubuntu/docker/Dockerfile +++ b/ubuntu/docker/Dockerfile @@ -1,25 +1,7 @@ -FROM debian:bookworm-slim +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 -ARG DEBIAN_FRONTEND=noninteractive ARG DISK_SIZE=4G -RUN apt-get update -qy \ - && apt-get install -y \ - bridge-utils \ - iproute2 \ - socat \ - qemu-kvm \ - tcpdump \ - ssh \ - inetutils-ping \ - dnsutils \ - iptables \ - nftables \ - telnet \ - cloud-utils \ - sshpass \ - && rm -rf /var/lib/apt/lists/* - ARG IMAGE COPY $IMAGE* / COPY *.py / diff --git a/ubuntu/docker/launch.py b/ubuntu/docker/launch.py index b9778c8c..d08dff3c 100755 --- a/ubuntu/docker/launch.py +++ b/ubuntu/docker/launch.py @@ -118,7 +118,7 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect([b"login: "], 1) + (ridx, match, res) = self.expect([b"login: "], 1) if match: # got a match! if ridx == 0: # login self.logger.debug("matched, login: ") @@ -134,8 +134,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 diff --git a/veos/docker/Dockerfile b/veos/docker/Dockerfile index 8ee0108a..4819516e 100644 --- a/veos/docker/Dockerfile +++ b/veos/docker/Dockerfile @@ -1,26 +1,4 @@ -FROM ubuntu:20.04 -LABEL maintainer="Kristian Larsson " -LABEL maintainer="Roman Dodin " - -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 \ - 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/veos/docker/launch.py b/veos/docker/launch.py index 3b6a2d9d..1cbf81ed 100755 --- a/veos/docker/launch.py +++ b/veos/docker/launch.py @@ -65,7 +65,7 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect([b"login:"], 1) + (ridx, match, res) = self.expect([b"login:"], 1) if match: # got a match! if ridx == 0: # login self.logger.debug("matched login prompt") @@ -86,8 +86,8 @@ 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(f"OUTPUT: {res.decode()}") + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 diff --git a/vios/docker/Dockerfile b/vios/docker/Dockerfile deleted file mode 100644 index d9e8aada..00000000 --- a/vios/docker/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -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/* - -ARG IMAGE -COPY $IMAGE* / -COPY *.py / - -EXPOSE 22 161/udp 5000 10000-10099 -HEALTHCHECK CMD ["/healthcheck.py"] -ENTRYPOINT ["/launch.py"] diff --git a/vjunosevolved/docker/Dockerfile b/vjunosevolved/docker/Dockerfile deleted file mode 100644 index 173e4d81..00000000 --- a/vjunosevolved/docker/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -FROM public.ecr.aws/docker/library/debian:bookworm-slim -LABEL org.opencontainers.image.authors="roman@dodin.dev,vista@birb.network" - -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update -qy \ - && apt-get install --no-install-recommends -y \ - dosfstools \ - bridge-utils \ - iproute2 \ - python3 \ - python3-passlib \ - socat \ - ssh \ - qemu-kvm \ - qemu-utils \ - inetutils-ping \ - dnsutils \ - telnet \ - && rm -rf /var/lib/apt/lists/* - -ARG IMAGE -COPY $IMAGE* / - -# copy conf file -COPY init.conf / -# copy config shell script -COPY make-config.sh / -# copy python scripts for launching VM -COPY *.py / - -EXPOSE 22 161/udp 830 5000 10000-10099 57400 -HEALTHCHECK CMD ["/healthcheck.py"] -ENTRYPOINT ["/launch.py"] diff --git a/vjunosrouter/docker/Dockerfile b/vjunosrouter/docker/Dockerfile deleted file mode 100644 index dc246787..00000000 --- a/vjunosrouter/docker/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -FROM public.ecr.aws/docker/library/debian:bookworm-slim -LABEL org.opencontainers.image.authors="roman@dodin.dev,vista@birb.network" - -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update -qy \ - && apt-get install --no-install-recommends -y \ - dosfstools \ - bridge-utils \ - iproute2 \ - python3 \ - socat \ - ssh \ - qemu-kvm \ - qemu-utils \ - inetutils-ping \ - dnsutils \ - telnet \ - && rm -rf /var/lib/apt/lists/* - -ARG IMAGE -COPY $IMAGE* / - -# copy conf file -COPY init.conf / -# copy config shell script -COPY make-config.sh / -# copy python scripts for launching VM -COPY *.py / - -EXPOSE 22 161/udp 830 5000 10000-10099 57400 -HEALTHCHECK CMD ["/healthcheck.py"] -ENTRYPOINT ["/launch.py"] diff --git a/vjunosswitch/docker/Dockerfile b/vjunosswitch/docker/Dockerfile deleted file mode 100644 index dc246787..00000000 --- a/vjunosswitch/docker/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -FROM public.ecr.aws/docker/library/debian:bookworm-slim -LABEL org.opencontainers.image.authors="roman@dodin.dev,vista@birb.network" - -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update -qy \ - && apt-get install --no-install-recommends -y \ - dosfstools \ - bridge-utils \ - iproute2 \ - python3 \ - socat \ - ssh \ - qemu-kvm \ - qemu-utils \ - inetutils-ping \ - dnsutils \ - telnet \ - && rm -rf /var/lib/apt/lists/* - -ARG IMAGE -COPY $IMAGE* / - -# copy conf file -COPY init.conf / -# copy config shell script -COPY make-config.sh / -# copy python scripts for launching VM -COPY *.py / - -EXPOSE 22 161/udp 830 5000 10000-10099 57400 -HEALTHCHECK CMD ["/healthcheck.py"] -ENTRYPOINT ["/launch.py"] diff --git a/vmx/docker/Dockerfile b/vmx/docker/Dockerfile deleted file mode 100644 index ee64b618..00000000 --- a/vmx/docker/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM ubuntu:20.04 -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/* - -COPY vmx /vmx -COPY *.py / - -EXPOSE 22 161/udp 830 5000 10000-10099 57400 -HEALTHCHECK CMD ["/healthcheck.py"] -ENTRYPOINT ["/launch.py"] diff --git a/vqfx/docker/Dockerfile b/vqfx/docker/Dockerfile deleted file mode 100644 index 49c233de..00000000 --- a/vqfx/docker/Dockerfile +++ /dev/null @@ -1,28 +0,0 @@ -FROM ubuntu:20.04 -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 \ - procps \ - tcpdump \ - && rm -rf /var/lib/apt/lists/* - -ARG RE_IMAGE -ARG PFE_IMAGE - -COPY $RE_IMAGE / -COPY $PFE_IMAGE / - -COPY healthcheck.py / -COPY vrnetlab.py / -COPY launch.py / - -EXPOSE 22 161/udp 830 5000 10000-10099 -HEALTHCHECK CMD ["/healthcheck.py"] -ENTRYPOINT ["/launch.py"] diff --git a/vrnetlab-base.dockerfile b/vrnetlab-base.dockerfile new file mode 100644 index 00000000..fa954754 --- /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 \ + 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 + diff --git a/vrp/docker/Dockerfile b/vrp/docker/Dockerfile index e41511f1..a7ee27e7 100644 --- a/vrp/docker/Dockerfile +++ b/vrp/docker/Dockerfile @@ -1,17 +1,4 @@ -FROM debian:stretch -MAINTAINER Kristian Larsson - -RUN apt-get update -qy \ - && apt-get upgrade -qy \ - && apt-get install -y \ - qemu-kvm \ - bridge-utils \ - socat \ - iproute2 \ - python3-ipy \ - python3-pexpect \ - ssh \ - && rm -rf /var/lib/apt/lists/* +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 ARG IMAGE COPY $IMAGE / diff --git a/vrp/docker/launch.py b/vrp/docker/launch.py index f170bb71..945efb4a 100755 --- a/vrp/docker/launch.py +++ b/vrp/docker/launch.py @@ -68,7 +68,7 @@ def bootstrap_spin(self): 4: '\n' # Press Enter to Continue } - (ridx, match, res) = self.tn.expect([b'localhost login: ', + (ridx, match, res) = self.expect([b'localhost login: ', b'Password: ', b'Enter Password:', b'Confirm Password:', @@ -97,8 +97,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 @@ -120,7 +120,7 @@ def bootstrap_config(self): # when simulator booting, config is not ok # Error: The system is busy in building configuration. Please wait for a moment... while True: - (idx, match, res) = self.tn.expect([b'Error:'], 1) + (idx, match, res) = self.expect([b'Error:'], 1) if match: if idx == 0: self.wait_write(cmd="commit", wait=None) diff --git a/vsr1000/docker/Dockerfile b/vsr1000/docker/Dockerfile index b588d42f..78bd905b 100644 --- a/vsr1000/docker/Dockerfile +++ b/vsr1000/docker/Dockerfile @@ -1,17 +1,4 @@ -FROM debian:stretch -MAINTAINER Kristian Larsson - -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 \ - && rm -rf /var/lib/apt/lists/* +FROM ghcr.io/srl-labs/vrnetlab-base:0.0.1 ARG IMAGE COPY $IMAGE* / diff --git a/vsr1000/docker/launch.py b/vsr1000/docker/launch.py index ca50d631..9ce0c626 100755 --- a/vsr1000/docker/launch.py +++ b/vsr1000/docker/launch.py @@ -49,7 +49,7 @@ def bootstrap_spin(self): self.start() return - (ridx, match, res) = self.tn.expect([b"Performing automatic"], 1) + (ridx, match, res) = self.expect([b"Performing automatic"], 1) if match: # got a match! if ridx == 0: # login self.logger.debug("VM started") @@ -103,8 +103,8 @@ 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()) + if res != "": + self.print(res) # reset spins if we saw some output self.spins = 0 diff --git a/vsrx/docker/Dockerfile b/vsrx/docker/Dockerfile deleted file mode 100644 index bcb21dd7..00000000 --- a/vsrx/docker/Dockerfile +++ /dev/null @@ -1,26 +0,0 @@ -FROM public.ecr.aws/docker/library/debian:bookworm-slim -LABEL org.opencontainers.image.authors="roman@dodin.dev,vista@birb.network" - -ENV DEBIAN_FRONTEND=noninteractive - -RUN apt-get update -qy \ - && apt-get install --no-install-recommends -y \ - bridge-utils \ - iproute2 \ - python3 \ - socat \ - qemu-kvm \ - qemu-utils \ - genisoimage \ - && rm -rf /var/lib/apt/lists/* - -ARG IMAGE -COPY $IMAGE* / - -COPY init.conf / -COPY make-config-iso.sh / -COPY *.py / - -EXPOSE 22 161/udp 830 5000 10000-10099 -HEALTHCHECK CMD ["/healthcheck.py"] -ENTRYPOINT ["/launch.py"] diff --git a/xrv/docker/Dockerfile b/xrv/docker/Dockerfile deleted file mode 100644 index fb4a41aa..00000000 --- a/xrv/docker/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -FROM ubuntu:20.04 -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/* - -ARG IMAGE -COPY $IMAGE* / -COPY *.py / - -EXPOSE 22 161/udp 830 5000 10000-10099 57400 -HEALTHCHECK CMD ["/healthcheck.py"] -ENTRYPOINT ["/launch.py"] diff --git a/xrv9k/Makefile b/xrv9k/Makefile deleted file mode 100644 index fede02f3..00000000 --- a/xrv9k/Makefile +++ /dev/null @@ -1,23 +0,0 @@ -VENDOR=Cisco -NAME=XRv9k -IMAGE_FORMAT=qcow2 -IMAGE_GLOB=*qcow2* -INSTALL=true - -# match versions like: -# TODO: add example file names here -# xrv9k-fullk9-x.vrr-6.1.3.qcow2 -# xrv9k-fullk9-x.vrr-6.2.1.qcow2 -# xrv9k-fullk9-x-7.10.1.qcow2 -VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+[^0-9]\([0-9]\+\.[0-9]\+\.[0-9]\+\(\.[0-9A-Z]\+\)\?\)\([^0-9].*\|$$\)/\1/') - --include ../makefile-sanity.include --include ../makefile.include - -ifeq ($(INSTALL),false) -$(info Install mode disabled) -else -$(info Install mode enabled) --include ../makefile-install.include -endif - diff --git a/xrv9k/docker/Dockerfile b/xrv9k/docker/Dockerfile deleted file mode 100644 index 4658bdf3..00000000 --- a/xrv9k/docker/Dockerfile +++ /dev/null @@ -1,28 +0,0 @@ -FROM ubuntu:20.04 -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/* - -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/xrv9k/docker/launch.py b/xrv9k/docker/launch.py deleted file mode 100755 index 601b624e..00000000 --- a/xrv9k/docker/launch.py +++ /dev/null @@ -1,412 +0,0 @@ -#!/usr/bin/env python3 - -import datetime -import logging -import os -import re -import signal -import sys -import time - - -import vrnetlab - -STARTUP_CONFIG_FILE = "/config/startup-config.cfg" - - -def handle_SIGCHLD(signal, frame): - os.waitpid(-1, os.WNOHANG) - - -def handle_SIGTERM(signal, frame): - sys.exit(0) - - -signal.signal(signal.SIGINT, handle_SIGTERM) -signal.signal(signal.SIGTERM, handle_SIGTERM) -signal.signal(signal.SIGCHLD, handle_SIGCHLD) - -TRACE_LEVEL_NUM = 9 -logging.addLevelName(TRACE_LEVEL_NUM, "TRACE") - - -def trace(self, message, *args, **kws): - # Yes, logger takes its '*args' as 'args'. - if self.isEnabledFor(TRACE_LEVEL_NUM): - self._log(TRACE_LEVEL_NUM, message, args, **kws) - - -logging.Logger.trace = trace - - -class XRV_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") - self.hostname = hostname - self.conn_mode = conn_mode - self.num_nics = nics - self.install_mode = install - self.qemu_args.extend( - [ - "-machine", - "smm=off", - "-boot", - "order=c", - "-cpu", - "qemu64,+ssse3,+sse4.1,+sse4.2", - "-serial", - "telnet:0.0.0.0:50%02d,server,nowait" % (self.num + 1), - "-serial", - "telnet:0.0.0.0:50%02d,server,nowait" % (self.num + 2), - "-serial", - "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)""" - res = [] - # mgmt interface - res.extend( - ["-device", "virtio-net-pci,netdev=mgmt,mac=%s" % vrnetlab.gen_mac(0)] - ) - res.extend( - [ - "-netdev", - "user,id=mgmt,net=10.0.0.0/24," - "tftp=/tftpboot," - "hostfwd=tcp:0.0.0.0:22-10.0.0.15:22," - "hostfwd=udp:0.0.0.0:161-10.0.0.15:161," - "hostfwd=tcp:0.0.0.0:830-10.0.0.15:830," - "hostfwd=tcp:0.0.0.0:57400-10.0.0.15:57400" - ] - ) - # dummy interface for xrv9k ctrl interface - res.extend( - [ - "-device", - "virtio-net-pci,netdev=ctrl-dummy,id=ctrl-dummy,mac=%s" - % vrnetlab.gen_mac(0), - "-netdev", - "tap,ifname=ctrl-dummy,id=ctrl-dummy,script=no,downscript=no", - ] - ) - # dummy interface for xrv9k dev interface - res.extend( - [ - "-device", - "virtio-net-pci,netdev=dev-dummy,id=dev-dummy,mac=%s" - % vrnetlab.gen_mac(0), - "-netdev", - "tap,ifname=dev-dummy,id=dev-dummy,script=no,downscript=no", - ] - ) - - return res - - def bootstrap_spin(self): - """""" - - if self.spins > 600: - # too many spins with no result -> give up - self.logger.debug( - "node is failing to boot or we can't catch the right prompt. Restarting..." - ) - self.stop() - self.start() - return - - (ridx, match, res) = self.tn.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:", - ], - 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.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") - 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() - # 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 - - # 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()) - # reset spins if we saw some output - self.spins = 0 - - self.spins += 1 - - 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 call-home in config - if not self._wait_config("show running-config call-home", "service active"): - return False - - self.wait_write("configure") - self.wait_write(f"hostname {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("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("0.0.0.0/0 10.0.0.2") - 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") - - # 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("ipv4 address 10.0.0.15/24") - self.wait_write("exit") - self.wait_write("commit") - self.wait_write("exit") - - 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 - - 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): - 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)] - - -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 - 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. - """ - 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)] - - 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__": - import argparse - - parser = argparse.ArgumentParser(description="") - parser.add_argument( - "--trace", action="store_true", help="enable trace level logging" - ) - parser.add_argument("--hostname", default="vr-xrv9k", help="Router hostname") - 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( - "--vcpu", type=int, default=2, help="Number of cpu cores to use" - ) - parser.add_argument( - "--ram", type=int, default=16384, help="Number RAM to use in MB" - ) - parser.add_argument( - "--connection-mode", - default="vrxcon", - help="Connection mode to use in the datapath", - ) - args = parser.parse_args() - - LOG_FORMAT = "%(asctime)s: %(module)-10s %(levelname)-8s %(message)s" - logging.basicConfig(format=LOG_FORMAT) - logger = logging.getLogger() - - logger.setLevel(logging.DEBUG) - if args.trace: - logger.setLevel(1) - - logger.debug(f"Environment variables: {os.environ}") - vrnetlab.boot_delay() - - if args.install: - vr = XRV_Installer( - args.hostname, - args.username, - args.password, - args.nics, - args.connection_mode, - args.vcpu, - args.ram, - ) - vr.install() - else: - vr = XRV( - args.hostname, - args.username, - args.password, - args.nics, - args.connection_mode, - args.vcpu, - args.ram, - ) - vr.start()