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 SONiC VM support #214

Closed
wants to merge 3 commits into from
Closed
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
12 changes: 12 additions & 0 deletions sonic/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
VENDOR=Sonic
NAME=sonic-vs
IMAGE_FORMAT=qcow
IMAGE_GLOB=*.qcow2
IMAGE=sonic-vs-202305.qcow2

# match versions like:
# 202305
VERSION=$(shell echo $(IMAGE) | sed -e 's/sonic-vs-//' | sed -e 's/.qcow2//')

-include ../makefile-sanity.include
-include ../makefile.include
29 changes: 29 additions & 0 deletions sonic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# SONiC VM

This is the vrnetlab docker image for SONiC's VM.
The scripts in this directory are based on FreeBSD and VSRX kinds.

> Available with [containerlab](https://containerlab.dev) as `vr-sonic` kind.

## Building the docker image

Download the latest `sonic-vs.img.gz` image from https://sonic.software/
unompress and place it in this directory. Rename the file to `sonic-vs-[version].qcow2` and reference that file in the `Makefile`.
hellt marked this conversation as resolved.
Show resolved Hide resolved

After typing `make`, a new image will appear called `vrnetlab/vr-sonic` tagged with version.
Run `docker images` to confirm this.

## System requirements

- CPU: 2 cores
- RAM: 4GB
- DISK: ~3.2GB

## Configuration

SONiC nodes boot with a basic configuration by default, enabling SSH and basic management connectivity. All factory default configuration is retained.
Full startup configuration can be passed by mounting it under `/config/config_db.json`, this is done automatically by Containerlab. Only SONiC json config format is accepted. This fill will replace existing default config.

## Contact

The author of this code is Adam Kulagowski (adam.kulagowski@codilime.com), CodiLime (codilime.com), feel free to reach him in case of problems.
24 changes: 24 additions & 0 deletions sonic/docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FROM debian:bookworm-slim

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update -qy \
&& apt-get upgrade -qy \
&& apt-get install -y \
bridge-utils \
iproute2 \
python3-ipy \
socat \
ssh \
sshpass \
qemu-kvm \
&& rm -rf /var/lib/apt/lists/*

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

EXPOSE 22 443 5000 8080
HEALTHCHECK CMD ["/healthcheck.py"]
ENTRYPOINT ["/launch.py"]
83 changes: 83 additions & 0 deletions sonic/docker/backup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/bin/bash

DEFAULT_USER="admin"
DEFAULT_PASSWORD="admin"
REMOTE_FILE="/etc/sonic/config_db.json"
TMP_FILE="/tmp/${REMOTE_FILE##*/}"
BACKUP_FILE="/config/${REMOTE_FILE##*/}"

handle_args() {
# Parse options
while getopts 'u:p:' OPTION; do
case "$OPTION" in
u)
user="$OPTARG"
;;
p)
password="$OPTARG"
;;
?)
usage
exit 1
;;
esac
done
shift "$(($OPTIND -1))"

# Assign defaults if options weren't provided
if [ -z "$user" ] ; then
user=$DEFAULT_USER
fi
if [ -z "$password" ] ; then
password=$DEFAULT_PASSWORD
fi

SSH_CMD="sshpass -p $password ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2022"
SCP_CMD="sshpass -p $password scp -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -P 2022"
HOST="$user@127.0.0.1"

# Parse commands
case $1 in

backup)
backup
;;

restore)
restore
;;

*)
usage
;;
esac
}

usage() {
echo "Usage: $(basename $0) [-u USERNAME] [-p PASSWORD] COMMAND"
echo "Options:"
echo " -u USERNAME VM SSH username (default: $DEFAULT_USER)"
echo " -p PASSWORD VM SSH password (default: $DEFAULT_PASSWORD)"
echo
echo "Commands:"
echo " backup Backup VM $REMOTE_FILE directory to $BACKUP_FILE"
echo " restore Restore VM $REMOTE_FILE directory from $BACKUP_FILE"
exit 0;
}

backup() {
echo "Backing up..."
$SCP_CMD $HOST:$REMOTE_FILE $BACKUP_FILE
}

restore() {
if [ -f "$BACKUP_FILE" ]; then
echo "Restoring from backup..."

$SCP_CMD $BACKUP_FILE $HOST:$TMP_FILE && $SSH_CMD $HOST "sudo cp $TMP_FILE $REMOTE_FILE && sudo config reload -y -f || true"
else
echo "$BACKUP_FILE not found. Nothing to restore."
fi
}

handle_args "$@"
156 changes: 156 additions & 0 deletions sonic/docker/launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#!/usr/bin/env python3

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

import vrnetlab

CONFIG_FILE = "/config/config_db.json"
DEFAULT_USER="admin"
DEFAULT_PASSWORD="YourPaSsWoRd"

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 SONiC_vm(vrnetlab.VM):
def __init__(self, hostname, username, password, conn_mode):
disk_image = "/"
for e in os.listdir("/"):
if re.search(".qcow2$", e):
disk_image = "/" + e
break
super(SONiC_vm, self).__init__(username, password, disk_image=disk_image, ram=4096)
self.qemu_args.extend(["-smp", "2"])
self.nic_type = "virtio-net-pci"
self.conn_mode = conn_mode
self.num_nics = 10
self.hostname = hostname

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"login:"], 1)
if match and ridx == 0: # login
self.logger.info("VM started")

# Login
self.wait_write("\r", None)
self.wait_write(DEFAULT_USER, wait="login:")
self.wait_write(DEFAULT_PASSWORD, wait="Password:")
self.wait_write("", wait="%s@" %(self.username))
self.logger.info("Login completed")

# run main config!
self.bootstrap_config()
self.startup_config()
# 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

def bootstrap_config(self):
""" Do the actual bootstrap config
"""
self.logger.info("applying bootstrap configuration")
self.wait_write("sudo -i", "$")
self.wait_write("/usr/sbin/ip address add 10.0.0.15/24 dev eth0", "#")
self.wait_write("passwd -q %s" %(self.username))
self.wait_write(self.password, "New password:")
self.wait_write(self.password, "password:")
self.wait_write("sleep 1", "#")
self.wait_write("hostnamectl set-hostname %s" %(self.hostname))
self.wait_write("sleep 1", "#")
self.logger.info("completed bootstrap configuration")

def startup_config(self):
"""Load additional config provided by user."""

if not os.path.exists(CONFIG_FILE):
self.logger.trace(f"Backup file {CONFIG_FILE} not found")
return

self.logger.trace(f"Backup file {CONFIG_FILE} exists")

subprocess.run(
f"/backup.sh -u {self.username} -p {self.password} restore",
check=True,
shell=True
)


class SONiC(vrnetlab.VR):
def __init__(self, hostname, username, password, conn_mode):
super(SONiC, self).__init__(username, password)
self.vms = [ SONiC_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="sonic", help="SONiC hostname")
parser.add_argument("--username", default="admin", help="Username")
parser.add_argument("--password", default="admin", 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 = SONiC(args.hostname,
args.username,
args.password,
conn_mode=args.connection_mode,
)
vr.start()