Repository: Xe/waifud Branch: main Commit: a0c21bcfe585 Files: 79 Total size: 232.9 KB Directory structure: gitextract_nizd10bl/ ├── .envrc ├── .github/ │ └── dependabot.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── config.example.dhall ├── default.nix ├── flake.nix ├── frontend/ │ ├── build.sh │ ├── css/ │ │ ├── build.sh │ │ ├── src/ │ │ │ ├── admin.css │ │ │ └── xess.css │ │ └── xess.css │ ├── deno.json │ ├── deps.ts │ ├── import_map.json │ ├── instance_create.tsx │ ├── instance_detail.tsx │ ├── static/ │ │ └── js/ │ │ └── .gitignore │ └── waifud/ │ └── mod.ts ├── lib/ │ ├── rotbart/ │ │ ├── Cargo.toml │ │ ├── scrapers/ │ │ │ ├── README.md │ │ │ ├── blaseball.sh │ │ │ ├── pokedex-hisui.sh │ │ │ └── pokedex.sh │ │ └── src/ │ │ ├── blaseball.rs │ │ ├── elfs.rs │ │ ├── lib.rs │ │ ├── mlp_fim.rs │ │ ├── pokemon.rs │ │ ├── xc1.rs │ │ └── xc2.rs │ ├── tailscale_client/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── lib.rs │ └── ts_localapi/ │ ├── Cargo.toml │ └── src/ │ └── lib.rs ├── scripts/ │ ├── .gitignore │ ├── metadata.json │ ├── mk-nixos-image.sh │ └── nixos-image.nix ├── shell.nix ├── src/ │ ├── admin/ │ │ └── mod.rs │ ├── api/ │ │ ├── audit.rs │ │ ├── cloudinit.rs │ │ ├── distros.rs │ │ ├── instances.rs │ │ ├── libvirt.rs │ │ ├── mod.rs │ │ └── vendor-data │ ├── bin/ │ │ ├── unique-monster.rs │ │ └── waifuctl.rs │ ├── build.rs │ ├── client/ │ │ └── mod.rs │ ├── config.rs │ ├── lib.rs │ ├── libvirt.rs │ ├── main.rs │ ├── migrate/ │ │ ├── 20220225-session.sql │ │ ├── 20220814-no-session.sql │ │ ├── base_schema.sql │ │ └── mod.rs │ ├── models.rs │ ├── scrape/ │ │ ├── amazon_linux.rs │ │ ├── arch.rs │ │ ├── mod.rs │ │ ├── nixos.rs │ │ ├── rocky_linux.rs │ │ └── ubuntu.rs │ └── tailauth.rs ├── templates/ │ ├── base.rs.xml │ ├── base.xml │ ├── meta-data │ └── templates.go └── var/ ├── .gitignore ├── base.yaml ├── xe-base-windows.yaml ├── xe-base.nix └── xe-base.yaml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .envrc ================================================ use flake ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "cargo" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" open-pull-requests-limit: 10 ================================================ FILE: .gitignore ================================================ *.qcow2 var/*.xml var/*.1 config.dhall result .direnv # Added by cargo /target ================================================ FILE: Cargo.toml ================================================ [package] name = "waifud" version = "0.1.0" edition = "2021" authors = [ "Xe Iaso " ] build = "src/build.rs" repository = "https://github.com/Xe/waifud" license = "mit" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [profile.release] lto = true [dependencies] anyhow = "1" async-trait = "0.1.68" axum = "0.6" axum-client-ip = "0.3" axum-macros = "0.3" axum-extra = { version = "0.5", features = ["spa"] } bb8 = "0.7" chrono = "0.4" clap = { version = "4", features = ["derive"] } clap_mangen = "0.2" clap_complete = "4" dirs = "4" edit = "0.1" failure = "0.1" futures = "0.3" hex = { version = "0.4", features = [ "serde" ] } hyper = "0.14" hyper-tls = "0.5" mac_address = "1" names = "0.14" rand = "0.8" rusqlite_migration = "1.0" scraper = "0.14.0" serde_dhall = "0.12" serde_json = "1" serde_yaml = "0.9" tabular = "0.2" thiserror = "1" tracing = "0.1" tracing-futures = "0.2" tracing-log = "0.1" tracing-subscriber = "0.3" url = "2" bb8-rusqlite = { git = "https://github.com/pleshevskiy/bb8-rusqlite", branch = "bump-rusqlite" } maud = { git = "https://github.com/Xe/maud", rev = "a40596c42c7603cc4610bbeddea04c4bd8b312d9", features = ["axum-core", "axum"] } virt = "0.3" virt-sys = "0.2" rotbart = { path = "./lib/rotbart" } tailscale_client = { path = "./lib/tailscale_client" } ts_localapi = { path = "./lib/ts_localapi" } [dependencies.rusqlite] version = "0.26" features = [ "bundled", "uuid", "serde_json", "chrono" ] [dependencies.serde] version = "1" features = [ "derive" ] [dependencies.reqwest] version = "0.11" features = [ "json" ] [dependencies.tokio] version = "1" features = [ "full" ] [dependencies.tower] version = "0.4" features = [ "full" ] [dependencies.tower-http] version = "0.4" features = [ "full" ] [dependencies.uuid] version = "0.8" features = [ "serde", "v4" ] [build-dependencies] ructe = { version = "0.15" } [dev-dependencies] ructe = { version = "0.15" } [workspace] members = [ "lib/*" ] ================================================ FILE: LICENSE ================================================ Copyright (c) 2022 Xe Iaso Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # waifud ![enbyware](https://pride-badges.pony.workers.dev/static/v1?label=enbyware&labelColor=%23555&stripeWidth=8&stripeColors=FCF434%2CFFFFFF%2C9C59D1%2C2C2C2C) ![made with Nix](https://img.shields.io/badge/made%20with-Nix-blue?logo=nixos) ![built with Garnix](https://img.shields.io/static/v1?label=Built%20with&message=Garnix&color=blue&style=flat&logo=nixos&link=https://garnix.io&labelColor=111212) ![license](https://img.shields.io/github/license/Xe/waifud) ![language count](https://img.shields.io/github/languages/count/Xe/waifud) ![repo size](https://img.shields.io/github/repo-size/Xe/waifud) A few tools to help me manage and run virtual machines across a homelab cluster. waifud was made for my own personal use and I do not expect it to be very useful outside that context. If you do want to run this on your infrastructure anyways, please [contact me](https://xeiaso.net/contact). THIS IS EXPERIMENTAL! USE IT AT YOUR OWN PERIL! TODO(Xe): Link to blogpost on the design/implementation once it is a thing. Blogposts about waifud: - [waifud Plans](https://xeiaso.net/blog/waifud-plans-2021-06-19) - [waifud Progress Report #1](https://xeiaso.net/blog/waifud-progress-2022-02-06) - [waifud Progress Report #2](https://xeiaso.net/blog/waifud-progress-report-2) Overall architecture diagram (with incomplete components marked with a clock): ```mermaid flowchart TD subgraph control plane WD[fa:fa-rust waifud] WC[fa:fa-rust waifuctl] ID[fa:fa-golang fa:fa-clock isekaid] MD[fa:fa-golang fa:fa-clock megamid] PD[fa:fa-golang fa:fa-clock portald] end subgraph VM plane LV[fa:fa-c libvirt] WH[fa:fa-linux runner\nnodes] VM[fa:fa-linux virtual\nmachines] end subgraph external TS[fa:fa-golang Tailscale] end PD --> |tailnet ingress for| WD WC --> |operator tool for| WD WC --> |usually connects via|PD ID --> |fetches node metadata\nand secrets for| WD VM --> |cloud-init\nmetadata| ID WD --> |manages libvirt on| WH LV --> |actually runs VMs| VM VM --> |network storage| MD WD --> |sets limits for\nrequests metrics from| MD WH --> |runs| LV WH <--> |subnet router\ninterconnect| TS TS --> |network layer for| PD VM --> |usually a part of| TS ``` ================================================ FILE: config.example.dhall ================================================ let Tailscale = { Type = { apiKey : Text, tailnet : Text } , default = { apiKey = env:TAILSCALE_API_KEY ? "" , tailnet = env:TAILSCALE_TAILNET ? "cetacean.org.github" } } let Config = { Type = { baseURL : Text , hosts : List Text , bindHost : Text , port : Natural , rpoolBase : Text , qemuPath : Text , tailscale : Tailscale.Type } , default = { baseURL = "http://100.100.100.100:23818" , hosts = [ "vmhost1", "vmhost2" ] , bindHost = "::" , port = 23818 , rpoolBase = "rpool/local/vms" , qemuPath = "/run/libvirt/nix-emulators/qemu-system-x86_64" , tailscale = Tailscale::{=} } } let defaultPort = env:PORT ? 23818 in Config::{ port = defaultPort } ================================================ FILE: default.nix ================================================ (import (fetchTarball { url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz"; sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; }) { src = ./.; }).defaultNix ================================================ FILE: flake.nix ================================================ { inputs = { naersk.url = "github:nmattia/naersk/master"; naersk.inputs.nixpkgs.follows = "nixpkgs"; nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; utils.url = "github:numtide/flake-utils"; xess = { url = "github:Xe/Xess"; inputs.nixpkgs.follows = "nixpkgs"; inputs.utils.follows = "utils"; }; deno2nix = { url = "github:Xe/deno2nix"; inputs.nixpkgs.follows = "nixpkgs"; inputs.flake-utils.follows = "utils"; }; }; outputs = { self, nixpkgs, utils, naersk, xess, deno2nix, ... }@inputs: utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; overlays = [ deno2nix.overlays.default ]; }; naersk-lib = pkgs.callPackage naersk { }; in rec { packages = rec { unique-monster = pkgs.stdenv.mkDerivation { src = self.packages."${system}".waifud; pname = "unique-monster"; version = self.packages."${system}".waifud-bin.version; phases = "installPhase"; installPhase = '' mkdir -p $out/bin cp $src/bin/unique-monster $out/bin ''; }; waifud-bin = naersk-lib.buildPackage { pname = "waifud-bin"; src = ./.; buildInputs = with pkgs; [ pkg-config openssl sqlite-interactive libvirt ]; }; waifud-frontend = let build = { entrypoint, name ? entrypoint, minify ? true }: pkgs.deno2nix.mkBundled { pname = "xesite-frontend-${name}"; inherit (waifud-bin) version; src = ./frontend; lockfile = ./frontend/deno.lock; output = "${entrypoint}.js"; outPath = "static/js"; entrypoint = "./${entrypoint}.tsx"; importMap = "./import_map.json"; inherit minify; }; instance_detail = build { entrypoint = "instance_detail"; }; instance_create = build { entrypoint = "instance_create"; }; in pkgs.symlinkJoin { name = "waifud-frontend-${waifud-bin.version}"; paths = [ instance_detail instance_create ]; }; waifud = pkgs.symlinkJoin { name = "waifud-${waifud-bin.version}"; paths = with self.packages."${system}"; [ waifud-bin waifud-frontend ]; }; waifuctl = pkgs.stdenv.mkDerivation { src = self.packages."${system}".waifud; pname = "waifuctl"; version = self.packages."${system}".waifud-bin.version; phases = "installPhase"; installPhase = '' mkdir -p $out/bin cp $src/bin/waifuctl $out/bin mkdir -p $out/share/man/man1 HOME=. $out/bin/waifuctl utils manpage $out/share/man/man1 gzip -r $out/share/man/man1 ''; }; }; defaultPackage = self.packages."${system}".waifuctl; apps = { unique-monster = utils.lib.mkApp { drv = self.packages."${system}".unique-monster; }; waifud = utils.lib.mkApp { drv = self.packages."${system}".waifud; }; waifuctl = utils.lib.mkApp { drv = self.packages."${system}".waifuctl; }; }; defaultApp = self.apps."${system}".waifuctl; nixosModules = { waifuctl = { ... }: { environment.defaultPackages = [ self.packages."${system}".waifuctl ]; }; waifud-common = { lib, ... }: { users.groups.waifud = lib.mkDefault { }; users.users.waifud = { createHome = true; description = "waifud user"; isSystemUser = true; group = "waifud"; home = "/var/lib/waifud"; }; }; waifud-host = { lib, pkgs, config, ... }: with lib; let cfg = config.xeserv.waifud; in { imports = [ self.nixosModules."${system}".waifud-common self.nixosModules."${system}".waifuctl ]; config = { systemd.services = { waifud = { wantedBy = [ "multi-user.target" ]; environment = { RUST_LOG = "tower_http=debug,waifud=debug,info"; }; serviceConfig = { User = "waifud"; Group = "waifud"; Restart = "always"; WorkingDirectory = "${self.packages."${system}".waifud}"; RestartSec = "30s"; ExecStart = "${waifud}/bin/waifud --config ${cfgDhall}"; }; }; }; }; }; waifud-runner = { pkgs, lib, config, ... }: with lib; let cfg = config.xeserv.waifud.runner; in { imports = [ self.nixosModules."${system}".waifud-common ]; options.xeserv.waifud.runner = with lib; { parentDataset = mkOption { type = types.str; default = "rpool/local/vms"; description = "the parent dataset to grant the waifud group zfs management access on"; }; sshKeys = mkOption { type = with types; listOf str; default = [ ]; description = "the list of SSH public keys to allow waifud to ssh in as"; }; }; config = { environment.defaultPackages = with pkgs; [ qemu zfs wget ]; virtualisation.libvirtd.enable = lib.mkDefault true; systemd.services.waifud-runner-setup = { wantedBy = [ "multi-user.target" ]; serviceConfig.Type = "oneshot"; script = '' /run/current-system/sw/bin/zfs allow -g waifud create,destroy,mount,snapshot,rollback ${cfg.parentDataset} ''; }; security.polkit.extraConfig = '' /* Allow users in the waifud group to manage the libvirt daemon without authentication */ polkit.addRule(function(action, subject) { if (action.id == "org.libvirt.unix.manage" && subject.isInGroup("waifud")) { return polkit.Result.YES; } }); ''; users.users.waifud.openssh.authorizedKeys.keys = cfg.sshKeys; security.sudo.extraRules = [{ groups = [ "waifud" ]; users = [ "waifud" ]; runAs = "root:root"; commands = [{ command = "/run/current-system/sw/bin/qemu-img"; options = [ "NOPASSWD" ]; }]; }]; }; }; }; devShell = with pkgs; mkShell { buildInputs = [ cargo cargo-watch rustc rustfmt rust-analyzer pre-commit rustPackages.clippy openssl pkg-config sqlite-interactive libvirt dhall dhall-json jq jo deno strace ]; DATABASE_URL = "./var/waifud.db"; RUST_LOG = "tower_http=trace,debug"; RUST_SRC_PATH = rustPlatform.rustLibSrc; }; }); } ================================================ FILE: frontend/build.sh ================================================ #!/usr/bin/env nix-shell #! nix-shell -p deno -i bash set -e cd $(dirname $0) DENO_FLAGS='--import-map=./import_map.json --lock deno.lock' if [ "$1" == "--dev" ]; then DENO_FLAGS="$DENO_FLAGS --watch" fi export RUST_LOG=info deno cache --import-map=./import_map.json --lock deno.lock --lock-write *.tsx deps.ts mkdir -p ./static/js deno bundle $DENO_FLAGS ./instance_detail.tsx ./static/js/instance_detail.js & deno bundle $DENO_FLAGS ./instance_create.tsx ./static/js/instance_create.js & wait ================================================ FILE: frontend/css/build.sh ================================================ #!/usr/bin/env nix-shell #! nix-shell -p nodePackages.clean-css-cli -i bash cleancss -o ./xess.css ./src/xess.css ./src/admin.css ================================================ FILE: frontend/css/src/admin.css ================================================ .breadcrumb { padding: 0 .5rem; } .breadcrumb ul { display: flex; flex-wrap: wrap; list-style: none; margin: 0; padding: 0; } .breadcrumb li:not(:last-child)::after { display: inline-block; margin: 0 .25rem; content: "/"; } .left { float: left; } .right { float: right; } ================================================ FILE: frontend/css/src/xess.css ================================================ @import url("https://cdn.xeiaso.net/static/css/iosevka/family.css"); main { font-family: Iosevka Aile Iaso, sans-serif; max-width: 50rem; padding: 2rem; margin: auto; } @media only screen and (max-device-width: 736px) { main { padding: 0rem; } } ::selection { background: #d3869b; } body { background: #282828; color: #ebdbb2; } pre { background-color: #3c3836; padding: 1em; border: 0; font-family: Iosevka Curly Iaso, monospace; } a, a:active, a:visited { color: #b16286; background-color: #1d2021; } h1, h2, h3, h4, h5 { margin-bottom: .1rem; font-family: Iosevka Etoile Iaso, serif; } blockquote { border-left: 1px solid #bdae93; margin: 0.5em 10px; padding: 0.5em 10px; } footer { align: center; } @media (prefers-color-scheme: light) { body { background: #fbf1c7; color: #3c3836; } pre { background-color: #ebdbb2; padding: 1em; border: 0; } a, a:active, a:visited { color: #b16286; background-color: #f9f5d7; } h1, h2, h3, h4, h5 { margin-bottom: .1rem; } blockquote { border-left: 1px solid #655c54; margin: 0.5em 10px; padding: 0.5em 10px; } } ================================================ FILE: frontend/css/xess.css ================================================ @import url(https://cdn.xeiaso.net/static/css/iosevka/family.css);main{font-family:Iosevka Aile Iaso,sans-serif;max-width:50rem;padding:2rem;margin:auto}@media only screen and (max-device-width:736px){main{padding:0}}::selection{background:#d3869b}body{background:#282828;color:#ebdbb2}pre{background-color:#3c3836;padding:1em;border:0;font-family:Iosevka Curly Iaso,monospace}a,a:active,a:visited{color:#b16286;background-color:#1d2021}h1,h2,h3,h4,h5{margin-bottom:.1rem;font-family:Iosevka Etoile Iaso,serif}blockquote{border-left:1px solid #bdae93;margin:.5em 10px;padding:.5em 10px}footer{align:center}@media (prefers-color-scheme:light){body{background:#fbf1c7;color:#3c3836}pre{background-color:#ebdbb2;padding:1em;border:0}a,a:active,a:visited{color:#b16286;background-color:#f9f5d7}h1,h2,h3,h4,h5{margin-bottom:.1rem}blockquote{border-left:1px solid #655c54;margin:.5em 10px;padding:.5em 10px}}.breadcrumb{padding:0 .5rem}.breadcrumb ul{display:flex;flex-wrap:wrap;list-style:none;margin:0;padding:0}.breadcrumb li:not(:last-child)::after{display:inline-block;margin:0 .25rem;content:"/"}.left{float:left}.right{float:right} ================================================ FILE: frontend/deno.json ================================================ { "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "xeact", }, "importMap": "./import_map.json", } ================================================ FILE: frontend/deps.ts ================================================ import * as xeact from "xeact"; export { xeact, //xterm }; ================================================ FILE: frontend/import_map.json ================================================ { "imports": { "xeact": "https://xena.greedo.xeserv.us/pkg/xeact/v0.69.71/xeact.ts", "xeact/jsx-runtime": "https://xena.greedo.xeserv.us/pkg/xeact/v0.69.71/jsx-runtime.js", "/": "./", "./": "./" } } ================================================ FILE: frontend/instance_create.tsx ================================================ /** @jsxImportSource xeact */ import { u } from "xeact"; import { getConfig, getDistros, makeInstance, NewInstance, } from "./waifud/mod.ts"; const user_data_template = `#cloud-config #vim:syntax=yaml users: - name: xe groups: [ wheel ] sudo: [ "ALL=(ALL) NOPASSWD:ALL" ] shell: /bin/bash `; export const Page = async () => { const distros = await getDistros(); const config = await getConfig(); const nameBox = ; const memoryBox = ; const cpuBox = ; const host = ( ); const disk_size_gb = ; const zvol_prefix = ; const distro = ( ); distro.onchange = () => { let selectedDistro: any | null = null; distros.forEach((d) => { if (d.name == distro.value) { selectedDistro = d; } }); if (selectedDistro == null) { console.log( "this shouldn't happen, selected distro doesn't exist in our list??", ); return; } const disk_size = parseInt(disk_size_gb.value, 10); if (disk_size < selectedDistro.minSize) { disk_size_gb.value = `${selectedDistro.minSize}`; } }; const user_data = ( ); const join_tailnet = ; const submit = ; submit.onclick = async () => { const req: NewInstance = { name: nameBox.value != "" ? nameBox.value : undefined, memory_mb: memoryBox.value != "" ? parseInt(memoryBox.value, 10) : undefined, cpus: cpuBox.value != "" ? parseInt(cpuBox.value, 10) : undefined, host: host.value, disk_size_gb: disk_size_gb.value != "" ? parseInt(disk_size_gb.value, 10) : undefined, zvol_prefix: zvol_prefix.value != "" ? zvol_prefix.value : undefined, distro: distro.value, user_data: user_data.value, join_tailnet: join_tailnet.checked, }; console.log(req); const instance = await makeInstance(req); console.log(instance); window.location.href = u(`/admin/instances/${instance.uuid}`); }; return (
Name {nameBox}
Memory (MB) {memoryBox}
CPU cores {cpuBox}
Host {host}
Disk size (GB) {disk_size_gb}
ZVol prefix {zvol_prefix}
Distro {distro}
Userdata {user_data}
Join tailnet + SSH? {join_tailnet}
{""}
{submit}
); }; ================================================ FILE: frontend/instance_detail.tsx ================================================ /** @jsxImportSource xeact */ export function Fragment({ children }: { children: any[] }): any[] { return children; } import { g, t, u } from "xeact"; import { deleteInstance, getAuditLogsForInstance, hardRebootInstance, rebootInstance, reinitInstance, shutdownInstance, startInstance, } from "./waifud/mod.ts"; type InstanceButtonProps = { text: string; instance_id: string; action: string; message: string; confirm?: boolean; }; function DeleteInstanceButton( { text, instance_id, message, confirm = true }: InstanceButtonProps, ) { const onclick = async () => { if (confirm) { const response = prompt( "Type 'I don't care about the data' to continue.", ); if (response !== "I don't care about the data") { g("messages").appendChild(t("Confirmation failed.")); return; } } await deleteInstance(instance_id); g("messages").appendChild(t(message)); alert(message); window.location.href = u("/admin/instances"); }; return (

); } function InstanceButton( { text, instance_id, action, message, confirm = false }: InstanceButtonProps, ) { const onclick = async () => { if (confirm) { const response = prompt( "Type 'I don't care about the data' to continue.", ); if (response !== "I don't care about the data") { g("messages").appendChild(t("Confirmation failed.")); return; } } switch (action) { case "reboot": await rebootInstance(instance_id); break; case "hardreboot": await hardRebootInstance(instance_id); break; case "reinit": await reinitInstance(instance_id); break; case "shutdown": await shutdownInstance(instance_id); break; case "start": await startInstance(instance_id); break; } g("messages").appendChild(t(message)); }; return (

); } export async function Page() { const instance_id = g("instance_id").innerText; const auditLogs = (await getAuditLogsForInstance(instance_id)).map((al) => ( {new Date(al.ts * 1000).toLocaleString()} {al.op} )); auditLogs.unshift( Time Operation , ); return (

Audit Logs

{auditLogs}

Messages

); } ================================================ FILE: frontend/static/js/.gitignore ================================================ *.js ================================================ FILE: frontend/waifud/mod.ts ================================================ import { u } from "xeact"; export type Config = { base_url: string, hosts: string[], bind_host: string, port: number, rpool_base: string, qemu_path: string, }; export const getConfig = async (): Promise => { const resp = await fetch(u("/admin/api/config")); if (resp.status !== 200) { const body = await resp.text(); throw new Error("wrong status code: " + resp.status + "\n\n" + body); } const result: Config = await resp.json(); return result; } export type Distro = { name: string; downloadURL: string; sha256Sum: string; minSize: string; format: string; }; export const getDistros = async (): Promise => { const resp = await fetch(u("/api/v1/distros")); if (resp.status !== 200) { const body = await resp.text(); throw new Error("wrong status code: " + resp.status + "\n\n" + body); } const result: Distro[] = await resp.json(); return result; }; export type AuditLog = { id: number; ts: number; kind: string; op: string; data: any; uuid?: string; name?: string; }; export const getAuditLogs = async (): Promise => { const resp = await fetch(u("/api/v1/auditlogs")); if (resp.status !== 200) { const body = await resp.text(); throw new Error("wrong status code: " + resp.status + "\n\n" + body); } const result: AuditLog[] = await resp.json(); return result; }; export const getAuditLogsForInstance = async (id: string): Promise => { const resp = await fetch(u(`/api/v1/auditlogs/instance/${id}`)); if (resp.status !== 200) { const body = await resp.text(); throw new Error("wrong status code: " + resp.status + "\n\n" + body); } const result: AuditLog[] = await resp.json(); return result; }; export type NewInstance = { name?: string; memory_mb?: number; cpus?: number; host: string; disk_size_gb?: number; zvol_prefix?: string; distro: string; user_data?: string; join_tailnet: boolean; }; export type Instance = { uuid: string; name: string; host: string; mac_address: string; memory: number; disk_size: number; zvol_name: string; status: string; distro: string; join_tailnet: boolean; }; export const makeInstance = async (ni: NewInstance): Promise => { const resp = await fetch(u("/api/v1/instances"), { method: "POST", body: JSON.stringify(ni), headers: { "Accept": "application/json", "Content-Type": "application/json", }, }); if (resp.status !== 200) { const body = await resp.text(); throw new Error("wrong status code: " + resp.status + "\n\n" + body); } const instance: Instance = await resp.json(); return instance; }; export const deleteInstance = async (id: string): Promise => { await fetch(u(`/api/v1/instances/${id}`), { method: "DELETE", }); } const doThingToInstance = (action: string): (id: string) => Promise => { return (async (id: string): Promise => { const resp = await fetch(u(`/api/v1/instances/${id}/${action}`), { method: "POST", }); if (resp.status !== 200) { const body = await resp.text(); throw new Error("wrong status code: " + resp.status + "\n\n" + body); } }); } export const rebootInstance = doThingToInstance("reboot"); export const hardRebootInstance = doThingToInstance("hardreboot"); export const reinitInstance = doThingToInstance("reinit"); export const shutdownInstance = doThingToInstance("shutdown"); export const startInstance = doThingToInstance("start"); ================================================ FILE: lib/rotbart/Cargo.toml ================================================ [package] name = "rotbart" version = "0.1.0" edition = "2021" authors = [ "Xe Iaso " ] repository = "https://github.com/Xe/waifud" license = "mit" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] lazy_static = "1" names = "0.14" ================================================ FILE: lib/rotbart/scrapers/README.md ================================================ # How to generate names.json Open https://xenoblade.github.io/xb2/bdat/common/BLD_NameList.html and paste this into the browser inspector: ```js names = []; Array.from(document.getElementsByClassName("sortable")[0].children[1].children) .forEach(row => names.push(row.children[2] .innerHTML .toLowerCase() .replaceAll(" ", "-"))); console.log(JSON.stringify(names)); ``` Then format it with jq. For ponies use this fragment: ```javascript names = []; Array.from(document.getElementsByClassName("listofponies")[0] .children[1] .children ).forEach(row => { let name = row.children[0] .textContent .toLowerCase() .replaceAll(" ", "-") .replaceAll(".", "") .replaceAll("ö", "o"); if (name.includes("unnamed")) { return; } if (name.includes("[")) { return; } if (name.includes("/")) { return; } if (name.includes("alt")) { return; } if (name.includes("pony")) { return; } if (name.includes("mare")) { return; } if (name.includes("student")) { return; } if (name.includes("'")) { return; } if (name.includes('"')) { return; } if (name.length > 10) { return; } console.log([name, name.length]); names.push(name); }); console.log(JSON.stringify(names)); ``` Combine all of the files like this: ```console $ jq -n '[inputs] | add' \ blaseball.json \ names-blades.json \ names-ponies-earth.json \ names-ponies-pegasus.json \ names-ponies-unicorn.json \ pokemon.json \ pokemon-hisui.json \ | jq -r '.[]' \ | sort \ | uniq \ | jq -nR '[inputs | select(length>0)]' > names.json ``` ================================================ FILE: lib/rotbart/scrapers/blaseball.sh ================================================ #!/usr/bin/env nix-shell #! nix-shell -p jq -p curl -i bash curl 'https://api.sibr.dev/chronicler/v2/entities?type=player&at=2020-11-01T00:00:00Z' \ | jq '.items[].data.name' -r \ | grep -v -- "-" \ | tr '[:upper:]' '[:lower:]' \ | tr ' ' '-' \ | sed 's/\.//g' \ | sed 's/://g' \ | jq --raw-input '.' \ | jq -s > blaseball.json ================================================ FILE: lib/rotbart/scrapers/pokedex-hisui.sh ================================================ #!/usr/bin/env nix-shell #! nix-shell -p jq -p curl -i bash cat pokedex-hisui.json \ | jq -r '.[].name' \ | tr '[:upper:]' '[:lower:]' \ | tr ' ' '-' \ | sed 's/\.//g' \ | sed 's/://g' \ | jq --raw-input '.' \ | jq -s > pokemon-hisui.json ================================================ FILE: lib/rotbart/scrapers/pokedex.sh ================================================ #!/usr/bin/env nix-shell #! nix-shell -p jq -p curl -i bash curl https://raw.githubusercontent.com/fanzeyi/pokemon.json/master/pokedex.json \ | jq -r '.[].name.english' \ | tr '[:upper:]' '[:lower:]' \ | tr ' ' '-' \ | sed 's/\.//g' \ | sed 's/://g' \ | jq --raw-input '.' \ | jq -s > pokemon.json ================================================ FILE: lib/rotbart/src/blaseball.rs ================================================ pub const FIRST_NAMES: &'static [&'static str] = &[ "abbie", "abbott", "abner", "acosta", "adalberto", "adelaide", "adeline", "adi", "adkins", "adrian", "adrianna", "agan", "agnes", "agustín", "aisha", "aitor", "alaynabella", "albert", "aldo", "aldon", "alejandro", "alex", "alexander", "alexandria", "alexi", "alford", "ali", "alison", "allan", "allis", "allison", "almond", "alston", "alvie", "alvis", "alx", "alyssa", "amal", "amaya", "amias", "amos", "anabela", "anaroniku", "anastasia", "anathema", "anaximandra", "andrew", "anemone", "aneurin", "ankle", "anna", "annick", "annie", "anthony", "antonio", "aoife", "apollo", "arantxa", "arches", "archie", "ardy", "ariadne", "armen", "artemesia", "arthur", "arturo", "arvin", "astrothesia", "athena", "atlas", "atma", "attila", "aubrey", "august", "augusta", "augusto", "aureliano", "aurora", "avi", "avila", "axel", "ayanna", "aymer", "bq", "baby", "backpatch", "badger", "badgerson", "baldwin", "balina", "balthazar", "bambi", "bandit", "bao", "barney", "barry", "bartleby", "bash", "basil", "basilio", "bates", "bauer", "beans", "beasley", "beau", "beck", "becker", "bees", "belinda", "ben", "bengi", "benjamin", "bennett", "benny", "benson", "bernie", "bert", "best", "bethel", "betsy", "bevan", "bevis", "billup", "bistro", "blaire", "blake", "blankenship", "blimp", "blondie", "blood", "bloom", "blossom", "bob", "bobbin", "boden", "bogan", "bones", "bonito", "bonk", "bonnie", "borg", "bortimus", "bottles", "boudicca", "boyd", "boyfriend", "brad", "branson", "breckon", "bree", "brewer", "bright", "brimtley", "brisket", "brock", "bront", "brooke", "bruno", "bryanayah", "bryce", "brynn", "buck", "buddy", "burke", "buster", "butch", "byron", "byung-hyun", "cactus", "cadence", "caim", "caleb", "caligula", "campos", "cannonball", "cantus", "cardamom", "carmelo", "carol", "carrol", "carter", "case", "cass", "cassidy", "castillo", "cat", "catmint", "cedric", "celeste", "celestial", "cell", "celo", "chadwick", "chambers", "chandra", "charlatan", "chester", "chet", "chibodee", "chip", "chips", "chorby", "chris", "christian", "churro", "cicero", "cindy", "cinnamon", "cissy", "clare", "claudio", "clementine", "clodius", "clove", "cody", "collins", "colton", "combs", "comfort", "commissioner", "concrete", "conditional", "conner", "conrad", "coolname", "corbyn", "cordula", "cornelius", "corriander", "cory", "cote", "cravel", "cricket", "crits", "crow", "cudi", "curry", "dabney", "daiya", "damir", "dander", "dani", "daniel", "danny", "darren", "dash", "dashiell", "dave", "davena", "david", "davie", "dax", "deangelo", "deandre", "declan", "demarkus", "denim", "denzel", "derrick", "dervin", "devon", "dexter", "dickerson", "didi", "dimi", "discovery", "djuna", "doc", "doginic", "dolores", "dominic", "domino", "don", "donia", "donna", "donnie", "douglas", "dovydas", "drea", "drew", "drosophila", "dudley", "dulce", "duncan", "dunlap", "dunn", "durham", "ed", "eddie", "eden", "edith", "edric", "eduardo", "eizabeth", "ekeko", "elijah", "eliot", "elip", "elisisor", "ellen", "ellie", "elliot", "elroy", "elsha", "elvis", "elwin", "emblem", "emilia", "emmet", "emmett", "emmy", "engine", "england", "enid", "ennead", "ephraim", "erica", "erickson", "erin", "eris", "esme", "esteban", "euclid", "eudora", "eugenia", "eurico", "evelton", "everett", "ezekiel", "fairwood", "famous", "faraday", "farrell", "feline", "felix", "fenry", "finn", "fionna", "fish", "fitzgerald", "flannery", "flattery", "fletcher", "florian", "fontaine", "forbes", "forrest", "foxy", "fran", "francisca", "francisco", "francois", "frank", "frankie", "françois", "frasier", "frazier", "freemium", "fynn", "gabriel", "gallup", "garcia", "geepa", "geordi", "georgina", "geraldine", "gerund", "gia", "gib", "ginny", "gita", "gizmo", "glabe", "gloria", "goeff", "goldy", "golem", "gomer", "goobie", "goodwin", "grant", "greer", "gregroy", "grey", "grimbo", "grit", "grollis", "guadalupe", "gunther", "gustavo", "guy", "gwen", "göran", "hadi", "hadleigh", "hahn", "halexandrey", "haman", "hands", "hank", "hans", "hapless", "harmon", "harold", "harper", "harriet", "harrington", "harry", "haruta", "hatfield", "hazel", "helga", "hen", "hendricks", "henevieve", "henry", "hercules", "hernando", "herring", "hewitt", "hierophantic", "higgins", "hildegard", "hillary", "hiroto", "hoagie", "hobbs", "holden", "hongo", "hops", "hotbox", "howell", "howie", "huber", "hubert", "hugs", "hui", "hurley", "hyena", "hyo-jin", "icarus", "ignacio", "igneus", "ilane", "ilhan", "inez", "ingrid", "inky", "ira", "irnee", "isaac", "isabella", "itsuki", "izuki", "jack", "jackie", "jackson", "jacob", "jacobus", "jacoby", "jada", "jade", "jake", "jam", "jame", "james", "jammy", "jan", "jana", "janet", "jaron", "jasmine", "jasper", "javier", "jaxon", "jay", "jayden", "jaylen", "jebediah", "jeff", "jefferson", "jeffier", "jeffrey", "jelly", "jem", "jenkins", "jenna", "jenny", "jesse", "jessi", "jessica", "jesus", "jesús", "ji-tae", "jim", "jimbo", "jo", "joana", "jode", "joe", "joel", "joey", "johannes", "johncy", "johndan", "johnny", "johnnyboy", "jolene", "jomgy", "jon", "jonathan", "jordan", "jorge", "jose", "joshua", "jose", "jot", "joyner", "juan", "juice", "juju", "julianne", "june", "junior", "justice", "justin", "jut", "jeff", "kaelin", "kai", "kaiba", "kaiden", "kaj", "kale", "kaloni", "kang-min", "karato", "karlee", "kathy", "katy", "kay", "kaylah", "kaz", "keanu", "keeley", "kelbasa", "kels", "kelvin", "kennedy", "kenny", "kevelyn", "kevin", "khaanyo", "khalid", "khulan", "kichiro", "kiki", "kina", "king", "kirkland", "kit", "kline", "knight", "kofi", "krzysztof", "kurt", "kylie", "lachlan", "lady", "lance", "lancelot", "landry", "lang", "langley", "lanie", "lars", "lawrence", "layla", "leach", "lee", "legory", "leif", "leliel", "len", "lenix", "lenjamin", "lenny", "leo", "les", "leticia", "lev", "lexi", "liam", "lili", "lily", "linda", "linnea", "linus", "lis", "livers", "livi", "lizzy", "logan", "london", "lorcan", "lorenzo", "lori", "lottie", "lotus", "lou", "loubert", "lowe", "lucas", "lucien", "lucy", "lucy-rose", "luis", "luka", "luna", "lurlene", "lydia", "lyndsey", "lyra", "madeline", "magi", "magie", "mags", "maisy", "malachi", "malcolm", "malik", "malin", "mambo", "manjula", "manu", "map", "marcellus", "marco", "marf", "margarito", "mariana", "marion", "markel", "marley", "marquez", "math", "matheo", "matteo", "mattias", "mavis", "mayra", "mcbaseball", "mckinley", "mccormick", "mcdowell", "mcfarland", "mckinney", "mclaughlin", "meera", "megan", "mel", "melba", "melton", "memorial", "mesmer", "meteora", "mia", "miah", "michael", "michelle", "mickey", "mieke", "miguel", "mikan", "mike", "miki", "milan", "miles", "milli", "millipede", "milner", "milo", "min-hyuk", "minato", "mindy", "minnie", "mint", "mira", "mo", "mohammed", "moira", "mokena", "mononymous", "montgomery", "moody", "mooney", "mordecai", "morgan", "morrow", "morte", "moses", "muggsy", "mullen", "mummy", "munavoi", "munro", "murphy", "murray", "muse", "nagomi", "nanci", "nandy", "natalie", "natha", "neerie", "neptunia", "nerd", "ness", "newton", "nic", "nicholas", "nickname", "nicky", "nicolae", "nikki", "niq", "nitzan", "nneka", "noah", "nolan", "nolanestophia", "nolastname", "noluvuyo", "noquiryn", "nora", "norman", "norris", "nova", "nuan", "nucleus", "nyx", "ogden", "ohmar", "oliver", "ooze", "ophelia", "orchid", "orion", "orpheus", "ortiz", "orville", "oscar", "ovid", "owen", "pacheco", "paco", "paige", "palomo", "pangolin", "pannonica", "parker", "patchwork", "patel", "patrick", "patty", "paul", "paula", "pavithra", "pavo", "peanut", "peanutiel", "pearl", "pedro", "peekaboo", "pelo", "pemmy", "penelope", "penny", "pepper", "percival", "persephone", "pete", "phil", "phineas", "pierogi", "pierre", "pigeon", "piper", "pippin", "planktos", "plums", "polkadot", "pollard", "poppy", "porkchop", "pranav", "premjeet", "prepper", "prince", "prophylaxis", "pudge", "pug", "qais", "quack", "quads", "quantum", "queithlein", "quill", "quinns", "qwazukee", "rafael", "rai", "ralph", "ram", "ramirez", "randall", "randy", "rat", "ray", "razz", "razzlynette", "raúl", "reb", "red", "reece", "reese", "reggie", "reia", "ren", "rey", "rhombus", "rhonda", "rhys", "rian", "richardson", "richmond", "ridley", "rigby", "riley", "rivers", "robbins", "robin", "rocha", "rocio", "rodriguez", "ron", "ronan", "ros", "rosa", "rosales", "rosalind", "roscoe", "rose", "rosemary", "rosey", "ross", "rosstin", "rudolph", "ruffian", "rufus", "rush", "ruslan", "russo", "rust", "ruth", "ryan", "rylan", "ryuji", "salamandra", "salem", "salih", "sam", "samothes", "sandford", "sandie", "sandoval", "santana", "saoirse", "sapphic", "sarahlynn", "sassy", "scarlet", "schneider", "schu", "scoobert", "scoop", "scores", "scouse", "scrap", "scratch", "scruffs", "sebastian", "seren", "serge", "sexton", "shane", "shannon", "shaquille", "sheev", "shelby", "sheri", "shirai", "shrimp", "sigmund", "silvaire", "silvia", "simba", "simon", "simone", "siobhan", "sixpack", "sleve", "slosh", "snyder", "so-hyun", "socks", "son", "sophie", "soraya", "sorrell", "sosa", "sparks", "spears", "speed", "spiff", "spits", "spradley", "squid", "squidgey", "stan", "stanislaw", "stasia", "stavros", "steals", "steph", "stephanie", "stephens", "stephon", "steve", "stevenson", "stew", "sticky", "stijn", "stitches", "stout", "strelitzia", "stu", "stylianos", "suede", "sullivan", "summers", "sunny", "suraj", "susananana", "sutton", "swamuel", "sweet", "sweets", "swish", "tad", "tai", "tallulah", "tamara", "tamsie", "tarn", "tavin", "telusaa", "terence", "terrell", "tevin", "theo", "theodore", "theophilous", "theryn", "thobeka", "thomas", "thrash", "tiana", "tiera", "tillman", "tim", "timmy", "titania", "tommy", "toni", "toomey", "torus", "tot", "tourmaline", "travis", "trevino", "trinity", "tristin", "truck", "tucker", "tuesday", "tuxie", "twofurious", "tybal", "tycho", "tyler", "tyreek", "tyrese", "ulrich", "umi", "una", "ursula", "usurper", "utena", "val", "valentine", "valueerror", "vannevar", "vasquez", "velasquez", "vernon", "vero", "veronica", "vessalius", "vidalia", "vinathan", "vinny", "viola", "violet", "vipsanius", "vito", "vivian", "wade", "waffles", "wall", "wally", "walton", "wanda", "washer", "weeble", "wei", "wendy", "wes", "wesley", "whimsy", "whit", "wichita", "wiley", "wilkerson", "will", "william", "willow", "wilma", "wilson", "winnie", "workman", "wyatt", "xandra", "ximena", "xiu", "yams", "yanna", "yasslyn", "yazmin", "yeong-ho", "yong", "york", "yosh", "yrjö", "yulia", "yummy", "yurts", "yusef", "yusuf", "zack", "zaine", "zane", "zap", "zeboriah", "zee", "zeke", "zelda", "zenzi", "zephyr", "zeruel", "zesty", "zi", "zion", "zippy", "ziwa", "zoey", "zohaib", "zutara", ]; pub const LAST_NAMES: &'static [&'static str] = &[ "abbott", "acevedo", "adams", "adamses", "airport", "alfredo", "aliciakeyes", "alighieri", "almeida", "alonzo", "alstott", "alvarado", "ampersand", "andante", "anene", "angry", "anice", "anteater", "anthony", "applesauce", "aqualuft", "arias", "arkady", "armstrong", "ashby", "ashwell", "aster", "atkinson", "atomic", "babatunde", "bailey", "baker", "ball", "ballard", "ballson", "balton", "banananana", "barajas", "baresi", "barios", "bark", "barker", "barlow", "barnes", "baron", "barrel", "bartell", "bartlette", "baserunner", "baskerville", "basquez", "bates", "bathtub", "batson", "bean", "beanbag", "beanpot", "beans", "beard", "beats", "bedard", "bedazzle", "bedframe", "beefsteak", "beeks", "belair", "belfy", "bellamy", "bendie", "benedicte", "benitez", "bentley", "berger", "bergeron", "berrigan", "best", "beyonce", "biancardi", "biblioteca", "bickle", "biederman", "bimblebottom", "birdfather", "birkenhagen", "biscuits", "bishop", "bittercorn", "blackburn", "blacksmith", "blanco", "blaseseer", "blaskets", "blather", "blomberg", "blortles", "blounder", "blueberry", "blueglass", "bluesky", "bluma", "bobson", "boingo", "bondo", "bong", "bookbaby", "boone", "bootleg", "borg", "boston", "bowen", "bowers", "boy", "boyea", "bradley", "braga", "breadwinner", "briggs", "bronx", "brothers", "brown", "browning", "buckley", "buckridge", "bugsnax", "bullock", "bundelle", "bunion", "buntoes", "burgertoes", "burkhard", "burton", "butt", "buttercup", "butts", "byrd", "byron", "cabal", "cain", "calvino", "camera", "campbell", "campos", "canberra", "candle", "cantburn", "capybara", "caracal", "carb", "carberry", "cardenas", "carpenter", "carver", "cash", "cashmoney", "caster", "castillo", "catalina", "catpashman", "cave", "ceilingfan", "celestina", "cena", "cerna", "cervantes", "cerveza", "chadwell", "chamberlain", "chamomile", "chang", "charcuterie", "chark", "chen", "chi", "chickadee", "chickensalt", "chill", "chimes", "chin", "cholewinski", "church", "cilantro", "cimino", "clab", "clambucket", "clampner", "clark", "clembons", "clemency", "clutch", "cobb", "coleman", "collins", "colon", "comas", "combs", "comeback", "content", "cookbook", "coopwood", "cornbread", "correia", "costa", "cotterpin", "cotton", "crankit", "crawford", "cresthill", "crikey", "cross", "crossing", "crounse", "crueller", "crumb", "crumpet", "crunch", "crutch", "culler", "cuthbert", "cylinder", "danger", "darkness", "datalake", "davids", "davis", "day", "debaskervilles", "demarzen", "desheilds", "deshields", "dean", "decksetter", "delacruz", "delaney", "deleuze", "dembélé", "denardi", "denman", "dennis", "destiny", "dewey", "di batterino", "diaz", "dice", "dickerson", "doctor", "dogwalker", "dollie", "donaldson", "dosime", "dotcom", "dougnut", "dovenpart", "doyle", "dracaena", "drama", "draper", "dreamy", "drennan", "drobot", "droodle", "drumsolo", "dry", "duckdinner", "dudley", "duduk", "duende", "duffy", "duggins", "dumpington", "dunno", "duo", "duodenum", "duran", "durango", "duress", "duvill", "easterbrook", "eberhardt", "eckhardt", "edwards", "eggburt", "eggleton", "eigengrau", "elemefayo", "elftower", "elliott", "ender", "england", "english", "enjoyable", "erock", "escobar", "espinoza", "estes", "evergreen", "facepunch", "fairwood", "falconer", "familia", "fantastic", "fardo", "fashion", "feather", "fenestrate", "ferguson", "ferraro", "fiasco", "fiesta", "fig", "fightcastle", "figueroa", "findlay", "fingerguns", "firestar", "firestone", "firewall", "fischer", "flahwah", "fledermaus", "flemming", "flex", "flores", "flum", "foamcore", "foible", "forbes", "fougere", "fouqet", "fox", "francobollo", "frank", "franklin", "frederick", "freed", "freeman", "friday", "friedrich", "friendo", "frihart", "fring", "frost", "frosting", "frumple", "furnace", "gagnon", "gallant", "galley", "galvanic", "games", "garbage", "garcia", "garner", "gawrsh", "george", "gesundheit", "ghighi", "giant", "gibas", "gigstad", "gildehaus", "givens", "givewell", "glass", "gleiss", "gloom", "glover", "glump", "goblin", "goedecke", "golightly", "gonzales", "gonzalez", "goo", "good", "goodhart", "gooseball", "gooseberry", "gorczyca", "gorge", "grackle", "grassly", "greatness", "greenlemon", "griffin", "griffith", "gritt", "groberg", "groblonx", "grocer", "gubbins", "guerra", "guerreiro", "gulp", "guzman", "gwiffin", "haddad", "hairston", "haley", "halifax", "hambone", "hambright", "hamburger", "hammer", "hardaway", "harding", "hardison", "harper", "harrell", "harrington", "harrison", "harrow", "harvey", "harvie", "hatchler", "haunt", "hayes", "haynes", "haza", "heartlight", "heat", "henderson", "hendler", "hendricks", "henriques", "herman", "hernandez", "herrold", "hess", "highlife", "highway", "hildebert", "hirsch", "hitherto", "hobbity", "hockeypuck", "hojo", "holbrook", "holloway", "hollywood", "homestyle", "honey", "honeywell", "hookrace", "horne", "horseman", "hotdogfingers", "houndlog", "howard", "howe", "hu", "hubet", "hubette", "huerta", "huhtala", "humdinger", "hunter", "hyperpop", "immenga", "inagame", "incarnate", "ingram", "innamorato", "inningson", "internet", "irby", "isarobot", "italodisco", "ito", "izquierda", "jackson", "james", "javier", "jaylee", "jeff", "jeggings", "jensen", "jesaulenko", "jespersen", "ji", "ji-eun", "johnson", "jokes", "jonbois", "jones", "judochop", "junebug", "junior jr", "kalette", "kane", "kappen jr.", "karim", "kath", "kehl", "kelp", "keming", "kendrick", "kennedy", "kenny", "kensington", "kerfuffle", "kerwin", "kesh", "keyes", "kiddo", "kiebala", "kim", "kimball", "king", "kingbird", "kirby", "kirchner", "kisselburg", "klaich", "knuckles", "koch", "koeppe", "konderla", "koning", "konk", "kramer", "kranch", "kravitz", "krill", "kropotkin", "krueger", "ksipra", "kugel", "kyser", "labelle", "laabs", "ladd", "ladrona", "lamani", "lampman", "lancaster", "langzone", "lanyard", "laplace", "larsen", "lascu", "latch", "latenight", "latke", "lauer", "laurie", "lawson", "lazarus", "leblanc", "lemath", "leaf", "leal", "leatherman", "lee", "leeks", "lemma", "lenny", "li", "lightner", "lin", "linard", "lincecum", "lingardo", "liu", "logan", "lompa", "longarms", "loofah", "lopez", "loser", "lott", "lotte", "lotus", "loveless", "lozano", "lutefisk", "mac", "macintosh", "maclear", "madrigal", "mae", "magpie", "mahle", "makin", "malackey", "maldonado", "mallow", "manco", "mandible", "mango", "manhattan", "mantilla", "marama", "marijuana", "marlow", "marovic", "marsh", "marshallow", "marzen", "mason", "massey", "mathews", "matos", "matsuyama", "matte", "mauser", "maybane", "maybe", "mayonnaise", "mcblase", "mccloud", "mccoy", "mcelroy", "mcg", "mcghee", "mcgill", "mcgribbits", "mckinley", "mcsriff", "mccall", "mcdaniel", "meadows", "meatbrick", "meh", "melcon", "melgoza", "melo", "melon", "melton", "mendizza", "mendoza", "meng", "merritt", "metzger", "michelotti", "michet", "midcentury", "middlebrook", "milicic", "mina", "mininger", "miran", "mist", "mitchell", "mocha", "mondale", "mondegreen", "monreal", "monstera", "monteiro", "moodley", "moon", "mora", "moran", "moreno", "morin", "morse", "moss", "mueller", "muggins", "murphy", "mustard", "myers", "nakamoto", "nakamura", "nameperson", "nanda", "narismulu", "nash", "nattee", "nava", "nelson", "neske", "nettle", "ng", "ngozi", "nibb", "nightmare", "nocturne", "nolan", "nopales", "norindr", "noscope", "notarobot", "novak", "nugget", "nyeos", "nyong'o", "o'brian", "o'lantern", "object", "obrien", "oconnor", "octothorp", "oki", "oko", "olive", "oliveira", "omelette", "osborn", "otherman", "otten", "outlaw", "overbey", "owens", "owlbears", "o’houlihan", "pace", "pacheco", "paider", "paint", "painter", "palladium", "pamplin", "pancakes", "pantheocide", "park", "parra", "passon", "pasta", "patchwork", "pate", "patterson", "payment", "peacelily", "pebble", "peck", "peep", "peeps", "pelagos", "peperomioides", "pepperdile", "perez", "permadeath", "peterson", "petty", "piazza", "picklestein", "pinceau", "pingleton", "pink", "pleck", "pliskin", "plums", "po", "podcast", "pony", "poole", "portmanteau", "potatorade", "pothos", "powers", "preisendorf", "prestige", "preston", "prettygood", "prowler", "pruessner", "puddles", "pynchon", "quartz", "quimby", "quitter", "ra", "rambutan", "ramos", "ramsey", "rangel", "rascal", "ratoon", "reddick", "redlight", "redox", "reeves", "relish", "ren", "rice", "richardson", "rincón", "ringmaster", "risset", "rivera", "roadhouse", "robbie", "robins", "robot", "robotnivic", "rocha", "roche", "rochester", "rodgers", "rodriguez", "rogers", "roland", "rolsenthal", "romayne", "ronero", "ronzoni", "root", "rosa", "rosales", "roseheart", "ross", "rotato", "rounder", "rubberbat", "rubberman", "rugrat", "ruiz", "rush", "ruth", "rutledge", "ryman", "saathoff", "saetang", "safari", "sagaba", "salad", "salt", "sanchez", "sanders", "sands", "santana", "santiago", "sasquatch", "sato", "scandal", "scantron", "schenn", "schiefer", "schmitt", "schofield", "schumacher", "scolopax", "scoresburg", "scorpler", "scotch", "scott", "scrobbles", "scrollbar", "scuttlebug", "seabright", "seagull", "seasalt", "sedillo", "seeth", "segee", "selach", "semiquaver", "septemberish", "seraph", "serotonin", "sharpe", "shelton", "shmurmgle", "short", "shortvat", "shotwell", "shriffle", "shufflecat", "shupe", "sierpinski", "silk", "simmons", "simpson", "skagerrak", "skitter", "sky", "slice", "sliders", "slugger", "slumps", "slurms", "smaht", "small", "smith", "snail", "snapjaw", "snart", "snodgrass", "snyder", "soares", "sobremesa", "sokol", "solis", "solo", "song", "soto", "soul", "soun", "sounders", "southwick", "spaceman", "sparks", "sparrow", "speedrun", "spheroid", "spieth", "splendor", "spliff", "splotter", "spoon", "sports", "sportsman", "spruce", "squall", "squantorini", "standlake", "stanton", "star", "starling", "statter", "statter jr.", "steakknife", "steeplechase", "stegmann", "stickybeak", "stink", "stompman", "strawberry", "street", "strewnberry", "strife", "stringlight", "stromboli", "strongbody", "succotash", "suljak", "summer", "sun", "sundae", "sunkcost", "sunset", "sunshine", "suplex", "sutherland", "suzanne", "suzuki", "swagger", "swain", "swan", "swandre", "swank", "swift", "swine", "swinger", "synomyn", "tabby", "tables", "tails", "takahashi", "tanaka", "tankris", "tasmin", "taswell", "tattersall", "taylor", "teixeira", "telephone", "tenderson", "tenley", "terermorphasis", "thane", "thibault", "thompson", "threetimes", "throck", "throckmorton", "throg", "throgmorten", "thwompson", "toast", "toaster", "tokkan", "tolleson", "tooke", "toothcake", "torres", "tosser", "towns", "townsend", "tratnyek", "treadstone", "tredwell", "trefeather", "triumphant", "trololol", "trombone", "truk", "tugboat", "tumblehome", "turner", "turnip", "turquoise", "twelve", "ultrabass", "underbuck", "uniondues", "uppercutski", "ups", "ursino", "vainglory", "valadez", "valenzuela", "vandermale", "vanstrander", "vapor", "vargas", "vaughan", "velazquez", "vesperidian", "vincent", "vine", "viney", "violence", "violet", "vodka", "voorhees", "wagg", "walker", "wallace", "wallop", "walton", "wan", "wanda", "wanderlust", "warhorse", "warren", "washington", "watson", "watts", "weatherman", "weeks", "weir", "wells", "wetchup", "whammy", "wheeler", "wheerer", "whelp", "whiskey", "whitney", "wigdoubt", "wilcox", "wildarms", "williams", "willow", "willowtree", "wilson", "winfield", "winkler", "winner", "winter", "winters", "wise", "wobin", "woman", "wood", "woodman", "woods", "wooly", "woomy", "wooten", "wormthrice", "wright", "wuppo", "wyeth", "xu", "yaboi", "yamamoto", "yamashita", "yardstick", "yarrum", "yesterday", "yolk", "youngblood", "yuniesky", "zavala", "zeagler", "zenith", "zephyr", "zhao", "zheng", "zhivago", "zhuge", "zimmerman", "zonker", "zoobrambana", "de-vos", ]; ================================================ FILE: lib/rotbart/src/elfs.rs ================================================ pub const ADJECTIVES: &'static [&'static str] = &[ "able", "abnorma", "again", "airexpl", "ang", "anger", "asail", "attack", "aurora", "awl", "ban", "band", "bare", "beat", "beated", "belly", "bind", "bite", "bloc", "blood", "body", "book", "breath", "bump", "cast", "cham", "clamp", "clap", "claw", "clear", "cli", "clip", "cloud", "contro", "convy", "coolhit", "crash", "cry", "cut", "descri", "d-fight", "dig", "ditch", "div", "doz", "dre", "dul", "du-pin", "dye", "earth", "edu", "eg-bomb", "egg", "elegy", "ele-hit", "embody", "empli", "engl", "erupt", "evens", "explor", "eyes", "fall", "fast", "f-car", "f-dance", "fears", "f-fight", "fight", "fir", "fire", "firehit", "flame", "flap", "flash", "flew", "force", "fra", "freeze", "frog", "g-bird", "genkiss", "gift", "g-kiss", "g-mouse", "grade", "grow", "hammer", "hard", "hat", "hate", "h-bomb", "hell-r", "hemp", "hint", "hit", "hu", "hunt", "hypnosi", "inha", "iro", "ironbar", "ir-wing", "j-gun", "kee", "kick", "knif", "knife", "knock", "level", "ligh", "lighhit", "light", "live", "l-wall", "mad", "majus", "mel", "melo", "mess", "milk", "mimi", "miss", "mixing", "move", "mud", "ni-bed", "noisy", "noonli", "null", "n-wave", "pat", "peace", "pin", "plan", "plane", "pois", "pol", "powde", "powe", "power", "prize", "protect", "proud", "rage", "recor", "reflac", "refrec", "regr", "reliv", "renew", "r-fight", "ring", "rkick", "rock", "round", "rus", "rush", "sand", "saw", "scissor", "scra", "script", "seen", "server", "shadow", "shell", "shine", "sho", "sight", "sin", "small", "smelt", "smok", "snake", "sno", "snow", "sou", "so-wave", "spar", "spec", "spid", "s-pin", "spra", "stam", "stare", "stea", "stone", "storm", "stru", "strug", "studen", "subs", "sucid", "sun-lig", "sunris", "suply", "s-wave", "tails", "tangl", "taste", "telli", "thank", "tonkick", "tooth", "torl", "train", "trikick", "tunge", "volt", "wa-gun", "watch", "wave", "w-bomb", "wfall", "wfing", "whip", "whirl", "wind", "wolf", "wood", "wor", "yuja", ]; pub const NOUNS: &'static [&'static str] = &[ "seed", "grass", "flowe", "shad", "cabr", "snake", "gold", "cow", "guiki", "pedal", "delan", "b-fly", "bide", "keyu", "fork", "lap", "pige", "pijia", "caml", "lat", "bird", "baboo", "viv", "aboke", "pikaq", "rye", "san", "bread", "lidel", "lide", "pip", "pikex", "rok", "jugen", "pud", "bude", "zhib", "gelu", "gras", "flow", "laful", "ath", "bala", "corn", "moluf", "desp", "daked", "mimi", "bolux", "koda", "gelud", "monk", "sumoy", "gedi", "wendi", "nilem", "nile", "nilec", "kezi", "yongl", "hude", "wanli", "geli", "guail", "madaq", "wuci", "wuci", "mujef", "jelly", "sicib", "gelu", "neluo", "boli", "jiale", "yed", "yede", "clo", "scare", "aoco", "dede", "dedei", "bawu", "jiug", "badeb", "badeb", "hole", "balux", "ges", "fant", "quar", "yihe", "swab", "slipp", "clu", "depos", "biliy", "yuano", "some", "no", "yela", "empt", "zecun", "xiahe", "bolel", "deji", "macid", "xihon", "xito", "luck", "menji", "gelu", "deci", "xide", "dasaj", "dongn", "ricul", "minxi", "baliy", "zenda", "luzel", "hele5", "0fenb", "kail", "jiand", "carp", "jinde", "lapu", "mude", "yifu", "linli", "sandi", "husi", "jinc", "oumu", "oumux", "cap", "kuiza", "pud", "tiao", "frman", "clau", "spark", "drago", "boliu", "guail", "miyou", "miy", "qiaok", "beil", "mukei", "rided", "madam", "bagep", "croc", "alige", "oudal", "oud", "dada", "hehe", "yedea", "nuxi", "nuxin", "rouy", "aliad", "stick", "qiang", "laand", "piqi", "pi", "pupi", "deke", "dekej", "nadi", "nadio", "mali", "pea", "elect", "flowe", "mal", "mali", "hushu", "nilee", "yuzi", "popoz", "duzi", "heba", "xian", "shan", "yeyea", "wuy", "luo", "kefe", "hula", "crow", "yadeh", "mow", "annan", "suoni", "kyli", "hulu", "hudel", "yehe", "gulae", "yehe", "blu", "gelan", "boat", "nip", "poit", "helak", "xinl", "bear", "linb", "mageh", "magej", "wuli", "yide", "rive", "fish", "aogu", "delie", "mante", "konmu", "delu", "helu", "huan", "huma", "dongf", "jinca", "hede", "defu", "liby", "jiapa", "meji", "hele", "buhu", "milk", "habi", "thun", "gard", "don", "yangq", "sanaq", "banq", "luj", "phix", "siei", "egg", ]; ================================================ FILE: lib/rotbart/src/lib.rs ================================================ mod blaseball; mod elfs; mod mlp_fim; mod pokemon; mod xc1; mod xc2; lazy_static::lazy_static! { pub static ref COMBINED_ADJ: Vec<&'static str> = { let mut adjs: Vec<&str> = vec![]; adjs.extend(xc1::ADJECTIVES.iter()); adjs.extend(xc2::ADJECTIVES.iter()); adjs.extend(elfs::ADJECTIVES.iter()); adjs.extend(blaseball::FIRST_NAMES.iter()); adjs.sort(); adjs.dedup(); adjs }; pub static ref COMBINED_NOUN: Vec<&'static str> = { let mut nouns: Vec<&str> = vec![]; nouns.extend(xc1::NOUNS.iter()); nouns.extend(xc2::NOUNS.iter()); nouns.extend(xc2::COMMON_BLADES.iter()); nouns.extend(elfs::NOUNS.iter()); nouns.extend(mlp_fim::PONIES.iter()); nouns.extend(pokemon::POKEDEX.iter()); nouns.extend(blaseball::LAST_NAMES.iter()); nouns.sort(); nouns.dedup(); nouns }; } pub fn unique_monster() -> Option { let mut generator = names::Generator::new(&COMBINED_ADJ, &COMBINED_NOUN, names::Name::Plain); generator.next() } ================================================ FILE: lib/rotbart/src/mlp_fim.rs ================================================ pub const PONIES: &'static [&'static str] = &[ // earth ponies "applejack", "pinkie-pie", "aloe", "butternut", "cheerilee", "coloratura", "dr-fauna", "junebug", "lighthoof", "mane-iac", "roma", "torch-song", "wrangler", "zesty", "big-bucks", "braeburn", "burnt-oak", "cattail", "code-red", "dr-horse", "gizmo", "gladmane", "hard-hat", "mr-stripes", "mudbriar", "oak-nut", "rockhoof", "sans-smirk", "starstreak", "svengallop", "toe-tapper", "twisty-pop", "apple-top", "jonagold", "magdalena", "red-gala", "sundowner", "apple-core", "bushel", "wensley", "avalon", "bell-perin", "belle-star", "berryshine", "betty-hoof", "blue-bows", "blue-cutie", "blue-nile", "bonnie", "bottlecap", "butter-pop", "candy-mane", "carlotta", "cascada", "charged-up", "cornflower", "crescendo", "creamcup", "cultivar", "daisy", "doseydotes", "dry-wheat", "floral-pan", "flounder", "flurry", "frou-frou", "hoda-kotb", "honey-dew", "jinx", "jorunn", "jubileena", "lady-gaval", "little-po", "long-shot", "luckette", "lucky-star", "majesty", "maribelle", "maybelline", "meadowluck", "millie", "mint-swirl", "minty", "mjolna", "oakey-doke", "obscurity", "offbeat", "penny-ante", "play-write", "rogue-ruby", "rose", "rosemary", "rosetta", "roxie-rave", "screwball", "screwy", "seasong", "serena", "shoeshine", "sky-view", "soft-spot", "soot-stain", "sun-streak", "sunfire", "surf", "sweetberry", "tarantella", "toffee", "tough-love", "tree-sap", "viola", "welly", "yuma-spurs", "zen-moment", "ace-point", "adante", "affero", "al-roker", "b-sharp", "baritone", "beuford", "big-top", "mr-breezy", "caboose", "comb-over", "cormano", "davenport", "dirtbound", "don-neigh", "dr-hooves", "eiffel", "end-zone", "felix", "free-throw", "funnel-web", "full-steam", "hay-fever", "heisenbuck", "hercules", "icy-drop", "iron-bark", "jim-beam", "john-bull", "kazooie", "klein", "leadwing", "levon-song", "lincoln", "matt-lauer", "mccree", "melilot", "noteworthy", "opulence", "pink-drink", "ragtime", "rivet", "royal-riff", "shamrock", "shiny-pear", "shortround", "smokestack", "sour-drops", "sourpuss", "star-gazer", "tall-order", "tall-tale", "two-ton", "vegemite", "wetzel", "wisp", "mr-zippy", "apple-rose", "aquamarine", "charm", "derpy", "drizzle", "fine-line", "fluttershy", "helia", "lemony-gem", "little-red", "merry-may", "minuette", "parasol", "peach-fuzz", "rarity", "sassaflash", "scootaloo", "sea-swirl", "swan-song", "twist", "yona", "comet-tail", "cosmic", "first-base", "grand-pear", "log-jam", "mane-moon", "rare-find", "sand-trap", "sandbar", "starburst", "thorn", "mr-waddle", "warm-front", "whiplash", // pegasi "fluttershy", "buttershy", "daring-do", "flitter", "hoops", "inky-rose", "open-skies", "snowdash", "somnambula", "sunshower", "fleetfoot", "soarin", "spitfire", "blaze", "high-winds", "misty-fly", "sun-chaser", "surprise", "wave-chill", "wind-waker", "wind-rider", "fast-clip", "tight-ship", "whiplash", "icy-rain", "lime-jelly", "parasol", "sassaflash", "sightseer", "starburst", "thorn", "warm-front", "whitewash", "wild-fire", "aqua-burst", "big-bell", "big-shot", "blue-buck", "bluebell", "bon-voyage", "buddy", "cool-beans", "cosmic", "cotton-sky", "descent", "dewdrop", "downdraft", "drizzle", "dusty-gust", "eff-stop", "geronimo", "grape-soda", "helia", "high-note", "honey-rays", "jetstream", "laurette", "merry-may", "mind-freak", "muggy-air", "parula", "pink-cloud", "prim-posy", "q-t-prism", "rain-dance", "rainy-day", "riverdance", "rosewing", "sandstorm", "serenity", "silverwing", "sky-flower", "skyra", "slipstream", "snowslide", "sugarshine", "sunlight", "sunny-rays", "sunstone", "sweet-buzz", "tiger-lily", "tut-junah", "wind-chill", "applejack", "berryshine", "caramel", "chip-mint", "dr-hooves", "felix", "hermes", "luckette", "noteworthy", "offbeat", "pinkie-pie", "pound-cake", "rivet", "scootaloo", "sea-swirl", "shoeshine", "wisp", // unicorns "rarity", "bluenote", "claude", "clear-sky", "fire-flare", "firelight", "flam", "flim", "hoofdini", "jack-pot", "jet-set", "joe", "lily-lace", "merry", "mistmane", "stygian", "sunburst", "aloha", "arpeggio", "bags-valet", "ballad", "beyond", "blue-belle", "blue-moon", "charm", "cinnabelle", "cold-front", "comet-tail", "coral-bits", "dj-pon-3", "dr-steth", "eliza", "fat-stacks", "fine-catch", "fine-line", "fly-wishes", "four-step", "foxxy-trot", "fresh-coat", "giza-hafir", "holly-dash", "infinity", "juno", "lemony-gem", "lilly-love", "log-jam", "lolli-love", "minuette", "mossy-rock", "nachtmusik", "nixie", "nook", "orchid-dew", "passionate", "pinny-lane", "pixie", "poppycock", "precious", "rachel-hay", "rare-find", "red-rose", "royal-pin", "say-cheese", "sea-swirl", "slapshot", "south-pole", "spellbound", "sugarberry", "sunspot", "swan-song", "top-marks", "undertone", "cultivar", "daisy", "discord", "noteworthy", "offbeat", "quake", "red-gala", "rose", "rosemary", "waxton", ]; ================================================ FILE: lib/rotbart/src/pokemon.rs ================================================ pub const POKEDEX: &'static [&'static str] = &[ "abomasnow", "abra", "absol", "accelgor", "aegislash", "aerodactyl", "aggron", "aipom", "alakazam", "alomomola", "altaria", "amaura", "ambipom", "amoonguss", "ampharos", "anorith", "araquanid", "arbok", "arcanine", "arceus", "archen", "archeops", "ariados", "armaldo", "aromatisse", "aron", "articuno", "audino", "aurorus", "avalugg", "axew", "azelf", "azumarill", "azurill", "bagon", "baltoy", "banette", "barbaracle", "barboach", "basculegion", "basculin", "bastiodon", "bayleef", "beartic", "beautifly", "beedrill", "beheeyem", "beldum", "bellossom", "bellsprout", "bergmite", "bewear", "bibarel", "bidoof", "binacle", "bisharp", "blacephalon", "blastoise", "blaziken", "blissey", "blitzle", "boldore", "bonsly", "bouffalant", "bounsweet", "braixen", "braviary", "breloom", "brionne", "bronzong", "bronzor", "bruxish", "budew", "buizel", "bulbasaur", "buneary", "bunnelby", "burmy", "butterfree", "buzzwole", "cacnea", "cacturne", "camerupt", "carbink", "carnivine", "carracosta", "carvanha", "cascoon", "castform", "caterpie", "celebi", "celesteela", "chandelure", "chansey", "charizard", "charjabug", "charmander", "charmeleon", "chatot", "cherrim", "cherubi", "chesnaught", "chespin", "chikorita", "chimchar", "chimecho", "chinchou", "chingling", "cinccino", "clamperl", "clauncher", "clawitzer", "claydol", "clefable", "clefairy", "cleffa", "cloyster", "cobalion", "cofagrigus", "combee", "combusken", "comfey", "conkeldurr", "corphish", "corsola", "cosmoem", "cosmog", "cottonee", "crabominable", "crabrawler", "cradily", "cranidos", "crawdaunt", "cresselia", "croagunk", "crobat", "croconaw", "crustle", "cryogonal", "cubchoo", "cubone", "cutiefly", "cyndaquil", "darkrai", "darmanitan", "dartrix", "darumaka", "decidueye", "dedenne", "deerling", "deino", "delcatty", "delibird", "delphox", "deoxys", "dewgong", "dewott", "dewpider", "dhelmise", "dialga", "diancie", "diggersby", "diglett", "ditto", "dodrio", "doduo", "donphan", "doublade", "dragalge", "dragonair", "dragonite", "drampa", "drapion", "dratini", "drifblim", "drifloon", "drilbur", "drowzee", "druddigon", "ducklett", "dugtrio", "dunsparce", "duosion", "durant", "dusclops", "dusknoir", "duskull", "dustox", "dwebble", "eelektrik", "eelektross", "eevee", "ekans", "electabuzz", "electivire", "electrike", "electrode", "elekid", "elgyem", "emboar", "emolga", "empoleon", "enamorus", "entei", "escavalier", "espeon", "espurr", "excadrill", "exeggcute", "exeggutor", "exploud", "farfetch'd", "fearow", "feebas", "fennekin", "feraligatr", "ferroseed", "ferrothorn", "finneon", "flaaffy", "flabébé", "flareon", "fletchinder", "fletchling", "floatzel", "floette", "florges", "flygon", "fomantis", "foongus", "forretress", "fraxure", "frillish", "froakie", "frogadier", "froslass", "furfrou", "furret", "gabite", "gallade", "galvantula", "garbodor", "garchomp", "gardevoir", "gastly", "gastrodon", "genesect", "gengar", "geodude", "gible", "gigalith", "girafarig", "giratina", "glaceon", "glalie", "glameow", "gligar", "gliscor", "gloom", "gogoat", "golbat", "goldeen", "golduck", "golem", "golett", "golisopod", "golurk", "goodra", "goomy", "gorebyss", "gothita", "gothitelle", "gothorita", "gourgeist", "granbull", "graveler", "greninja", "grimer", "grotle", "groudon", "grovyle", "growlithe", "grubbin", "grumpig", "gulpin", "gumshoos", "gurdurr", "guzzlord", "gyarados", "hakamo-o", "happiny", "hariyama", "haunter", "hawlucha", "haxorus", "heatmor", "heatran", "heliolisk", "helioptile", "heracross", "herdier", "hippopotas", "hippowdon", "hitmonchan", "hitmonlee", "hitmontop", "ho-oh", "honchkrow", "honedge", "hoopa", "hoothoot", "hoppip", "horsea", "houndoom", "houndour", "huntail", "hydreigon", "hypno", "igglybuff", "illumise", "incineroar", "infernape", "inkay", "ivysaur", "jangmo-o", "jellicent", "jigglypuff", "jirachi", "jolteon", "joltik", "jumpluff", "jynx", "kabuto", "kabutops", "kadabra", "kakuna", "kangaskhan", "karrablast", "kartana", "kecleon", "keldeo", "kingdra", "kingler", "kirlia", "klang", "kleavor", "klefki", "klink", "klinklang", "koffing", "komala", "kommo-o", "krabby", "kricketot", "kricketune", "krokorok", "krookodile", "kyogre", "kyurem", "lairon", "lampent", "landorus", "lanturn", "lapras", "larvesta", "larvitar", "latias", "latios", "leafeon", "leavanny", "ledian", "ledyba", "lickilicky", "lickitung", "liepard", "lileep", "lilligant", "lillipup", "linoone", "litleo", "litten", "litwick", "lombre", "lopunny", "lotad", "loudred", "lucario", "ludicolo", "lugia", "lumineon", "lunala", "lunatone", "lurantis", "luvdisc", "luxio", "luxray", "lycanroc", "machamp", "machoke", "machop", "magby", "magcargo", "magearna", "magikarp", "magmar", "magmortar", "magnemite", "magneton", "magnezone", "makuhita", "malamar", "mamoswine", "manaphy", "mandibuzz", "manectric", "mankey", "mantine", "mantyke", "maractus", "mareanie", "mareep", "marill", "marowak", "marshadow", "marshtomp", "masquerain", "mawile", "medicham", "meditite", "meganium", "melmetal", "meloetta", "meltan", "meowstic", "meowth", "mesprit", "metagross", "metang", "metapod", "mew", "mewtwo", "mienfoo", "mienshao", "mightyena", "milotic", "miltank", "mime-jr", "mimikyu", "minccino", "minior", "minun", "misdreavus", "mismagius", "moltres", "monferno", "morelull", "mothim", "mr-mime", "mudbray", "mudkip", "mudsdale", "muk", "munchlax", "munna", "murkrow", "musharna", "naganadel", "natu", "necrozma", "nidoking", "nidoqueen", "nidoran♀", "nidoran♂", "nidorina", "nidorino", "nihilego", "nincada", "ninetales", "ninjask", "noctowl", "noibat", "noivern", "nosepass", "numel", "nuzleaf", "octillery", "oddish", "omanyte", "omastar", "onix", "oranguru", "oricorio", "oshawott", "overqwil", "pachirisu", "palkia", "palossand", "palpitoad", "pancham", "pangoro", "panpour", "pansage", "pansear", "paras", "parasect", "passimian", "patrat", "pawniard", "pelipper", "persian", "petilil", "phanpy", "phantump", "pheromosa", "phione", "pichu", "pidgeot", "pidgeotto", "pidgey", "pidove", "pignite", "pikachu", "pikipek", "piloswine", "pineco", "pinsir", "piplup", "plusle", "poipole", "politoed", "poliwag", "poliwhirl", "poliwrath", "ponyta", "poochyena", "popplio", "porygon", "porygon-z", "porygon2", "primarina", "primeape", "prinplup", "probopass", "psyduck", "pumpkaboo", "pupitar", "purrloin", "purugly", "pyroar", "pyukumuku", "quagsire", "quilava", "quilladin", "qwilfish", "raichu", "raikou", "ralts", "rampardos", "rapidash", "raticate", "rattata", "rayquaza", "regice", "regigigas", "regirock", "registeel", "relicanth", "remoraid", "reshiram", "reuniclus", "rhydon", "rhyhorn", "rhyperior", "ribombee", "riolu", "rockruff", "roggenrola", "roselia", "roserade", "rotom", "rowlet", "rufflet", "sableye", "salamence", "salandit", "salazzle", "samurott", "sandile", "sandshrew", "sandslash", "sandygast", "sawk", "sawsbuck", "scatterbug", "sceptile", "scizor", "scolipede", "scrafty", "scraggy", "scyther", "seadra", "seaking", "sealeo", "seedot", "seel", "seismitoad", "sentret", "serperior", "servine", "seviper", "sewaddle", "sharpedo", "shaymin", "shedinja", "shelgon", "shellder", "shellos", "shelmet", "shieldon", "shiftry", "shiinotic", "shinx", "shroomish", "shuckle", "shuppet", "sigilyph", "silcoon", "silvally", "simipour", "simisage", "simisear", "skarmory", "skiddo", "skiploom", "skitty", "skorupi", "skrelp", "skuntank", "slaking", "slakoth", "sliggoo", "slowbro", "slowking", "slowpoke", "slugma", "slurpuff", "smeargle", "smoochum", "sneasel", "sneasler", "snivy", "snorlax", "snorunt", "snover", "snubbull", "solgaleo", "solosis", "solrock", "spearow", "spewpa", "spheal", "spinarak", "spinda", "spiritomb", "spoink", "spritzee", "squirtle", "stakataka", "stantler", "staraptor", "staravia", "starly", "starmie", "staryu", "steelix", "steenee", "stoutland", "stufful", "stunfisk", "stunky", "sudowoodo", "suicune", "sunflora", "sunkern", "surskit", "swablu", "swadloon", "swalot", "swampert", "swanna", "swellow", "swinub", "swirlix", "swoobat", "sylveon", "taillow", "talonflame", "tangela", "tangrowth", "tapu-bulu", "tapu-fini", "tapu-koko", "tapu-lele", "tauros", "teddiursa", "tentacool", "tentacruel", "tepig", "terrakion", "throh", "thundurus", "timburr", "tirtouga", "togedemaru", "togekiss", "togepi", "togetic", "torchic", "torkoal", "tornadus", "torracat", "torterra", "totodile", "toucannon", "toxapex", "toxicroak", "tranquill", "trapinch", "treecko", "trevenant", "tropius", "trubbish", "trumbeak", "tsareena", "turtonator", "turtwig", "tympole", "tynamo", "type-null", "typhlosion", "tyranitar", "tyrantrum", "tyrogue", "tyrunt", "umbreon", "unfezant", "unown", "ursaluna", "ursaring", "uxie", "vanillish", "vanillite", "vanilluxe", "vaporeon", "venipede", "venomoth", "venonat", "venusaur", "vespiquen", "vibrava", "victini", "victreebel", "vigoroth", "vikavolt", "vileplume", "virizion", "vivillon", "volbeat", "volcanion", "volcarona", "voltorb", "vullaby", "vulpix", "wailmer", "wailord", "walrein", "wartortle", "watchog", "weavile", "weedle", "weepinbell", "weezing", "whimsicott", "whirlipede", "whiscash", "whismur", "wigglytuff", "wimpod", "wingull", "wishiwashi", "wobbuffet", "woobat", "wooper", "wormadam", "wurmple", "wynaut", "wyrdeer", "xatu", "xerneas", "xurkitree", "yamask", "yanma", "yanmega", "yungoos", "yveltal", "zangoose", "zapdos", "zebstrika", "zekrom", "zeraora", "zigzagoon", "zoroark", "zorua", "zubat", "zweilous", "zygarde", ]; ================================================ FILE: lib/rotbart/src/xc1.rs ================================================ /*! https://xenoblade.github.io/xb1/bdat/bdat_common/BTL_enelist.html ```javascript onlyUnique = (value, index, self) => self.indexOf(value) === index; adjectives = []; nouns = []; Array.from(document.getElementsByClassName("sortable")[0].children[1].children) .map(row => row.children[2].innerText) .filter(row => !row.includes("(")) .filter(row => !row.includes("'")) .map(row => row.toLowerCase()) .map(row => row.split(" ")) .filter(row => row.length == 2) .forEach(([adj, noun]) => { adjectives.push(adj); nouns.push(noun); }); adjectives.sort(); nouns.sort(); adjectives = adjectives.filter(onlyUnique); nouns = nouns.filter(onlyUnique); console.log(JSON.stringify({adjectives, nouns})); ``` */ pub const ADJECTIVES: &[&str] = &[ "abnormal", "abominable", "acid", "active", "admiral", "affluent", "aged", "ageless", "aggressive", "agile", "agouti", "air", "amber", "ammos", "amorous", "ancient", "android", "antibody", "antol", "aora", "apocrypha", "aqua", "arachno", "archer", "arel", "arena", "arm", "armoured", "arrogant", "asara", "asha", "ashy", "assault", "atmos", "atomis", "atomizek", "atrophy", "aura", "avalanche", "azul", "babel", "babeli", "baby", "baelfael", "baelzeb", "bagrus", "balanced", "banquet", "barbaric", "barbaro", "basin", "beach", "beautiful", "benevolent", "berserk", "big", "bizarre", "black", "blizzard", "bois", "bono", "bonterra", "bosque", "bow", "brabilam", "brave", "breezy", "bright", "broken", "brutal", "buio", "bulganon", "bunker", "buono", "butterfly", "caelum", "calm", "canyon", "captain", "captured", "carbon", "caris", "caura", "cautious", "cave", "cellar", "chimai", "chloro", "chordy", "ciconia", "clamorous", "clandestine", "clap", "clifftop", "clima", "clinger", "clowd", "cold", "colony", "commander", "common", "confined", "conflagrant", "confusion", "coppice", "corladio", "corriente", "costa", "craft", "cratere", "crista", "cruz", "cumulus", "cunning", "cute", "daksha", "dark", "daughter", "dazzling", "deadly", "decay", "defective", "defence", "defensive", "deified", "deinos", "deluded", "demon", "desert", "despotic", "destroyer", "destructive", "detox", "devoted", "dim", "dinosaur", "director", "disciple", "dogmatic", "doom", "dorsiar", "drakos", "drifter", "drunk", "duel", "dummy", "easy", "eater", "egil", "ei", "elder", "elegant", "elite", "emeraude", "enchanting", "energy", "ent", "entma", "envy", "eques", "erratic", "eryth", "escape", "eternal", "ether", "evil", "experienced", "experimental", "exposure", "extra", "face", "fair", "faithful", "falsel", "fascia", "fate", "feltl", "femuny", "ferocious", "field", "fiendish", "fierce", "fiery", "fighter", "final", "fine", "fio", "firework", "fiume", "flabbergasted", "flailing", "flamme", "flash", "flavel", "flutes", "flying", "fool", "fork", "frenzied", "frost", "fuchsia", "funeral", "furious", "gadolt", "general", "gentle", "ghostly", "giant", "gigas", "gimran", "glacier", "gloria", "glorious", "glory", "gluttonous", "gluttony", "gold", "goldi", "graceful", "gracile", "greed", "greedy", "green", "grom", "grove", "guard", "gust", "hand", "hanz", "happiness", "hard", "hasal", "heavy", "hidden", "hista", "hitter", "hover", "hungry", "hyle", "illustrious", "immovable", "impenetrable", "indomitable", "infernal", "inferno", "inja", "invited", "iron", "itinerant", "itmos", "jada", "jadals", "jade", "javelin", "jelly", "jewel", "judicious", "jungle", "junk", "kamikaze", "klanis", "knuckle", "korlba", "krawla", "krawli", "kukukoro", "kurian", "kyel", "lacus", "laeklit", "lahar", "lake", "lakebed", "lampo", "lancer", "large", "largo", "last", "latio", "lazy", "leader", "leg", "lelepago", "leone", "licorne", "light", "lightning", "lightspeed", "little", "living", "lograt", "lophos", "lubum", "lunar", "lupus", "lurker", "m35", "m56", "m59", "m85", "m87", "m88", "m97", "machine", "mad", "magestic", "magnificent", "magnis", "maker", "makna", "maleza", "man-eater", "marble", "marmor", "marsh", "mass-produced", "master", "masterful", "materia", "meat", "mechon", "meditative", "medium", "mell", "mellow", "metal", "mild", "mining", "mischievious", "mist", "mistol", "monta", "moonlight", "morule", "mount", "mumkhar", "musical", "mysterious", "mystical", "mythical", "napping", "nero", "newgate", "niece", "night", "noble", "noto", "oasis", "obart", "obelis", "obsessive", "offensive", "officer", "ogre", "opulent", "ore", "orluga", "oros", "otol", "palti", "panasowa", "pandora", "partner", "pawn", "peeling", "pelargos", "perna", "petra", "phoenix", "pillager", "plain", "plane", "plasma", "plump", "poison", "poleaxe", "polkan", "porcu", "possessio", "powerful", "prado", "prairie", "praying", "precious", "primo", "primordial", "prison", "prom", "proper", "prosperous", "protective", "prudent", "puera", "pulse", "quadrupedal", "quarto", "queen", "quinto", "racti", "radiant", "rage", "randa", "ranger", "ravine", "reckless", "red", "reef", "reinforcement", "resolute", "resplendent", "revolutionary", "rhoen", "ridge", "rius", "rock", "roguish", "royal", "sabulum", "sacred", "saldox", "sani", "sanjibal", "satisfied", "satorl", "scout", "sea", "secondo", "sentimental", "sentinel", "serene", "sero", "sesna", "sestago", "setor", "shadeless", "shadow", "shield", "shimmering", "sierra", "silk", "sinful", "singing", "sky", "sloth", "slugger", "sniper", "snowal", "snowi", "soft", "sol", "solare", "soldier", "solid", "solidum", "somati", "sonicia", "soothed", "sparas", "spear", "speedy", "splendid", "stella", "stone", "storm", "stormy", "strange", "subterranean", "suelo", "sunlight", "sureny", "swift", "synchronised", "tank", "tarifa", "tele", "telethia", "tempest", "tempestuous", "temporal", "teneb", "tephra", "terra", "territorial", "test", "teterra", "throne", "tocos", "tored", "trainer", "tramont", "tranquil", "trava", "troop", "tumultuous", "turbulent", "turtle", "tussock", "ucan", "ugly", "unine", "unreliable", "upper", "uragano", "vagabond", "vagrant", "vague", "venaes", "venerable", "vengeful", "verdant", "veteran", "vicious", "victorious", "vilae", "violent", "vivid", "wallslide", "wandering", "wasp", "watcher", "water", "weather", "whapol", "white", "wicked", "willow", "wind", "wise", "wizard", "woeful", "wood", "wool", "worker", "wrathful", "xord", "yellow", "young", "zanza", "zealous", "zefa", "zegia", "zeldi", ]; pub const NOUNS: &[&str] = &[ "1", "2", "a", "abaasy", "acon", "ageshu", "aglovale", "aim", "albatro", "alfead", "allocer", "altrich", "amon", "andante", "andos", "ansel", "anstan", "antol", "anzabi", "apety", "apis", "arachno", "arca", "ardun", "arielle", "aries", "armu", "arsene", "astas", "atrophy", "auburn", "b", "balgas", "balteid", "bana", "bandaz", "barbas", "barbatos", "barg", "barnaby", "bathin", "bayern", "behemoth", "belagon", "beleth", "belgazas", "belmo", "bifrons", "bird", "bluchal", "bluco", "bors", "botis", "bracken", "brog", "buer", "bug", "bugworm", "bune", "bunnia", "bunnit", "bunnitzol", "bunniv", "butterfly", "c", "captain", "cardamon", "caterpile", "chilkin", "commander", "cornelius", "crawler", "crocell", "dablon", "daedala", "danaemos", "daulton", "deinos", "device", "dickson", "digalus", "donnis", "dorothea", "dragon", "dummy", "e", "edegia", "eduardo", "egel", "egil", "ei", "ekidno", "eks", "eligos", "eluca", "empress", "entia", "eugen", "exception", "face", "felix", "feris", "fischer", "fish", "fla", "flamii", "flamral", "flier", "florence", "focalor", "forte", "frengel", "frog", "ga", "gaheris", "galdo", "galdon", "galgaron", "galvin", "gamigin", "gawain", "geldesia", "generator", "gigapur", "godwin", "gogol", "goliante", "golteus", "gonzalez", "gozra", "grady", "gragus", "gravar", "gremory", "gross", "grune", "guardian", "gwynry", "harmelon", "heinrich", "hiln", "hode", "holand", "hox", "igna", "imlaly", "impulso", "ipos", "jerome", "jozan", "juju", "jurom", "jutard", "kaelin", "king", "kircheis", "kisling", "klesida", "konev", "krabble", "kromar", "labolas", "laia", "lamorak", "lancelot", "lecrough", "leraje", "lesunia", "lexos", "lizard", "long-distance", "lorithia", "m104", "m31", "m32", "m32x", "m42", "m46x", "m51", "m53", "m53x", "m55", "m63", "m64", "m64x", "m67", "m69", "m69x", "m71", "m72", "m82", "m84", "m86", "machine", "magdalena", "mahatos", "mammut", "marcus", "marin", "matrix", "mechon", "medorlo", "moabit", "moramora", "morax", "mordred", "mu", "mumkhar", "murakmor", "naberius", "nebula", "nemesis", "nova", "obart", "oracion", "ories", "orluga", "orobas", "orthlus", "pagul", "paimon", "palamedes", "palsadia", "paramecia", "patrichev", "pavlovsk", "piranhax", "pod", "ponio", "prototype", "pterix", "purson", "quadwing", "queen", "ragoel", "ramshyde", "raxeal", "redrob", "retrato", "rezno", "rhana", "rhangrot", "rhogul", "rhogulia", "robusto", "rockwell", "rodriguez", "ronove", "rotbart", "rufus", "sallos", "salvacion", "sardi", "sauros", "schvaik", "scout", "selua", "sergeant", "shellfish", "sitri", "skeeter", "skyray", "slobos", "sonid", "sprahda", "subspecies", "taos", "te", "tele", "telethia", "tentacle", "tirkin", "tokilos", "tolosnia", "torquidon", "torta", "tristan", "tuber", "tude", "turtle", "type", "upa", "vagul", "valencia", "vanflare", "vang", "varla", "vassago", "volfen", "volff", "watchtower", "widardun", "wisp", "wolfol", "xord", "yado", "yozel", "zagamei", "zanden", "zanza", "zektol", "zepar", "zo", "zolos", "zomar", ]; ================================================ FILE: lib/rotbart/src/xc2.rs ================================================ /*! https://xenoblade.github.io/xb2/bdat/common/BTL_EnBook.html ```javascript onlyUnique = (value, index, self) => self.indexOf(value) === index; adjectives = []; nouns = []; Array.from(document.getElementsByClassName("sortable")[0].children[1].children) .map(row => row.children[2].innerText) .filter(row => !row.includes("(")) .filter(row => !row.includes("'")) .map(row => row.toLowerCase()) .map(row => row.split(" ")) .filter(row => row.length == 2) .forEach(([adj, noun]) => { adjectives.push(adj); nouns.push(noun); }); adjectives.sort(); nouns.sort(); adjectives = adjectives.filter(onlyUnique); nouns = nouns.filter(onlyUnique); console.log(JSON.stringify({adjectives, nouns})); ``` */ pub const ADJECTIVES: &[&str] = &[ "aatoban", "abrachi", "acar", "acenia", "acute", "agam", "ahaato", "ahaid", "alda", "alonzo", "amari", "amman", "amost", "anbu", "ancient", "anguished", "antecedent", "antipathetic", "aplom", "arcah", "archer", "ardainian", "arlo", "armor", "armored", "arno", "arogan", "arrah", "arrow", "artifice", "asset", "astle", "astor", "atrocious", "aurea", "autumn-shower", "avero", "avys", "awarth", "aybam", "azure", "bafoo", "bagis", "bagoan", "baigun", "barz", "bauz", "beast-hunter", "beat", "beatific", "bebelk", "bebth", "belgio", "berserker", "biban", "biblis", "birial", "blade", "bland", "bledku", "blood", "blue", "blue-eyed", "bobbile", "bohn", "bolc", "boss", "brave", "brazay", "breed", "brewl", "bright", "brionac", "brish", "brogen", "broog", "brutton", "bubble", "buden", "buma", "burran", "burrig", "caliber", "canzin", "captain", "carbis", "cardine", "cardorl", "cartbreaker", "cascade", "cave", "celsars", "chefko", "chelta", "chibal", "chickenheart", "childre", "chituk", "clabor", "clap", "climactic", "climber", "cling", "cloche", "cobalt", "colnicas", "confiscator", "coora", "crane", "crawler", "crimson", "cunning", "cursed", "dagus", "dajan", "dakhim", "dalakio", "dalian", "dalya", "damodan", "damp", "darkblood", "darml", "dayvol", "deadfire", "decapitator", "dedicated", "deej", "deep-green", "deidon", "deimos", "demon", "demon-shell", "derrah", "desert", "dettl", "diggel", "dirid", "disas", "dobri", "docel", "dockle", "doltom", "dominal", "dormic", "dormine", "dorrl", "doryu", "drac", "dread", "dreadnik", "drive", "droth", "drub", "drum", "drux", "duel", "dusky", "dux", "dynal", "eanl", "eclipse", "effi", "elder", "elidor", "emerald", "empress", "emton", "enada", "engineer", "enlightened", "epicurean", "episto", "erratic", "ers", "eryagh", "esko", "espina", "everdark", "evileye", "excavator", "fabel", "fact", "falc", "familion", "fane", "faros", "fayl", "felusi", "femni", "fend", "ferrii", "fers", "fiar", "field", "figgle", "firm", "fissa", "flanck", "flash", "fleet", "flink", "float", "flow", "fort", "fradde", "fratte", "fresh", "froga", "fubbl", "funcel", "fuvor", "gabnun", "gabondo", "gaddon", "galahem", "gamen", "gaoid", "gareid", "gargen", "garnia", "garon", "gast", "gattle", "gazust", "gazzam", "gefillon", "gemini", "genni", "gerolf", "ghudan", "giga", "giron", "gladiator", "glamorous", "glaw", "glorious", "glorr", "glox", "gneo", "gobeen", "goldol", "goliath", "gorian", "gourmand", "graaz", "grad", "grads", "grandum", "grash", "grass", "grat", "gravur", "gray", "graze", "greetz", "grievous", "grohl", "gronta", "ground", "growsa", "gulnid", "gyan", "haaken", "haldood", "handwringing", "harbinger", "hard", "hard-bitten", "hardl", "haywire", "hazan", "hazzard", "heggl", "heidl", "heroic", "highbohn", "highscreeb", "hool", "hooligan", "horiz", "howitzer", "hungry", "huust", "igard", "illumi", "imba", "immovable", "impassable", "implacable", "incandescent", "indoline", "infernal", "ingle", "insectivore", "insufferable", "interceptor", "ionospheric", "jadas", "jadde", "jadeite", "jailer", "jaim", "jakki", "javelin", "jenth", "jewel", "joobs", "jubel", "judicial", "jumbri", "kadar", "kalymon", "kanoo", "karlin", "karor", "karyl", "kast", "kattl", "keat", "kendra", "king", "kitarmo", "klaret", "klim", "knoober", "koror", "kran", "krim", "kustal", "lafda", "lance", "land", "lapis", "lapse", "latollo", "leaf", "leap", "ledro", "lefth", "legarre", "leggin", "legia", "lekut", "leo", "leonine", "leran", "lethal", "levma", "liar", "libelte", "liberion", "ligar", "limdo", "lindwurm", "linka", "little", "lun", "lunar", "mabalus", "mabas", "mabluk", "machine-gun", "madoline", "magmund", "magnl", "magra", "mahi", "mahn", "mailer", "mailoo", "majacan", "makfur", "malicious", "man-eating", "manda", "mant", "maramal", "marauder", "margl", "margot", "marna", "martial", "martz", "masque", "massido", "mayn", "mees", "megabroot", "megalo", "meldl", "melm", "melz", "menacing", "mergen", "meson", "messar", "messenger", "mia", "misdan", "mishgal", "mogen", "moist", "moonlighting", "mordow", "morg", "moskel", "mukkle", "muscley", "myrmidon", "myrrhes", "nairoo", "nant", "natto", "nebri", "nefto", "nekrino", "nel", "neleid", "nelva", "neml", "nemus", "nereus", "nilhez", "nitpicking", "noble", "noggle", "noigan", "noign", "nomad", "nomadic", "nomul", "noog", "nookka", "norgam", "nose", "nossi", "novl", "nowaak", "nula", "nuruba", "nutch", "nyol", "obri", "obsi", "odolera", "olphen", "oone", "organl", "overaffectionate", "pactusk", "pain", "pallov", "parady", "parasite", "parole", "pawn", "peerless", "peri", "pernicious", "perplexed", "phantom", "phoebus", "pidor", "pinch", "pipe", "piros", "poison", "praetorian", "presser", "pride", "prink", "prom", "psit", "pugli", "quake", "queen", "rabres", "radclyffe", "radliev", "radyo", "raflas", "raging", "raider", "ralsh", "rambl", "rangel", "ranger", "rankor", "ransro", "rapturous", "ratchet", "ravenwing", "ravine", "razor", "reast", "rebra", "rebul", "rebus", "red", "redom", "reed", "reeg", "reeking", "reener", "regel", "reggl", "regodos", "regus", "rekon", "remorseful", "revl", "reyo", "ribage", "rinker", "rip", "ripbik", "rippl", "rivarl", "river", "riveral", "robal", "robol", "rock", "rondel", "rook", "rooka", "roose", "rowda", "ruchik", "rudoni", "ruffian", "ruga", "saber", "sable", "sabri", "sad", "salsh", "salteau", "sammel", "samoo", "sandi", "sandl", "sarabashi", "sarchess", "scowling", "scribo", "scura", "scurvy", "security", "segel", "selmo", "sequestered", "serpentine", "seveeto", "shadow", "sharion", "shezl", "shield", "shimmun", "shralk", "shreddle", "shungle", "sinon", "skad", "skeeter", "skode", "skyfist", "slade", "slayg", "sleepwalker", "slithe", "sloam", "sloth", "small", "smart", "snide", "sniping", "snowdol", "somelia", "soothsayer", "sorbl", "sordis", "sorolle", "soul-eater", "sowl", "spanner", "sparda", "speed", "spellbinder", "spike", "spinel", "spirit", "spitt", "sprack", "spring", "spring-shower", "stark", "steeky", "stellar", "sting", "stonic", "straat", "strom", "sugar", "supercharged", "supporter", "survee", "svena", "sweeper", "sweet", "sygian", "talent", "tales", "tannia", "tantalese", "tattooed", "tawa", "telah", "telen", "telgoo", "tempest", "teppus", "territorial", "tetora", "tiquil", "tirkin", "tolen", "tolmeda", "tomlok", "tonbre", "torrl", "totorio", "trainer", "trets", "trilut", "trock", "troog", "tsorrid", "twondus", "typhon", "tyrannotitan", "uis", "uluran", "uncid", "unflinching", "urbs", "urobas", "vabra", "vagrant", "vaids", "valkan", "vallum", "valt", "valta", "vashar", "vaugel", "vay", "velvan", "venal", "ventts", "victor", "vile", "vint", "violent", "vogar", "vokkon", "vool", "wacon", "wall", "watcher", "water", "wendel", "werval", "whisp", "whispering", "winter", "wood", "wormeater", "wrath", "xane", "yardl", "yellow", "young", "youse", "yurem", "zafirah", "zaguin", "zalidor", "zamban", "zangiv", "zardl", "zekor", "zeld", "zeoth", "zext", "ziggan", "zigul", "zike", "zoke", "zooz", ]; pub const NOUNS: &[&str] = &[ "ageshu", "aion", "alfonso", "alfred", "aligo", "amaruq", "anlood", "ansel", "antol", "aplacus", "arachno", "archibald", "ardun", "argus", "aries", "armu", "aspar", "aspid", "baldr", "balgas", "beaufort", "behemoth", "benf", "bernard", "beru", "bigelow", "billy", "blant", "bot", "brennan", "brent", "brog", "bufa", "buloofo", "bunnit", "camill", "caterpile", "cavill", "cetus", "citadel", "clive", "colossus", "conroy", "crustip", "curtis", "dagmara", "damian", "darius", "derrick", "dimitri", "douglas", "driver", "dylan", "edgar", "edwin", "egg", "ekidno", "eks", "elliott", "ellook", "eluca", "elwyn", "emblem", "erg", "eugene", "feris", "fighter", "flamii", "flier", "galgan", "garaffa", "garlus", "gerald", "glenn", "gogol", "goliante", "gonzalez", "grace", "grady", "grebel", "griffox", "grzeg", "guldo", "gyanna", "hermes", "hiln", "honnold", "howard", "hox", "hugo", "igna", "jacob", "jagron", "jimmy", "jo", "julio", "kamron", "kapiba", "keeper", "knight", "kollin", "korbin", "krabble", "kurodil", "kustal", "laia", "leon", "lexos", "ligia", "lizard", "locks", "loyalist", "ludd", "lysaat", "madadh", "major", "malcom", "mambor", "mammut", "marcus", "marrin", "marvin", "medea", "medooz", "melvin", "melvyn", "milltear", "mitchell", "montgomery", "moramora", "mork", "morris", "murph", "muller", "nest", "nipper", "ophelia", "ophion", "ories", "orion", "oscar", "padraig", "pagul", "parisax", "peng", "phoebus", "pippito", "piranhax", "plambus", "pod", "polly", "ponio", "private", "pterix", "puffot", "quadwing", "quincy", "radclyffe", "rapchor", "reginald", "remington", "rhana", "rhinon", "rhogul", "rider", "riik", "rodonya", "ropl", "rosa", "rotbart", "rott", "runner", "rusholme", "sadie", "saggie", "sauros", "saxton", "scandia", "scorpox", "scout", "sentinel", "sentry", "sergeant", "serprond", "seàirdeant", "siren", "skeet", "skeeter", "skull", "skwaror", "snaidhpear", "soldier", "sollmeyer", "solomon", "sovereign", "squood", "standard", "stanley", "star", "stein", "stoyan", "symbol", "tanca", "taos", "tirkin", "totem", "trupair", "ulysses", "upa", "urchon", "vaclav", "vang", "volff", "william", "wisp", "xavier", "xiaxia", ]; pub const COMMON_BLADES: &'static [&'static str] = &[ "aizen", "akatsuki", "akebono", "azai", "arai", "arufumi", "ikazuchi", "ikaruga", "izayoi", "izumo", "ichiro", "ikaku", "ikki", "issen", "inazuma", "ushio", "oryuu", "owashi", "okina", "oboro", "orochi", "kaiden", "kaibyaku", "karkan", "kagemitsu", "kagero", "kazan", "katsumasa", "kanesada", "kanehira", "kanemitsu", "kei", "kijin", "kibitsu", "gokuto", "kirim", "gingar", "kur", "kuzan", "kusanagi", "kurochi", "krogane", "genno", "kouki", "kouru", "kogarashi", "kokras", "gogyo", "kojiro", "kosor", "kotetsu", "kongir", "konjiki", "sakon", "sangar", "shun", "shikiso", "shishi", "shichisei", "shiko", "shippun", "shiden", "shura", "shungen", "shin-mei", "shinra", "jin-rai", "jinryuu", "suro", "sulgar", "seigai", "sysor", "seiten", "seimei", "zeku", "zeno", "sordai", "soten", "sohmei", "shouryu", "sohaya", "taiga", "daiko", "tyzan", "taisei", "tyhei", "tadar", "tsurugi", "tenka", "tenku", "denko", "toshi", "tokka", "hagan", "hakusui", "hakuto", "hayate", "hayabusa", "hanni", "hei", "hiken", "bizen", "hynk", "hideh", "hiden", "bakuya", "huga", "hiryu", "fuwei", "fugetsu", "hukut", "fudor", "hekireki", "bengara", "horoh", "hokuto", "shigan", "mikazuchi", "mizuchi", "mitsukage", "mior", "mu", "mugen", "musashi", "mujo", "musou", "muchika", "murakumo", "murasame", "meikyo", "mejiro", "yago", "yakumo", "yasha", "yata", "yanagi", "yamato", "yuki", "yuzen", "yuso", "yumo", "yoshikiri", "rykiri", "ranmaru", "rikuzen", "ryusei", "ryo", "rogen", "reiki", "roho", "ai", "aui", "auba", "akana", "yoiyami", "asagi", "asai", "aska", "azuki", "atori", "amanei", "amayori", "ayame", "anzu", "koyuki", "kyoka", "isuzu", "ichiku", "iroha", "uzura", "uzuki", "umi", "ema", "orka", "kaeda", "sarasa", "kanami", "kanon", "karin", "karei", "karyn", "kanna", "kiko", "kisaragi", "kiri", "kinsei", "quina", "kuko", "kurumi", "kurenai", "kogoku", "kokutan", "kokuyo", "kokoro", "konoha", "kohana", "kohaku", "sakuya", "sazami", "satsuki", "sango", "shiori", "shigura", "shisui", "shizuku", "shinome", "shinobu", "shimoki", "shussu", "shuraya", "shiranui", "shirayuki", "shirayuri", "shirome", "suiren", "suzu", "suzukaze", "suzuna", "suzuran", "sumira", "tsumugi", "sekirei", "setsuka", "sora", "tamayori", "chigusa", "chidori", "tsugumi", "tsukumi", "zutsuji", "tsubaki", "tsumi", "tsura", "tsuruba", "tomae", "torwa", "nazuna", "natsuki", "nadeshiko", "natori", "ne-ne", "nenoh", "neyuki", "nosuri", "hasu", "hazuki", "hatsuharu", "hatsuyuki", "haruka", "harusa", "haruna", "higana", "hisui", "hinagi", "hinagetsu", "hinata", "hibari", "hibiki", "himawari", "faera", "fubuki", "fuyoshi", "yuzu", "botania", "honoka", "madoka", "mikagami", "mikazuki", "mika", "mikoto", "misaki", "midori", "minazuki", "minami", "minori", "miyuki", "mirei", "mutsuki", "maegi", "mochi", "momiji", "moyoi", "yayoi", "yuka", "yutsuji", "yuna", "yukina", "yukine", "yuzuki", "yura", "yuri", "yomogi", "rania", "rinnia", "lindora", "lin", "ruri", "reika", "rengenne", "wakaba", "utsuwaka", "kai", "kukir", "kuro", "goemon", "koske", "kotar", "goro", "kontro", "sasuke", "shimaru", "shiro", "tamar", "tamon", "tibbi", "chamaru", "tokoto", "totetsu", "baku", "hutar", "pochi", "bonten", "ryta", "riku", ]; ================================================ FILE: lib/tailscale_client/Cargo.toml ================================================ [package] name = "tailscale_client" version = "0.1.0" edition = "2021" authors = [ "Xe Iaso " ] repository = "https://github.com/Xe/waifud" license = "mit" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] serde_json = "1" thiserror = "1" [dependencies.chrono] version = "0.4" features = [ "serde" ] [dependencies.serde] version = "1" features = [ "derive" ] [dependencies.reqwest] version = "0.11" features = [ "json" ] ================================================ FILE: lib/tailscale_client/src/lib.rs ================================================ use chrono::prelude::*; use serde::{Deserialize, Serialize}; #[derive(thiserror::Error, Debug)] pub enum Error { #[error("error making request: {0}")] Reqwest(#[from] reqwest::Error), #[error("error parsing json: {0}")] JSON(#[from] serde_json::Error), } pub type Result = std::result::Result; /// The Tailscale API Client. Each call will be its own method. pub struct Client { cli: reqwest::Client, api_key: String, tailnet: String, base_url: String, } /// The minimal form of a Tailscale API key, only shows the ID. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Key { pub id: String, } /// Full information about a Tailscale API key. #[derive(Debug, Clone, Deserialize, Serialize)] pub struct KeyInfo { pub id: String, #[serde(skip_serializing)] pub key: Option, pub created: DateTime, pub expires: DateTime, /// TODO(Xe): make this into a better value pub capabilities: serde_json::Value, } #[test] fn test_keyinfo_full() { let inp = r#"{ "id": "k123456CNTRL", "key": "tskey-k123456CNTRL-abcdefghijklmnopqrstuvwxyz", "created": "2021-12-09T23:22:39Z", "expires": "2022-03-09T23:22:39Z", "capabilities": {"devices": {"create": {"reusable": false, "ephemeral": false}}} }"#; let _: KeyInfo = serde_json::from_str(inp).unwrap(); } #[test] fn test_keyinfo_partial() { let inp = r#"{ "id": "k123456CNTRL", "created": "2021-12-09T23:22:39Z", "expires": "2022-03-09T23:22:39Z", "capabilities": {"devices": {"create": {"reusable": false, "ephemeral": false}}} }"#; let _: KeyInfo = serde_json::from_str(inp).unwrap(); } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Capabilities { pub reusable: bool, pub ephemeral: bool, pub preauthorized: bool, pub tags: Vec, } impl Client { /// Maybe construct a new client with a given user agent string. pub fn new(user_agent: String, api_key: String, tailnet: String) -> Result { let cli = reqwest::Client::builder().user_agent(user_agent).build()?; Ok(Client { cli, api_key, tailnet, base_url: "https://api.tailscale.com".to_string(), }) } /// List all active node authentication keys in the tailnet. pub async fn list_keys(&self) -> Result> { #[derive(Debug, Clone, Deserialize, Serialize)] struct Wrapper { keys: Vec, } let w: Wrapper = self .cli .get(&format!( "{}/api/v2/tailnet/{}/keys", self.base_url, self.tailnet )) .basic_auth(&self.api_key, None::<&String>) .send() .await? .error_for_status()? .json() .await?; Ok(w.keys) } /// Creates a new machine authkey for the tailnet. This cannot be used /// to create API keys. pub async fn create_key(&self, caps: Capabilities) -> Result { #[derive(Debug, Clone, Deserialize, Serialize)] struct Outer { capabilities: Inner, } #[derive(Debug, Clone, Deserialize, Serialize)] struct Inner { devices: Inner2, } #[derive(Debug, Clone, Deserialize, Serialize)] struct Inner2 { create: Capabilities, } let msg = Outer { capabilities: Inner { devices: Inner2 { create: caps }, }, }; Ok(self .cli .post(&format!( "{}/api/v2/tailnet/{}/keys", self.base_url, self.tailnet )) .basic_auth(&self.api_key, None::<&String>) .json(&msg) .send() .await? .error_for_status()? .json() .await?) } /// Get detailed information for a given key by ID. pub async fn key_info(&self, key_id: String) -> Result { Ok(self .cli .get(&format!( "{}/api/v2/tailnet/{}/keys/{}", self.base_url, self.tailnet, key_id, )) .basic_auth(&self.api_key, None::<&String>) .send() .await? .error_for_status()? .json() .await?) } /// Delete a key by ID. pub async fn delete_key(&self, key_id: String) -> Result { self.cli .delete(&format!( "{}/api/v2/tailnet/{}/keys/{}", self.base_url, self.tailnet, key_id, )) .basic_auth(&self.api_key, None::<&String>) .send() .await? .error_for_status()? .json() .await?; Ok(()) } } ================================================ FILE: lib/ts_localapi/Cargo.toml ================================================ [package] name = "ts_localapi" version = "0.1.0" edition = "2021" authors = [ "Xe Iaso " ] repository = "https://github.com/Xe/waifud" license = "mit" [dependencies] hyper = "0.14" hyperlocal = "0.8" serde = { version = "1", features = [ "derive" ] } serde_json = "1" thiserror = "1" ================================================ FILE: lib/ts_localapi/src/lib.rs ================================================ use hyper::{body::Buf, Body, Client, Request, StatusCode}; use hyperlocal::{UnixClientExt, Uri}; use serde::{Deserialize, Serialize}; use std::net::{IpAddr, SocketAddr}; #[derive(Debug, thiserror::Error)] pub enum Error { #[error("hyper error: {0}")] Hyper(#[from] hyper::Error), #[error("http error: {0}")] HTTP(#[from] hyper::http::Error), #[error("json error: {0}")] JSON(#[from] serde_json::Error), #[error("wanted status code {0}, but tailscaled returned status code {1}")] WrongStatusCode(StatusCode, StatusCode), } #[derive(Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WhoisResponse { #[serde(rename = "Node")] pub node: WhoisPeer, #[serde(rename = "UserProfile")] pub user_profile: User, } pub async fn whois(ip_port: SocketAddr) -> Result { let ip_port = if let SocketAddr::V6(ip_port) = ip_port { let ip = ip_port .ip() .to_ipv4() .map(|ip| IpAddr::V4(ip)) .unwrap_or(IpAddr::V6(ip_port.ip().clone())); (ip, ip_port.port()).into() } else { ip_port }; let url: hyper::Uri = Uri::new( "/var/run/tailscale/tailscaled.sock", &format!("/localapi/v0/whois?addr={ip_port}"), ) .into(); let client = Client::unix(); let req = Request::builder() .uri(url) .header("Host", "local-tailscaled.sock") .body(Body::empty()) .unwrap(); let resp = client.request(req).await?; if !resp.status().is_success() { return Err(Error::WrongStatusCode(StatusCode::OK, resp.status())); } let body = hyper::body::aggregate(resp).await?; Ok(serde_json::from_reader(body.reader())?) } #[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct User { #[serde(rename = "ID")] pub id: u64, #[serde(rename = "LoginName")] pub login_name: String, #[serde(rename = "DisplayName")] pub display_name: String, #[serde(rename = "ProfilePicURL")] pub profile_pic_url: String, #[serde(rename = "Roles")] pub roles: Vec>, } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WhoisPeer { #[serde(rename = "ID")] pub id: i64, #[serde(rename = "StableID")] pub stable_id: String, #[serde(rename = "Name")] pub name: String, #[serde(rename = "User")] pub user: i64, #[serde(rename = "Key")] pub key: String, #[serde(rename = "KeyExpiry")] pub key_expiry: String, #[serde(rename = "Machine")] pub machine: String, #[serde(rename = "DiscoKey")] pub disco_key: String, #[serde(rename = "Addresses")] pub addresses: Vec, #[serde(rename = "AllowedIPs")] pub allowed_ips: Vec, #[serde(rename = "Endpoints")] pub endpoints: Vec, #[serde(rename = "Hostinfo")] pub hostinfo: Hostinfo, #[serde(rename = "Created")] pub created: String, #[serde(rename = "PrimaryRoutes")] pub primary_routes: Option>, #[serde(rename = "MachineAuthorized")] pub machine_authorized: Option, #[serde(rename = "Capabilities")] pub capabilities: Option>, #[serde(rename = "ComputedName")] pub computed_name: String, #[serde(rename = "ComputedNameWithHost")] pub computed_name_with_host: String, } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Hostinfo { #[serde(rename = "OS")] pub os: Option, #[serde(rename = "Hostname")] pub hostname: String, #[serde(rename = "RoutableIPs")] pub routable_ips: Option>, #[serde(rename = "Services")] pub services: Vec, } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Service { #[serde(rename = "Proto")] pub proto: String, #[serde(rename = "Port")] pub port: i64, #[serde(rename = "Description")] pub description: Option, } ================================================ FILE: scripts/.gitignore ================================================ *.qcow2* ================================================ FILE: scripts/metadata.json ================================================ {"unstable":{"fname":"nixos-unstable-within-202307091255.qcow2","sha256":"858f149120dc86d8bb1696b3d62f03c597a9706082709bd4220ac24d25640efe"}} ================================================ FILE: scripts/mk-nixos-image.sh ================================================ #!/usr/bin/env nix-shell #! nix-shell -i bash -p nixos-generators -p qemu -p rsync -p jo set -ex DATE="$(date +%Y%m%d%H%M)" NIX_PATH=nixpkgs=channel:nixos-unstable-small nixos-generate -f qcow -c ./nixos-image.nix -o ./nixos-unstable-within-${DATE} # NIX_PATH=nixpkgs=channel:nixos-21.11-small nixos-generate -f qcow -c ./nixos-image.nix -o ./nixos-unstable-within-${DATE} qemu-img convert -c -O qcow2 ./nixos-unstable-within-${DATE}/nixos.qcow2 nixos-unstable-within-${DATE}.qcow2 # qemu-img convert -c -O qcow2 ./nixos-21.11-within-${DATE}/nixos.qcow2 nixos-21.11-within-${DATE}.qcow2 sha256sum nixos-unstable-within-${DATE}.qcow2 > nixos-unstable-within-${DATE}.qcow2.sha256 # sha256sum nixos-21.11-within-${DATE}.qcow2 > nixos-21.11-within-${DATE}.qcow2.sha256 rsync -avz --progress *.qcow2* lufta:/srv/http/xena.greedo.xeserv.us/pkg/nixos/ rm ./nixos-unstable-within-${DATE} # rm ./nixos-21.11-within-${DATE} rm -f metadata.json touch metadata.json jo -o metadata.json \ unstable=$(jo \ fname=nixos-unstable-within-${DATE}.qcow2 \ sha256=$(cat nixos-unstable-within-${DATE}.qcow2.sha256 | cut -d' ' -f1)) # 21.11=$(jo \ # fname=nixos-21.11-within-${DATE}.qcow2 \ # sha256=$(cat nixos-unstable-within-${DATE}.qcow2.sha256 | cut -d' ' -f1)) \ rsync -avz --progress metadata.json lufta:/srv/http/xena.greedo.xeserv.us/pkg/nixos/ ================================================ FILE: scripts/nixos-image.nix ================================================ { lib, pkgs, ... }: { boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "virtio_pci" "sr_mod" "virtio_blk" ]; boot.initrd.kernelModules = [ ]; boot.kernelModules = [ ]; boot.extraModulePackages = [ ]; boot.growPartition = true; boot.kernelParams = [ "console=ttyS0" ]; boot.loader.grub.device = "/dev/vda"; boot.loader.timeout = 0; fileSystems."/" = { device = "/dev/disk/by-label/nixos"; fsType = "ext4"; autoResize = true; }; nix = { package = pkgs.nixVersions.stable; extraOptions = '' experimental-features = nix-command flakes ''; settings = { auto-optimise-store = true; sandbox = true; substituters = [ "https://xe.cachix.org" "https://nix-community.cachix.org" "https://cuda-maintainers.cachix.org" "https://cache.floxdev.com?trusted=1" "https://cache.garnix.io" ]; trusted-users = [ "root" "cadey" ]; trusted-public-keys = [ "xe.cachix.org-1:kT/2G09KzMvQf64WrPBDcNWTKsA79h7+y2Fn2N7Xk2Y=" "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" "cuda-maintainers.cachix.org-1:0dq3bujKpuEPMCX6U4WylrUDZ9JyUG0VpVZa7CNfq5E=" "flox-store-public-0:8c/B+kjIaQ+BloCmNkRUKwaVPFWkriSAd0JJvuDu4F0=" "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" ]; }; }; systemd.services."within.website-first-run" = { description = "bootstrap the first run of a NixOS machine on waifud"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" "polkit.service" ]; path = [ "/run/current-system/sw/" ]; script = with pkgs; '' if ! [ -f /etc/nixos/configuration.nix ]; then install -D ${./nixos-image.nix} /mnt/etc/nixos/configuration.nix fi ''; }; systemd.services.cloud-init.requires = lib.mkForce [ "network.target" ]; services.tailscale.enable = true; services.openssh.enable = true; services.cloud-init = { enable = true; ext4.enable = true; }; users.motd = "Welcome to waifud <3"; } ================================================ FILE: shell.nix ================================================ (import ( fetchTarball { url = "https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee43199025dbf.tar.gz"; sha256 = "0x2jn3vrawwv9xp15674wjz9pixwjyj3j771izayl962zziivbx2"; } ) { src = ./.; }).shellNix ================================================ FILE: src/admin/mod.rs ================================================ use crate::{ api::libvirt::Machine, models::{Distro, Instance}, tailauth::Tailauth, Config, Result, State, }; use axum::{extract::Path, Extension, Json}; use maud::{html, Markup, PreEscaped}; use rusqlite::params; use std::sync::Arc; use ts_localapi::User; use uuid::Uuid; use virt::{connect::Connect, domain::Domain}; fn import_js(name: &str) -> PreEscaped { PreEscaped(format!( r#""# )) } pub async fn config(Extension(config): Extension>, _: Tailauth) -> Json { Json((*config).clone()) } pub fn base( title: Option, crumbs: Option<&[(&str, Option<&str>)]>, user_data: User, body: Markup, ) -> Markup { let page_title = title.clone().unwrap_or("waifud".to_string()); let title = title .map(|s| format!("{s} - waifud")) .unwrap_or("waifud".to_string()); let crumbs = if let Some(crumbs) = crumbs { html! { nav.breadcrumb.nav { div.right { {(user_data.display_name)} " " img style="width:32px;height:32px" src=(user_data.profile_pic_url); } ul { li { a href="/admin" { "waifud" } } @for (name, link) in crumbs { li { @match link { Some(link) => a href=(link) {(name)}, None => span aria-current="page" {(name)}, } } } } } } } else { html! { nav.nav { div.right { {(user_data.display_name)} " " img style="width:32px;height:32px" src=(user_data.profile_pic_url); } a href="/admin" {"waifud"} " " a href="/admin/distros" {"Distros"} " " a href="/admin/instances" {"Instances"} } } }; html! { (maud::DOCTYPE) html { head { meta charset="utf-8"; title {(title)} meta name="viewport" content="width=device-width, initial-scale=1.0"; link rel="icon" href="data:image/svg+xml,🔥"; link rel="stylesheet" type="text/css" href="/static/css/xess.css"; } body.top { main { (crumbs) br; h1 {(page_title)} br; (body); hr; footer { p { "Powered with dokis by " a href="https://github.com/Xe/waifud" {"waifud"} ". ❤️" } } } } } } } pub async fn instance_create(Tailauth(user, _): Tailauth) -> Markup { base( Some("Create instance".to_string()), Some(&[("Instances", Some("/admin/instances")), ("Create", None)]), user, html! { (import_js("instance_create.js")) div #app { "Loading..." } }, ) } pub async fn instance( Extension(state): Extension>, Tailauth(user, _): Tailauth, Path(id): Path, ) -> Result { let conn = state.pool.get().await?; let instance = Instance::from_uuid(&conn, id)?; let conn = Connect::open(&format!("qemu+ssh://root@{}/system", instance.host))?; let machine: Option = Domain::lookup_by_uuid_string(&conn, &id.to_string()) .ok() .and_then(|dom| Machine::try_from(dom).ok()); Ok(base( Some(instance.name.clone()), Some(&[ ("Instances", Some("/admin/instances")), (&instance.name, None), ]), user, html! { (import_js("instance_detail.js")) table { tr { th {"Status"} td {(instance.status)} } tr { th {"IP Address"} td { @if let Some(m) = machine { (m.addr.unwrap_or("".to_string())) } } } tr { th {"Host"} td {(instance.host)} } tr { th {"Memory"} td {(instance.memory) " MB"} } tr { th {"Disk size"} td {(instance.disk_size) " GB"} } tr { th {"ZVol name"} td {(instance.zvol_name)} } tr { th {"Distro"} td {(instance.distro)} } tr { th {"UUID"} td #instance_id {(instance.uuid.to_string())} } } h2 {"Quick Actions"} div #app {"Loading..."} }, )) } pub async fn instances( Extension(state): Extension>, Tailauth(user, _): Tailauth, ) -> Result { let conn = state.pool.get().await?; let mut result: Vec = Vec::new(); let mut stmt = conn.prepare( "SELECT uuid, name, host, mac_address, memory, disk_size, zvol_name, status, distro, join_tailnet FROM instances", )?; let instances = stmt.query_map(params![], |row| { Ok(Instance { uuid: row.get(0)?, name: row.get(1)?, host: row.get(2)?, mac_address: row.get(3)?, memory: row.get(4)?, disk_size: row.get(5)?, zvol_name: row.get(6)?, status: row.get(7)?, distro: row.get(8)?, join_tailnet: row.get(9)?, }) })?; for instance in instances { result.push(instance?); } Ok(base( Some("Instances".to_string()), Some(&[("Instances", None)]), user, html! { p{ a href="/admin/instances/create" {"Create a new instance"} } table { tr { th {"Name"} th {"Host"} th {"Memory"} th {"Disk"} th {"Distro"} th {"Status"} } @for i in result { tr { td {a href={"/admin/instances/" (i.uuid.to_string())} {(i.name)}} td {(i.host)} td {(i.memory) " MB"} td {(i.disk_size) " GB"} td {(i.distro)} td {(i.status)} } } } }, )) } pub async fn home( Extension(state): Extension>, Tailauth(user, _): Tailauth, ) -> Result { let conn = state.pool.get().await?; let mut stmt = conn.prepare( " WITH distro_count ( val ) AS ( SELECT COUNT(*) FROM distros ) , instance_count ( val ) AS ( SELECT COUNT(*) FROM instances ) , instance_memory ( amt ) AS ( SELECT SUM(memory) FROM instances ) SELECT dc.val AS distros , ic.val AS instances , im.amt AS ram_use FROM distro_count dc , instance_count ic , instance_memory im ", )?; let (distro_count, instance_count, total_memory): (i32, i32, Option) = stmt.query_row(params![], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))?; Ok(base( Some("Home".to_string()), None, user.clone(), html! { p { "Hello " (user.login_name) "! I am tracking " (distro_count) " distribution image" @if distro_count != 1 { "s" } ", " (instance_count) " VM instance" @if instance_count != 1 { "s" } " that use a total of " (total_memory.unwrap_or(0)) " megabytes of RAM." } p{ a href="/admin/instances/create" {"Create a new instance"} } }, )) } pub async fn distro_list( Extension(state): Extension>, Tailauth(user, _): Tailauth, ) -> Result { let conn = state.pool.get().await?; let mut stmt = conn.prepare( "SELECT name, download_url, sha256sum, min_size, format FROM distros ORDER BY name ASC", )?; let iter = stmt.query_map(params![], |row| { Ok(Distro { name: row.get(0)?, download_url: row.get(1)?, sha256sum: row.get(2)?, min_size: row.get(3)?, format: row.get(4)?, }) })?; let mut result: Vec = vec![]; for distro in iter { result.push(distro.unwrap()); } Ok(base( Some("Distros".to_string()), Some(&[("Distros", None)]), user, html! { table { tr { th {"Name"} th {"Min. Size (gb)"} } @for d in result { tr { td {(d.name)} td {(d.min_size)} } } } }, )) } pub async fn test_handler(Tailauth(user, _): Tailauth) -> Result { Ok(base( Some("Test Page lol".to_string()), None, user, html! { p {"I'm baby tonx narwhal ennui crucifix taiyaki yr farm-to-table lomo locavore chillwave next level. Af palo santo bicycle rights try-hard gentrify jianbing viral heirloom actually sartorial fashion axe pickled artisan selvage cred. Celiac hammock sriracha yes plz, fit migas semiotics bruh shabby chic gluten-free chambray portland pug. Vice activated charcoal cornhole messenger bag enamel pin, put a bird on it blog ascot kale chips green juice sartorial twee retro. Try-hard hashtag umami leggings tote bag chillwave."} h2 {"Lumbersexual polaroid"} p { "Migas trust fund sriracha pop-up occupy. Chicharrones meggings bruh green juice squid. Brunch ennui umami fit gastropub 8-bit dreamcatcher. Bespoke portland pork belly vegan direct trade shoreditch austin franzen same +1 hoodie sustainable pickled celiac succulents. Lo-fi squid pok pok, chillwave master cleanse DIY tbh enamel pin gastropub iPhone yes plz lyft actually lumbersexual." } p { "Next level gastropub intelligentsia flannel tote bag, pug tilde lumbersexual poke mustache occupy. Seitan viral poutine messenger bag, echo park wayfarers af bruh poke distillery jianbing. Chillwave activated charcoal +1, disrupt shoreditch swag humblebrag lyft bushwick readymade same taxidermy kickstarter cold-pressed unicorn. Organic cloud bread polaroid tacos listicle man braid poutine chia skateboard fixie." } }, )) } ================================================ FILE: src/api/audit.rs ================================================ use crate::{models::AuditEvent, tailauth::Tailauth, Result, State}; use axum::{ extract::{Extension, Path}, Json, }; use std::sync::Arc; #[instrument(err, skip(state))] pub async fn list( Extension(state): Extension>, _: Tailauth, ) -> Result>> { let conn = state.pool.get().await?; Ok(Json(AuditEvent::get_all(&conn)?)) } #[instrument(err, skip(state))] pub async fn list_for_instance( Path(id): Path, Extension(state): Extension>, _: Tailauth, ) -> Result>> { let conn = state.pool.get().await?; Ok(Json(AuditEvent::get_for_instance(id, &conn)?)) } ================================================ FILE: src/api/cloudinit.rs ================================================ use crate::{models::Instance, Error, State}; use axum::extract::{Extension, Path}; use rusqlite::params; use serde::{Deserialize, Serialize}; use std::sync::Arc; use uuid::Uuid; #[instrument(err)] pub async fn user_data( Path(id): Path, Extension(state): Extension>, ) -> Result { let conn = state.pool.get().await?; Ok(conn.query_row( "SELECT user_data FROM cloudconfig_seeds WHERE uuid = ?1", params![id], |row| Ok(row.get(0)?), )?) } #[instrument(err)] pub async fn meta_data( Path(id): Path, Extension(state): Extension>, ) -> Result { let conn = state.pool.get().await?; let hostname: String = conn.query_row( "SELECT name FROM instances WHERE uuid = ?1", params![id], |row| row.get(0), )?; conn.execute( "UPDATE instances SET status = ?1 WHERE uuid = ?2", params!["running", id], )?; let ins = Instance::from_uuid(&conn, id)?; conn.execute( "INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)", params!["instance", "running", serde_json::to_string(&ins)?], )?; Ok(format!( "instance-id: {} local-hostname: {}", id, hostname, )) } #[instrument(err, skip(ts))] pub async fn vendor_data( Path(id): Path, Extension(ts): Extension>, Extension(state): Extension>, ) -> Result { let conn = state.pool.get().await?; let i: Instance = Instance::from_uuid(&conn, id)?; if i.join_tailnet { let key_info = ts .create_key(tailscale_client::Capabilities { reusable: false, ephemeral: true, preauthorized: true, tags: vec!["tag:vm".to_string()], }) .await?; conn.execute( "INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)", params![ "tailnet authkey", "create", serde_json::to_string(&key_info)? ], )?; if i.distro == "ubuntu-20.04".to_string() || i.distro == "ubuntu-22.04".to_string() { Ok(format!("#cloud-config\n{}", serde_yaml::to_string(&CloudConfig{ write_files: vec![ File{ owner: "root:root".to_string(), path: "/etc/update-motd.d/69-waifud".to_string(), permissions: "0755".to_string(), content: "#!/bin/sh\n#\n# This file is written by waifud.\necho \"\"\necho \"Welcome to waifud <3\"\n".to_string(), }, ], runcmd: vec![ vec!["sh".into(), "-c".into(), "curl -fsSL https://tailscale.com/install.sh | sh".into()], vec!["systemctl".into(), "enable".into(), "--now".into(), "tailscaled.service".into()], vec!["tailscale".into(), "up".into(), "--authkey".into(), key_info.key.unwrap(), "--ssh".into(), "--advertise-tags=tag:vm".into()], vec!["apt".into(), "install".into(), "-y".into(), "systemd-container".into()] ], })?)) } else { Ok(format!("#cloud-config\n{}", serde_yaml::to_string(&CloudConfig{ write_files: vec![ File{ owner: "root:root".into(), path: "/etc/update-motd.d/69-waifud".into(), permissions: "0755".into(), content: "#!/bin/sh\n#\n# This file is written by waifud.\necho \"\"\necho \"Welcome to waifud <3\"\n".into(), }, ], runcmd: vec![ vec!["sh".into(), "-c".into(), "curl -fsSL https://tailscale.com/install.sh | sh".into()], vec!["systemctl".into(), "enable".into(), "--now".into(), "tailscaled.service".into()], vec!["tailscale".into(), "up".into(), "--authkey".into(), key_info.key.unwrap(), "--ssh".into()], ], })?)) } } else { Ok(include_str!("./vendor-data").to_string()) } } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CloudConfig { #[serde(rename = "write_files")] pub write_files: Vec, pub runcmd: Vec>, } #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct File { pub owner: String, pub path: String, pub permissions: String, pub content: String, } ================================================ FILE: src/api/distros.rs ================================================ use crate::{models::Distro, tailauth::Tailauth, Result, State}; use axum::{ extract::{Extension, Path}, Json, }; use rusqlite::params; use std::sync::Arc; #[instrument(err)] pub async fn create( Extension(state): Extension>, _: Tailauth, Json(distro): Json, ) -> Result> { let conn = state.pool.get().await?; let mut distro = distro; if distro.format == "".to_string() { distro.format = "waifud://qcow2".into(); } { let d = distro.clone(); conn.execute( "INSERT INTO distros ( name , download_url , sha256sum , min_size , format ) VALUES ( ?1 , ?2 , ?3 , ?4 , ?5 )", params![d.name, d.download_url, d.sha256sum, d.min_size, d.format], )?; } conn.execute( "INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)", params!["distro", "create", serde_json::to_string(&distro)?], )?; Ok(Json(distro)) } #[instrument(err)] pub async fn update( Path(name): Path, Extension(state): Extension>, _: Tailauth, Json(distro): Json, ) -> Result> { let conn = state.pool.get().await?; let mut distro = distro; if distro.format == "".to_string() { distro.format = "waifud://qcow2".into(); } let d = distro.clone(); conn.execute( " INSERT INTO distros( name , download_url , sha256sum , min_size , format ) VALUES ( ?5 , ?1 , ?2 , ?3 , ?4 ) ON CONFLICT DO UPDATE SET download_url=?1 , sha256sum=?2 , min_size=?3 , format=?4 ", params![d.download_url, d.sha256sum, d.min_size, d.format, d.name], )?; conn.execute( "INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)", params!["distro", "update", serde_json::to_string(&d)?], )?; Ok(Json(distro)) } #[instrument(err)] pub async fn delete( Extension(state): Extension>, Path(name): Path, _: Tailauth, ) -> Result<()> { let conn = state.pool.get().await?; let d = Distro::from_name(&conn, name)?; conn.execute("DELETE FROM distros WHERE name = ?1", params![d.name])?; conn.execute( "INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)", params!["distro", "update", serde_json::to_string(&d)?], )?; Ok(()) } #[instrument(err)] pub async fn get( Extension(state): Extension>, Path(name): Path, _: Tailauth, ) -> Result> { let conn = state.pool.get().await?; Ok(Json(conn.query_row( "SELECT name , download_url , sha256sum , min_size , format FROM distros WHERE name = ?1", params![name], |row| { Ok(Distro { name: row.get(0)?, download_url: row.get(1)?, sha256sum: row.get(2)?, min_size: row.get(3)?, format: row.get(4)?, }) }, )?)) } #[instrument(err)] pub async fn list( Extension(state): Extension>, _: Tailauth, ) -> Result>> { let conn = state.pool.get().await?; let mut stmt = conn.prepare( "SELECT name, download_url, sha256sum, min_size, format FROM distros ORDER BY name ASC", )?; let iter = stmt.query_map(params![], |row| { Ok(Distro { name: row.get(0)?, download_url: row.get(1)?, sha256sum: row.get(2)?, min_size: row.get(3)?, format: row.get(4)?, }) })?; let mut result: Vec = vec![]; for distro in iter { result.push(distro.unwrap()); } Ok(Json(result)) } ================================================ FILE: src/api/instances.rs ================================================ use crate::{ api::libvirt::Machine, libvirt::{random_mac, NewInstance}, models::{Distro, Instance}, tailauth::Tailauth, Config, Error, State, }; use axum::{ extract::{Extension, Path}, Json, }; use rusqlite::params; use std::{ convert::TryFrom, net::SocketAddr, os::unix::prelude::ExitStatusExt, process::ExitStatus, sync::Arc, time::Duration, }; use tokio::{net::lookup_host, process::Command, task::spawn_blocking, time::sleep}; use uuid::Uuid; use virt::{connect::Connect, domain::Domain}; #[instrument(err)] #[axum_macros::debug_handler] pub async fn reinit( Path(id): Path, Extension(state): Extension>, _: Tailauth, ) -> Result<(), Error> { let conn = state.pool.get().await?; let mut i = Instance::from_uuid(&conn, id)?; let nuke: Result<(), Error> = { let host = i.host.clone(); let id = i.uuid.clone(); spawn_blocking(move || { let conn = Connect::open(&format!("qemu+ssh://root@{}/system", host))?; let dom = Domain::lookup_by_uuid_string(&conn, &id.to_string())?; dom.destroy()?; Ok(()) }) .await? }; nuke?; sleep(Duration::from_millis(500)).await; debug!("rolling back zvol"); let output = Command::new("ssh") .args([ "-lroot", &i.host, "zfs", "rollback", &format!("{}@init", i.zvol_name), ]) .output() .await?; if output.status != ExitStatus::from_raw(0) { let stderr = String::from_utf8(output.stderr).unwrap(); return Err(Error::CantRollbackZvol( i.host.clone(), "init".to_string(), stderr, )); } let nuke: Result<(), Error> = { let host = i.host.clone(); let id = i.uuid.clone(); spawn_blocking(move || { let conn = Connect::open(&format!("qemu+ssh://root@{}/system", host))?; let dom = Domain::lookup_by_uuid_string(&conn, &id.to_string())?; dom.create()?; Ok(()) }) .await? }; nuke?; i.status = "reinit".to_string(); conn.execute( "INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)", params!["instance", "reinit", serde_json::to_string(&i)?], )?; Ok(()) } #[instrument(err)] #[axum_macros::debug_handler] pub async fn delete( Path(id): Path, Extension(state): Extension>, _: Tailauth, ) -> Result<(), Error> { let conn = state.pool.get().await?; let i = Instance::from_uuid(&conn, id)?; let nuke: Result<(), Error> = { let host = i.host.clone(); let id = i.uuid.clone(); spawn_blocking(move || { let conn = Connect::open(&format!("qemu+ssh://root@{}/system", host))?; let dom = Domain::lookup_by_uuid_string(&conn, &id.to_string())?; dom.destroy()?; dom.undefine_flags(virt::sys::VIR_DOMAIN_UNDEFINE_NVRAM)?; Ok(()) }) .await? }; nuke?; sleep(Duration::from_millis(500)).await; debug!("destroying zvol"); let output = Command::new("ssh") .args(["-lroot", &i.host, "zfs", "destroy", "-rf", &i.zvol_name]) .output() .await?; if output.status != ExitStatus::from_raw(0) { let stderr = String::from_utf8(output.stderr).unwrap(); return Err(Error::CantDeleteZvol(i.host.clone(), stderr)); } conn.execute("DELETE FROM instances WHERE uuid = ?1", params![id])?; conn.execute("DELETE FROM cloudconfig_seeds WHERE uuid = ?1", params![id])?; conn.execute( "INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)", params!["instance", "delete", serde_json::to_string(&i)?], )?; Ok(()) } #[instrument(err)] #[axum_macros::debug_handler] pub async fn get_machine( Path(id): Path, Extension(state): Extension>, _: Tailauth, ) -> Result, Error> { let conn = state.pool.get().await?; let mut stmt = conn.prepare("SELECT host FROM instances WHERE uuid = ?1")?; let host: String = stmt.query_row(params![id], |row| row.get(0))?; let conn = Connect::open(&format!("qemu+ssh://root@{}/system", host))?; let dom = Domain::lookup_by_uuid_string(&conn, &id.to_string())?; Ok(Json(Machine::try_from(dom)?)) } #[instrument(err)] #[axum_macros::debug_handler] pub async fn hard_reboot( Path(id): Path, Extension(state): Extension>, _: Tailauth, ) -> Result<(), Error> { let conn = state.pool.get().await?; let i = Instance::from_uuid(&conn, id)?; let vc = Connect::open(&format!("qemu+ssh://root@{}/system", i.host))?; let dom = Domain::lookup_by_uuid_string(&vc, &id.to_string())?; dom.destroy()?; dom.create()?; conn.execute( "UPDATE instances SET status = ?1 WHERE uuid = ?2", params!["rebooting", id], )?; conn.execute( "INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)", params!["instance", "hard reboot", serde_json::to_string(&i)?], )?; Ok(()) } #[instrument(err)] #[axum_macros::debug_handler] pub async fn shutdown( Path(id): Path, Extension(state): Extension>, _: Tailauth, ) -> Result<(), Error> { let conn = state.pool.get().await?; let i = Instance::from_uuid(&conn, id)?; let vc = Connect::open(&format!("qemu+ssh://root@{}/system", i.host))?; let dom = Domain::lookup_by_uuid_string(&vc, &id.to_string())?; dom.shutdown()?; conn.execute( "UPDATE instances SET status = ?1 WHERE uuid = ?2", params!["off", id], )?; conn.execute( "INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)", params!["instance", "shutdown", serde_json::to_string(&i)?], )?; Ok(()) } #[instrument(err)] #[axum_macros::debug_handler] pub async fn start( Path(id): Path, Extension(state): Extension>, _: Tailauth, ) -> Result<(), Error> { let conn = state.pool.get().await?; let i = Instance::from_uuid(&conn, id)?; let vc = Connect::open(&format!("qemu+ssh://root@{}/system", i.host))?; let dom = Domain::lookup_by_uuid_string(&vc, &id.to_string())?; dom.create()?; conn.execute( "UPDATE instances SET status = ?1 WHERE uuid = ?2", params!["starting", id], )?; conn.execute( "INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)", params!["instance", "start", serde_json::to_string(&i)?], )?; Ok(()) } #[instrument(err)] #[axum_macros::debug_handler] pub async fn reboot( Path(id): Path, Extension(state): Extension>, _: Tailauth, ) -> Result<(), Error> { let conn = state.pool.get().await?; let i = Instance::from_uuid(&conn, id)?; let vc = Connect::open(&format!("qemu+ssh://root@{}/system", i.host))?; let dom = Domain::lookup_by_uuid_string(&vc, &id.to_string())?; dom.reboot(0)?; conn.execute( "UPDATE instances SET status = ?1 WHERE uuid = ?2", params!["rebooting", id], )?; conn.execute( "INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)", params!["instance", "reboot", serde_json::to_string(&i)?], )?; Ok(()) } #[instrument(err)] #[axum_macros::debug_handler] pub async fn get_by_name( Path(name): Path, Extension(state): Extension>, _: Tailauth, ) -> Result, Error> { let conn = state.pool.get().await?; Ok(Json(Instance::from_name(&conn, name)?)) } #[instrument(err)] #[axum_macros::debug_handler] pub async fn get( Path(id): Path, Extension(state): Extension>, _: Tailauth, ) -> Result, Error> { let conn = state.pool.get().await?; Ok(Json(Instance::from_uuid(&conn, id)?)) } #[instrument(err)] #[axum_macros::debug_handler] pub async fn list( Extension(state): Extension>, _: Tailauth, ) -> Result>, Error> { let conn = state.pool.get().await?; let mut result: Vec = Vec::new(); let mut stmt = conn.prepare( "SELECT uuid, name, host, mac_address, memory, disk_size, zvol_name, status, distro, join_tailnet FROM instances", )?; let instances = stmt.query_map(params![], |row| { Ok(Instance { uuid: row.get(0)?, name: row.get(1)?, host: row.get(2)?, mac_address: row.get(3)?, memory: row.get(4)?, disk_size: row.get(5)?, zvol_name: row.get(6)?, status: row.get(7)?, distro: row.get(8)?, join_tailnet: row.get(9)?, }) })?; for instance in instances { result.push(instance?); } Ok(Json(result)) } #[instrument(err)] pub async fn create( Extension(state): Extension>, Extension(config): Extension>, _: Tailauth, Json(details): Json, ) -> Result, Error> { let id = Uuid::new_v4(); let addrs: Vec = lookup_host(details.host.clone() + ":22".into()) .await? .collect(); if addrs.len() == 0 { return Err(Error::HostDoesntExist(details.host)); } let conn = state.pool.get().await?; let distro = conn.query_row( "SELECT name, download_url, sha256sum, min_size, format FROM distros WHERE name = ?1", params![details.distro.clone()], |row| { Ok(Distro { name: row.get(0)?, download_url: row.get(1)?, sha256sum: row.get(2)?, min_size: row.get(3)?, format: row.get(4)?, }) }, )?; let details = NewInstance { name: details.name.or(rotbart::unique_monster()), memory_mb: details.memory_mb.or(Some(512)), host: details.host.clone(), disk_size_gb: details.disk_size_gb.or(Some(distro.min_size)), zvol_prefix: details.zvol_prefix.or(Some("rpool/safe/vms".into())), distro: distro.name.clone(), sata: details.sata.or(Some(false)), cpus: details.cpus.or(Some(2)), user_data: details .user_data .or(Some(include_str!("../../var/xe-base.yaml").into())), join_tailnet: details.join_tailnet.clone(), }; let mac_addr = random_mac(); let zvol_name = format!( "{}/{}", details.zvol_prefix.clone().unwrap(), details.name.clone().unwrap() ); let ins = Instance { uuid: id, name: details.name.clone().unwrap(), host: details.host.clone(), memory: details.memory_mb.unwrap(), disk_size: details.disk_size_gb.unwrap(), mac_address: mac_addr.clone(), zvol_name: zvol_name.clone(), status: "init".into(), distro: details.distro.clone(), join_tailnet: details.join_tailnet.clone(), }; { let ins = ins.clone(); conn.execute( "INSERT INTO instances(uuid, name, host, mac_address, memory, disk_size, zvol_name, status, distro, join_tailnet) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", params![ ins.uuid, ins.name, ins.host, mac_addr, ins.memory, ins.disk_size, zvol_name, ins.status, ins.distro, ins.join_tailnet, ], )?; conn.execute( "INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)", params!["instance", "create", serde_json::to_string(&ins)?], )?; conn.execute( "INSERT INTO cloudconfig_seeds(uuid, user_data) VALUES (?1, ?2)", params![id.clone(), details.user_data.clone().unwrap()], )?; } drop(conn); { let ins = ins.clone(); tokio::spawn(async move { if let Err(why) = make_instance(config, state, details, ins, distro, mac_addr, id).await { error!("can't make instance: {}", why); } }); } Ok(Json(ins)) } #[instrument(ret, level = "debug", err, skip(config, state, details, id, mac_addr))] async fn make_instance( config: Arc, state: Arc, details: NewInstance, ins: Instance, distro: Distro, mac_addr: String, id: Uuid, ) -> Result<(), Error> { let conn = state.pool.get().await?; let mut ins = ins.clone(); debug!("name: {}", details.name.as_ref().unwrap()); debug!("checking if image exists"); let output = Command::new("ssh") .args([ "-oStrictHostKeyChecking=accept-new", &details.host.clone(), "stat", &format!("$HOME/.cache/within/mkvm/qcow2/{}", distro.sha256sum), ]) .output() .await?; if output.status != ExitStatus::from_raw(0) { debug!("downloading image"); ins.status = "downloading image".into(); conn.execute( "UPDATE instances SET status = ?1 WHERE uuid = ?2", params![ins.status, id], )?; conn.execute( "INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)", params!["instance", ins.status, serde_json::to_string(&ins)?], )?; let output = Command::new("ssh") .args([ &details.host.clone(), "wget", "-O", &format!("$HOME/.cache/within/mkvm/qcow2/{}", distro.sha256sum), &distro.download_url, ]) .output() .await?; if output.status != ExitStatus::from_raw(0) { let stderr = String::from_utf8(output.stderr).unwrap(); Command::new("ssh") .args([ "-oStrictHostKeyChecking=accept-new", &details.host.clone(), "rm", &format!("$HOME/.cache/within/mkvm/qcow2/{}", distro.sha256sum), ]) .status() .await?; return Err(Error::CantDownloadImage( distro.download_url.clone(), stderr, )); } } debug!("making zvol"); let output = Command::new("ssh") .args([ "-lroot", "-oStrictHostKeyChecking=accept-new", &details.host.clone(), "zfs", "create", "-V", &format!("{}G", details.disk_size_gb.unwrap()), &format!( "{}/{}", details.zvol_prefix.as_ref().unwrap(), &details.name.as_ref().unwrap() ), ]) .output() .await?; if output.status != ExitStatus::from_raw(0) { let stderr = String::from_utf8(output.stderr).unwrap(); return Err(Error::CantMakeZvol(details.host.clone(), stderr)); } debug!("hydrating zvol"); ins.status = "hydrating zvol".into(); conn.execute( "UPDATE instances SET status = ?1 WHERE uuid = ?2", params![ins.status, id], )?; conn.execute( "INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)", params!["instance", ins.status, serde_json::to_string(&ins)?], )?; let output = Command::new("ssh") .args([ "-lroot", "-oStrictHostKeyChecking=accept-new", &details.host.clone(), "qemu-img", "convert", "-O", "raw", &format!("/home/cadey/.cache/within/mkvm/qcow2/{}", distro.sha256sum), &format!( "/dev/zvol/{}/{}", details.zvol_prefix.as_ref().unwrap(), &details.name.as_ref().unwrap() ), ]) .output() .await?; if output.status != ExitStatus::from_raw(0) { let stderr = String::from_utf8(output.stderr).unwrap(); return Err(Error::CantHydrateZvol(details.host.clone(), stderr)); } debug!("making init snapshot"); let output = Command::new("ssh") .args([ "-lroot", "-oStrictHostKeyChecking=accept-new", &details.host.clone(), "zfs", "snapshot", &format!( "{}/{}@init", details.zvol_prefix.as_ref().unwrap(), &details.name.as_ref().unwrap() ), ]) .output() .await?; if output.status != ExitStatus::from_raw(0) { let stderr = String::from_utf8(output.stderr).unwrap(); return Err(Error::CantMakeInitSnapshot(details.host.clone(), stderr)); } let mut buf: Vec = vec![]; debug!("rendering xml"); crate::templates::base_xml( &mut buf, details.name.clone().unwrap(), id.to_string(), mac_addr.clone(), details.zvol_prefix.clone().unwrap(), details.sata.unwrap(), details.memory_mb.unwrap() * 1024, details.cpus.unwrap(), format!("{}/api/cloudinit/{}/", config.clone().base_url, id), config.qemu_path.clone(), )?; let buf = String::from_utf8(buf).unwrap(); trace!("libvirt xml:\n{}", buf); let addr: Result<(), Error> = { let details = details.clone(); spawn_blocking(move || { debug!("connecting to host"); let lc = Connect::open(&format!("qemu+ssh://root@{}/system", details.host.clone()))?; debug!("defining domain"); let dom = Domain::define_xml(&lc, &buf)?; debug!("starting domain"); dom.create()?; Ok(()) }) .await? }; let addr = addr?; debug!("ip: {:?}", addr); ins.status = "waiting for cloud-init".into(); conn.execute( "UPDATE instances SET status = ?1 WHERE uuid = ?2", params![ins.status, id], )?; conn.execute( "INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)", params!["instance", ins.status, serde_json::to_string(&ins)?], )?; Ok(()) } ================================================ FILE: src/api/libvirt.rs ================================================ use crate::{Config, Error, Result}; use axum::{extract::Extension, Json}; use serde::{Deserialize, Serialize}; use std::{convert::TryFrom, sync::Arc}; use virt::{connect::Connect, domain::Domain}; #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Machine { pub name: String, pub host: String, pub active: bool, pub uuid: String, pub addr: Option, pub memory_megs: u64, pub cpus: u32, } impl TryFrom for Machine { type Error = Error; fn try_from(dom: Domain) -> Result { let addr: Option = if dom.is_active()? { let mut addr: Vec = dom .interface_addresses(virt_sys::VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_LEASE, 0)? .into_iter() .map(|iface| iface.addrs.clone()) .filter(|addrs| addrs.get(0).is_some()) .map(|addrs| addrs.get(0).unwrap().clone().addr) .collect(); if addr.get(0).is_none() { None } else { Some(addr.swap_remove(0)) } } else { None }; let info = dom.get_info()?; let max_memory = dom.get_max_memory()? / 1024; let conn = dom.get_connect()?; let host = conn.get_hostname()?; Ok(Machine { name: dom.get_name()?, host, active: dom.is_active()?, uuid: dom.get_uuid_string()?, addr, memory_megs: max_memory, cpus: info.nr_virt_cpu, }) } } #[instrument(err, skip(cfg))] pub async fn get_machines(Extension(cfg): Extension>) -> Result>> { let mut result = Vec::new(); for host in &cfg.hosts { result.extend_from_slice(&list_all_vms( &format!("qemu+ssh://root@{}/system", host), host.to_string(), )?); } Ok(Json(result)) } #[instrument(skip(uri), err)] fn list_all_vms(uri: &str, host: String) -> Result> { debug!("connecting to {}: {}", host, uri); let mut conn = Connect::open(uri)?; let mut result = vec![]; for dom in conn.list_all_domains(0)? { result.push(Machine::try_from(dom)?); } conn.close()?; Ok(result) } ================================================ FILE: src/api/mod.rs ================================================ pub mod audit; pub mod cloudinit; pub mod distros; pub mod instances; pub mod libvirt; ================================================ FILE: src/api/vendor-data ================================================ #cloud-config write_files: - owner: root:root path: /etc/update-motd.d/69-waifud permissions: '0755' content: | #!/bin/sh # # This file is written by waifud. echo "" echo "Welcome to waifud <3" ================================================ FILE: src/bin/unique-monster.rs ================================================ use clap::Parser; use names::Name; #[derive(Parser)] #[clap(author, version, about, long_about = None)] struct Cli { #[clap(short, long, default_value = "5")] count: usize, #[clap(short, long)] add_numbers: bool, } fn main() { let cli = Cli::parse(); let generator = names::Generator::new( &rotbart::COMBINED_ADJ, &rotbart::COMBINED_NOUN, if cli.add_numbers { Name::Numbered } else { Name::Plain }, ); generator .take(cli.count) .for_each(|name| println!("{name}")); } ================================================ FILE: src/bin/waifuctl.rs ================================================ #![deny(missing_docs)] //! waifuctl lets you manage VM instances on waifud. #[macro_use] extern crate tracing; use chrono::prelude::*; use clap::{Args, Parser, Subcommand}; use clap_complete::{generate, Shell}; use serde::{Deserialize, Serialize}; use serde_dhall::StaticType; use std::{ convert::TryInto, fs, io::{self, stdout, Write}, path::PathBuf, process::exit, time::Duration, }; use tabular::{row, Table}; use waifud::{ client::Client, libvirt::NewInstance, models::{Distro, Instance}, Error, Result, }; #[derive(Debug, Parser)] #[clap(author, version, about, long_about = None)] #[clap(propagate_version = true)] /// waifuctl lets you manage VM instances on waifud. struct Opt { /// waifud host to connect to, formatted as a http/https URL #[clap(short = 'H', long)] pub host: Option, #[clap(subcommand)] cmd: Command, } #[derive(Deserialize, Serialize, Debug, StaticType, Clone)] struct Config { /// waifud host to connect to, formatted as a http/https URL pub host: String, /// Default cloudconfig to preload into every VM pub userdata: String, } #[derive(Subcommand, Debug)] enum ConfigCmd { /// Shows current config Show, /// Set the waifud host to an arbitrary URL SetHost { /// The waifud host url: String, }, /// Set the default cloudconfig added to instances SetUserdata, } #[derive(Subcommand, Debug)] enum Command { /// Manage audit logs Audit { /// Format all audit logs in JSON #[clap(long)] json: bool, }, /// Manage waifuctl configuration Config { #[clap(subcommand)] cmd: ConfigCmd, }, /// List all instances List, Create(CreateOpts), /// Delete an instance by name Delete { /// Instance name name: String, }, Distro { #[clap(subcommand)] cmd: DistroCmd, }, /// Reset a VM back to factory settings Reinit { /// Instance name name: String, }, /// Turn an instance on Start { /// Instance name name: String, }, /// Turn an instance off Shutdown { /// Instance name name: String, }, /// Manually trigger instance reboot Reboot { /// Instance name name: String, /// Unsafely force reboot #[clap(short, long)] hard: bool, }, /// Utilities to help with managing the waifud project Utils { #[clap(subcommand)] cmd: UtilsCmd, }, } /// Create a new instance #[derive(Args, Debug)] struct CreateOpts { /// Instance name, leave blank to autogenerate #[clap(short, long)] name: Option, /// Memory in megabytes #[clap(short, long, default_value = "512")] memory: i32, /// CPU cores #[clap(short, long, default_value = "2")] cpus: i32, /// Host to put the VM on #[clap(short = 'H', long)] host: String, /// Disk size in GB, leave blank to use distribution default #[clap(short = 's', long = "disk-size")] disk_size: Option, /// ZFS dataset to put the VM disk in #[clap(short, long = "zvol", default_value = "rpool/local/vms")] zvol_prefix: String, /// File containing cloud-init user data, if not set will default to configured value #[clap(short, long)] user_data: Option, /// Distribution to use #[clap(short, long)] distro: String, /// Automagically join the tailnet #[clap(short, long)] join_tailnet: bool, } impl TryInto for CreateOpts { type Error = anyhow::Error; fn try_into(self) -> Result { let user_data = match self.user_data { Some(user_data) => Some(fs::read_to_string(user_data)?), None => None, }; Ok(NewInstance { name: self.name, memory_mb: Some(self.memory), cpus: Some(self.cpus), host: self.host, disk_size_gb: self.disk_size, zvol_prefix: Some(self.zvol_prefix), distro: self.distro, sata: Some(false), user_data, join_tailnet: self.join_tailnet, }) } } /// Manage distribution images in waifud #[derive(Subcommand, Debug)] enum DistroCmd { /// Create a new base distro snapshot Create(CreateDistroOpts), /// Delete a distro image Delete { name: String }, /// List all distros List { /// Show more information #[clap(short)] verbose: bool, }, /// Scrapes current versions for distributions Scrape, /// Updates a base distro snapshot Update(CreateDistroOpts), } /// Defines a base distro snapshot for waifud to use #[derive(Args, Debug)] struct CreateDistroOpts { /// Distribution name, include the version as a suffix #[clap(short, long)] pub name: String, /// Download URL for the qcow2 base snapshot #[clap(short, long = "download-url")] pub download_url: String, /// The sha256 of the qcow2 base snapshot #[clap(short, long = "sha256")] pub sha256sum: String, /// The minimum size of a VM created from this snapshot (gigabytes) #[clap(short, long)] pub min_size: i32, /// The format of the disk image #[clap(short, long, default_value = "waifud://qcow2")] pub format: String, } impl Into for CreateDistroOpts { fn into(self) -> Distro { Distro { name: self.name, download_url: self.download_url, sha256sum: self.sha256sum, min_size: self.min_size, format: self.format, } } } #[derive(Subcommand, Debug)] enum UtilsCmd { /// Generate shell completions Completions { #[clap(value_parser)] shell: Shell, }, /// Generate manpages to a given folder Manpage { path: PathBuf }, } async fn list_instances(cli: Client) -> Result { let instances = cli.list_instances().await?; let mut table = Table::new("{:>} {:<} {:<} {:<} {:<} {:<} {:<}"); table.add_row(row!( "name", "host", "distro", "memory", "ip", "status", "id" )); for instance in instances { let m = cli.get_instance_machine(instance.uuid).await; table.add_row(row!( instance.name, instance.host, instance.distro, instance.memory, match m { Ok(m) => m.addr.unwrap_or("".into()), Err(_) => "".to_string(), }, instance.status, instance.uuid, )); } println!("{}", table); Ok(()) } async fn wait_until_status(cli: &Client, i: Instance, want: T) -> Result where T: Into, { let want = want.into(); let mut i = i.clone(); loop { i = cli.get_instance(i.uuid).await?; io::stdout().flush()?; print!( "{}: {} \r", i.name, i.status ); if i.status == want { break; } tokio::time::sleep(Duration::from_millis(1000)).await; } io::stdout().flush()?; print!("\n"); Ok(()) } async fn start_instance(cli: Client, name: String) -> Result { let i = cli.get_instance_by_name(name).await?; cli.start_instance(i.uuid).await?; wait_until_status(&cli, i.clone(), "running").await?; println!("{} is running", i.name); Ok(()) } async fn shutdown_instance(cli: Client, name: String) -> Result { let i = cli.get_instance_by_name(name).await?; cli.shutdown_instance(i.uuid).await?; println!("shut down {}", i.name); Ok(()) } async fn reboot_instance(cli: Client, name: String, hard: bool) -> Result { let i = cli.get_instance_by_name(name).await?; if hard { cli.hard_reboot_instance(i.uuid).await } else { cli.reboot_instance(i.uuid).await }?; wait_until_status(&cli, i, "running").await?; Ok(()) } #[instrument(ret, level = "debug", err, skip(cli))] async fn create_instance(cli: Client, cfg: Config, opts: CreateOpts) -> Result { let mut ni: NewInstance = opts.try_into()?; if ni.user_data.is_none() { ni.user_data = Some(cfg.userdata); } let i = cli.create_instance(ni).await?; println!("created instance {} on {}", i.name, i.host); wait_until_status(&cli, i.clone(), "running").await?; let m = cli.get_instance_machine(i.uuid).await?; println!( "\r{}: {}: IP address: {}", i.name, i.status, m.addr.unwrap() ); Ok(()) } async fn delete_instance(cli: Client, name: String) -> Result { let i = cli.get_instance_by_name(name.clone()).await; match i { Ok(i) => cli.delete_instance(i.uuid).await?, Err(why) => { eprintln!("no instance named {} was found: {}", name, why); return Err(Error::InstanceDoesntExist(name)); } }; Ok(()) } async fn reinit_instance(cli: Client, name: String) -> Result<()> { let i = cli.get_instance_by_name(name.clone()).await?; cli.reinit_instance(i.uuid).await?; Ok(()) } async fn create_distro(cli: Client, opts: CreateDistroOpts) -> Result { let d: Distro = opts.into(); let d = cli.create_distro(d).await?; println!("created {}", d.name); Ok(()) } async fn update_distro(cli: Client, opts: CreateDistroOpts) -> Result { if let Err(why) = cli.get_distro(opts.name.clone()).await { println!("can't get distro {}: {}", opts.name, why); exit(1); } let d: Distro = opts.into(); let d = cli.update_distro(d).await?; println!("created {}", d.name); Ok(()) } async fn scrape_distros(cli: Client) -> Result { let distros = waifud::scrape::get_all().await?; for distro in distros { cli.update_distro(distro.clone()).await?; println!("updated {}", distro.name); } Ok(()) } async fn list_distros(cli: Client, verbose: bool) -> Result { let distros = cli.list_distros().await?; if verbose { let mut table = Table::new("{:>} {:<} {:<} {:<}"); table.add_row(row!("name", "min size", "sha256", "url")); for distro in distros { table.add_row(row!( distro.name, distro.min_size, distro.sha256sum, distro.download_url, )); } println!("{}", table); } else { let mut table = Table::new("{:<} {:<}"); table.add_row(row!("name", "disk GB")); distros.into_iter().for_each(|d| { table.add_row(row!(d.name, d.min_size.to_string())); }); println!("{}", table); } Ok(()) } async fn delete_distro(cli: Client, name: String) -> Result<()> { cli.delete_distro(name).await?; Ok(()) } async fn audit_list(cli: Client, json: bool) -> Result<()> { let logs = cli.audit_logs().await?; if json { serde_json::to_writer(stdout(), &logs)?; return Ok(()); } let mut table = Table::new("{:>} {:<} {:<} {:<}"); table.add_row(row!("timestamp", "kind", "name", "op")); for log in logs { let ts = NaiveDateTime::from_timestamp(log.ts, 0); table.add_row(row!( ts.to_string(), log.kind, log.name.unwrap_or("".into()), log.op )); } println!("{}", table); Ok(()) } fn config_show(cfg: Config) -> Result { println!("waifud host: {}", cfg.host); println!("default cloudconfig:\n\n{}", cfg.userdata); Ok(()) } fn config_set_host(cfg: Config, url: String) -> Result { let mut cfg = cfg.clone(); cfg.host = url.clone(); let mut fname = dirs::config_dir().unwrap(); fname.push("xeserv"); let _ = fs::create_dir_all(&fname); fname.push("waifuctl"); fname.set_extension("dhall"); let mut fout = fs::File::create(&fname).unwrap(); let cfg = serde_dhall::serialize(&cfg) .static_type_annotation() .to_string()?; fout.write_all(cfg.as_bytes())?; println!("set host to {} in {}", url, fname.to_str().unwrap()); Ok(()) } fn config_set_userdata(cfg: Config) -> Result { let userdata = edit::edit(&cfg.userdata)?; let mut cfg = cfg.clone(); cfg.userdata = userdata; let mut fname = dirs::config_dir().unwrap(); fname.push("xeserv"); let _ = fs::create_dir_all(&fname); fname.push("waifuctl"); fname.set_extension("dhall"); let mut fout = fs::File::create(&fname).unwrap(); let cfg = serde_dhall::serialize(&cfg) .static_type_annotation() .to_string()?; fout.write_all(cfg.as_bytes())?; println!("wrote default cloudconfig to {}", fname.to_str().unwrap()); Ok(()) } fn utils_completions(shell: Shell) -> Result { let cmd = clap::Command::new("waifuctl"); let mut cmd = Opt::augment_args(cmd); generate( shell, &mut cmd, "waifuctl".to_string(), &mut std::io::stdout(), ); Ok(()) } fn utils_gen_manpage(path: PathBuf) -> Result { let cmd = clap::Command::new("waifuctl"); let cmd = Opt::augment_args(cmd); let man = clap_mangen::Man::new(cmd.clone()); let mut buffer: Vec = Default::default(); man.render(&mut buffer)?; std::fs::write(path.join("waifuctl.1"), buffer)?; for scmd in cmd.get_subcommands() { let man = clap_mangen::Man::new(scmd.clone()); let mut buffer: Vec = Default::default(); man.render(&mut buffer)?; std::fs::write( path.join(&format!("waifuctl-{}.1", scmd.get_name())), buffer, )?; if scmd.has_subcommands() { for sscmd in scmd.get_subcommands() { let man = clap_mangen::Man::new(sscmd.clone()); let mut buffer: Vec = Default::default(); man.render(&mut buffer)?; std::fs::write( path.join(&format!( "waifuctl-{}-{}.1", scmd.get_name(), sscmd.get_name() )), buffer, )?; } } } Ok(()) } #[tokio::main] async fn main() -> Result<()> { tracing_subscriber::fmt::init(); let mut opt = Opt::parse(); let cfg = { let mut fname = dirs::config_dir().unwrap(); fname.push("xeserv"); let _ = fs::create_dir_all(&fname); fname.push("waifuctl"); fname.set_extension("dhall"); if opt.host.is_none() { if let Err(_) = fs::metadata(&fname) { let mut fout = fs::File::create(&fname).unwrap(); let cfg = serde_dhall::serialize(&Config { host: "http://[::]:23818".into(), userdata: include_str!("../../var/base.yaml").to_string(), }) .static_type_annotation() .to_string()?; fout.write_all(cfg.as_bytes())?; } } let cfg = serde_dhall::from_file(&fname).parse::()?; debug!("config: {:?}", cfg); if cfg.host.len() == 0 { println!("welcome to waifud, you may want to run `waifuctl config set-host` to point waifuctl to your waifud server"); } cfg }; if let None = opt.host { opt.host = Some(cfg.host.clone()); } debug!("{:?}", opt); let cli = Client::new(opt.host.unwrap())?; if let Err(why) = match opt.cmd { Command::Audit { json } => audit_list(cli, json).await, Command::Distro { cmd } => match cmd { DistroCmd::Create(opts) => create_distro(cli, opts).await, DistroCmd::Delete { name } => delete_distro(cli, name).await, DistroCmd::List { verbose } => list_distros(cli, verbose).await, DistroCmd::Scrape => scrape_distros(cli).await, DistroCmd::Update(opts) => update_distro(cli, opts).await, }, Command::List => list_instances(cli).await, Command::Create(opts) => create_instance(cli, cfg, opts).await, Command::Delete { name } => delete_instance(cli, name).await, Command::Reboot { name, hard } => reboot_instance(cli, name, hard).await, Command::Reinit { name } => reinit_instance(cli, name).await, Command::Start { name } => start_instance(cli, name).await, Command::Shutdown { name } => shutdown_instance(cli, name).await, Command::Config { cmd } => match cmd { ConfigCmd::Show => config_show(cfg), ConfigCmd::SetHost { url } => config_set_host(cfg, url), ConfigCmd::SetUserdata => config_set_userdata(cfg), }, Command::Utils { cmd } => match cmd { UtilsCmd::Completions { shell } => utils_completions(shell), UtilsCmd::Manpage { path } => utils_gen_manpage(path), }, } { eprintln!("OOPSIE WOOPSIE!! Uwu We made a fucky wucky!! A wittle fucko boingo! The code monkeys at our headquarters are working VEWY HAWD to fix this!"); eprintln!("{}", why); } Ok(()) } ================================================ FILE: src/build.rs ================================================ use ructe::{Result, Ructe}; fn main() -> Result<()> { Ructe::from_env()?.compile_templates("templates") } ================================================ FILE: src/client/mod.rs ================================================ use crate::{ api::libvirt::Machine, libvirt::NewInstance, models::{AuditEvent, Distro, Instance}, Result, }; use reqwest::header; use std::time::Duration; use url::Url; use uuid::Uuid; pub struct Client { base_url: Url, cli: reqwest::Client, } impl Client { pub fn new(base_url: String) -> Result { let mut headers = header::HeaderMap::new(); headers.insert( header::USER_AGENT, header::HeaderValue::from_str(crate::APPLICATION_NAME)?, ); let cli = reqwest::Client::builder() .default_headers(headers) .connect_timeout(Duration::from_millis(500)) .build()?; Ok(Client { base_url: Url::parse(&base_url)?, cli, }) } pub async fn audit_logs(&self) -> Result> { let mut u = self.base_url.clone(); u.set_path("/api/v1/auditlogs"); Ok(self .cli .get(u) .send() .await? .error_for_status()? .json() .await?) } pub async fn create_instance(&self, ni: NewInstance) -> Result { let mut u = self.base_url.clone(); u.set_path("/api/v1/instances"); Ok(self .cli .post(u) .json(&ni) .send() .await? .error_for_status()? .json() .await?) } pub async fn delete_instance(&self, id: Uuid) -> Result { let mut u = self.base_url.clone(); u.set_path(&format!("/api/v1/instances/{}", id)); self.cli.delete(u).send().await?.error_for_status()?; Ok(()) } pub async fn reinit_instance(&self, id: Uuid) -> Result { let mut u = self.base_url.clone(); u.set_path(&format!("/api/v1/instances/{}/reinit", id)); self.cli.post(u).send().await?.error_for_status()?; Ok(()) } pub async fn list_instances(&self) -> Result> { let mut u = self.base_url.clone(); u.set_path("/api/v1/instances"); Ok(self .cli .get(u) .send() .await? .error_for_status()? .json() .await?) } pub async fn get_instance(&self, id: Uuid) -> Result { let mut u = self.base_url.clone(); u.set_path(&format!("/api/v1/instances/{}", id)); Ok(self .cli .get(u) .send() .await? .error_for_status()? .json() .await?) } pub async fn get_instance_by_name(&self, name: String) -> Result { let mut u = self.base_url.clone(); u.set_path(&format!("/api/v1/instances/name/{}", name)); Ok(self .cli .get(u) .send() .await? .error_for_status()? .json() .await?) } pub async fn get_instance_machine(&self, id: Uuid) -> Result { let mut u = self.base_url.clone(); u.set_path(&format!("/api/v1/instances/{}/machine", id)); Ok(self .cli .get(u) .send() .await? .error_for_status()? .json() .await?) } pub async fn shutdown_instance(&self, id: Uuid) -> Result<()> { let mut u = self.base_url.clone(); u.set_path(&format!("/api/v1/instances/{}/shutdown", id)); self.cli.post(u).send().await?.error_for_status()?; Ok(()) } pub async fn start_instance(&self, id: Uuid) -> Result<()> { let mut u = self.base_url.clone(); u.set_path(&format!("/api/v1/instances/{}/start", id)); self.cli.post(u).send().await?.error_for_status()?; Ok(()) } pub async fn hard_reboot_instance(&self, id: Uuid) -> Result<()> { let mut u = self.base_url.clone(); u.set_path(&format!("/api/v1/instances/{}/hardreboot", id)); self.cli.post(u).send().await?.error_for_status()?; Ok(()) } pub async fn reboot_instance(&self, id: Uuid) -> Result<()> { let mut u = self.base_url.clone(); u.set_path(&format!("/api/v1/instances/{}/reboot", id)); self.cli.post(u).send().await?.error_for_status()?; Ok(()) } pub async fn create_distro(&self, d: Distro) -> Result { let mut u = self.base_url.clone(); u.set_path("/api/v1/distros"); Ok(self .cli .post(u) .json(&d) .send() .await? .error_for_status()? .json() .await?) } pub async fn list_distros(&self) -> Result> { let mut u = self.base_url.clone(); u.set_path("/api/v1/distros"); Ok(self .cli .get(u) .send() .await? .error_for_status()? .json() .await?) } pub async fn update_distro(&self, d: Distro) -> Result { let mut u = self.base_url.clone(); u.set_path(&format!("/api/v1/distros/{}", d.name)); Ok(self .cli .post(u) .json(&d) .send() .await? .error_for_status()? .json() .await?) } pub async fn get_distro(&self, name: String) -> Result { let mut u = self.base_url.clone(); u.set_path(&format!("/api/v1/distros/{}", name)); Ok(self .cli .get(u) .send() .await? .error_for_status()? .json() .await?) } pub async fn delete_distro(&self, name: String) -> Result { let mut u = self.base_url.clone(); u.set_path(&format!("/api/v1/distros/{}", name)); self.cli.delete(u).send().await?.error_for_status()?; Ok(()) } } ================================================ FILE: src/config.rs ================================================ use serde::{Deserialize, Serialize}; use std::{fmt, net::IpAddr}; #[derive(Clone, Serialize, Deserialize)] pub struct Config { #[serde(rename = "baseURL")] pub base_url: String, pub hosts: Vec, #[serde(rename = "bindHost")] pub bind_host: IpAddr, pub port: u16, #[serde(rename = "rpoolBase")] pub rpool_base: String, #[serde(rename = "qemuPath")] pub qemu_path: String, #[serde(skip_serializing)] pub tailscale: Tailscale, } impl fmt::Debug for Config { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Config()") } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Tailscale { #[serde(rename = "apiKey")] pub api_key: String, pub tailnet: String, } ================================================ FILE: src/lib.rs ================================================ #[macro_use] extern crate tracing; use axum::{ http::StatusCode, response::{IntoResponse, Response}, }; use bb8::Pool; use bb8_rusqlite::RusqliteConnectionManager; use hyper::header::InvalidHeaderValue; use rusqlite::Connection; use std::{env, fmt, net::AddrParseError}; pub const APPLICATION_NAME: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); pub fn establish_connection() -> Result { let database_url = env::var("DATABASE_URL").unwrap_or("./var/waifud.db".to_string()); Ok(Connection::open(&database_url)?) } pub type Result = std::result::Result; pub mod admin; pub mod api; pub mod client; pub mod config; pub mod libvirt; pub mod migrate; pub mod models; pub mod scrape; pub mod tailauth; pub use config::Config; pub struct State { pub pool: Pool, } impl fmt::Debug for State { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "State()") } } impl State { pub async fn new() -> Result { let mgr = RusqliteConnectionManager::new( env::var("DATABASE_URL").unwrap_or("./var/waifud.db".to_string()), ); let pool = bb8::Pool::builder().build(mgr).await?; Ok(State { pool }) } } #[derive(thiserror::Error, Debug)] pub enum Error { #[error("libvirt error: {0}")] Libvirt(#[from] virt::error::Error), #[error("dhall parsing error: {0}")] Dhall(#[from] serde_dhall::Error), #[error("json parsing error: {0}")] Json(#[from] serde_json::Error), #[error("yaml error: {0}")] YAML(#[from] serde_yaml::Error), #[error("database error: {0}")] SQLite(#[from] rusqlite::Error), #[error("database pool error: {0}")] SQLitePool(#[from] bb8_rusqlite::Error), #[error("internal tokio error: {0}")] TokioJoin(#[from] tokio::task::JoinError), #[error("hyper error: {0}")] Hyper(#[from] hyper::Error), #[error("can't convert HTTP header to string: {0}")] HTTPHeaderToString(#[from] axum::http::header::ToStrError), #[error("address parse error: {0}")] AddrParse(#[from] AddrParseError), #[error("io error: {0}")] IO(#[from] std::io::Error), #[error("reqwest error: {0}")] Reqwest(#[from] reqwest::Error), #[error("url error: {0}")] URL(#[from] url::ParseError), #[error("tailscale error: {0}")] Tailscale(#[from] tailscale_client::Error), #[error("other error: {0}")] Catchall(String), #[error("{0}")] Anyhow(#[from] anyhow::Error), #[error("hex decode error: {0}")] Hex(#[from] hex::FromHexError), #[error("tailscaled localapi error: {0}")] TailscaledLocalAPI(#[from] ts_localapi::Error), #[error("invalid header value: {0}")] InvalidHTTPHeader(#[from] InvalidHeaderValue), // Application errors #[error("host {0} doesn't exist")] HostDoesntExist(String), #[error("instance {0} doesn't exist")] InstanceDoesntExist(String), #[error("can't download {0}:\n\n{1}")] CantDownloadImage(String, String), #[error("can't create zfs zvol on {0}:\n\n{1}")] CantMakeZvol(String, String), #[error("can't delete zfs zvol on {0}:\n\n{1}")] CantDeleteZvol(String, String), #[error("can't rollback zfs zvol to snapshot {1} on {0}:\n\n{2}")] CantRollbackZvol(String, String, String), #[error("can't hydrate zfs zvol on {0}:\n\n{1}")] CantHydrateZvol(String, String), #[error("can't create zfs init snapshot on {0}:\n\n{1}")] CantMakeInitSnapshot(String, String), #[error("internal middleware logic error")] BadMiddlewareStack, #[error("insufficient authorization to perform this action")] Unauthorized, #[error("can't make token: {0}")] CantMakeToken(String), } impl From> for Error where E: std::error::Error + Send + 'static, { fn from(err: bb8::RunError) -> Self { Self::Catchall(format!("{}", err)) } } impl IntoResponse for Error { fn into_response(self) -> Response { let interm: (StatusCode, String) = self.into(); interm.into_response() } } impl Into<(StatusCode, String)> for Error { fn into(self) -> (StatusCode, String) { match self { Error::Unauthorized => ( StatusCode::UNAUTHORIZED, "you lack authorization".to_string(), ), Error::Libvirt(why) => (StatusCode::INTERNAL_SERVER_ERROR, why.message().to_string()), Error::Dhall(why) => (StatusCode::BAD_REQUEST, format!("{}", why)), Error::SQLite(err) => match err { rusqlite::Error::QueryReturnedNoRows => { (StatusCode::NOT_FOUND, "404 not found".into()) } _ => (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", err)), }, _ => (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", self)), } } } include!(concat!(env!("OUT_DIR"), "/templates.rs")); ================================================ FILE: src/libvirt.rs ================================================ use mac_address::MacAddress; use rand::Rng; use serde::{Deserialize, Serialize}; #[derive(Deserialize, Serialize, Clone, Debug)] pub struct NewInstance { pub name: Option, pub memory_mb: Option, pub cpus: Option, pub host: String, pub disk_size_gb: Option, pub zvol_prefix: Option, pub distro: String, pub sata: Option, pub user_data: Option, pub join_tailnet: bool, } pub fn random_mac() -> String { let mut addr = rand::thread_rng().gen::<[u8; 6]>(); addr[0] = (addr[0] | 2) & 0xfe; MacAddress::new(addr).to_string() } ================================================ FILE: src/main.rs ================================================ #[macro_use] extern crate tracing; use axum::{ routing::{delete, get, post}, Extension, Router, }; use axum_extra::routing::SpaRouter; use std::{net::SocketAddr, sync::Arc}; use tower::limit::ConcurrencyLimitLayer; use tower_http::trace::TraceLayer; use waifud::{ admin, api::{self, audit, cloudinit, distros, instances}, Config, Result, State, }; #[tokio::main] async fn main() -> Result { tracing_subscriber::fmt::init(); waifud::migrate::run()?; let cfg: Config = serde_dhall::from_file("./config.dhall").parse()?; let files = SpaRouter::new("/static", "static"); let middleware = tower::ServiceBuilder::new() .layer(TraceLayer::new_for_http()) .layer(ConcurrencyLimitLayer::new(64)) .layer(Extension(Arc::new(tailscale_client::Client::new( waifud::APPLICATION_NAME.to_string(), cfg.tailscale.api_key.clone(), cfg.tailscale.tailnet.clone(), )?))) .layer(Extension(Arc::new(State::new().await?))) .layer(Extension(Arc::new(cfg))); let admin_panel = Router::new() .route("/", get(admin::home)) .route("/api/config", get(admin::config)) .route("/test", get(admin::test_handler)) .route("/instances", get(admin::instances)) .route("/instances/create", get(admin::instance_create)) .route("/instances/:id", get(admin::instance)) .route("/distros", get(admin::distro_list)) .layer(middleware.clone()); let cloudinit = Router::new() .route("/:id/meta-data", get(cloudinit::meta_data)) .route("/:id/user-data", get(cloudinit::user_data)) .route("/:id/vendor-data", get(cloudinit::vendor_data)) .layer(middleware.clone()); let api = Router::new() .route("/auditlogs", get(audit::list)) .route("/auditlogs/instance/:id", get(audit::list_for_instance)) .route("/distros", get(distros::list)) .route("/distros", post(distros::create)) .route("/distros/:name", post(distros::update)) .route("/distros/:name", get(distros::get)) .route("/distros/:name", delete(distros::delete)) .route("/instances", post(instances::create)) .route("/instances", get(instances::list)) .route("/instances/:id", get(instances::get)) .route("/instances/:id/reinit", post(instances::reinit)) .route("/instances/:id/hardreboot", post(instances::hard_reboot)) .route("/instances/:id/reboot", post(instances::reboot)) .route("/instances/:id/start", post(instances::start)) .route("/instances/:id/shutdown", post(instances::shutdown)) .route("/instances/name/:name", get(instances::get_by_name)) .route("/instances/:id", delete(instances::delete)) .route("/instances/:id/machine", get(instances::get_machine)) .route("/libvirt/machines", get(api::libvirt::get_machines)) .layer(middleware.clone()); let app = Router::new() .nest("/api/v1", api) .nest("/api/cloudinit", cloudinit) .nest("/admin", admin_panel) .merge(files); // tokio::spawn(waifud::scrape::cron()); let addr = &"[::]:23818".parse()?; info!("listening on {}", addr); axum::Server::bind(addr) .serve(app.into_make_service_with_connect_info::()) .await?; Ok(()) } ================================================ FILE: src/migrate/20220225-session.sql ================================================ CREATE TABLE IF NOT EXISTS sessions ( uuid TEXT NOT NULL PRIMARY KEY , user TEXT NOT NULL , expired BOOLEAN NOT NULL DEFAULT FALSE ); ================================================ FILE: src/migrate/20220814-no-session.sql ================================================ DROP TABLE sessions; ================================================ FILE: src/migrate/base_schema.sql ================================================ CREATE TABLE IF NOT EXISTS instances ( uuid TEXT PRIMARY KEY NOT NULL , name TEXT NOT NULL UNIQUE , host TEXT NOT NULL , mac_address TEXT NOT NULL , memory INTEGER NOT NULL , disk_size INTEGER NOT NULL , zvol_name TEXT NOT NULL , status TEXT NOT NULL DEFAULT 'unknown' , distro TEXT NOT NULL , join_tailnet BOOLEAN NOT NULL DEFAULT FALSE ); CREATE TABLE IF NOT EXISTS audit_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT , ts INTEGER NOT NULL DEFAULT (STRFTIME('%s', 'now')) , kind TEXT NOT NULL , op TEXT NOT NULL , data TEXT , uuid TEXT GENERATED ALWAYS AS (json_extract(data, '$.uuid')) , name TEXT GENERATED ALWAYS AS (json_extract(data, '$.name')) ); CREATE INDEX IF NOT EXISTS audit_logs_uuid ON audit_logs(uuid); CREATE INDEX IF NOT EXISTS audit_logs_name ON audit_logs(name); CREATE TABLE IF NOT EXISTS cloudconfig_seeds ( uuid TEXT PRIMARY KEY NOT NULL , user_data TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS distros ( name TEXT PRIMARY KEY NOT NULL , download_url TEXT NOT NULL , sha256sum TEXT NOT NULL , min_size INTEGER NOT NULL , format TEXT NOT NULL ); ================================================ FILE: src/migrate/mod.rs ================================================ use crate::establish_connection; use anyhow::Result; use rusqlite_migration::{Migrations, M}; #[instrument(err)] pub fn run() -> Result<()> { info!("running"); let mut conn = establish_connection()?; let migrations = Migrations::new(vec![ M::up(include_str!("./base_schema.sql")), M::up(include_str!("./20220225-session.sql")), M::up(include_str!("./20220814-no-session.sql")), ]); conn.pragma_update(None, "journal_mode", &"WAL").unwrap(); migrations.to_latest(&mut conn)?; Ok(()) } ================================================ FILE: src/models.rs ================================================ use bb8::PooledConnection; use bb8_rusqlite::RusqliteConnectionManager; use rusqlite::params; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::Result; #[derive(Debug, Default, Deserialize, Serialize, Clone)] pub struct Instance { pub uuid: Uuid, pub name: String, pub host: String, pub mac_address: String, pub memory: i32, pub disk_size: i32, pub zvol_name: String, pub status: String, pub distro: String, pub join_tailnet: bool, } impl Instance { pub fn from_name( conn: &PooledConnection<'_, RusqliteConnectionManager>, name: String, ) -> Result { let mut stmt = conn.prepare( "SELECT uuid, name, host, mac_address, memory, disk_size, zvol_name, status, distro, join_tailnet FROM instances WHERE name = ?1", )?; let instance = stmt.query_row(params![name], |row| { Ok(Instance { uuid: row.get(0)?, name: row.get(1)?, host: row.get(2)?, mac_address: row.get(3)?, memory: row.get(4)?, disk_size: row.get(5)?, zvol_name: row.get(6)?, status: row.get(7)?, distro: row.get(8)?, join_tailnet: row.get(9)?, }) })?; Ok(instance) } pub fn from_uuid( conn: &PooledConnection<'_, RusqliteConnectionManager>, id: Uuid, ) -> Result { let mut stmt = conn.prepare( "SELECT uuid, name, host, mac_address, memory, disk_size, zvol_name, status, distro, join_tailnet FROM instances WHERE uuid = ?1", )?; let instance = stmt.query_row(params![id], |row| { Ok(Instance { uuid: row.get(0)?, name: row.get(1)?, host: row.get(2)?, mac_address: row.get(3)?, memory: row.get(4)?, disk_size: row.get(5)?, zvol_name: row.get(6)?, status: row.get(7)?, distro: row.get(8)?, join_tailnet: row.get(9)?, }) })?; Ok(instance) } } pub struct CloudconfigSeed { pub uuid: Uuid, pub user_data: String, } #[derive(Deserialize, Serialize, Debug, Clone)] pub struct Distro { pub name: String, #[serde(rename = "downloadURL")] pub download_url: String, #[serde(rename = "sha256Sum")] pub sha256sum: String, #[serde(rename = "minSize")] pub min_size: i32, pub format: String, } impl Distro { pub fn from_name( conn: &PooledConnection<'_, RusqliteConnectionManager>, name: String, ) -> Result { Ok(conn.query_row( "SELECT name , download_url , sha256sum , min_size , format FROM distros WHERE name = ?1", params![name], |row| { Ok(Distro { name: row.get(0)?, download_url: row.get(1)?, sha256sum: row.get(2)?, min_size: row.get(3)?, format: row.get(4)?, }) }, )?) } } #[derive(Deserialize, Serialize, Debug, Clone)] pub struct AuditEvent { pub id: i32, pub ts: i64, pub kind: String, pub op: String, pub data: Option, pub uuid: Option, pub name: Option, } impl AuditEvent { pub fn get_for_instance( uuid: Uuid, conn: &PooledConnection<'_, RusqliteConnectionManager>, ) -> Result> { let mut stmt = conn.prepare("SELECT id, ts, kind, op, data, uuid, name from audit_logs where uuid=? and kind='instance'")?; let vals: Vec = stmt .query_map(params![uuid.to_string()], |row| { Ok(AuditEvent { id: row.get(0)?, ts: row.get(1)?, kind: row.get(2)?, op: row.get(3)?, data: row.get(4)?, uuid: row.get(5)?, name: row.get(6)?, }) })? .into_iter() .map(Result::unwrap) .collect(); Ok(vals) } pub fn get_all( conn: &PooledConnection<'_, RusqliteConnectionManager>, ) -> Result> { let mut stmt = conn.prepare("SELECT id, ts, kind, op, data, uuid, name from audit_logs")?; let vals: Vec = stmt .query_map(params![], |row| { Ok(AuditEvent { id: row.get(0)?, ts: row.get(1)?, kind: row.get(2)?, op: row.get(3)?, data: row.get(4)?, uuid: row.get(5)?, name: row.get(6)?, }) })? .into_iter() .map(Result::unwrap) .collect(); Ok(vals) } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Session { pub uuid: Uuid, pub user: String, pub expired: bool, } impl Session { #[instrument(skip(conn), err)] pub fn get( conn: &PooledConnection<'_, RusqliteConnectionManager>, id: Uuid, ) -> Result { Ok(conn.query_row( "SELECT (uuid, user, expired) FROM sessions WHERE uuid = ?1", params![id], |row| { Ok(Session { uuid: row.get(0)?, user: row.get(1)?, expired: row.get(2)?, }) }, )?) } } ================================================ FILE: src/scrape/amazon_linux.rs ================================================ use crate::{models::Distro, Error}; use scraper::Html; use url::Url; /// # Scraper for Amazon Linux /// /// This scrapes the Amazon Linux cloud image site and extracts out the URL of the latest /// release of Amazon Linux and its sha256 sum. pub async fn scrape() -> crate::Result { let sel = scraper::Selector::parse("a").expect("selector to parse"); let res = { let https = hyper_tls::HttpsConnector::new(); let cli = hyper::Client::builder().build::<_, hyper::Body>(https); cli.get(hyper::Uri::from_static( "https://cdn.amazonlinux.com/os-images/latest/kvm/", )) .await }?; let release_base = res .headers() .get(axum::http::header::LOCATION) .ok_or(Error::Catchall( "why did the redirect not work?".to_string(), ))? .to_str()?; let response_html = reqwest::get(release_base) .await? .error_for_status()? .text() .await?; let doc = Html::parse_document(&response_html); let link = doc .select(&sel) .filter(|elem| elem.value().attr("href").is_some()) .map(|elem| elem.value().attr("href").unwrap()) .filter(|link| link.starts_with("amzn2-kvm")) .take(1) .map(ToString::to_string) .collect::>(); let link = link.get(0).ok_or(crate::Error::Catchall( "can't get last element of Amazon Linux image list".to_string(), ))?; let image_url = Url::parse(release_base)?.join(link)?; let shasum_url = Url::parse(release_base)?.join("SHA256SUMS")?; let shasum = reqwest::get(shasum_url) .await? .error_for_status()? .text() .await?; let shasum = shasum .split(" ") .take(1) .map(ToString::to_string) .collect::>(); let shasum = shasum.get(0).unwrap(); Ok(Distro { name: "amazon-linux-2".to_string(), download_url: image_url.to_string(), sha256sum: shasum.to_string(), min_size: 25, format: "waifud://qcow2".to_string(), }) } ================================================ FILE: src/scrape/arch.rs ================================================ use scraper::Html; use crate::models::Distro; /// # Scraper for https://geo.mirror.pkgbuild.com/images/ /// /// This scrapes the Arch Linux cloud image site and extracts out the URL of the latest /// release of Arch Linux and its sha256 sum. const RELEASE_BASE: &'static str = "https://geo.mirror.pkgbuild.com/images/"; pub async fn scrape() -> crate::Result { let sel = scraper::Selector::parse("a").expect("selector to parse"); let response_html = reqwest::get("https://geo.mirror.pkgbuild.com/images/") .await? .error_for_status()? .text() .await?; let doc = Html::parse_document(&response_html); let link = doc.select(&sel).last().ok_or(crate::Error::Catchall( "can't get last element of Arch image list".to_string(), ))?; let u = url::Url::parse(RELEASE_BASE)?.join(link.value().attr("href").ok_or( crate::Error::Catchall("link has no href, how???".to_string()), )?)?; let response_html = reqwest::get(u.as_str()) .await? .error_for_status()? .text() .await?; let doc = Html::parse_document(&response_html); let links: Vec = doc .select(&sel) .filter(|elem| elem.value().attr("href").is_some()) .map(|elem| elem.value().attr("href")) .map(Option::unwrap) .filter(|path| path.starts_with("Arch-Linux-x86_64-cloudimg-")) .map(ToString::to_string) .collect(); if links.len() != 4 { return Err(crate::Error::Catchall( "wrong number of things in the list, wanted 4".to_string(), )); } let image_url = links.get(0).unwrap(); let shasum_url = links.get(1).unwrap(); let shasum = reqwest::get(u.join(&shasum_url)?) .await? .error_for_status()? .text() .await?; let shasum = shasum .split(" ") .take(1) .map(ToString::to_string) .collect::>(); let shasum = shasum.get(0).unwrap(); let image_url = u.join(&image_url)?.as_str().to_string(); Ok(Distro { name: "arch".to_string(), download_url: image_url, sha256sum: shasum.to_string(), min_size: 2, format: "waifud://qcow2".to_string(), }) } ================================================ FILE: src/scrape/mod.rs ================================================ use std::time::Duration; use crate::{models::Distro, Result}; use futures::future::join_all; use rusqlite::params; use tokio::time::Instant; pub mod amazon_linux; pub mod arch; pub mod nixos; pub mod rocky_linux; pub mod ubuntu; pub async fn get_all() -> Result> { let ubuntus = join_all(vec![ ubuntu::scrape(("22.04", "jammy")), ubuntu::scrape(("20.04", "focal")), ubuntu::scrape(("18.04", "bionic")), ]) .await; let mut result: Vec = vec![ arch::scrape().await?, amazon_linux::scrape().await?, rocky_linux::scrape(9).await?, ]; for distro in ubuntus { result.push(distro?); } for distro in nixos::scrape().await? { result.push(distro); } Ok(result) } pub async fn cron() { let mut conn = crate::establish_connection().unwrap(); 'outer: loop { tokio::time::sleep_until( Instant::now() .checked_add(Duration::from_secs(24 * 60 * 60)) .unwrap(), ) .await; debug!("scraping distro images from upstream"); let distros = get_all().await.unwrap(); let tx = conn.transaction().unwrap(); for d in distros { if let Err(why) = tx.execute( "UPDATE distros SET download_url = ?1 , sha256sum = ?2 , min_size = ?3 , format = ?4 WHERE name = ?5", params![d.download_url, d.sha256sum, d.min_size, d.format, d.name], ) { error!("can't update distros: {why}"); continue 'outer; } if let Err(why) = tx.execute( "INSERT INTO audit_logs(kind, op, data) VALUES (?1, ?2, ?3)", params!["distro", "update", serde_json::to_string(&d).unwrap()], ) { error!("can't update audit logs: {why}"); continue 'outer; } } if let Err(why) = tx.commit() { error!("can't commit transaction: {why}"); } } } ================================================ FILE: src/scrape/nixos.rs ================================================ use crate::models::Distro; use serde::{Deserialize, Serialize}; use std::collections::HashMap; #[derive(Deserialize, Serialize, Debug)] pub struct Metadata { pub fname: String, pub sha256: String, } pub async fn scrape() -> crate::Result> { let md: HashMap = reqwest::get("https://xena.greedo.xeserv.us/pkg/nixos/metadata.json") .await? .error_for_status()? .json() .await?; let mut result: Vec = vec![]; for (key, val) in md.iter() { result.push(Distro { name: format!("nixos-{key}"), download_url: format!("https://xena.greedo.xeserv.us/pkg/nixos/{}", val.fname), sha256sum: val.sha256.clone(), min_size: 8, format: "waifud://qcow2".to_string(), }) } Ok(result) } ================================================ FILE: src/scrape/rocky_linux.rs ================================================ use scraper::{ElementRef, Html}; use crate::models::Distro; /// # Scraper for Rocky Linux cloud images /// /// This scrapes the Rocky Linux cloud image site and extracts out the URL of the latest /// release of Rocky Linux and its sha256 sum. const RELEASE_BASE: &'static str = "http://download.rockylinux.org/pub/rocky/"; #[instrument] pub async fn scrape(version: i32) -> crate::Result { let sel = scraper::Selector::parse("a").expect("selector to parse"); let mut base: String = RELEASE_BASE.to_string(); base.push_str(&format!("{}", version)); base.push_str("/images/"); if version == 9 { base.push_str("x86_64/"); } let u = url::Url::parse(&base)?; debug!("url: {u}"); let response_html = reqwest::get(u.as_str()) .await? .error_for_status()? .text() .await?; let doc = Html::parse_document(&response_html); let elems = doc.select(&sel).collect::>(); let elems = elems .into_iter() .rev() .filter(|elem| elem.value().attr("href").is_some()) .map(|elem| elem.value().attr("href").unwrap()) .filter(|link| link.contains("GenericCloud")) .filter(|link| link.contains("x86_64")) .filter(|link| !link.contains("latest")) .filter(|link| link.ends_with(".qcow2")) .collect::>(); let link = elems.get(0).ok_or(crate::Error::Catchall( "can't get second to last element of image list".to_string(), ))?; let u = u.join(link)?; debug!(url = u.to_string(), link = link); let image_url = u.to_string(); let shasum_url = u.join("./CHECKSUM")?; debug!("shasum url: {shasum_url}"); let mut shasums = reqwest::get(shasum_url) .await? .error_for_status()? .text() .await?; if version != 8 { shasums = shasums .split("\n") .filter(|line| line.contains("SHA256")) .collect::>() .join("\n"); } let mut shasum = String::new(); for line in shasums.split("\n").filter(|line| line.contains(link)) { if line == "" { break; } if version != 8 { let sides: Vec<&str> = line.split(" ").collect(); if sides.len() != 4 { error!("Somehow this doesn't have 3 spaces in it {line:?}"); continue; } shasum = sides.get(3).unwrap().to_string() } else { let sides: Vec<&str> = line.split(" ").collect(); if sides.len() != 2 { error!("Somehow this doesn't have two spaces in it {line:?}"); continue; } shasum = sides.get(0).unwrap().to_string(); } } let image_url = u.join(&image_url)?.as_str().to_string(); Ok(Distro { name: format!("rocky-linux-{version}"), download_url: image_url, sha256sum: shasum.to_string(), min_size: 10, format: "waifud://qcow2".to_string(), }) } ================================================ FILE: src/scrape/ubuntu.rs ================================================ use scraper::{ElementRef, Html}; use std::collections::HashMap; use crate::{models::Distro, Error}; /// # Scraper for Ubuntu cloud images /// /// This scrapes the Ubuntu cloud image site and extracts out the URL of the latest /// release of Ubuntu and its sha256 sum. const RELEASE_BASE: &'static str = "http://cloud-images.ubuntu.com/daily/server/"; pub async fn scrape((version, name): (&str, &str)) -> crate::Result { let sel = scraper::Selector::parse("a").expect("selector to parse"); let mut base: String = RELEASE_BASE.to_string(); base.push_str(&name); base.push_str("/"); let u = url::Url::parse(&base)?; debug!("url: {u}"); let response_html = reqwest::get(u.as_str()) .await? .error_for_status()? .text() .await?; let doc = Html::parse_document(&response_html); let elems = doc.select(&sel).collect::>(); let elems = elems.into_iter().rev().collect::>(); let link = elems.get(2).ok_or(crate::Error::Catchall( "can't get second to last element of image list".to_string(), ))?; let u = u .join(name)? .join(link.value().attr("href").ok_or(crate::Error::Catchall( "link has no href, how???".to_string(), ))?)?; debug!("url: {u}"); let response_html = reqwest::get(u.as_str()) .await? .error_for_status()? .text() .await?; let doc = Html::parse_document(&response_html); let links: Vec = doc .select(&sel) .filter(|elem| elem.value().attr("href").is_some()) .map(|elem| elem.value().attr("href")) .map(Option::unwrap) .filter(|path| path.ends_with("-server-cloudimg-amd64.img")) .map(ToString::to_string) .collect(); let image_url = links.get(0).unwrap(); let shasum_url = u.join("SHA256SUMS")?; let shasums = reqwest::get(shasum_url) .await? .error_for_status()? .text() .await?; let mut sha_map = HashMap::::new(); for line in shasums.split("\n") { let sides: Vec<&str> = line.split(" *").collect(); if sides.len() != 2 { error!("Somehow this doesn't have two spaces in it {line:?}"); continue; } sha_map.insert( sides.get(1).unwrap().to_string(), sides.get(0).unwrap().to_string(), ); } // println!("{}", serde_dhall::serialize(&sha_map).to_string()?); let mut key: String = name.clone().to_string(); key.push_str("-server-cloudimg-amd64.img"); let shasum = sha_map .get(&key) .ok_or(Error::Catchall(format!("can't find shasum for {name}")))?; let image_url = u.join(&image_url)?.as_str().to_string(); Ok(Distro { name: format!("ubuntu-{version}"), download_url: image_url, sha256sum: shasum.to_string(), min_size: 5, format: "waifud://qcow2".to_string(), }) } ================================================ FILE: src/tailauth.rs ================================================ use crate::Error; use async_trait::async_trait; use axum::{extract::FromRequestParts, http::request::Parts, RequestPartsExt}; pub struct Tailauth(pub ts_localapi::User, pub ts_localapi::WhoisPeer); #[async_trait] impl FromRequestParts for Tailauth where S: Send + Sync, { type Rejection = Error; async fn from_request_parts(req: &mut Parts, _state: &S) -> Result { let addr: axum_client_ip::ClientIp = req.extract().await.map_err(|_| Error::BadMiddlewareStack)?; let result = ts_localapi::whois((addr.0, 0).into()).await?; info!( user = result.user_profile.login_name, ip = addr.0.to_string(), platform = result .node .clone() .hostinfo .os .unwrap_or("".to_string()) ); Ok(Tailauth(result.user_profile, result.node)) } } ================================================ FILE: templates/base.rs.xml ================================================ @(name: String, uuid: String, mac_address: String, zvol: String, sata: bool, memory: i32, cpus: i32, seed: String, qemu_path: String) @name @uuid @memory @memory @cpus hvm /run/libvirt/nix-ovmf/OVMF_CODE.fd destroy restart destroy @qemu_path @if sata { } else { } @if sata {
} else { } /dev/urandom ================================================ FILE: templates/base.xml ================================================ {{.Name}} {{.UUID}} {{.Memory}} {{.Memory}} 2 hvm destroy restart destroy /run/libvirt/nix-emulators/qemu-system-x86_64 {{if .SATA}} {{else}} {{end}} {{if .SATA}}
{{else}} {{end}} /dev/urandom ================================================ FILE: templates/meta-data ================================================ instance-id: {{.ID}} local-hostname: {{.Name}} ================================================ FILE: templates/templates.go ================================================ package templates import "embed" //go:embed meta-data *.xml var FS embed.FS ================================================ FILE: var/.gitignore ================================================ *.db* files *.pubkey *.privkey ================================================ FILE: var/base.yaml ================================================ #cloud-config #vim:syntax=yaml # See https://cloudinit.readthedocs.io/en/latest/topics/examples.html # for examples of what to put in here. users: - name: user groups: [ wheel ] sudo: [ "ALL=(ALL) NOPASSWD:ALL" ] shell: /bin/bash ssh-authorized-keys: - # get your authorized key from `ssh-add -L` ================================================ FILE: var/xe-base-windows.yaml ================================================ #cloud-config #vim:syntax=yaml users: - name: Xe passwd: "hunter2" primary_group: Administrators groups: [ Administrators, Users ] ssh_authorized_keys: - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPg9gYKVglnO2HQodSJt4z4mNrUSUiyJQ7b+J798bwD9 cadey@shachi ================================================ FILE: var/xe-base.nix ================================================ { config, pkgs, modulesPath, ... }: { imports = [ (modulesPath + "/profiles/qemu-guest.nix") ]; boot.initrd.availableKernelModules = [ "ata_piix" "uhci_hcd" "virtio_pci" "sr_mod" "virtio_blk" ]; boot.initrd.kernelModules = [ ]; boot.kernelModules = [ ]; boot.extraModulePackages = [ ]; users.users.xe = { isNormalUser = true; initialPassword = "hunter2"; extraGroups = [ "wheel" ]; openssh.authorizedKeys.keys = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPg9gYKVglnO2HQodSJt4z4mNrUSUiyJQ7b+J798bwD9" ]; }; services.openssh.enable = true; security.sudo.wheelNeedsPassword = false; services.cloud-init = { enable = true; ext4.enable = true; }; } ================================================ FILE: var/xe-base.yaml ================================================ #cloud-config #vim:syntax=yaml users: - name: root groups: [ wheel ] sudo: [ "ALL=(ALL) NOPASSWD:ALL" ] shell: /bin/sh ssh-authorized-keys: - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPg9gYKVglnO2HQodSJt4z4mNrUSUiyJQ7b+J798bwD9 - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPYr9hiLtDHgd6lZDgQMkJzvYeAXmePOrgFaWHAjJvNU