Showing preview only (252K chars total). Download the full file or copy to clipboard to get everything.
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 <me@xeiaso.net>" ]
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 <me@xeiaso.net>
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






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).
<big>THIS IS EXPERIMENTAL! USE IT AT YOUR OWN PERIL!</big>
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 = <input type="text" placeholder="crobat" />;
const memoryBox = <input type="text" placeholder="512" />;
const cpuBox = <input type="text" placeholder="2" />;
const host = (
<select>
{config.hosts.map(host => <option value={host}>{host}</option>)}
</select>
);
const disk_size_gb = <input type="text" value="25" />;
const zvol_prefix = <input type="text" value="rpool/local/vms" />;
const distro = (
<select id="selectBox">
{distros.map((d) => <option value={d.name}>{d.name}</option>)}
</select>
);
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 = (
<textarea rows="10" cols="40">{user_data_template}</textarea>
);
const join_tailnet = <input type="checkbox" checked="true" />;
const submit = <button>Create that sucker</button>;
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 (
<div>
<table>
<tr>
<th>Name</th>
<td>{nameBox}</td>
</tr>
<tr>
<th>Memory (MB)</th>
<td>{memoryBox}</td>
</tr>
<tr>
<th>CPU cores</th>
<td>{cpuBox}</td>
</tr>
<tr>
<th>Host</th>
<td>{host}</td>
</tr>
<tr>
<th>Disk size (GB)</th>
<td>{disk_size_gb}</td>
</tr>
<tr>
<th>ZVol prefix</th>
<td>{zvol_prefix}</td>
</tr>
<tr>
<th>Distro</th>
<td>{distro}</td>
</tr>
<tr>
<th>Userdata</th>
<td>{user_data}</td>
</tr>
<tr>
<th>Join tailnet + SSH?</th>
<td>{join_tailnet}</td>
</tr>
<tr>
<td>{""}</td>
</tr>
<tr>
<td>{submit}</td>
</tr>
</table>
</div>
);
};
================================================
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 (
<div>
<button onclick={() => onclick()}>{text}</button>
<br />
</div>
);
}
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 (
<div>
<button onclick={() => onclick()}>{text}</button>
<br />
</div>
);
}
export async function Page() {
const instance_id = g("instance_id").innerText;
const auditLogs = (await getAuditLogsForInstance(instance_id)).map((al) => (
<tr>
<td>{new Date(al.ts * 1000).toLocaleString()}</td>
<td>{al.op}</td>
</tr>
));
auditLogs.unshift(
<tr>
<th>Time</th>
<th>Operation</th>
</tr>,
);
return (
<div>
<InstanceButton
text="Reboot"
instance_id={instance_id}
action="reboot"
message="VM Rebooted."
/>
<InstanceButton
text="Hard Reboot"
instance_id={instance_id}
action="hardreboot"
message="VM hard-rebooted."
/>
<InstanceButton
text="Recreate VM"
instance_id={instance_id}
action="reinit"
message="Recreating VM from scratch."
confirm={true}
/>
<InstanceButton
text="Shutdown"
instance_id={instance_id}
action="shutdown"
message="VM shut down."
/>
<InstanceButton
text="Start"
instance_id={instance_id}
action="start"
message="VM Started."
/>
<DeleteInstanceButton
text="Delete instance"
instance_id={instance_id}
action="delete"
message="Instance deleted, redirecting you to instances page."
/>
<div>
<h3>Audit Logs</h3>
<table>
{auditLogs}
</table>
</div>
<div id="messages">
<h3>Messages</h3>
</div>
</div>
);
}
================================================
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<Config> => {
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<Distro[]> => {
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<AuditLog[]> => {
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<AuditLog[]> => {
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<Instance> => {
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<void> => {
await fetch(u(`/api/v1/instances/${id}`), {
method: "DELETE",
});
}
const doThingToInstance = (action: string): (id: string) => Promise<void> => {
return (async (id: string): Promise<void> => {
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 <me@xeiaso.net>" ]
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<String> {
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 <me@xeiaso.net>" ]
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<T = (), E = Error> = std::result::Result<T, E>;
/// 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<String>,
pub created: DateTime<Utc>,
pub expires: DateTime<Utc>,
/// 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<String>,
}
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<Self> {
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<Vec<Key>> {
#[derive(Debug, Clone, Deserialize, Serialize)]
struct Wrapper {
keys: Vec<Key>,
}
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<KeyInfo> {
#[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<KeyInfo> {
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 <me@xeiaso.net>" ]
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<WhoisResponse, Error> {
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<Option<serde_json::Value>>,
}
#[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<String>,
#[serde(rename = "AllowedIPs")]
pub allowed_ips: Vec<String>,
#[serde(rename = "Endpoints")]
pub endpoints: Vec<String>,
#[serde(rename = "Hostinfo")]
pub hostinfo: Hostinfo,
#[serde(rename = "Created")]
pub created: String,
#[serde(rename = "PrimaryRoutes")]
pub primary_routes: Option<Vec<String>>,
#[serde(rename = "MachineAuthorized")]
pub machine_authorized: Option<bool>,
#[serde(rename = "Capabilities")]
pub capabilities: Option<Vec<String>>,
#[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<String>,
#[serde(rename = "Hostname")]
pub hostname: String,
#[serde(rename = "RoutableIPs")]
pub routable_ips: Option<Vec<String>>,
#[serde(rename = "Services")]
pub services: Vec<Service>,
}
#[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<String>,
}
================================================
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<String> {
PreEscaped(format!(
r#"<script type ="module">
import {{ Page }} from "/static/js/{name}";
const g = (name) => document.getElementById(name);
const r = (callback) => window.addEventListener("DOMContentLoaded", callback);
const x = (elem) => {{
while (elem.lastChild) {{
elem.removeChild(elem.lastChild);
}}
}};
r(async () => {{
const page = await Page();
const root = g("app");
x(root);
root.appendChild(page);
}});
</script>"#
))
}
pub async fn config(Extension(config): Extension<Arc<Config>>, _: Tailauth) -> Json<Config> {
Json((*config).clone())
}
pub fn base(
title: Option<String>,
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,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🔥</text></svg>";
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<Arc<State>>,
Tailauth(user, _): Tailauth,
Path(id): Path<Uuid>,
) -> Result<Markup> {
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<Machine> = 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<Arc<State>>,
Tailauth(user, _): Tailauth,
) -> Result<Markup> {
let conn = state.pool.get().await?;
let mut result: Vec<Instance> = 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<Arc<State>>,
Tailauth(user, _): Tailauth,
) -> Result<Markup> {
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<i32>) =
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<Arc<State>>,
Tailauth(user, _): Tailauth,
) -> Result<Markup> {
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<Distro> = 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<Markup> {
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<Arc<State>>,
_: Tailauth,
) -> Result<Json<Vec<AuditEvent>>> {
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<uuid::Uuid>,
Extension(state): Extension<Arc<State>>,
_: Tailauth,
) -> Result<Json<Vec<AuditEvent>>> {
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<Uuid>,
Extension(state): Extension<Arc<State>>,
) -> Result<String, Error> {
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<Uuid>,
Extension(state): Extension<Arc<State>>,
) -> Result<String, Error> {
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<Uuid>,
Extension(ts): Extension<Arc<tailscale_client::Client>>,
Extension(state): Extension<Arc<State>>,
) -> Result<String, Error> {
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<File>,
pub runcmd: Vec<Vec<String>>,
}
#[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<Arc<State>>,
_: Tailauth,
Json(distro): Json<Distro>,
) -> Result<Json<Distro>> {
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<String>,
Extension(state): Extension<Arc<State>>,
_: Tailauth,
Json(distro): Json<Distro>,
) -> Result<Json<Distro>> {
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<Arc<State>>,
Path(name): Path<String>,
_: 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<Arc<State>>,
Path(name): Path<String>,
_: Tailauth,
) -> Result<Json<Distro>> {
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<Arc<State>>,
_: Tailauth,
) -> Result<Json<Vec<Distro>>> {
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<Distro> = 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<Uuid>,
Extension(state): Extension<Arc<State>>,
_: 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<Uuid>,
Extension(state): Extension<Arc<State>>,
_: 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<Uuid>,
Extension(state): Extension<Arc<State>>,
_: Tailauth,
) -> Result<Json<Machine>, 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<Uuid>,
Extension(state): Extension<Arc<State>>,
_: 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<Uuid>,
Extension(state): Extension<Arc<State>>,
_: 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<Uuid>,
Extension(state): Extension<Arc<State>>,
_: 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<Uuid>,
Extension(state): Extension<Arc<State>>,
_: 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<String>,
Extension(state): Extension<Arc<State>>,
_: Tailauth,
) -> Result<Json<Instance>, 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<Uuid>,
Extension(state): Extension<Arc<State>>,
_: Tailauth,
) -> Result<Json<Instance>, 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<Arc<State>>,
_: Tailauth,
) -> Result<Json<Vec<Instance>>, Error> {
let conn = state.pool.get().await?;
let mut result: Vec<Instance> = 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<Arc<State>>,
Extension(config): Extension<Arc<Config>>,
_: Tailauth,
Json(details): Json<NewInstance>,
) -> Result<Json<Instance>, Error> {
let id = Uuid::new_v4();
let addrs: Vec<SocketAddr> = 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<Config>,
state: Arc<State>,
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<u8> = 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<String>,
pub memory_megs: u64,
pub cpus: u32,
}
impl TryFrom<Domain> for Machine {
type Error = Error;
fn try_from(dom: Domain) -> Result<Self, Self::Error> {
let addr: Option<String> = if dom.is_active()? {
let mut addr: Vec<String> = 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<Arc<Config>>) -> Result<Json<Vec<Machine>>> {
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<Vec<Machine>> {
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<String>,
#[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<String>,
/// 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<i32>,
/// 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<PathBuf>,
/// Distribution to use
#[clap(short, long)]
distro: String,
/// Automagically join the tailnet
#[clap(short, long)]
join_tailnet: bool,
}
impl TryInto<NewInstance> for CreateOpts {
type Error = anyhow::Error;
fn try_into(self) -> Result<NewInstance, anyhow::Error> {
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<Distro> 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<T>(cli: &Client, i: Instance, want: T) -> Result
where
T: Into<String>,
{
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<u8> = 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<u8> = 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<u8> = 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::<Config>()?;
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
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
SYMBOL INDEX (182 symbols across 36 files)
FILE: frontend/instance_detail.tsx
function Fragment (line 3) | function Fragment({ children }: { children: any[] }): any[] {
type InstanceButtonProps (line 18) | type InstanceButtonProps = {
function DeleteInstanceButton (line 26) | function DeleteInstanceButton(
function InstanceButton (line 52) | function InstanceButton(
function Page (line 92) | async function Page() {
FILE: frontend/waifud/mod.ts
type Config (line 3) | type Config = {
type Distro (line 23) | type Distro = {
type AuditLog (line 43) | type AuditLog = {
type NewInstance (line 77) | type NewInstance = {
type Instance (line 89) | type Instance = {
FILE: lib/rotbart/src/blaseball.rs
constant FIRST_NAMES (line 1) | pub const FIRST_NAMES: &'static [&'static str] = &[
constant LAST_NAMES (line 1092) | pub const LAST_NAMES: &'static [&'static str] = &[
FILE: lib/rotbart/src/elfs.rs
constant ADJECTIVES (line 1) | pub const ADJECTIVES: &'static [&'static str] = &[
constant NOUNS (line 26) | pub const NOUNS: &'static [&'static str] = &[
FILE: lib/rotbart/src/lib.rs
function unique_monster (line 35) | pub fn unique_monster() -> Option<String> {
FILE: lib/rotbart/src/mlp_fim.rs
constant PONIES (line 1) | pub const PONIES: &'static [&'static str] = &[
FILE: lib/rotbart/src/pokemon.rs
constant POKEDEX (line 1) | pub const POKEDEX: &'static [&'static str] = &[
FILE: lib/rotbart/src/xc1.rs
constant ADJECTIVES (line 30) | pub const ADJECTIVES: &[&str] = &[
constant NOUNS (line 575) | pub const NOUNS: &[&str] = &[
FILE: lib/rotbart/src/xc2.rs
constant ADJECTIVES (line 31) | pub const ADJECTIVES: &[&str] = &[
constant NOUNS (line 735) | pub const NOUNS: &[&str] = &[
constant COMMON_BLADES (line 955) | pub const COMMON_BLADES: &'static [&'static str] = &[
FILE: lib/tailscale_client/src/lib.rs
type Error (line 5) | pub enum Error {
type Result (line 13) | pub type Result<T = (), E = Error> = std::result::Result<T, E>;
type Client (line 16) | pub struct Client {
method new (line 74) | pub fn new(user_agent: String, api_key: String, tailnet: String) -> Re...
method list_keys (line 86) | pub async fn list_keys(&self) -> Result<Vec<Key>> {
method create_key (line 110) | pub async fn create_key(&self, caps: Capabilities) -> Result<KeyInfo> {
method key_info (line 148) | pub async fn key_info(&self, key_id: String) -> Result<KeyInfo> {
method delete_key (line 164) | pub async fn delete_key(&self, key_id: String) -> Result {
type Key (line 25) | pub struct Key {
type KeyInfo (line 31) | pub struct KeyInfo {
function test_keyinfo_full (line 42) | fn test_keyinfo_full() {
function test_keyinfo_partial (line 54) | fn test_keyinfo_partial() {
type Capabilities (line 65) | pub struct Capabilities {
FILE: lib/ts_localapi/src/lib.rs
type Error (line 7) | pub enum Error {
type WhoisResponse (line 23) | pub struct WhoisResponse {
function whois (line 30) | pub async fn whois(ip_port: SocketAddr) -> Result<WhoisResponse, Error> {
type User (line 65) | pub struct User {
type WhoisPeer (line 80) | pub struct WhoisPeer {
type Hostinfo (line 121) | pub struct Hostinfo {
type Service (line 134) | pub struct Service {
FILE: src/admin/mod.rs
function import_js (line 15) | fn import_js(name: &str) -> PreEscaped<String> {
function config (line 38) | pub async fn config(Extension(config): Extension<Arc<Config>>, _: Tailau...
function base (line 42) | pub fn base(
function instance_create (line 122) | pub async fn instance_create(Tailauth(user, _): Tailauth) -> Markup {
function instance (line 136) | pub async fn instance(
function instances (line 204) | pub async fn instances(
function home (line 264) | pub async fn home(
function distro_list (line 316) | pub async fn distro_list(
function test_handler (line 361) | pub async fn test_handler(Tailauth(user, _): Tailauth) -> Result<Markup> {
FILE: src/api/audit.rs
function list (line 9) | pub async fn list(
function list_for_instance (line 19) | pub async fn list_for_instance(
FILE: src/api/cloudinit.rs
function user_data (line 9) | pub async fn user_data(
function meta_data (line 23) | pub async fn meta_data(
function vendor_data (line 53) | pub async fn vendor_data(
type CloudConfig (line 122) | pub struct CloudConfig {
type File (line 129) | pub struct File {
FILE: src/api/distros.rs
function create (line 10) | pub async fn create(
function update (line 52) | pub async fn update(
function delete (line 99) | pub async fn delete(
function get (line 118) | pub async fn get(
function list (line 148) | pub async fn list(
FILE: src/api/instances.rs
function reinit (line 23) | pub async fn reinit(
function delete (line 97) | pub async fn delete(
function get_machine (line 147) | pub async fn get_machine(
function hard_reboot (line 165) | pub async fn hard_reboot(
function shutdown (line 194) | pub async fn shutdown(
function start (line 221) | pub async fn start(
function reboot (line 248) | pub async fn reboot(
function get_by_name (line 275) | pub async fn get_by_name(
function get (line 287) | pub async fn get(
function list (line 299) | pub async fn list(
function create (line 333) | pub async fn create(
function make_instance (line 443) | async fn make_instance(
FILE: src/api/libvirt.rs
type Machine (line 8) | pub struct Machine {
type Error (line 19) | type Error = Error;
method try_from (line 21) | fn try_from(dom: Domain) -> Result<Self, Self::Error> {
function get_machines (line 58) | pub async fn get_machines(Extension(cfg): Extension<Arc<Config>>) -> Res...
function list_all_vms (line 70) | fn list_all_vms(uri: &str, host: String) -> Result<Vec<Machine>> {
FILE: src/bin/unique-monster.rs
type Cli (line 6) | struct Cli {
function main (line 14) | fn main() {
FILE: src/bin/waifuctl.rs
type Opt (line 33) | struct Opt {
type Config (line 43) | struct Config {
type ConfigCmd (line 52) | enum ConfigCmd {
type Command (line 67) | enum Command {
type CreateOpts (line 124) | struct CreateOpts {
type Error (line 163) | type Error = anyhow::Error;
method try_into (line 165) | fn try_into(self) -> Result<NewInstance, anyhow::Error> {
type DistroCmd (line 188) | enum DistroCmd {
type CreateDistroOpts (line 207) | struct CreateDistroOpts {
method into (line 230) | fn into(self) -> Distro {
type UtilsCmd (line 242) | enum UtilsCmd {
function list_instances (line 252) | async fn list_instances(cli: Client) -> Result {
function wait_until_status (line 281) | async fn wait_until_status<T>(cli: &Client, i: Instance, want: T) -> Result
function start_instance (line 307) | async fn start_instance(cli: Client, name: String) -> Result {
function shutdown_instance (line 318) | async fn shutdown_instance(cli: Client, name: String) -> Result {
function reboot_instance (line 328) | async fn reboot_instance(cli: Client, name: String, hard: bool) -> Result {
function create_instance (line 343) | async fn create_instance(cli: Client, cfg: Config, opts: CreateOpts) -> ...
function delete_instance (line 368) | async fn delete_instance(cli: Client, name: String) -> Result {
function reinit_instance (line 382) | async fn reinit_instance(cli: Client, name: String) -> Result<()> {
function create_distro (line 389) | async fn create_distro(cli: Client, opts: CreateDistroOpts) -> Result {
function update_distro (line 397) | async fn update_distro(cli: Client, opts: CreateDistroOpts) -> Result {
function scrape_distros (line 410) | async fn scrape_distros(cli: Client) -> Result {
function list_distros (line 420) | async fn list_distros(cli: Client, verbose: bool) -> Result {
function delete_distro (line 448) | async fn delete_distro(cli: Client, name: String) -> Result<()> {
function audit_list (line 453) | async fn audit_list(cli: Client, json: bool) -> Result<()> {
function config_show (line 479) | fn config_show(cfg: Config) -> Result {
function config_set_host (line 486) | fn config_set_host(cfg: Config, url: String) -> Result {
function config_set_userdata (line 507) | fn config_set_userdata(cfg: Config) -> Result {
function utils_completions (line 529) | fn utils_completions(shell: Shell) -> Result {
function utils_gen_manpage (line 543) | fn utils_gen_manpage(path: PathBuf) -> Result {
function main (line 584) | async fn main() -> Result<()> {
FILE: src/build.rs
function main (line 3) | fn main() -> Result<()> {
FILE: src/client/mod.rs
type Client (line 12) | pub struct Client {
method new (line 18) | pub fn new(base_url: String) -> Result<Self> {
method audit_logs (line 36) | pub async fn audit_logs(&self) -> Result<Vec<AuditEvent>> {
method create_instance (line 49) | pub async fn create_instance(&self, ni: NewInstance) -> Result<Instanc...
method delete_instance (line 63) | pub async fn delete_instance(&self, id: Uuid) -> Result {
method reinit_instance (line 70) | pub async fn reinit_instance(&self, id: Uuid) -> Result {
method list_instances (line 77) | pub async fn list_instances(&self) -> Result<Vec<Instance>> {
method get_instance (line 90) | pub async fn get_instance(&self, id: Uuid) -> Result<Instance> {
method get_instance_by_name (line 103) | pub async fn get_instance_by_name(&self, name: String) -> Result<Insta...
method get_instance_machine (line 116) | pub async fn get_instance_machine(&self, id: Uuid) -> Result<Machine> {
method shutdown_instance (line 129) | pub async fn shutdown_instance(&self, id: Uuid) -> Result<()> {
method start_instance (line 136) | pub async fn start_instance(&self, id: Uuid) -> Result<()> {
method hard_reboot_instance (line 143) | pub async fn hard_reboot_instance(&self, id: Uuid) -> Result<()> {
method reboot_instance (line 150) | pub async fn reboot_instance(&self, id: Uuid) -> Result<()> {
method create_distro (line 157) | pub async fn create_distro(&self, d: Distro) -> Result<Distro> {
method list_distros (line 171) | pub async fn list_distros(&self) -> Result<Vec<Distro>> {
method update_distro (line 184) | pub async fn update_distro(&self, d: Distro) -> Result<Distro> {
method get_distro (line 198) | pub async fn get_distro(&self, name: String) -> Result<Distro> {
method delete_distro (line 211) | pub async fn delete_distro(&self, name: String) -> Result {
FILE: src/config.rs
type Config (line 5) | pub struct Config {
method fmt (line 21) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
type Tailscale (line 27) | pub struct Tailscale {
FILE: src/lib.rs
constant APPLICATION_NAME (line 14) | pub const APPLICATION_NAME: &str = concat!(env!("CARGO_PKG_NAME"), "/", ...
function establish_connection (line 16) | pub fn establish_connection() -> Result<Connection> {
type Result (line 21) | pub type Result<T = (), E = Error> = std::result::Result<T, E>;
type State (line 35) | pub struct State {
method fmt (line 40) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
method new (line 46) | pub async fn new() -> Result<Self> {
type Error (line 56) | pub enum Error {
method from (line 153) | fn from(err: bb8::RunError<E>) -> Self {
method into (line 166) | fn into(self) -> (StatusCode, String) {
method into_response (line 159) | fn into_response(self) -> Response {
FILE: src/libvirt.rs
type NewInstance (line 6) | pub struct NewInstance {
function random_mac (line 19) | pub fn random_mac() -> String {
FILE: src/main.rs
function main (line 19) | async fn main() -> Result {
FILE: src/migrate/20220225-session.sql
type sessions (line 1) | CREATE TABLE IF NOT EXISTS sessions
FILE: src/migrate/base_schema.sql
type instances (line 1) | CREATE TABLE IF NOT EXISTS instances
type audit_logs (line 14) | CREATE TABLE IF NOT EXISTS audit_logs
type audit_logs_uuid (line 24) | CREATE INDEX IF NOT EXISTS audit_logs_uuid
type audit_logs_name (line 27) | CREATE INDEX IF NOT EXISTS audit_logs_name
type cloudconfig_seeds (line 30) | CREATE TABLE IF NOT EXISTS cloudconfig_seeds
type distros (line 35) | CREATE TABLE IF NOT EXISTS distros
FILE: src/migrate/mod.rs
function run (line 6) | pub fn run() -> Result<()> {
FILE: src/models.rs
type Instance (line 10) | pub struct Instance {
method from_name (line 24) | pub fn from_name(
method from_uuid (line 49) | pub fn from_uuid(
type CloudconfigSeed (line 75) | pub struct CloudconfigSeed {
type Distro (line 81) | pub struct Distro {
method from_name (line 93) | pub fn from_name(
type AuditEvent (line 121) | pub struct AuditEvent {
method get_for_instance (line 132) | pub fn get_for_instance(
method get_all (line 157) | pub fn get_all(
type Session (line 181) | pub struct Session {
method get (line 189) | pub fn get(
FILE: src/scrape/amazon_linux.rs
function scrape (line 10) | pub async fn scrape() -> crate::Result<crate::models::Distro> {
FILE: src/scrape/arch.rs
constant RELEASE_BASE (line 10) | const RELEASE_BASE: &'static str = "https://geo.mirror.pkgbuild.com/imag...
function scrape (line 12) | pub async fn scrape() -> crate::Result<crate::models::Distro> {
FILE: src/scrape/mod.rs
function get_all (line 14) | pub async fn get_all() -> Result<Vec<Distro>> {
function cron (line 38) | pub async fn cron() {
FILE: src/scrape/nixos.rs
type Metadata (line 6) | pub struct Metadata {
function scrape (line 11) | pub async fn scrape() -> crate::Result<Vec<Distro>> {
FILE: src/scrape/rocky_linux.rs
constant RELEASE_BASE (line 10) | const RELEASE_BASE: &'static str = "http://download.rockylinux.org/pub/r...
function scrape (line 13) | pub async fn scrape(version: i32) -> crate::Result<crate::models::Distro> {
FILE: src/scrape/ubuntu.rs
constant RELEASE_BASE (line 11) | const RELEASE_BASE: &'static str = "http://cloud-images.ubuntu.com/daily...
function scrape (line 13) | pub async fn scrape((version, name): (&str, &str)) -> crate::Result<crat...
FILE: src/tailauth.rs
type Tailauth (line 5) | pub struct Tailauth(pub ts_localapi::User, pub ts_localapi::WhoisPeer);
type Rejection (line 12) | type Rejection = Error;
method from_request_parts (line 14) | async fn from_request_parts(req: &mut Parts, _state: &S) -> Result<Sel...
Condensed preview — 79 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (268K chars).
[
{
"path": ".envrc",
"chars": 10,
"preview": "use flake\n"
},
{
"path": ".github/dependabot.yml",
"chars": 537,
"preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
},
{
"path": ".gitignore",
"chars": 81,
"preview": "*.qcow2\nvar/*.xml\nvar/*.1\nconfig.dhall\nresult\n.direnv\n\n# Added by cargo\n\n/target\n"
},
{
"path": "Cargo.toml",
"chars": 2007,
"preview": "[package]\nname = \"waifud\"\nversion = \"0.1.0\"\nedition = \"2021\"\nauthors = [ \"Xe Iaso <me@xeiaso.net>\" ]\nbuild = \"src/build."
},
{
"path": "LICENSE",
"chars": 1067,
"preview": "Copyright (c) 2022 Xe Iaso <me@xeiaso.net>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
},
{
"path": "README.md",
"chars": 2292,
"preview": "# waifud\n\n\n\nDENO_FLAGS='--import-map=./import_map.js"
},
{
"path": "frontend/css/build.sh",
"chars": 131,
"preview": "#!/usr/bin/env nix-shell\n#! nix-shell -p nodePackages.clean-css-cli -i bash\n\ncleancss -o ./xess.css ./src/xess.css ./src"
},
{
"path": "frontend/css/src/admin.css",
"chars": 320,
"preview": ".breadcrumb {\n padding: 0 .5rem;\n}\n\n.breadcrumb ul {\n display: flex;\n flex-wrap: wrap;\n list-style: none;\n "
},
{
"path": "frontend/css/src/xess.css",
"chars": 1255,
"preview": "@import url(\"https://cdn.xeiaso.net/static/css/iosevka/family.css\");\n\nmain {\n font-family: Iosevka Aile Iaso, sans-seri"
},
{
"path": "frontend/css/xess.css",
"chars": 1132,
"preview": "@import url(https://cdn.xeiaso.net/static/css/iosevka/family.css);main{font-family:Iosevka Aile Iaso,sans-serif;max-widt"
},
{
"path": "frontend/deno.json",
"chars": 138,
"preview": "{\n \"compilerOptions\": {\n \"jsx\": \"react-jsx\",\n \"jsxImportSource\": \"xeact\",\n },\n \"importMap\": \"./im"
},
{
"path": "frontend/deps.ts",
"chars": 68,
"preview": "import * as xeact from \"xeact\";\n\nexport {\n xeact,\n //xterm\n};\n"
},
{
"path": "frontend/import_map.json",
"chars": 239,
"preview": "{\n \"imports\": {\n \"xeact\": \"https://xena.greedo.xeserv.us/pkg/xeact/v0.69.71/xeact.ts\",\n \"xeact/jsx-runt"
},
{
"path": "frontend/instance_create.tsx",
"chars": 3455,
"preview": "/** @jsxImportSource xeact */\n\nimport { u } from \"xeact\";\nimport {\n getConfig,\n getDistros,\n makeInstance,\n NewInsta"
},
{
"path": "frontend/instance_detail.tsx",
"chars": 3663,
"preview": "/** @jsxImportSource xeact */\n\nexport function Fragment({ children }: { children: any[] }): any[] {\n return children;\n}"
},
{
"path": "frontend/static/js/.gitignore",
"chars": 5,
"preview": "*.js\n"
},
{
"path": "frontend/waifud/mod.ts",
"chars": 3794,
"preview": "import { u } from \"xeact\";\n\nexport type Config = { \n base_url: string,\n hosts: string[],\n bind_host: string,\n"
},
{
"path": "lib/rotbart/Cargo.toml",
"chars": 309,
"preview": "[package]\nname = \"rotbart\"\nversion = \"0.1.0\"\nedition = \"2021\"\nauthors = [ \"Xe Iaso <me@xeiaso.net>\" ]\nrepository = \"http"
},
{
"path": "lib/rotbart/scrapers/README.md",
"chars": 1576,
"preview": "# How to generate names.json\n\nOpen https://xenoblade.github.io/xb2/bdat/common/BLD_NameList.html and paste\nthis into the"
},
{
"path": "lib/rotbart/scrapers/blaseball.sh",
"chars": 353,
"preview": "#!/usr/bin/env nix-shell\n#! nix-shell -p jq -p curl -i bash\n\ncurl 'https://api.sibr.dev/chronicler/v2/entities?type=play"
},
{
"path": "lib/rotbart/scrapers/pokedex-hisui.sh",
"chars": 268,
"preview": "#!/usr/bin/env nix-shell\n#! nix-shell -p jq -p curl -i bash\n\ncat pokedex-hisui.json \\\n | jq -r '.[].name' \\\n | tr "
},
{
"path": "lib/rotbart/scrapers/pokedex.sh",
"chars": 327,
"preview": "#!/usr/bin/env nix-shell\n#! nix-shell -p jq -p curl -i bash\n\ncurl https://raw.githubusercontent.com/fanzeyi/pokemon.json"
},
{
"path": "lib/rotbart/src/blaseball.rs",
"chars": 31257,
"preview": "pub const FIRST_NAMES: &'static [&'static str] = &[\n \"abbie\",\n \"abbott\",\n \"abner\",\n \"acosta\",\n \"adalberto"
},
{
"path": "lib/rotbart/src/elfs.rs",
"chars": 4284,
"preview": "pub const ADJECTIVES: &'static [&'static str] = &[\n \"able\", \"abnorma\", \"again\", \"airexpl\", \"ang\", \"anger\", \"asail\", \""
},
{
"path": "lib/rotbart/src/lib.rs",
"chars": 1098,
"preview": "mod blaseball;\nmod elfs;\nmod mlp_fim;\nmod pokemon;\nmod xc1;\nmod xc2;\n\nlazy_static::lazy_static! {\n pub static ref COM"
},
{
"path": "lib/rotbart/src/mlp_fim.rs",
"chars": 6414,
"preview": "pub const PONIES: &'static [&'static str] = &[\n // earth ponies\n \"applejack\",\n \"pinkie-pie\",\n \"aloe\",\n \"b"
},
{
"path": "lib/rotbart/src/pokemon.rs",
"chars": 12679,
"preview": "pub const POKEDEX: &'static [&'static str] = &[\n \"abomasnow\",\n \"abra\",\n \"absol\",\n \"accelgor\",\n \"aegislash"
},
{
"path": "lib/rotbart/src/xc1.rs",
"chars": 12755,
"preview": "/*!\nhttps://xenoblade.github.io/xb1/bdat/bdat_common/BTL_enelist.html\n\n```javascript\nonlyUnique = (value, index, self) ="
},
{
"path": "lib/rotbart/src/xc2.rs",
"chars": 18151,
"preview": "/*!\nhttps://xenoblade.github.io/xb2/bdat/common/BTL_EnBook.html\n\n\n```javascript\nonlyUnique = (value, index, self) => sel"
},
{
"path": "lib/tailscale_client/Cargo.toml",
"chars": 502,
"preview": "[package]\nname = \"tailscale_client\"\nversion = \"0.1.0\"\nedition = \"2021\"\nauthors = [ \"Xe Iaso <me@xeiaso.net>\" ]\nrepositor"
},
{
"path": "lib/tailscale_client/src/lib.rs",
"chars": 4901,
"preview": "use chrono::prelude::*;\nuse serde::{Deserialize, Serialize};\n\n#[derive(thiserror::Error, Debug)]\npub enum Error {\n #["
},
{
"path": "lib/ts_localapi/Cargo.toml",
"chars": 299,
"preview": "[package]\nname = \"ts_localapi\"\nversion = \"0.1.0\"\nedition = \"2021\"\nauthors = [ \"Xe Iaso <me@xeiaso.net>\" ]\nrepository = \""
},
{
"path": "lib/ts_localapi/src/lib.rs",
"chars": 4163,
"preview": "use hyper::{body::Buf, Body, Client, Request, StatusCode};\nuse hyperlocal::{UnixClientExt, Uri};\nuse serde::{Deserialize"
},
{
"path": "scripts/.gitignore",
"chars": 9,
"preview": "*.qcow2*\n"
},
{
"path": "scripts/metadata.json",
"chars": 142,
"preview": "{\"unstable\":{\"fname\":\"nixos-unstable-within-202307091255.qcow2\",\"sha256\":\"858f149120dc86d8bb1696b3d62f03c597a9706082709b"
},
{
"path": "scripts/mk-nixos-image.sh",
"chars": 1418,
"preview": "#!/usr/bin/env nix-shell\n#! nix-shell -i bash -p nixos-generators -p qemu -p rsync -p jo\n\nset -ex\n\nDATE=\"$(date +%Y%m%d%"
},
{
"path": "scripts/nixos-image.nix",
"chars": 2088,
"preview": "{ lib, pkgs, ... }:\n\n{\n boot.initrd.availableKernelModules =\n [ \"ata_piix\" \"uhci_hcd\" \"virtio_pci\" \"sr_mod\" \"virtio_"
},
{
"path": "shell.nix",
"chars": 238,
"preview": "(import (\n fetchTarball {\n url = \"https://github.com/edolstra/flake-compat/archive/99f1c2157fba4bfe6211a321fd0ee4319"
},
{
"path": "src/admin/mod.rs",
"chars": 12045,
"preview": "use crate::{\n api::libvirt::Machine,\n models::{Distro, Instance},\n tailauth::Tailauth,\n Config, Result, Stat"
},
{
"path": "src/api/audit.rs",
"chars": 675,
"preview": "use crate::{models::AuditEvent, tailauth::Tailauth, Result, State};\nuse axum::{\n extract::{Extension, Path},\n Json"
},
{
"path": "src/api/cloudinit.rs",
"chars": 4707,
"preview": "use crate::{models::Instance, Error, State};\nuse axum::extract::{Extension, Path};\nuse rusqlite::params;\nuse serde::{Des"
},
{
"path": "src/api/distros.rs",
"chars": 4079,
"preview": "use crate::{models::Distro, tailauth::Tailauth, Result, State};\nuse axum::{\n extract::{Extension, Path},\n Json,\n};"
},
{
"path": "src/api/instances.rs",
"chars": 18343,
"preview": "use crate::{\n api::libvirt::Machine,\n libvirt::{random_mac, NewInstance},\n models::{Distro, Instance},\n tail"
},
{
"path": "src/api/libvirt.rs",
"chars": 2330,
"preview": "use crate::{Config, Error, Result};\nuse axum::{extract::Extension, Json};\nuse serde::{Deserialize, Serialize};\nuse std::"
},
{
"path": "src/api/mod.rs",
"chars": 87,
"preview": "pub mod audit;\npub mod cloudinit;\npub mod distros;\npub mod instances;\npub mod libvirt;\n"
},
{
"path": "src/api/vendor-data",
"chars": 220,
"preview": "#cloud-config\nwrite_files:\n- owner: root:root\n path: /etc/update-motd.d/69-waifud\n permissions: '0755'\n content: |\n "
},
{
"path": "src/bin/unique-monster.rs",
"chars": 585,
"preview": "use clap::Parser;\nuse names::Name;\n\n#[derive(Parser)]\n#[clap(author, version, about, long_about = None)]\nstruct Cli {\n "
},
{
"path": "src/bin/waifuctl.rs",
"chars": 17305,
"preview": "#![deny(missing_docs)]\n\n//! waifuctl lets you manage VM instances on waifud.\n\n#[macro_use]\nextern crate tracing;\n\nuse ch"
},
{
"path": "src/build.rs",
"chars": 111,
"preview": "use ructe::{Result, Ructe};\n\nfn main() -> Result<()> {\n Ructe::from_env()?.compile_templates(\"templates\")\n}\n"
},
{
"path": "src/client/mod.rs",
"chars": 6025,
"preview": "use crate::{\n api::libvirt::Machine,\n libvirt::NewInstance,\n models::{AuditEvent, Distro, Instance},\n Result"
},
{
"path": "src/config.rs",
"chars": 769,
"preview": "use serde::{Deserialize, Serialize};\nuse std::{fmt, net::IpAddr};\n\n#[derive(Clone, Serialize, Deserialize)]\npub struct C"
},
{
"path": "src/lib.rs",
"chars": 5037,
"preview": "#[macro_use]\nextern crate tracing;\n\nuse axum::{\n http::StatusCode,\n response::{IntoResponse, Response},\n};\nuse bb8"
},
{
"path": "src/libvirt.rs",
"chars": 620,
"preview": "use mac_address::MacAddress;\nuse rand::Rng;\nuse serde::{Deserialize, Serialize};\n\n#[derive(Deserialize, Serialize, Clone"
},
{
"path": "src/main.rs",
"chars": 3380,
"preview": "#[macro_use]\nextern crate tracing;\n\nuse axum::{\n routing::{delete, get, post},\n Extension, Router,\n};\nuse axum_ext"
},
{
"path": "src/migrate/20220225-session.sql",
"chars": 142,
"preview": "CREATE TABLE IF NOT EXISTS sessions\n ( uuid TEXT NOT NULL PRIMARY KEY\n , user TEXT NOT NULL\n , expired BOOLEAN NOT NU"
},
{
"path": "src/migrate/20220814-no-session.sql",
"chars": 21,
"preview": "DROP TABLE sessions;\n"
},
{
"path": "src/migrate/base_schema.sql",
"chars": 1130,
"preview": "CREATE TABLE IF NOT EXISTS instances\n ( uuid TEXT PRIMARY KEY NOT NULL\n , name TEXT NOT NULL UNIQUE\n , host TEXT NOT "
},
{
"path": "src/migrate/mod.rs",
"chars": 540,
"preview": "use crate::establish_connection;\nuse anyhow::Result;\nuse rusqlite_migration::{Migrations, M};\n\n#[instrument(err)]\npub fn"
},
{
"path": "src/models.rs",
"chars": 5773,
"preview": "use bb8::PooledConnection;\nuse bb8_rusqlite::RusqliteConnectionManager;\nuse rusqlite::params;\nuse serde::{Deserialize, S"
},
{
"path": "src/scrape/amazon_linux.rs",
"chars": 2144,
"preview": "use crate::{models::Distro, Error};\nuse scraper::Html;\nuse url::Url;\n\n/// # Scraper for Amazon Linux\n///\n/// This scrape"
},
{
"path": "src/scrape/arch.rs",
"chars": 2295,
"preview": "use scraper::Html;\n\nuse crate::models::Distro;\n\n/// # Scraper for https://geo.mirror.pkgbuild.com/images/\n///\n/// This s"
},
{
"path": "src/scrape/mod.rs",
"chars": 2156,
"preview": "use std::time::Duration;\n\nuse crate::{models::Distro, Result};\nuse futures::future::join_all;\nuse rusqlite::params;\nuse "
},
{
"path": "src/scrape/nixos.rs",
"chars": 869,
"preview": "use crate::models::Distro;\nuse serde::{Deserialize, Serialize};\nuse std::collections::HashMap;\n\n#[derive(Deserialize, Se"
},
{
"path": "src/scrape/rocky_linux.rs",
"chars": 3085,
"preview": "use scraper::{ElementRef, Html};\n\nuse crate::models::Distro;\n\n/// # Scraper for Rocky Linux cloud images\n///\n/// This sc"
},
{
"path": "src/scrape/ubuntu.rs",
"chars": 3032,
"preview": "use scraper::{ElementRef, Html};\nuse std::collections::HashMap;\n\nuse crate::{models::Distro, Error};\n\n/// # Scraper for "
},
{
"path": "src/tailauth.rs",
"chars": 950,
"preview": "use crate::Error;\nuse async_trait::async_trait;\nuse axum::{extract::FromRequestParts, http::request::Parts, RequestParts"
},
{
"path": "templates/base.rs.xml",
"chars": 2482,
"preview": "@(name: String, uuid: String, mac_address: String, zvol: String, sata: bool, memory: i32, cpus: i32, seed: String, qemu_"
},
{
"path": "templates/base.xml",
"chars": 2669,
"preview": "<domain type=\"kvm\" xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>\n <name>{{.Name}}</name>\n <uuid>{{.UUID}}</"
},
{
"path": "templates/meta-data",
"chars": 47,
"preview": "instance-id: {{.ID}}\nlocal-hostname: {{.Name}}\n"
},
{
"path": "templates/templates.go",
"chars": 78,
"preview": "package templates\n\nimport \"embed\"\n\n//go:embed meta-data *.xml\nvar FS embed.FS\n"
},
{
"path": "var/.gitignore",
"chars": 31,
"preview": "*.db*\nfiles\n*.pubkey\n*.privkey\n"
},
{
"path": "var/base.yaml",
"chars": 323,
"preview": "#cloud-config\n#vim:syntax=yaml\n\n# See https://cloudinit.readthedocs.io/en/latest/topics/examples.html\n# for examples of "
},
{
"path": "var/xe-base-windows.yaml",
"chars": 273,
"preview": "#cloud-config\n#vim:syntax=yaml\n\nusers:\n - name: Xe\n passwd: \"hunter2\"\n primary_group: Administrators\n groups: "
},
{
"path": "var/xe-base.nix",
"chars": 706,
"preview": "{ config, pkgs, modulesPath, ... }:\n\n{\n imports = [ (modulesPath + \"/profiles/qemu-guest.nix\") ];\n\n boot.initrd.availa"
},
{
"path": "var/xe-base.yaml",
"chars": 337,
"preview": "#cloud-config\n#vim:syntax=yaml\n\nusers:\n - name: root\n groups: [ wheel ]\n sudo: [ \"ALL=(ALL) NOPASSWD:ALL\" ]\n s"
}
]
About this extraction
This page contains the full source code of the Xe/waifud GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 79 files (232.9 KB), approximately 73.1k tokens, and a symbol index with 182 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.