---
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