Skip to content

Commit

Permalink
Sonic vm (#220)
Browse files Browse the repository at this point in the history
* Add SONIC VM support

* Fixing:
- bad directory name
- major issues reported by DeepSource

* Changed:
- fixes default password
- readded shell=true

* no install recommends for the apt

* formatted with ruff

---------

Co-authored-by: Adam Kulagowski <adam.kulagowski@codilime.com>
  • Loading branch information
hellt and adam-kulagowski committed Jul 3, 2024
1 parent c56ef35 commit 42cfaab
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 0 deletions.
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
30 changes: 30 additions & 0 deletions sonic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# 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 using the options documented on the [containerlab.dev website](https://containerlab.dev/manual/kinds/sonic-vm/).

Uncompress and place the `.img` file in this directory. Rename the file to `sonic-vs-[version].qcow2` and run `make`.

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 public.ecr.aws/docker/library/debian:bookworm-slim

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update -qy \
&& apt-get install -y --no-install-recommends \
bridge-utils \
iproute2 \
python3-ipy \
qemu-kvm \
qemu-utils \
socat \
ssh \
sshpass \
&& 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 "$@"
166 changes: 166 additions & 0 deletions sonic/docker/launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#!/usr/bin/env python3

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

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(f"Startup complete in: {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().__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()

0 comments on commit 42cfaab

Please sign in to comment.