Skip to content

Commit

Permalink
Add vJunos-router support (#196)
Browse files Browse the repository at this point in the history
* Initial vJunos-router support

* Spelling correction

* use ecr.aws

---------

Co-authored-by: Roman Dodin <dodin.roman@gmail.com>
  • Loading branch information
tomncarter and hellt committed May 28, 2024
1 parent d273133 commit 1e1ec73
Show file tree
Hide file tree
Showing 7 changed files with 339 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Since the changes we made in this fork are VM specific, we added a few popular r
* Juniper vMX
* Juniper vSRX
* Juniper vJunos-switch
* Juniper vJunos-router
* Juniper vJunosEvolved
* Nokia SR OS
* OpenBSD
Expand Down
12 changes: 12 additions & 0 deletions vjunosrouter/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
VENDOR=Juniper
NAME=vJunos-router
IMAGE_FORMAT=qcow
IMAGE_GLOB=*.qcow2

# 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
15 changes: 15 additions & 0 deletions vjunosrouter/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# vrnetlab / Juniper vJunos-router

This is the vrnetlab docker image for Juniper's vJunos-router. This is built from the vJunos-switch template.

## Building the docker image

Download the vJunos-router .qcow2 image from <https://support.juniper.net/support/downloads/?p=vjunos-router>
and place it in this directory. After typing `make`, a new image will appear called `vrnetlab/vjunosrouter`.
Run `docker images` to confirm this.

## System requirements

CPU: 4 cores
RAM: 5GB
DISK: ~4.5GB
30 changes: 30 additions & 0 deletions vjunosrouter/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
FROM public.ecr.aws/docker/library/debian:bookworm-slim

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update -qy \
&& apt-get install -y \
dosfstools \
bridge-utils \
iproute2 \
socat \
ssh \
qemu-kvm \
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"]
31 changes: 31 additions & 0 deletions vjunosrouter/docker/init.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
system {
host-name {HOSTNAME};
root-authentication {
plain-text-password-value "admin@123";
}
login {
user admin {
class super-user;
authentication {
plain-text-password-value "admin@123";
}
}
}
services {
ssh {
root-login allow;
}
netconf {
ssh;
}
}
}
interfaces {
fxp0 {
unit 0 {
family inet {
address 10.0.0.15/24;
}
}
}
}
199 changes: 199 additions & 0 deletions vjunosrouter/docker/launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
#!/usr/bin/env python3
import datetime
import logging
import os
import re
import signal
import subprocess
import sys

import vrnetlab

# loadable startup config
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 VJUNOSROUTER_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(VJUNOSROUTER_vm, self).__init__(
username, password, disk_image=disk_image, ram=5120
)
# device hostname
self.hostname = hostname

# read init.conf configuration file to replace hostname placehodler
# with given hostname
with open("init.conf", "r") as file:
cfg = file.read()

# replace HOSTNAME file var with nodes given hostname
new_cfg = cfg.replace("{HOSTNAME}", hostname)

# write changes to init.conf file
with open("init.conf", "w") as file:
file.write(new_cfg)

# pass in user startup config
self.startup_config()

# these QEMU cmd line args are translated from the shipped libvirt XML file
self.qemu_args.extend(["-smp", "4,sockets=1,cores=4,threads=1"])
# Additional CPU info
self.qemu_args.extend(
[
"-cpu",
"IvyBridge,vme=on,ss=on,vmx=on,f16c=on,rdrand=on,hypervisor=on,arat=on,tsc-adjust=on,umip=on,arch-capabilities=on,pdpe1gb=on,skip-l1dfl-vmentry=on,pschange-mc-no=on,bmi1=off,avx2=off,bmi2=off,erms=off,invpcid=off,rdseed=off,adx=off,smap=off,xsaveopt=off,abm=off,svm=on",
]
)
# mount config disk with juniper.conf base configs
self.qemu_args.extend(
[
"-drive",
"if=none,id=config_disk,file=/config.img,format=raw",
"-device",
"virtio-blk-pci,drive=config_disk",
]
)
self.qemu_args.extend(["-overcommit", "mem-lock=off"])
self.qemu_args.extend(
["-display", "none", "-no-user-config", "-nodefaults", "-boot", "strict=on"]
)
self.nic_type = "virtio-net-pci"
self.num_nics = 11
self.smbios = ["type=1,product=VM-VMX,family=lab"]
self.qemu_args.extend(["-machine", "pc,usb=off,dump-guest-core=off,accel=kvm"])
self.qemu_args.extend(
["-device", "piix3-usb-uhci,id=usb,bus=pci.0,addr=0x1.0x2"]
)
self.conn_mode = conn_mode

def startup_config(self):
"""Load additional config provided by user and append initial
configurations set by vrnetlab."""
# if startup cfg DNE
if not os.path.exists(STARTUP_CONFIG_FILE):
self.logger.trace(f"Startup config file {STARTUP_CONFIG_FILE} is not found")
# rename init.conf to juniper.conf, this is our startup config
os.rename("init.conf", "juniper.conf")

# if startup cfg file is found
else:
self.logger.trace(
f"Startup config file {STARTUP_CONFIG_FILE} found, appending initial configuration"
)
# append startup cfg to inital configuration
append_cfg = f"cat init.conf {STARTUP_CONFIG_FILE} >> juniper.conf"
subprocess.run(append_cfg, shell=True)

# generate mountable config disk based on juniper.conf file with base vrnetlab configs
subprocess.run(["./make-config.sh", "juniper.conf", "config.img"], check=True)

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

# 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)
if match: # got a match!
if ridx == 0: # login
self.logger.info("VM started")

# Login
self.wait_write("\r", None)
self.wait_write("admin", wait="login:")
self.wait_write(self.password, wait="Password:")
self.wait_write("\r", None)
self.logger.info("Login completed")

# 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


class VJUNOSROUTER(vrnetlab.VR):
def __init__(self, hostname, username, password, conn_mode):
super(VJUNOSROUTER, self).__init__(username, password)
self.vms = [VJUNOSROUTER_vm(hostname, username, password, 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-VJUNOSROUTER", help="vJunos-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 = VJUNOSROUTER(
args.hostname,
args.username,
args.password,
conn_mode=args.connection_mode,
)
vr.start()
51 changes: 51 additions & 0 deletions vjunosrouter/docker/make-config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/bin/bash
# Create a config metadisk from a supplied juniper.conf to attach
# to a vJunos VM instance
usage() {
echo "Usage : make-config.sh <juniper-config> <config-disk>"
exit 0;
}
cleanup () {
echo "Cleaning up..."
umount -f -q $MNTDIR
losetup -d $LOOPDEV
rm -rfv $STAGING
rm -rfv $MNTDIR
}

cleanup_failed () {
cleanup;
rm -rfv $2
exit 1
}

if [ $# != 2 ]; then
usage;
fi


STAGING=`mktemp -d -p /var/tmp`
MNTDIR=`mktemp -d -p /var/tmp`
mkdir $STAGING/config
cp -v $1 $STAGING/config
qemu-img create -f qcow2 $2 1M
LOOPDEV=`losetup --show -f $2`
if [ $? != 0 ]; then
cleanup_failed;
fi
mkfs.vfat -v -n "vmm-data" $LOOPDEV
if [ $? != 0 ]; then
echo "Failed to format disk $LOOPDEV; exiting"
cleanup_failed;
fi
mount -t vfat $LOOPDEV $MNTDIR
if [ $? != 0 ]; then
echo "Failed to mount metadisk $LOOPDEV; exiting"
cleanup_failed;

fi
echo "Copying file(s) to config disk $2"
(cd $STAGING; tar cvzf $MNTDIR/vmm-config.tgz .)
cleanup
echo "Config disk $2 created"
exit 0

0 comments on commit 1e1ec73

Please sign in to comment.