Skip to content

Commit

Permalink
Implement user-machines for member-only VPS
Browse files Browse the repository at this point in the history
This commit adds a NixVirt (libvirt-based) orchestration infrastructure
that gives each ACM member (defined in
user-machines/secrets/user-vms.json) their own persistent virtual
machine.
  • Loading branch information
diamondburned committed May 3, 2024
1 parent a5a6f71 commit 363d91a
Show file tree
Hide file tree
Showing 15 changed files with 771 additions and 117 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ result
results
/servers/exp*
.envrc
.venv
.nixos-test-history
1 change: 1 addition & 0 deletions .shellcheckrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
disable=SC2016
60 changes: 0 additions & 60 deletions containers/cs306/image.nix

This file was deleted.

36 changes: 0 additions & 36 deletions containers/cs306/test.nix

This file was deleted.

25 changes: 25 additions & 0 deletions nix/sources.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
{
"NixVirt": {
"branch": "master",
"description": "LibVirt domain management for Nix",
"homepage": null,
"owner": "AshleyYakeley",
"repo": "NixVirt",
"rev": "f6bc308804cc3c35457ed012c031b121af1578a8",
"sha256": "19j9rcsbvg9qmrxaqmswd6mxy9qr983hs1f7p827kqm07bpmdwh2",
"type": "tarball",
"url": "https://github.com/AshleyYakeley/NixVirt/archive/f6bc308804cc3c35457ed012c031b121af1578a8.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz",
"version": "master"
},
"acm-nixie": {
"branch": "main",
"description": "acmCSUF's version of the nixie bot.",
Expand Down Expand Up @@ -109,6 +122,18 @@
"url": "https://github.com/EthanThatOneKid/discord_conversation_summary_bot/archive/3771e8cabc7f7992b81f21dea53f7bd338a9aa07.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"flake-compat": {
"branch": "master",
"description": null,
"homepage": null,
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"sha256": "0m9grvfsbwmvgwaxvdzv6cmyvjnlww004gfxjvcl806ndqaxzy4j",
"type": "tarball",
"url": "https://github.com/edolstra/flake-compat/archive/0f9255e01c2351cc7d116c072cb317785dd33b33.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"fullyhacks-qrms": {
"branch": "main",
"description": null,
Expand Down
46 changes: 46 additions & 0 deletions scripts/lib/init
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,49 @@ lib::scripts_path() {
printf -v tailPath "/%s" "$@"
echo -n "$(lib::root_path)/scripts$tailPath"
}

# log prints a message with a timestamp prefix.
lib::log() {
printf -v prefix "[%(%H:%M:%S)T] "
echo "${prefix}${*}" >&2
}

# logf prints a formatted message with a timestamp prefix.
# shellcheck disable=SC2059
#
# Usage: lib::logf <format> <args...>
lib::logf() {
printf -v prefix "[%(%H:%M:%S)T] "
printf "${prefix}${1}\n" "${@:2}" >&2
}

# fatal prints a message and exits with status 1.
lib::fatal() {
lib::log "$@"
exit 1
}

# mktmpname takes in a file path and returns a temporary file path pointing to
# the same directory. Note that the temporary file is created by this function.
#
# Usage: lib::mktmpname <file>
lib::mktmpname() {
local file="$1"
local dir
dir="$(dirname "$file")"
mktemp "$dir/.$(basename "$file").XXXXXXXX"
}

# write_file writes the given content to the given file path.
# It differs from regularly piping to a file in that the function will try to
# create a temporary file for writing before moving it to the target file.
# This is to prevent partial writes in case of an error.
#
# Usage: command | lib::write_file <file>
lib::write_file() {
local file="$1"
local tmpFile
tmpFile="$(lib::mktmpname "$file")"
cat > "$tmpFile"
mv "$tmpFile" "$file"
}
19 changes: 0 additions & 19 deletions servers/cs306/services.nix
Original file line number Diff line number Diff line change
Expand Up @@ -131,23 +131,4 @@ in
enable = true;
config = builtins.readFile <acm-aws/secrets/dischord-config.toml>;
};

services.sshwifty = {
enable = true;
config = {
Servers = [
{
ListenInterface = "127.0.0.1";
ListenPort = 38274;
}
];
Presets = [
# {
# Title = "GitHub";
# Type = "SSH";
# }
];
OnlyAllowPresetRemotes = true;
};
};
}
55 changes: 55 additions & 0 deletions servers/cs306/user-vms.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{ config, lib, pkgs, ... }:

let
inherit (import <acm-aws/user-machines/vm/config.nix> { inherit pkgs; })
ips;
in

{
imports = [
<acm-aws/user-machines/vm>
];

acm.user-vms = {
enable = true;
users = builtins.fromJSON (builtins.readFile <acm-aws/user-machines/secrets/user-vms.json>);
# Pin all CPU usages to the 4 host cores.
cpuPinning = [4 5 6 7];
poolDirectory = "/var/lib/acm-vm";
};

services.diamondburned.caddy.sites."https://vps.acmcsuf.com" = ''
root * ${pkgs.writeTextDir "vps.json" (builtins.toJSON config.acm.user-vms.usersData)}
rewrite * /vps.json
file_server
'';

services.sshwifty = {
enable = true;
config = {
Servers = [
{
ListenInterface = "127.0.0.1";
ListenPort = 38274;
}
];
Presets = map
(offset:
let
ip = ips.ipFromOffset offset;
in
{
Title = "SSH to ${ip}";
Type = "SSH";
Host = "${ip}:22";
}
)
(ips.range);
OnlyAllowPresetRemotes = true;
};
};

services.diamondburned.caddy.sites."http://ssh.acmcsuf.com" = ''
reverse_proxy * localhost:38274
'';
}
8 changes: 6 additions & 2 deletions shell.nix
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,23 @@ in
pkgs.mkShell {
name = "acm-aws-shell";
buildInputs = with pkgs; [
cloud-init
terraform
awscli2
# rnix-lsp
nix-update
jq
niv
git
git-crypt
openssl
yamllint
gomod2nix
expect

# editor tools.
yamllint
shellcheck
nodePackages.bash-language-server
# rnix-lsp
];

shellHook = ''
Expand Down
Binary file added user-machines/secrets/user-vms.json
Binary file not shown.
77 changes: 77 additions & 0 deletions user-machines/users-validate
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
. "$(dirname "$0")/../scripts/lib/init"

USERS_FILE="$(dirname "$0")/secrets/user-vms.json"
USERS_ID_REGEX='^[a-zA-Z0-9\-_.]{1,32}$'

main() {
lib::require_installed uuidgen jq

# Format the JSON file.
users_jq '.' | lib::write_file "$USERS_FILE"

# Guarantee that there are no null IDs.
readarray -t nullUsers < <(users_jq -c '.[] | select(.id == null)')
if (( ${#nullUsers[@]} > 0 )); then
lib::log "Null IDs found in $USERS_FILE for the following users:"
for user in "${nullUsers[@]}"; do
lib::log " $user"
done
return 1
fi

# Guarantee that all user IDs are unique.
readarray -t duplicateIDs < <(users_jq -r '.[].id' | sort | uniq -d)
if (( ${#duplicateIDs[@]} > 0 )); then
lib::log "Duplicate user IDs found in $USERS_FILE for the following users:"
for user in "${duplicateIDs[@]}"; do
lib::log " - $user"
done
return 1
fi

# Validate all user IDs. We ask jq to return one JSON string per line in
# case the IDs have a new line. Ideally this should be never, but...
readarray -t userIDs < <(users_jq '.[].id')
for id in "${userIDs[@]}"; do
# Trim the quotes manually then compare that result with the parsed JSON
# string. None of our characters should require special escaping, so
# manually trimming should just work.
id="${id%\"}"
id="${id#\"}"
if [[ $id != $(jq -r -n --arg id "$id" '$id') ]]; then
lib::logf "Invalid user ID %q found in %s." "$id" "$USERS_FILE"
return 1
fi

# Actually pass the ID through regex.
if [[ ! $id =~ $USERS_ID_REGEX ]]; then
lib::logf "Invalid user ID %q found in %s." "$id" "$USERS_FILE"
return 1
fi
done

# Guarantee that all users have a unique UUID.
readarray -t idsWithNoUUID < <(users_jq -r '.[] | select(.uuid == null) | .id')
for id in "${idsWithNoUUID[@]}"; do
uuid=$(uuidgen)
dupe=$(users_jq --arg uuid "$uuid" -r '.[] | select(.uuid == $uuid) | .id')
if [[ $dupe != "" ]]; then
lib::fatal "uuidgen returned colliding UUID $uuid, please retry."
fi

lib::logf "Assigning user with ID %q UUID %s." "$id" "$uuid"
users_jq \
--arg id "$id" \
--arg uuid "$uuid" \
'(.[] | select(.id == $id)).uuid = $uuid' | lib::write_file "$USERS_FILE"
done
}

users_jq() {
jq "$@" "$USERS_FILE"
}

if lib::is_main; then
main "$@"
fi
Loading

0 comments on commit 363d91a

Please sign in to comment.