Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions common/vrnetlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,11 @@ def __init__(
mgmt_dhcp=False,
min_dp_nics=0,
use_scrapli=False,
data_intf_prefix="eth"
data_intf_prefix="eth",
arch="x86_64",
):
self.use_scrapli = use_scrapli
self.arch = arch

# configure logging
self.logger = logging.getLogger()
Expand Down Expand Up @@ -305,13 +307,15 @@ def __init__(
]
)

machine = "pc" if self.arch == "x86_64" else "virt,virtualization=on -accel tcg,tb-size=128"

# Build qemu args
self.qemu_args = [
"qemu-system-x86_64",
f"qemu-system-{self.arch}",
"-display",
"none",
"-machine",
"pc",
machine,
f"-chardev socket,id=monitor0,host=::,port=40{self.num:02d},server=on,wait=off",
"-monitor chardev:monitor0",
f"-chardev socket,id=serial0,host=::,port=50{self.num:02d},server=on,wait=off,telnet=on",
Expand All @@ -323,6 +327,9 @@ def __init__(
"-smp",
self.smp, # cpu core configuration
]
if self.arch == "aarch64":
# Use UEFI firmware for ARM64
self.qemu_args.extend(["-drive", "if=pflash,unit=0,format=raw,file=/usr/share/AAVMF/AAVMF_CODE.fd,readonly=on"])

# Always use -drive to create the disk device - migration requires exact device match
self.qemu_args.extend(["-drive", f"if={driveif},file={overlay_disk_image}"])
Expand All @@ -347,7 +354,7 @@ def start(self):
self.logger.info("END ENVIRONMENT VARIABLES".center(60, "-"))

self.logger.info(
f"Launching {self.__class__.__name__} with {self.smp} SMP/VCPU and {self.ram} M of RAM"
f"Launching {self.__class__.__name__} arch {self.arch} with {self.smp} SMP/VCPU and {self.ram} M of RAM"
)

# give nice colours. Red if disabled, Green if enabled
Expand Down Expand Up @@ -438,6 +445,9 @@ def start(self):
self.scrapli_tn.open()
else:
self.tn = telnetlib.Telnet("127.0.0.1", 5000 + self.num)
# This enables super verbose telnetlib debugging
# if self.logger.isEnabledFor(logging.DEBUG):
# self.tn.set_debuglevel(2)
break
except:
self.logger.error(
Expand Down
23 changes: 19 additions & 4 deletions makefile.include
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
IMG_NAME=$(shell echo $(NAME) | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')
IMG_VENDOR=$(shell echo $(VENDOR) | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')
IMG_REPOSITORY=$(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME)
IMAGES=$(shell ls $(IMAGE_GLOB) 2>/dev/null)
NUM_IMAGES=$(shell ls $(IMAGES) | wc -l)
VRNETLAB_VERION=$$(git log -1 --format=format:"Commit: %H from %aD")

# If DOCKER_PLATFORM is set, add --platform flag to docker build
ifdef DOCKER_PLATFORM
DOCKER_PLATFORM_ARG=--platform $(DOCKER_PLATFORM)
else
DOCKER_PLATFORM_ARG=
endif

ifeq ($(NUM_IMAGES), 0)
docker-image: no-image usage
else
Expand All @@ -28,7 +36,7 @@ docker-build-image-copy:
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)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION)"
@echo "Building docker image using $(IMAGE) as $(IMG_REPOSITORY):$(VERSION)"
ifeq ($(DONT_COPY_COMMON), 1)
echo "Don't need to copy common assets"
else
Expand All @@ -42,19 +50,26 @@ else
endif
@[ -f ./vswitch.xml ] && cp vswitch.xml docker/ || true
$(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) .)
(cd docker; docker buildx build $(DOCKER_PLATFORM_ARG) --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 $(IMG_REPOSITORY):$(VERSION) .)

docker-build: docker-build-common docker-clean-build
# Override docker-build-extra in your Makefile to run additional commands after build
docker-build-extra: ;

docker-build: docker-build-common docker-build-extra docker-clean-build

docker-push:
for IMAGE in $(IMAGES); do \
$(MAKE) IMAGE=$$IMAGE docker-push-image; \
done

# Override docker-push-image-extra in your Makefile to push additional tags
docker-push-image-extra: ;

docker-push-image:
@if [ -z "$$IMAGE" ]; then echo "ERROR: No IMAGE specified"; exit 1; fi
@if [ "$(IMAGE)" = "$(VERSION)" ]; then echo "ERROR: Incorrect version string"; exit 1; fi
docker push $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION)
docker push $(IMG_REPOSITORY):$(VERSION)
$(MAKE) docker-push-image-extra

usage:
@echo "Usage: put the $(VENDOR) $(NAME) $(IMAGE_FORMAT) image in this directory and run:"
Expand Down
27 changes: 23 additions & 4 deletions mikrotik/routeros/Makefile
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
VENDOR=Mikrotik
NAME=RouterOS
IMAGE_FORMAT=vmdk
IMAGE_GLOB=*.vmdk
IMAGE_FORMAT=vmdk/vdi
IMAGE_GLOB=*.vmdk *.vdi

# match versions like:
# chr-6.39.2.vmdk
VERSION=$(shell echo $(IMAGE) | sed -rn 's/.*chr-(.+)\.vmdk/\1/p')
# chr-6.39.2.vmdk -> 6.39.2
# chr-7.20.4-arm64.vdi -> 7.20.4-arm64
VERSION=$(shell echo $(IMAGE) | sed -rn 's/.*chr-(.+)\.(vmdk|vdi)/\1/p')

# Detect target platform from image filename
# arm64 images have -arm64 in the filename, otherwise assume amd64
DOCKER_PLATFORM=$(shell if echo "$(VERSION)" | grep -q '\-arm64'; then echo "linux/arm64"; else echo "linux/amd64"; fi)

-include ../../makefile-sanity.include
-include ../../makefile.include

# Add -amd64 tag for amd64 images (for consistency with -arm64 tags)
# The base version tag is kept for backward compatibility
docker-build-extra:
@if ! echo "$(VERSION)" | grep -q '\-arm64'; then \
echo "Adding -amd64 tag for $(IMG_REPOSITORY):$(VERSION)"; \
docker tag $(IMG_REPOSITORY):$(VERSION) $(IMG_REPOSITORY):$(VERSION)-amd64; \
fi

docker-push-image-extra:
@if ! echo "$(VERSION)" | grep -q '\-arm64'; then \
echo "Pushing -amd64 tag for $(IMG_REPOSITORY):$(VERSION)"; \
docker push $(IMG_REPOSITORY):$(VERSION)-amd64; \
fi
15 changes: 13 additions & 2 deletions mikrotik/routeros/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,23 @@
This is the vrnetlab docker image for Mikrotik RouterOS (ROS).

## Building the docker image
Download the Cloud Hosted Router VMDK image from https://www.mikrotik.com/download
Download the Cloud Hosted Router (CHR) VMDK or arm64 VDI image from https://www.mikrotik.com/download
Copy the vmdk image into this folder, then run `make docker-image`.

### Cross platform builds

It is possible to build amd64 images on ARM64 Macs, due to the built in Rosetta 2 emulation.

For building arm64 images on amd64 machines please resort to:
https://docs.docker.com/build/building/multi-platform/#strategies


Tested booting and responding to SSH:
* chr-6.39.2.vmdk MD5:eb99636e3cdbd1ea79551170c68a9a27
* chr-6.47.9.vmdk
* chr-7.1beta5.vmdk
* chr-7.16.2.vmdk
* chr-7.20.4-arm64.vdi


## System requirements
Expand All @@ -20,5 +29,7 @@ RAM: <1GB

Disk: <1GB

On Apple ARM64 systems a CPU with nested virtualization support is needed. (M3 or greater)

## Containerlab
Containerlab kind for routeros is [vr-ros](https://containerlab.srlinux.dev/manual/kinds/vr-ros/).
Containerlab kind for routeros is [mikrotik_ros](https://containerlab.dev/manual/kinds/vr-ros/).
13 changes: 10 additions & 3 deletions mikrotik/routeros/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
FROM public.ecr.aws/docker/library/debian:bookworm-slim
FROM public.ecr.aws/docker/library/debian:trixie-slim
LABEL org.opencontainers.image.authors="[email protected]"

COPY --from=ghcr.io/astral-sh/uv:0.9.13 /uv /uvx /bin/

ARG DEBIAN_FRONTEND=noninteractive

RUN apt-get update -qy \
Expand All @@ -12,6 +14,7 @@ RUN apt-get update -qy \
socat \
qemu-kvm \
qemu-system-x86 \
qemu-efi-aarch64 \
tcpdump \
ssh \
inetutils-ping \
Expand All @@ -22,10 +25,14 @@ RUN apt-get update -qy \
ftp \
&& rm -rf /var/lib/apt/lists/*

# use uv to install python 3.12. trixie ships with 3.13, which has no telnet package
RUN uv python install 3.12
RUN uv python pin 3.12

ARG IMAGE
COPY $IMAGE* /
COPY *.py /

EXPOSE 22 161/udp 830 5000 5678 8291 10000-10099
HEALTHCHECK CMD ["/healthcheck.py"]
ENTRYPOINT ["/launch.py"]
HEALTHCHECK CMD ["uv", "run", "/healthcheck.py"]
ENTRYPOINT ["uv", "run", "/launch.py"]
38 changes: 29 additions & 9 deletions mikrotik/routeros/docker/launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,29 @@ def trace(self, message, *args, **kws):
class ROS_vm(vrnetlab.VM):
def __init__(self, hostname, username, password, conn_mode):
for e in os.listdir("/"):
if re.search(".vmdk$", e):
if re.search("(.vmdk|.vdi)$", e):
disk_image = "/" + e

# determine architecture from disk image name
if re.search("-arm64", disk_image):
arch = "aarch64"
else:
arch = "x86_64"

if arch == "aarch64":
ram_size = 512 # arm64 needs more ram
cpu_type = "cortex-a72" # seems to be faster than cortex-a710
else:
ram_size = 256
cpu_type = "qemu64"

# the default cpu=host only works when running clab on an amd64 machine
extra_args = {} if platform.machine() == "x86_64" else {"cpu": "qemu64"}
extra_args = {} if platform.machine() == "x86_64" else {"cpu": cpu_type}

super(ROS_vm, self).__init__(username, password, disk_image=disk_image, ram=ram_size, driveif="virtio", arch=arch, **extra_args)
if self.arch != "aarch64":
self.qemu_args.extend(["-boot", "n"])

super(ROS_vm, self).__init__(username, password, disk_image=disk_image, ram=256, **extra_args)
self.qemu_args.extend(["-boot", "n"])
self.hostname = hostname
self.conn_mode = conn_mode
self.nic_type = "virtio-net" # "e1000" is default but breaks mtu > 1500 on vlan subinterfaces on RouterOS 6.x.x
Expand Down Expand Up @@ -89,7 +104,7 @@ def gen_mgmt(self):
res.append("-device")

res.append(
self.nic_type + ",netdev=br-mgmt,mac=%(mac)s" % {"mac": self.get_mgmt_mac()}
self.nic_type + ",romfile=,netdev=br-mgmt,mac=%(mac)s" % {"mac": self.get_mgmt_mac()}
)
res.append("-netdev")
res.append("bridge,br=br-mgmt,id=br-mgmt" % {"i": 0})
Expand Down Expand Up @@ -120,17 +135,22 @@ def bootstrap_spin(self):
elif ridx == 1:
self.wait_write("admin+ct", wait="RouterOS Login: ")
self.wait_write("", wait="Password: ")
self.wait_write(
"n", wait="Do you want to see the software license? [Y/n]: "
)

# not happening on arm64
if self.arch != "aarch64":
self.wait_write(
"n", wait="Do you want to see the software license? [Y/n]: "
)

# 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.tn.expect([b"new password>"], 10)
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>")
self.wait_write(f"{self.password}", wait="repeat new password>")
changed = self.tn.read_until(b"Password changed", 10)
self.logger.debug(f"Got '{changed}' for password change response")

self.logger.debug("Login completed")

Expand Down