bigbluebutton-Github/bigbluebutton-tests/gns3/gns3-bbb.py
Brent Baccala f9e4434605
gns3 virtual network for testing (#15673)
* add scripts to create gns3 virtual network for testing

* gns3 virtual testing network: introduce declarative syntax, i.e,
declare the objects we want and only create them if they don't exist

* gns3 virtual testing network: improve node autostart logic

* gns3 virtual testing network: because of declarative syntax,
we no longer need a special --create-test-server option

* gns3 virtual testing network: allow for servers based on arbitrary git commits

* gns3 virtual testing network: avoid browsers complaining about duplicate certificate serial numbers

* gns3 virtual testing network: allocate space for two more servers

* gns3 virtual testing network: add subnets to NAT devices, so they will boot properly

* gns3 virtual testing network: allow number of ports on an Ethernet switch to be configured

* gns3 virtual testing network: move everything 100 units to the left to center PublicIP switch

* gns3 virtual testing network: restart the server by stop'ing and start'ing it
because bbb-conf --restart doesn't start nginx if it isn't running

* gns3 virtual testing network: -d switch now deletes one server;
--delete-everything does what -d used to do

* gns3 virtual testing network: new GUI image name (it's different for every user)

* gns3 virtual testing network: start node logic now marks
previously running nodes as started

* gns3 virtual testing network: add a Bash function to automate ssh logins

* gns3 virtual testing network: use "$@" instead of $* when relaying arguments to a bash command

* gns3 virtual testing network: allow ssh connections to arbitrary hosts behind NAT4

* gns3 virtual testing network: improve start node logic to start new servers when NAT1 already running

* gns3 virtual testing network: add scp command to bigbluebutton-ssh.sh

* gns3 virtual testing network: gns3-bbb.py script now accepts a list of
servers to install, or nothing to just install the infrastructure

* gns3 virtual testing network: can now copy SSL CA key and certificate
from local directory to virtual machine instead of always having the
VM create a new CA

* gns3 virtual testing network: get the quoting right in scp shell function for filenames with spaces

* gns3 virtual testing network: update README

* gns3 virtual testing network: update README

* gns3 virtual testing network: add NPDC submodule and use its gns3 import in gns3-bbb.py

* gns3 virtual testing network: use 3 network interfaces for testclient

* gns3 virtual testing network: renumber NAT devices to match interface numbering on testclient

* gns3 testing network: fix dnsmasq configuration on NAT1 so that it works for devices on the PublicIP subnet

* gns3 testing network: fix dnsmasq configuration on NAT4/5/6, but still not quite right

* gns3 testing network: "fix" dnsmasq configuration so that proxyjumped ssh works,
along with access to the servers from the testclients

* gns3 virtual testing network: label subnet switches with their CIDR prefices

* gns3 virtual testing network: move the clients and their switches a bit to the right

* gns3 virtual testing network: rename NAT1 to be the project name (default BigBlueButton)
and get rid of InternetSwitch, that I never use

* gns3 virtual testing network: allow multiple clients, all names starting with 'testclient'

* gns3 virtual testing network: factor out server NAT code, to allow for recreating deleted server NAT nodes

* gns3 virtual testing network: refactor client code (move things inside the function)
and set "dhcp-identifier: mac" on all client interfaces

* gns3 virtual testing network: make NAT1 advertise itself as ca.test into DNS domain test

* gns3 virtual testing network: refactor master gateway code into its own subroutine

* gns3 virtual testing network: better use of the gns3 library support for declarative nodes
(nodes that are declared but only created if they don't already exist)

* code cleanups; make notification_url consistently a global var;
change label on cloud from "Internet" to interface name

* typos from 39c5d6

* gns3 virtual testing network: move certificate authority from /ca to /opt/ca

* gns3 virtual testing network: move testserver.sh from / to /root

* gns3 virtual testing network: master gateway can now web proxy for servers, allowing
a web browser on the bare metal machine to access those servers

* gns3 virtual testing network: use a file() function instead of a predetermined list of files we need

* gns3 virtual testing network: add a minimal bird configuration to use OSPF to make proxy work

* gns3 virtual testing network: new-dhcp-lease.sh now picks mac addresses
based on server name, allowing multiple servers, and some bug fixes

* gns3 virtual testing network: get rid of generate NAT per-boot script (do everything at boot with iptables-persistent)

* gns3 virtual testing network: move client NAT boxes 50 coordinates to the right

* gns3 virtual testing network: autostart the nodes that were created

* gns3 virtual testing network: use 2 minute DHCP leases to make
it quicker to recover from update that replace the NAT devices

* gns3 virtual testing network: change NAT rules around because server needs to connect to itself during bbb-install

* gns3 virtual testing network: use version of NPDC that has 2 minutes timeouts
on the bare metal machine's DHCP leases

* gns3 virtual testing network: Improve the DNS configuration by configuring
the gateway to not use the bare metal machine for DNS lookups on the ".test"
domain, on either the real interface or the dummy interfaces used to
inject DNS names into the bare metal machine's DNS table.  This removes
the need to put entries into /etc/hosts (which was causing problems
when things changed IP addresses) because the gateway now consistently
uses its own DNS server (dnsmasq) to resolve the ".test" domain.

* gns3 virtual testing network: set "noninteractive" flag when updating
persistent iptables, otherwise the system tries to prompt the user
during an automatically cloud-init run (the prompts show up in
/var/log/cloud-init-output.log and stall the cloud-init run)

* gns3 virtual testing network: configure proxy to redirect http->https for the BigBlueButton servers

* gns3 virtual testing network: put some extra checks on the SSL certificates
generated for the proxy server, because if they're screwed up the apache
web server won't start at all

* gns3 virtual testing network: simplify getportrange.cgi by just having it accept GETs

* gns3 virtual testing network: two bug fixes with punching UDP ports through the proxy server
   1. if the server changed public IP addresses, a new iptables rule was not created
   2. the math was wrong; it's supposed to be a range of 100 ports, not 1000

* gns3 virtual testing network: iptables rules still weren't right on
the server's NAT gateways; connections to the gateway (128.8.8.254)
need to go through (they weren't), while only connections to the server get hairpined back

* gns3 virtual testing network: make NAT gateways listen for ssh on port 2222,
so you can connect to them easily (with a suitable ssh config file) by
just doing "ssh -p 2222 focal-260"

* gns3 virtual testing network: now that DNS is working right (read: better),
we don't need the funky shell functions for ssh and scp

* gns3 virtual testing network: updated README

* gns3 virtual testing network: switching to ISC bind/dhcpd for dynamic DNS
and dropping the proxy server and the restricted port ranges
This version works, but still has issues

* gns3 virtual testing network: don't need new-dhcp-lease.sh anymore
(it's now done by ISC dhcpd server, which has built-in support for DDNS)

* gns3 virtual testing network: DDNS server no longer requires crypto key

* gns3 virtual testing network: rearrange some comments, remove an unused script

* gns3 virtual testing network: no longer need to set a port range on the servers,
since they are directly reachable from the bare metal machine using OSPF and dynamic DNS

* gns3 virtual testing network: drop final remnants of old "NAT1"
name; it's now consistently "master gateway"

* gns3 virtual testing network: reduce DHCP lease times to ten seconds

* gns3 testing network: move testserver.sh from /root to /, because ubuntu needs permission to exec it

* gns3 virtual testing network: move client nodes a bit to the right in the GUI

* gns3 virtual testing network: simplify bird (OSPF) configuration a bit

* gns3 virtual testing network: add --public-subnet and --domain options

* gns3 virtual testing network: use most recent ubuntu-open-desktop image, if several are available

* gns3 virtual testing network: pick up the server's domain name from
the testing network now that we have a --domain option (commit f35f21)

* gns3 virtual testing network: introduce --server-subnet option

* gns3 virtual testing network: label server subnets with subnet prefix (if possible)

* gns3 virtual testing network: all calls to dpkg-reconfigure are noninteractive

* gns3 virtual testing network: turn off DHCP/DDNS conflict detection

* gns3 virtual testing network: use 120 second DHCP leases; 10 second leases were a problem

* gns3 virtual testing network: fix commands to save iptables (wasn't saving NAT table)

* gns3 virtual testing network: add a 'certbot' node running Let's Encrypt's boulder server

* gns3 virtual testing network: switch 'certbot' to use smallstep's step-ca server

* gns3 virtual testing network: first attempt to dummy letsencrypt's acme service (doesn't work)

* gns3 virtual testing network: a dummy ACME server works; server certbots use it to get SSL certificates

* gns3 virtual testing network: switch testservers to use certbot to get SSL certificates

* gns3 virtual testing network: put generateCA.sh back in (in case CA key and cert don't already exist), and remove getcert.cgi

* gns3 virtual testing network: add a standard BigBlueButton turn server

* gns3 virtual testing network: can now use 400 permission for CA key file

* gns3 virtual testing network: dnsmasq cname doesn't work with IP addresses (why should it?); use interface_name instead

* gns3 virtual testing network: install server certificate from cloud-init, rather than doing it after boot

* gns3 virtual testing network: fix certbot on master gateway so that it can update certificates as they expire

* gns3 virtual testing network: only build and/or start NAT4/5/6 if testclient is requested

* gns3 virtual testing network: generate root CA certificate on bare metal machine and install it in all VMs from there

* gns3 virtual testing network: use certbot nginx plugin instead of standalone mode

interacts better with nginx (don't have to stop and restart nginx everytime you do a certbot authentication)

* gns3 virtual testing network: add --no-nat switch to build virtual servers without NAT gateways

* gns3 virtual testing network: need to not only start step-ca, but enable it (start on every boot)

* gns3 virtual testing network: save DNS domain name in GNS3 project variables,
so we don't have to specify it on the command line every time we run this script

* gns3 virtual testing network: add --no-install switch

* gns3 virtual testing network: fix bogus help text

* gns3 virtual testing network: --delete now deletes just a specific server and its associated nodes

Old behavior (deleting nodes whose name contains a substring) is now done with --delete-substring

The old behavior could delete too much (focal-260 would match focal-260-2) and too little
(subnet switches are named by their CIDR prefix if possible)

* gns3 virtual testing network: when creating a new server, check for existing nodes in all GUI locations to be used

* gns3 virtual testing network: update comments

* gns3 virtual testing network: add return code checks when calling openssl

* gns3 virtual testing network: README update

* gns3 virtual testing network: add 'natturn' TURN server behind a NAT gateway

* gns3 virtual testing network: make TURN server observe --no-install option

* gns3 virtual testing network: only try to install 'natturn' if it doesn't already exist,
because even though it's got "declarative" procedures (you declare what you want but it
only creates it if needed), natturn's subnet will conflict with server subnets

* gns3 virtual testing network: allow Ubuntu release (18 or 20) to be set for a new server

* gns3 virtual testing network: add --repository and --install-script options
and move computation of the install script from testserver.sh to the main script

* gns3 virtual testing network: add --proxy-server option; we pass this option to bbb-install script

* add --release option to specify which BigBlueButton server release should be installed,
and improve --install-script option to accept local files or arbitrary URLs

* gns3 virtual testing network: guess version of install script instead of defaulting to 2.6

* gns3 virtual testing network: enable NAT in both directions on gateway device

* gns3 virtual testing network: check server release for validity before attempting install

* updated README and newest version of NPDC

* gns3 virtual testing network: add option to install greenlight

* gns3 virtual testing network: drop --domain switch and instead use an initialization server instead to figure out DNS domain

* gns3 virtual testing network: don't create turn or natturn devices unless specifically requested

* gns3 virtual testing network: updated README

* gns3 virtual testing network: use a newer version of NPDC that monitors console logs, so we watch the servers boot

* gns3 virtual testing network: updated README

* gns3 virtual testing network: update NPDC for backwards compatibility with Ubuntu 18

* gns3 virtual testing network: update instructions for git submodules

* gns3 virtual testing network: save public IP subnet in GNS3 project variables,
so we don't have to specify it every time the script runs

* gns3 virtual testing network: name NAT gateways more consistently

* gns3 virtual testing network: add --quiet switch

* gns3 virtual testing network: compute veth domain using initsrv and use it to label the cloud icon

* gns3 virtual testing network: improve handling of nginx hash bucket errors on install

* gns3 virtual testing network: bump NPDC version; avoid trying to print console messages on VNC consoles (it won't work)
2023-03-23 14:30:04 -04:00

1159 lines
51 KiB
Python
Executable File

#!/usr/bin/python3
#
# Script to setup a GNS3 project for testing BigBlueButton.
#
# The project will be centered around a dummy public subnet with both
# the bare metal system and most virtual nodes connected to it using
# virtual NAT gateways. Servers can be created with the --no-nat
# switch to connect them directly to the dummy public subnet.
#
# "stun.l.google.com" is mimicked with a fake DNS entry so that
# BigBlueButton servers see the dummy public subnet as their public IP
# address.
#
# smallstep.com's step-ca server is used to run an ACME Certificate
# Authority in a manner compatible with certbot, allowing bbb-install
# to run certbot normally. The CA's root certificate and key are
# expected to be in the script's directory and are created (using
# openssl) if they don't exist. The root certificate is then
# installed and trusted on all Ubuntu nodes created by this script.
#
# All the Ubuntu nodes will be configured to accept the user's public
# ssh key and entire authorized_keys file for ssh access.
#
# BigBlueButton servers with NAT gateways will be configured to forward
# port 22 to the server, while the NAT gateway listens for ssh
# on port 2222.
#
# The NAT gateway between the bare metal machine and the dummy public
# subnet is given the same name (for ssh access) as the GNS3 project
# (default "BigBlueButton"). This node also operates the step-ca and
# stun.l.google.com servers.
#
# The first time the script is run, the required infrastructure nodes
# are created. The --public-subnet switch can be specified at this
# time, and the public subnet's CIDR prefix will also be saved.
#
# If the machine running the script is configured to use an APT http
# proxy, that proxy will also be used by the created nodes.
#
# The gns3 library provides "declarative" functions that only create
# nodes if they don't already exist, and we use this feature
# throughout the script. Running the script on a pre-built network
# should change nothing. A single node can be rebuilt by deleting it
# and rerunning the script.
# RUNTIME DEPENDENCIES
#
# genisoimage must be installed
# openssl must be installed
#
# USAGE
#
# ./gns3-bbb.py focal-260 (to install a released server)
# ./gns3-bbb.py focal-e41349 (to install a server based on a git commit)
# ./gns3-bbb.py testclient (to install a test client)
#
# By default, the GNS3 project is called "BigBlueButton". If the
# project infrastructure (switches and NAT gateways) doesn't exist, it
# will be created.
#
# The '-d' option deletes a single server and its associated subnet and NAT nodes.
#
# The '--delete-everything' switch deletes EVERYTHING in an existing project.
#
# See the comments in NPDC/GNS3/gns3.py for more options and how
# authentication is done.
#
# We use an Ubuntu cloud image and a client image created using the
# GNS3/ubuntu.py script in BrentBaccala's NPDC github repository.
from NPDC.GNS3 import gns3
import sys
import os
import json
import ipaddress
import requests
import argparse
import subprocess
SSH_AUTHORIZED_KEYS_FILES = ['~/.ssh/id_rsa.pub', "~/.ssh/authorized_keys"]
# These are bootable images provided by Canonical, Inc, that have the cloud-init package
# installed. When booted in a VM, cloud-init will configure them based on configuration
# provided (in our case) on a ISO image attached to a virtual CD-ROM device.
#
# Pick up the latest versions from here:
#
# https://cloud-images.ubuntu.com/releases/bionic/release/ubuntu-18.04-server-cloudimg-amd64.img
# https://cloud-images.ubuntu.com/releases/focal/release/ubuntu-20.04-server-cloudimg-amd64.img
# https://cloud-images.ubuntu.com/releases/jammy/release/ubuntu-22.04-server-cloudimg-amd64.img
#
# Updated versions are released several times a month. If you don't have the latest version,
# this file's cloud-init configuration will run a package update (if package_upgrade is True).
cloud_images = {
22: 'ubuntu-22.04-server-cloudimg-amd64.img',
20: 'ubuntu-20.04-server-cloudimg-amd64.img',
18: 'ubuntu-18.04-server-cloudimg-amd64.img'
}
ubuntu_release = {
20: 'focal',
18: 'bionic'
}
# We currently use Ubuntu 20 for everything
cloud_image = cloud_images[20]
# set to True to immediately upgrade to latest package versions; slows thing down a bit
package_upgrade = False
# Parse the command line options
parser = argparse.ArgumentParser(parents=[gns3.parser('BigBlueButton')],
description='Start an BigBlueButton test network in GNS3',
formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('--client-image', type=str,
help='Ubuntu image to be used for test clients')
parser.add_argument('--public-subnet', type=str,
help='public IP subnet to be "stolen" for our use (default 128.8.8.0/24)')
parser.add_argument('--server-subnet', type=str, default='192.168.1.0/24',
help='private IP subnet to be used for NAT-ed BBB server (default 192.168.1.0/24)')
parser.add_argument('-r', '--repository', type=str,
help='package repository to be used for BigBlueButton server install')
parser.add_argument('-g', '--greenlight', action='store_true',
help='install Greenlight')
parser.add_argument('--ubuntu-release', type=int, default=20,
help='Ubuntu release (18 or 20; default 20) to be used for BigBlueButton server install')
parser.add_argument('--release', type=str,
help='BigBlueButton release to be used for BigBlueButton server install (default is server hostname)')
parser.add_argument('--install-script', type=str,
help='install script to be used for BigBlueButton server install\n'
+ 'can be a local file, a URL, or a filename on https://ubuntu.bigbluebutton.org/')
parser.add_argument('--proxy-server', type=str,
help='proxy server to be passed to BigBlueButton server install script')
parser.add_argument('--no-nat', action='store_true',
help='install BBB server without a NAT gateway')
parser.add_argument('--no-install', action='store_true',
help="don't run bbb-install script on BBB server")
parser.add_argument('--quiet', default=False, action='store_true',
help="don't print console logs to stdout")
parser.add_argument('--delete', type=str,
help="delete a BBB server and its associated subnet and NAT nodes")
parser.add_argument('version', nargs='*',
help="""version of BigBlueButton server to be installed
(focal-250, focal-25-dev, focal-260, focal-GITREV)
version names starting with 'testclient' install clients""")
args = parser.parse_args()
server_subnet = ipaddress.ip_network(args.server_subnet)
if not server_subnet.is_private:
print(f"{args.server_subnet} must be a private IP prefix")
exit(1)
# Various scripts we'll use
#
# How to open files in the same directory as the script, from https://stackoverflow.com/a/4060259/1493790
__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__)))
def file(fn):
with open(os.path.join(__location__, fn)) as f:
return f.read()
# Check if our SSL root Certificate Authority key and certificate are available, create them if not
ssl_root_key_fn = os.path.join(__location__, 'bbb-dev-ca.key')
ssl_root_crt_fn = os.path.join(__location__, 'bbb-dev-ca.crt')
try:
with open(ssl_root_key_fn) as f:
ssl_root_key = f.read()
except:
subprocess.run(f'openssl genrsa -out {ssl_root_key_fn} 2048'.split()).check_returncode()
with open(ssl_root_key_fn) as f:
ssl_root_key = f.read()
try:
with open(ssl_root_crt_fn) as f:
ssl_root_crt = f.read()
except:
subprocess.run(f'openssl req -x509 -new -nodes -key {ssl_root_key_fn} -sha256 -days 1460 -out {ssl_root_crt_fn} -subj /C=CA/ST=BBB/L=BBB/O=BBB/OU=BBB/CN=BBB-DEV'.split()).check_returncode()
with open(ssl_root_crt_fn) as f:
ssl_root_crt = f.read()
# Open the GNS3 server
gns3_server, gns3_project = gns3.open_project_with_standard_options(args)
# Extract and validate project's public subnet
gns3_variables = gns3_project.variables()
if 'public-subnet' in gns3_variables:
if not args.public_subnet:
args.public_subnet = gns3_variables['public-subnet']
elif args.public_subnet != gns3_variables['public-subnet']:
print(f"Public subnet '{args.public_subnet}' doesn't match project's public subnet '{gns3_variables['public-subnet']}'")
exit(1)
else:
if not args.public_subnet:
args.public_subnet = '128.8.8.0/24'
gns3_variables['public-subnet'] = args.public_subnet
gns3_project.set_variables(gns3_variables)
public_subnet = ipaddress.ip_network(args.public_subnet)
if not public_subnet.is_global:
print(f"{args.public_subnet} must be a public IP prefix")
exit(1)
# Delete a device (and its associated nodes) if that's what we were requested to do
if args.delete:
# currently, the project's delete method doesn't complain if nothing matches,
# so we can just do this, even though some of these devices might not exist
# (if the device was created without NAT)
gns3_project.delete(args.delete)
gns3_project.delete(args.delete + '-NAT')
# If the server's subnet was named by its CIDR block, we now have an orphan switch
switches = set(node['node_id'] for node in gns3_project.nodes() if node['node_type'] == 'ethernet_switch')
linked_switches = set(node['node_id'] for link in gns3_project.links() for node in link['nodes'])
for orphan in switches.difference(linked_switches):
gns3_project.delete(orphan)
exit(0)
# Make sure the cloud and client images exist on the GNS3 server
if not cloud_image in gns3_server.images():
print(f"{cloud_image} isn't available on GNS3 server {args.host}")
exit(1)
if not cloud_images[args.ubuntu_release] in gns3_server.images():
print(f"{cloud_images[args.ubuntu_release]} isn't available on GNS3 server {args.host}")
exit(1)
# An Ubuntu 20 image created by GNS3/ubuntu.py in Brent Baccala's NPDC github repository
#
# This image comes with the console GUI pre-installed, which cloud_image lacks.
#
# Default is the most recent ubuntu-open-desktop image (last in the sort order)
if any(v.startswith('testclient') for v in args.version):
if args.client_image:
assert args.client_image in gns3_server.images()
else:
args.client_image = sorted(image for image in gns3_server.images() if image.startswith('ubuntu-open-desktop'))[-1]
# Find out if the system we're running on is configured to use an apt proxy.
apt_proxy = None
apt_config_command = ['apt-config', '--format', '%f %v%n', 'dump']
apt_config_proc = subprocess.Popen(apt_config_command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
for config_line in apt_config_proc.stdout.read().decode().split('\n'):
if ' ' in config_line:
key,value = config_line.split(' ', 1)
if key == 'Acquire::http::Proxy':
apt_proxy = value
# Obtain any credentials to authenticate ourself to the VM
ssh_authorized_keys = []
for keyfilename in SSH_AUTHORIZED_KEYS_FILES:
keyfilename = os.path.expanduser(keyfilename)
if os.path.exists(keyfilename):
with open(keyfilename) as f:
for l in f.read().split('\n'):
if l.startswith('ssh-'):
ssh_authorized_keys.append(l)
### FUNCTIONS TO CREATE VARIOUS KINDS OF GNS3 OBJECTS
def master_gateway(hostname, public_subnet=None, x=0, y=0):
# A NAT gateway between our public "Internet" and the actual Internet
#
# BBB's default STUN server is stun.l.google.com:19302, so we run
# coturn and configure the master gateway to mimic it.
#
# It also operates an ACME server and mimics acme-v02.api.letsencrypt.org,
# so our test servers can run certbot to get their SSL certificates.
if not isinstance(public_subnet, ipaddress.IPv4Network):
raise TypeError("master_gateway() requires public_subnet to be an ipaddress.IPv4Network")
# Use the first host address on the subnet for our master gateway
# The rest of them will be available for assignment with DHCP
public_subnet_hosts = list(public_subnet.hosts())
master_gateway_address = str(public_subnet_hosts[0])
network_config = {'version': 2,
'ethernets': {'ens4': {'dhcp4': 'on', 'dhcp-identifier': 'mac' },
'ens5': {'addresses': [f'{master_gateway_address}/{public_subnet.prefixlen}'] }
}}
# resolver1.opendns.com is used by bbb-install to determine external IP address
# stun.l.google.com is used by clients to determine their external IP address (and maybe the servers too)
# I'd like {acme_server} to be a cname, too, but that isn't good enough (see below)
acme_server="acme-v02.api.letsencrypt.org"
dnsmasq_conf = f"""
listen-address={master_gateway_address}
bind-dynamic
interface-name=resolver1.opendns.com,ens5
interface-name=stun.l.google.com,ens5
"""
# 120 second DHCP lease times because I change things around so
# much in the virtual network. I tried 10 second lease times,
# but had problems with OSPF route flaps for 10 seconds lease
# time on the bare metal system, so it's probably best to
# use 120 second leases throughout.
dhcpd_conf = f"""
ddns-updates on;
ddns-update-style standard;
update-optimization off;
authoritative;
# double curlies because it's a Python f-string
zone {args.domain}. {{ }}
# Update conflict detection prevents a DHCP server from changing
# a DNS entry that it didn't create. Since I often delete the
# virtual network devices and recreate them, it's best to keep
# this turned off.
update-conflict-detection off;
allow unknown-clients;
default-lease-time 120;
max-lease-time 120;
log-facility local7;
subnet {str(public_subnet.network_address)} netmask {str(public_subnet.netmask)} {{
range {str(public_subnet_hosts[1])} {str(public_subnet_hosts[-1])};
option subnet-mask {str(public_subnet.netmask)};
option domain-name-servers {master_gateway_address};
option domain-name "{args.domain}";
option routers {master_gateway_address};
option broadcast-address {str(public_subnet.broadcast_address)};
}}
"""
# step-ca's JSON configuration file
ca = {
"root": "/opt/ca/bbb-dev-ca.crt",
"crt": "/opt/ca/bbb-dev-ca.crt",
"key": "/opt/ca/bbb-dev-ca.key",
"address": ":8000",
"dnsNames": [ acme_server, hostname, f'{hostname}.{args.domain}', 'localhost' ],
"logger": {
"format": "text"
},
"db": {
"type": "badgerv2",
"dataSource": "/opt/ca/db"
},
"authority": {
"provisioners": [
{
"type": "ACME",
"name": "acme"
}
]
}
}
# nginx is used to redirect /directory to /acme/acme/directory, so that certbot can be used
# without a server argument. It's configured to mimic {acme_server}
nginx_site=f"""
server {{
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name {acme_server};
ssl_certificate /etc/letsencrypt/live/{acme_server}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{acme_server}/privkey.pem;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_dhparam /etc/nginx/ssl/dhp-4096.pem;
access_log /var/log/nginx/access.log;
return 301 https://{acme_server}:8000/acme/acme$request_uri;
}}
"""
# The master gateway's name is the name of the project, and that's
# what it announces itself as to DHCP and DNS.
user_data = {'hostname': hostname,
'packages': ['dnsmasq', 'isc-dhcp-server', 'coturn', 'bird', 'iptables-persistent',
'nginx', 'python3-certbot-nginx'],
'package_upgrade': package_upgrade,
'users': [{'name': 'ubuntu',
'plain_text_passwd': 'ubuntu',
'ssh_authorized_keys': ssh_authorized_keys,
'lock_passwd': False,
'shell': '/bin/bash',
'sudo': 'ALL=(ALL) NOPASSWD:ALL',
}],
'write_files': [
{'path': '/etc/dhcp/dhcpd.conf',
'permissions': '0644',
'content': dhcpd_conf
},
{'path': '/etc/dnsmasq.d/gns3-bbb',
'permissions': '0644',
'content': dnsmasq_conf
},
{'path': '/etc/bird/bird.conf',
'permissions': '0644',
'content': file('bird.conf')
},
{'path': '/etc/systemd/system/step-ca.service',
'permissions': '0644',
'content': file('step-ca.service')
},
{'path': '/etc/nginx/sites-available/redirect-ca',
'permissions': '0644',
'content': nginx_site
},
{'path': '/opt/ca/ca.json',
'permissions': '0755',
'content': json.dumps(ca)
},
{'path': '/opt/ca/bbb-dev-ca.key',
'permissions': '0400',
'content': ssl_root_key
},
{'path': '/opt/ca/bbb-dev-ca.crt',
'permissions': '0444',
'content': ssl_root_crt
},
{'path': '/usr/local/share/ca-certificates/bbb-dev/bbb-dev-ca.crt',
'permissions': '0444',
'content': ssl_root_crt
},
# /var/www/html/bbb-dev-ca.crt should no longer be used for anything
{'path': '/var/www/html/bbb-dev-ca.crt',
'permissions': '0444',
'content': ssl_root_crt
},
],
'runcmd': [
# configure coturn to listen on 19302, like stun.l.google.com
f'echo aux-server={master_gateway_address}:19302 >> /etc/turnserver.conf',
'systemctl restart coturn',
# add CA root certificate from /usr/local/share/ca-certificates
'update-ca-certificates',
# now everything we need to operate a certificate authority
"wget -q https://dl.step.sm/gh-release/certificates/docs-ca-install/v0.21.0/step-ca_0.21.0_amd64.deb",
"dpkg -i step-ca_0.21.0_amd64.deb",
"rm step-ca_0.21.0_amd64.deb",
# putting {acme_server} in dnsmasq as a cname isn't enough,
# because the local step-ca server doesn't do hostname lookups using dnsmasq,
# and it will contact {acme_server} to do verification for certbot below
f'echo {master_gateway_address} {acme_server} >> /etc/hosts',
"systemctl enable step-ca",
"systemctl start step-ca",
"bash -c 'while ! nc -z localhost 8000; do sleep 1; done'",
# get a certificate for nginx
f"certbot --server https://localhost:8000/acme/acme/directory certonly --nginx --non-interactive --agree-tos -d {acme_server} -m root@localhost",
# complete nginx ssl configuration and restart nginx
"mkdir -p /etc/nginx/ssl",
"openssl dhparam -dsaparam -out /etc/nginx/ssl/dhp-4096.pem 4096",
"ln -s /etc/nginx/sites-available/redirect-ca /etc/nginx/sites-enabled/",
"systemctl restart nginx",
# enable packet forwarding
'sysctl net.ipv4.ip_forward=1',
'sed -i /net.ipv4.ip_forward=1/s/^#// /etc/sysctl.conf',
# enable NAT (both directions)
# Connections into the testing network should appear to come from this gateway
# on our public "Internet" to allow outside clients to connect to the servers
# Connections out of the testing network should appear to come from this gateway
# on its bare metal facing virtual subnet for things like package installs
'iptables -t nat -A POSTROUTING -o ens4 -j MASQUERADE',
'iptables -t nat -A POSTROUTING -o ens5 -j MASQUERADE',
'netfilter-persistent save',
],
}
if notification_url:
user_data['phone_home'] = {'url': notification_url, 'tries': 1}
# If the system we're running on is configured to use an apt proxy, use it for the NAT instance as well.
#
# This will break things if the instance can't reach the proxy.
if apt_proxy:
user_data['apt'] = {'http_proxy': apt_proxy}
return gns3_project.ubuntu_node(user_data, image=cloud_image, network_config=network_config,
ram=1024, disk=4096, ethernets=2, x=x, y=y)
# Initialization server - only used once when project starts to discover project's DNS name
#
# This server has almost no configuration at all. Once it boots, we can ssh in to find the DNS domain,
# which will be used to build the master gateway node, since we need the DNS domain to build its config files.
#
# Issues:
# - if we couldn't find a notification URL, probably will hang. Should at least warn. Improve to fallback on polling
# - assumes that this script has access to at least one of the authorized keys
def initialization_server(hostname, x=0, y=0):
# Use dhcp-identifier: mac because I'm still having problems with
# cloned GNS3 ubuntu nodes using the same client identifiers; it's
# a cloud-init issue.
network_config = {'version': 2,
'ethernets': {'ens4': {'dhcp4': 'on', 'dhcp-identifier': 'mac' },
}}
user_data = {'hostname': hostname,
'ssh_authorized_keys': ssh_authorized_keys
}
if notification_url:
user_data['phone_home'] = {'url': notification_url, 'tries': 1}
return gns3_project.ubuntu_node(user_data, image=cloud_image, network_config=network_config,
ram=512, x=x, y=y)
# BigBlueButton TURN server
def turn_server(hostname, x=0, y=0):
# Use dhcp-identifier: mac because I'm still having problems with
# cloned GNS3 ubuntu nodes using the same client identifiers; it's
# a cloud-init issue.
network_config = {'version': 2,
'ethernets': {'ens4': {'dhcp4': 'on', 'dhcp-identifier': 'mac' },
}}
user_data = {'hostname': hostname,
'package_upgrade': package_upgrade,
'users': [{'name': 'ubuntu',
'plain_text_passwd': 'ubuntu',
'ssh_authorized_keys': ssh_authorized_keys,
'lock_passwd': False,
'shell': '/bin/bash',
'sudo': 'ALL=(ALL) NOPASSWD:ALL',
}],
'write_files': [
{'path': '/usr/local/share/ca-certificates/bbb-dev/bbb-dev-ca.crt',
'permissions': '0444',
'content': ssl_root_crt
},
],
'runcmd': [
'update-ca-certificates',
]
}
if not args.no_install:
user_data['runcmd'].append(f'wget -qO- https://ubuntu.bigbluebutton.org/bbb-install.sh | sudo bash -s -- -c {hostname}.{args.domain}:secret -e root@{hostname}.{args.domain}')
# If the system we're running on is configured to use an apt proxy, use it for the clients as well.
#
# This will break things if the instance can't reach the proxy.
if apt_proxy:
user_data['apt'] = {'http_proxy': apt_proxy}
user_data['write_files'].append(
{'path': '/etc/apt/apt.conf.d/proxy.conf',
'permissions': '0644',
'content': f'Acquire::http::Proxy "{apt_proxy}";\n'
}
)
return gns3_project.ubuntu_node(user_data, image=cloud_image, network_config=network_config,
ram=1024, disk=4096, x=x, y=y)
# BigBlueButton test clients
def BBB_client(hostname, x=0, y=0):
# Use dhcp-identifier: mac because I'm still having problems with
# cloned GNS3 ubuntu nodes using the same client identifiers; it's
# a cloud-init issue.
network_config = {'version': 2,
'ethernets': {'ens4': {'dhcp4': 'on', 'dhcp-identifier': 'mac', 'optional': True },
'ens5': {'dhcp4': 'on', 'dhcp-identifier': 'mac', 'optional': True },
'ens6': {'dhcp4': 'on', 'dhcp-identifier': 'mac', 'optional': True },
}}
user_data = {'hostname': hostname,
'package_upgrade': package_upgrade,
# We can safely writing files into /home/ubuntu without worrying about its permissions changing
# because this is a chained cloud-init that has already booted once and created /home/ubuntu.
# If this were an initial boot of a cloud image from Canonical, putting files into /home/ubuntu
# would cause that directory's permissions to change to root.root, which would be a problem.
'write_files': [
{'path': '/home/ubuntu/testclient.sh',
'permissions': '0755',
'content': file('testclient.sh')
},
{'path': '/usr/local/share/ca-certificates/bbb-dev/bbb-dev-ca.crt',
'permissions': '0444',
'content': ssl_root_crt
},
],
'runcmd': [
'update-ca-certificates',
'su ubuntu -c /home/ubuntu/testclient.sh',
],
}
# If the system we're running on is configured to use an apt proxy, use it for the clients as well.
#
# This will break things if the instance can't reach the proxy.
if apt_proxy:
user_data['apt'] = {'http_proxy': apt_proxy}
user_data['write_files'].append(
{'path': '/etc/apt/apt.conf.d/proxy.conf',
'permissions': '0644',
'content': f'Acquire::http::Proxy "{apt_proxy}";\n'
}
)
# If git user name/email has been set on the current system, it's convenient to set them on the client, too.
try:
git_user_name = subprocess.check_output('git config --get user.name'.split()).strip().decode()
git_user_email = subprocess.check_output('git config --get user.email'.split()).strip().decode()
if git_user_name != '':
user_data['runcmd'].append(f'su ubuntu -c \'git config --global --add user.name "{git_user_name}"\'')
if git_user_email != '':
user_data['runcmd'].append(f'su ubuntu -c \'git config --global --add user.email "{git_user_email}"\'')
except subprocess.CalledProcessError:
pass
# need this many virtual CPUs to run the stress tests, which stress the client perhaps more than the server
return gns3_project.ubuntu_node(user_data, image=args.client_image, network_config=network_config,
cpus=12, ram=8192, disk=8192, ethernets=3, vnc=True, x=x, y=y)
# TWO KINDS OF NAT GATEWAYS
# A NAT gateway configured for test clients.
def client_NAT_gateway(hostname, x=0, y=0, nat_interface='192.168.1.1/24'):
interface = ipaddress.ip_interface(nat_interface)
hosts = list(interface.network.hosts())
assert hosts[0] == interface.ip
# We want DNS service on the NAT gateways so that when we proxy ssh through
# them, they can resolve testclient addresses.
# This is really broken, but we have to make dnsmasq authoritative
# for its DNS domain, or it will hang indefinitely on AAAA
# lookups. See the stackexchange link below. We make it
# authoritative for the test-client domain (so we can lookup
# testclient), but set testclient's domain search path to test,
# so it can lookup names like focal-250.
#
# If we didn't set anything authoritative, AAAA lookups would
# hang. If we set the test domain authoritative, server lookups
# like focal-250 would return nothing, because they're registered
# with the master gateway, not this one.
dnsmasq_conf = f"""
listen-address={hosts[0]}
bind-interfaces
dhcp-range={hosts[1]},{hosts[-1]},2m
# Don't use dhcp-sequential-ip; assign IP addresses based on hash of client MAC
# Otherwise, the IP address change around on reboots, and BBB doesn't like that.
# dhcp-sequential-ip
dhcp-authoritative
# The testclient will register itself with DHCP as 'testclient'
# This option will cause dnsmasq to announce it in DNS as 'testclient.test-client'
domain=test-client
# Make the DNS server authoritative for these domains, or else it will hang
# See https://unix.stackexchange.com/questions/720570
auth-zone=test-client
auth-zone=in-addr.arpa
# auth-server is required when auth-zone is defined; use a non-existent dummy server
auth-server=dns.test-client
# tell the test clients to search args.domain and not test-client
dhcp-option = option:domain-search,{args.domain}
"""
network_config = {'version': 2,
'ethernets': {'ens4': {'dhcp4': 'on'},
'ens5': {'addresses': [nat_interface],
'nameservers': {'search' : ['test-client'], 'addresses' : [str(interface.ip)]}},
}}
user_data = {'hostname': hostname,
'packages': ['dnsmasq', 'iptables-persistent'],
'package_upgrade': package_upgrade,
'users': [{'name': 'ubuntu',
'plain_text_passwd': 'ubuntu',
'ssh_authorized_keys': ssh_authorized_keys,
'lock_passwd': False,
'shell': '/bin/bash',
'sudo': 'ALL=(ALL) NOPASSWD:ALL',
}],
'write_files': [
{'path': '/etc/dnsmasq.d/gns3-bbb',
'permissions': '0644',
'content': dnsmasq_conf
},
{'path': '/usr/local/share/ca-certificates/bbb-dev/bbb-dev-ca.crt',
'permissions': '0444',
'content': ssl_root_crt
},
],
'runcmd': [
'update-ca-certificates',
# enable packet forwarding
'sysctl net.ipv4.ip_forward=1',
'sed -i /net.ipv4.ip_forward=1/s/^#// /etc/sysctl.conf',
# enable NAT
'iptables -t nat -A POSTROUTING -o ens4 -j MASQUERADE',
'netfilter-persistent save',
],
}
if notification_url:
user_data['phone_home'] = {'url': notification_url, 'tries': 1}
# If the system we're running on is configured to use an apt proxy, use it for the NAT instance as well.
#
# This will break things if the instance can't reach the proxy.
if apt_proxy:
user_data['apt'] = {'http_proxy': apt_proxy}
return gns3_project.ubuntu_node(user_data, image=cloud_image, network_config=network_config,
ram=1024, disk=4096, ethernets=2, x=x, y=y)
# A NAT gateway for some kind of server (either BigBlueButton or natturn)
# between our public "Internet" and a server's private subnet
#
# 'hostname' should be of the form 'server-NAT', and the NAT gateway will
# present itself in DHCP/DNS using the server's name
def server_NAT_gateway(hostname, public_subnet=None, x=100, y=100):
assert(hostname.endswith('-NAT'))
hostname = hostname[:-4]
if not isinstance(public_subnet, ipaddress.IPv4Network):
raise TypeError("server_NAT_gateway() requires public_subnet to be an ipaddress.IPv4Network")
# Use the first host address on the subnet for our master gateway
# The rest of them will be available for assignment with DHCP
public_subnet_hosts = list(public_subnet.hosts())
master_gateway_address = str(public_subnet_hosts[0])
server_subnet_hosts = list(server_subnet.hosts())
server_nat_address = str(server_subnet_hosts[0])
server_address = str(server_subnet_hosts[1])
first_dhcp_address = str(server_subnet_hosts[2])
last_dhcp_address = str(server_subnet_hosts[-1])
dnsmasq_conf = f"""
listen-address={server_nat_address}
bind-interfaces
dhcp-host={hostname},{server_address}
dhcp-range={first_dhcp_address},{last_dhcp_address},2m
# Don't use dhcp-sequential-ip; assign IP addresses based on hash of client MAC
# Otherwise, the IP address change around on reboots, and BBB doesn't like that.
# dhcp-sequential-ip
dhcp-authoritative
# tell the test servers to search args.domain
dhcp-option = option:domain-search,{args.domain}
"""
# Note that 'hostname-NAT' announces itself into DHCP as 'hostname'
#
# This is so that the testclients will connect to the NAT gateway to reach the server.
network_config = {'version': 2,
'ethernets': {'ens4': {'dhcp4': 'on',
'dhcp4-overrides' : {'hostname' : hostname}},
'ens5': {'addresses': [f'{server_nat_address}/{server_subnet.prefixlen}']},
}}
user_data = {'hostname': hostname + '-NAT',
'packages': ['dnsmasq', 'iptables-persistent'],
'package_upgrade': package_upgrade,
'users': [{'name': 'ubuntu',
'plain_text_passwd': 'ubuntu',
'ssh_authorized_keys': ssh_authorized_keys,
'lock_passwd': False,
'shell': '/bin/bash',
'sudo': 'ALL=(ALL) NOPASSWD:ALL',
}],
'write_files': [
{'path': '/etc/dnsmasq.d/gns3-bbb',
'permissions': '0644',
'content': dnsmasq_conf
},
{'path': '/usr/local/share/ca-certificates/bbb-dev/bbb-dev-ca.crt',
'permissions': '0444',
'content': ssl_root_crt
},
# Our NAT configuration tunnels port 22 through to the BigBlueButton server
# Use port 2222 for ssh connections to the server's NAT gateway
{'path': '/etc/ssh/sshd_config.d/port.conf',
'permissions': '0644',
'content': "Port 2222"
},
],
'runcmd': [
'update-ca-certificates',
# enable packet forwarding
'sysctl net.ipv4.ip_forward=1',
'sed -i /net.ipv4.ip_forward=1/s/^#// /etc/sysctl.conf',
# enable NAT
'iptables -t nat -A POSTROUTING -o ens4 -j MASQUERADE',
# These NAT statements assume that the server is on server_address, and relay ssh, web and UDP traffic to it
# We ensure that the server is on server_address with a dhcp-host statement in /etc/dnsmasq.conf
f'iptables -t nat -A PREROUTING -p tcp -i ens4 --dport 22 -j DNAT --to-destination {server_address}',
f'iptables -t nat -A PREROUTING -p tcp -i ens4 --dport 80 -j DNAT --to-destination {server_address}',
f'iptables -t nat -A PREROUTING -p tcp -i ens4 --dport 443 -j DNAT --to-destination {server_address}',
# port 3478 is TURN; this rule is for NAT gateways fronting TURN servers
f'iptables -t nat -A PREROUTING -p tcp -i ens4 --dport 3478 -j DNAT --to-destination {server_address}',
f'iptables -t nat -A PREROUTING -p udp -i ens4 --dport 16384:32768 -j DNAT --to-destination {server_address}',
# Also, bbb-install requires the server to have the ability to connect to itself using its DNS name,
# - we don't know our own IP address at this point
# - rule has to be on PREROUTING, else we can't use DNAT
# - since it's on PREROUTING, we can't use -o lo
# - so grab traffic bound for everything on the public subnet except the master gateway,
# which we need to connect to for certbot to work
f'iptables -t nat -A PREROUTING -p tcp -d {master_gateway_address} --dport 80 -j ACCEPT',
f'iptables -t nat -A PREROUTING -p tcp -d {master_gateway_address} --dport 443 -j ACCEPT',
f'iptables -t nat -A PREROUTING -p tcp -d {public_subnet.with_prefixlen} --dport 80 -j DNAT --to-destination {server_address}',
f'iptables -t nat -A PREROUTING -p tcp -d {public_subnet.with_prefixlen} --dport 443 -j DNAT --to-destination {server_address}',
# hairpin case - we need to rewrite the source address before sending the packets back to the server
# no hairpin on port 22; we can ssh into the server, then ssh back to the NAT gateway if needed
f'iptables -t nat -A POSTROUTING -s {server_subnet.with_prefixlen} -d {server_address} -p tcp --dport 80 -j MASQUERADE',
f'iptables -t nat -A POSTROUTING -s {server_subnet.with_prefixlen} -d {server_address} -p tcp --dport 443 -j MASQUERADE',
f'iptables -t nat -A POSTROUTING -s {server_subnet.with_prefixlen} -d {server_address} -p udp --dport 16384:32768 -j MASQUERADE',
'netfilter-persistent save',
],
}
if notification_url:
user_data['phone_home'] = {'url': notification_url, 'tries': 1}
# If the system we're running on is configured to use an apt proxy, use it for the NAT instance as well.
#
# This will break things if the instance can't reach the proxy.
if apt_proxy:
user_data['apt'] = {'http_proxy': apt_proxy}
return gns3_project.ubuntu_node(user_data, image=cloud_image, network_config=network_config,
ram=1024, disk=4096, ethernets=2, x=x, y=y)
# BIG BLUE BUTTON SERVERS
def BBB_server_standalone(hostname, x=100, y=300):
# A BigBlueButton server without an associated NAT gateway
network_config = {'version': 2, 'ethernets': {'ens4': {'dhcp4': 'on' }}}
if not args.release:
args.release = hostname
if not args.install_script:
if '25' in args.release:
args.install_script = 'bbb-install-2.5.sh'
elif '26' in args.release:
args.install_script = 'bbb-install-2.6.sh'
else:
print("Can't guess which install script version to use")
exit(1)
if args.install_script.startswith('http:') or args.install_script.startswith('https:'):
install_script_request = requests.get(args.install_script)
install_script_request.raise_for_status()
install_script = install_script_request.text
elif '/' in args.install_script:
install_script = file(args.install_script)
else:
install_script_request = requests.get(f"https://ubuntu.bigbluebutton.org/{args.install_script}")
install_script_request.raise_for_status()
install_script = install_script_request.text
user_data = {'hostname': hostname,
'package_upgrade': package_upgrade,
'users': [{'name': 'ubuntu',
'plain_text_passwd': 'ubuntu',
'ssh_authorized_keys': ssh_authorized_keys,
'lock_passwd': False,
'shell': '/bin/bash',
'sudo': 'ALL=(ALL) NOPASSWD:ALL',
}],
# I'd like to put these files in /home/ubuntu, but then /home/ubuntu will be owned by root
# Alternately, I'd put this in /root, but then user ubuntu can't read it
'write_files': [
{'path': '/testserver.sh',
'permissions': '0755',
'content': file('testserver.sh')
},
{'path': '/bbb-install.sh',
'permissions': '0755',
'content': install_script
},
{'path': '/usr/local/share/ca-certificates/bbb-dev/bbb-dev-ca.crt',
'permissions': '0444',
'content': ssl_root_crt
},
],
'runcmd': [
# add CA root certificate from /usr/local/share/ca-certificates
'update-ca-certificates',
],
}
if not args.no_install:
install_options = []
if args.repository:
install_options.append(f'-r {args.repository}')
repo = args.repository
else:
repo = 'ubuntu.bigbluebutton.org'
distro = ubuntu_release[args.ubuntu_release]
try:
requests.head(f'https://{repo}/{args.release}/dists/bigbluebutton-{distro}/Release.gpg').raise_for_status()
except requests.exceptions.HTTPError:
print(f'Release {args.release} does not exist on {repo} for distribution {distro}')
exit(1)
if args.proxy_server:
install_options.append(f'-p {args.proxy_server}')
if args.greenlight:
install_options.append('-g')
install_options_str = ' '.join(install_options)
user_data['runcmd'].append(f'sudo -u ubuntu RELEASE="{args.release}" INSTALL_OPTIONS="{install_options_str}" /testserver.sh')
if notification_url:
user_data['phone_home'] = {'url': notification_url, 'tries': 1}
# If the system we're running on is configured to use an apt proxy, use it for the server as well.
#
# This will break things if the instance can't reach the proxy.
if apt_proxy:
user_data['apt'] = {'http_proxy': apt_proxy}
user_data['write_files'].append(
{'path': '/etc/apt/apt.conf.d/proxy.conf',
'permissions': '0644',
'content': f'Acquire::http::Proxy "{apt_proxy}";\n'
}
)
return gns3_project.ubuntu_node(user_data, image=cloud_images[args.ubuntu_release], network_config=network_config,
cpus=4, ram=8192, disk=16384, x=x, y=y)
# BBB server with optional attached NAT gateway
def BBB_server(name, x=100, public_subnet=None, depends_on=None):
server = BBB_server_standalone(name, x=x, y=300)
if args.no_nat:
gns3_project.link(server, 0, PublicIP_switch)
if depends_on:
gns3_project.depends_on(server, depends_on)
else:
if server_subnet.with_prefixlen not in gns3_project.node_names():
switch = gns3_project.switch(server_subnet.with_prefixlen, x=x, y=200)
else:
# can't have two gns3 nodes with the same name, so do this instead
switch = gns3_project.switch(name + '-subnet', x=x, y=200)
server_nat = server_NAT_gateway(name + '-NAT', public_subnet=public_subnet, x=x, y=100)
gns3_project.link(server_nat, 0, PublicIP_switch)
gns3_project.link(server_nat, 1, switch)
gns3_project.link(server, 0, switch)
gns3_project.depends_on(server, server_nat)
if depends_on:
gns3_project.depends_on(server_nat, depends_on)
return server
# THE VIRTUAL NETWORK
# Create a GNS3 "cloud" for Internet access.
#
# It's done early in the script like this so that the gns3 library
# knows which interface we're using, because it might need that
# information to construct a notification URL (a global variable).
internet = gns3_project.cloud(gns3_variables.get('subnet', args.interface), args.interface, x=-500, y=0)
notification_url = gns3_project.notification_url()
# Either extract DNS domain from GNS3 variables, or use an init server
# to determine the DNS domain and then set it in the project variables
# for future reference.
if 'domain' in gns3_variables:
args.domain = gns3_variables['domain']
if 'domain' not in gns3_variables or 'initsrv' in args.version:
init_server = initialization_server('initsrv', x=-200, y=-100)
gns3_project.link(init_server, 0, internet)
gns3_project.start_nodes(init_server, wait_for_everything=True)
# We could ssh to 'ubuntu@initsrv', but that would requiring waiting for the DNS entry to appear,
# so let's ssh directly to the IP address
ipaddr = gns3_project.httpd.instances_reported['initsrv']
stdout = subprocess.check_output(['ssh', f'ubuntu@{ipaddr}',
'-o', 'UserKnownHostsFile=/dev/null', '-o', 'StrictHostKeyChecking=no',
'netplan ip leases ens4 | grep ^DOMAINNAME= | cut -d = -f 2'],
stderr = subprocess.DEVNULL)
args.domain = stdout.strip().decode()
stdout = subprocess.check_output(['ssh', f'ubuntu@{ipaddr}',
'-o', 'UserKnownHostsFile=/dev/null', '-o', 'StrictHostKeyChecking=no',
'netplan ip leases ens4 | grep ^NETMASK= | cut -d = -f 2'],
stderr = subprocess.DEVNULL)
veth_netmask = stdout.strip().decode()
stdout = subprocess.check_output(['ssh', f'ubuntu@{ipaddr}',
'-o', 'UserKnownHostsFile=/dev/null', '-o', 'StrictHostKeyChecking=no',
'netplan ip leases ens4 | grep ^ADDRESS= | cut -d = -f 2'],
stderr = subprocess.DEVNULL)
veth_address = stdout.strip().decode()
veth_subnet = ipaddress.ip_network(veth_address + "/" + veth_netmask, strict=False)
# This isn't atomic; we're using the copy of gns3_variables that we got a minute or so ago
gns3_variables['domain'] = args.domain
gns3_variables['subnet'] = str(veth_subnet)
gns3_project.set_variables(gns3_variables)
# If user asked to create just an initsrv, do nothing else
if args.version == ['initsrv']:
exit(1)
# improve delete method so that it can take init_server directly as an arg
gns3_project.delete(init_server['node_id'])
# Create the master gateway
master = master_gateway(args.project, public_subnet=public_subnet, x=-200, y=0)
# An Ethernet switch for our public "Internet"
PublicIP_switch = gns3_project.switch(public_subnet.with_prefixlen, x=0, y=0, ethernets=16)
gns3_project.link(master, 0, internet)
gns3_project.link(master, 1, PublicIP_switch)
# The BigBlueButton servers and/or test clients
for v in args.version:
if v == 'turn':
# A standard BigBlueButton TURN server
turn_node = turn_server('turn', x=-200, y=-200)
gns3_project.link(turn_node, 0, PublicIP_switch)
gns3_project.depends_on(turn_node, master)
elif v == 'natturn':
# A BigBlueButton TURN server behind a NAT gateway (like AWS or Azure)
if 'natturn' not in gns3_project.node_names():
natturn_nat = server_NAT_gateway('natturn-NAT', public_subnet=public_subnet, x=0, y=-100)
if server_subnet.with_prefixlen not in gns3_project.node_names():
natturn_switch = gns3_project.switch(server_subnet.with_prefixlen, x=0, y=-200)
else:
# can't have two gns3 nodes with the same name, so do this instead
natturn_switch = gns3_project.switch('natturn-subnet', x=0, y=-200)
natturn_node = turn_server('natturn', x=0, y=-300)
gns3_project.link(natturn_nat, 0, PublicIP_switch)
gns3_project.link(natturn_nat, 1, natturn_switch)
gns3_project.link(natturn_node, 0, natturn_switch)
gns3_project.depends_on(natturn_node, natturn_nat)
gns3_project.depends_on(natturn_nat, master)
elif v.startswith('testclient'):
# Create client NAT gateways first
# Remember, they won't be created if they already exist
#
# NAT4: public subnet to carrier grade NAT subnet
#
# We put a switch on here to ensure that NAT6's interface will be up when it boots.
# Otherwise, if the interface is down, it won't start its DHCP server (ever).
#
# NAT4, NAT5, and NAT6 are numbered to match the corresponding 'ens[456]'
# interface names on 'testclient'.
subnet = '100.64.1.1/24'
nat4 = client_NAT_gateway('NAT4', x=150, y=-200, nat_interface=subnet)
gns3_project.link(nat4, 0, PublicIP_switch)
nat4_switch = gns3_project.switch(subnet, x=350, y=-200)
gns3_project.link(nat4, 1, nat4_switch)
gns3_project.depends_on(nat4, master)
# NAT5: public subnet to private client subnet, not overlapping server address space
#
# Put a switch on here for the same reason as NAT4.
subnet = '192.168.128.1/24'
nat5 = client_NAT_gateway('NAT5', x=150, y=-100, nat_interface=subnet)
gns3_project.link(nat5, 0, PublicIP_switch)
nat5_switch = gns3_project.switch(subnet, x=350, y=-100)
gns3_project.link(nat5, 1, nat5_switch)
gns3_project.depends_on(nat5, master)
# NAT6: public subnet to private client subnet, overlapping server address space
#
# Put a switch on here for the same reason as NAT4.
subnet = '192.168.1.1/24'
nat6 = client_NAT_gateway('NAT6', x=150, y=0, nat_interface=subnet)
gns3_project.link(nat6, 0, PublicIP_switch)
nat6_switch = gns3_project.switch(subnet, x=350, y=0)
gns3_project.link(nat6, 1, nat6_switch)
gns3_project.depends_on(nat6, master)
# Create actual test client, now
# find an unoccupied y coordinate on the GUI
for y in (-200, -100, 0, 100, 200):
try:
next(n for n in gns3_project.nodes() if n['x'] == 550 and n['y'] == y)
except StopIteration:
break
client = BBB_client(v, x=550, y=y)
gns3_project.link(client, 0, nat4_switch)
gns3_project.depends_on(client, nat4)
gns3_project.link(client, 1, nat5_switch)
gns3_project.depends_on(client, nat5)
gns3_project.link(client, 2, nat6_switch)
gns3_project.depends_on(client, nat6)
else:
# Create a server
# find an spot on the GUI
if args.no_nat:
y_coordinates_needed = [300]
else:
y_coordinates_needed = [100, 200, 300]
for x in (0, 200, -200, 400, -400):
try:
next(n for n in gns3_project.nodes() if n['x'] == x and n['y'] in y_coordinates_needed)
except StopIteration:
break
BBB_server(v, x=x, public_subnet=public_subnet, depends_on=master)
# The difference between these two is that start_nodes waits for notification that
# the nodes booted, while start_node does not.
#
# The project might not have a notification_url if the script couldn't figure out
# a local IP address suitable for a callback.
if notification_url:
gns3_project.start_nodes(quiet=args.quiet)
else:
for node in args.version:
gns3_project.start_node(node, quiet=args.quiet)