--- title: Host a DevOps exam using NixOS subtitle: \footnotesize Deploy a Bastion host and a KVM virtual machine per student in order for them to deploy a website which will be auto-validated. author: Yvan SRAKA, Ryan LAHFA theme: default --- ## Host a DevOps exam using NixOS - DevOps students have to test their skills in a real-world exam, what’s better than scripting it using NixOps in order to reuse it? - They will be provided with their own user account on the Bastion and access to a jump user account, WireGuard will be auto-configured on the host to enable access to their KVM host directly. - The main point is controlling the KVM guests “declaratively” and giving choice to the students to choose their guest OS: Debian or NixOS. - We will see how we can easily generate Nix expression from a scripting language, e.g. Python, and feed it to a NixOps deployment. ## Disclaimer: _all this is incomplete and experimental!_ This was tried during a real exam this summer, but it's very alpha-quality and had to get a lot of last-minute fixes due to external issues (e.g. disks dying…) ## Git repository The full code can be found here\footnote{https://git.newtype.fr/yvan/devops-exam-model}, it's still a bit messy... `\nix` folder contains files on which will focus this talk: - \color{red}`student-setup.nix` \color{black} is generated by \color{red} `generate-setup.py` \color{black} (that rely on \color{red} `nix-expr.py`\color{black}) from students data. - \color{red}`deploy.sh` \color{black} push on our setup \color{red} `exam.nix`\color{black}, \color{red} `student.nix`\color{black}, \color{red} `kvm-guests.nix` \color{black} and \color{red} `student-setup.nix` \color{black} ## `deploy.sh` ```bash ##!/usr/bin/env bash echo "[+] Regenerating of the setup" python3 generate_setup.py echo "[+] Sending Nix files" rsync --inplace --temp-dir=/tmp -avPz *.nix \ yvan@bastion:/etc/nixos/ echo "[+] Rebuilding of the exam machine" ssh -t yvan@bastion "sudo nixos-rebuild switch" ``` ## `generate_setup.py` _(subset of)_ ```python def students(csv_filename): with open(csv_filename, 'r', newline='') as csvfile: ereader = csv.reader(csvfile, delimiter=';') next(ereader) # exhaust header. for index, student in enumerate(ereader): name, surname, email, username = student wg_pubkey, wg_privkey = wireguard_parameters() keys = read_keys(username) yield { "surname": surname, "name": name, "email": email, "username": username, "keys": keys, "wireguardPublicKey": wg_pubkey, "wireguardPrivateKey": wg_privkey, } ``` ## `student-setup.nix` _(generated)_ ```nix ltorvalds = { email = "torvalds@linux-foundation.org"; guestOperatingSystem = "debian"; index = 42; keys = [ "ssh-ed25519 ... torvalds@linux-foundation.org" ]; name = "Linus"; surname = "Torvalds"; username = "ltorvalds"; wireguardPrivateKey = "0ECE2Js+RkxQVTyJ9BvZB0DjpEGnWMy1X5cI8R2RdHA="; wireguardPublicKey = "3o9Dhmrrql/5PZEhi5kS+Fob1m8rN70SXzDGy48bMR0="; }; rstallman = { ... } ``` ## `student.nix` _(subset of)_ ```nix mkGuest = name: student: { memory = "1G"; netDevice = "tap${toString student.index}"; vncDisplay = "localhost:${toString student.index}"; operatingSystem = student.guestOperatingSystem; }; services.kvmGuests = { enable = true; guests = mapAttrs mkGuest cfg.students; }; # Create users for each student + management/jump accounts. users.users = (mapAttrs mkBastionUser cfg.students) // ({ jump = mkJumpUser cfg.students; admin = adminUser; }); ``` # \Huge Q/A