From b3a1ab513251056b855ff8a2eaa2037470fa51df Mon Sep 17 00:00:00 2001 From: rafabr Date: Mon, 14 Oct 2024 06:40:49 -0300 Subject: [PATCH] Add support for Huawei NE40E (#250) * Add support for Huawei NE40E * DeepSource suggestions/fixes * simplify sed and format python * reuse parent mgmt interface generator * Improve startup config * Add support to CE12800 * added platform name in the tag added cleanup of the docker build context after the image is built * use tftp instead of sftp --------- Co-authored-by: Rafael Brandao Co-authored-by: Roman Dodin --- huawei_vrp/Makefile | 12 ++ huawei_vrp/README.md | 27 ++++ huawei_vrp/docker/Dockerfile | 21 +++ huawei_vrp/docker/launch.py | 270 +++++++++++++++++++++++++++++++++++ makefile.include | 5 +- 5 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 huawei_vrp/Makefile create mode 100644 huawei_vrp/README.md create mode 100644 huawei_vrp/docker/Dockerfile create mode 100755 huawei_vrp/docker/launch.py diff --git a/huawei_vrp/Makefile b/huawei_vrp/Makefile new file mode 100644 index 00000000..d9de8e38 --- /dev/null +++ b/huawei_vrp/Makefile @@ -0,0 +1,12 @@ +VENDOR=Huawei +NAME=VRP +IMAGE_FORMAT=qcow2 +IMAGE_GLOB=*.qcow2 + +# match versions like: +# huawei_ne40e-.qcow2 +# huawei_ce12800-.qcow2 +VERSION=$(shell echo $(IMAGE) | sed -e 's/huawei_\(ne40e\|ce12800\)-\(.*\)\.qcow2/\1-\2/') + +-include ../makefile-sanity.include +-include ../makefile.include diff --git a/huawei_vrp/README.md b/huawei_vrp/README.md new file mode 100644 index 00000000..0a0b4799 --- /dev/null +++ b/huawei_vrp/README.md @@ -0,0 +1,27 @@ +# Huawei NE40E + +Rename your qcow2 disk image to conform to the following pattern: + +``` +huawei_ne40e-.qcow2 +or +huawei_ce12800-.qcow2 +``` + +Build the image with: + +``` +make +``` + +The resulting image will be tagged as: + +``` +vrnetlab/huawei_vrp:- +``` + +for example, if the qcow2 image is named `huawei_ne40e-8.180.qcow2`, then the image will be tagged as: + +``` +vrnetlab/huawei_vrp:ne40e-8.180 +``` diff --git a/huawei_vrp/docker/Dockerfile b/huawei_vrp/docker/Dockerfile new file mode 100644 index 00000000..210423ca --- /dev/null +++ b/huawei_vrp/docker/Dockerfile @@ -0,0 +1,21 @@ +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/* + +ARG IMAGE +COPY $IMAGE* / +COPY *.py / + +EXPOSE 22 80 161/udp 443 830 +HEALTHCHECK CMD ["/healthcheck.py"] +ENTRYPOINT ["/launch.py"] diff --git a/huawei_vrp/docker/launch.py b/huawei_vrp/docker/launch.py new file mode 100755 index 00000000..d71f9242 --- /dev/null +++ b/huawei_vrp/docker/launch.py @@ -0,0 +1,270 @@ +#!/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 VRP_vm(vrnetlab.VM): + def __init__(self, username, password, hostname, conn_mode): + disk_image = None + self.vm_type = "UNKNOWN" + for e in sorted(os.listdir("/")): + if not disk_image and re.search(".qcow2$", e): + disk_image = "/" + e + if "huawei_ne40e" in e: + self.vm_type = "NE40E" + if "huawei_ce12800" in e: + self.vm_type = "CE12800" + + super(VRP_vm, self).__init__( + username, + password, + disk_image=disk_image, + ram=2048, + smp="2", + driveif="virtio", + ) + + self.hostname = hostname + self.conn_mode = conn_mode + self.num_nics = 14 + self.nic_type = "virtio-net-pci" + + def bootstrap_spin(self): + """This function should be called periodically to do work.""" + + if self.spins > 300: + # too many spins with no result -> give up + self.stop() + self.start() + return + + (ridx, match, res) = self.tn.expect([b""], 1) + + if match and ridx == 0: # got a match! + # run main config! + self.logger.info("Running bootstrap_config()") + self.startup_config() + self.bootstrap_config() + time.sleep(1) + # 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 + + time.sleep(5) + + # 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_mgmt_interface(self): + self.wait_write(cmd="mmi-mode enable", wait=None) + self.wait_write(cmd="system-view", wait=">") + self.wait_write(cmd="ip vpn-instance __MGMT_VPN__", wait="]") + self.wait_write(cmd="ipv4-family", wait="]") + self.wait_write(cmd="quit", wait="]") + self.wait_write(cmd="quit", wait="]") + if self.vm_type == "CE12800": + mgmt_interface = "MEth" + if self.vm_type == "NE40E": + mgmt_interface = "GigabitEthernet" + self.wait_write(cmd=f"interface {mgmt_interface} 0/0/0", wait="]") + # 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) + if match and idx == 0: + time.sleep(5) + else: + break + self.wait_write(cmd="undo shutdown", wait=None) + self.wait_write(cmd="ip binding vpn-instance __MGMT_VPN__", wait="]") + self.wait_write(cmd="ip address 10.0.0.15 24", wait="]") + self.wait_write(cmd="quit", wait="]") + self.wait_write( + cmd="ip route-static vpn-instance __MGMT_VPN__ 0.0.0.0 0 10.0.0.2", wait="]" + ) + + def bootstrap_config(self): + """Do the actual bootstrap config""" + self.bootstrap_mgmt_interface() + self.wait_write(cmd=f"sysname {self.hostname}", wait="]") + + if self.vm_type == "CE12800": + self.wait_write(cmd="aaa", wait="]") + self.wait_write(cmd="undo local-user policy security-enhance", wait="]") + self.wait_write(cmd="quit", wait="]") + if self.vm_type == "NE40E": + self.wait_write(cmd="undo user-security-policy enable", wait="]") + + self.wait_write(cmd="aaa", wait="]") + self.wait_write(cmd=f"undo local-user {self.username}", wait="]") + self.wait_write( + cmd=f"local-user {self.username} password irreversible-cipher {self.password}", + wait="]", + ) + self.wait_write(cmd=f"local-user {self.username} service-type ssh", wait="]") + self.wait_write( + cmd=f"local-user {self.username} user-group manage-ug", wait="]" + ) + self.wait_write(cmd="quit", wait="]") + + # SSH + self.wait_write(cmd="user-interface vty 0 4", wait="]") + self.wait_write(cmd="authentication-mode aaa", wait="]") + self.wait_write(cmd="protocol inbound ssh", wait="]") + self.wait_write(cmd="quit", wait="]") + self.wait_write(cmd=f"undo ssh user {self.username}", wait="]") + self.wait_write( + cmd=f"ssh user {self.username} authentication-type password ", wait="]" + ) + self.wait_write(cmd=f"ssh user {self.username} service-type all ", wait="]") + self.wait_write(cmd="stelnet server enable", wait="]") + + # NETCONF + self.wait_write(cmd="snetconf server enable", wait="]") + self.wait_write(cmd="netconf", wait="]") + self.wait_write(cmd="protocol inbound ssh port 830", wait="]") + self.wait_write(cmd="quit", wait="]") + + self.wait_write(cmd="commit", wait="]") + self.wait_write(cmd="return", wait="]") + self.wait_write(cmd="save", wait=">") + self.wait_write(cmd="undo mmi-mode enable", wait=">") + + def startup_config(self): + if not os.path.exists(STARTUP_CONFIG_FILE): + self.logger.trace(f"Startup config file {STARTUP_CONFIG_FILE} not found") + return + + + vrnetlab.run_command(["cp", STARTUP_CONFIG_FILE, "/tftpboot/containerlab.cfg"]) + + + if self.vm_type == "CE12800": + with open(STARTUP_CONFIG_FILE, "r+") as file: + cfg = file.read() + modified = False + + if "device board 1 " not in cfg: + cfg = "device board 1 board-type CE-LPUE\n" + cfg + modified = True + + if "interface NULL0" not in cfg: + cfg = cfg + "\ninterface NULL0" + modified = True + + if modified: + file.seek(0) + file.write(cfg) + file.truncate() + + + self.bootstrap_mgmt_interface() + self.wait_write(cmd="commit", wait="]") + + + self.wait_write(cmd=f"return", wait="]") + time.sleep(1) + self.wait_write(cmd=f"tftp 10.0.0.2 vpn-instance __MGMT_VPN__ get containerlab.cfg", wait=">") + self.wait_write(cmd="startup saved-configuration containerlab.cfg", wait=">") + self.wait_write(cmd="reboot fast", wait=">") + self.wait_write(cmd="reboot", wait="#") + self.wait_write(cmd="", wait="The current login time is") + print(f"File '{STARTUP_CONFIG_FILE}' successfully loaded") + + def gen_mgmt(self): + """Generate qemu args for the mgmt interface(s)""" + # call parent function to generate the mgmt interface + res = super().gen_mgmt() + + # Creates required dummy interface + res.append(f"-device virtio-net-pci,netdev=dummy,mac={vrnetlab.gen_mac(0)}") + res.append("-netdev tap,ifname=vrp-dummy,id=dummy,script=no,downscript=no") + + return res + + +class VRP(vrnetlab.VR): + def __init__(self, hostname, username, password, conn_mode): + super(VRP, self).__init__(username, password) + self.vms = [VRP_vm(username, password, hostname, conn_mode)] + + +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-VRP", help="Router hostname") + parser.add_argument("--username", default="vrnetlab", help="Username") + parser.add_argument("--password", default="VR-netlab9", help="Password") + parser.add_argument( + "--connection-mode", + default="tc", + 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) + + vr = VRP( + args.hostname, args.username, args.password, conn_mode=args.connection_mode + ) + vr.start() diff --git a/makefile.include b/makefile.include index 10ef78aa..81d026af 100644 --- a/makefile.include +++ b/makefile.include @@ -11,11 +11,14 @@ docker-image: for IMAGE in $(IMAGES); do \ echo "Making $$IMAGE"; \ $(MAKE) IMAGE=$$IMAGE docker-build; \ + $(MAKE) IMAGE=$$IMAGE docker-clean-build; \ done endif docker-clean-build: + @echo "--> Cleaning docker build context" -rm -f docker/*.qcow2* docker/*.tgz* docker/*.vmdk* docker/*.iso docker/*.xml docker/*.bin + -rm -f docker/healthcheck.py docker/vrnetlab.py docker-pre-build: ; @@ -35,7 +38,7 @@ endif $(MAKE) IMAGE=$$IMAGE docker-build-image-copy (cd docker; docker build --build-arg http_proxy=$(http_proxy) --build-arg HTTP_PROXY=$(HTTP_PROXY) --build-arg https_proxy=$(https_proxy) --build-arg HTTPS_PROXY=$(HTTPS_PROXY) --build-arg IMAGE=$(IMAGE) --build-arg VERSION=$(VERSION) --label "vrnetlab-version=$(VRNETLAB_VERION)" -t $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION) .) -docker-build: docker-build-common +docker-build: docker-build-common docker-clean-build docker-push: for IMAGE in $(IMAGES); do \