├── .deepsource.toml ├── .env ├── .gitignore ├── .gitlab-ci.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── aoscx ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ └── launch.py ├── asav ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ └── launch.py ├── build-base-image.sh ├── c8000v ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ └── launch.py ├── cat9kv ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ └── launch.py ├── cisco └── iol │ ├── Makefile │ ├── README.md │ └── docker │ ├── Dockerfile │ └── entrypoint.sh ├── cmglinux ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ └── launch.py ├── common ├── healthcheck.py └── vrnetlab.py ├── csr ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ └── launch.py ├── dell_sonic ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ ├── backup.sh │ └── launch.py ├── dev-notes.md ├── exos ├── Makefile └── docker │ ├── Dockerfile │ └── launch.py ├── fortigate ├── .gitignore ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ ├── README.md │ └── launch.py ├── freebsd ├── Makefile ├── README.md ├── docker │ ├── Dockerfile │ ├── backup.sh │ └── launch.py └── download.sh ├── ftdv ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ └── launch.py ├── ftosv ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ └── launch.py ├── huawei_vrp ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ └── launch.py ├── makefile-install.include ├── makefile-sanity.include ├── makefile.include ├── n9kv ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ ├── OVMF.fd │ └── launch.py ├── nxos ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ └── launch.py ├── ocnos ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ └── launch.py ├── openbsd ├── Makefile ├── README.md ├── docker │ ├── Dockerfile │ ├── backup.sh │ └── launch.py └── download.sh ├── openwrt ├── Makefile ├── README.md ├── docker │ ├── Dockerfile │ └── launch.py └── download.py ├── pan ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ └── launch.py ├── pyproject.toml ├── routeros ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ └── launch.py ├── sonic ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ ├── backup.sh │ └── launch.py ├── sros ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ └── launch.py ├── ubuntu ├── Makefile ├── README.md ├── docker │ ├── Dockerfile │ └── launch.py └── download.sh ├── uv.lock ├── veos ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ └── launch.py ├── vios ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ └── launch.py ├── viosl2 ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ └── launch.py ├── vjunosevolved ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ ├── init.conf │ ├── launch.py │ └── make-config.sh ├── vjunosrouter ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ ├── init.conf │ ├── launch.py │ └── make-config.sh ├── vjunosswitch ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ ├── init.conf │ ├── launch.py │ └── make-config.sh ├── vmx ├── Makefile ├── README.md ├── docker │ ├── Dockerfile │ └── launch.py └── vmx-extract.sh ├── vqfx ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ └── launch.py ├── vrnetlab-base.dockerfile ├── vsr1000 ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ └── launch.py ├── vsrx ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ ├── init.conf │ ├── launch.py │ └── make-config-iso.sh ├── vstc ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ └── launch.py ├── xrv ├── Makefile ├── README.md └── docker │ ├── Dockerfile │ └── launch.py └── xrv9k ├── LICENSE ├── Makefile ├── README.md └── docker ├── Dockerfile └── launch.py /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "python" 5 | enabled = true 6 | 7 | [analyzers.meta] 8 | runtime_version = "3.x.x" 9 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PYTHONPATH=".:./common" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | venv 4 | *.qcow2 5 | cisco*.bin 6 | *.gz 7 | *.xz 8 | *.vmdk 9 | *.iso 10 | *cidfile 11 | 12 | .DS_Store 13 | */.DS_Store 14 | 15 | # These are files that get built by make and should not be checked in to the repo 16 | **/healthcheck.py 17 | **/vrnetlab.py 18 | # Still allow for the common python scripts 19 | !common/healthcheck.py 20 | !common/vrnetlab.py 21 | 22 | .envrc 23 | .python-version 24 | 25 | *.xml 26 | 27 | # ignore clab- dirs 28 | clab-* -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: vrnetlab/ci-builder 2 | 3 | stages: 4 | - build 5 | 6 | build: 7 | stage: build 8 | tags: 9 | - vrnetlab 10 | script: 11 | # make sure we pulled LFS files 12 | - git lfs pull 13 | # We allow the user to control which Docker registry is used through the 14 | # env var DOCKER_REGISTRY. If it is not set then we assume we should use 15 | # the GitLab built-in Docker registry so we check if it is enabled. 16 | # CI_REGISTRY is only set when the GitLab Docker registry is enabled 17 | - if [ -z "${DOCKER_REGISTRY}" ]; then if [ -n "${CI_REGISTRY}" ]; then export DOCKER_USER=gitlab-ci-token; export DOCKER_PASSWORD=${CI_BUILD_TOKEN}; export DOCKER_REGISTRY=${CI_REGISTRY_IMAGE}; fi; fi 18 | - 'echo "DOCKER_REGISTRY: ${DOCKER_REGISTRY}"' 19 | # if DOCKER_REGISTRY set, either explicitly by user or implicitly by GitLab 20 | # (see above) we login to repo, build images and push them 21 | - if [ -n "${DOCKER_REGISTRY}" ]; then docker login -u ${DOCKER_USER} -p=${DOCKER_PASSWORD} ${DOCKER_REGISTRY}; fi 22 | - if [ -n "${DOCKER_REGISTRY}" ]; then make && make docker-push; fi 23 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of conduct 2 | 3 | Please be civilised and nice to each other. We're all humans. Act like one. 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to vrnetlab 2 | 3 | Thank you for (considering) contributing to vrnetlab! 4 | 5 | ## Bugs? 6 | 7 | Please report bugs as issues here on Github! Also feel free to use issues for asking questions on how to use vrnetlab etc too. 8 | 9 | ## New feature? 10 | 11 | Do you want to build something new? Please open an issue describing your idea so that we can align how it is best implemented. Discussing before you start coding significantly increases the chance that your code will be merged smoothly and without lots of refactoring etc. 12 | 13 | If you don't want to code but have an idea for a new feature, please open an issue. 14 | 15 | ## Code submission 16 | 17 | Submit your code as a Pull Request (PR). Make sure it applies cleanly to the master branch! 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Kristian Larsson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vrnetlab - VR Network Lab 2 | 3 | This is a fork of the original [plajjan/vrnetlab](https://github.com/plajjan/vrnetlab) 4 | project and was created specifically to make vrnetlab-based images runnable by 5 | [containerlab](https://containerlab.srlinux.dev). 6 | 7 | The documentation provided in this fork only explains the parts that have been 8 | changed from the upstream project. To get a general overview of the vrnetlab 9 | project itself, consider reading the [docs of the upstream repo](https://github.com/vrnetlab/vrnetlab/blob/master/README.md). 10 | 11 | ## What is this fork about? 12 | 13 | At [containerlab](https://containerlab.srlinux.dev) we needed to have 14 | [a way to run virtual routers](https://containerlab.dev/manual/vrnetlab/) 15 | alongside the containerized Network Operating Systems. 16 | 17 | Vrnetlab provides perfect machinery to package most-common routing VMs in 18 | container packaging. What upstream vrnetlab doesn't do, though, is create 19 | datapaths between the VMs in a "container-native" way. 20 | 21 | Vrnetlab relies on a separate VM ([vr-xcon](https://github.com/vrnetlab/vrnetlab/tree/master/vr-xcon)) 22 | to stitch sockets exposed on each container and that doesn't play well with the 23 | regular ways of interconnecting container workloads. 24 | 25 | This fork adds the additional option `connection-mode` to the `launch.py` script 26 | of supported VMs. The `connection-mode` option controls how vrnetlab creates 27 | datapaths for launched VMs. 28 | 29 | The `connection-mode` values make it possible to run vrnetlab containers with 30 | networking that doesn't require a separate container and is native to tools such 31 | as docker. 32 | 33 | ### Container-native networking? 34 | 35 | Yes, the term is bloated. What it actually means is this fork makes it possible 36 | to add interfaces to a container hosting a qemu VM and vrnetlab will recognize 37 | those interfaces and stitch them with the VM interfaces. 38 | 39 | With this you can, for example, add veth pairs between containers as you would 40 | normally and vrnetlab will make sure these ports get mapped to your routers' 41 | ports. In essence, that allows you to work with your vrnetlab containers like a 42 | normal container and get the datapath working in the same "native" way. 43 | 44 | > [!IMPORTANT] 45 | > Although the changes we made here are of a general purpose and you can run 46 | > vrnetlab routers with docker CLI or any other container runtime, the purpose 47 | > of this work was to couple vrnetlab with containerlab. 48 | > 49 | > With this being said, we recommend the readers start their journey from 50 | > this [documentation entry](https://containerlab.dev/manual/vrnetlab/) 51 | > which will show you how easy it is to run routers in a containerized setting. 52 | 53 | ## Connection modes 54 | 55 | As mentioned above, the major change this fork brings is the ability to run 56 | vrnetlab containers without requiring [vr-xcon](https://github.com/vrnetlab/vrnetlab/tree/master/vr-xcon) 57 | and instead uses container-native networking. 58 | 59 | For containerlab the default connection mode value is `connection-mode=tc`. 60 | With this particular mode we use **tc-mirred** redirects to stitch a container's 61 | interfaces `eth1+` with the ports of the qemu VM running inside. 62 | 63 | ![diagram showing network connections via tc redirects](https://gitlab.com/rdodin/pics/-/wikis/uploads/4d31c06e6258e70edc887b17e0e758e0/image.png) 64 | 65 | Using tc redirection (tc-mirred) we get a transparent pipe between a container's 66 | interfaces and those of the VMs running within. 67 | 68 | We scrambled through many connection alternatives, which are described in 69 | [this post](https://netdevops.me/2021/transparently-redirecting-packetsframes-between-interfaces/), 70 | but tc redirect (tc-mirred :star:) works best of all. 71 | 72 | ### Mode List 73 | 74 | Full list of connection mode values: 75 | 76 | | Connection Mode | LACP Support | Description | 77 | | --------------- | :-----------------: | :---------- | 78 | | tc-mirred | :white_check_mark: | Creates a linux bridge and attaches `eth` and `tap` interfaces to it. Cleanest solution for point-to-point links. 79 | | bridge | :last_quarter_moon: | No additional kernel modules and has native qemu/libvirt support. Does not support passing STP. Requires restricting `MAC_PAUSE` frames in order to support LACP. 80 | | ovs-bridge | :white_check_mark: | Same as a regular bridge, but uses OvS (Open vSwitch). 81 | | macvtap | :x: | Requires mounting entire `/dev` to a container namespace. Needs file descriptor manipulation due to no native qemu support. 82 | 83 | ## Management interface 84 | 85 | There are two types of management connectivity for NOS VMs: _pass-through_ and _host-forwarded_ (legacy) management interfaces. 86 | 87 | _Pass-through management_ interfaces allows the use of the assigned management IP within the NOS VM, management traffic is transparently passed through to the VM, and the NOS configuration can accurately reflect the management IP. However, it is no longer possible to send or receive traffic directly in the vrnetlab container (e.g. for installing additional packages within the container), other than to pre-defined exceptions, such as the QEMU serial port on TCP port 5000. 88 | 89 | NOSes defaulting to _pass-through_ management interfaces are: 90 | 91 | * None so far, we are gathering feedback on this, and will update this list as feedback is received. Please contact us in [Discord](https://discord.gg/vAyddtaEV9) or open up an issue here if you have found any issues when trying the passthrough mode. 92 | 93 | In case of _host-forwarded_ management interfaces, certain ports are forwarded to the NOS VM IP, which is always 10.0.0.15/24. The management gateway in this case is 10.0.0.2/24, and outgoing traffic is NATed to the container management IP. This management interface connection mode does not allow for traffic such as LLDP to pass through the management interface. 94 | 95 | NOSes defaulting to _host-forwarded_ management interfaces are: 96 | 97 | * all current systems 98 | 99 | It is possible to change from the default management interface mode by setting the `CLAB_MGMT_PASSTHROUGH` environment variable to 'true' or 'false', however, it is left up to the user to provide a startup configuration compatible with the requested mode. 100 | 101 | ## Which vrnetlab routers are supported? 102 | 103 | Since the changes we made in this fork are VM specific, we added a few popular 104 | routing products: 105 | 106 | * Arista vEOS 107 | * Cisco XRv9k 108 | * Cisco XRv 109 | * Cisco FTDv 110 | * Juniper vMX 111 | * Juniper vSRX 112 | * Juniper vJunos-switch 113 | * Juniper vJunos-router 114 | * Juniper vJunosEvolved 115 | * Nokia SR OS 116 | * OpenBSD 117 | * FreeBSD 118 | * Ubuntu 119 | 120 | The rest are left untouched and can be contributed back by the community. 121 | 122 | ## Does the build process change? 123 | 124 | No. You build the images exactly as before. 125 | -------------------------------------------------------------------------------- /aoscx/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Aruba 2 | NAME=ArubaOS-CX 3 | IMAGE_FORMAT=vmdk 4 | IMAGE_GLOB=*.vmdk 5 | 6 | # match versions like: 7 | # arubaoscx-disk-image-genericx86-p4-20230531220439.vmdk 8 | VERSION=$(shell echo $(IMAGE) | sed -rn 's/.*arubaoscx-disk-image-genericx86-p4-(.+)\.vmdk/\1/p') 9 | 10 | -include ../makefile-sanity.include 11 | -include ../makefile.include 12 | -------------------------------------------------------------------------------- /aoscx/README.md: -------------------------------------------------------------------------------- 1 | # vrnetlab / ArubaOS-CX (aoscx) 2 | 3 | This is the vrnetlab docker image for ArubaOS-CX. 4 | 5 | ## Building the docker image 6 | 7 | Download the OVA image from [Aruba Support Portal](https://asp.arubanetworks.com/downloads/software/RmlsZTpkOGRiYjc2Ni0wMTdkLTExZWUtYTY3Yi00Zjg4YjUyOWExMzQ%3D). Unzip the downloaded zip file and then untar the OVA file to get the vmdk image. 8 | Place the vmdk image into this folder, then run `make docker-image`. 9 | 10 | The image will be tagged with the timestamp present in the vmdk file. Optionally you can tag it with the release version of the downloaded software release: 11 | 12 | ``` 13 | docker tag vrnetlab/vr-aoscx:20210610000730 vrnetlab/vr-aoscx:10.07.0010 14 | ``` 15 | 16 | Tested booting and responding to SSH: 17 | 18 | * `ArubaOS-CX_10_12_0006.ova` (`arubaoscx-disk-image-genericx86-p4-20230531220439.vmdk`) 19 | * `ArubaOS-CX_10_13_0005.ova` (`arubaoscx-disk-image-genericx86-p4-20231110145644.vmdk`) 20 | * `ArubaOS-CX_10_14_1000.ova` (`arubaoscx-disk-image-genericx86-p4-20240731173624.vmdk`) 21 | 22 | ## System requirements 23 | 24 | CPU: 4 core 25 | 26 | RAM: 8GB 27 | 28 | Disk: <1GB 29 | -------------------------------------------------------------------------------- /aoscx/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/debian:bookworm-slim 2 | MAINTAINER Stefano Sasso 3 | 4 | ENV DEBIAN_FRONTEND=noninteractive 5 | 6 | RUN apt-get update -qy \ 7 | && apt-get upgrade -qy \ 8 | && apt-get install -y \ 9 | bridge-utils \ 10 | iproute2 \ 11 | python3-ipy \ 12 | socat \ 13 | tcpdump \ 14 | ssh \ 15 | inetutils-ping \ 16 | dnsutils \ 17 | iptables \ 18 | telnet \ 19 | ftp \ 20 | qemu-system-x86 \ 21 | qemu-utils \ 22 | && rm -rf /var/lib/apt/lists/* 23 | 24 | ARG IMAGE 25 | COPY $IMAGE* / 26 | COPY *.py / 27 | 28 | EXPOSE 22 80 161/udp 443 830 5000 5678 8291 10000-10099 29 | HEALTHCHECK CMD ["/healthcheck.py"] 30 | ENTRYPOINT ["/launch.py"] 31 | -------------------------------------------------------------------------------- /asav/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Cisco 2 | NAME=ASAv 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # match versions like: 7 | # asav9-18-2.qcow2 8 | VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+[^0-9]\([0-9]\+\-[0-9]\+\-[0-9]\+[a-z]\?\)\([^0-9].*\|$$\)/\1/') 9 | 10 | -include ../makefile-sanity.include 11 | -include ../makefile.include 12 | -include ../makefile-install.include -------------------------------------------------------------------------------- /asav/README.md: -------------------------------------------------------------------------------- 1 | vrnetlab / Cisco ASAv 2 | =========================== 3 | This is the vrnetlab docker image for Cisco ASAv. 4 | 5 | Building the docker image 6 | ------------------------- 7 | Put the .qcow2 file in this directory and run `make docker-image` and 8 | you should be good to go. The resulting image is called `vr-asav`. You can tag 9 | it with something else if you want, like `my-repo.example.com/vr-asav` and then 10 | push it to your repo. The tag is the same as the version of the ASAv image, so 11 | if you have asav9-18-2.qcow2 your final docker image will be called 12 | vr-asav:9-18-2 13 | 14 | Please note that you will always need to specify version when starting your 15 | router as the "latest" tag is not added to any images since it has no meaning 16 | in this context. 17 | 18 | It's been tested to boot and respond to SSH with: 19 | 20 | * 9.18.2 (asav9-18-2.qcow2) 21 | 22 | Usage 23 | ----- 24 | ``` 25 | docker run -d --privileged --name my-asav-firewall vr-asav 26 | ``` 27 | 28 | Interface mapping 29 | ----------------- 30 | Management0/0 is always configured as a management interface. 31 | 32 | | vr-asav | vr-xcon | 33 | | :---: | :---: | 34 | | Management0/0 | 0 | 35 | | GigabitEthernet0/0 | 1 | 36 | | GigabitEthernet0/1 | 2 | 37 | | GigabitEthernet0/2 | 3 | 38 | | GigabitEthernet0/3 | 4 | 39 | | GigabitEthernet0/4 | 5 | 40 | | GigabitEthernet0/5 | 6 | 41 | | GigabitEthernet0/6 | 7 | 42 | | GigabitEthernet0/7 | 8 | 43 | 44 | System requirements 45 | ------------------- 46 | CPU: 1 core 47 | 48 | RAM: 2GB 49 | 50 | Disk: <500MB 51 | 52 | FUAQ - Frequently or Unfrequently Asked Questions 53 | ------------------------------------------------- 54 | ##### Q: Has this been extensively tested? 55 | A: Nope. 56 | -------------------------------------------------------------------------------- /asav/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | MAINTAINER Kristian Larsson 3 | 4 | ENV DEBIAN_FRONTEND=noninteractive 5 | 6 | RUN apt-get update -qy \ 7 | && apt-get upgrade -qy \ 8 | && apt-get install -y \ 9 | bridge-utils \ 10 | iproute2 \ 11 | python3-ipy \ 12 | socat \ 13 | qemu-kvm \ 14 | tcpdump \ 15 | ssh \ 16 | inetutils-ping \ 17 | dnsutils \ 18 | telnet \ 19 | genisoimage \ 20 | && rm -rf /var/lib/apt/lists/* 21 | 22 | ARG VERSION 23 | ENV VERSION=${VERSION} 24 | ARG IMAGE 25 | COPY $IMAGE* / 26 | COPY *.py / 27 | 28 | EXPOSE 22 161/udp 830 5000 10000-10099 29 | HEALTHCHECK CMD ["/healthcheck.py"] 30 | ENTRYPOINT ["/launch.py"] 31 | -------------------------------------------------------------------------------- /asav/docker/launch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import datetime 4 | import logging 5 | import os 6 | import re 7 | import signal 8 | import sys 9 | import time 10 | 11 | import vrnetlab 12 | 13 | 14 | def handle_SIGCHLD(signal, frame): 15 | os.waitpid(-1, os.WNOHANG) 16 | 17 | 18 | def handle_SIGTERM(signal, frame): 19 | sys.exit(0) 20 | 21 | 22 | signal.signal(signal.SIGINT, handle_SIGTERM) 23 | signal.signal(signal.SIGTERM, handle_SIGTERM) 24 | signal.signal(signal.SIGCHLD, handle_SIGCHLD) 25 | 26 | TRACE_LEVEL_NUM = 9 27 | logging.addLevelName(TRACE_LEVEL_NUM, "TRACE") 28 | 29 | 30 | def trace(self, message, *args, **kws): 31 | # Yes, logger takes its '*args' as 'args'. 32 | if self.isEnabledFor(TRACE_LEVEL_NUM): 33 | self._log(TRACE_LEVEL_NUM, message, args, **kws) 34 | 35 | 36 | logging.Logger.trace = trace 37 | 38 | 39 | class ASAv_vm(vrnetlab.VM): 40 | def __init__(self, username, password, install_mode=False): 41 | for e in os.listdir("/"): 42 | if re.search(".qcow2$", e): 43 | disk_image = "/" + e 44 | 45 | super(ASAv_vm, self).__init__( 46 | username, password, disk_image=disk_image, ram=2048 47 | ) 48 | self.nic_type = "e1000" 49 | self.install_mode = install_mode 50 | self.num_nics = 8 51 | 52 | def bootstrap_spin(self): 53 | """This function should be called periodically to do work.""" 54 | 55 | if self.spins > 300: 56 | # too many spins with no result -> give up 57 | self.stop() 58 | self.start() 59 | return 60 | 61 | (ridx, match, res) = self.tn.expect([b"ciscoasa>"], 1) 62 | if match: # got a match! 63 | if ridx == 0: # login 64 | if self.install_mode: 65 | self.logger.debug("matched, ciscoasa>") 66 | self.wait_write("", wait=None) 67 | self.wait_write("", None) 68 | self.wait_write("", wait="ciscoasa>") 69 | self.running = True 70 | return 71 | 72 | self.logger.debug("matched, ciscoasa>") 73 | self.wait_write("", wait=None) 74 | 75 | # run main config! 76 | self.bootstrap_config() 77 | # close telnet connection 78 | self.tn.close() 79 | # startup time? 80 | startup_time = datetime.datetime.now() - self.start_time 81 | self.logger.info("Startup complete in: %s" % startup_time) 82 | # mark as running 83 | self.running = True 84 | return 85 | 86 | # no match, if we saw some output from the router it's probably 87 | # booting, so let's give it some more time 88 | if res != b"": 89 | self.logger.trace("OUTPUT: %s" % res.decode()) 90 | # reset spins if we saw some output 91 | self.spins = 0 92 | 93 | self.spins += 1 94 | 95 | return 96 | 97 | def bootstrap_config(self): 98 | """Do the actual bootstrap config""" 99 | self.logger.info("applying bootstrap configuration") 100 | self.wait_write("", None) 101 | self.wait_write("enable", wait="ciscoasa>") 102 | self.wait_write("VR-netlab9", wait="Enter Password:") 103 | self.wait_write("VR-netlab9", wait="Repeat Password:") 104 | self.wait_write("", wait="ciscoasa#") 105 | self.wait_write("configure terminal", wait="#") 106 | self.wait_write("N", wait="[Y]es, [N]o, [A]sk later:") 107 | self.wait_write("", wait="(config)#") 108 | self.wait_write("aaa authentication ssh console LOCAL") 109 | self.wait_write("aaa authentication enable console LOCAL") 110 | self.wait_write( 111 | "username %s password %s privilege 15" % (self.username, self.password) 112 | ) 113 | self.wait_write("interface Management0/0") 114 | self.wait_write("nameif management") 115 | self.wait_write("ip address 10.0.0.15 255.255.255.0") 116 | self.wait_write("no shutdown") 117 | self.wait_write("ssh 0.0.0.0 0.0.0.0 management") 118 | self.wait_write("ssh version 2") 119 | self.wait_write("ssh key-exchange group dh-group14-sha256") 120 | self.wait_write("crypto key generate ecdsa") 121 | self.wait_write("write") 122 | self.wait_write("end") 123 | self.wait_write("\r", None) 124 | 125 | 126 | class ASAv(vrnetlab.VR): 127 | def __init__(self, username, password): 128 | super(ASAv, self).__init__(username, password) 129 | self.vms = [ASAv_vm(username, password)] 130 | 131 | 132 | class ASAv_installer(ASAv): 133 | """ASAv installer""" 134 | 135 | def __init__(self, username, password): 136 | super(ASAv, self).__init__(username, password) 137 | self.vms = [ASAv_vm(username, password, install_mode=True)] 138 | 139 | def install(self): 140 | self.logger.info("Installing ASAv") 141 | asav = self.vms[0] 142 | while not asav.running: 143 | asav.work() 144 | time.sleep(30) 145 | asav.stop() 146 | self.logger.info("Installation complete") 147 | 148 | 149 | if __name__ == "__main__": 150 | import argparse 151 | 152 | parser = argparse.ArgumentParser(description="") 153 | parser.add_argument( 154 | "--trace", action="store_true", help="enable trace level logging" 155 | ) 156 | parser.add_argument("--username", default="vrnetlab", help="Username") 157 | parser.add_argument("--password", default="VR-netlab9", help="Password") 158 | parser.add_argument("--install", action="store_true", help="Install ASAv") 159 | args = parser.parse_args() 160 | 161 | LOG_FORMAT = "%(asctime)s: %(module)-10s %(levelname)-8s %(message)s" 162 | logging.basicConfig(format=LOG_FORMAT) 163 | logger = logging.getLogger() 164 | 165 | logger.setLevel(logging.DEBUG) 166 | if args.trace: 167 | logger.setLevel(1) 168 | 169 | if args.install: 170 | vr = ASAv_installer(args.username, args.password) 171 | vr.install() 172 | else: 173 | vr = ASAv(args.username, args.password) 174 | vr.start() 175 | -------------------------------------------------------------------------------- /build-base-image.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # this script builds the vrnetlab base container image 3 | # that is used in the dockerfiles of the NOS images 4 | 5 | set -e 6 | 7 | if [ -z "$1" ]; then 8 | echo "Usage: $0 " 9 | exit 1 10 | fi 11 | 12 | sudo docker build -t ghcr.io/srl-labs/vrnetlab-base:$1 \ 13 | -f vrnetlab-base.dockerfile . -------------------------------------------------------------------------------- /c8000v/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=cisco 2 | NAME=c8000v 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # match versions like: 7 | # c8000v-17.11.01a.qcow2 8 | VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+[^0-9]\([0-9]\+\.[0-9]\+\.[0-9]\+[a-z]\?\)\([^0-9].*\|$$\)/\1/') 9 | 10 | -include ../makefile-sanity.include 11 | -include ../makefile.include 12 | -include ../makefile-install.include 13 | 14 | docker-build: docker-build-common 15 | docker run --cidfile cidfile --privileged $(REGISTRY)$(VENDOR)_$(NAME):$(VERSION) --trace --install 16 | docker commit --change='ENTRYPOINT ["/launch.py"]' $$(cat cidfile) $(REGISTRY)$(VENDOR)_$(NAME):$(VERSION) 17 | docker rm -f $$(cat cidfile) 18 | -------------------------------------------------------------------------------- /c8000v/README.md: -------------------------------------------------------------------------------- 1 | # Cisco Catalyst 8000V Edge Software 2 | 3 | This is the vrnetlab docker image for Cisco Catalyst 8000V Edge Software, or 4 | 'c8000v' for short. 5 | 6 | The Catalyst 8000v platform is a successor to the CSR 1000v. As such, this 7 | platform directory 'c8000v' started off as a copy of the 'csr' directory. With 8 | time we imagine the two platforms will diverge. One such change is already 9 | planned to support using the Catalyst 8000v in one of the two modes: 10 | 11 | - regular, 12 | - SD-WAN Controller mode (managed by Viptela). 13 | 14 | Right now the SD-WAN flavor is still split off because to enable the Controller 15 | mode you have to effectively boot the router into a completely different mode. 16 | In the near future these modifications will be merged back into the 'c8000v' 17 | platform that will produce both the regular and sd-wan images. 18 | 19 | On installation of Catalyst 8000v the user is presented with the choice of 20 | output, which can be over serial console, a video console or through automatic 21 | detection of one or the other. Empirical studies show that the automatic 22 | detection is far from infallible and so we force the use of the serial console 23 | by feeding the VM an .iso image that contains a small bootstrap configuration 24 | that sets the output to serial console. This means we have to boot up the VM 25 | once to feed it this configuration and then restart it for the changes to take 26 | effect. Naturally we want to do this in the build process as to avoid having to 27 | restart the router once for every time we run the docker image. Unfortunately 28 | docker doesn't allow us to run docker build with `--privileged` so there is no 29 | KVM acceleration making this process excruciatingly slow were it to be performed 30 | in the docker build phase. Instead we build a basic image using docker build, 31 | which essentially just assembles the required files, then run it with 32 | `--privileged` to start up the VM and feed it the .iso image. After we are done 33 | we shut down the VM and commit this new state into the final docker image. This 34 | is unorthodox but works and saves us a lot of time. 35 | 36 | ## Building the docker image 37 | 38 | Put the .qcow2 file in this directory and run `make docker-image` and you should 39 | be good to go. The resulting image is called `vr-c8000v`. You can tag it with 40 | something else if you want, like `my-repo.example.com/vr-c8000v` and then push 41 | it to your repo. The tag is the same as the version of the Catalyst 8000v image, 42 | so if you have c8000v-universalk9.16.04.01.qcow2 your final docker image will be 43 | called `vr-c8000v:16.04.01` 44 | 45 | It's been tested to boot and respond to SSH with: 46 | 47 | - 16.03.01a (c8000v-universalk9.16.03.01a.qcow2) 48 | - 16.04.01 (c8000v-universalk9.16.04.01.qcow2) 49 | - 17.11.01a (c8000v-universalk9_16G_serial.17.11.01a.qcow2) 50 | 51 | ## Usage 52 | 53 | ```bash 54 | docker run -d --privileged --name my-c8000v-router vr-c8000v 55 | ``` 56 | 57 | ## Interface mapping 58 | 59 | IOS XE 16.03.01 and 16.04.01 does only support 10 interfaces, GigabitEthernet1 is always configured 60 | as a management interface and then we can only use 9 interfaces for traffic. If you configure vrnetlab 61 | to use more then 10 the interfaces will be mapped like the table below. 62 | 63 | The following images have been verified to NOT exhibit this behavior 64 | 65 | - c8000v-universalk9.03.16.02.S.155-3.S2-ext.qcow2 66 | - c8000v-universalk9.03.17.02.S.156-1.S2-std.qcow2 67 | 68 | | vr-c8000v | vr-xcon | 69 | | :-------: | :-----: | 70 | | Gi2 | 10 | 71 | | Gi3 | 1 | 72 | | Gi4 | 2 | 73 | | Gi5 | 3 | 74 | | Gi6 | 4 | 75 | | Gi7 | 5 | 76 | | Gi8 | 6 | 77 | | Gi9 | 7 | 78 | | Gi10 | 8 | 79 | | Gi11 | 9 | 80 | 81 | ## System requirements 82 | 83 | CPU: 1 core 84 | 85 | RAM: 4GB 86 | 87 | Disk: <500MB 88 | 89 | ## License handling 90 | 91 | You can feed a license file into c8000v by putting a text file containing the 92 | license in this directory next to your .qcow2 image. Name the license file the 93 | same as your .qcow2 file but append ".license", e.g. if you have 94 | "c8000v-universalk9.16.04.01.qcow2" you would name the license file 95 | "c8000v-universalk9.16.04.01.qcow2.license". 96 | 97 | The license is bound to a specific UDI and usually expires within a given time. 98 | To make sure that everything works out smoothly we configure the clock to 99 | a specific date during the installation process. This is because the license 100 | only has an expiration date not a start date. 101 | 102 | The license unlocks feature and throughput. The default throughput for C8000v is 103 | 20Mbit/s which is perfectly for basic management and testing. 104 | 105 | ## Known issues 106 | 107 | If during the image boot process (not during the install process) you notice messages like: 108 | 109 | ``` 110 | % Failed to initialize nvram 111 | % Failed to initialize backup nvram 112 | ``` 113 | 114 | Then the image will boot, but SSH might not work. You still can use telnet to access the running VM. For instance: 115 | 116 | ```bash 117 | telnet 5000 118 | ``` 119 | -------------------------------------------------------------------------------- /c8000v/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/srl-labs/vrnetlab-base:0.1.0 2 | 3 | ARG VERSION 4 | ENV VERSION=${VERSION} 5 | ARG IMAGE 6 | COPY $IMAGE* / 7 | COPY *.py / 8 | 9 | EXPOSE 22 161/udp 830 5000 10000-10099 10 | HEALTHCHECK CMD ["/healthcheck.py"] 11 | ENTRYPOINT ["/launch.py"] 12 | -------------------------------------------------------------------------------- /cat9kv/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Cisco 2 | NAME=cat9kv 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # match versions like: 7 | # csr1000v-universalk9.16.03.01a.qcow2 8 | # csr1000v-universalk9.16.04.01.qcow2 9 | VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+[^0-9]\([0-9]\+\.[0-9]\+\.[0-9]\+[a-z]\?\)\([^0-9].*\|$$\)/\1/') 10 | 11 | -include ../makefile-sanity.include 12 | -include ../makefile.include -------------------------------------------------------------------------------- /cat9kv/README.md: -------------------------------------------------------------------------------- 1 | # Cisco Catalyst 9000V 2 | 3 | This is the vrnetlab image for the Cisco Catalyst 9000v (cat9kv, c9000v). 4 | 5 | The Cat9kv emulates two types of ASICs that are found in the common Catalyst 9000 hardware platforms, either: 6 | 7 | - UADP (Cisco Unified Access Data Plane) 8 | - Cisco Silicon One Q200 (referred to as Q200 for short) 9 | 10 | The Q200 is a newer ASIC, however doen't support as many features as the UADP ASIC emulation. 11 | 12 | > Insufficient RAM will not allow the node to boot correctly. 13 | 14 | Eight interfaces will always appear regardless if you have defined any links in the `*.clab.yaml` topology file. The Cat9kv requires 8 interfaces at minimum to boot, so dummy interfaces are created if there are an insufficient amount of interfaces (links) defined. 15 | 16 | ## Building the image 17 | 18 | Copy the Cat9kv .qcow2 file in this directory and you can perform `make docker-image`. On average the image takes approxmiately ~4 minutes to build as an initial install process occurs. 19 | 20 | The UADP and Q200 use the same .qcow2 image. The default image created is the UADP image. 21 | 22 | To configure the Q200 image or enable a higher throughput dataplane for UADP; you must supply the relevant `vswitch.xml` file. You can place that file in this directory and build the image. 23 | 24 | > You can obtain a `vswitch.xml` file from the relevant CML node definiton file. 25 | 26 | Known working versions: 27 | 28 | - cat9kv-prd-17.12.01prd9.qcow2 (UADP & Q200) 29 | 30 | ## Usage 31 | 32 | You can define the image easily and use it in a topolgy. As mentioned earlier no links are requried to be defined. 33 | 34 | ```yaml 35 | # topology.clab.yaml 36 | name: mylab 37 | topology: 38 | nodes: 39 | cat9kv: 40 | kind: cisco_cat9kv 41 | image: vrnetlab/vr-cat9kv: 42 | ``` 43 | 44 | You can also supply a vswitch.xml file using `binds`. Below is an example topology file. 45 | 46 | ```yaml 47 | # topology.clab.yaml 48 | name: mylab 49 | topology: 50 | nodes: 51 | cat9kv: 52 | kind: cisco_cat9kv 53 | image: vrnetlab/vr-cat9kv: 54 | binds: 55 | - /path/to/vswitch.xml:/vswitch.xml 56 | ``` 57 | 58 | ### Interface naming 59 | 60 | Currently a maximum of 8 data-plane interfaces are supported. 9 interfaces total if including the management interface. 61 | 62 | - `eth0` - Node management interface 63 | - `eth1` - First dataplane interface (GigabitEthernet1/0/1). 64 | - `ethX` - Subsequent dataplane interfaces will count onwards from 1. For example, the third dataplane interface will be `eth3` 65 | 66 | You can also use interface aliases of `GigabitEthernet1/0/x` or `Gi1/0/x` 67 | 68 | ### Environment Variables 69 | 70 | | Environment Variable | Default | 71 | | --------------------- | ------------- | 72 | | VCPU | 4 | 73 | | RAM | 18432 | 74 | 75 | ### Example 76 | 77 | ```yaml 78 | name: my-example-lab 79 | topology: 80 | nodes: 81 | my-cat9kv: 82 | kind: cisco_cat9kv 83 | image: vrnetlab/vr-cat9kv:17.12.01 84 | env: 85 | VCPU: 6 86 | RAM: 12288 87 | ``` 88 | 89 | ## System requirements 90 | 91 | | | UADP (Default)| Q200 | 92 | | --------- | ------------- | ----- | 93 | | vCPU | 4 | 4 | 94 | | RAM (MB) | 18432 | 12288 | 95 | | Disk (GB) | 4 | 4 | 96 | -------------------------------------------------------------------------------- /cat9kv/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/srl-labs/vrnetlab-base:0.1.0 2 | 3 | ARG VERSION 4 | ENV VERSION=${VERSION} 5 | ARG IMAGE 6 | COPY $IMAGE* / 7 | COPY *.py / 8 | # for vSwitch.xml file (specifies ASIC emulation parameters), won't throw error if vswitch.xml isn't present 9 | COPY vswitch.xm[l] /img_dir/conf/ 10 | 11 | EXPOSE 22 161/udp 830 5000 10000-10099 12 | HEALTHCHECK CMD ["/healthcheck.py"] 13 | ENTRYPOINT ["/launch.py"] 14 | -------------------------------------------------------------------------------- /cisco/iol/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Cisco 2 | NAME=IOL 3 | IMAGE_FORMAT=bin 4 | IMAGE_GLOB=*.bin 5 | NOT_VM_IMAGE=1 6 | 7 | # match versions like: 8 | # cisco_iol-17.12.01.bin 9 | # cisco_iol-L2-17.12.01.bin 10 | VERSION=$(shell echo $(IMAGE) | sed -e 's/cisco_iol-\(.*\)\.bin/\1/') 11 | 12 | -include ../../makefile-sanity.include 13 | -include ../../makefile.include -------------------------------------------------------------------------------- /cisco/iol/README.md: -------------------------------------------------------------------------------- 1 | # Cisco IOL (IOS on Linux) 2 | 3 | This is the containerlab/vrnetlab image for Cisco IOL (IOS On Linux). 4 | 5 | CML recently introduced IOL-XE which compared to other Cisco images, runs very lightly since it executes purely as a binary and has no requirement for a virtualisation layer. 6 | 7 | There are two types of IOL you can obtain: 8 | 9 | - IOL, meant for Layer 3 operation as a router. 10 | - IOL-L2, meant to act as a L2/L2+ switch. 11 | 12 | ## Building the image 13 | 14 | Copy the `x86_64_crb_linux-adventerprisek9-ms` into this directory and rename it to `cisco_iol-x.y.z.bin` (x.y.z being the version number). For example `cisco_iol-17.12.01.bin`. The `.bin` extension is important. 15 | 16 | > If using IOL-L2 it is recommended to name your image to identify it as IOL-L2. For example: `cisco_iol-L2-x.y.z.bin` 17 | 18 | > If you are getting the image from the CML refplat, the IOL image is under the `iol-xe-x.y.z` directory or `ioll2-xe-x.y.z` for IOL-L2. 19 | 20 | ### Build command 21 | 22 | Execute 23 | 24 | ``` 25 | make docker-image 26 | ``` 27 | 28 | and the image will be built and tagged. You can view the image by executing `docker images`. 29 | 30 | ``` 31 | containerlab@containerlab:~$ docker images 32 | REPOSITORY TAG IMAGE ID CREATED SIZE 33 | vrnetlab/cisco_iol L2-17.12.01 c207d920446e 5 seconds ago 607MB 34 | vrnetlab/cisco_iol 17.12.01 30be6c875c80 12 minutes ago 704MB 35 | ``` 36 | 37 | ## Usage 38 | 39 | You can define the image easily and use it in a topology. 40 | 41 | ```yaml 42 | # topology.clab.yaml 43 | name: mylab 44 | topology: 45 | nodes: 46 | iol: 47 | kind: cisco_iol 48 | image: vrnetlab/cisco_iol: 49 | ``` 50 | 51 | **IOL-L2** 52 | 53 | ```yaml 54 | # topology.clab.yaml 55 | name: mylab 56 | topology: 57 | nodes: 58 | iol: 59 | kind: cisco_iol 60 | image: vrnetlab/cisco_iol: 61 | type: l2 62 | ``` 63 | -------------------------------------------------------------------------------- /cisco/iol/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/debian:bookworm-slim 2 | 3 | # Install dependencies 4 | RUN apt-get update && \ 5 | apt-get install -y --no-install-recommends \ 6 | iproute2 \ 7 | iputils-ping \ 8 | net-tools \ 9 | sudo \ 10 | curl \ 11 | ca-certificates \ 12 | gnupg \ 13 | && apt-get clean 14 | 15 | # Add containerlab gemfury for custom IOUYAP 16 | RUN echo "deb [trusted=yes] https://netdevops.fury.site/apt/ /" | \ 17 | sudo tee -a /etc/apt/sources.list.d/netdevops.list 18 | 19 | # Update and install IOUYAP 20 | RUN apt-get update && \ 21 | apt-get install -y iouyap 22 | 23 | # Copy the entrypoint script 24 | COPY entrypoint.sh /entrypoint.sh 25 | RUN chmod +x /entrypoint.sh 26 | 27 | # Set working directory 28 | WORKDIR /iol 29 | 30 | # Add the IOL image and config files 31 | COPY *.bin /iol/iol.bin 32 | 33 | # Make the IOL image and script executable 34 | RUN chmod +x /iol/iol.bin 35 | 36 | ENTRYPOINT ["/entrypoint.sh"] 37 | -------------------------------------------------------------------------------- /cisco/iol/docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Launching IOL with PID" $IOL_PID 4 | 5 | # Clear ip addressing on eth0 (it 'belongs' to IOL now) 6 | ip addr flush dev eth0 7 | ip -6 addr flush dev eth0 8 | 9 | echo "Flushed eth0 addresses" 10 | 11 | sleep 5 12 | 13 | # Run IOUYAP 14 | exec /usr/bin/iouyap 513 -q & 15 | 16 | # Get the highest numbered eth interface 17 | max_eth=$(ls /sys/class/net | grep eth | grep -o -E '[0-9]+' | sort -n | tail -1) 18 | num_slots=$(( (max_eth + 4) / 4 )) 19 | 20 | # Start IOL 21 | exec /iol/iol.bin $IOL_PID -e $num_slots -s 0 -c config.txt -n 1024 22 | -------------------------------------------------------------------------------- /cmglinux/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Canonical 2 | NAME=Ubuntu 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # match versions like: 7 | # cmg-linux-24.3.R1.qcow2 8 | VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+[^0-9]\([0-9]\+\.[0-9]\+\.[A-Z][0-9]\+\(-[0-9]\+\)\?\)[^0-9].*$$/\1/') 9 | 10 | 11 | -include ../makefile-sanity.include 12 | -include ../makefile.include 13 | -------------------------------------------------------------------------------- /cmglinux/README.md: -------------------------------------------------------------------------------- 1 | # CMG Linux VM 2 | 3 | ## Introduction 4 | 5 | CMG Linux is utilized as a DB VM as one of Nokia MAG-C VMs. 6 | The image of CMG Linux is released in qcow2 format. 7 | Since some of MAG-C tools in CMG Linux are written with systemd or systemctl utilization, 8 | then CMG Linux cannot be containerized because docker does not run systemd inside the container. 9 | Given this reasoning, the approach to containerized CMG Linux is to create a container 10 | to run a CMG Linux VM in the same way Vrnetlab has done. 11 | 12 | ## Build the docker image 13 | 14 | It is required to provide CMG Linux qcow2 image to build the docker image. 15 | Nokia representative can provide the qcow2 file. 16 | 17 | Make sure that your python virtualenv has `yaml` package installed. 18 | 19 | Copy the `cmg-linux.qcow2` file in `vrnetlab/cmglinux` directory 20 | and rename the file by appending the version to it. 21 | For example, for CMG Linux version 24.3.r1, 22 | make sure that the qcow2 file will be named as `cmg-linux-24.3.R1.qcow2`. 23 | The version 24.3.R1 will be used as a container image tag. 24 | 25 | Run `make docker-image` to start the build process. 26 | The resulting image is called `vrnetlab/cmglinux:`. 27 | You can tag it with something else. for example, `cmglinux:`. 28 | 29 | ## Host requirements 30 | 31 | * 4 vCPU 32 | * 6 GB RAM 33 | 34 | ## Configuration 35 | 36 | Initial config is carried out via cloud-init. 37 | By default CMG-Linux boots by using a pre-defined cloud-init config drive. 38 | 39 | Custom configuration can be added by binding the local `config_drive` 40 | directory to `/config_drive` directory in the container. 41 | The accepted structure of `config_drive`is shown below. 42 | Any other directories or files not specified below are ignored. 43 | 44 | ``` text 45 | config_drive/ 46 | └── openstack/ 47 | ├── latest/ 48 | │ ├── meta_data.json 49 | │ └── user_data 50 | └── content/ 51 | ├── 0000 (referenced content files) 52 | ├── 0001 53 | └── .... 54 | ``` 55 | 56 | The internal `launch.py` script also modifies the content of `user_data` to add `clab`as 57 | default user with password `clab@123`. Moreover, it also modifies `user_data` 58 | to configure the management network interface. 59 | 60 | Also `9.9.9.9` configured as the DNS resolver. Change it with `resolvectl` if required. 61 | 62 | ## Example containerlab topology 63 | 64 | Below is an example of Containerlab topology using CMG Linux. 65 | 66 | ``` yaml 67 | name: test_cmglinux 68 | prefix: __lab-name 69 | topology: 70 | nodes: 71 | cmg-1: 72 | kind: generic_vm 73 | image: vrnetlab/vr-cmglinux:24.3.R3 74 | binds: 75 | - config_drive_cmg1:/config_drive 76 | cmg-2: 77 | kind: generic_vm 78 | image: vrnetlab/vr-cmglinux:24.3.R1 79 | binds: 80 | - config_drive_cmg2:/config_drive 81 | alpine: 82 | kind: linux 83 | image: alpine:dev 84 | links: 85 | - endpoints: 86 | - cmg-1:eth1 87 | - alpine:eth1 88 | - endpoints: 89 | - cmg-1:eth2 90 | - alpine:eth2 91 | - endpoints: 92 | - cmg-1:eth3 93 | - alpine:eth3 94 | - endpoints: 95 | - cmg-2:eth1 96 | - alpine:eth4 97 | - endpoints: 98 | - cmg-2:eth2 99 | - alpine:eth5 100 | ``` 101 | -------------------------------------------------------------------------------- /cmglinux/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/debian:bookworm-slim AS builder 2 | 3 | ARG DISK_SIZE=4G 4 | 5 | RUN apt-get update -qy && \ 6 | apt-get install -y --no-install-recommends qemu-utils && \ 7 | rm -rf /var/lib/apt/lists/* 8 | 9 | ARG IMAGE 10 | COPY $IMAGE* / 11 | RUN qemu-img resize /$IMAGE $DISK_SIZE 12 | 13 | FROM public.ecr.aws/docker/library/debian:bookworm-slim 14 | 15 | ARG DEBIAN_FRONTEND=noninteractive 16 | ARG DISK_SIZE=4G 17 | 18 | RUN apt-get update -qy \ 19 | && apt-get install -y --no-install-recommends\ 20 | bridge-utils \ 21 | iproute2 \ 22 | socat \ 23 | qemu-kvm \ 24 | tcpdump \ 25 | ssh \ 26 | inetutils-ping \ 27 | dnsutils \ 28 | iptables \ 29 | nftables \ 30 | telnet \ 31 | cloud-utils \ 32 | sshpass \ 33 | && rm -rf /var/lib/apt/lists/* 34 | 35 | ARG IMAGE 36 | COPY --from=builder $IMAGE* / 37 | COPY *.py / 38 | 39 | EXPOSE 22 5000 10000-10099 40 | HEALTHCHECK CMD ["/healthcheck.py"] 41 | ENTRYPOINT ["/launch.py"] 42 | -------------------------------------------------------------------------------- /common/healthcheck.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | 5 | try: 6 | health_file = open("/health", "r") 7 | health = health_file.read() 8 | health_file.close() 9 | except FileNotFoundError: 10 | print("health status file not found") 11 | sys.exit(2) 12 | 13 | exit_status, message = health.strip().split(" ", 1) 14 | 15 | if message != "": 16 | print(message) 17 | 18 | sys.exit(int(exit_status)) 19 | -------------------------------------------------------------------------------- /csr/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=cisco 2 | NAME=csr1000v 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # match versions like: 7 | # csr1000v-universalk9.16.03.01a.qcow2 8 | # csr1000v-universalk9.16.04.01.qcow2 9 | VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+[^0-9]\([0-9]\+\.[0-9]\+\.[0-9]\+[sb]\?\?\)\([^0-9].*\|$$\)/\1/') 10 | 11 | -include ../makefile-sanity.include 12 | -include ../makefile.include 13 | 14 | docker-pre-build: 15 | -cat cidfile | xargs --no-run-if-empty docker rm -f 16 | -rm cidfile 17 | 18 | docker-build: docker-build-common 19 | docker run --cidfile cidfile --privileged $(REGISTRY)$(VENDOR)_$(NAME):$(VERSION) --trace --install 20 | docker commit --change='ENTRYPOINT ["/launch.py"]' $$(cat cidfile) $(REGISTRY)$(VENDOR)_$(NAME):$(VERSION) 21 | docker rm -f $$(cat cidfile) 22 | -------------------------------------------------------------------------------- /csr/README.md: -------------------------------------------------------------------------------- 1 | vrnetlab / Cisco CSR1000v 2 | =========================== 3 | This is the vrnetlab docker image for Cisco CSR1000v. 4 | 5 | On installation of CSR1000v the user is presented with the choice of output, 6 | which can be over serial console, a video console or through automatic 7 | detection of one or the other. Empirical studies show that the automatic 8 | detection is far from infallible and so we force the use of the serial console 9 | by feeding the VM an .iso image that contains a small bootstrap configuration 10 | that sets the output to serial console. This means we have to boot up the VM 11 | once to feed it this configuration and then restart it for the changes to take 12 | effect. Naturally we want to do this in the build process as to avoid having to 13 | restart the router once for every time we run the docker image. Unfortunately 14 | docker doesn't allow us to run docker build with `--privileged` so there is no 15 | KVM acceleration making this process excruciatingly slow were it to be 16 | performed in the docker build phase. Instead we build a basic image using 17 | docker build, which essentially just assembles the required files, then run it 18 | with `--privileged` to start up the VM and feed it the .iso image. After we are 19 | done we shut down the VM and commit this new state into the final docker image. 20 | This is unorthodox but works and saves us a lot of time. 21 | 22 | Building the docker image 23 | ------------------------- 24 | Put the .qcow2 file in this directory and run `make docker-image` and 25 | you should be good to go. The resulting image is called `vrnetlab\vr-csr`. You can tag 26 | it with something else if you want, like `my-repo.example.com/vr-csr` and then 27 | push it to your repo. The tag is the same as the version of the CSR image, so 28 | if you have `csr1000v-universalk9.17.04.03-serial.qcow2` your final docker image will be 29 | called `vrnetlab\vr-csr:17.07.03` 30 | 31 | Please note that you will always need to specify version when starting your 32 | router as the "latest" tag is not added to any images since it has no meaning 33 | in this context. 34 | 35 | It's been tested to boot and respond to SSH with: 36 | 37 | * 17.03.02 (csr1000v-universalk9.17.03.02-serial.qcow2) 38 | 39 | Usage 40 | ----- 41 | ``` 42 | docker run -d --privileged --name my-csr-router vrnetlab/vr-csr 43 | ``` 44 | 45 | Interface mapping 46 | ----------------- 47 | IOS XE 17.03.02 supports a maximum of 26 interfaces, GigabitEthernet1 is always configured 48 | as a management interface leaving upto 25 interfaces for traffic. 49 | 50 | System requirements 51 | ------------------- 52 | CPU: 1 core 53 | 54 | RAM: 4GB 55 | 56 | Disk: <500MB 57 | 58 | License handling 59 | ---------------- 60 | You can feed a license file into CSR1000V by putting a text file containing the 61 | license in this directory next to your .qcow2 image. Name the license file the 62 | same as your .qcow2 file but append ".license", e.g. if you have 63 | "csr1000v-universalk9.17.03.02.qcow2" you would name the license file 64 | "csr1000v-universalk9.16.03.03.qcow2.license". 65 | 66 | The license is bound to a specific UDI and usually expires within a given time. 67 | To make sure that everything works out smoothly we configure the clock to 68 | a specific date during the installation process. This is because the license 69 | only has an expiration date not a start date. 70 | 71 | The license unlocks feature and throughput, the default throughput 72 | for CSR is 100Kbit/s and is totally useless if you want to configure the device 73 | with a fairly large configuration. 74 | 75 | FUAQ - Frequently or Unfrequently Asked Questions 76 | ------------------------------------------------- 77 | ##### Q: Has this been extensively tested? 78 | A: Nope. 79 | -------------------------------------------------------------------------------- /csr/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/srl-labs/vrnetlab-base:0.1.0 2 | 3 | ARG VERSION 4 | ENV VERSION=${VERSION} 5 | ARG IMAGE 6 | COPY $IMAGE* / 7 | COPY *.py / 8 | 9 | EXPOSE 22 161/udp 830 5000 10000-10099 10 | HEALTHCHECK CMD ["/healthcheck.py"] 11 | ENTRYPOINT ["/launch.py"] 12 | -------------------------------------------------------------------------------- /dell_sonic/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Dell 2 | NAME=sonic 3 | IMAGE_FORMAT=qcow 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # rename the disk image file as dell-sonic-.qcow2 7 | # examples: 8 | # for a file named "dell-sonic-4.2.1.qcow2" the image will be named "vrnetlab/dell_sonic:4.2.1" 9 | VERSION=$(shell echo $(IMAGE) | sed -e 's/dell-sonic-//' | sed -e 's/.qcow2//') 10 | 11 | -include ../makefile-sanity.include 12 | -include ../makefile.include 13 | -------------------------------------------------------------------------------- /dell_sonic/README.md: -------------------------------------------------------------------------------- 1 | # Dell Enterprise SONiC 2 | 3 | This is the vrnetlab docker image for SONiC's VM. 4 | The scripts in this directory are based on FreeBSD and VSRX kinds. 5 | 6 | > Available with [containerlab](https://containerlab.dev) as [`dell_sonic`](https://containerlab.dev/manual/kinds/dell_sonic/) kind. 7 | 8 | ## Building the docker image 9 | 10 | Download the latest Dell SONiC qcow2 disk image from Dell support website. Rename the file to `dell-sonic-[version].qcow2` and run `make`. 11 | 12 | After typing `make`, a new image will appear named `vrnetlab/vr-dell_sonic:`. Run `docker images` to confirm this. 13 | 14 | ## System requirements 15 | 16 | - CPU: 2 cores 17 | - RAM: 4GB 18 | - DISK: ~3.2GB 19 | 20 | ## Configuration 21 | 22 | SONiC nodes boot with a basic configuration by default, enabling SSH and basic management connectivity. All factory default configuration is retained. 23 | 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. 24 | -------------------------------------------------------------------------------- /dell_sonic/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/debian:bookworm-slim 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | 5 | RUN apt-get update -qy \ 6 | && apt-get install -y --no-install-recommends \ 7 | bridge-utils \ 8 | iproute2 \ 9 | python3-ipy \ 10 | qemu-kvm \ 11 | qemu-utils \ 12 | socat \ 13 | ssh \ 14 | sshpass \ 15 | && rm -rf /var/lib/apt/lists/* 16 | 17 | ARG IMAGE 18 | COPY $IMAGE* / 19 | COPY *.py / 20 | COPY backup.sh / 21 | 22 | EXPOSE 22 443 5000 8080 23 | HEALTHCHECK CMD ["/healthcheck.py"] 24 | ENTRYPOINT ["/launch.py"] -------------------------------------------------------------------------------- /dell_sonic/docker/backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DEFAULT_USER="admin" 4 | DEFAULT_PASSWORD="admin" 5 | REMOTE_FILE="/etc/sonic/config_db.json" 6 | TMP_FILE="/tmp/${REMOTE_FILE##*/}" 7 | BACKUP_FILE="/config/${REMOTE_FILE##*/}" 8 | 9 | handle_args() { 10 | # Parse options 11 | while getopts 'u:p:' OPTION; do 12 | case "$OPTION" in 13 | u) 14 | user="$OPTARG" 15 | ;; 16 | p) 17 | password="$OPTARG" 18 | ;; 19 | ?) 20 | usage 21 | exit 1 22 | ;; 23 | esac 24 | done 25 | shift "$(($OPTIND -1))" 26 | 27 | # Assign defaults if options weren't provided 28 | if [ -z "$user" ] ; then 29 | user=$DEFAULT_USER 30 | fi 31 | if [ -z "$password" ] ; then 32 | password=$DEFAULT_PASSWORD 33 | fi 34 | 35 | SSH_CMD="sshpass -p $password ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" 36 | SCP_CMD="sshpass -p $password scp -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" 37 | HOST="$user@localhost" 38 | 39 | # Parse commands 40 | case $1 in 41 | 42 | backup) 43 | backup 44 | ;; 45 | 46 | restore) 47 | restore 48 | ;; 49 | 50 | *) 51 | usage 52 | ;; 53 | esac 54 | } 55 | 56 | usage() { 57 | echo "Usage: $(basename $0) [-u USERNAME] [-p PASSWORD] COMMAND" 58 | echo "Options:" 59 | echo " -u USERNAME VM SSH username (default: $DEFAULT_USER)" 60 | echo " -p PASSWORD VM SSH password (default: $DEFAULT_PASSWORD)" 61 | echo 62 | echo "Commands:" 63 | echo " backup Backup VM $REMOTE_FILE directory to $BACKUP_FILE" 64 | echo " restore Restore VM $REMOTE_FILE directory from $BACKUP_FILE" 65 | exit 0; 66 | } 67 | 68 | backup() { 69 | echo "Retrieveing the config from the VM..." 70 | # copy the original config to the tmp location and set permissions to 777 71 | # and then copy out the file from the temp location 72 | $SSH_CMD $HOST "sudo cp $REMOTE_FILE $TMP_FILE && sudo chmod 777 $TMP_FILE" && \ 73 | $SCP_CMD $HOST:$TMP_FILE $BACKUP_FILE 74 | } 75 | 76 | wait_for_ssh() { 77 | local max_retries=30 78 | local retry_interval=2 79 | 80 | for ((i=1; i<=$max_retries; i++)); do 81 | echo "Waiting for VM's SSH to become available... (Attempt $i/$max_retries)" 82 | if $SSH_CMD -o ConnectTimeout=5 $HOST exit 2>/dev/null; then 83 | echo "SSH connection established." 84 | return 0 85 | fi 86 | sleep $retry_interval 87 | done 88 | 89 | echo "SSH connection could not be established after $max_retries attempts." 90 | return 1 91 | } 92 | 93 | restore() { 94 | if [ -f "$BACKUP_FILE" ]; then 95 | echo "Copying startup config file to the VM..." 96 | 97 | if wait_for_ssh; then 98 | $SCP_CMD $BACKUP_FILE $HOST:$TMP_FILE && $SSH_CMD $HOST "sudo config load -y $TMP_FILE && sudo config save -y" 99 | else 100 | echo "Failed to establish SSH connection. Config copy operation aborted." 101 | fi 102 | else 103 | echo "$BACKUP_FILE not found. Nothing to push to the VM." 104 | fi 105 | } 106 | 107 | handle_args "$@" 108 | -------------------------------------------------------------------------------- /dell_sonic/docker/launch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import datetime 4 | import logging 5 | import os 6 | import re 7 | import signal 8 | import subprocess 9 | import sys 10 | 11 | import vrnetlab 12 | 13 | CONFIG_FILE = "/config/config_db.json" 14 | DEFAULT_USER = "admin" 15 | DEFAULT_PASSWORD = "YourPaSsWoRd" 16 | 17 | 18 | def handle_SIGCHLD(_signal, _frame): 19 | os.waitpid(-1, os.WNOHANG) 20 | 21 | 22 | def handle_SIGTERM(_signal, _frame): 23 | sys.exit(0) 24 | 25 | 26 | signal.signal(signal.SIGINT, handle_SIGTERM) 27 | signal.signal(signal.SIGTERM, handle_SIGTERM) 28 | signal.signal(signal.SIGCHLD, handle_SIGCHLD) 29 | 30 | TRACE_LEVEL_NUM = 9 31 | logging.addLevelName(TRACE_LEVEL_NUM, "TRACE") 32 | 33 | 34 | def trace(self, message, *args, **kws): 35 | # Yes, logger takes its '*args' as 'args'. 36 | if self.isEnabledFor(TRACE_LEVEL_NUM): 37 | self._log(TRACE_LEVEL_NUM, message, args, **kws) 38 | 39 | 40 | logging.Logger.trace = trace 41 | 42 | 43 | class Dell_Sonic_VM(vrnetlab.VM): 44 | def __init__(self, hostname, username, password, conn_mode): 45 | disk_image = "/" 46 | for e in os.listdir("/"): 47 | if re.search(".qcow2$", e): 48 | disk_image = "/" + e 49 | break 50 | super(Dell_Sonic_VM, self).__init__( 51 | username, password, disk_image=disk_image, ram=4096 52 | ) 53 | self.qemu_args.extend(["-smp", "2"]) 54 | self.nic_type = "virtio-net-pci" 55 | self.conn_mode = conn_mode 56 | self.num_nics = 10 57 | self.hostname = hostname 58 | 59 | def bootstrap_spin(self): 60 | """This function should be called periodically to do work.""" 61 | 62 | if self.spins > 300: 63 | # too many spins with no result -> give up 64 | self.stop() 65 | self.start() 66 | return 67 | 68 | ridx, match, res = self.tn.expect([b"login:"], 1) 69 | if match and ridx == 0: # login 70 | self.logger.info("VM started") 71 | 72 | # Login 73 | self.wait_write("\r", None) 74 | self.wait_write(DEFAULT_USER, wait="login:") 75 | self.wait_write(DEFAULT_PASSWORD, wait="Password:") 76 | self.wait_write("", wait="%s@" % (self.username)) 77 | self.logger.info("Login completed") 78 | 79 | # run main config! 80 | self.bootstrap_config() 81 | self.startup_config() 82 | # close telnet connection 83 | self.tn.close() 84 | # startup time? 85 | startup_time = datetime.datetime.now() - self.start_time 86 | self.logger.info(f"Startup complete in: {startup_time}") 87 | # mark as running 88 | self.running = True 89 | return 90 | 91 | # no match, if we saw some output from the router it's probably 92 | # booting, so let's give it some more time 93 | if res != b"": 94 | self.logger.trace("OUTPUT: %s" % res.decode()) 95 | # reset spins if we saw some output 96 | self.spins = 0 97 | 98 | self.spins += 1 99 | 100 | return 101 | 102 | def bootstrap_config(self): 103 | """Do the actual bootstrap config""" 104 | self.logger.info("applying bootstrap configuration") 105 | self.wait_write("sudo -i", "$") 106 | 107 | # set ipv4/6 address of the management interface if it is not managed by dhcp 108 | if not self.mgmt_address_ipv4 == "dhcp": 109 | self.wait_write( 110 | f"sudo /usr/sbin/ip address add {self.mgmt_address_ipv4} dev eth0", "#" 111 | ) 112 | if not self.mgmt_address_ipv4 == "dhcp": 113 | # note, v6 address is not being applied for whatever reason 114 | self.wait_write( 115 | f"sudo /usr/sbin/ip -6 address add {self.mgmt_address_ipv6} dev eth0", "#" 116 | ) 117 | self.wait_write("passwd -q %s" % (self.username)) 118 | self.wait_write(self.password, "New password:") 119 | self.wait_write(self.password, "password:") 120 | self.wait_write("sleep 1", "#") 121 | # set hostname by changing the default config file 122 | # using hostanamectl did not work, since the default config file is read afterwards. 123 | self.wait_write( 124 | f'sudo sed -i \'s/"hostname": "sonic",/"hostname": "{self.hostname}",/g\' /etc/sonic/config_db.json', 125 | "#", 126 | ) 127 | self.wait_write("logout", "#") 128 | self.logger.info("completed bootstrap configuration") 129 | 130 | def startup_config(self): 131 | """Load additional config provided by user.""" 132 | 133 | if not os.path.exists(CONFIG_FILE): 134 | self.logger.trace( 135 | f"Startup config file {CONFIG_FILE} is not provided, nothing to do" 136 | ) 137 | return 138 | 139 | self.logger.trace( 140 | f"Startup config file {CONFIG_FILE} found, copying it to the VM" 141 | ) 142 | 143 | subprocess.run( 144 | f"/backup.sh -u {self.username} -p {self.password} restore", 145 | check=True, 146 | shell=True, 147 | ) 148 | 149 | 150 | class Dell_SONiC(vrnetlab.VR): 151 | def __init__(self, hostname, username, password, conn_mode): 152 | super().__init__(username, password) 153 | self.vms = [Dell_Sonic_VM(hostname, username, password, conn_mode)] 154 | 155 | 156 | if __name__ == "__main__": 157 | import argparse 158 | 159 | parser = argparse.ArgumentParser(description="") 160 | parser.add_argument( 161 | "--trace", action="store_true", help="enable trace level logging" 162 | ) 163 | parser.add_argument("--hostname", default="sonic", help="SONiC hostname") 164 | parser.add_argument("--username", default="admin", help="Username") 165 | parser.add_argument("--password", default="admin", help="Password") 166 | parser.add_argument( 167 | "--connection-mode", default="tc", help="Connection mode to use in the datapath" 168 | ) 169 | args = parser.parse_args() 170 | 171 | LOG_FORMAT = "%(asctime)s: %(module)-10s %(levelname)-8s %(message)s" 172 | logging.basicConfig(format=LOG_FORMAT) 173 | logger = logging.getLogger() 174 | 175 | logger.setLevel(logging.DEBUG) 176 | if args.trace: 177 | logger.setLevel(1) 178 | 179 | vr = Dell_SONiC( 180 | args.hostname, 181 | args.username, 182 | args.password, 183 | conn_mode=args.connection_mode, 184 | ) 185 | vr.start() 186 | -------------------------------------------------------------------------------- /dev-notes.md: -------------------------------------------------------------------------------- 1 | # Developer notes 2 | 3 | ## vrnetlab module and vscode pylance 4 | 5 | We added `.env` file in the root of the repo to add `common` dir to the python path so that the pylance extension can find the module. 6 | 7 | However, if this doesn't work for you, add the following to the `settings.json` file in vscode: 8 | 9 | ```json 10 | { 11 | "python.analysis.extraPaths": ["common"] 12 | } 13 | ``` 14 | -------------------------------------------------------------------------------- /exos/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Extreme 2 | NAME=EXOS 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # match versions like: 7 | # v15.3.2.11 8 | VERSION=$(shell echo $(IMAGE) | sed -n 's/^EXOS-VM_\(.*\)\.qcow.*/\1/p' | cut -d'-' -f1) 9 | 10 | -include ../makefile-sanity.include 11 | -include ../makefile.include 12 | -------------------------------------------------------------------------------- /exos/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/debian:bookworm-slim 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | 5 | RUN apt-get update -qy \ 6 | && apt-get install --no-install-recommends -y \ 7 | iproute2 \ 8 | python3 \ 9 | socat \ 10 | qemu-kvm \ 11 | qemu-utils \ 12 | telnet \ 13 | && rm -rf /var/lib/apt/lists/* 14 | 15 | ARG IMAGE 16 | COPY $IMAGE* / 17 | COPY *.py / 18 | 19 | EXPOSE 22 80 161/udp 443 830 20 | HEALTHCHECK CMD ["/healthcheck.py"] 21 | ENTRYPOINT ["/launch.py"] 22 | -------------------------------------------------------------------------------- /exos/docker/launch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import datetime 4 | import logging 5 | import re 6 | import signal 7 | import sys 8 | import time 9 | import os 10 | 11 | import vrnetlab 12 | 13 | STARTUP_CONFIG_FILE = "/config/startup-config.xsf" 14 | 15 | 16 | def handle_SIGCHLD(signal, frame): 17 | os.waitpid(-1, os.WNOHANG) 18 | 19 | 20 | def handle_SIGTERM(signal, frame): 21 | sys.exit(0) 22 | 23 | 24 | signal.signal(signal.SIGINT, handle_SIGTERM) 25 | signal.signal(signal.SIGTERM, handle_SIGTERM) 26 | signal.signal(signal.SIGCHLD, handle_SIGCHLD) 27 | 28 | TRACE_LEVEL_NUM = 9 29 | logging.addLevelName(TRACE_LEVEL_NUM, "TRACE") 30 | 31 | 32 | def trace(self, message, *args, **kws): 33 | # Yes, logger takes its '*args' as 'args'. 34 | if self.isEnabledFor(TRACE_LEVEL_NUM): 35 | self._log(TRACE_LEVEL_NUM, message, args, **kws) 36 | 37 | 38 | logging.Logger.trace = trace 39 | 40 | 41 | class EXOS_vm(vrnetlab.VM): 42 | def __init__(self, username, password, hostname, conn_mode): 43 | disk_image = None 44 | for e in sorted(os.listdir("/")): 45 | if not disk_image and re.search(".qcow2$", e): 46 | disk_image = "/" + e 47 | 48 | super(EXOS_vm, self).__init__( 49 | username, 50 | password, 51 | disk_image=disk_image, 52 | ram=512, 53 | cpu="core2duo", 54 | driveif="ide", 55 | ) 56 | 57 | self.hostname = hostname 58 | self.conn_mode = conn_mode 59 | self.num_nics = 13 60 | self.nic_type = "rtl8139" 61 | 62 | def bootstrap_spin(self): 63 | """ This function should be called periodically to do work. 64 | """ 65 | 66 | if self.spins > 300: 67 | # too many spins with no result -> give up 68 | self.stop() 69 | self.start() 70 | return 71 | 72 | (ridx, match, res) = self.tn.expect([rb'node is now available for login.', 73 | rb'\[[yY]\/[nN]\/q\]'], 1) 74 | 75 | if match: # got a match! 76 | if ridx == 0: 77 | time.sleep(1) 78 | self.wait_write(cmd='', wait=None) 79 | self.wait_write(cmd='admin', wait='login:') 80 | self.wait_write(cmd='', wait='password:') 81 | else: 82 | self.wait_write(cmd='q', wait=None) 83 | self.wait_write(cmd='', wait='#') 84 | self.logger.info("Found config prompt") 85 | # run main config! 86 | self.logger.info("Running bootstrap_config()") 87 | self.bootstrap_config() 88 | self.startup_config() 89 | (ridx, match, res) = self.tn.expect([rb'node is now available for login.'],1) 90 | time.sleep(1) 91 | # close telnet connection 92 | self.tn.close() 93 | # startup time? 94 | startup_time = datetime.datetime.now() - self.start_time 95 | self.logger.info("Startup complete in: %s" % startup_time) 96 | # mark as running 97 | self.running = True 98 | return 99 | 100 | # no match, if we saw some output from the router it's probably 101 | # booting, so let's give it some more time 102 | if res != b'': 103 | self.logger.trace("OUTPUT: %s" % res.decode()) 104 | # reset spins if we saw some output 105 | self.spins = 0 106 | 107 | self.spins += 1 108 | 109 | return 110 | 111 | def bootstrap_config(self): 112 | """ Do the actual bootstrap config 113 | """ 114 | self.wait_write(cmd=f"configure snmp sysName {self.hostname}", wait=None) 115 | self.wait_write(cmd="configure vlan Mgmt ipaddress 10.0.0.15/24", wait="#") 116 | self.wait_write(cmd="configure iproute add default 10.0.0.2 vr VR-Mgmt", wait="#") 117 | if self.username == 'admin': 118 | self.wait_write(cmd="configure account admin password", wait="#") 119 | self.wait_write(cmd="", wait="Current user's password:") 120 | self.wait_write(cmd=self.password, wait="New password:") 121 | self.wait_write(cmd=self.password, wait="Reenter password:") 122 | else: 123 | self.wait_write(cmd=f"create account admin {self.username} {self.password}", wait="#") 124 | self.wait_write(cmd="disable cli prompting", wait="#") 125 | self.wait_write(cmd="enable ssh2", wait="#") 126 | self.wait_write(cmd="save", wait="#") 127 | 128 | def startup_config(self): 129 | if not os.path.exists(STARTUP_CONFIG_FILE): 130 | self.logger.trace(f"Startup config file {STARTUP_CONFIG_FILE} not found") 131 | self.wait_write(cmd="enable cli prompting", wait="#") 132 | return 133 | vrnetlab.run_command(["cp", STARTUP_CONFIG_FILE, "/tftpboot/containerlab.xsf"]) 134 | self.wait_write(cmd="tftp get 10.0.0.2 vr VR-Mgmt containerlab.xsf", wait=None) 135 | self.wait_write(cmd="load script containerlab.xsf", wait="#") 136 | self.wait_write(cmd="save", wait="#") 137 | self.wait_write(cmd="enable cli prompting", wait="#") 138 | 139 | 140 | class EXOS(vrnetlab.VR): 141 | def __init__(self, hostname, username, password, conn_mode): 142 | super(EXOS, self).__init__(username, password) 143 | self.vms = [EXOS_vm(username, password,hostname, conn_mode)] 144 | 145 | 146 | if __name__ == '__main__': 147 | import argparse 148 | parser = argparse.ArgumentParser(description='') 149 | parser.add_argument( 150 | "--trace", action="store_true", help="enable trace level logging" 151 | ) 152 | parser.add_argument('--hostname', default='vr-exos', help='Router hostname') 153 | parser.add_argument('--username', default='vrnetlab', help='Username') 154 | parser.add_argument('--password', default='VR-netlab9', help='Password') 155 | parser.add_argument( 156 | "--connection-mode", 157 | default="tc", 158 | help="Connection mode to use in the datapath", 159 | ) 160 | 161 | args = parser.parse_args() 162 | 163 | LOG_FORMAT = "%(asctime)s: %(module)-10s %(levelname)-8s %(message)s" 164 | logging.basicConfig(format=LOG_FORMAT) 165 | logger = logging.getLogger() 166 | 167 | logger.setLevel(logging.DEBUG) 168 | 169 | if args.trace: 170 | logger.setLevel(1) 171 | 172 | vr = EXOS( 173 | args.hostname, args.username, args.password, conn_mode=args.connection_mode 174 | ) 175 | vr.start() 176 | -------------------------------------------------------------------------------- /fortigate/.gitignore: -------------------------------------------------------------------------------- 1 | *.qcow2 -------------------------------------------------------------------------------- /fortigate/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Fortigate 2 | VR_NAME=fortios 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qcow2 5 | -include ../makefile-sanity.include 6 | -include ../makefile.include 7 | IMAGE=$(shell find . -type f -iname *.qcow2 | head -n1 | cut -c3- ) 8 | VERSION=$(shell echo $(IMAGE) | sed 's|^fortios-v||' | sed -e 's/\.qcow2//') 9 | .DEFAULT_GOAL=docker-build-fortigate 10 | docker-build-fortigate: 11 | -rm -f docker/$(IMAGE) docker/healthcheck.py docker/vrnetlab.py 12 | cp $(IMAGE) docker 13 | cp ../common/* docker/ 14 | @echo "Building docker image using $(IMAGE) as $(REGISTRY)vr-$(VR_NAME):$(VERSION)" 15 | (cd docker; docker build --build-arg IMAGE=$(IMAGE) --build-arg http_proxy=$(http_proxy) --build-arg https_proxy=$(https_proxy) -t $(REGISTRY)vr-$(VR_NAME):$(VERSION) .) 16 | -rm -f docker/$(IMAGE) docker/healthcheck.py docker/vrnetlab.py 17 | 18 | docker-run-fortigate: 19 | docker run -it -d --privileged --rm $(REGISTRY)vr-$(VR_NAME):$(VERSION) 20 | -------------------------------------------------------------------------------- /fortigate/README.md: -------------------------------------------------------------------------------- 1 | # Fortinet Fortigate 2 | 3 | Support for the Fortinet Fortigate launched by containerlab. 4 | 5 | ## Building the docker image 6 | 7 | Add your qcow2 image to the root of this folder. 8 | Naming format: fortios-vX.Y.Z.qcow2 9 | 10 | `make` 11 | 12 | ## Running the docker image manually 13 | 14 | If you need to run the image without using containerlab: 15 | 16 | `make docker-run-fortigate` 17 | 18 | ## Tested versions 19 | 20 | * Fortigate 7.0.14 KVM 21 | -------------------------------------------------------------------------------- /fortigate/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm-slim 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | RUN apt-get update -qy \ 5 | && apt-get upgrade -qy \ 6 | && apt-get install -y \ 7 | bridge-utils \ 8 | iproute2 \ 9 | python3-ipy \ 10 | socat \ 11 | qemu-kvm \ 12 | tcpdump \ 13 | tftpd-hpa \ 14 | ssh \ 15 | inetutils-ping \ 16 | dnsutils \ 17 | openvswitch-switch \ 18 | iptables \ 19 | telnet \ 20 | genisoimage \ 21 | dos2unix \ 22 | vim \ 23 | curl \ 24 | && rm -rf /var/lib/apt/lists/* 25 | 26 | ARG IMAGE 27 | 28 | 29 | COPY *.py / 30 | COPY $IMAGE / 31 | EXPOSE 22 161/udp 830 5000 10000-10099 3443 80 443 32 | HEALTHCHECK CMD ["python3", "/healthcheck.py"] 33 | ENTRYPOINT ["/launch.py"] -------------------------------------------------------------------------------- /fortigate/docker/README.md: -------------------------------------------------------------------------------- 1 | # vrnetlab / Fortinet Fortigaste v7 2 | ======================= 3 | Experimental support for Fortinet fortigate launched by containerlab. 4 | 5 | ## Building the docker image 6 | Add your qcow2 image to the root of this folder. 7 | Naming format: fortios-vX.Y.Z.qcow2 8 | 9 | `make docker-build-fortigate` 10 | 11 | ## Running the docker image 12 | `make docker-run-fortigate` 13 | -------------------------------------------------------------------------------- /fortigate/docker/launch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import datetime 3 | import logging 4 | import os 5 | import re 6 | import signal 7 | import sys 8 | import uuid 9 | 10 | import vrnetlab 11 | 12 | 13 | def handle_SIGCHLD(_unused_signal, _unused_frame): 14 | os.waitpid(-1, os.WNOHANG) 15 | 16 | 17 | def handle_SIGTERM(_unused_signal, _unused_frame): 18 | sys.exit(0) 19 | 20 | 21 | signal.signal(signal.SIGINT, handle_SIGTERM) 22 | signal.signal(signal.SIGTERM, handle_SIGTERM) 23 | signal.signal(signal.SIGCHLD, handle_SIGCHLD) 24 | 25 | TRACE_LEVEL_NUM = 9 26 | logging.addLevelName(TRACE_LEVEL_NUM, "TRACE") 27 | 28 | 29 | def trace(self, message, *args, **kws): 30 | # Yes, logger takes its '*args' as 'args'. 31 | if self.isEnabledFor(TRACE_LEVEL_NUM): 32 | self.log(TRACE_LEVEL_NUM, message, *args, **kws) 33 | 34 | 35 | logging.Logger.trace = trace 36 | 37 | 38 | class FortiOS_vm(vrnetlab.VM): 39 | def __init__(self, hostname, username, password, conn_mode): 40 | for e in os.listdir("."): 41 | if re.search(".qcow2$", e): 42 | disk_image = "./" + e 43 | # call parents __init__ function here 44 | super(FortiOS_vm, self).__init__( 45 | username, 46 | password, 47 | disk_image=disk_image, 48 | ram=2048, 49 | driveif="virtio", 50 | # fortios fails to respond to network requests if the pci bus is setup :D 51 | provision_pci_bus=False, 52 | ) 53 | self.conn_mode = conn_mode 54 | self.hostname = hostname 55 | self.num_nics = 12 56 | self.nic_type = "virtio-net-pci" 57 | self.highest_port = 0 58 | self.qemu_args.extend(["-uuid", os.getenv("FORTIGATE_UUID") or str(uuid.uuid4())]) 59 | self.spins = 0 60 | self.running = None 61 | 62 | # set up the extra empty disk image 63 | # for fortigate logs 64 | vrnetlab.run_command( 65 | ["qemu-img", "create", "-f", "qcow2", "empty.qcow2", "30G"] 66 | ) 67 | 68 | self.qemu_args.extend( 69 | [ 70 | "-drive", 71 | "if=virtio,format=qcow2,file=empty.qcow2,index=1", 72 | ] 73 | ) 74 | 75 | 76 | def bootstrap_spin(self): 77 | """This function should be called periodically to do work. 78 | 79 | returns False when it has failed and given up, otherwise True 80 | """ 81 | if self.spins > 300: 82 | # too many spins with no result -> restart 83 | self.logger.warning("no output from serial console, restarting VCP") 84 | self.stop() 85 | self.start() 86 | self.spins = 0 87 | return 88 | 89 | (ridx, match, res) = self.tn.expect([b"login:", b"FortiGate-VM64-KVM #"], 1) 90 | if match: # got a match! 91 | if ridx == 0: # matched login prompt, so should login 92 | self.logger.debug("ridx == 0") 93 | self.logger.info("matched login prompt") 94 | 95 | self.wait_write(self.username, wait=None) 96 | self.wait_write("", wait=self.username) 97 | self.wait_write(self.password, wait="Password") 98 | self.wait_write(self.password, wait=None) 99 | 100 | if ridx == 1: 101 | # if we dont match the FortiGate-VM64-KVM # we assume we already have some configuration and 102 | # may continue with configure the system to our needs. 103 | self.logger.debug("ridx == 1") 104 | self.wait_write("config system global", wait=None) 105 | hostname_command = "set hostname " + self.hostname 106 | self.wait_write(hostname_command, wait="global") 107 | self.wait_write("end", wait=hostname_command) 108 | self.running = True 109 | self.tn.close() 110 | # calc startup time 111 | startup_time = datetime.datetime.now() - self.start_time 112 | self.logger.info(f"Startup complete in { startup_time }") 113 | return 114 | 115 | else: 116 | # no match, if we saw some output from the router it's probably 117 | # booting, so let's give it some more time 118 | if res != b"": 119 | self.logger.trace(f"OUTPUT FORTIGATE: {res.decode()}") 120 | # reset spins if we saw some output 121 | self.spins = 0 122 | 123 | self.spins += 1 124 | 125 | def _wait_reset(self): 126 | """ 127 | This function waits for the login prompt after the VM was resetted. 128 | If commands are issued that enforce a reboot this comes in hand. 129 | e.g factoryreset or factoryreset2 130 | """ 131 | self.logger.debug("waiting for reset") 132 | wait_spins = 0 133 | while wait_spins < 90: 134 | _, match, data = self.tn.expect([b"login: "], timeout=10) 135 | self.logger.trace(data.decode("UTF-8")) 136 | if match: 137 | self.logger.debug("reset finished") 138 | return True 139 | wait_spins += 1 140 | self.logger.error("Reset took to long") 141 | return False 142 | 143 | 144 | class FortiOS(vrnetlab.VR): 145 | def __init__(self, hostname, username, password, conn_mode): 146 | super(FortiOS, self).__init__(username, password) 147 | self.logger.debug("Hostname") 148 | self.logger.debug(hostname) 149 | self.vms = [FortiOS_vm(hostname, username, password, conn_mode)] 150 | 151 | 152 | if __name__ == "__main__": 153 | import argparse 154 | 155 | parser = argparse.ArgumentParser(description="") 156 | parser.add_argument( 157 | "--trace", action="store_true", help="enable trace level logging" 158 | ) 159 | parser.add_argument("--hostname", default="vr-fortinet", help="Fortinet hostname") 160 | parser.add_argument("--username", default="admin", help="Username") 161 | parser.add_argument("--password", default="admin", help="Password") 162 | parser.add_argument( 163 | "--connection-mode", 164 | default="tc", 165 | help="Connection mode to use in the datapath", 166 | ) 167 | args = parser.parse_args() 168 | 169 | LOG_FORMAT = "%(asctime)s: %(module)-10s %(levelname)-8s %(message)s" 170 | logging.basicConfig(format=LOG_FORMAT) 171 | logger = logging.getLogger() 172 | 173 | logger.setLevel(logging.DEBUG) 174 | if args.trace: 175 | logger.setLevel(1) 176 | vrnetlab.boot_delay() 177 | vr = FortiOS( 178 | args.hostname, args.username, args.password, conn_mode=args.connection_mode 179 | ) 180 | vr.start() 181 | -------------------------------------------------------------------------------- /freebsd/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=FreeBSD 2 | NAME=FreeBSD 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # match versions like: 7 | # freebsd-13.2-zfs-2023-04-21.qcow2 8 | VERSION=$(shell echo $(IMAGE) | sed -e 's/freebsd-\([0-9]\+\.[0-9]\)-.*/\1/') 9 | 10 | -include ../makefile-sanity.include 11 | -include ../makefile.include 12 | 13 | download: 14 | /bin/bash download.sh 15 | 16 | build: download 17 | $(MAKE) docker-image -------------------------------------------------------------------------------- /freebsd/README.md: -------------------------------------------------------------------------------- 1 | # vrnetlab / FreeBSD 2 | 3 | This is the vrnetlab docker image for FreeBSD. 4 | 5 | This docker image requires a custom-built FreeBSD image with pre-installed [cloud-init](https://cloudinit.readthedocs.io/en/latest/). You can download such images from https://bsd-cloud-image.org/. 6 | 7 | ## Building the docker image 8 | 9 | Run `make download`. It will try to download the latest FreeBSD release from https://bsd-cloud-image.org/ to this directory. Then run `make` to build a docker image. 10 | 11 | If for some reasons you can't obtain an image from https://bsd-cloud-image.org/, you can build it yourself with the script from [this repository](https://github.com/goneri/pcib). 12 | 13 | It's been tested to boot, respond to SSH and have correct interface mapping 14 | with the following images: 15 | 16 | * freebsd-13.2-zfs-2023-04-21.qcow2 17 | 18 | ## Usage 19 | 20 | ``` 21 | docker run -d --privileged --name vrnetlab/vr-freebsd: --username --password 22 | ``` 23 | 24 | Where: 25 | 26 | * `container_name` - name of the created container. 27 | * `tag`- FreeBSD release version (e.g., 13.2). 28 | * `username`, `password` - FreeBSD VM credentials. 29 | 30 | Example: 31 | 32 | ``` 33 | docker run -d --privileged --name my-obsd-router vrnetlab/vr-freebsd:13.2 --username admin --password admin 34 | ``` 35 | 36 | It will take about 1 minute for the container to boot. After that, you can try to ssh to the container's IP or telnet to port 5000 for console access. 37 | 38 | To obtain the container's IP run: 39 | 40 | ``` 41 | docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' 42 | ``` 43 | 44 | ## Interface mapping 45 | 46 | Interface `vtnet0` is always configured as a management interface. Interfaces `vtnet1` to `vio16` can be used for data plane. 47 | 48 | ## System requirements 49 | 50 | CPU: 1 core 51 | RAM: 512MB 52 | DISK: 4.0GB 53 | -------------------------------------------------------------------------------- /freebsd/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/debian:bookworm-slim AS builder 2 | 3 | ARG DISK_SIZE=4G 4 | 5 | RUN apt-get update -qy && \ 6 | apt-get install -y --no-install-recommends qemu-utils && \ 7 | rm -rf /var/lib/apt/lists/* 8 | 9 | ARG IMAGE 10 | COPY $IMAGE* / 11 | RUN qemu-img resize /$IMAGE $DISK_SIZE 12 | 13 | FROM public.ecr.aws/docker/library/debian:bookworm-slim 14 | 15 | ARG DEBIAN_FRONTEND=noninteractive 16 | ARG DISK_SIZE=4G 17 | 18 | RUN apt-get update -qy \ 19 | && apt-get install -y --no-install-recommends\ 20 | bridge-utils \ 21 | iproute2 \ 22 | socat \ 23 | qemu-kvm \ 24 | tcpdump \ 25 | ssh \ 26 | inetutils-ping \ 27 | dnsutils \ 28 | iptables \ 29 | nftables \ 30 | telnet \ 31 | cloud-utils \ 32 | sshpass \ 33 | && rm -rf /var/lib/apt/lists/* 34 | 35 | ARG IMAGE 36 | COPY --from=builder $IMAGE* / 37 | COPY *.py / 38 | COPY --chmod=0755 backup.sh / 39 | 40 | EXPOSE 22 5000 10000-10099 41 | HEALTHCHECK CMD ["/healthcheck.py"] 42 | ENTRYPOINT ["/launch.py"] 43 | -------------------------------------------------------------------------------- /freebsd/docker/backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DEFAULT_USER="admin" 4 | DEFAULT_PASSWORD="admin" 5 | BACKUP_FILE="backup.tar.gz" 6 | BACKUP_PATH=/config/$BACKUP_FILE 7 | REMOTE_BACKUP_PATH=/tmp/$BACKUP_FILE 8 | 9 | handle_args() { 10 | # Parse options 11 | while getopts 'u:p:' OPTION; do 12 | case "$OPTION" in 13 | u) 14 | user="$OPTARG" 15 | ;; 16 | p) 17 | password="$OPTARG" 18 | ;; 19 | ?) 20 | usage 21 | exit 1 22 | ;; 23 | esac 24 | done 25 | shift "$(($OPTIND -1))" 26 | 27 | # Assign defaults if options weren't provided 28 | if [ -z "$user" ] ; then 29 | user=$DEFAULT_USER 30 | fi 31 | if [ -z "$password" ] ; then 32 | password=$DEFAULT_PASSWORD 33 | fi 34 | 35 | SSH_CMD="sshpass -p $password ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" 36 | SCP_CMD="sshpass -p $password scp -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" 37 | HOST="$user@localhost" 38 | 39 | # Parse commands 40 | case $1 in 41 | 42 | backup) 43 | backup 44 | ;; 45 | 46 | restore) 47 | restore 48 | ;; 49 | 50 | *) 51 | usage 52 | ;; 53 | esac 54 | } 55 | 56 | usage() { 57 | echo "Usage: $(basename $0) [-u USERNAME] [-p PASSWORD] COMMAND" 58 | echo "Options:" 59 | echo " -u USERNAME VM SSH username (default: admin)" 60 | echo " -p PASSWORD VM SSH password (default: admin)" 61 | echo 62 | echo "Commands:" 63 | echo " backup Backup VM /etc directory to $BACKUP_PATH" 64 | echo " restore Restore VM /etc directory from $BACKUP_PATH" 65 | exit 0; 66 | } 67 | 68 | backup() { 69 | echo "Backing up..." 70 | $SSH_CMD $HOST "sudo tar zcf $REMOTE_BACKUP_PATH /etc > & /dev/null" 71 | $SCP_CMD $HOST:$REMOTE_BACKUP_PATH $BACKUP_PATH 72 | } 73 | 74 | restore() { 75 | if [ -f "$BACKUP_PATH" ]; then 76 | echo "Restoring from backup..." 77 | # Put backup file to VM, untar, and reboot. 78 | $SCP_CMD $BACKUP_PATH $HOST:$REMOTE_BACKUP_PATH && $SSH_CMD $HOST "sudo tar xzf $REMOTE_BACKUP_PATH -C /" && $SSH_CMD $HOST "sudo shutdown -r now || true" 79 | else 80 | echo "$BACKUP_PATH not found. Nothing to restore." 81 | fi 82 | } 83 | 84 | handle_args "$@" 85 | -------------------------------------------------------------------------------- /freebsd/download.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Webpage with links to images 4 | base_url="https://bsd-cloud-image.org" 5 | 6 | # Download the webpage content 7 | webpage_content=$(curl -s "$base_url") 8 | 9 | # Find URLs that match the pattern "freebsd*zfs*.qcow2" and select the most recent version 10 | download_url=$(echo "$webpage_content" | grep -oE 'https?://[^"]+freebsd[^"]+zfs[^"]+\.qcow2' | sort | tail -n 1) 11 | 12 | # Extract the filename from the URL 13 | filename=$(basename "$download_url") 14 | 15 | # Check if the file already exists in the current directory 16 | if [ -e "$filename" ]; then 17 | echo "File $filename already exists. Skipping download." 18 | else 19 | # Download the URL 20 | curl -O "$download_url" 21 | echo "Download complete: $filename" 22 | fi -------------------------------------------------------------------------------- /ftdv/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Cisco 2 | NAME=FTDv 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # match versions like: 7 | # Cisco_Secure_Firewall_Threat_Defense_Virtual-7.2.5-208.qcow2 8 | VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+-\([0-9]\+\.[0-9]\+\.[0-9]\+\)-[0-9]\+.*/\1/') 9 | 10 | -include ../makefile-sanity.include 11 | -include ../makefile.include 12 | -include ../makefile-install.include 13 | -------------------------------------------------------------------------------- /ftdv/README.md: -------------------------------------------------------------------------------- 1 | # vrnetlab / Cisco FTDv 2 | 3 | This is the vrnetlab docker image for Cisco FTDv. 4 | 5 | ## Building the docker image 6 | 7 | Download the FTDv KVM install package from https://software.cisco.com/download/home/286306503/type/286306337. A valid service contract associated with your Cisco.com profile is required. 8 | 9 | Put the `.qcow2` file in this directory and run `make` and you should be good to go. The resulting image is called `vrnetlab/vr-ftdv`. 10 | 11 | It's been tested to boot, respond to SSH and have correct interface mapping 12 | with the following images: 13 | 14 | * Cisco_Secure_Firewall_Threat_Defense_Virtual-7.2.5-208.qcow2 15 | 16 | ## Usage 17 | 18 | ``` 19 | docker run -d --privileged --name vrnetlab/vr-ftdv: --username --password 20 | ``` 21 | 22 | Where: 23 | 24 | * `container_name` - name of the created container. 25 | * `tag`- FTDv version (e.g., 7.2.5). 26 | * `username`, `password` - FTDv credentials. 27 | 28 | Example: 29 | 30 | ``` 31 | docker run -d --privileged --name my-ftdv vrnetlab/vr-ftdv:7.2.5 --username admin --password Admin123! 32 | ``` 33 | 34 | It will take about 1-2 minutes for the container to boot. After that, you can try to ssh to the container's IP or telnet to port 5000 for console access. 35 | 36 | To obtain the container's IP run: 37 | 38 | ``` 39 | docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' 40 | ``` 41 | 42 | ## System requirements 43 | 44 | CPU: 4 core 45 | RAM: 8GB 46 | Disk: 4.6GB (Thin Provision disk size is 48.24GB) 47 | -------------------------------------------------------------------------------- /ftdv/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm-slim 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | 5 | RUN apt-get update -qy \ 6 | && apt-get upgrade -qy \ 7 | && apt-get install -y \ 8 | bridge-utils \ 9 | iproute2 \ 10 | python3-ipy \ 11 | socat \ 12 | qemu-kvm \ 13 | tcpdump \ 14 | ssh \ 15 | inetutils-ping \ 16 | dnsutils \ 17 | iptables \ 18 | nftables \ 19 | telnet \ 20 | genisoimage \ 21 | && rm -rf /var/lib/apt/lists/* 22 | 23 | ARG IMAGE 24 | COPY $IMAGE* / 25 | COPY *.py / 26 | 27 | EXPOSE 22 80 161/udp 443 5000 8305 10000-10099 28 | HEALTHCHECK CMD ["/healthcheck.py"] 29 | ENTRYPOINT ["/launch.py"] 30 | -------------------------------------------------------------------------------- /ftosv/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Dell 2 | NAME=FTOS10v 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # match versions like: dellftos.10.9.2.4.qcow2 7 | VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+\([0-9][0-9]\.[0-9]\.[0-9]\.[0-9]\)\([^0-9].*\|$$\)/\1/') 8 | 9 | -include ../makefile-sanity.include 10 | -include ../makefile.include 11 | 12 | -------------------------------------------------------------------------------- /ftosv/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/debian:bookworm-slim 2 | LABEL maintainer="Kristian Larsson " 3 | LABEL maintainer="Roman Dodin " 4 | LABEL maintainer="Stefano Sasso " 5 | 6 | ARG DEBIAN_FRONTEND=noninteractive 7 | RUN apt-get update -qy \ 8 | && apt-get install --no-install-recommends -y \ 9 | dosfstools \ 10 | bridge-utils \ 11 | iproute2 \ 12 | python3 \ 13 | socat \ 14 | ssh \ 15 | tcpdump \ 16 | qemu-kvm \ 17 | qemu-utils \ 18 | inetutils-ping \ 19 | dnsutils \ 20 | telnet \ 21 | && rm -rf /var/lib/apt/lists/* 22 | 23 | ARG IMAGE 24 | COPY $IMAGE* / 25 | COPY *.py / 26 | 27 | EXPOSE 22 80 161/udp 443 830 5000 6030 10000-10099 57400 28 | HEALTHCHECK CMD ["/healthcheck.py"] 29 | ENTRYPOINT ["/launch.py"] 30 | -------------------------------------------------------------------------------- /huawei_vrp/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Huawei 2 | NAME=VRP 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # match versions like: 7 | # huawei_ne40e-.qcow2 8 | # huawei_ce12800-.qcow2 9 | VERSION=$(shell echo $(IMAGE) | sed -e 's/huawei_\(ne40e\|ce12800\)-\(.*\)\.qcow2/\1-\2/') 10 | 11 | -include ../makefile-sanity.include 12 | -include ../makefile.include 13 | -------------------------------------------------------------------------------- /huawei_vrp/README.md: -------------------------------------------------------------------------------- 1 | # Huawei NE40E 2 | 3 | Rename your qcow2 disk image to conform to the following pattern: 4 | 5 | ``` 6 | huawei_ne40e-.qcow2 7 | or 8 | huawei_ce12800-.qcow2 9 | ``` 10 | 11 | Build the image with: 12 | 13 | ``` 14 | make 15 | ``` 16 | 17 | The resulting image will be tagged as: 18 | 19 | ``` 20 | vrnetlab/huawei_vrp:- 21 | ``` 22 | 23 | for example, if the qcow2 image is named `huawei_ne40e-8.180.qcow2`, then the image will be tagged as: 24 | 25 | ``` 26 | vrnetlab/huawei_vrp:ne40e-8.180 27 | ``` 28 | -------------------------------------------------------------------------------- /huawei_vrp/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/srl-labs/vrnetlab-base:0.1.0 2 | 3 | ARG IMAGE 4 | COPY $IMAGE* / 5 | COPY *.py / 6 | 7 | EXPOSE 22 80 443 161/udp 830 5000 10000-10099 57400 8 | HEALTHCHECK CMD ["/healthcheck.py"] 9 | ENTRYPOINT ["/launch.py"] 10 | -------------------------------------------------------------------------------- /makefile-install.include: -------------------------------------------------------------------------------- 1 | # 2 | # This Makefile can be included by images that need to run an install phase, 3 | # i.e. in addition to doing the docker build, we also want to run some stuff 4 | # inside that image to come up with the final output image. in the case of 5 | # JUNOS we want to do this as the first time the vMX RE boots up it detects 6 | # that it's in a vMX RE mode and then reboots. By starting it up and letting it 7 | # do this first check-and-reboot during the image build time we save ourselves 8 | # from doing this on *every* run of the container image later. 9 | # 10 | # Since we start the virtual router we are actually running a virtual machine 11 | # with qemu and for that we want KVM hardware acceleration, which requires 12 | # running docker with --privileged. `docker build` doesn't have the 13 | # --privileged argument, so instead we first run the build as normal up to the 14 | # point where we want to start the virtual router. Then we use `docker run 15 | # --privilged ...` do the needful and after commit it using `docker commit ...` 16 | # to create the final output image. 17 | # 18 | # One of the problems with this is that normally the docker build is kind of 19 | # idempotent in that it uses a command cache and if there are no changes to the 20 | # Dockerfile or input files it will not rerun those commands but use a cached 21 | # image. This greatly speeds up the build process. However, when doing this 22 | # manual `docker run` dance we miss this opportunity since it will always be 23 | # run.... so we worked around it. Before doing docker run we check the SHA sum 24 | # of the built image and compare this to the ones of the previously built 25 | # image. If they are the same it means the docker build was entirely cached and 26 | # there's no need to run the image, otherwise if there's a change we do run it. 27 | # When comparing the hashes we omit the last layer of the previous build. It 28 | # contains the committed changes from the install phase of the previous build. 29 | # Include this makefile to have your image automatically do that dance. 30 | 31 | docker-pre-build: 32 | -cat cidfile | xargs --no-run-if-empty docker rm -f 33 | -rm cidfile 34 | -docker tag $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION) $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION)-previous-build 35 | 36 | docker-build: docker-build-common 37 | -docker inspect --format '{{.RootFS.Layers}}' $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION)-previous-build | tr -d '][' | awk '{ $$(NF)=""; print }' > built-image-sha-previous 38 | docker inspect --format '{{.RootFS.Layers}}' $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION) | tr -d '][' > built-image-sha-current 39 | if [ "$$(cat built-image-sha-previous | sed -e 's/[[:space:]]*$$//')" = "$$(cat built-image-sha-current)" ]; then echo "Previous image is the same as current, retagging!"; \ 40 | docker tag $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION)-previous-build $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION) || true; \ 41 | else \ 42 | echo "Current build differ from previous, running install!"; \ 43 | docker run --cidfile cidfile --privileged $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION) --trace --install; \ 44 | docker commit --change='ENTRYPOINT ["/launch.py"]' $$(cat cidfile) $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION); \ 45 | docker rm -f $$(cat cidfile); \ 46 | fi 47 | docker rmi -f $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION)-previous-build || true 48 | rm built-image-sha* 49 | -------------------------------------------------------------------------------- /makefile-sanity.include: -------------------------------------------------------------------------------- 1 | ifdef DOCKER_REGISTRY 2 | ifneq ($(DOCKER_REGISTRY), $(shell echo $(DOCKER_REGISTRY) | sed -ne '/^[A-Za-z0-9.\/\-]\+\(:[0-9]\+\)\?\([A-Za-z0-9.\/-]\+\)\?$$/p')) 3 | $(error Bad docker registry URL. Should follow format registry.example.com/foo, registry.example.com:1234 or registry.example.com:1234/foo) 4 | endif 5 | REGISTRY=$(DOCKER_REGISTRY)/ 6 | else 7 | REGISTRY=vrnetlab/ 8 | endif 9 | -------------------------------------------------------------------------------- /makefile.include: -------------------------------------------------------------------------------- 1 | IMG_NAME=$(shell echo $(NAME) | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]') 2 | IMG_VENDOR=$(shell echo $(VENDOR) | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]') 3 | IMAGES=$(shell ls $(IMAGE_GLOB) 2>/dev/null) 4 | NUM_IMAGES=$(shell ls $(IMAGES) | wc -l) 5 | VRNETLAB_VERION=$$(git log -1 --format=format:"Commit: %H from %aD") 6 | 7 | ifeq ($(NUM_IMAGES), 0) 8 | docker-image: no-image usage 9 | else 10 | docker-image: 11 | for IMAGE in $(IMAGES); do \ 12 | echo "Making $$IMAGE"; \ 13 | $(MAKE) IMAGE=$$IMAGE docker-build; \ 14 | $(MAKE) IMAGE=$$IMAGE docker-clean-build; \ 15 | done 16 | endif 17 | 18 | docker-clean-build: 19 | @echo "--> Cleaning docker build context" 20 | -rm -f docker/*.qcow2* docker/*.tgz* docker/*.vmdk* docker/*.iso docker/*.xml docker/*.bin 21 | -rm -f docker/healthcheck.py docker/vrnetlab.py 22 | 23 | docker-pre-build: ; 24 | 25 | docker-build-image-copy: 26 | cp $(IMAGE)* docker/ 27 | 28 | docker-build-common: docker-clean-build docker-pre-build 29 | @if [ -z "$$IMAGE" ]; then echo "ERROR: No IMAGE specified"; exit 1; fi 30 | @if [ "$(IMAGE)" = "$(VERSION)" ]; then echo "ERROR: Incorrect version string ($(IMAGE)). The regexp for extracting version information is likely incorrect, check the regexp in the Makefile or open an issue at https://github.com/hellt/vrnetlab/issues/new including the image file name you are using."; exit 1; fi 31 | @echo "Building docker image using $(IMAGE) as $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION)" 32 | ifeq ($(NOT_VM_IMAGE), 1) 33 | echo "ok" 34 | else 35 | cp ../common/* docker/ 36 | endif 37 | @[ -f ./vswitch.xml ] && cp vswitch.xml docker/ || true 38 | $(MAKE) IMAGE=$$IMAGE docker-build-image-copy 39 | (cd docker; docker build --build-arg http_proxy=$(http_proxy) --build-arg HTTP_PROXY=$(HTTP_PROXY) --build-arg https_proxy=$(https_proxy) --build-arg HTTPS_PROXY=$(HTTPS_PROXY) --build-arg IMAGE=$(IMAGE) --build-arg VERSION=$(VERSION) --label "vrnetlab-version=$(VRNETLAB_VERION)" -t $(REGISTRY)$(IMG_VENDOR)_$(IMG_NAME):$(VERSION) .) 40 | 41 | docker-build: docker-build-common docker-clean-build 42 | 43 | docker-push: 44 | for IMAGE in $(IMAGES); do \ 45 | $(MAKE) IMAGE=$$IMAGE docker-push-image; \ 46 | done 47 | 48 | docker-push-image: 49 | @if [ -z "$$IMAGE" ]; then echo "ERROR: No IMAGE specified"; exit 1; fi 50 | @if [ "$(IMAGE)" = "$(VERSION)" ]; then echo "ERROR: Incorrect version string"; exit 1; fi 51 | docker push $(REGISTRY)$(VENDOR)_$(NAME):$(VERSION) 52 | 53 | usage: 54 | @echo "Usage: put the $(VENDOR) $(NAME) $(IMAGE_FORMAT) image in this directory and run:" 55 | @echo " make" 56 | 57 | no-image: 58 | @echo "ERROR: you have no $(IMAGE_FORMAT) ($(IMAGE_GLOB)) image" 59 | 60 | version-test: 61 | @echo Extracting version from filename $(IMAGE) 62 | @echo Version: $(VERSION) 63 | 64 | all: docker-image 65 | -------------------------------------------------------------------------------- /n9kv/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Cisco 2 | NAME=n9kv 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # rename the disk image file as n9kv-.qcow2 7 | # examples: 8 | # for a file named "n9kv-9300-10.5.2.qcow2" the image will be named "vrnetlab/cisco_n9kv:9300-10.5.2" 9 | VERSION=$(shell echo $(IMAGE) | sed -e 's/n9kv-\(.*\)\.qcow2/\1/') 10 | 11 | -include ../makefile-sanity.include 12 | -include ../makefile.include 13 | -------------------------------------------------------------------------------- /n9kv/README.md: -------------------------------------------------------------------------------- 1 | # Cisco Nexus 9000v / n9kv 2 | 3 | This is the vrnetlab docker image for Cisco Nexus 9000v virtual switch. 4 | 5 | 6 | ## Building the docker image 7 | 8 | Put the .qcow2 file in this directory and run make docker-image and you should be good to go. The resulting image is 9 | called vr-n9kv. You can tag it with something else if you want, like my-repo.example.com/vr-n9kv and then push it to 10 | your repo. The tag is the same as the version of the NXOS image, so if you have nxosv.9.2.4.qcow2 your final docker 11 | image will be called vr-n9kv:9.2.4 12 | 13 | 14 | ## System requirements 15 | 16 | * CPU: 4 core 17 | * RAM: 10 GB 18 | * Disk: <3GB 19 | 20 | -------------------------------------------------------------------------------- /n9kv/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/srl-labs/vrnetlab-base:0.1.0 2 | 3 | ARG IMAGE 4 | COPY $IMAGE* / 5 | COPY OVMF.fd / 6 | COPY *.py / 7 | 8 | EXPOSE 22 80 161/udp 443 830 5000 6030 10000-10099 50051 9 | HEALTHCHECK CMD ["/healthcheck.py"] 10 | ENTRYPOINT ["/launch.py"] 11 | -------------------------------------------------------------------------------- /n9kv/docker/OVMF.fd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellt/vrnetlab/5a91e1a4c2d63c3b1ad309487a59ec68fba813f9/n9kv/docker/OVMF.fd -------------------------------------------------------------------------------- /nxos/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Cisco 2 | NAME=NXOS Titanium 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # match versions like: 7 | # TODO: add example file names here 8 | # possible file names 9 | # titanium-final.7.3.0.d1.1.qcow2 10 | VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+[^0-9]\([0-9]\.[0-9]\.[0-9]\.[A-Za-z][0-9]\.[0-9]\)[^0-9].*$$/\1/') 11 | 12 | -include ../makefile-sanity.include 13 | -include ../makefile.include 14 | -------------------------------------------------------------------------------- /nxos/README.md: -------------------------------------------------------------------------------- 1 | vrnetlab / Cisco Nexus NXOS 2 | =========================== 3 | This is the vrnetlab docker image for Cisco Nexus NXOS Titanium emulator. 4 | 5 | Building the docker image 6 | ------------------------- 7 | Titanium doesn't appear to be exactly official but you can get it from the 8 | Internet. VIRL is said to include it, so you may have luck in extracting it 9 | from there. 10 | 11 | Anyway, put the .qcow2 file in this directory and run `make docker-image` and 12 | you should be good to go. The resulting image is called `vr-nxos`. You can tag 13 | it with something else if you want, like `my-repo.example.com/vr-nxos` and then 14 | push it to your repo. The tag is the same as the version of the NXOS image, so 15 | if you have nxosv-7.2.0.D1.1.qcow2 your final docker image will be called 16 | vr-nxos:7.2.0.D1.1 17 | 18 | Usage 19 | ----- 20 | ``` 21 | docker run -d --privileged --name my-nxos-router vr-nxos 22 | ``` 23 | 24 | System requirements 25 | ------------------- 26 | CPU: 1 core 27 | 28 | RAM: 2GB 29 | 30 | Disk: <500MB 31 | 32 | 33 | FUAQ - Frequently or Unfrequently Asked Questions 34 | ------------------------------------------------- 35 | ##### Q: Has this been extensively tested? 36 | A: Nope. I don't use Nexus myself (yet) so not much testing at all really. 37 | Please do try it out and let me know if it works. 38 | -------------------------------------------------------------------------------- /nxos/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/srl-labs/vrnetlab-base:0.1.0 2 | 3 | ARG IMAGE 4 | COPY $IMAGE* / 5 | COPY *.py / 6 | 7 | EXPOSE 22 161/udp 830 5000 10000-10099 8 | HEALTHCHECK CMD ["/healthcheck.py"] 9 | ENTRYPOINT ["/launch.py"] 10 | -------------------------------------------------------------------------------- /nxos/docker/launch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import datetime 4 | import logging 5 | import os 6 | import re 7 | import signal 8 | import sys 9 | import time 10 | 11 | import vrnetlab 12 | from scrapli.driver.core import NXOSDriver 13 | 14 | STARTUP_CONFIG_FILE = "/config/startup-config.cfg" 15 | 16 | 17 | def handle_SIGCHLD(signal, frame): 18 | os.waitpid(-1, os.WNOHANG) 19 | 20 | 21 | def handle_SIGTERM(signal, frame): 22 | sys.exit(0) 23 | 24 | 25 | signal.signal(signal.SIGINT, handle_SIGTERM) 26 | signal.signal(signal.SIGTERM, handle_SIGTERM) 27 | signal.signal(signal.SIGCHLD, handle_SIGCHLD) 28 | 29 | TRACE_LEVEL_NUM = 9 30 | logging.addLevelName(TRACE_LEVEL_NUM, "TRACE") 31 | 32 | 33 | def trace(self, message, *args, **kws): 34 | # Yes, logger takes its '*args' as 'args'. 35 | if self.isEnabledFor(TRACE_LEVEL_NUM): 36 | self._log(TRACE_LEVEL_NUM, message, args, **kws) 37 | 38 | 39 | logging.Logger.trace = trace 40 | 41 | 42 | class NXOS_vm(vrnetlab.VM): 43 | def __init__(self, hostname, username, password, conn_mode): 44 | for e in os.listdir("/"): 45 | if re.search(".qcow2$", e): 46 | disk_image = "/" + e 47 | super(NXOS_vm, self).__init__( 48 | username, 49 | password, 50 | disk_image=disk_image, 51 | ram=4096, 52 | smp="2", 53 | use_scrapli=True, 54 | ) 55 | self.credentials = [["admin", "admin"]] 56 | self.hostname = hostname 57 | self.conn_mode = conn_mode 58 | 59 | def bootstrap_spin(self): 60 | """This function should be called periodically to do work.""" 61 | 62 | if self.spins > 300: 63 | # too many spins with no result -> give up 64 | self.stop() 65 | self.start() 66 | return 67 | 68 | (ridx, match, res) = self.con_expect([b"login:"]) 69 | if match: # got a match! 70 | if ridx == 0: # login 71 | self.logger.debug("matched login prompt") 72 | try: 73 | username, password = self.credentials.pop(0) 74 | except IndexError as exc: 75 | self.logger.error("no more credentials to try") 76 | return 77 | self.logger.debug( 78 | "trying to log in with %s / %s" % (username, password) 79 | ) 80 | self.wait_write(username, wait=None) 81 | self.wait_write(password, wait="Password:") 82 | 83 | # run main config! 84 | self.apply_config() 85 | 86 | # startup time? 87 | startup_time = datetime.datetime.now() - self.start_time 88 | self.logger.info("Startup complete in: %s" % startup_time) 89 | # mark as running 90 | self.running = True 91 | return 92 | 93 | # no match, if we saw some output from the router it's probably 94 | # booting, so let's give it some more time 95 | if res != b"": 96 | self.write_to_stdout(res) 97 | # reset spins if we saw some output 98 | self.spins = 0 99 | 100 | self.spins += 1 101 | 102 | return 103 | 104 | def apply_config(self): 105 | scrapli_timeout = os.getenv("SCRAPLI_TIMEOUT", vrnetlab.DEFAULT_SCRAPLI_TIMEOUT) 106 | self.logger.info( 107 | f"Scrapli timeout is {scrapli_timeout}s (default {vrnetlab.DEFAULT_SCRAPLI_TIMEOUT}s)" 108 | ) 109 | 110 | # init scrapli 111 | nxos_scrapli_dev = { 112 | "host": "127.0.0.1", 113 | "auth_bypass": True, 114 | "auth_strict_key": False, 115 | "timeout_socket": scrapli_timeout, 116 | "timeout_transport": scrapli_timeout, 117 | "timeout_ops": scrapli_timeout, 118 | } 119 | 120 | nxos_config = f"""hostname {self.hostname} 121 | username {self.username} password 0 {self.password} role network-admin 122 | ! 123 | vrf context management 124 | ip route 0.0.0.0/0 {self.mgmt_gw_ipv4} 125 | ipv6 route ::/0 {self.mgmt_gw_ipv6} 126 | exit 127 | ! 128 | interface mgmt0 129 | ip address {self.mgmt_address_ipv4} 130 | ipv6 address {self.mgmt_address_ipv6} 131 | exit 132 | ! 133 | no feature ssh 134 | ssh key rsa 2048 force 135 | feature ssh 136 | ! 137 | """ 138 | 139 | con = NXOSDriver(**nxos_scrapli_dev) 140 | con.commandeer(conn=self.scrapli_tn) 141 | 142 | if os.path.exists(STARTUP_CONFIG_FILE): 143 | self.logger.info("Startup configuration file found") 144 | with open(STARTUP_CONFIG_FILE, "r") as config: 145 | nxos_config += config.read() 146 | else: 147 | self.logger.warning("User provided startup configuration is not found.") 148 | 149 | res = con.send_configs(nxos_config.splitlines()) 150 | con.send_config("copy running-config startup-config") 151 | 152 | for response in res: 153 | self.logger.info(f"CONFIG:{response.channel_input}") 154 | self.logger.info(f"RESULT:{response.result}") 155 | 156 | con.close() 157 | 158 | 159 | class NXOS(vrnetlab.VR): 160 | def __init__(self, hostname, username, password, conn_mode): 161 | super(NXOS, self).__init__(username, password) 162 | self.vms = [NXOS_vm(hostname, username, password, conn_mode)] 163 | 164 | 165 | if __name__ == "__main__": 166 | import argparse 167 | 168 | parser = argparse.ArgumentParser(description="") 169 | parser.add_argument("--hostname", default="vr-nxos", help="Router hostname") 170 | parser.add_argument( 171 | "--trace", action="store_true", help="enable trace level logging" 172 | ) 173 | parser.add_argument("--username", default="admin", help="Username") 174 | parser.add_argument("--password", default="admin", help="Password") 175 | parser.add_argument( 176 | "--connection-mode", 177 | default="tc", 178 | help="Connection mode to use in the datapath", 179 | ) 180 | args = parser.parse_args() 181 | 182 | LOG_FORMAT = "%(asctime)s: %(module)-10s %(levelname)-8s %(message)s" 183 | logging.basicConfig(format=LOG_FORMAT) 184 | logger = logging.getLogger() 185 | 186 | logger.setLevel(logging.DEBUG) 187 | if args.trace: 188 | logger.setLevel(1) 189 | 190 | vrnetlab.boot_delay() 191 | vr = NXOS( 192 | args.hostname, args.username, args.password, conn_mode=args.connection_mode 193 | ) 194 | vr.start() 195 | -------------------------------------------------------------------------------- /ocnos/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=IPInfusion 2 | NAME=OcNOS 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # match versions like: 7 | # DEMO_VM-OcNOS-6.0.2.11-MPLS-x86-MR.qcow2 8 | #VERSION=$(shell echo $(IMAGE) | sed -rn 's/DEMO_VM-OcNOS-(.+)-MPLS-.*.qcow/\1/p') 9 | 10 | # match versions like: 11 | # OcNOS-SP-PLUS-x86-6.5.2-101-GA.qcow2 12 | VERSION=$(shell echo $(IMAGE) | sed -rn 's/OcNOS-SP-PLUS-x86-(.+)-GA.qcow2/\1/p') 13 | 14 | -include ../makefile-sanity.include 15 | -include ../makefile.include 16 | -------------------------------------------------------------------------------- /ocnos/README.md: -------------------------------------------------------------------------------- 1 | # vrnetlab / IPInfusion OcNOS 2 | 3 | This is the vrnetlab docker image for IPInfusion OcNOS. 4 | 5 | ## Building the docker image 6 | 7 | Download the OcNOS-VM image from https://www.ipinfusion.com/products/ocnos-vm/ 8 | Copy the qcow2 image into this folder, then run `make docker-image`. 9 | 10 | Tested booting and responding to Telnet: 11 | 12 | - OcNOS-SP-PLUS-x86-6.5.2-101-GA.qcow2 MD5:796c121be77d43ffffbf6214a44f54eb 13 | 14 | Tested booting and responding to SSH: 15 | (The relevant parts of the Makefile for this version are commented out.) 16 | 17 | - DEMO_VM-OcNOS-6.0.2.11-MPLS-x86-MR.qcow2 MD5:08bbaf99347c33f75d15f552bda762e1 18 | 19 | ## Serial console issues 20 | 21 | (This issue did not occur in version 6.5.2-101.) 22 | The image of OcNOS version 6.0.2.11 distributed from the official website has a bug that prevents connection via serial console. 23 | This problem can be corrected by modifying /boot/grub/grub.cfg in the image. 24 | 25 | For example, it can be modified as follows 26 | 27 | ``` 28 | OCNOS_IMAGE="DEMO_VM-OcNOS-6.0.2.11-MPLS-x86-MR.qcow2" 29 | 30 | modprobe nbd 31 | qemu-nbd --connect=/dev/nbd0 $OCNOS_IMAGE 32 | mkdir -p /tmp/OCNOS_ROOT 33 | mount /dev/nbd0p1 /tmp/OCNOS_ROOT 34 | sed -ie 's/\( *linux.*\)$/\1 console=ttyS0,115200n8/' /tmp/OCNOS_ROOT/boot/grub/grub.cfg 35 | umount /tmp/OCNOS_ROOT 36 | qemu-nbd --disconnect /dev/nbd0 37 | ``` 38 | 39 | ## System requirements 40 | 41 | CPU: 2 core 42 | 43 | RAM: <4GB 44 | 45 | Disk: <4GB 46 | 47 | ## Containerlab 48 | 49 | Containerlab kind for OcNOS is [ipinfusion_ocnos](https://containerlab.dev/manual/kinds/ipinfusion-ocnos/). 50 | -------------------------------------------------------------------------------- /ocnos/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/debian:bookworm-slim 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | 5 | RUN apt-get update -qy \ 6 | && apt-get install --no-install-recommends -y \ 7 | iproute2 \ 8 | python3 \ 9 | socat \ 10 | qemu-kvm \ 11 | qemu-utils \ 12 | telnet \ 13 | && rm -rf /var/lib/apt/lists/* 14 | 15 | ARG IMAGE 16 | COPY $IMAGE* / 17 | COPY *.py / 18 | 19 | EXPOSE 22 23 161/udp 443 830 5000 10000-10099 20 | HEALTHCHECK CMD ["/healthcheck.py"] 21 | ENTRYPOINT ["/launch.py"] 22 | -------------------------------------------------------------------------------- /openbsd/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=OpenBSD 2 | NAME=OpenBSD 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # match versions like: 7 | # openbsd-min_v7.7_2025-04-28-11-38.qcow2 8 | VERSION=$(shell echo $(IMAGE) | sed -e 's/openbsd-min_v\([0-9]\.[0-9]\)_.*/\1/') 9 | 10 | -include ../makefile-sanity.include 11 | -include ../makefile.include 12 | 13 | download: 14 | /bin/bash download.sh 15 | 16 | build: download 17 | $(MAKE) docker-image -------------------------------------------------------------------------------- /openbsd/README.md: -------------------------------------------------------------------------------- 1 | # vrnetlab / OpenBSD 2 | 3 | This is the vrnetlab docker image for OpenBSD. 4 | 5 | This docker image requires a custom-built OpenBSD image with pre-installed [cloud-init](https://cloudinit.readthedocs.io/en/latest/). You can download such images from https://github.com/hcartiaux/openbsd-cloud-image. 6 | 7 | ## Building the docker image 8 | 9 | Run `make download`. It will try to download the latest OpenBSD release from https://github.com/hcartiaux/openbsd-cloud-image to this directory. Then run `make` to build a docker image. 10 | 11 | If for some reasons you can't obtain an image from https://github.com/hcartiaux/openbsd-cloud-image, you can build it yourself with the script from [this repository](https://github.com/goneri/pcib). 12 | 13 | It's been tested to boot, respond to SSH and have correct interface mapping 14 | with the following images: 15 | 16 | * openbsd-7.3-2023-04-22.qcow2 17 | 18 | ## Usage 19 | 20 | ``` 21 | docker run -d --privileged --name vrnetlab/openbsd-openbsd: --username --password 22 | ``` 23 | 24 | Where: 25 | 26 | * `container_name` - name of the created container. 27 | * `tag`- OpenBSD release version (e.g., 7.3). 28 | * `username`, `password` - OpenBSD VM credentials. 29 | 30 | Example: 31 | 32 | ``` 33 | docker run -d --privileged --name my-obsd-router vrnetlab/openbsd-openbsd:7.3 --username admin --password admin 34 | ``` 35 | 36 | It will take about 1 minute for the container to boot. After that, you can try to ssh to the container's IP or telnet to port 5000 for console access. 37 | 38 | To obtain the container's IP run: 39 | 40 | ``` 41 | docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' 42 | ``` 43 | 44 | ## Interface mapping 45 | 46 | Interface `vio0` is always configured as a management interface. Interfaces `vio1` to `vio17` can be used for data plane. 47 | 48 | ## System requirements 49 | 50 | CPU: 1 core 51 | RAM: 512MB 52 | DISK: 4.0GB 53 | -------------------------------------------------------------------------------- /openbsd/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/debian:bookworm-slim AS builder 2 | 3 | ARG DISK_SIZE=4G 4 | ARG IMAGE 5 | 6 | RUN apt-get update -qy && \ 7 | apt-get install -y --no-install-recommends qemu-utils && \ 8 | rm -rf /var/lib/apt/lists/* 9 | 10 | COPY $IMAGE* / 11 | RUN qemu-img resize /${IMAGE} ${DISK_SIZE} 12 | 13 | FROM public.ecr.aws/docker/library/debian:bookworm-slim 14 | 15 | RUN apt-get update -qy \ 16 | && apt-get install -y --no-install-recommends\ 17 | bridge-utils \ 18 | iproute2 \ 19 | python3-ipy \ 20 | socat \ 21 | qemu-kvm \ 22 | tcpdump \ 23 | ssh \ 24 | inetutils-ping \ 25 | dnsutils \ 26 | iptables \ 27 | nftables \ 28 | telnet \ 29 | cloud-utils \ 30 | sshpass \ 31 | && rm -rf /var/lib/apt/lists/* 32 | 33 | ARG IMAGE 34 | COPY --from=builder $IMAGE* / 35 | COPY *.py / 36 | COPY --chmod=0755 backup.sh / 37 | 38 | EXPOSE 22 5000 10000-10099 39 | HEALTHCHECK CMD ["/healthcheck.py"] 40 | ENTRYPOINT ["/launch.py"] 41 | -------------------------------------------------------------------------------- /openbsd/docker/backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DEFAULT_USER="admin" 4 | DEFAULT_PASSWORD="admin" 5 | BACKUP_FILE="backup.tar.gz" 6 | BACKUP_PATH=/config/$BACKUP_FILE 7 | REMOTE_BACKUP_PATH=/tmp/$BACKUP_FILE 8 | 9 | handle_args() { 10 | # Parse options 11 | while getopts 'u:p:' OPTION; do 12 | case "$OPTION" in 13 | u) 14 | user="$OPTARG" 15 | ;; 16 | p) 17 | password="$OPTARG" 18 | ;; 19 | ?) 20 | usage 21 | exit 1 22 | ;; 23 | esac 24 | done 25 | shift "$(($OPTIND -1))" 26 | 27 | # Assign defaults if options weren't provided 28 | if [ -z "$user" ] ; then 29 | user=$DEFAULT_USER 30 | fi 31 | if [ -z "$password" ] ; then 32 | password=$DEFAULT_PASSWORD 33 | fi 34 | 35 | SSH_CMD="sshpass -p $password ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" 36 | SCP_CMD="sshpass -p $password scp -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" 37 | HOST="$user@localhost" 38 | 39 | # Parse commands 40 | case $1 in 41 | 42 | backup) 43 | backup 44 | ;; 45 | 46 | restore) 47 | restore 48 | ;; 49 | 50 | *) 51 | usage 52 | ;; 53 | esac 54 | } 55 | 56 | usage() { 57 | echo "Usage: $(basename $0) [-u USERNAME] [-p PASSWORD] COMMAND" 58 | echo "Options:" 59 | echo " -u USERNAME VM SSH username (default: admin)" 60 | echo " -p PASSWORD VM SSH password (default: admin)" 61 | echo 62 | echo "Commands:" 63 | echo " backup Backup VM /etc directory to $BACKUP_PATH" 64 | echo " restore Restore VM /etc directory from $BACKUP_PATH" 65 | exit 0; 66 | } 67 | 68 | backup() { 69 | echo "Backing up..." 70 | $SSH_CMD $HOST "sudo tar zcf $REMOTE_BACKUP_PATH /etc 2>/dev/null" 71 | $SCP_CMD $HOST:$REMOTE_BACKUP_PATH $BACKUP_PATH 72 | } 73 | 74 | restore() { 75 | if [ -f "$BACKUP_PATH" ]; then 76 | echo "Restoring from backup..." 77 | # Put backup file to VM, untar, and reboot. 78 | $SCP_CMD $BACKUP_PATH $HOST:$REMOTE_BACKUP_PATH && $SSH_CMD $HOST "sudo tar xzf $REMOTE_BACKUP_PATH -C /" && $SSH_CMD $HOST "sudo shutdown -r now || true" 79 | else 80 | echo "$BACKUP_PATH not found. Nothing to restore." 81 | fi 82 | } 83 | 84 | handle_args "$@" 85 | -------------------------------------------------------------------------------- /openbsd/download.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Repo name 4 | repo="hcartiaux/openbsd-cloud-image" 5 | 6 | # Asset name 7 | asset="openbsd-min.qcow2" 8 | 9 | # Link to the API information of the latest release 10 | api_url="https://api.github.com/repos/${repo}/releases/latest" 11 | 12 | # Query the API and get the latest tag 13 | tag=$(curl -s "$api_url" | jq -r ".tag_name") 14 | 15 | # Link to the latest release 16 | download_url="https://github.com/${repo}/releases/latest/download/${asset}" 17 | 18 | # Build the filename from the asset and the tag 19 | filename="${asset%.*}_${tag}.${asset##*.}" 20 | 21 | # Check if the file already exists in the current directory 22 | if [ -e "$filename" ]; then 23 | echo "File $filename already exists. Skipping download." 24 | else 25 | # Download the file 26 | curl -L -s "$download_url" -o "$filename" 27 | echo "Download complete: $filename" 28 | fi -------------------------------------------------------------------------------- /openwrt/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=OpenWRT 2 | NAME=OpenWRT 3 | IMAGE_FORMAT=img 4 | IMAGE_GLOB=*.img 5 | 6 | # match versions like: 7 | # openwrt-12.09-x86-kvm_guest-combined-ext4.img 8 | # openwrt-14.07-x86-kvm_guest-combined-ext4.img 9 | # openwrt-15.05.1-x86-kvm_guest-combined-ext4.img 10 | # openwrt-15.05-x86-kvm_guest-combined-ext4.img 11 | VERSION=$(shell echo $(IMAGE) | sed -E 's/openwrt-([0-9]{1,2}.[0-9]{1,2}|[0-9]{1,2}.[0-9]{1,2}.[0-9]{1,2})-.*/\1/') 12 | 13 | -include ../makefile-sanity.include 14 | -include ../makefile.include 15 | 16 | download: 17 | python3 download.py 18 | 19 | build: download 20 | $(MAKE) docker-image 21 | -------------------------------------------------------------------------------- /openwrt/README.md: -------------------------------------------------------------------------------- 1 | # OpenWRT 2 | 3 | This is the vrnetlab Docker image for OpenWRT, designed to be used with containerlab. 4 | 5 | ## Building the docker image 6 | 7 | Run `make build` to automatically download images from the public OpenWRT image 8 | repository and build them into vrnetlab docker images. `build` consists of the 9 | `download` step and `docker-image` step, which can be run separately. 10 | 11 | Use `make download` to automatically download images from the public OpenWRT 12 | image repository at . The download script will get 13 | everything major and minor version, e.g. 12.09, 14.07, 15.05, 23.05.3 etc. 14 | 15 | You can also download images manually by navigating to 16 | and grabbing the file. You have to gunzip it. 17 | 18 | Whichever way you get the images, once you have them, run `make docker-image` 19 | to build the docker images. The resulting image is called `vrnetlab/openwrt_openwrt:version`. You can 20 | tag it with something else if you want, like `my-repo.example.com/vr-openwrt` 21 | and then push it to your repo. The tag is the same as the version of the 22 | OpenWRT image, so if you have openwrt-15.05-x86-kvm_guest-combined-ext4.img 23 | your final docker image will be called vr-openwrt:15.05. 24 | 25 | As per OpenWRT defaults, `br-lan` (`eth0`) is the LAN interface and eth1 the WAN interface. 26 | During bootstrap, however, the `LAN interface` is reassigned from `eth0` to `eth2`, while `eth0` is reserved and configured as the `management interface`. 27 | 28 | Tested booting and responding to SSH: 29 | 30 | * openwrt-24.10.0-x86-64-generic-ext4-combined.img MD5:68d7204d3707b629698a011bbfd1e9f1 31 | * openwrt-23.05.5-x86-64-generic-ext4-combined.img MD5:34f6ca5acd50156ce936858a8ff014cf 32 | * openwrt-23.05.3-x86-64-generic-ext4-combined.img MD5:818f6ba04103915ad53f2d003c42aa84 33 | * openwrt-15.05.1-x86-64-combined-ext4.img MD5:307d8cdb11faeb1b5e27fe55078bd152 34 | 35 | ## Usage 36 | 37 | ``` 38 | docker run -d --privileged --name openwrt1 vrnetlab/openwrt_openwrt:24.10.0 39 | ``` 40 | 41 | ### Usage with containerlab 42 | 43 | ```yaml 44 | name: openwrt 45 | 46 | topology: 47 | nodes: 48 | openwrt: 49 | kind: openwrt 50 | image: vrnetlab/openwrt_openwrt:24.10.0 51 | mgmt-ipv4: 172.20.20.12 # optional 52 | mgmt_ipv6: 2001:172:20:20::12 # optional 53 | ports: 54 | - 8080:80 # required for LuCI web interface (HTTP); adjust host ports if running multiple nodes or based on your setup 55 | - 8443:443 # required for LuCI web interface (HTTPS); adjust host ports if running multiple nodes or based on your setup 56 | env: 57 | USERNAME: root # default: root 58 | PASSWORD: mypassword # default: VR-netlab9 59 | CLAB_MGMT_PASSTHROUGH: "false" # default: "false" 60 | PACKET_REPOSITORY_DNS_SERVER: 8.8.8.8 # default 8.8.8.8 61 | PACKET_REPOSITORY_DOMAINS: "example.com" # additional repository domains (space-separated); creates a host route via the MGMT interface 62 | PACKAGES: "tinc htop tcpdump btop luci-proto-gre" # installed on boot if not already present 63 | ``` 64 | 65 | ## System requirements 66 | 67 | CPU: 1 core 68 | 69 | RAM: 128 MB 70 | 71 | Disk: 256 MB 72 | 73 | ## FAQ - Frequently or Unfrequently Asked Questions 74 | 75 | ### Q: Has this been extensively tested? 76 | 77 | A: Not really – but it's great for testing interoperability between OpenWRT and FreeRTR. 78 | Feel free to take it for a spin and share your feedback! :-) 79 | 80 | ### Q: What is the MGMT interface? 81 | 82 | A: `eth0` 83 | 84 | ### Q: What is the WAN interface? 85 | 86 | A: `eth1` 87 | 88 | ### Q: What is the LAN interface? 89 | 90 | A: `eth2` 91 | 92 | ### Q: Can the MGMT interface be remapped? 93 | 94 | A: No, this is not supported. 95 | 96 | ### Q: Does my configuration survive a redeploy? 97 | 98 | A: Only if you're using the `openwrt` kind or if you're using a bind mount. 99 | 100 | ### Q: How do I get persistence without using the `openwrt` kind? 101 | 102 | A: Use the `linux` kind and bind-mount a volume to `/overlay/`. Make sure the container has write permissions. 103 | 104 | ### Q: How can I get a shell into OpenWRT? 105 | 106 | A: You can use one of the following methods: 107 | 108 | * `ssh root@clab--` 109 | * `telnet clab-- 5000` 110 | * `docker exec -it clab-- telnet localhost 5000` 111 | 112 | ### Q: How can I access the LuCI web interface? 113 | 114 | A: Open [http://127.0.0.1:8080](http://127.0.0.1:8080) or `http://` in your web browser. 115 | 116 | ### Q: Can automatic package upgrades be disabled? 117 | 118 | A: No, but you are welcome to create a pull request (PR) to add that functionality. 119 | 120 | ### Q: Why are new installed luci-proto not showing up? 121 | 122 | A: you need to reload the network process 123 | 124 | ``` 125 | /etc/init.d/network restart 126 | ``` 127 | 128 | ### Q: Which connection modes are supported? 129 | 130 | A: Currently, only `tc` is supported. But you're welcome to create a pull request (PR) to add support for more connection modes. 131 | 132 | ### Q: Which is better – transparent or non-transparent MGMT interface? 133 | 134 | A: It depends on your use case. The non-transparent mode uses the `10.0.0.0/24` subnet to communicate between QEMU and the VM. 135 | 136 | ## Q: How many interfaces are supported? 137 | 138 | A: Up to 16 by default, but this can be increased if needed using the `NICS` environment variable, e.g. `NICS=32`. 139 | 140 | ### Q: How to delete all openwrt docker images? 141 | 142 | ``` 143 | docker rmi $(docker images --quiet --filter reference=vrnetlab/openwrt_openwrt) 144 | ``` -------------------------------------------------------------------------------- /openwrt/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/srl-labs/vrnetlab-base:0.2.1 2 | LABEL maintainer="Andreas Cymbal takalele@konnex.me" 3 | 4 | ARG DEBIAN_FRONTEND=noninteractive 5 | 6 | RUN uv add click 7 | 8 | ARG IMAGE 9 | COPY $IMAGE* / 10 | COPY *.py / 11 | -------------------------------------------------------------------------------- /openwrt/download.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import re 4 | import shutil 5 | import sys 6 | import os 7 | import gzip 8 | 9 | import requests 10 | from bs4 import BeautifulSoup, Tag 11 | 12 | base_url = "https://downloads.openwrt.org/" 13 | 14 | def get_rel(url, version): 15 | res = requests.get(url) 16 | if not res.status_code == 200: 17 | return 18 | c = res.content 19 | soup = BeautifulSoup(c, "lxml") 20 | links = soup.find_all("a") 21 | for l in links: 22 | #print(l) 23 | 24 | #filename = l.string.strip() 25 | filename = l['href'] 26 | if not (re.search('combined-ext4.img.gz', filename) or re.search('generic-ext4-combined.img.gz', filename)): 27 | #print("ignoring {}".format(filename)) 28 | continue 29 | if re.search('^openwrt-x86-', filename): 30 | local_filename = re.sub('^openwrt-x86-', 'openwrt-{}-x86-'.format(version), filename) 31 | file_url = "{}{}".format(url, filename) 32 | if not os.path.exists(filename): 33 | print("Downloading {} -> {}".format(file_url, filename)) 34 | r = requests.get(file_url, stream=True) 35 | print(filename) 36 | base_name, file_extension = os.path.splitext(filename) 37 | if file_extension == ".gz": 38 | output_file = base_name 39 | print(output_file) 40 | with open(filename, 'wb') as f: 41 | shutil.copyfileobj(r.raw, f) 42 | try: 43 | with gzip.open(filename, 'rb') as f_in: 44 | with open(output_file, 'wb') as f_out: 45 | shutil.copyfileobj(f_in, f_out) 46 | print(f"The file was successfully unpacked: {output_file}") 47 | except gzip.BadGzipFile: 48 | if not os.path.exists(output_file): 49 | print(f"Warning: The file '{filename}' is not a valid GZIP file and could not be unpacked.") 50 | else: 51 | print(f"gzip: {filename}: decompression OK, trailing garbage ignored. ") 52 | except Exception as e: 53 | print(f"Error unpacking the file '{filename}': {e}") 54 | else: 55 | print("File '{}' already exists. Skipping download.".format(filename)) 56 | 57 | 58 | def main(): 59 | res = requests.get("https://downloads.openwrt.org/") 60 | if not res.status_code == 200: 61 | return 62 | c = res.content 63 | soup = BeautifulSoup(c, "lxml") 64 | links = soup.find_all("a") 65 | for l in links: 66 | m = re.search('\/\/', l.attrs['href']) 67 | if not m: 68 | rel_url = "{}{}x86/64/".format(base_url, l.attrs['href']) 69 | else: 70 | current_href = l['href'] 71 | new_href = 'https:' + current_href 72 | l['href'] = new_href 73 | rel_url = "{}x86/64/".format(l.attrs['href']) 74 | m = re.search('[^0-9]([0-9]{2}\.[0-9]{2}[^0-9](?:[0-9]{1,2}))|[^0-9]([0-9]{2}\.[0-9]{2})', l.attrs['href']) 75 | if not m: 76 | continue 77 | #print(l.string.strip(), l.attrs['href'], rel_url) 78 | get_rel(rel_url, m.group(1)) 79 | 80 | 81 | 82 | main() 83 | -------------------------------------------------------------------------------- /pan/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=PaloAlto 2 | NAME=PA-VM 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # match versions like: 7 | # PA-VNM-KVM-7.0.1.qcow2 8 | # PA-VM-KVM-10.0.6.qcow2 9 | VERSION=$(shell echo $(IMAGE) | sed -re 's/PA-(VM|VNM)-KVM-(.*)\.qcow2/\2/') 10 | 11 | -include ../makefile-sanity.include 12 | -include ../makefile.include 13 | -------------------------------------------------------------------------------- /pan/README.md: -------------------------------------------------------------------------------- 1 | # pan / PA-VM 2 | 3 | This is the vrnetlab docker image for Palo Alto PA-VM firewalls. 4 | 5 | 6 | ## Building the docker image 7 | 8 | Download PA-VM in KVM/qcow2 format from the Palo Alto website (you will need a login/access). 9 | Place the .qcow2 file in this directory and run make. The resulting images is called `vrnetlab/vr-pan:VERSION`. You can 10 | tag it with something else if you want, like `my-repo.example.com/vr-pan` and 11 | then push it to your repo. 12 | 13 | It's been tested to boot, respond to SSH and have correct interface mapping 14 | with the following images: 15 | 16 | * PA-VNM-KVM-7.0.1.qcow2 17 | * PA-VNM-KVM-9.1.9.qcow2 18 | * PA-VNM-KVM-10.0.6.qcow2 19 | 20 | 21 | ## System requirements 22 | 23 | * CPU: 2 core 24 | * RAM: 6GB 25 | * Disk: <1GB 26 | 27 | -------------------------------------------------------------------------------- /pan/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # base image dockerfile is defined in https://github.com/hellt/vrnetlab 2 | FROM ghcr.io/srl-labs/vrnetlab-base:0.2.0 3 | 4 | ARG IMAGE 5 | COPY $IMAGE* / 6 | COPY *.py / 7 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | dependencies = [ 3 | "passlib>=1.7.4", 4 | "pyyaml>=6.0.2", 5 | "scrapli>=2024.7.30.post1", 6 | "scrapli-community", 7 | ] 8 | description = "Building containers for VM-based Network OSes for Containerlab" 9 | name = "vrnetlab" 10 | readme = "README.md" 11 | requires-python = ">=3.11,<3.13" 12 | version = "0.1.0" 13 | 14 | [tool.uv.sources] 15 | scrapli-community = { git = "https://github.com/scrapli/scrapli_community", rev = "d862833" } 16 | -------------------------------------------------------------------------------- /routeros/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Mikrotik 2 | NAME=RouterOS 3 | IMAGE_FORMAT=vmdk 4 | IMAGE_GLOB=*.vmdk 5 | 6 | # match versions like: 7 | # chr-6.39.2.vmdk 8 | VERSION=$(shell echo $(IMAGE) | sed -rn 's/.*chr-(.+)\.vmdk/\1/p') 9 | 10 | -include ../makefile-sanity.include 11 | -include ../makefile.include 12 | -------------------------------------------------------------------------------- /routeros/README.md: -------------------------------------------------------------------------------- 1 | # vrnetlab / Mikrotik RouterOS (ROS) 2 | 3 | This is the vrnetlab docker image for Mikrotik RouterOS (ROS). 4 | 5 | ## Building the docker image 6 | Download the Cloud Hosted Router VMDK image from https://www.mikrotik.com/download 7 | Copy the vmdk image into this folder, then run `make docker-image`. 8 | 9 | Tested booting and responding to SSH: 10 | * chr-6.39.2.vmdk MD5:eb99636e3cdbd1ea79551170c68a9a27 11 | * chr-6.47.9.vmdk 12 | * chr-7.1beta5.vmdk 13 | * chr-7.16.2.vmdk 14 | 15 | 16 | ## System requirements 17 | CPU: 1 core 18 | 19 | RAM: <1GB 20 | 21 | Disk: <1GB 22 | 23 | ## Containerlab 24 | Containerlab kind for routeros is [vr-ros](https://containerlab.srlinux.dev/manual/kinds/vr-ros/). 25 | -------------------------------------------------------------------------------- /routeros/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/debian:bookworm-slim 2 | LABEL org.opencontainers.image.authors="roman@dodin.dev" 3 | 4 | ARG DEBIAN_FRONTEND=noninteractive 5 | 6 | RUN apt-get update -qy \ 7 | && apt-get upgrade -qy \ 8 | && apt-get install -y \ 9 | bridge-utils \ 10 | iproute2 \ 11 | python3-ipy \ 12 | socat \ 13 | qemu-kvm \ 14 | tcpdump \ 15 | ssh \ 16 | inetutils-ping \ 17 | dnsutils \ 18 | iptables \ 19 | nftables \ 20 | telnet \ 21 | ftp \ 22 | && rm -rf /var/lib/apt/lists/* 23 | 24 | ARG IMAGE 25 | COPY $IMAGE* / 26 | COPY *.py / 27 | 28 | EXPOSE 22 161/udp 830 5000 5678 8291 10000-10099 29 | HEALTHCHECK CMD ["/healthcheck.py"] 30 | ENTRYPOINT ["/launch.py"] 31 | -------------------------------------------------------------------------------- /sonic/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Sonic 2 | NAME=sonic-vs 3 | IMAGE_FORMAT=qcow 4 | IMAGE_GLOB=*.qcow2 5 | IMAGE=sonic-vs-202305.qcow2 6 | 7 | # match versions like: 8 | # 202305 9 | VERSION=$(shell echo $(IMAGE) | sed -e 's/sonic-vs-//' | sed -e 's/.qcow2//') 10 | 11 | -include ../makefile-sanity.include 12 | -include ../makefile.include 13 | -------------------------------------------------------------------------------- /sonic/README.md: -------------------------------------------------------------------------------- 1 | # SONiC VM 2 | 3 | This is the vrnetlab docker image for SONiC's VM. 4 | The scripts in this directory are based on FreeBSD and VSRX kinds. 5 | 6 | > Available with [containerlab](https://containerlab.dev) as [`sonic-vm`](https://containerlab.dev/manual/kinds/sonic-vm/) kind. 7 | 8 | ## Building the docker image 9 | 10 | Download the latest `sonic-vs.img.gz` image using the options documented on the [containerlab.dev website](https://containerlab.dev/manual/kinds/sonic-vm/). 11 | 12 | Uncompress and place the `.img` file in this directory. Rename the file to `sonic-vs-[version].qcow2` and run `make`. 13 | 14 | After typing `make`, a new image will appear named `vrnetlab/vr-sonic:`. 15 | 16 | Run `docker images` to confirm this. 17 | 18 | ## System requirements 19 | 20 | - CPU: 2 cores 21 | - RAM: 4GB 22 | - DISK: ~3.2GB 23 | 24 | ## Configuration 25 | 26 | SONiC nodes boot with a basic configuration by default, enabling SSH and basic management connectivity. All factory default configuration is retained. 27 | 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. 28 | 29 | ## Contact 30 | 31 | The author of this code is Adam Kulagowski (), CodiLime (codilime.com), feel free to reach him in case of problems. 32 | -------------------------------------------------------------------------------- /sonic/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/debian:bookworm-slim 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | 5 | RUN apt-get update -qy \ 6 | && apt-get install -y --no-install-recommends \ 7 | bridge-utils \ 8 | iproute2 \ 9 | python3-ipy \ 10 | qemu-kvm \ 11 | qemu-utils \ 12 | socat \ 13 | ssh \ 14 | sshpass \ 15 | && rm -rf /var/lib/apt/lists/* 16 | 17 | ARG IMAGE 18 | COPY $IMAGE* / 19 | COPY *.py / 20 | COPY backup.sh / 21 | 22 | EXPOSE 22 443 5000 8080 23 | HEALTHCHECK CMD ["/healthcheck.py"] 24 | ENTRYPOINT ["/launch.py"] -------------------------------------------------------------------------------- /sonic/docker/backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DEFAULT_USER="admin" 4 | DEFAULT_PASSWORD="admin" 5 | REMOTE_FILE="/etc/sonic/config_db.json" 6 | TMP_FILE="/tmp/${REMOTE_FILE##*/}" 7 | BACKUP_FILE="/config/${REMOTE_FILE##*/}" 8 | 9 | handle_args() { 10 | # Parse options 11 | while getopts 'u:p:' OPTION; do 12 | case "$OPTION" in 13 | u) 14 | user="$OPTARG" 15 | ;; 16 | p) 17 | password="$OPTARG" 18 | ;; 19 | ?) 20 | usage 21 | exit 1 22 | ;; 23 | esac 24 | done 25 | shift "$(($OPTIND -1))" 26 | 27 | # Assign defaults if options weren't provided 28 | if [ -z "$user" ] ; then 29 | user=$DEFAULT_USER 30 | fi 31 | if [ -z "$password" ] ; then 32 | password=$DEFAULT_PASSWORD 33 | fi 34 | 35 | SSH_CMD="sshpass -p $password ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" 36 | SCP_CMD="sshpass -p $password scp -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" 37 | HOST="$user@localhost" 38 | 39 | # Parse commands 40 | case $1 in 41 | 42 | backup) 43 | backup 44 | ;; 45 | 46 | restore) 47 | restore 48 | ;; 49 | 50 | *) 51 | usage 52 | ;; 53 | esac 54 | } 55 | 56 | usage() { 57 | echo "Usage: $(basename $0) [-u USERNAME] [-p PASSWORD] COMMAND" 58 | echo "Options:" 59 | echo " -u USERNAME VM SSH username (default: $DEFAULT_USER)" 60 | echo " -p PASSWORD VM SSH password (default: $DEFAULT_PASSWORD)" 61 | echo 62 | echo "Commands:" 63 | echo " backup Backup VM $REMOTE_FILE directory to $BACKUP_FILE" 64 | echo " restore Restore VM $REMOTE_FILE directory from $BACKUP_FILE" 65 | exit 0; 66 | } 67 | 68 | backup() { 69 | echo "Retrieveing the config from the VM..." 70 | # copy the original config to the tmp location and set permissions to 777 71 | # and then copy out the file from the temp location 72 | $SSH_CMD $HOST "sudo cp $REMOTE_FILE $TMP_FILE && sudo chmod 777 $TMP_FILE" && \ 73 | $SCP_CMD $HOST:$TMP_FILE $BACKUP_FILE 74 | } 75 | 76 | wait_for_ssh() { 77 | local max_retries=30 78 | local retry_interval=2 79 | 80 | for ((i=1; i<=$max_retries; i++)); do 81 | echo "Waiting for VM's SSH to become available... (Attempt $i/$max_retries)" 82 | if $SSH_CMD -o ConnectTimeout=5 $HOST exit 2>/dev/null; then 83 | echo "SSH connection established." 84 | return 0 85 | fi 86 | sleep $retry_interval 87 | done 88 | 89 | echo "SSH connection could not be established after $max_retries attempts." 90 | return 1 91 | } 92 | 93 | restore() { 94 | if [ -f "$BACKUP_FILE" ]; then 95 | echo "Copying startup config file to the VM..." 96 | 97 | if wait_for_ssh; then 98 | $SCP_CMD $BACKUP_FILE $HOST:$TMP_FILE && $SSH_CMD $HOST "sudo config load -y $TMP_FILE && sudo config save -y" 99 | else 100 | echo "Failed to establish SSH connection. Config copy operation aborted." 101 | fi 102 | else 103 | echo "$BACKUP_FILE not found. Nothing to push to the VM." 104 | fi 105 | } 106 | 107 | handle_args "$@" 108 | -------------------------------------------------------------------------------- /sonic/docker/launch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import datetime 4 | import logging 5 | import os 6 | import re 7 | import signal 8 | import subprocess 9 | import sys 10 | 11 | import vrnetlab 12 | 13 | CONFIG_FILE = "/config/config_db.json" 14 | DEFAULT_USER = "admin" 15 | DEFAULT_PASSWORD = "YourPaSsWoRd" 16 | 17 | 18 | def handle_SIGCHLD(_signal, _frame): 19 | os.waitpid(-1, os.WNOHANG) 20 | 21 | 22 | def handle_SIGTERM(_signal, _frame): 23 | sys.exit(0) 24 | 25 | 26 | signal.signal(signal.SIGINT, handle_SIGTERM) 27 | signal.signal(signal.SIGTERM, handle_SIGTERM) 28 | signal.signal(signal.SIGCHLD, handle_SIGCHLD) 29 | 30 | TRACE_LEVEL_NUM = 9 31 | logging.addLevelName(TRACE_LEVEL_NUM, "TRACE") 32 | 33 | 34 | def trace(self, message, *args, **kws): 35 | # Yes, logger takes its '*args' as 'args'. 36 | if self.isEnabledFor(TRACE_LEVEL_NUM): 37 | self._log(TRACE_LEVEL_NUM, message, args, **kws) 38 | 39 | 40 | logging.Logger.trace = trace 41 | 42 | 43 | class SONiC_vm(vrnetlab.VM): 44 | def __init__(self, hostname, username, password, conn_mode): 45 | disk_image = "/" 46 | for e in os.listdir("/"): 47 | if re.search(".qcow2$", e): 48 | disk_image = "/" + e 49 | break 50 | super(SONiC_vm, self).__init__( 51 | username, password, disk_image=disk_image, ram=4096 52 | ) 53 | self.qemu_args.extend(["-smp", "2"]) 54 | self.nic_type = "virtio-net-pci" 55 | self.conn_mode = conn_mode 56 | self.num_nics = 96 57 | self.hostname = hostname 58 | 59 | def bootstrap_spin(self): 60 | """This function should be called periodically to do work.""" 61 | 62 | if self.spins > 300: 63 | # too many spins with no result -> give up 64 | self.stop() 65 | self.start() 66 | return 67 | 68 | ridx, match, res = self.tn.expect([b"login:"], 1) 69 | if match and ridx == 0: # login 70 | self.logger.info("VM started") 71 | 72 | # Login 73 | self.wait_write("\r", None) 74 | self.wait_write(DEFAULT_USER, wait="login:") 75 | self.wait_write(DEFAULT_PASSWORD, wait="Password:") 76 | self.wait_write("", wait="%s@" % (self.username)) 77 | self.logger.info("Login completed") 78 | 79 | # run main config! 80 | self.bootstrap_config() 81 | self.startup_config() 82 | # close telnet connection 83 | self.tn.close() 84 | # startup time? 85 | startup_time = datetime.datetime.now() - self.start_time 86 | self.logger.info(f"Startup complete in: {startup_time}") 87 | # mark as running 88 | self.running = True 89 | return 90 | 91 | # no match, if we saw some output from the router it's probably 92 | # booting, so let's give it some more time 93 | if res != b"": 94 | self.logger.trace("OUTPUT: %s" % res.decode()) 95 | # reset spins if we saw some output 96 | self.spins = 0 97 | 98 | self.spins += 1 99 | 100 | return 101 | 102 | def bootstrap_config(self): 103 | """Do the actual bootstrap config""" 104 | self.logger.info("applying bootstrap configuration") 105 | self.wait_write("sudo -i", "$") 106 | self.wait_write("/usr/sbin/ip address add 10.0.0.15/24 dev eth0", "#") 107 | self.wait_write("passwd -q %s" % (self.username)) 108 | self.wait_write(self.password, "New password:") 109 | self.wait_write(self.password, "password:") 110 | self.wait_write("sleep 1", "#") 111 | self.wait_write("hostnamectl set-hostname %s" % (self.hostname)) 112 | self.wait_write("sleep 1", "#") 113 | self.wait_write("printf '127.0.0.1\\t%s\\n' >> /etc/hosts" % (self.hostname)) 114 | self.wait_write("sleep 1", "#") 115 | self.logger.info("completed bootstrap configuration") 116 | 117 | def startup_config(self): 118 | """Load additional config provided by user.""" 119 | 120 | if not os.path.exists(CONFIG_FILE): 121 | self.logger.trace(f"Backup file {CONFIG_FILE} not found") 122 | return 123 | 124 | self.logger.trace(f"Backup file {CONFIG_FILE} exists") 125 | 126 | subprocess.run( 127 | f"/backup.sh -u {self.username} -p {self.password} restore", 128 | check=True, 129 | shell=True, 130 | ) 131 | 132 | 133 | class SONiC(vrnetlab.VR): 134 | def __init__(self, hostname, username, password, conn_mode): 135 | super().__init__(username, password) 136 | self.vms = [SONiC_vm(hostname, username, password, conn_mode)] 137 | 138 | 139 | if __name__ == "__main__": 140 | import argparse 141 | 142 | parser = argparse.ArgumentParser(description="") 143 | parser.add_argument( 144 | "--trace", action="store_true", help="enable trace level logging" 145 | ) 146 | parser.add_argument("--hostname", default="sonic", help="SONiC hostname") 147 | parser.add_argument("--username", default="admin", help="Username") 148 | parser.add_argument("--password", default="admin", help="Password") 149 | parser.add_argument( 150 | "--connection-mode", default="tc", help="Connection mode to use in the datapath" 151 | ) 152 | args = parser.parse_args() 153 | 154 | LOG_FORMAT = "%(asctime)s: %(module)-10s %(levelname)-8s %(message)s" 155 | logging.basicConfig(format=LOG_FORMAT) 156 | logger = logging.getLogger() 157 | 158 | logger.setLevel(logging.DEBUG) 159 | if args.trace: 160 | logger.setLevel(1) 161 | 162 | vr = SONiC( 163 | args.hostname, 164 | args.username, 165 | args.password, 166 | conn_mode=args.connection_mode, 167 | ) 168 | vr.start() 169 | -------------------------------------------------------------------------------- /sros/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Nokia 2 | NAME=SROS 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qcow2 5 | 6 | 7 | ## match versions like: 8 | # sros-vm-22.10.R3.qcow2 9 | # sros-vm-23.3.R1.qcow2 10 | VERSION=$(shell echo $(IMAGE) | sed -e 's/\(magc-\)\?.\+[^0-9]\([0-9]\+\.[0-9]\+\.[A-Z][0-9]\+\(-[0-9]\+\)\?\)[^0-9].*$$/\1\2/') 11 | 12 | -include ../makefile-sanity.include 13 | -include ../makefile.include 14 | -------------------------------------------------------------------------------- /sros/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # base image dockerfile is defined in https://github.com/hellt/vrnetlab 2 | FROM ghcr.io/srl-labs/vrnetlab-base:0.2.0 3 | 4 | ARG IMAGE 5 | COPY $IMAGE* / 6 | COPY *.py / 7 | -------------------------------------------------------------------------------- /ubuntu/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Canonical 2 | NAME=Ubuntu 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # match versions like: 7 | # jammy-ubuntu-cloud.qcow2 8 | VERSION=$(shell echo $(IMAGE) | sed -e 's/\([a-z]\+\)-ubuntu-cloud.*/\1/') 9 | 10 | 11 | -include ../makefile-sanity.include 12 | -include ../makefile.include 13 | 14 | download: 15 | /bin/bash download.sh 16 | 17 | build: download 18 | $(MAKE) docker-image -------------------------------------------------------------------------------- /ubuntu/README.md: -------------------------------------------------------------------------------- 1 | # Ubuntu VM 2 | 3 | To download a compatible image of the Ubuntu VM execute the [download.sh](download.sh) script that will download a cloud-init image of Ubuntu from . The version is set in the script and can be changed manually. 4 | 5 | Once the qcow2 image is downloaded, build the container with the following command: 6 | 7 | ```bash 8 | make 9 | ``` 10 | 11 | The resulting container will be tagged as `vrnetlab/vr-ubuntu:`, e.g. `vrnetlab/vr-ubuntu:jammy`. 12 | 13 | ## Host requirements 14 | 15 | * 1 vCPU, 512 MB RAM 16 | 17 | ## Configuration 18 | 19 | Initial config is carried out via cloud-init. 20 | 21 | * `9.9.9.9` configured as the DNS resolver. Change it with `resolvectl` if required. 22 | -------------------------------------------------------------------------------- /ubuntu/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/debian:bookworm-slim AS builder 2 | 3 | ARG DISK_SIZE=4G 4 | 5 | RUN apt-get update -qy && \ 6 | apt-get install -y --no-install-recommends qemu-utils && \ 7 | rm -rf /var/lib/apt/lists/* 8 | 9 | ARG IMAGE 10 | COPY $IMAGE* / 11 | RUN qemu-img resize /$IMAGE $DISK_SIZE 12 | 13 | FROM public.ecr.aws/docker/library/debian:bookworm-slim 14 | 15 | ARG DEBIAN_FRONTEND=noninteractive 16 | ARG DISK_SIZE=4G 17 | 18 | RUN apt-get update -qy \ 19 | && apt-get install -y --no-install-recommends\ 20 | bridge-utils \ 21 | iproute2 \ 22 | socat \ 23 | qemu-kvm \ 24 | tcpdump \ 25 | ssh \ 26 | inetutils-ping \ 27 | dnsutils \ 28 | iptables \ 29 | nftables \ 30 | telnet \ 31 | cloud-utils \ 32 | sshpass \ 33 | && rm -rf /var/lib/apt/lists/* 34 | 35 | ARG IMAGE 36 | COPY --from=builder $IMAGE* / 37 | COPY *.py / 38 | 39 | EXPOSE 22 5000 10000-10099 40 | HEALTHCHECK CMD ["/healthcheck.py"] 41 | ENTRYPOINT ["/launch.py"] 42 | -------------------------------------------------------------------------------- /ubuntu/download.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | version="jammy" 4 | 5 | # Download latest jammy lts cloud image 6 | download_url="https://cloud-images.ubuntu.com/$version/current/$version-server-cloudimg-amd64-disk-kvm.img" 7 | 8 | # Extract the filename from the URL 9 | filename="$version-ubuntu-cloud.qcow2" 10 | 11 | # Check if the file already exists in the current directory 12 | if [ -e "$filename" ]; then 13 | echo "File $filename already exists. Skipping download." 14 | else 15 | # Download the URL 16 | curl -o $filename "$download_url" 17 | echo "Download complete: $filename" 18 | fi -------------------------------------------------------------------------------- /veos/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Arista 2 | NAME=vEOS 3 | IMAGE_FORMAT=vmdk 4 | IMAGE_GLOB=*.vmdk 5 | 6 | # match versions like: 7 | # vEOS-lab-4.16.6M.vmdk 8 | # vEOS-lab-4.16.14M.vmdk 9 | # vEOS-lab-4.17.1.1F.vmdk 10 | # vEOS-lab-4.17.1F.vmdk 11 | # vEOS-lab-4.20.0-EFT2.vmdk 12 | VERSION=$(shell echo $(IMAGE) | sed -e 's/.*-\([0-9]\.\([0-9]\+\.\)\{1,2\}[0-9]\{1,2\}\([A-Z]\|\-EFT[0-9]\)\)\.vmdk$$/\1/') 13 | 14 | -include ../makefile-sanity.include 15 | -include ../makefile.include 16 | 17 | docker-pre-build: 18 | # checking if ZTP config contains a string (DISABLE=True) in the file /zerotouch-config 19 | # if it does, we don't need to write this file 20 | @echo Checking ZTP status 21 | ZTPOFF=$(shell docker run --rm -it -e LIBGUESTFS_DEBUG=0 -v $$(pwd):/work cmattoon/guestfish --ro -a $(IMAGE) -m /dev/sda2 cat /zerotouch-config 2> /dev/null || echo "false"); \ 22 | echo "$@: ZTPOFF is $$ZTPOFF" && \ 23 | if [ "$$ZTPOFF" != "DISABLE=True" ]; then \ 24 | echo "Disabling ZTP" && docker run --rm -it -e LIBGUESTFS_DEBUG=0 -v $$(pwd):/work cmattoon/guestfish -a $(IMAGE) -m /dev/sda2 write /zerotouch-config "DISABLE=True"; \ 25 | fi -------------------------------------------------------------------------------- /veos/README.md: -------------------------------------------------------------------------------- 1 | # vrnetlab / Arista vEOS 2 | 3 | This is the vrnetlab docker image for Arista vEOS. 4 | 5 | > Originally developed by Kristian Larsson (@plajjan), adapted by @hellt to be integrated with [containerlab](https://containerlab.srlinux.dev) networking. 6 | 7 | ## Added in this fork 8 | 9 | * integration with containerlab as [vr-vmx](https://containerlab.srlinux.dev/manual/kinds/vr-vmx/) kind. 10 | * docker networking using `--connection-mode` flag 11 | * hostname, username and password configuration via flags 12 | * added support for [boot delay](https://containerlab.srlinux.dev/manual/vrnetlab/#boot-delay) to allow for a smooth start of the big topologies 13 | * enabled gNMI 14 | * fixes for auto image upgrade disrupted node config 15 | * fixes to boot vEOS64 image 16 | * base image updated to Ubuntu:20.04 17 | 18 | ## Building the docker image 19 | Download vEOS in vmdk format from https://www.arista.com/en/support/software-download 20 | Place the .vmdk file in this directory and run make. The resulting images is called `vrnetlab/vr-veos`. You can 21 | tag it with something else if you want, like `my-repo.example.com/vr-veos` and 22 | then push it to your repo. 23 | 24 | 25 | It's been tested to boot, respond to SSH and have correct interface mapping 26 | with the following images: 27 | 28 | * vEOS64-lab-4.25.2F 29 | 30 | 31 | ## System requirements 32 | 33 | * CPU: 1 core 34 | * RAM: 2GB 35 | * Disk: <1GB 36 | 37 | -------------------------------------------------------------------------------- /veos/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/debian:bookworm-slim 2 | LABEL maintainer="Kristian Larsson " 3 | LABEL maintainer="Roman Dodin " 4 | 5 | ARG DEBIAN_FRONTEND=noninteractive 6 | RUN apt-get update -qy \ 7 | && apt-get install -y --no-install-recommends \ 8 | bridge-utils \ 9 | iproute2 \ 10 | python3-ipy \ 11 | socat \ 12 | qemu-kvm \ 13 | qemu-utils \ 14 | tcpdump \ 15 | tftpd-hpa \ 16 | ssh \ 17 | inetutils-ping \ 18 | dnsutils \ 19 | iptables \ 20 | nftables \ 21 | telnet \ 22 | && rm -rf /var/lib/apt/lists/* 23 | 24 | ARG IMAGE 25 | COPY $IMAGE* / 26 | COPY *.py / 27 | 28 | EXPOSE 22 80 161/udp 443 830 5000 6030 10000-10099 57400 29 | HEALTHCHECK CMD ["/healthcheck.py"] 30 | ENTRYPOINT ["/launch.py"] 31 | -------------------------------------------------------------------------------- /vios/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Cisco 2 | NAME=vIOS 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # Match images like: 7 | # - cisco_vios-158-3.M2.qcow2 8 | # Extract version cisco_vios and qcow2, for example: 158-3.M2 9 | VERSION=$(shell echo $(IMAGE) | sed -e 's/cisco_vios-\(.*\)\.qcow2/\1/') 10 | 11 | -include ../makefile-sanity.include 12 | -include ../makefile.include 13 | -------------------------------------------------------------------------------- /vios/README.md: -------------------------------------------------------------------------------- 1 | # Cisco vIOS 2 | 3 | This is the vrnetlab docker image for Cisco vIOS router. 4 | 5 | ## Justification 6 | 7 | Cisco vIOS is a virtual router that can be used for testing and development purposes. 8 | It is older than IOS XE and IOS XR (running only 15.x IOS version), however, it has several advantages: 9 | 10 | - Small memory footprint (512MB vs 4GB+ for IOS XE/XR). With KSM enabled, the memory usage can be even lower. 11 | - Easy to run on a laptop or a small server with limited resources for education purposes. 12 | - Good for scalability testing of applications, when you don't need all new features of IOS XE/XR. 13 | 14 | ## Building the docker image 15 | 16 | Qemu disk image can be obtained from Cisco Modeling Labs (CML). 17 | More information about Cisco vIOS: 18 | 19 | 20 | Once you extract disk image, format the name to the following format: 21 | `cisco_vios-[VERSION].qcow2` 22 | Where `[VERSION]` is the desired version of the image, for example `15.6.3M1`. 23 | 24 | Finally, you can build the docker image with the `make docker-image` command. 25 | 26 | Tested with versions: 27 | 28 | - 15.9.3M6 29 | 30 | ## System requirements 31 | 32 | - CPU: 1 core 33 | - RAM: 512MB 34 | - Disk: <1GB 35 | 36 | ## Network interfaces 37 | 38 | The router supports up to 16 GigabitEthernet interfaces. 39 | 40 | - The first interface `GigaEthernet0/0` is used as the management interface (it is placed in separated VRF). 41 | - The rest of the interfaces are numbered from `GigaEthernet0/1` and are used as data interfaces. 42 | They are mapped to the docker container interfaces `eth1`, `eth2`, etc. 43 | 44 | ## Management plane 45 | 46 | The following protocols are enabled on the management interface: 47 | 48 | - CLI SSH on port 22 49 | - NETCONF via SSH on port 22 (the same credentials are used as for CLI SSH) 50 | - SNMPv2c on port 161 (`public` used as community string) 51 | 52 | ## Environment variables 53 | 54 | | ID | Description | Default | 55 | |-----------------|---------------------------|------------| 56 | | USERNAME | SSH username | admin | 57 | | PASSWORD | SSH password | admin | 58 | | HOSTNAME | device hostname | vios | 59 | | TRACE | enable trace logging | false | 60 | | CONNECTION_MODE | interface connection mode | tc | 61 | 62 | ## Configuration persistence 63 | 64 | The startup configuration can be provided by mounting a file to `/config/startup-config.cfg`. 65 | The changes done in the router configuration during runtime are not automatically persisted outside 66 | the container - after stopping the container, the content of the flash/NVRAM is lost. 67 | User is responsible for persistence of the changes, for example, by copying the configuration 68 | to mounted startup-configuration file. 69 | 70 | ## Sample containerlab topology 71 | 72 | ```yaml 73 | name: vios-lab 74 | 75 | topology: 76 | kinds: 77 | linux: 78 | image: vrnetlab/cisco_vios:15.9.3M6 79 | nodes: 80 | vios1: 81 | kind: linux 82 | binds: 83 | - vios1.cfg:/config/startup-config.cfg 84 | env: 85 | HOSTNAME: xrv1 86 | vios2: 87 | kind: linux 88 | binds: 89 | - vios2.cfg:/config/startup-config.cfg 90 | env: 91 | HOSTNAME: xrv2 92 | links: 93 | - endpoints: ["vios1:eth1","vios2:eth1"] 94 | ``` 95 | -------------------------------------------------------------------------------- /vios/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/srl-labs/vrnetlab-base:0.1.0 2 | 3 | ARG IMAGE 4 | COPY $IMAGE* / 5 | COPY *.py / 6 | 7 | EXPOSE 22 161/udp 5000 10000-10099 8 | HEALTHCHECK CMD ["/healthcheck.py"] 9 | ENTRYPOINT ["/launch.py"] 10 | -------------------------------------------------------------------------------- /viosl2/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Cisco 2 | NAME=vIOSL2 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # Match images like: 7 | # - cisco_viosl2-15.2.qcow2 8 | # Extract version, for example: 15.2 9 | VERSION=$(shell echo $(IMAGE) | sed -e 's/cisco_viosl2-\(.*\)\.qcow2/\1/') 10 | 11 | -include ../makefile-sanity.include 12 | -include ../makefile.include 13 | -------------------------------------------------------------------------------- /viosl2/README.md: -------------------------------------------------------------------------------- 1 | # Cisco vIOS L2 2 | 3 | This is the vrnetlab docker image for Cisco vIOS L2 switch. Everything is based on the vios vrnetlab router files and 4 | modified to support the vIOS L2 image. 5 | 6 | ## Justification 7 | 8 | Cisco vIOS L2 is a virtual switch that can be used for testing and development purposes. 9 | It is older than IOL L2 (running only 15.x IOS version), however, it has several advantages: 10 | 11 | - Small memory footprint (768MB vs 4GB+ for IOS XE). With KSM enabled, the memory usage can be even lower. 12 | - Easy to run on a laptop or a small server with limited resources for education purposes. 13 | - Good for scalability testing of applications, when you don't need all new features of IOS XE. 14 | 15 | ## Building the docker image 16 | 17 | Qemu disk image can be obtained from Cisco Modeling Labs (CML). 18 | More information about Cisco vIOS: 19 | 20 | 21 | Once you extract disk image, rename the image file to the following format: 22 | `cisco_viosl2-[VERSION].qcow2` 23 | Where `[VERSION]` is the desired version of the image, for example `15.2` or `15.2.2020`. 24 | 25 | Finally, you can build the docker image with the `make docker-image` command. 26 | 27 | Tested with versions: 28 | 29 | - 15.2 (image: vios_l2-adventerprisek9-m.ssa.high_iron_20200929.qcow2) 30 | 31 | ## System requirements 32 | 33 | - CPU: 1 core 34 | - RAM: 768MB 35 | - Disk: <1GB 36 | 37 | ## Network interfaces 38 | 39 | The router supports up to 16 GigabitEthernet interfaces. 40 | 41 | - The first interface `GigabitEthernet0/0` is used as the management interface (it is placed in separated VRF) and is 42 | mapped to the docker container interface `eth0`. 43 | - The rest of the interfaces are numbered from `GigabitEthernet0/1` and are used as data interfaces. 44 | They are mapped to the docker container interfaces `eth1`, `eth2`, etc. 45 | - The interfaces are used in groups of four, e.g. `GigabitEthernet0/0` to `GigabitEthernet0/3`, `GigabitEthernet1/0` to 46 | `GigabitEthernet1/3`, etc. 47 | 48 | ## Management plane 49 | 50 | The following protocols are enabled on the management interface: 51 | 52 | - CLI SSH on port 22 53 | - NETCONF via SSH on port 22 (the same credentials are used as for CLI SSH) 54 | - SNMPv2c on port 161 (`public` used as community string) 55 | 56 | ## Environment variables 57 | 58 | | ID | Description | Default | 59 | |-----------------|---------------------------|---------| 60 | | USERNAME | SSH username | admin | 61 | | PASSWORD | SSH password | admin | 62 | | HOSTNAME | device hostname | viosl2 | 63 | | TRACE | enable trace logging | false | 64 | | CONNECTION_MODE | interface connection mode | tc | 65 | 66 | ## Configuration persistence 67 | 68 | The startup configuration can be provided by mounting a file to `/config/startup-config.cfg`. 69 | The changes done in the switch configuration during runtime are not automatically persisted outside 70 | the container - after stopping the container, the content of the flash/NVRAM is lost. 71 | User is responsible for persistence of the changes, for example, by copying the configuration 72 | to mounted startup-configuration file. 73 | 74 | ## Sample containerlab topology 75 | 76 | ```yaml 77 | name: viosl2-lab 78 | 79 | topology: 80 | kinds: 81 | linux: 82 | image: vrnetlab/cisco_viosl2:15.2 83 | nodes: 84 | viosl2-1: 85 | kind: linux 86 | binds: 87 | - viosl2-1.cfg:/config/startup-config.cfg 88 | env: 89 | HOSTNAME: viosl2-1 90 | viosl2-2: 91 | kind: linux 92 | binds: 93 | - viosl2-2.cfg:/config/startup-config.cfg 94 | env: 95 | HOSTNAME: viosl2-2 96 | links: 97 | - endpoints: ["viosl2-1:eth1","viosl2-2:eth1"] 98 | ``` 99 | -------------------------------------------------------------------------------- /viosl2/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/srl-labs/vrnetlab-base:0.1.0 2 | 3 | ARG IMAGE 4 | COPY $IMAGE* / 5 | COPY *.py / 6 | 7 | EXPOSE 22 161/udp 5000 10000-10099 8 | HEALTHCHECK CMD ["/healthcheck.py"] 9 | ENTRYPOINT ["/launch.py"] 10 | -------------------------------------------------------------------------------- /vjunosevolved/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Juniper 2 | NAME=vJunosEvolved 3 | IMAGE_FORMAT=qcow 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # match versions like: 7 | # vJunosEvolved-23.1R1.8.qcow2 8 | # vJunosEvolved-22.1R2.2.qcow2 9 | # vJunosEvolved-21.2R4.3.qcow2 10 | # ... 11 | 12 | VERSION=$(shell echo $(IMAGE) | sed -e 's/vjunosevolved-//i' | sed -e 's/.qcow2//i') 13 | 14 | -include ../makefile-sanity.include 15 | -include ../makefile.include 16 | -------------------------------------------------------------------------------- /vjunosevolved/README.md: -------------------------------------------------------------------------------- 1 | # vrnetlab / Juniper vJunosEvolved 2 | 3 | This is the vrnetlab docker image for Juniper's vJunosEvolved. 4 | 5 | > Available with [containerlab](https://containerlab.dev) as juniper_vjunosevolved. 6 | 7 | There are two variants of vJunosEvolved: the default variant, modelling a Juniper BT chipset, and starting with JunosEvo 24.2, a BX variant, modelling a chassis with two Juniper BX chipsets in it. 8 | 9 | ## Building the docker image 10 | 11 | Download the vJunosEvolved .qcow2 image from 12 | and place it in this directory. After typing `make`, a new image will appear called `vrnetlab/vjunosevolved`. 13 | Run `docker images` to confirm this. 14 | 15 | ## System requirements 16 | 17 | CPU: 4 cores 18 | RAM: 8GB 19 | DISK: ~2.5GB 20 | -------------------------------------------------------------------------------- /vjunosevolved/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/srl-labs/vrnetlab-base:0.2.1 2 | LABEL org.opencontainers.image.authors="roman@dodin.dev,vista@birb.network" 3 | 4 | ARG IMAGE 5 | COPY $IMAGE* / 6 | 7 | # copy conf file 8 | COPY init.conf / 9 | # copy config shell script 10 | COPY make-config.sh / 11 | # copy python scripts for launching VM 12 | COPY *.py / 13 | 14 | EXPOSE 22 161/udp 830 5000 10000-10099 57400 15 | -------------------------------------------------------------------------------- /vjunosevolved/docker/init.conf: -------------------------------------------------------------------------------- 1 | system { 2 | host-name {HOSTNAME}; 3 | root-authentication { 4 | encrypted-password "{CRYPT_PSWD}"; ## SECRET-DATA 5 | } 6 | login { 7 | user admin { 8 | class super-user; 9 | authentication { 10 | encrypted-password "{CRYPT_PSWD}"; ## SECRET-DATA 11 | } 12 | } 13 | } 14 | services { 15 | ssh { 16 | root-login allow; 17 | } 18 | netconf { 19 | ssh; 20 | } 21 | } 22 | management-instance; 23 | } 24 | interfaces { 25 | re0:mgmt-0 { 26 | unit 0 { 27 | family inet { 28 | address {MGMT_IP_IPV4}; 29 | } 30 | family inet6 { 31 | address {MGMT_IP_IPV6}; 32 | } 33 | } 34 | } 35 | } 36 | routing-instances { 37 | mgmt_junos { 38 | routing-options { 39 | static { 40 | route 0.0.0.0/0 next-hop {MGMT_GW_IPV4}; 41 | } 42 | rib mgmt_junos.inet6.0 { 43 | static { 44 | route ::/0 next-hop {MGMT_GW_IPV6}; 45 | } 46 | } 47 | } 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /vjunosevolved/docker/make-config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Create a config metadisk from a supplied juniper.conf to attach 3 | # to a vJunos VM instance 4 | usage() { 5 | echo "Usage : make-config.sh " 6 | exit 0; 7 | } 8 | cleanup () { 9 | echo "Cleaning up..." 10 | umount -f -q $MNTDIR 11 | losetup -d $LOOPDEV 12 | rm -rfv $STAGING 13 | rm -rfv $MNTDIR 14 | } 15 | 16 | cleanup_failed () { 17 | cleanup; 18 | rm -rfv $2 19 | exit 1 20 | } 21 | 22 | if [ $# != 2 ]; then 23 | usage; 24 | fi 25 | 26 | 27 | STAGING=`mktemp -d -p /var/tmp` 28 | MNTDIR=`mktemp -d -p /var/tmp` 29 | mkdir $STAGING/config 30 | cp -v $1 $STAGING/config 31 | qemu-img create -f raw $2 32M 32 | LOOP_EXITCODE=1 33 | while [ $LOOP_EXITCODE != 0 ]; do 34 | LOOPDEV=`losetup -f` 35 | if [ ! -b ${LOOPDEV} ]; then 36 | echo "Free loop device ${LOOPDEV} does not exist, manually creating loop device node" 37 | LOOPINDEX=`echo ${LOOPDEV} | grep -Po "\d+"` 38 | mknod ${LOOPDEV} b 7 ${LOOPINDEX} 39 | fi 40 | losetup ${LOOPDEV} $2 41 | LOOP_EXITCODE=$? 42 | done 43 | mkfs.vfat -v -n "vmm-data" $LOOPDEV 44 | if [ $? != 0 ]; then 45 | echo "Failed to format disk $LOOPDEV; exiting" 46 | cleanup_failed; 47 | fi 48 | mount -t vfat $LOOPDEV $MNTDIR 49 | if [ $? != 0 ]; then 50 | echo "Failed to mount metadisk $LOOPDEV; exiting" 51 | cleanup_failed; 52 | 53 | fi 54 | echo "Copying file(s) to config disk $2" 55 | (cd $STAGING; tar cvzf $MNTDIR/vmm-config.tgz .) 56 | cleanup 57 | echo "Config disk $2 created" 58 | exit 0 59 | -------------------------------------------------------------------------------- /vjunosrouter/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Juniper 2 | NAME=vJunos-router 3 | IMAGE_FORMAT=qcow 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # match versions like: 7 | # vJunos-router-23.2R1.15.qcow2 8 | # ... 9 | VERSION=$(shell echo $(IMAGE) | sed -e 's/vJunos-router-//i' | sed -e 's/.qcow2//i') 10 | 11 | -include ../makefile-sanity.include 12 | -include ../makefile.include -------------------------------------------------------------------------------- /vjunosrouter/README.md: -------------------------------------------------------------------------------- 1 | # vrnetlab / Juniper vJunos-router 2 | 3 | This is the vrnetlab docker image for Juniper's vJunos-router. This is built from the vJunos-switch template. 4 | 5 | ## Building the docker image 6 | 7 | Download the vJunos-router .qcow2 image from 8 | and place it in this directory. After typing `make`, a new image will appear called `vrnetlab/vjunosrouter`. 9 | Run `docker images` to confirm this. 10 | 11 | ## System requirements 12 | 13 | CPU: 4 cores 14 | RAM: 5GB 15 | DISK: ~4.5GB 16 | -------------------------------------------------------------------------------- /vjunosrouter/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/srl-labs/vrnetlab-base:0.2.1 2 | LABEL org.opencontainers.image.authors="roman@dodin.dev,vista@birb.network" 3 | 4 | ARG IMAGE 5 | COPY $IMAGE* / 6 | 7 | # copy conf file 8 | COPY init.conf / 9 | # copy config shell script 10 | COPY make-config.sh / 11 | # copy python scripts for launching VM 12 | COPY *.py / 13 | 14 | EXPOSE 22 161/udp 830 5000 10000-10099 57400 15 | -------------------------------------------------------------------------------- /vjunosrouter/docker/init.conf: -------------------------------------------------------------------------------- 1 | system { 2 | host-name {HOSTNAME}; 3 | root-authentication { 4 | plain-text-password-value "admin@123"; 5 | } 6 | login { 7 | user admin { 8 | class super-user; 9 | authentication { 10 | plain-text-password-value "admin@123"; 11 | } 12 | } 13 | } 14 | services { 15 | ssh { 16 | root-login allow; 17 | } 18 | netconf { 19 | ssh; 20 | } 21 | } 22 | management-instance; 23 | } 24 | chassis { 25 | fpc 0 { 26 | pic 0 { 27 | number-of-ports 12; 28 | } 29 | } 30 | } 31 | interfaces { 32 | fxp0 { 33 | unit 0 { 34 | family inet { 35 | address {MGMT_IP_IPV4}; 36 | } 37 | family inet6 { 38 | address {MGMT_IP_IPV6}; 39 | } 40 | } 41 | } 42 | } 43 | routing-instances { 44 | mgmt_junos { 45 | routing-options { 46 | static { 47 | route 0.0.0.0/0 next-hop {MGMT_GW_IPV4}; 48 | } 49 | rib mgmt_junos.inet6.0 { 50 | static { 51 | route ::/0 next-hop {MGMT_GW_IPV6}; 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /vjunosrouter/docker/make-config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Create a config metadisk from a supplied juniper.conf to attach 3 | # to a vJunos VM instance 4 | usage() { 5 | echo "Usage : make-config.sh " 6 | exit 0; 7 | } 8 | cleanup () { 9 | echo "Cleaning up..." 10 | umount -f -q $MNTDIR 11 | losetup -d $LOOPDEV 12 | rm -rfv $STAGING 13 | rm -rfv $MNTDIR 14 | } 15 | 16 | cleanup_failed () { 17 | cleanup; 18 | rm -rfv $2 19 | exit 1 20 | } 21 | 22 | if [ $# != 2 ]; then 23 | usage; 24 | fi 25 | 26 | 27 | STAGING=`mktemp -d -p /var/tmp` 28 | MNTDIR=`mktemp -d -p /var/tmp` 29 | mkdir $STAGING/config 30 | cp -v $1 $STAGING/config 31 | qemu-img create -f raw $2 32M 32 | LOOP_EXITCODE=1 33 | while [ $LOOP_EXITCODE != 0 ]; do 34 | LOOPDEV=`losetup -f` 35 | if [ ! -b ${LOOPDEV} ]; then 36 | echo "Free loop device ${LOOPDEV} does not exist, manually creating loop device node" 37 | LOOPINDEX=`echo ${LOOPDEV} | grep -Po "\d+"` 38 | mknod ${LOOPDEV} b 7 ${LOOPINDEX} 39 | fi 40 | losetup ${LOOPDEV} $2 41 | LOOP_EXITCODE=$? 42 | done 43 | mkfs.vfat -v -n "vmm-data" $LOOPDEV 44 | if [ $? != 0 ]; then 45 | echo "Failed to format disk $LOOPDEV; exiting" 46 | cleanup_failed; 47 | fi 48 | mount -t vfat $LOOPDEV $MNTDIR 49 | if [ $? != 0 ]; then 50 | echo "Failed to mount metadisk $LOOPDEV; exiting" 51 | cleanup_failed; 52 | 53 | fi 54 | echo "Copying file(s) to config disk $2" 55 | (cd $STAGING; tar cvzf $MNTDIR/vmm-config.tgz .) 56 | cleanup 57 | echo "Config disk $2 created" 58 | exit 0 59 | -------------------------------------------------------------------------------- /vjunosswitch/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Juniper 2 | NAME=vJunos-switch 3 | IMAGE_FORMAT=qcow 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # match versions like: 7 | # vJunos-switch-23.1R1.8.qcow2 8 | # vJunos-switch-22.1R2.2.qcow2 9 | # vJunos-switch-21.2R4.3.qcow2 10 | # ... 11 | 12 | VERSION=$(shell echo $(IMAGE) | sed -e 's/vjunos-switch-//i' | sed -e 's/.qcow2//i') 13 | 14 | -include ../makefile-sanity.include 15 | -include ../makefile.include 16 | -------------------------------------------------------------------------------- /vjunosswitch/README.md: -------------------------------------------------------------------------------- 1 | # vrnetlab / Juniper vJunos-switch 2 | 3 | This is the vrnetlab docker image for Juniper's vJunos-switch. 4 | 5 | > Available with [containerlab](https://containerlab.dev) as vr-vjunosswitch. 6 | 7 | ## Building the docker image 8 | 9 | Download the vJunos-switch .qcow2 image from 10 | and place it in this directory. After typing `make`, a new image will appear called `vrnetlab/vjunosswitch`. 11 | Run `docker images` to confirm this. 12 | 13 | ## System requirements 14 | 15 | CPU: 4 cores 16 | RAM: 5GB 17 | DISK: ~4.5GB 18 | -------------------------------------------------------------------------------- /vjunosswitch/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/srl-labs/vrnetlab-base:0.2.1 2 | LABEL org.opencontainers.image.authors="roman@dodin.dev,vista@birb.network" 3 | 4 | ARG IMAGE 5 | COPY $IMAGE* / 6 | 7 | # copy conf file 8 | COPY init.conf / 9 | # copy config shell script 10 | COPY make-config.sh / 11 | # copy python scripts for launching VM 12 | COPY *.py / 13 | 14 | EXPOSE 22 161/udp 830 5000 10000-10099 57400 15 | -------------------------------------------------------------------------------- /vjunosswitch/docker/init.conf: -------------------------------------------------------------------------------- 1 | system { 2 | host-name {HOSTNAME}; 3 | root-authentication { 4 | plain-text-password-value "admin@123"; 5 | } 6 | login { 7 | user admin { 8 | class super-user; 9 | authentication { 10 | plain-text-password-value "admin@123"; 11 | } 12 | } 13 | } 14 | services { 15 | ssh { 16 | root-login allow; 17 | } 18 | netconf { 19 | ssh; 20 | } 21 | } 22 | management-instance; 23 | } 24 | chassis { 25 | fpc 0 { 26 | pic 0 { 27 | number-of-ports 56; 28 | } 29 | } 30 | } 31 | interfaces { 32 | fxp0 { 33 | unit 0 { 34 | family inet { 35 | address {MGMT_IP_IPV4}; 36 | } 37 | family inet6 { 38 | address {MGMT_IP_IPV6}; 39 | } 40 | } 41 | } 42 | } 43 | routing-instances { 44 | mgmt_junos { 45 | routing-options { 46 | static { 47 | route 0.0.0.0/0 next-hop {MGMT_GW_IPV4}; 48 | } 49 | rib mgmt_junos.inet6.0 { 50 | static { 51 | route ::/0 next-hop {MGMT_GW_IPV6}; 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /vjunosswitch/docker/make-config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Create a config metadisk from a supplied juniper.conf to attach 3 | # to a vJunos VM instance 4 | usage() { 5 | echo "Usage : make-config.sh " 6 | exit 0; 7 | } 8 | cleanup () { 9 | echo "Cleaning up..." 10 | umount -f -q $MNTDIR 11 | losetup -d $LOOPDEV 12 | rm -rfv $STAGING 13 | rm -rfv $MNTDIR 14 | } 15 | 16 | cleanup_failed () { 17 | cleanup; 18 | rm -rfv $2 19 | exit 1 20 | } 21 | 22 | if [ $# != 2 ]; then 23 | usage; 24 | fi 25 | 26 | 27 | STAGING=`mktemp -d -p /var/tmp` 28 | MNTDIR=`mktemp -d -p /var/tmp` 29 | mkdir $STAGING/config 30 | cp -v $1 $STAGING/config 31 | qemu-img create -f raw $2 32M 32 | LOOP_EXITCODE=1 33 | while [ $LOOP_EXITCODE != 0 ]; do 34 | LOOPDEV=`losetup -f` 35 | if [ ! -b ${LOOPDEV} ]; then 36 | echo "Free loop device ${LOOPDEV} does not exist, manually creating loop device node" 37 | LOOPINDEX=`echo ${LOOPDEV} | grep -Po "\d+"` 38 | mknod ${LOOPDEV} b 7 ${LOOPINDEX} 39 | fi 40 | losetup ${LOOPDEV} $2 41 | LOOP_EXITCODE=$? 42 | done 43 | mkfs.vfat -v -n "vmm-data" $LOOPDEV 44 | if [ $? != 0 ]; then 45 | echo "Failed to format disk $LOOPDEV; exiting" 46 | cleanup_failed; 47 | fi 48 | mount -t vfat $LOOPDEV $MNTDIR 49 | if [ $? != 0 ]; then 50 | echo "Failed to mount metadisk $LOOPDEV; exiting" 51 | cleanup_failed; 52 | 53 | fi 54 | echo "Copying file(s) to config disk $2" 55 | (cd $STAGING; tar cvzf $MNTDIR/vmm-config.tgz .) 56 | cleanup 57 | echo "Config disk $2 created" 58 | exit 0 59 | -------------------------------------------------------------------------------- /vmx/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Juniper 2 | NAME=vMX 3 | IMAGE_FORMAT=tgz 4 | IMAGE_GLOB=*.tgz 5 | 6 | # match versions like: 7 | # vmx-14.1R6.4.tgz 8 | # vmx-15.1F4.15.tgz 9 | # vmx-bundle-15.1F6.9.tgz 10 | # vmx-bundle-16.1R1.7.tgz 11 | # vmx-bundle-16.1R2.11.tgz 12 | # vmx-bundle-17.1R1.8.tgz 13 | # vmx-bundle-16.1R4-S2.2.tgz 14 | # vmx-bundle-17.1R1-S1.tgz 15 | VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+[^0-9]\([0-9][0-9]\.[0-9][A-Z][0-9]\+\(\.[0-9]\+\|-[SD][0-9]\+\(\.[0-9]\+\)\?\)\)[^0-9].*$$/\1/') 16 | 17 | -include ../makefile-sanity.include 18 | -include ../makefile.include 19 | -include ../makefile-install.include 20 | 21 | docker-build-image-copy: 22 | ./vmx-extract.sh $(IMAGE) 23 | -------------------------------------------------------------------------------- /vmx/README.md: -------------------------------------------------------------------------------- 1 | # vrnetlab / Juniper vMX 2 | 3 | This is the vrnetlab docker image for Juniper vMX. 4 | 5 | > Originally developed by Kristian Larsson (@plajjan), adapted by @hellt to be integrated with [containerlab](https://containerlab.srlinux.dev) networking. 6 | 7 | ## Added in this fork 8 | 9 | * integration with containerlab as [vr-vmx](https://containerlab.srlinux.dev/manual/kinds/vr-vmx/) kind. 10 | * docker networking using `--connection-mode` flag 11 | * hostname, username and password configuration via flags 12 | * added support for [boot delay](https://containerlab.srlinux.dev/manual/vrnetlab/#boot-delay) to allow for a smooth start of the big topologies 13 | * enabled gNMI 14 | * fixes for auto image upgrade disrupted node config 15 | * base image updated to Ubuntu:20.04 16 | 17 | ## Building the docker image 18 | Download vMX from http://www.juniper.net/support/downloads/?p=vmx#sw 19 | Put the .tgz file in this directory and run `make` and you should be good to 20 | go. The resulting image is called `vrnetlab/vr-vmx`. During the build it is normal to 21 | receive some error messages about files that do not exist, like; 22 | 23 | mv: cannot stat '/tmp/vmx*/images/jinstall64-vmx*img': No such file or directory 24 | mv: cannot stat '/tmp/vmx*/images/vPFE-lite-*.img': No such file or directory 25 | 26 | This is because different versions of JUNOS use different filenames. 27 | 28 | The build of vMX is excruciatingly slow, often taking 10-20 minutes. This is 29 | because the first time the VCP (control plane) starts up, it reads a config 30 | file that controls whether it should run as a VRR of VCP in a vMX. 31 | 32 | It's been tested to boot, respond to SSH and have correct interface mapping 33 | with the following images: 34 | 35 | * vmx-bundle-20.2R1.10.tgz 36 | 37 | 38 | ## System requirements 39 | 40 | CPU: 4 cores - 3 for the vFPC (virtual FPC - the forwarding plane) and 1 for 41 | VCP (the RE / control plane). 42 | 43 | RAM: 6GB - 2 for VCP and 4 for vFPC 44 | 45 | Disk: ~5GB for JUNOS 15.1, ~7GB for JUNOS 16 (I know, it's huge!!) 46 | -------------------------------------------------------------------------------- /vmx/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/debian:bookworm-slim 2 | LABEL org.opencontainers.image.authors="roman@dodin.dev" 3 | 4 | ENV DEBIAN_FRONTEND=noninteractive 5 | 6 | RUN apt-get update -qy \ 7 | && apt-get install -y --no-install-recommends \ 8 | bridge-utils \ 9 | iproute2 \ 10 | python3 \ 11 | socat \ 12 | qemu-kvm \ 13 | qemu-utils \ 14 | tcpdump \ 15 | procps \ 16 | inetutils-ping \ 17 | dnsutils \ 18 | telnet \ 19 | && rm -rf /var/lib/apt/lists/* 20 | 21 | COPY vmx /vmx 22 | COPY *.py / 23 | 24 | EXPOSE 22 161/udp 830 5000 10000-10099 57400 25 | HEALTHCHECK CMD ["/healthcheck.py"] 26 | ENTRYPOINT ["/launch.py"] 27 | -------------------------------------------------------------------------------- /vmx/vmx-extract.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | IMAGE=$1 4 | 5 | echo "Extracting Juniper vMX tgz" 6 | rm -rf tmp docker/vmx 7 | mkdir -p tmp docker/vmx 8 | tar -zxvf ${IMAGE} -C tmp/ --wildcards vmx*/images/*img --wildcards vmx*/images/*qcow2 9 | # VCP 10 | # The 're' directory contains files for a standalone RE 11 | mkdir -p docker/vmx/re 12 | mv -v tmp/vmx*/images/vmxhdd.img docker/vmx/re 13 | mv -v tmp/vmx*/images/junos-vmx*qcow2 docker/vmx/re # 16.1 and newer 14 | mv -v tmp/vmx*/images/jinstall64-vmx*img docker/vmx/re 15 | mv -v tmp/vmx*/images/metadata-usb-re*.img docker/vmx/re 16 | mv -v tmp/vmx*/images/metadata_usb.img docker/vmx/re/metadata-usb-re.img # old style 17 | 18 | # vFPC / vPFE 19 | mv -v tmp/vmx*/images/vPFE-lite-*.img docker/vmx/vfpc.img # 14.1 20 | mv -v tmp/vmx*/images/vFPC*.img docker/vmx/vfpc.img # 15.1 and newer 21 | mv -v tmp/vmx*/images/metadata-usb-*.img docker/vmx/ 22 | mv -v tmp/vmx*/images/metadata_usb.img docker/vmx/metadata-usb-re.img # old style 23 | # clean up 24 | rm -rfv tmp -------------------------------------------------------------------------------- /vqfx/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=juniper 2 | VR_NAME=vqfx 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qcow2 5 | 6 | # New vqfx are named: vqfx-19.4R1.10-re-qemu.qcow2 7 | VERSION=$(shell echo $(IMAGE) | sed -e 's/^vqfx-//'|sed -e 's/-re-qemu.qcow2//' ) 8 | # Old QFX are named: vqfx10k-re-15.1X53-D60.vmdk, and vqfx10k-re-18.4R1.8.vmdk and vqfx10k-pfe-18.4R1.8.vmdk so we need that version number extracted to convert them. 9 | VMDK_VERSION:=$(shell ls *-re-*.vmdk | sed -re 's/vqfx10k-re-([^;]*)\.vmdk.*$$/\1/') 10 | 11 | PFE_BASE_VERSION=$(shell echo $VERSION | sed -e s'/.*//') 12 | PFE_IMAGE=$(shell ls vqfx-$(PFE_BASE_VERSION)*-pfe-qemu.qcow*) 13 | 14 | -include ../makefile-sanity.include 15 | -include ../makefile.include 16 | 17 | format-legacy-images: 18 | @if ls *.vmdk; then echo "VMDKs exist, converting them to qcow format"; qemu-img convert -f vmdk -O qcow2 *-re-*.vmdk vqfx-$(VMDK_VERSION)-re-qemu.qcow2 && qemu-img convert -f vmdk -O qcow *-pfe-*.vmdk vqfx-$(VMDK_VERSION)-pfe-qemu.qcow; echo "VMDKs have been converted"; fi 19 | 20 | # TODO: we should make sure we only copy one PFE image (the latest?), in case there are many 21 | docker-pre-build: 22 | 23 | echo "pfe $(PFE_IMAGE)" 24 | cp vqfx*-pfe*.qcow* docker/ 25 | echo "image $(IMAGE)" 26 | echo "version $(VERSION)" 27 | 28 | # mostly copied from makefile.include, i wish this was easier to change 29 | docker-build-common: docker-clean-build docker-pre-build 30 | @if [ -z "$$IMAGE" ]; then echo "ERROR: No IMAGE specified"; exit 1; fi 31 | @if [ "$(IMAGE)" = "$(VERSION)" ]; then echo "ERROR: Incorrect version string ($(IMAGE)). The regexp for extracting version information is likely incorrect, check the regexp in the Makefile or open an issue at https://github.com/hellt/vrnetlab/issues/new including the image file name you are using."; exit 1; fi 32 | @echo "Building docker image using $(IMAGE) as $(REGISTRY)vr-$(VR_NAME):$(VERSION)" 33 | cp ../common/* docker/ 34 | $(MAKE) IMAGE=$$IMAGE docker-build-image-copy 35 | (cd docker; docker build --build-arg http_proxy=$(http_proxy) --build-arg https_proxy=$(https_proxy) --build-arg RE_IMAGE=$(IMAGE) --build-arg PFE_IMAGE=$(PFE_IMAGE) -t $(REGISTRY)vr-$(VR_NAME):$(VERSION) .) 36 | -------------------------------------------------------------------------------- /vqfx/README.md: -------------------------------------------------------------------------------- 1 | # vrnetlab / Juniper vQFX 2 | ======================= 3 | Experimental support for Juniper vQFX v18+ launched by containerlab. 4 | 5 | ## Building the docker image 6 | To be added. -------------------------------------------------------------------------------- /vqfx/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/debian:bookworm-slim 2 | ENV DEBIAN_FRONTEND=noninteractive 3 | 4 | RUN apt-get update -qy \ 5 | && apt-get install -y --no-install-recommends \ 6 | bridge-utils \ 7 | iproute2 \ 8 | python3-ipy \ 9 | socat \ 10 | qemu-kvm \ 11 | qemu-utils \ 12 | procps \ 13 | tcpdump \ 14 | && rm -rf /var/lib/apt/lists/* 15 | 16 | ARG RE_IMAGE 17 | ARG PFE_IMAGE 18 | 19 | COPY $RE_IMAGE / 20 | COPY $PFE_IMAGE / 21 | 22 | COPY healthcheck.py / 23 | COPY vrnetlab.py / 24 | COPY launch.py / 25 | 26 | EXPOSE 22 161/udp 830 5000 10000-10099 27 | HEALTHCHECK CMD ["/healthcheck.py"] 28 | ENTRYPOINT ["/launch.py"] 29 | -------------------------------------------------------------------------------- /vrnetlab-base.dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/debian:bookworm-slim 2 | LABEL org.opencontainers.image.authors="roman@dodin.dev" 3 | 4 | COPY --from=ghcr.io/astral-sh/uv:0.5.18 /uv /uvx /bin/ 5 | 6 | ARG DEBIAN_FRONTEND=noninteractive 7 | 8 | RUN apt-get update -qy \ 9 | && apt-get install -y --no-install-recommends \ 10 | ca-certificates \ 11 | bridge-utils \ 12 | iproute2 \ 13 | socat \ 14 | qemu-kvm \ 15 | qemu-utils \ 16 | tcpdump \ 17 | tftpd-hpa \ 18 | ssh \ 19 | inetutils-ping \ 20 | dnsutils \ 21 | iptables \ 22 | nftables \ 23 | telnet \ 24 | git \ 25 | dosfstools \ 26 | genisoimage \ 27 | ovmf \ 28 | && rm -rf /var/lib/apt/lists/* 29 | 30 | # copying the uv project 31 | COPY pyproject.toml /pyproject.toml 32 | COPY uv.lock /uv.lock 33 | RUN /bin/uv sync --frozen 34 | 35 | # copy core vrnetlab scripts 36 | COPY ./common/healthcheck.py ./common/vrnetlab.py / 37 | 38 | HEALTHCHECK CMD ["uv", "run", "/healthcheck.py"] 39 | ENTRYPOINT ["uv", "run", "/launch.py"] -------------------------------------------------------------------------------- /vsr1000/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=HP 2 | NAME=VSR1000 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qco 5 | 6 | # match versions like: 7 | # VSR1000_HPE-CMW710-R0326-X64.qcow 8 | # VSR1000_HPE-CMW710-E0321P01-X64.qco 9 | VERSION=$(shell echo $(IMAGE) | sed -n 's/.*CMW\([0-9]\)\([0-9]\+\)-\([ER][0-9][0-9][0-9][0-9]\).*/\1.\2-\3/p') 10 | 11 | -include ../makefile-sanity.include 12 | -include ../makefile.include 13 | 14 | -------------------------------------------------------------------------------- /vsr1000/README.md: -------------------------------------------------------------------------------- 1 | vrnetlab / HP VSR1000 2 | ===================== 3 | This is the vrnetlab docker image for HP VSR1000. 4 | 5 | Building the docker image 6 | ------------------------- 7 | Download the HPE VSR1001 image from 8 | https://h10145.www1.hpe.com/downloads/SoftwareReleases.aspx?ProductNumber=JG811AAE 9 | Unzip the downloaded zip file, place the .qco image in this directory and run 10 | `make docker-image`. The tag is the same as the version of the VSR1000 image, 11 | so if you have VSR1000_HPE-CMW710-R0326-X64.qco your docker image will be 12 | called vr-vsr1000:7.10-R0326 13 | 14 | Tested booting and responding to SSH: 15 | * VSR1000_HPE-CMW710-R0326-X64.qco MD5:4153d638bfa72ca72a957ea8682ad0e2 16 | 17 | Usage 18 | ----- 19 | ``` 20 | docker run -d --privileged --name my-vsr1000-router vr-vsr1000:7.10-R0326 21 | ``` 22 | 23 | System requirements 24 | ------------------- 25 | CPU: 1 core 26 | 27 | RAM: 1GB 28 | 29 | Disk: <1GB 30 | 31 | FUAQ - Frequently or Unfrequently Asked Questions 32 | ------------------------------------------------- 33 | ##### Q: Has this been extensively tested? 34 | A: Nope. It starts and you can connect to it. Take it for a spin and provide some feedback :-) 35 | -------------------------------------------------------------------------------- /vsr1000/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stretch 2 | MAINTAINER Kristian Larsson 3 | 4 | ENV DEBIAN_FRONTEND=noninteractive 5 | 6 | RUN apt-get update -qy \ 7 | && apt-get upgrade -qy \ 8 | && apt-get install -y \ 9 | bridge-utils \ 10 | iproute2 \ 11 | python3-ipy \ 12 | socat \ 13 | qemu-kvm \ 14 | && rm -rf /var/lib/apt/lists/* 15 | 16 | ARG IMAGE 17 | COPY $IMAGE* / 18 | COPY *.py / 19 | 20 | EXPOSE 22 161/udp 830 5000 6000 10000-10099 21 | HEALTHCHECK CMD ["/healthcheck.py"] 22 | ENTRYPOINT ["/launch.py"] 23 | -------------------------------------------------------------------------------- /vsr1000/docker/launch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import datetime 4 | import logging 5 | import os 6 | import re 7 | import signal 8 | import sys 9 | import telnetlib 10 | import time 11 | 12 | import vrnetlab 13 | 14 | def handle_SIGCHLD(signal, frame): 15 | os.waitpid(-1, os.WNOHANG) 16 | 17 | def handle_SIGTERM(signal, frame): 18 | sys.exit(0) 19 | 20 | signal.signal(signal.SIGINT, handle_SIGTERM) 21 | signal.signal(signal.SIGTERM, handle_SIGTERM) 22 | signal.signal(signal.SIGCHLD, handle_SIGCHLD) 23 | 24 | TRACE_LEVEL_NUM = 9 25 | logging.addLevelName(TRACE_LEVEL_NUM, "TRACE") 26 | def trace(self, message, *args, **kws): 27 | # Yes, logger takes its '*args' as 'args'. 28 | if self.isEnabledFor(TRACE_LEVEL_NUM): 29 | self._log(TRACE_LEVEL_NUM, message, args, **kws) 30 | logging.Logger.trace = trace 31 | 32 | class VSR_vm(vrnetlab.VM): 33 | def __init__(self, username, password): 34 | for e in os.listdir("/"): 35 | if re.search(".qco$", e): 36 | disk_image = "/" + e 37 | super(VSR_vm, self).__init__(username, password, disk_image=disk_image, ram=1024) 38 | 39 | # The VSR supports up to 15 user nics 40 | self.num_nics = 7 41 | 42 | def bootstrap_spin(self): 43 | """ This function should be called periodically to do work. 44 | """ 45 | 46 | if self.spins > 300: 47 | # too many spins with no result -> give up 48 | self.stop() 49 | self.start() 50 | return 51 | 52 | (ridx, match, res) = self.tn.expect([b"Performing automatic"], 1) 53 | if match: # got a match! 54 | if ridx == 0: # login 55 | self.logger.debug("VM started") 56 | 57 | self.wait_write("", wait="(qemu)", con=self.qm) 58 | 59 | # To allow access to aux0 serial console 60 | self.logger.debug("Writing to QEMU Monitor") 61 | 62 | # Cred to @plajjan for this one 63 | commands = """\x04 64 | 65 | 66 | system-view 67 | user-interface aux 0 68 | authentication-mode none 69 | user-role network-admin 70 | quit 71 | 72 | """ 73 | 74 | key_map = { 75 | '\x04': 'ctrl-d', 76 | ' ': 'spc', 77 | '-': 'minus', 78 | '\n': 'kp_enter' 79 | } 80 | 81 | qemu_commands = [ "sendkey {}".format(key_map.get(c) or c) for c in commands ] 82 | 83 | for c in qemu_commands: 84 | self.wait_write(c, wait="(qemu)", con=self.qm) 85 | # Pace the characters sent via QEMU Monitor 86 | time.sleep(0.1) 87 | 88 | self.logger.debug("Done writing to QEMU Monitor") 89 | self.logger.debug("Switching to line aux0") 90 | 91 | self.tn = telnetlib.Telnet("127.0.0.1", 5000 + self.num) 92 | 93 | # run main config! 94 | self.bootstrap_config() 95 | # close telnet connection 96 | self.tn.close() 97 | # startup time? 98 | startup_time = datetime.datetime.now() - self.start_time 99 | self.logger.info("Startup complete in: %s" % startup_time) 100 | # mark as running 101 | self.running = True 102 | return 103 | 104 | # no match, if we saw some output from the router it's probably 105 | # booting, so let's give it some more time 106 | if res != b'': 107 | self.logger.trace("OUTPUT: %s" % res.decode()) 108 | # reset spins if we saw some output 109 | self.spins = 0 110 | 111 | self.spins += 1 112 | 113 | return 114 | 115 | def bootstrap_config(self): 116 | """ Do the actual bootstrap config 117 | """ 118 | self.logger.info("applying bootstrap configuration") 119 | self.wait_write("\r", None) 120 | # Wait for the prompt 121 | time.sleep(1) 122 | self.wait_write("system-view", "") 123 | self.wait_write("ssh server enable", "[HPE]") 124 | self.wait_write("user-interface class vty", "[HPE]") 125 | self.wait_write("authentication-mode scheme", "[HPE-line-class-vty]") 126 | self.wait_write("protocol inbound ssh", "[HPE-line-class-vty]") 127 | self.wait_write("quit", "[HPE-line-class-vty]") 128 | self.wait_write("local-user %s" % (self.username), "[HPE]") 129 | self.wait_write("password simple %s" % (self.password), "[HPE-luser-manage-%s]" % (self.username)) 130 | self.wait_write("service-type ssh", "[HPE-luser-manage-%s]" % (self.username)) 131 | self.wait_write("authorization-attribute user-role network-admin", "[HPE-luser-manage-%s]" % (self.username)) 132 | self.wait_write("quit", "[HPE-luser-manage-%s]" % (self.username)) 133 | self.wait_write("interface GigabitEthernet%s/0" % (self.num_nics + 1), "[HPE]") 134 | self.wait_write("ip address 10.0.0.15 255.255.255.0", "[HPE-GigabitEthernet%s/0]" % (self.num_nics + 1)) 135 | self.wait_write("quit", "[HPE-GigabitEthernet%s/0]" % (self.num_nics + 1)) 136 | self.wait_write("quit", "[HPE]") 137 | self.wait_write("quit", "") 138 | self.logger.info("completed bootstrap configuration") 139 | 140 | class VSR(vrnetlab.VR): 141 | def __init__(self, username, password): 142 | super(VSR, self).__init__(username, password) 143 | self.vms = [ VSR_vm(username, password) ] 144 | 145 | if __name__ == '__main__': 146 | import argparse 147 | parser = argparse.ArgumentParser(description='') 148 | parser.add_argument('--trace', action='store_true', help='enable trace level logging') 149 | parser.add_argument('--username', default='vrnetlab', help='Username') 150 | parser.add_argument('--password', default='VR-netlab9', help='Password') 151 | args = parser.parse_args() 152 | 153 | LOG_FORMAT = "%(asctime)s: %(module)-10s %(levelname)-8s %(message)s" 154 | logging.basicConfig(format=LOG_FORMAT) 155 | logger = logging.getLogger() 156 | 157 | logger.setLevel(logging.DEBUG) 158 | if args.trace: 159 | logger.setLevel(1) 160 | 161 | vr = VSR(args.username, args.password) 162 | vr.start() 163 | -------------------------------------------------------------------------------- /vsrx/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Juniper 2 | NAME=vSRX 3 | IMAGE_FORMAT=qcow 4 | IMAGE_GLOB=*.qcow2 5 | IMAGE=ffp-12.1X47-D15.4-packetmode.qcow2 6 | 7 | # match versions like: 8 | # 12.1X47-D15.4 9 | VERSION=$(shell echo $(IMAGE) | sed -e 's/junos-vsrx3-x86-64-//' | sed -e 's/.qcow2//') 10 | 11 | -include ../makefile-sanity.include 12 | -include ../makefile.include 13 | -------------------------------------------------------------------------------- /vsrx/README.md: -------------------------------------------------------------------------------- 1 | # Juniper vSRX 2 | 3 | This is the vrnetlab docker image for Juniper's vSRX. 4 | Both "classic" vSRX 2.0 and the new vSRX 3.0 images are compatible with this vrnetlab kind. 5 | 6 | > Available with [containerlab](https://containerlab.dev) as `juniper_vsrx` kind. 7 | 8 | ## Building the docker image 9 | 10 | Download the vSRX 3.0 trial `.qcow2` image from https://support.juniper.net/support/downloads/?p=vsrx-evaluation 11 | and place it in this directory. A Juniper account is required to download the evaluation image. 12 | 13 | After typing `make`, a new image will appear called `vrnetlab/vjunosevolved`. 14 | Run `docker images` to confirm this. 15 | 16 | ## System requirements 17 | 18 | CPU: 2 cores 19 | RAM: 4GB 20 | DISK: Depends on kind. vSRX 2.0: ~4-5GB; vSRX 3.0: ~1.5GB 21 | 22 | ## Configuration 23 | 24 | vSRX nodes boot with a basic configuration by default, enabling SSH and Netconf, and basic management connectivity. All factory default configuration is removed. 25 | Further startup configuration can be passed by mounting it under `/config/startup-config.cfg`, this is done automatically by Containerlab. Only Juniper conf format is accepted. 26 | 27 | Note: The last version of vrnetlab to support `set` command-based startup configurations in vSRX is v0.17.1. A tool like [junoser](https://github.com/codeout/junoser) might help with converting your startup configurations to the canonical Juniper conf format. 28 | -------------------------------------------------------------------------------- /vsrx/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/srl-labs/vrnetlab-base:0.2.1 2 | LABEL org.opencontainers.image.authors="roman@dodin.dev,vista@birb.network" 3 | 4 | ARG IMAGE 5 | COPY $IMAGE* / 6 | 7 | COPY init.conf / 8 | COPY make-config-iso.sh / 9 | COPY *.py / 10 | 11 | EXPOSE 22 161/udp 830 5000 10000-10099 12 | -------------------------------------------------------------------------------- /vsrx/docker/init.conf: -------------------------------------------------------------------------------- 1 | system { 2 | host-name {HOSTNAME}; 3 | root-authentication { 4 | plain-text-password-value "admin@123"; 5 | } 6 | login { 7 | user admin { 8 | class super-user; 9 | authentication { 10 | plain-text-password-value "admin@123"; 11 | } 12 | } 13 | } 14 | services { 15 | ssh { 16 | root-login allow; 17 | } 18 | netconf { 19 | ssh; 20 | } 21 | } 22 | management-instance; 23 | } 24 | interfaces { 25 | fxp0 { 26 | unit 0 { 27 | family inet { 28 | address {MGMT_IP_IPV4}; 29 | } 30 | family inet6 { 31 | address {MGMT_IP_IPV6}; 32 | } 33 | } 34 | } 35 | } 36 | routing-instances { 37 | mgmt_junos { 38 | routing-options { 39 | static { 40 | route 0.0.0.0/0 next-hop {MGMT_GW_IPV4}; 41 | } 42 | rib mgmt_junos.inet6.0 { 43 | static { 44 | route ::/0 next-hop {MGMT_GW_IPV6}; 45 | } 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /vsrx/docker/launch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import datetime 4 | import logging 5 | import os 6 | import re 7 | import signal 8 | import subprocess 9 | import sys 10 | import uuid 11 | 12 | import vrnetlab 13 | 14 | STARTUP_CONFIG_FILE = "/config/startup-config.cfg" 15 | 16 | 17 | def handle_SIGCHLD(signal, frame): 18 | os.waitpid(-1, os.WNOHANG) 19 | 20 | 21 | def handle_SIGTERM(signal, frame): 22 | sys.exit(0) 23 | 24 | 25 | signal.signal(signal.SIGINT, handle_SIGTERM) 26 | signal.signal(signal.SIGTERM, handle_SIGTERM) 27 | signal.signal(signal.SIGCHLD, handle_SIGCHLD) 28 | 29 | TRACE_LEVEL_NUM = 9 30 | logging.addLevelName(TRACE_LEVEL_NUM, "TRACE") 31 | 32 | 33 | def trace(self, message, *args, **kws): 34 | # Yes, logger takes its '*args' as 'args'. 35 | if self.isEnabledFor(TRACE_LEVEL_NUM): 36 | self._log(TRACE_LEVEL_NUM, message, args, **kws) 37 | 38 | 39 | logging.Logger.trace = trace 40 | 41 | 42 | class VSRX_vm(vrnetlab.VM): 43 | def __init__(self, hostname, username, password, conn_mode): 44 | for e in os.listdir("/"): 45 | if re.search(".qcow2$", e): 46 | disk_image = "/" + e 47 | super(VSRX_vm, self).__init__( 48 | username, 49 | password, 50 | disk_image=disk_image, 51 | ram=4096, 52 | driveif="virtio", 53 | cpu="SandyBridge,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,aes=on", 54 | smp="2,sockets=1,cores=2,threads=1", 55 | mgmt_passthrough=False, 56 | ) 57 | self.nic_type = "virtio-net-pci" 58 | self.conn_mode = conn_mode 59 | self.num_nics = 10 60 | self.hostname = hostname 61 | 62 | with open("init.conf", "r") as file: 63 | cfg = file.read() 64 | 65 | cfg = cfg.replace("{MGMT_IP_IPV4}", self.mgmt_address_ipv4) 66 | cfg = cfg.replace("{MGMT_GW_IPV4}", self.mgmt_gw_ipv4) 67 | cfg = cfg.replace("{MGMT_IP_IPV6}", self.mgmt_address_ipv6) 68 | cfg = cfg.replace("{MGMT_GW_IPV6}", self.mgmt_gw_ipv6) 69 | cfg = cfg.replace("{HOSTNAME}", self.hostname) 70 | 71 | with open("init.conf", "w") as file: 72 | cfg = file.write(cfg) 73 | 74 | self.startup_config() 75 | 76 | # generate UUID to attach 77 | self.qemu_args.extend(["-uuid", str(uuid.uuid4())]) 78 | # mount config disk with startup config (juniper.conf) 79 | self.qemu_args.extend( 80 | [ 81 | "-drive", 82 | "if=ide,index=1,id=config_disk,file=/config.iso,media=cdrom", 83 | ] 84 | ) 85 | 86 | def startup_config(self): 87 | """Load additional config provided by user and append initial 88 | configurations set by vrnetlab.""" 89 | # if startup cfg DNE 90 | if not os.path.exists(STARTUP_CONFIG_FILE): 91 | self.logger.trace(f"Startup config file {STARTUP_CONFIG_FILE} is not found") 92 | # rename init.conf to juniper.conf, this is our startup config 93 | os.rename("init.conf", "juniper.conf") 94 | 95 | # if startup cfg file is found 96 | else: 97 | self.logger.trace( 98 | f"Startup config file {STARTUP_CONFIG_FILE} found, appending initial configuration" 99 | ) 100 | # append startup cfg to inital configuration 101 | append_cfg = f"cat init.conf {STARTUP_CONFIG_FILE} >> juniper.conf" 102 | subprocess.run(append_cfg, shell=True) 103 | 104 | # generate mountable config disk based on juniper.conf file with base vrnetlab configs 105 | subprocess.run( 106 | ["./make-config-iso.sh", "juniper.conf", "config.iso"], check=True 107 | ) 108 | 109 | def bootstrap_spin(self): 110 | """This function should be called periodically to do work.""" 111 | 112 | if self.spins > 300: 113 | # too many spins with no result -> give up 114 | self.stop() 115 | self.start() 116 | return 117 | 118 | (ridx, match, res) = self.tn.expect([b"login:"], 1) 119 | if match: # got a match! 120 | if ridx == 0: # login 121 | self.logger.info("VM started") 122 | # close telnet connection 123 | self.tn.close() 124 | # startup time? 125 | startup_time = datetime.datetime.now() - self.start_time 126 | self.logger.info("Startup complete in: %s" % startup_time) 127 | # mark as running 128 | self.running = True 129 | return 130 | 131 | # no match, if we saw some output from the router it's probably 132 | # booting, so let's give it some more time 133 | if res != b"": 134 | self.logger.trace("OUTPUT: %s" % res.decode()) 135 | # reset spins if we saw some output 136 | self.spins = 0 137 | 138 | self.spins += 1 139 | 140 | return 141 | 142 | 143 | class VSRX(vrnetlab.VR): 144 | def __init__(self, hostname, username, password, conn_mode): 145 | super(VSRX, self).__init__(username, password) 146 | self.vms = [VSRX_vm(hostname, username, password, conn_mode)] 147 | 148 | 149 | if __name__ == "__main__": 150 | import argparse 151 | 152 | parser = argparse.ArgumentParser(description="") 153 | parser.add_argument( 154 | "--trace", action="store_true", help="enable trace level logging" 155 | ) 156 | parser.add_argument("--hostname", default="vr-vsrx", help="SRX hostname") 157 | parser.add_argument("--username", default="vrnetlab", help="Username") 158 | parser.add_argument("--password", default="VR-netlab9", help="Password") 159 | parser.add_argument( 160 | "--connection-mode", default="tc", help="Connection mode to use in the datapath" 161 | ) 162 | args = parser.parse_args() 163 | 164 | LOG_FORMAT = "%(asctime)s: %(module)-10s %(levelname)-8s %(message)s" 165 | logging.basicConfig(format=LOG_FORMAT) 166 | logger = logging.getLogger() 167 | 168 | logger.setLevel(logging.DEBUG) 169 | if args.trace: 170 | logger.setLevel(1) 171 | 172 | vr = VSRX( 173 | args.hostname, 174 | args.username, 175 | args.password, 176 | conn_mode=args.connection_mode, 177 | ) 178 | vr.start() 179 | -------------------------------------------------------------------------------- /vsrx/docker/make-config-iso.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Create a config metadisk from a supplied juniper.conf to attach 3 | # to a vSRX VM instance 4 | usage() { 5 | echo "Usage : make-config-iso.sh " 6 | exit 0; 7 | } 8 | cleanup () { 9 | rm -rfv $STAGING 10 | } 11 | 12 | if [ $# != 2 ]; then 13 | usage; 14 | fi 15 | 16 | 17 | STAGING=`mktemp -d -p /var/tmp` 18 | mkdir $STAGING/config 19 | cp -v $1 $STAGING/config 20 | mkisofs -l -o $2 $STAGING/config 21 | 22 | cleanup 23 | echo "Config disk $2 created" 24 | exit 0 25 | -------------------------------------------------------------------------------- /vstc/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Spirent 2 | NAME=vstc 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*.qcow2 5 | 6 | VERSION=$(shell echo $(IMAGE) | sed -e 's/spirent_vstc-\(.*\)\.qcow2/\1/') 7 | 8 | -include ../makefile-sanity.include 9 | -include ../makefile.include 10 | -------------------------------------------------------------------------------- /vstc/README.md: -------------------------------------------------------------------------------- 1 | # Spirent vSTC 2 | 3 | This is the vrnetlab image for the Spirent vSTC (Virtual Spirent Test Center). 4 | 5 | Default credentials are: `admin`:`spt_admin` 6 | 7 | On bootup the node will be boostrapped with IP addressing config, and then rebooted to fully apply the configuration. 8 | 9 | Please wait for the 'Startup Completed' message in the `docker logs` output before attempting to connect to the node via the STC Client or SSH. 10 | 11 | ## Example 12 | 13 | You should use the Spirent vSTC with the `generic_vm` kind in your topology definition. 14 | 15 | Interface naming follows `ethX` naming convention, with `eth0` reserved for the clab management network. 16 | 17 | ```yaml 18 | name: vstc_lab 19 | topology: 20 | nodes: 21 | # example DUT 22 | r1: 23 | kind: nokia_sros 24 | image: vrnetlab/nokia_sros:24.10.R1 25 | # STC traffic generator 26 | vstc: 27 | kind: generic_vm 28 | image: vrnetlab/spirent_vstc:5.55.3216 29 | 30 | links: 31 | - endpoints: ["vstc:eth1","r1:1/1/1"] 32 | - endpoints: ["vstc:eth2","r1:1/1/2"] 33 | ``` 34 | -------------------------------------------------------------------------------- /vstc/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/srl-labs/vrnetlab-base:0.2.1 2 | 3 | ARG VERSION 4 | ENV VERSION=${VERSION} 5 | ARG IMAGE 6 | COPY $IMAGE* / 7 | COPY *.py / -------------------------------------------------------------------------------- /vstc/docker/launch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import datetime 4 | import logging 5 | import os 6 | import re 7 | import signal 8 | import sys 9 | 10 | import vrnetlab 11 | 12 | def handle_SIGCHLD(signal, frame): 13 | os.waitpid(-1, os.WNOHANG) 14 | 15 | 16 | def handle_SIGTERM(signal, frame): 17 | sys.exit(0) 18 | 19 | 20 | signal.signal(signal.SIGINT, handle_SIGTERM) 21 | signal.signal(signal.SIGTERM, handle_SIGTERM) 22 | signal.signal(signal.SIGCHLD, handle_SIGCHLD) 23 | 24 | TRACE_LEVEL_NUM = 9 25 | logging.addLevelName(TRACE_LEVEL_NUM, "TRACE") 26 | 27 | 28 | def trace(self, message, *args, **kws): 29 | # Yes, logger takes its '*args' as 'args'. 30 | if self.isEnabledFor(TRACE_LEVEL_NUM): 31 | self._log(TRACE_LEVEL_NUM, message, args, **kws) 32 | 33 | 34 | logging.Logger.trace = trace 35 | 36 | 37 | class STC_vm(vrnetlab.VM): 38 | def __init__(self, hostname, username, password): 39 | for e in os.listdir("/"): 40 | if re.search(".qcow2$", e): 41 | disk_image = "/" + e 42 | 43 | super(STC_vm, self).__init__( 44 | username, password, disk_image=disk_image, use_scrapli=True, min_dp_nics=1, mgmt_passthrough=True 45 | ) 46 | 47 | self.hostname = hostname 48 | self.num_nics = 9 49 | self.nic_type = "virtio-net-pci" 50 | self.conn_mode = "tc" 51 | self.wait_pattern = ">>" 52 | 53 | def bootstrap_spin(self): 54 | """This function should be called periodically to do work.""" 55 | 56 | if self.spins > 600: 57 | # too many spins with no result -> give up 58 | self.stop() 59 | self.start() 60 | return 61 | 62 | (ridx, match, res) = self.con_expect( 63 | [b"login:"] 64 | ) 65 | if match: 66 | self.bootstrap_config() 67 | 68 | self.scrapli_tn.close() 69 | # startup time? 70 | startup_time = datetime.datetime.now() - self.start_time 71 | self.logger.info("Startup complete in: %s", startup_time) 72 | # mark as running 73 | self.running = True 74 | return 75 | elif res: 76 | self.write_to_stdout(res) 77 | 78 | return 79 | 80 | def bootstrap_config(self): 81 | 82 | config = "" 83 | 84 | if self.mgmt_address_ipv4 != "dhcp" and self.mgmt_address_ipv4 is not None: 85 | v4_mgmt_address = vrnetlab.cidr_to_ddn(self.mgmt_address_ipv4) 86 | config += f"""mode static 87 | ipaddress {v4_mgmt_address[0]} 88 | netmask {v4_mgmt_address[1]} 89 | gwaddress {self.mgmt_gw_ipv4} 90 | """ 91 | 92 | if self.mgmt_address_ipv6 != "dhcp" and self.mgmt_address_ipv6 is not None: 93 | v6_mgmt_address = self.mgmt_address_ipv6.split("/") 94 | config += f"""ipv6mode static 95 | ipv6address {v6_mgmt_address[0]} 96 | ipv6prefixlen {v6_mgmt_address[1]} 97 | ipv6gwaddress {self.mgmt_gw_ipv6} 98 | """ 99 | 100 | if not config: 101 | return 102 | 103 | # login 104 | self.wait_write("admin", "") 105 | self.wait_write("spt_admin", "Password:") 106 | 107 | for line in config.splitlines(): 108 | self.wait_write(line) 109 | 110 | self.wait_write("activate") 111 | self.wait_write("reboot") 112 | 113 | self.con_read_until("login:") 114 | 115 | 116 | class STC(vrnetlab.VR): 117 | def __init__(self, hostname, username, password): 118 | super(STC, self).__init__(username, password) 119 | self.vms = [STC_vm(hostname, username, password)] 120 | 121 | 122 | if __name__ == "__main__": 123 | import argparse 124 | 125 | parser = argparse.ArgumentParser(description="") 126 | parser.add_argument( 127 | "--trace", action="store_true", help="enable trace level logging" 128 | ) 129 | parser.add_argument("--username", default="admin", help="Username") 130 | parser.add_argument("--password", default="spt_admin", help="Password") 131 | parser.add_argument("--hostname", default="stc", help="Hostname") 132 | parser.add_argument("--connection-mode", default="tc", help="Ignored, does nothing") 133 | 134 | args = parser.parse_args() 135 | 136 | LOG_FORMAT = "%(asctime)s: %(module)-10s %(levelname)-8s %(message)s" 137 | logging.basicConfig(format=LOG_FORMAT) 138 | logger = logging.getLogger() 139 | 140 | logger.setLevel(logging.DEBUG) 141 | if args.trace: 142 | logger.setLevel(1) 143 | 144 | vr = STC( 145 | args.hostname, 146 | args.username, 147 | args.password, 148 | ) 149 | vr.start() 150 | -------------------------------------------------------------------------------- /xrv/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Cisco 2 | NAME=XRv 3 | IMAGE_FORMAT=vmdk 4 | IMAGE_GLOB=*vmdk* 5 | QCOW=$(shell ls *qcow2* 2>/dev/null) 6 | 7 | # match versions like: 8 | # iosxrv-k9-demo-5.3.3.51U.vmdk 9 | # iosxrv-k9-demo-6.1.2.vmdk 10 | # iosxrv-k9-demo-6.2.2.15I.DT_IMAGE.vmdk 11 | # iosxrv-k9-demo-6.2.2.1T-dhcpfix.vmdk 12 | # iosxrv-k9-demo-6.2.2.22I.vmdk 13 | VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+[^0-9]\([0-9]\+\.[0-9]\+\.[0-9]\+\(\.[0-9A-Z]\+\)\?\)\([^0-9].*\|$$\)/\1/') 14 | 15 | -include ../makefile-sanity.include 16 | -include ../makefile.include 17 | 18 | convert-image: 19 | @if [ -z "$QCOW" ]; then echo "\033[1;31mERROR:\033[0m No .qcow2 image found"; exit 1; fi 20 | @printf "\033[1;32mFound image $(QCOW)\033[0m.\n" 21 | ifeq (, $(shell which qemu-img)) 22 | @printf "\033[1;31mERROR\033[0m: qemu-img not found. Please install 'qemu-img' or 'qemu-utils'.\n"; exit 1; 23 | endif 24 | $(eval FILE_NAME := $(shell basename $(QCOW) .qcow2)) 25 | qemu-img convert -cpf qcow2 -O vmdk $(QCOW) $(FILE_NAME).vmdk 26 | 27 | 28 | -------------------------------------------------------------------------------- /xrv/README.md: -------------------------------------------------------------------------------- 1 | # vrnetlab / Cisco IOS XRv 2 | 3 | This is the vrnetlab docker image for Cisco IOS XRv. 4 | 5 | > Originally developed by Kristian Larsson (@plajjan), adapted by @hellt to be integrated with [containerlab](https://containerlab.srlinux.dev) networking. 6 | 7 | There are two flavours of virtual XR routers, XRv and XRv9000 where the latter 8 | has a much more complete forwarding plane. This is for XRv, if you have the 9 | XRv9k see the 'xrv9k' directory instead. 10 | 11 | It's not recommended to run XRv with less than 4GB of RAM. I have experienced 12 | weird issues when trying to use less RAM. 13 | 14 | ## Added in this fork 15 | 16 | * integration with containerlab as [vr-vmx](https://containerlab.srlinux.dev/manual/kinds/vr-vmx/) kind. 17 | * docker networking using `--connection-mode` flag 18 | * hostname, username and password configuration via flags 19 | * added support for [boot delay](https://containerlab.srlinux.dev/manual/vrnetlab/#boot-delay) to allow for a smooth start of the big topologies 20 | * enabled gNMI 21 | * fixes for auto image upgrade disrupted node config 22 | * base image updated to Ubuntu:20.04 23 | 24 | ## Building the docker image 25 | 26 | Obtain XRv vmkd image and put the .vmdk file in this directory and run `make docker-image`. The resulting image is called `vrnetlab/vr-xrv`. You can tag it with something else if you want, like `my-repo.example.com/vr-xrv` and then 27 | push it to your repo. The tag is the same as the version of the XRv image, so if you have iosxrv-k9-demo.vmdk-5.3.3 your final docker image will be called `vrnetlab/vr-xrv:5.3.3` 28 | 29 | * 6.1.2 30 | -------------------------------------------------------------------------------- /xrv/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/srl-labs/vrnetlab-base:0.1.0 2 | 3 | ARG IMAGE 4 | COPY $IMAGE* / 5 | COPY *.py / 6 | 7 | EXPOSE 22 161/udp 830 5000 10000-10099 57400 8 | HEALTHCHECK CMD ["/healthcheck.py"] 9 | ENTRYPOINT ["/launch.py"] 10 | -------------------------------------------------------------------------------- /xrv9k/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Kristian Larsson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /xrv9k/Makefile: -------------------------------------------------------------------------------- 1 | VENDOR=Cisco 2 | NAME=XRv9k 3 | IMAGE_FORMAT=qcow2 4 | IMAGE_GLOB=*qcow2* 5 | INSTALL=true 6 | 7 | # match versions like: 8 | # TODO: add example file names here 9 | # xrv9k-fullk9-x.vrr-6.1.3.qcow2 10 | # xrv9k-fullk9-x.vrr-6.2.1.qcow2 11 | # xrv9k-fullk9-x-7.10.1.qcow2 12 | VERSION=$(shell echo $(IMAGE) | sed -e 's/.\+[^0-9]\([0-9]\+\.[0-9]\+\.[0-9]\+\(\.[0-9A-Z]\+\)\?\)\([^0-9].*\|$$\)/\1/') 13 | 14 | -include ../makefile-sanity.include 15 | -include ../makefile.include 16 | 17 | ifeq ($(INSTALL),false) 18 | $(info Install mode disabled) 19 | else 20 | $(info Install mode enabled) 21 | -include ../makefile-install.include 22 | endif 23 | 24 | -------------------------------------------------------------------------------- /xrv9k/README.md: -------------------------------------------------------------------------------- 1 | # vrnetlab / Cisco IOS XRv9k 2 | This is the vrnetlab docker image for Cisco IOS XRv9k. 3 | 4 | > Originally developed by Kristian Larsson (@plajjan), adapted by @hellt to be integrated with [containerlab](https://containerlab.srlinux.dev) networking. 5 | 6 | There are two flavours of virtual XR routers, XRv and XRv9k where the latter 7 | has a much more complete forwarding plane. This is for XRv9k if you have the 8 | non-9k see the 'xrv' directory instead. 9 | 10 | ## Added in this fork 11 | 12 | * integration with containerlab as [vr-xrv9k](https://containerlab.srlinux.dev/manual/kinds/vr-xrv9k/) kind. 13 | * docker networking using `--connection-mode` flag 14 | * Added `vcpu` and `ram` flags to tune the resources allocated to XRv VM 15 | * hostname, username and password configuration via flags 16 | * added support for [boot delay](https://containerlab.srlinux.dev/manual/vrnetlab/#boot-delay) to allow for a smooth start of the big topologies 17 | * enabled gNMI 18 | * qemu arguments were augmented with `-machine smm=off` and `-boot order=c` values to allow XRv 7 to boot. 19 | * base image updated to Ubuntu:20.04 20 | 21 | ## Building the docker image 22 | 23 | By default the XRv9k build time will take ~20 minutes as the image undergoes a first boot installation phase. This greatly decreases boot times for your labs. 24 | 25 | The install procedure introduces some side effects as various values (such as macpool) are baked-in during this installation procedure. 26 | 27 | This can cause issues, and for this reason you may want to disable the pre-install procedure. You can do this by issuing: 28 | 29 | ``` 30 | make docker-image INSTALL=false 31 | ``` 32 | 33 | > Please note that disabling the install feature will mean the boot time of XRv9k will increase to 20+ minutes. 34 | 35 | ### Installation steps 36 | 37 | 1. Obtain the XRv9k image (.qcow2) from Cisco (or CML). A .iso version is also shipped but this is currently unsupported and you must convert the .iso to .qcow2. 38 | 39 | 2. Place the .qcow2 file in this directory 40 | 41 | 3. Perform `make docker-image` (or `make docker-image INSTALL=false`) 42 | 43 | 4. Begin labbing. The image will be listed as `vrnetlab/vr-xrv9k` 44 | 45 | > The tag is the same as the version of the XRv9k image, 46 | so if you have xrv9k-fullk9-x.vrr-6.2.1.qcow2 your final docker image will be called vr-xrv9k:6.2.1 47 | 48 | ## Known working versions 49 | 50 | It's been tested to boot and respond to SSH with: 51 | 52 | * xrv9k-fullk9-x-7.2.1.qcow2 53 | * xrv9k-fullk9-x-7.7.1.qcow2 54 | * xrv9k-fullk9-x-7.11.1.qcow2 55 | 56 | -------------------------------------------------------------------------------- /xrv9k/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/srl-labs/vrnetlab-base:0.1.0 2 | 3 | ARG IMAGE 4 | COPY $IMAGE* / 5 | COPY *.py / 6 | 7 | EXPOSE 22 80 443 161/udp 830 5000 10000-10099 57400 8 | HEALTHCHECK CMD ["/healthcheck.py"] 9 | ENTRYPOINT ["/launch.py"] 10 | --------------------------------------------------------------------------------