Skip to content

Commit

Permalink
Merge pull request #152 from akielaries/vJunosEvolved
Browse files Browse the repository at this point in the history
support for vJunosEvolved
  • Loading branch information
hellt authored Dec 9, 2023
2 parents b02e319 + 7be9b32 commit 6486509
Show file tree
Hide file tree
Showing 10 changed files with 338 additions and 3 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
IMAGES_DIR=
VRS = vr-xcon vr-bgp csr nxos routeros sros veos vjunosswitch vmx vsr1000 vqfx vrp xrv xrv9k vsrx openbsd
VRS = vr-xcon vr-bgp csr nxos routeros sros veos vjunosswitch vjunosevolved vmx vsr1000 vqfx vrp xrv xrv9k vsrx openbsd
VRS_PUSH = $(VRS:=-push)

.PHONY: all $(VRS) $(VRS_PUSH)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,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 vJunosEvolved
* Nokia SR OS
* OpenBSD

Expand Down
7 changes: 6 additions & 1 deletion config-engine-lite/configengine
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ class Router:
driver = napalm.get_network_driver("junos")
elif self.type == "vjunosswitch":
driver = napalm.get_network_driver("junos")
elif self.type == "vjunosevolved":
driver = napalm.get_network_driver("junos")
elif self.type == "csr":
driver = napalm.get_network_driver("ios")
else:
Expand Down Expand Up @@ -166,6 +168,9 @@ class ConfigBootstrap:
if router.type == "vjunosswitch":
router.template = junos

if router.type == "vjunosevolved":
router.template = junos

if router.type == "csr":
router.template = ios

Expand Down Expand Up @@ -292,7 +297,7 @@ if __name__ == '__main__':
if not args.config or not os.path.isfile(args.config):
print("Configuration template doesn't exist")
sys.exit(1)
if args.type not in [ "vmx", "csr", "xrv", "vsrx", "vjunosswitch"]:
if args.type not in [ "vmx", "csr", "xrv", "vsrx", "vjunosswitch", "vjunosevolved"]:
print("Invalid router type {}".format(args.type))
sys.exit(1)

Expand Down
4 changes: 3 additions & 1 deletion topology-machine/topomachine
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class VrTopo:
for r, val in self.routers.items():
if 'type' not in val:
raise ValueError("'type' is not defined for router %s" % r)
if val['type'] not in ('dummy', 'xcon', 'bgp', 'xrv', 'xrv9k', 'vmx', 'sros', 'csr', 'vjunosswitch', 'vqfx', 'vrp', 'vsrx'):
if val['type'] not in ('dummy', 'xcon', 'bgp', 'xrv', 'xrv9k', 'vmx', 'sros', 'csr', 'vjunosswitch', 'vjunosevolved', 'vqfx', 'vrp', 'vsrx'):
raise ValueError("Unknown type %s for router %s" % (val['type'], r))

# expand p2p links
Expand Down Expand Up @@ -140,6 +140,8 @@ class VrTopo:
return "ge-0/0/%d" % (interface-1)
elif r['type'] == 'vjunosswitch':
return "ge-0/0/%d" % (interface-1)
elif r['type'] == 'vjunosevolved':
return "et-0/0/%d" % (interface-1)

return None

Expand Down
15 changes: 15 additions & 0 deletions vjunosevolved/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
VENDOR=Juniper
NAME=vJunosEvolved
IMAGE_FORMAT=qcow
IMAGE_GLOB=*.qcow2

# match versions like:
# vJunosEvolved-23.1R1.8.qcow2
# vJunosEvolved-22.1R2.2.qcow2
# vJunosEvolved-21.2R4.3.qcow2
# ...

VERSION=$(shell echo $(IMAGE) | sed -e 's/vjunosevolved-//i' | sed -e 's/.qcow2//i')

-include ../makefile-sanity.include
-include ../makefile.include
17 changes: 17 additions & 0 deletions vjunosevolved/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# vrnetlab / Juniper vJunosEvolved

This is the vrnetlab docker image for Juniper's vJunosEvolved.

> Available with [containerlab](https://containerlab.dev) as juniper_vjunosevolved.
## Building the docker image

Download the vJunosEvolved .qcow2 image from <https://www.juniper.net/us/en/dm/vjunos-labs.html>
and place it in this directory. After typing `make`, a new image will appear called `vrnetlab/vjunosevolved`.
Run `docker images` to confirm this.

## System requirements

CPU: 4 cores
RAM: 8GB
DISK: ~2.5GB
28 changes: 28 additions & 0 deletions vjunosevolved/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
FROM debian:bookworm-slim

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update -qy \
&& apt-get upgrade -qy \
&& apt-get install -y \
dosfstools \
bridge-utils \
iproute2 \
python3-ipy \
socat \
qemu-kvm \
&& 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 vjunosevolved/docker/init.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
system {
host-name {HOSTNAME};
root-authentication {
encrypted-password "{CRYPT_PSWD}"; ## SECRET-DATA
}
login {
user admin {
class super-user;
authentication {
encrypted-password "{CRYPT_PSWD}"; ## SECRET-DATA
}
}
}
services {
ssh {
root-login allow;
}
netconf {
ssh;
}
}
}
interfaces {
re0:mgmt-0 {
unit 0 {
family inet {
address 10.0.0.15/24;
}
}
}
}
185 changes: 185 additions & 0 deletions vjunosevolved/docker/launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
#!/usr/bin/env python3

import datetime
import logging
import os
import subprocess
import re
import signal
import sys
import uuid
import crypt

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 VJUNOSEVOLVED_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(VJUNOSEVOLVED_vm, self).__init__(username, password, disk_image=disk_image, ram=8192)

# device hostname
self.hostname = hostname
# create SHA-512 hash of the password
password_hash = crypt.crypt("admin@123", crypt.mksalt(crypt.METHOD_SHA512))

# 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
# replace CRYPT_PSWD file var with nodes given password
# (Evo does not accept plaintext passwords in config)
new_cfg = cfg.replace("{HOSTNAME}", hostname).replace("{CRYPT_PSWD}", password_hash)

# 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=off"
])
self.qemu_args.extend(["-overcommit", "mem-lock=off"])
# generate UUID to attach
self.qemu_args.extend(["-uuid", str(uuid.uuid4())])

# extend QEMU args with device USB details
self.qemu_args.extend(["-device", "piix3-usb-uhci,id=usb,bus=pci.0,addr=0x1.0x2"])

# mount config disk with juniper.conf base configs
self.qemu_args.extend([
"-drive",
"file=/config.img,format=raw,if=none,id=config_disk",
"-device",
"usb-storage,bus=usb.0,port=1,drive=config_disk,id=usb-disk0,removable=off,write-cache=on",
])

self.qemu_args.extend(["-no-user-config", "-nodefaults", "-boot", "strict=on"])
self.nic_type = "virtio-net-pci"
self.num_nics = 17
self.hostname = hostname
self.smbios = [
"type=0,vendor=Bochs,version=Bochs", "type=3,manufacturer=Bochs", "type=1,manufacturer=Bochs,product=Bochs,serial=chassis_no=0:slot=0:type=1:assembly_id=0x0D20:platform=251:master=0:channelized=no" ]
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"Juniper"], 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 VJUNOSEVOLVED(vrnetlab.VR):
def __init__(self, hostname, username, password, conn_mode):
super(VJUNOSEVOLVED, self).__init__(username, password)
self.vms = [ VJUNOSEVOLVED_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-vjunosevolved", help="vJunosEvolved 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 = VJUNOSEVOLVED(args.hostname,
args.username,
args.password,
conn_mode=args.connection_mode,
)
vr.start()
51 changes: 51 additions & 0 deletions vjunosevolved/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 raw $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 6486509

Please sign in to comment.