#!/bin/bash set -e ############################################################################### # Script to create a virtual machine for testing NetworkManager. # # Commands: # - build: build a new VM, named "$VM" ("nm"). # - run: start the VM. # - exec: run bash inside the VM, connecting via ssh (this is the default). # - stop: stop the VM. # - reexec: stop and exec. # - clean: stop the VM and delete the image. # # Commands exec and reexec accepts extra arguments, which are the command to # execute in the VM instead of opening an ssh session. # # NetworkManager directories: # # The NetworkManager root directory is mounted in the VM as a filesystem share. # You can run `make install` and run tests. # # Create a symlink ./.git/NetworkManager-ci, to share the CI directory too. # # Required packages: # # Your host needs libvirt, libvirt-nss and guestfs-tools. To access the VM with # `ssh root@$VM`, configure /etc/nsswitch.conf as explained in # https://libvirt.org/nss.html (otherwise, `nm-in-vm exec` won't work, either). # # Prepare for testing: # # There is a script nm-env-prepare.sh to generate a net1 interface for testing. # Currently NM-ci requires a working eth1, so use this before running a CI test: # `nm-env-prepare.sh --prefix eth -i 1 && sleep 1 && nmcli device connect eth1` # # Additional VMs: # # By default, the VM named 'nm' is created, but additional ones can be created: # $ VM=nm2 nm-in-vm build # $ VM=nm2 nm-in-vm exec # $ VM=nm2 nm-in-vm stop # # Choosing a different OS: # # By default Fedora is used, but you can choose a different OS. Most from the # list `virt-builder --list` will work. # $ OS_VERSION=debian-12 nm-in-vm build # $ nm-in-vm exec # $ nm-in-vm stop ############################################################################### # Check for libvirt if ! (command -v virsh && command -v virt-builder && command -v virt-install) &>/dev/null; then echo "libvirt and guestfs-tools are required" >&2 exit 1 fi # set defaults if user didn't define these values VM=${VM:="nm"} OS_VERSION=${OS_VERSION:=} RAM=${RAM:=2048} IMAGE_SIZE=${IMAGE_SIZE:=10G} ROOT_PASSWORD=${ROOT_PASSWORD:=nm} LIBVIRT_POOL=${LIBVIRT_POOL:=default} # only useful if BASEDIR_VM_IMAGE is empty BASEDIR_VM_IMAGE=${BASEDIR_VM_IMAGE:=} BASEDIR_NM=${BASEDIR_NM:=} BASEDIR_NM_CI=${BASEDIR_NM_CI:=} HOST_BRIDGE=${HOST_BRIDGE:=virbr0} SSH_LOG_LEVEL=${SSH_LOG_LEVEL:=ERROR} if [[ -z $OS_VERSION ]]; then # if running Fedora, select same version, else select latest Fedora if grep -q "^ID=fedora$" /etc/os-release 2>/dev/null ; then OS_VERSION="$(sed -n 's/^VERSION_ID=\([0-9]\+\)$/fedora-\1/p' /etc/os-release)" else OS_VERSION=$(virt-builder --list | grep '^fedora' | sort | tail -n 1 | cut -d" " -f 1) fi fi if [[ -z $BASEDIR_NM ]]; then BASEDIR_NM="$(readlink -f "$(dirname -- "$BASH_SOURCE")/..")" fi if [[ -z $BASEDIR_NM_CI && -d "$BASEDIR_NM/.git/NetworkManager-ci" ]]; then BASEDIR_NM_CI="$(readlink -f "$BASEDIR_NM/.git/NetworkManager-ci")" fi if [[ -z $BASEDIR_VM_IMAGE ]]; then libvirt_pool_path_xml=$(virsh pool-dumpxml $LIBVIRT_POOL | grep -F '') if [[ $libvirt_pool_path_xml =~ \(.*)\ ]]; then BASEDIR_VM_IMAGE=${BASH_REMATCH[1]} else BASEDIR_VM_IMAGE=$BASEDIR_NM fi fi # compute some values that depends on user selectable variables basedir_vm_image=$(readlink -f "$BASEDIR_VM_IMAGE") vm_image_file="$VM.qcow2" os_variant=${OS_VERSION//-/} # virt-install --os-variant value, deduced from OS_VERSION os_variant=${os_variant/centosstream/centos-stream} datadir="$BASEDIR_NM/tools/nm-guest-data" ############################################################################## do_build() { local t=$'\t' local nm_ci_build_args local nm_ci_install_args local extra_pkgs local install_pkgs local install_files local gen_files=( "bin-nm-env-prepare.sh:/usr/bin/nm-env-prepare.sh" "bin-nm-deploy.sh:/usr/bin/nm-deploy.sh" "etc-motd-vm:/etc/motd" "etc-bashrc.my:/etc/bashrc.my" "nm-90-my.conf:/etc/NetworkManager/conf.d/90-my.conf" "nm-95-user.conf:/etc/NetworkManager/conf.d/95-user.conf" "home-bash_history:/root/.bash_history" "home-gdbinit:/root/.gdbinit" "home-gdb_history:/root/.gdb_history" "home-behaverc:/root/.behaverc" "systemd-20-nm.override:/etc/systemd/system/NetworkManager.service.d/20-nm.override" ) if vm_is_installed; then echo "The virtual machine '$VM' is already installed, skiping build" >&2 return 0 fi if vm_image_exists; then echo "The image '$basedir_vm_image/$vm_image_file' already exists, skiping build" >&2 return 0 fi if [[ -n $BASEDIR_NM_CI ]]; then nm_ci_build_args=( --mkdir "$BASEDIR_NM_CI" --link "$BASEDIR_NM_CI:/NetworkManager-ci" --append-line "/etc/fstab:/NM_CI${t}$BASEDIR_NM_CI${t}9p${t}trans=virtio,rw,_netdev${t}0${t}0" ) nm_ci_install_args=( --filesystem "$BASEDIR_NM_CI,/NM_CI" ) fi if [[ $OS_VERSION =~ fedora* || $OS_VERSION =~ centos* ]]; then extra_pkgs=(bash-completion bind-utils ccache clang-tools-extra cryptsetup cscope \'dbus\*\' dhcp-client dhcp-relay dhcp-server dnsmasq dracut-network ethtool firewalld gcc gdb glibc-langpack-pl hostapd intltool iproute ipsec-tools iputils iscsi-initiator-utils iw ldns libreswan libselinux-utils libyaml-devel logrotate lvm2 mdadm mlocate net-tools NetworkManager NetworkManager-openvpn NetworkManager-ovs NetworkManager-ppp NetworkManager-pptp NetworkManager-strongswan NetworkManager-team NetworkManager-vpnc NetworkManager-wifi nfs-utils nispor nmap-ncat nmstate nss-tools openvpn \'openvswitch2\*\' perl-IO-Pty-Easy perl-IO-Tty procps psmisc python3-behave python3-black python3-devel python3-netaddr python3-pip python3-pyte python3-pyyaml qemu-kvm radvd rp-pppoe scsi-target-utils strace systemd tcpdump tcpreplay tuned /usr/bin/debuginfo-install /usr/bin/pytest /usr/bin/python vim wireguard-tools wireshark-cli) install_pkgs=( --run "$BASEDIR_NM/contrib/fedora/REQUIRED_PACKAGES" --run-command "dnf install -y --skip-broken ${extra_pkgs[*]}" --run-command "dnf debuginfo-install -y --skip-broken NetworkManager \ \$(ldd /usr/sbin/NetworkManager \ | sed -n 's/.* => \(.*\) (0x[0-9A-Fa-f]*)\$/\1/p' \ | xargs -n1 readlink -f)" ) elif [[ $OS_VERSION =~ debian* || $OS_VERSION =~ ubuntu* ]]; then install_pkgs=( --run "$BASEDIR_NM/contrib/debian/REQUIRED_PACKAGES" ) fi install_files=(--upload "$BASEDIR_NM/contrib/scripts/NM-log:/usr/bin/NM-log") for f in "${gen_files[@]}"; do gen_file "${f%:*}" install_files+=(--upload "$datadir/data-$f") done echo "Creating VM" echo " - VM NAME: $VM" echo " - OS VERSION: $OS_VERSION" echo " - SIZE: $IMAGE_SIZE" echo " - RAM: $RAM" echo " - ROOT PASSWORD: $ROOT_PASSWORD" echo " - IMAGE PATH: $basedir_vm_image/$vm_image_file" echo " - NM DIR: $BASEDIR_NM" echo " - NM CI DIR: $([[ -n $BASEDIR_NM_CI ]] && echo "$BASEDIR_NM_CI" || echo '')" echo " - HOST BRIDGE: $HOST_BRIDGE" virt-builder "$OS_VERSION" \ --output "$basedir_vm_image/$vm_image_file" \ --size "$IMAGE_SIZE" \ --format qcow2 \ --arch x86_64 \ --hostname "$VM" \ --root-password password:nm \ --ssh-inject root \ --append-line "/etc/ssh/sshd_config:PermitRootLogin yes" \ --mkdir "$BASEDIR_NM" \ --link "$BASEDIR_NM:/NetworkManager" \ --append-line "/etc/fstab:/NM${t}$BASEDIR_NM${t}9p${t}trans=virtio,rw,_netdev${t}0${t}0" \ "${nm_ci_build_args[@]}" \ --update \ "${install_pkgs[@]}" \ --run-command "pip3 install --user behave_html_formatter" \ --mkdir "/etc/systemd/system/NetworkManager.service.d" \ "${install_files[@]}" \ --write "/var/lib/NetworkManager/secret_key:nm-in-container-secret-key" \ --chmod "700:/var/lib/NetworkManager" \ --chmod "600:/var/lib/NetworkManager/secret_key" \ --edit "/etc/systemd/journald.conf:s/.*RateLimitBurst=.*/RateLimitBurst=0/" \ --delete "/etc/NetworkManager/system-connections/*" \ --append-line "/etc/bashrc:. /etc/bashrc.my" \ --run-command "updatedb" virt-install \ --name "$VM" \ --ram "$RAM" \ --disk "path=$basedir_vm_image/$vm_image_file,format=qcow2" \ --os-variant "$os_variant" \ --filesystem "$BASEDIR_NM,/NM" \ "${nm_ci_install_args[@]}" \ --network "bridge=$HOST_BRIDGE" \ --import \ --autoconsole none \ --noreboot } do_clean() { vm_is_running && virsh shutdown "$VM" &>/dev/null vm_is_installed && virsh undefine "$VM" &>/dev/null rm -f "$basedir_vm_image/$vm_image_file" } do_run() { vm_is_installed || do_build vm_is_running && return 0 virsh start "$VM" >&2 } do_exec() { do_run local failed=0 while ! ping -c 1 "$VM" &>/dev/null; do failed=$((failed + 1)) (( failed < 15 )) || die "Timeout trying to ping the VM" sleep 1 done ssh \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o LogLevel="$SSH_LOG_LEVEL" \ "root@$VM" "$@" } do_reexec() { vm_is_running && do_stop local waited=0 while vm_is_running; do waited=$((waited + 1)) (( waited < 30 )) || die "Timeout waiting for VM shutdown" sleep 1 done do_exec "$@" } do_stop() { vm_is_running && virsh shutdown "$VM" >&2 } ############################################################################### vm_image_exists() { [[ -f "$basedir_vm_image/$vm_image_file" ]] || return 1 } vm_is_installed() { virsh list --all --name | grep --fixed-strings --line-regexp "$VM" &>/dev/null || return 1 } vm_is_running() { virsh list --name | grep --fixed-strings --line-regexp "$VM" &>/dev/null || return 1 } gen_file() { sed "s|{{BASEDIR_NM}}|$BASEDIR_NM|g" "$datadir/$1.in" > "$datadir/data-$1" if [[ $1 =~ bin-* ]]; then chmod 755 "$datadir/data-$1" else chmod 644 "$datadir/data-$1" fi } usage() { echo "nm-in-vm [-h|--help] build|run|exec|stop|reexec|clean" } help() { usage echo awk '/^####*$/ { if (on) exit; on=-1; } !/^####*$/ { if (on) print(substr($0,3)) }' "$BASH_SOURCE" echo } die() { echo "$1" >&2 exit 1 } ############################################################################### cmd= while (( $# > 0 )); do case "$1" in build|run|exec|reexec|stop|clean) cmd="$1" shift ;; -h|--help) help exit 0 ;; --) shift break ;; *) [[ $cmd != "" ]] && break echo "Invalid argument '$1'" >&2 echo $(usage) >&2 exit 1 ;; esac done if [[ $cmd == "" ]]; then cmd=exec fi if [[ $UID == 0 ]]; then die "cannot run as root" fi if [[ $cmd != exec && $cmd != reexec && $# != 0 ]]; then die "Extra arguments are only allowed with exec|reexec command" fi do_$cmd "$@"