Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add vJunos-router support #196

Merged
merged 3 commits into from
May 28, 2024
Merged
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
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