forked from vrnetlab/vrnetlab
-
Notifications
You must be signed in to change notification settings - Fork 77
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
1 parent
c56ef35
commit 42cfaab
Showing
5 changed files
with
315 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 "$@" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |