aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCollin Williams <96917990+bluedragon1221@users.noreply.github.com>2026-03-03 16:45:26 -0600
committerCollin Williams <96917990+bluedragon1221@users.noreply.github.com>2026-03-03 16:45:26 -0600
commitc29a14135736c2516bb5d951e6e97dc8143598db (patch)
tree925628abfe934e3ea2cfd4a70357f8f9b5acb474
parentce2cc260710f801c10302847796b931f7ed19709 (diff)
changes
-rw-r--r--flake.lock98
-rw-r--r--flake.nix53
-rw-r--r--hosts/ganymede/nixos.nix26
-rw-r--r--hosts/mercury/hjem.nix6
-rw-r--r--hosts/mercury/nixos.nix2
-rw-r--r--lib/nix-furnace/mkSystem.nix1
-rw-r--r--modules/desktop/hjem/wm/sway.nix1
-rw-r--r--modules/desktop/options.nix1
-rw-r--r--modules/services/nixos/cgit/default.nix4
-rw-r--r--modules/services/nixos/goaccess.nix4
-rw-r--r--modules/services/nixos/qbittorrent.nix7
-rw-r--r--modules/services/options.nix2
-rw-r--r--modules/system/nixos/networking/default.nix2
-rw-r--r--modules/wireguard/nixos/default.nix50
-rw-r--r--modules/wireguard/options.nix41
-rw-r--r--pkgs/util/default.nix2
-rw-r--r--pkgs/yo.nix93
-rw-r--r--pkgs/yo/README.md2
-rw-r--r--pkgs/yo/default.nix59
-rw-r--r--pkgs/yo/nixos.rb37
-rw-r--r--pkgs/yossh/README.md129
-rw-r--r--pkgs/yossh/default.nix (renamed from pkgs/yoshi.nix)2
-rw-r--r--pkgs/yossh/doc/lua_doc.md118
-rw-r--r--pkgs/yossh/doc/nix.md44
-rwxr-xr-xpkgs/yossh/examples/home_wifi.lua33
-rw-r--r--pkgs/yossh/examples/mega_bastion.lua61
-rw-r--r--pkgs/yossh/examples/test.lua18
-rw-r--r--pkgs/yossh/tests/unit_tests.lua190
-rw-r--r--pkgs/yossh/yoshi.lua276
29 files changed, 1026 insertions, 336 deletions
diff --git a/flake.lock b/flake.lock
index 65286c2..f94d240 100644
--- a/flake.lock
+++ b/flake.lock
@@ -97,28 +97,6 @@
"type": "github"
}
},
- "deploy-rs": {
- "inputs": {
- "flake-compat": "flake-compat",
- "nixpkgs": [
- "nixpkgs"
- ],
- "utils": "utils"
- },
- "locked": {
- "lastModified": 1766051518,
- "narHash": "sha256-znKOwPXQnt3o7lDb3hdf19oDo0BLP4MfBOYiWkEHoik=",
- "owner": "serokell",
- "repo": "deploy-rs",
- "rev": "d5eff7f948535b9c723d60cd8239f8f11ddc90fa",
- "type": "github"
- },
- "original": {
- "owner": "serokell",
- "repo": "deploy-rs",
- "type": "github"
- }
- },
"disko": {
"inputs": {
"nixpkgs": [
@@ -158,22 +136,6 @@
"flake-compat": {
"flake": false,
"locked": {
- "lastModified": 1733328505,
- "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
- "owner": "edolstra",
- "repo": "flake-compat",
- "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
- "type": "github"
- },
- "original": {
- "owner": "edolstra",
- "repo": "flake-compat",
- "type": "github"
- }
- },
- "flake-compat_2": {
- "flake": false,
- "locked": {
"lastModified": 1767039857,
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
"owner": "NixOS",
@@ -393,24 +355,9 @@
"type": "github"
}
},
- "nmd": {
- "locked": {
- "lastModified": 1759339018,
- "narHash": "sha256-13x2gvgnnr3cJ5qp7zz7ZnUSrg0DF/sU9KiwZkML6J0=",
- "owner": "gvolpe",
- "repo": "nmd",
- "rev": "5ecbe493e22de649e79c8e51dc5e92659940e081",
- "type": "github"
- },
- "original": {
- "owner": "gvolpe",
- "repo": "nmd",
- "type": "github"
- }
- },
"pre-commit": {
"inputs": {
- "flake-compat": "flake-compat_2",
+ "flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": [
"lanzaboote",
@@ -436,7 +383,6 @@
"agenix": "agenix",
"betterfox": "betterfox",
"copyparty": "copyparty",
- "deploy-rs": "deploy-rs",
"disko": "disko",
"firefox-csshacks": "firefox-csshacks",
"glide-browser": "glide-browser",
@@ -444,7 +390,6 @@
"lanzaboote": "lanzaboote",
"nixos-facter-modules": "nixos-facter-modules",
"nixpkgs": "nixpkgs",
- "nmd": "nmd",
"tmux-tsunami": "tmux-tsunami",
"vermilion": "vermilion",
"yoshi-lua": "yoshi-lua"
@@ -500,7 +445,7 @@
"nixpkgs"
],
"rust-overlay": "rust-overlay",
- "systems": "systems_3"
+ "systems": "systems_2"
},
"locked": {
"lastModified": 1763430012,
@@ -546,21 +491,6 @@
"type": "github"
}
},
- "systems_3": {
- "locked": {
- "lastModified": 1681028828,
- "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
- "owner": "nix-systems",
- "repo": "default",
- "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
- "type": "github"
- },
- "original": {
- "owner": "nix-systems",
- "repo": "default",
- "type": "github"
- }
- },
"tmux-tsunami": {
"inputs": {
"nix-std": "nix-std",
@@ -582,24 +512,6 @@
"type": "github"
}
},
- "utils": {
- "inputs": {
- "systems": "systems_2"
- },
- "locked": {
- "lastModified": 1731533236,
- "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
- "owner": "numtide",
- "repo": "flake-utils",
- "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
- "type": "github"
- },
- "original": {
- "owner": "numtide",
- "repo": "flake-utils",
- "type": "github"
- }
- },
"vermilion": {
"inputs": {
"nixpkgs": "nixpkgs_2"
@@ -621,11 +533,11 @@
"yoshi-lua": {
"flake": false,
"locked": {
- "lastModified": 1770052758,
- "narHash": "sha256-oq1im7UCFcgWJCYPjA/drqfunu905lt3JRxTJWnWI94=",
+ "lastModified": 1772547188,
+ "narHash": "sha256-vmuc/G4tF6WMHUMlHYmwctPRjT+1u15aO0W6xwPALX8=",
"owner": "bluedragon1221",
"repo": "yoshi",
- "rev": "e6e2cda0fceb8c30f2662cc961074e7191730bc7",
+ "rev": "fa789d321c1fb4f97755bc199f1666836a87bdcd",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
index 5cb45aa..0fb3545 100644
--- a/flake.nix
+++ b/flake.nix
@@ -1,10 +1,6 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
- deploy-rs = {
- url = "github:serokell/deploy-rs";
- inputs.nixpkgs.follows = "nixpkgs";
- };
agenix = {
url = "github:ryantm/agenix";
@@ -60,30 +56,14 @@
url = "github:yokoffing/Betterfox";
flake = false;
};
-
- nmd.url = "github:gvolpe/nmd";
};
outputs = inputs: let
inherit (import ./lib/nix-furnace/mkSystem.nix) mkNixosSystem genDocs;
- system = "x86_64-linux";
-
- buildPkgs = import inputs.nixpkgs {inherit system;};
-
- deployPkgs = import inputs.nixpkgs {
- inherit system;
- overlays = [
- inputs.deploy-rs.overlays.default
- (self: super: {
- deploy-rs = {
- inherit (buildPkgs) deploy-rs;
- lib = super.deploy-rs.lib;
- };
- })
- ];
- };
- in rec {
+ buildSystem = "x86_64-linux";
+ buildPkgs = import inputs.nixpkgs {system = buildSystem;};
+ in {
nixosConfigurations."mercury" = mkNixosSystem {
inherit inputs;
hostname = "mercury";
@@ -99,28 +79,17 @@
hostname = "ganymede";
username = "collin";
};
- nixosConfigurations."gliese" = mkNixosSystem {
- inherit inputs;
- hostname = "gliese";
- username = "green";
- };
- packages."x86_64-linux".docs = buildPkgs.callPackage genDocs {
- inherit inputs;
- pkgs = buildPkgs;
- hostname = "mercury";
- };
-
- deploy.nodes."ganymede" = {
- hostname = "ganymede";
- sshUser = "root";
-
- profiles.system = {
- user = "root";
- path = deployPkgs.deploy-rs.lib.activate.nixos nixosConfigurations."ganymede";
+ packages.${buildSystem} = {
+ "docs" = buildPkgs.callPackage genDocs {
+ pkgs = buildPkgs;
+ hostname = "mercury";
+ };
+ default = buildPkgs.callPackage ./pkgs/yo.nix {
+ pkgs = buildPkgs;
};
};
- checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks deploy) inputs.deploy-rs.lib;
+ projects."nixos-rb" = ./pkgs/yo/nixos.rb;
};
}
diff --git a/hosts/ganymede/nixos.nix b/hosts/ganymede/nixos.nix
index 5ceb4e6..14be488 100644
--- a/hosts/ganymede/nixos.nix
+++ b/hosts/ganymede/nixos.nix
@@ -15,8 +15,6 @@
./caddy.nix
];
- # services.tailscale.extraSetFlags = ["--advertise-exit-node"];
-
# backup usb teather configuration
systemd.network.networks."80-usb-teather" = {
name = "enp0s20f0u2";
@@ -58,6 +56,30 @@
};
};
+ # deploy user
+ users.groups."deploy" = {};
+ users.users."deploy" = {
+ isSystemUser = true;
+ group = "deploy";
+
+ openssh.authorizedKeys.keys = config.users.users."collin".openssh.authorizedKeys.keys;
+ };
+ security.sudo.extraRules = [
+ {
+ users = ["deploy"];
+ commands = [
+ {
+ command = "/nix/store/*/bin/switch-to-configuration";
+ options = ["NOPASSWD"];
+ }
+ {
+ command = "/run/current-system/sw/bin/shutdown";
+ options = ["NOPASSWD"];
+ }
+ ];
+ }
+ ];
+
services.fail2ban.enable = true;
# merge logs from subdomains
diff --git a/hosts/mercury/hjem.nix b/hosts/mercury/hjem.nix
index 9e0ca77..793ee2a 100644
--- a/hosts/mercury/hjem.nix
+++ b/hosts/mercury/hjem.nix
@@ -30,11 +30,11 @@ in {
captive-browser # https://words.filippo.io/captive-browser
captiveBrowserDesktop
- inputs.glide-browser.packages."x86_64-linux".default
- inputs.vermilion.packages."x86_64-linux".default
+ # inputs.glide-browser.packages."x86_64-linux".default
+ # inputs.vermilion.packages."x86_64-linux".default
# (pkgs.callPackage ../../pkgs/yoshi.nix {inherit inputs;})
- (pkgs.callPackage ../../pkgs/yo.nix {inherit inputs;})
+ (pkgs.callPackage ../../pkgs/yo {inherit inputs;})
];
files.".config/captive-browser.toml".text = ''
diff --git a/hosts/mercury/nixos.nix b/hosts/mercury/nixos.nix
index fee1a5e..932fdb0 100644
--- a/hosts/mercury/nixos.nix
+++ b/hosts/mercury/nixos.nix
@@ -30,8 +30,6 @@
}
];
- security.soteria.enable = true;
-
services.printing.enable = true;
environment.defaultPackages = lib.mkForce []; # im not a noob
diff --git a/lib/nix-furnace/mkSystem.nix b/lib/nix-furnace/mkSystem.nix
index 856655a..3bdc818 100644
--- a/lib/nix-furnace/mkSystem.nix
+++ b/lib/nix-furnace/mkSystem.nix
@@ -68,7 +68,6 @@ let
genDocs = {
lib,
pkgs,
- inputs,
hostname,
...
}: let
diff --git a/modules/desktop/hjem/wm/sway.nix b/modules/desktop/hjem/wm/sway.nix
index 14f8637..62e4203 100644
--- a/modules/desktop/hjem/wm/sway.nix
+++ b/modules/desktop/hjem/wm/sway.nix
@@ -38,7 +38,6 @@
settings = ''
exec ${config.collinux.desktop.wallpaper_cmd}
exec ${pkgs.dunst}/bin/dunst
- exec ${pkgs.soteria}/bin/soteria
include catppuccin-mocha
diff --git a/modules/desktop/options.nix b/modules/desktop/options.nix
index 1bd03fe..9081ba9 100644
--- a/modules/desktop/options.nix
+++ b/modules/desktop/options.nix
@@ -25,6 +25,7 @@ in {
autologin = {
enable = mkEnableOption "autologin";
command = mkOption {
+ internal = true;
type = lib.types.str;
default = with config.collinux.desktop;
if (wm.sway.enable && !gnome.enable && !wm.niri.enable)
diff --git a/modules/services/nixos/cgit/default.nix b/modules/services/nixos/cgit/default.nix
index b35a631..3c225ee 100644
--- a/modules/services/nixos/cgit/default.nix
+++ b/modules/services/nixos/cgit/default.nix
@@ -60,8 +60,8 @@ in {
root-desc=Git repos associated with Ganymede
readme=:README.md
- about-filter=${custom_cgit}/lib/cgit/filters/html-converters/md2html
- source-filter=${custom_cgit}/lib/cgit/filters/syntax-highlighting.py
+ about-filter=${pkgs.cgit}/lib/cgit/filters/html-converters/md2html
+ source-filter=${pkgs.cgit}/lib/cgit/filters/syntax-highlighting.py
footer=
virtual-root=/
diff --git a/modules/services/nixos/goaccess.nix b/modules/services/nixos/goaccess.nix
index f8940c9..bb174cb 100644
--- a/modules/services/nixos/goaccess.nix
+++ b/modules/services/nixos/goaccess.nix
@@ -7,8 +7,8 @@
cfg = config.collinux.services.goaccess;
geoip = pkgs.fetchurl {
- url = "https://github.com/P3TERX/GeoLite.mmdb/releases/download/2026.02.13/GeoLite2-City.mmdb";
- hash = "sha256-FnvfGHH8b0506muEvlDXjgNKruorWeMXEbquGdZoH6Q=";
+ url = "https://github.com/P3TERX/GeoLite.mmdb/releases/download/2026.03.01/GeoLite2-City.mmdb";
+ hash = "sha256-SZ/m8b53sawF86Ak46uX7CxdzEMFrmob7Yd12Y+xJfA=";
};
settings = {
diff --git a/modules/services/nixos/qbittorrent.nix b/modules/services/nixos/qbittorrent.nix
index d0249d0..13bf114 100644
--- a/modules/services/nixos/qbittorrent.nix
+++ b/modules/services/nixos/qbittorrent.nix
@@ -10,7 +10,10 @@ in {
];
config = lib.mkIf cfg.enable {
- users.users."qbittorrent".extraGroups = ["fileserver"]; # torrent files go to /media/library
+ users.users."qbittorrent" = {
+ uid = 985;
+ extraGroups = ["fileserver"]; # torrent files go to /media/library
+ };
networking.firewall.allowedTCPPorts = [49252];
networking.firewall.allowedUDPPorts = [49252];
@@ -20,5 +23,7 @@ in {
webuiPort = cfg.port;
torrentingPort = 49252;
};
+
+ systemd.services.qbittorrent.serviceConfig.PrivateUsers = lib.mkForce false;
};
}
diff --git a/modules/services/options.nix b/modules/services/options.nix
index bd0a990..16c5bea 100644
--- a/modules/services/options.nix
+++ b/modules/services/options.nix
@@ -115,6 +115,7 @@ in {
})
// {
users = mkOption {
+ description = "List of users to configure on the copyparty server";
type = lib.types.attrsOf (lib.types.submodule ({config, ...}: {
options = {
name = mkOption {
@@ -137,6 +138,7 @@ in {
caddy = {
enable = mkEnableOption "caddy https server";
envFile = mkOption {
+ description = "Absolute path to file that contains environment files to run caddy with";
type = types.str;
example = "/run/secrets.d/caddy-env";
};
diff --git a/modules/system/nixos/networking/default.nix b/modules/system/nixos/networking/default.nix
index 04cd1c3..f6baaac 100644
--- a/modules/system/nixos/networking/default.nix
+++ b/modules/system/nixos/networking/default.nix
@@ -19,8 +19,8 @@
networkmanager.enable = false;
};
- # disable all ipv6
boot.kernel.sysctl = {
+ # disable all ipv6
"net.ipv6.conf.all.disable_ipv6" = 1;
"net.ipv6.conf.default.disable_ipv6" = 1;
};
diff --git a/modules/wireguard/nixos/default.nix b/modules/wireguard/nixos/default.nix
deleted file mode 100644
index 9f05e43..0000000
--- a/modules/wireguard/nixos/default.nix
+++ /dev/null
@@ -1,50 +0,0 @@
-{
- config,
- lib,
- ...
-}: let
- cfg = config.collinux.wireguard;
-in
- lib.mkIf cfg.enable {
- networking.firewall.checkReversePath = "loose";
-
- networking.useNetworkd = true;
- systemd.network = {
- enable = true;
-
- netdevs."10-wg" = {
- netdevConfig = {
- Kind = "wireguard";
- Name = "wg0";
- };
-
- wireguardConfig = {
- PrivateKeyFile = cfg.privateKeyFile;
- ListenPort = 51820;
- };
-
- wireguardPeers =
- cfg.peers
- |> builtins.mapAttrs (_: m:
- {
- PublicKey = m.publicKey;
- AllowedIPs = [m.ip];
- PersistentKeepalive = 25;
- }
- // (lib.optionalAttrs (m.endpoint != null) {
- Endpoint = m.endpoint;
- }))
- |> builtins.attrValues;
- };
-
- networks."12-wireguard" = {
- name = "wg0";
-
- networkConfig = {
- Address = cfg.ip;
- DHCP = "no";
- Gateway = cfg.gateway;
- };
- };
- };
- }
diff --git a/modules/wireguard/options.nix b/modules/wireguard/options.nix
deleted file mode 100644
index c3c574f..0000000
--- a/modules/wireguard/options.nix
+++ /dev/null
@@ -1,41 +0,0 @@
-{lib, ...}: let
- inherit (lib) mkOption mkEnableOption;
-in {
- options = {
- collinux.wireguard = {
- enable = mkEnableOption "wireguard";
- ip = mkOption {
- type = lib.types.str;
- description = "host's ip address on the wireguard network (with cidr)";
- };
- gateway = mkOption {
- type = lib.types.str;
- description = "host's gateway";
- };
- privateKeyFile = mkOption {
- type = lib.types.str;
- description = "path to local private key file";
- example = "/run/secretd.d/wireguard-key";
- };
- peers = mkOption {
- type = lib.types.attrsOf (lib.types.submodule {
- options = {
- publicKey = mkOption {
- type = lib.types.str;
- description = "peer's public key";
- };
- ip = mkOption {
- type = lib.types.str;
- description = "peer's IP address";
- };
- endpoint = mkOption {
- type = lib.types.nullOr lib.types.str;
- description = "peer's endpoint";
- default = null;
- };
- };
- });
- };
- };
- };
-}
diff --git a/pkgs/util/default.nix b/pkgs/util/default.nix
index 9dbe067..79d131c 100644
--- a/pkgs/util/default.nix
+++ b/pkgs/util/default.nix
@@ -1,4 +1,4 @@
-{pkgs ? import <nixpkgs> {system = "x86_64-linux";}, ...}: let
+{pkgs, ...}: let
lsdbus-src = pkgs.fetchFromGitHub {
owner = "kmarkus";
repo = "lsdbus";
diff --git a/pkgs/yo.nix b/pkgs/yo.nix
deleted file mode 100644
index 8a71e51..0000000
--- a/pkgs/yo.nix
+++ /dev/null
@@ -1,93 +0,0 @@
-{pkgs, ...}: let
- yo = pkgs.writeText "yo.lua" ''
- local CONF_FILE = os.getenv("HOME") .. "/.config/yo.conf"
-
- local conf = {}
- for l in io.open(CONF_FILE):lines() do
- if l:match("^(%a+)%s") then
- local k, v = l:match("^(%a+)%s+(.*)$")
- conf[k] = v
- end
- end
-
- local SUDO = "sudo"
- if conf.sudo then
- SUDO = conf.sudo
- elseif os.getenv("SUDO") then
- SUDO = os.getenv("SUDO")
- end
-
- local TARGET = "/home/collin/nixos"
-
- local function getHostname()
- local f = io.popen("hostname")
- local h = f:read("*l")
- f:close()
- return h
- end
-
- local function buildConfiguration(args)
- local flake_path = args.flakePath
- local hostname = args.hostname
- local extra_args = table.concat(args.extraArgs or {}, " ")
- local build_cmd = ('nix build %s#nixosConfigurations."%s".config.system.build.toplevel --log-format internal-json %s |& ${pkgs.nix-output-monitor}/bin/nom --json'):format(flake_path, hostname, extra_args)
- print("+ "..build_cmd)
- os.execute(build_cmd)
- end
-
- local function switchToConfiguration(verb)
- local proc_store_path = io.popen(("readlink -f %s/result"):format(TARGET))
- local store_path = proc_store_path:read("*l")
- proc_store_path:close()
-
- -- create generation
- local create_gen_cmd = ("nix build --no-link --profile %s %s"):format("/nix/var/nix/profiles/system", store_path)
-
- local verb_cmd = ("%s/result/bin/switch-to-configuration %s"):format(TARGET, verb)
-
- local sudo_verb_cmd = ("%s ${pkgs.bashNonInteractive}/bin/bash -c '%s; %s'"):format(SUDO, create_gen_cmd, verb_cmd)
- print("+ "..sudo_verb_cmd)
- os.execute(sudo_verb_cmd)
- end
-
- local function deployConfiguration(args)
- local flake_path = args.flakePath
- cmd = ("nix run github:serokell/deploy-rs %s -- --skip-checks -- --log-format internal-json |& ${pkgs.nix-output-monitor}/bin/nom --json"):format(flake_path)
- print("+ "..cmd)
- os.execute(cmd)
- end
-
- if arg[1] == "deploy" or arg[1] == "dep" or arg[1] == "d" then
- deployConfiguration{
- flakePath = TARGET
- }
- elseif arg[1] == "switch" or arg[1] == "sw" then
- buildConfiguration{
- flakePath = TARGET,
- hostname = getHostname()
- }
- switchToConfiguration("switch")
- elseif arg[1] == "boot" then
- buildConfiguration {
- flakePath = TARGET,
- hostname = getHostname()
- }
- switchToConfiguration("boot")
- elseif arg[1] == "test" then
- buildConfiguration {
- flakePath = TARGET,
- hostname = getHostname()
- }
- switchToConfiguration("test")
- elseif arg[1] == "build" then
- local hostname = arg[2] or getHostname()
- buildConfiguration {
- flakePath = TARGET,
- hostname = hostname
- }
- end
- '';
-in
- pkgs.writeShellScriptBin "yo" ''
- exec ${pkgs.lua}/bin/lua ${yo} "$@"
- ''
diff --git a/pkgs/yo/README.md b/pkgs/yo/README.md
new file mode 100644
index 0000000..a308057
--- /dev/null
+++ b/pkgs/yo/README.md
@@ -0,0 +1,2 @@
+# `nixos.rb`
+A small Ruby library for writing `nixos-rebuild` replacements.
diff --git a/pkgs/yo/default.nix b/pkgs/yo/default.nix
new file mode 100644
index 0000000..06e88d4
--- /dev/null
+++ b/pkgs/yo/default.nix
@@ -0,0 +1,59 @@
+{pkgs, ...}: let
+ yo = pkgs.writeText "yo.rb" ''
+ require '${./nixos.rb}'
+
+ FLAKE_PATH = "/home/collin/nixos"
+
+ case ARGV[0]
+ when "deploy", "dep"
+ hostname = ARGV[1] or abort "must specify hostname to build"
+ ssh_host = ARGV[2] or abort "must specify ssh host to target"
+
+ store_path = Nix.build_configuration(
+ flake_path: FLAKE_PATH,
+ hostname: hostname
+ )
+ Nix.switch_to_configuration_remote(store_path: store_path, ssh_host: ssh_host, use_magic_rollback: true)
+ when "switch", "sw"
+ store_path = Nix.build_configuration(
+ flake_path: FLAKE_PATH,
+ hostname: `hostname`.chomp
+ )
+ Nix.switch_to_configuration(
+ store_path: store_path,
+ verb: "switch",
+ sudo_cmd: "run0 --background="
+ )
+ when "boot"
+ store_path = Nix.build_configuration(
+ flake_path: FLAKE_PATH,
+ hostname: `hostname`.chomp
+ )
+ Nix.switch_to_configuration(
+ store_path: store_path,
+ verb: "boot",
+ sudo_cmd: "run0 --background="
+ )
+ when "test"
+ store_path = Nix.build_configuration(
+ flake_path: FLAKE_PATH,
+ hostname: `hostname`.chomp
+ )
+ Nix.switch_to_configuration(
+ store_path: store_path,
+ verb: "test",
+ sudo_cmd: "run0 --background="
+ )
+ when "build"
+ hostname = ARGV[1] or `hostname`.chomp
+
+ Nix.build_configuration(
+ flake_path: FLAKE_PATH,
+ hostname: hostname
+ )
+ end
+ '';
+in
+ pkgs.writeShellScriptBin "yo" ''
+ exec ${pkgs.ruby}/bin/ruby ${yo} "$@"
+ ''
diff --git a/pkgs/yo/nixos.rb b/pkgs/yo/nixos.rb
new file mode 100644
index 0000000..73a1872
--- /dev/null
+++ b/pkgs/yo/nixos.rb
@@ -0,0 +1,37 @@
+module Nix
+ def build_configuration(flake_path:, hostname:)
+ flake_target = "#{flake_path}#nixosConfigurations.'#{hostname}'.config.system.build.toplevel"
+ `nix build --no-link --print-out-paths #{flake_target}`.chomp.tap do
+ raise "Build command failed" unless $?.success?
+ end
+ end
+
+ def switch_to_configuration(store_path:, verb:, sudo_cmd: "sudo")
+ create_gen_cmd = "nix build --no-link --profile /nix/var/nix/profiles/system #{store_path}"
+ switch_cmd = "#{store_path}/bin/switch-to-configuration #{verb}"
+ system("#{sudo_cmd} bash -c '#{create_gen_cmd}; #{switch_cmd}'") or raise "Switch command failed"
+ end
+
+ def switch_to_configuration_remote(store_path:, ssh_host:, use_magic_rollback: false)
+ system("nix copy --to ssh://#{ssh_host} #{store_path}") or raise "Copy closure command failed"
+
+ create_gen_cmd = "nix build --no-link --profile /nix/var/nix/profiles/system #{store_path}"
+ switch_cmd = "#{store_path}/bin/switch-to-configuration switch"
+
+ if use_magic_rollback
+ test_cmd = "#{store_path}/bin/switch-to-configuration test"
+ system(%[ssh #{ssh_host} 'bash -c "#{test_cmd}; shutdown -r +5"']) or raise "Remote test command failed"
+
+ puts <<~MSG
+ Remote deployment complete.
+ If everything works, press ENTER to confirm and cancel rollback.
+ If SSH dies, it will restart (reversing changes) automatically in 5 minutes
+ MSG
+ $stdin.gets
+
+ system(%[ssh #{ssh_host} 'bash -c "shutdown -c; #{create_gen_cmd}; #{switch_cmd}"']) or raise "Remote switch command failed"
+ else
+ system(%[ssh #{ssh_host} 'bash -c "#{create_gen_cmd}; #{switch_cmd}"']) or raise "Remote switch command failed"
+ end
+ end
+end
diff --git a/pkgs/yossh/README.md b/pkgs/yossh/README.md
new file mode 100644
index 0000000..56179ae
--- /dev/null
+++ b/pkgs/yossh/README.md
@@ -0,0 +1,129 @@
+# Yo SSH Host Initiator (Y.O.S.H.I)
+Yoshi is a lua library that wraps the `ssh` client, allowing you to define complex host configurations using the power of a full programming langage (lua) instead of the `ssh_config` dsl.
+
+Obligatory code example (`~/bin/yossh`):
+```lua
+#!/usr/bin/env lua
+yoshi = require'yoshi'
+
+local function isHome()
+ local _, _, code = os.execute("nc -z -w1 192.168.50.2 2222")
+ return code == 0
+end
+
+yoshi.hosts["ganymede"] = function()
+ if isHome() then
+ return yoshi.ssh{
+ HostName = "192.168.50.2",
+ Port = 2222,
+ LocalForward = 8010
+ }
+ else
+ return yoshi.ssh{
+ HostName = "williamsfam.us.com",
+ LocalForward = 8010,
+ DynamicFoward = 9090
+ }
+ end
+end
+
+yoshi.hosts["io"] = function()
+ if isHome() then
+ return yoshi.ssh{HostName = "192.168.50.3"}
+ else
+ return yoshi.ssh{
+ HostName = "192.168.50.3",
+ User = "admin",
+ ProxyJump = "collin@ganymede",
+ }
+ end
+end
+
+yoshi.run(arg)
+```
+
+```
+$ yossh io
+Connection to 192.168.50.2 2222 port [tcp/ethernet-ip-1] succeeded!
++ ssh admin@192.168.50.3
+io:~$
+```
+
+## 🍄 Getting Started
+First, clone this repo, and make sure you have ssh and lua installed.
+```
+sudo apt install ssh luajit5.4 # or whatever
+git clone https://github.com/bluedragon1221/yoshi
+```
+
+Next, place `yoshi.lua` in a place that lua can find it.
+Consult the [lua documentation](https://www.lua.org/pil/8.1.html) for help.
+
+Now you can write a simple configuration.
+As an example, let's look at the equivelant of this `ssh_config` block:
+```
+Host box
+ HostName 192.168.50.3
+ User yoshi
+```
+
+Using Yoshi, it would look like this:
+```lua
+-- yoshi_cfg.lua
+yoshi = require'yoshi'
+
+yoshi.hosts["box"] = yoshi.ssh{
+ HostName = "192.168.50.3",
+ User = "yoshi"
+}
+
+yoshi.run(arg)
+```
+
+> (To create more complicated configurations, read the [docs](./doc/lua_doc.md))
+
+To test your Yoshi configuration, you can run `lua yoshi_cfg.lua`.
+This would be annoying to type every time you need to access your remote server, though!
+So we'll place the script in the `$PATH` (ex. `~/bin/yossh` or something), and use a shabang to tell the script to execute with lua:
+```lua
+#!/usr/bin/env lua
+yoshi = require'yoshi'
+
+yoshi.hosts["box"] = yoshi.ssh{
+ HostName = "192.168.50.3",
+ User = "yoshi"
+}
+
+yoshi.run(arg)
+```
+
+Assuming `~/bin` is in the `$PATH` and you made `yossh` executable with `chmod +x`, running `yossh` in the terminal will give you a nice help page:
+```bash
+$ yossh
+yossh - smart SSH wrapper
+
+USAGE:
+ yossh <host>
+ yossh <user@host>
+ yossh <host:port>
+ yossh <user@host:port>
+
+AVAILABLE HOSTS:
+ - box
+```
+
+And you can ssh into `box`:
+```bash
+$ yossh box
++ ssh yoshi@192.168.50.3
+io:~$ whoami
+yoshi
+```
+
+## 📖 Docs
+- [`doc/lua_doc.md`](doc/lua_doc.md) contains comprehensive docs for the Yoshi lua api
+- [`doc/nix.md`](doc/nix.md) discusses packaging your Yoshi configuration with nix
+
+---
+
+[![BrainMade](https://brainmade.org/88x31-dark.png)](https://brainmade.org)
diff --git a/pkgs/yoshi.nix b/pkgs/yossh/default.nix
index 9e261df..4ea0127 100644
--- a/pkgs/yoshi.nix
+++ b/pkgs/yossh/default.nix
@@ -7,7 +7,7 @@
pkgs.writeText "yossh.lua"
# lua
''
- yoshi = dofile'${inputs.yoshi-lua}/yoshi.lua'
+ yoshi = dofile'${./yoshi.lua}'
local function isHome()
local _, _, code = os.execute("nc -z -w1 192.168.50.2 2222")
diff --git a/pkgs/yossh/doc/lua_doc.md b/pkgs/yossh/doc/lua_doc.md
new file mode 100644
index 0000000..b029f56
--- /dev/null
+++ b/pkgs/yossh/doc/lua_doc.md
@@ -0,0 +1,118 @@
+# Lua Documentation
+Lua API documentation for Yoshi
+
+## `yoshi.hosts`
+- Description: List of defined ssh hosts. Each entry must be a function that returns an `sshCommand` when called.
+- Type: table of `function(): sshCommand`
+- Example:
+```lua
+yoshi.hosts["webserver"] = function()
+ -- this is just a random example to show why you might want yoshi.hosts[host] to be a function
+ if os.getenv("IM_FEELING") == "sad" then
+ return yoshi.ssh{HostName = "sad.webserver.local"}
+ else
+ return yoshi.ssh{HostName = "happy.webserver.local"}
+ end
+end
+```
+
+## `yoshi.ssh`
+- Description: Constructs an `sshCommand`
+- Type: `function(table): sshCommand`
+- Example:
+```lua
+yoshi.ssh{
+ HostName = "webserver.local",
+ Port = 2227,
+ ProxyJump = "bastion"
+}
+```
+
+## `yoshi.sshCommand.User`
+- Description: Specifies the target user in the ssh connection. Analogous to the `User` key in `ssh_config`
+- Type: string or nil
+- Example: `yoshi`, `collin123`
+
+## `yoshi.sshCommand.HostName`
+- Description: Specifies the target host in the ssh connection. The only required element to `yoshi.sshCommand`. Analogous to the `HostName` key in `ssh_config`
+- Type: string
+- Example: `192.168.0.27`, `github.com`, `140.82.114.3`
+
+## `yoshi.sshCommand.Port`
+- Description: Specifies the target port in the ssh connection. Defaults to `22` if unspecified. Analagous to the `Port` key in `ssh_config`
+- Type: integer, `0 <= i <= 65535`
+- Examples: `22`, `2222`, `2225`
+
+## `yoshi.sshCommand.RemoteCommand`
+- Description: Specifies a command to run on the target host instead of the default shell. Analagous to the `RemoteCommand` key in `ssh_config`
+- Type: string
+- Examples: `whoami`, `nixos-rebuild switch --flake /etc/nixos`, `tmux new-session -A`
+
+## `yoshi.sshCommand.SessionType`
+- Description: Specifies the type of ssh session. "none" means that no shell access is provded. Analogous to the `SessionType` key in `ssh_config`
+- Type: "subsystem", "none", or "default"
+- Examples: `"none"`
+
+## `yoshi.sshCommand.LocalForward`
+- Description: Specifies a port on the target host to forward to the local host in the format `{local_port, target_port}`. If no local port is specified, will assume host port and local port are the same. Analagous to the `LocalForward` key in `ssh_config` or the `-L` flag on the command line
+- Type: `table[2] of integer` or integer, `0 <= i <= 65535`
+- Examples: `8010`, `{8010, 80}`
+
+## `yoshi.sshCommand.DynamicForward`
+- Description: Specifies a port on the local host to serve as a SOCKS5 proxy, forwarding all traffic through the target host. Analagous to the `DynamicForward` key in `ssh_config` or the `-D` option on the command line
+- Type: integer, `0 <= i <= 65535`
+- Examples: `9090`
+
+## `yoshi.sshCommand.ProxyJump`
+- Description: Specifies a host to act as a gateway to the target host if not directly accessable by the local host. It will inherit `User`, `Port`, and `HostName` options from `yoshi.hosts[host]` if it exists. Analagous to the `ProxyJump` key in `ssh_config` or the `-J` flag on the command line
+- Type: string in the form `user@host:port`
+- Example: `box`, `yoshi@box`, `box:27`, `yoshi@box:27`
+- Full Example:
+```lua
+yoshi.hosts["bastion"] = sshCommand.ssh{
+ HostName = "bastion.mywebsite.com",
+ Port = 23,
+}
+
+yoshi.hosts["server"] = sshCommand.ssh{
+ HostName = "192.168.0.2",
+ ProxyJump = "myuser@bastion"
+}
+
+assert_eq(
+ yoshi.getHost("server"):toCommand(),
+ "ssh -J myuser@bastion.mywebsite.com:23 server"
+)
+```
+
+## `yoshi.sshCommand:toCommand`
+- Description: Formats `self` as a valid `ssh` command to be run in the terminal.
+- Type: `sshCommand:function(): string`
+- Example:
+```lua
+os.execute(yoshi.getHost("io"):toCommand())
+```
+
+## `yoshi.getHost`
+- Description: Looks up and evaluates `yoshi.hosts[hostname]`, returning the resulting `sshCommand`. Raises an error if the host is not found or the host function returns nil.
+- Type: `function(string): sshCommand`
+- Example:
+```lua
+yoshi.hosts["webserver"] = function()
+ return yoshi.ssh{
+ HostName = "webserver.local",
+ Port = 22,
+ ProxyJump = "bastion"
+ }
+end
+
+os.execute(yoshi.getHost("webserver"):toCommand())
+```
+
+## `yoshi.run`
+- Description: Executes the ssh command associated with the given target host as provided by `arg`, a list of commandline arguments. If no arguments are provided, prints a help page. The first argument is a path of the form `[jumphost/.../]host`, where any leading path segments are used as explicit proxy jump hops. Subsequent arguments may include ssh flags such as `-D`, `-L`, `-N`, or `-- command`.
+- Type: `function(table of string, table of any)`
+- Example:
+```lua
+yoshi.run(arg, {dry_run = true}) -- arg is the global name for the lua program's commandline arguments
+```
diff --git a/pkgs/yossh/doc/nix.md b/pkgs/yossh/doc/nix.md
new file mode 100644
index 0000000..9631392
--- /dev/null
+++ b/pkgs/yossh/doc/nix.md
@@ -0,0 +1,44 @@
+# Packing your Yoshi configuration with nix
+If you want to declare Yoshi in your nixos configuration, here's how.
+
+First, add Yoshi to your nix flake:
+```nix
+{
+ inputs = {
+ yoshi = {
+ url = "github:bluedragon1221/yoshi";
+ flake = false;
+ };
+ };
+}
+```
+
+Next, write a package for Yoshi. This is how I achieved it:
+```sh
+{
+ pkgs,
+ inputs,
+ ...
+}: let
+ yossh = pkgs.writeText "yossh.lua" ''
+ yoshi = dofile'${inputs.yoshi-lua}/yoshi.lua'
+
+ yoshi.hosts["box"] = yoshi.ssh{
+ HostName = "192.168.50.3",
+ User = "yoshi"
+ }
+
+ yoshi.run(arg)
+ '';
+in
+ pkgs.writeShellScriptBin "yossh" ''
+ exec ${pkgs.lua}/bin/lua ${yossh} "$@"
+ ''
+```
+
+Then add the package in `environment.systemPackages`:
+```nix
+environment.systemPackages = [
+ (pkgs.callPackage ./yossh.nix {inherit inputs;})
+]
+```
diff --git a/pkgs/yossh/examples/home_wifi.lua b/pkgs/yossh/examples/home_wifi.lua
new file mode 100755
index 0000000..8df2aad
--- /dev/null
+++ b/pkgs/yossh/examples/home_wifi.lua
@@ -0,0 +1,33 @@
+#!/bin/sh
+yoshi = dofile'yoshi.lua'
+
+local function isHome()
+ local ok, reason, code = os.execute("nc -z -w1 192.168.50.2 2222")
+ return code == 0
+end
+
+yoshi.hosts["ganymede"] = function()
+ if isHome() then
+ return yoshi.ssh{
+ HostName = "192.168.50.2",
+ Port = 2222,
+ LocalForward = 8010
+ }
+ else
+ return yoshi.ssh{
+ HostName = "williamsfam.us.com",
+ LocalForward = 8010,
+ DynamicForward = 9090
+ }
+ end
+end
+
+yoshi.hosts["io"] = function()
+ return yoshi.ssh{
+ HostName = "192.168.50.3",
+ User = "admin",
+ ProxyJump = isHome() or "collin@ganymede",
+ }
+end
+
+yoshi.run(arg, {dry_run = true})
diff --git a/pkgs/yossh/examples/mega_bastion.lua b/pkgs/yossh/examples/mega_bastion.lua
new file mode 100644
index 0000000..d4eb3ee
--- /dev/null
+++ b/pkgs/yossh/examples/mega_bastion.lua
@@ -0,0 +1,61 @@
+-- situation: fictional network where there are three departments:
+-- - tech (172.16.10.0/24)
+-- - HR (172.16.20.0/24)
+-- - Secretary (172.16.40.0/24)
+-- Each department has
+-- - an ssh bastion server at .1:22 (only way to access other devices on the subnet from outside the subnet)
+-- - a shared sftp server at .2:25
+--
+-- This exact yoshi script would be deployed to all personal computers inside the network.
+local yoshi = dofile"yoshi.lua"
+
+local function inNetwork()
+ -- company internal landing page
+ local _, _, code = os.execute("nc -z -w1 172.16.0.1 443")
+ return code == 0
+end
+
+local function currDept()
+ local ip_addr = io.popen("hostname -I")
+ local _, _, third_octet, _ = string.gmatch(ip_addr, "%d%.%d%.%d%.%d")
+
+ return third_octet
+end
+
+local depts = {
+ tech = 10,
+ hr = 20,
+ secretary = 40
+}
+
+yoshi.hosts["global-bastion"] = yoshi.ssh{
+ HostName = "bastion.company-website.com",
+ Port = 2222
+}
+
+for name, subnet in pairs(depts) do
+ yoshi.hosts[name.."-bastion"] = function()
+ return yoshi.ssh{
+ HostName = "172.16."..subnet..".1",
+ ProxyJump = inNetwork() or "global-bastion"
+ }
+ end
+
+ yoshi.hosts[name.."-fileserver"] = function()
+ return yoshi.ssh{
+ HostName = "172.16."..subnet..".2",
+ SessionType = "none",
+ LocalForward = {2225, 25},
+ ProxyJump = inNetwork() and currDept() == subnet or name.."-bastion"
+ }
+ end
+end
+
+yoshi.hosts[name.."-db"] = yoshi.ssh{
+ HostName = "172.16.10.3",
+ SessionType = "none",
+ LocalForward = 5432,
+ ProxyJump = inNetwork() and currDept() == subnet or "tech-bastion"
+}
+
+yoshi.run(arg, {dry_run = true})
diff --git a/pkgs/yossh/examples/test.lua b/pkgs/yossh/examples/test.lua
new file mode 100644
index 0000000..a594305
--- /dev/null
+++ b/pkgs/yossh/examples/test.lua
@@ -0,0 +1,18 @@
+yoshi = dofile'yoshi.lua'
+
+yoshi.hosts["you"] = function()
+ return yoshi.ssh{
+ HostName = "192.168.50.2",
+ DynamicForward = 7320,
+ Port = 27
+ }
+end
+
+yoshi.hosts["me"] = function()
+ return yoshi.ssh{
+ HostName = "test.local",
+ ProxyJump = "jumper@you"
+ }
+end
+
+yoshi.run(arg, {dry_run = true})
diff --git a/pkgs/yossh/tests/unit_tests.lua b/pkgs/yossh/tests/unit_tests.lua
new file mode 100644
index 0000000..0ce044c
--- /dev/null
+++ b/pkgs/yossh/tests/unit_tests.lua
@@ -0,0 +1,190 @@
+-- ChatGPT wrote my unit tests
+
+local yoshi = dofile'yoshi.lua'
+local tests = {}
+
+----------------------------------------------------------------
+-- Minimal assert functions
+----------------------------------------------------------------
+
+local function fail(msg)
+ error("TEST FAILED: " .. msg, 2)
+end
+
+local function assertEqual(a, b, msg)
+ if a ~= b then fail(msg or string.format("%s ~= %s", tostring(a), tostring(b))) end
+end
+
+local function assertNotEqual(a, b, msg)
+ if a == b then fail(msg or string.format("%s == %s", tostring(a), tostring(b))) end
+end
+
+local function assertDeepEqual(a, b, msg)
+ local function compare(x, y)
+ if type(x) ~= type(y) then return false end
+ if type(x) ~= "table" then return x == y end
+ local k_checked = {}
+ for k,v in pairs(x) do
+ if not compare(v, y[k]) then return false end
+ k_checked[k] = true
+ end
+ for k,_ in pairs(y) do
+ if not k_checked[k] then return false end
+ end
+ return true
+ end
+ if not compare(a,b) then fail(msg or "Tables not equal") end
+end
+
+----------------------------------------------------------------
+-- Helpers
+----------------------------------------------------------------
+
+-- reset hosts between tests
+local function resetHosts()
+ yoshi.hosts = {}
+end
+
+----------------------------------------------------------------
+-- Tests
+----------------------------------------------------------------
+
+function tests.test_constructor()
+ local cmd = yoshi.ssh{
+ HostName = "localhost",
+ User = "me",
+ Port = 2222,
+ SessionType = "none",
+ DynamicForward = 8080,
+ LocalForward = 8000,
+ RemoteCommand = "ls -la",
+ }
+
+ assertEqual(cmd.HostName, "localhost")
+ assertEqual(cmd.User, "me")
+ assertEqual(cmd.Port, 2222)
+ assertEqual(cmd.SessionType, "none")
+ assertEqual(cmd.DynamicForward, 8080)
+ assertDeepEqual(cmd.LocalForward, {8000,8000})
+ assertEqual(cmd.RemoteCommand, "ls -la")
+end
+
+function tests.test_proxyjump()
+ resetHosts()
+
+ yoshi.hosts["bastion"] = function()
+ return yoshi.ssh{HostName="bastion.internal", User="bastionuser"}
+ end
+
+ local cmd = yoshi.ssh{
+ HostName = "server.internal",
+ User = "admin",
+ ProxyJump = "bastion",
+ }
+
+ assertEqual(#cmd.ProxyJumps, 1)
+ assertEqual(cmd.ProxyJumps[1].host, "bastion.internal")
+ assertEqual(cmd.ProxyJumps[1].user, "bastionuser")
+end
+
+function tests.test_nested_proxyjump()
+ resetHosts()
+ yoshi.hosts["jump1"] = function()
+ return yoshi.ssh{HostName="jump1.host", ProxyJump="jump2"}
+ end
+ yoshi.hosts["jump2"] = function()
+ return yoshi.ssh{HostName="jump2.host", User="jumpuser"}
+ end
+
+ local cmd = yoshi.ssh{
+ HostName = "target.host",
+ ProxyJump = "jump1"
+ }
+
+ local hops = cmd.ProxyJumps
+ assertEqual(#hops, 2)
+ assertEqual(hops[1].host, "jump2.host")
+ assertEqual(hops[1].user, "jumpuser")
+ assertEqual(hops[2].host, "jump1.host")
+end
+
+function tests.test_cli_hop_override()
+ resetHosts()
+ yoshi.hosts["ganymede"] = function() return yoshi.ssh{HostName="192.168.50.2"} end
+ yoshi.hosts["io"] = function() return yoshi.ssh{HostName="192.168.50.3", ProxyJump="ganymede"} end
+
+ local arg = {"collin@ganymede/io"}
+ local target = yoshi.getHost("io")
+ local path = {"collin@ganymede"} -- simulate split_path(arg[1])
+
+ local acc = {}
+ for i = 1, #path do
+ local hop = yoshi.parse_user_host_port(path[i])
+ yoshi.collect_proxy_jumps(hop, acc)
+ end
+
+ target.ProxyJumps = acc
+ assertEqual(#target.ProxyJumps, 1)
+ assertEqual(target.ProxyJumps[1].user, "collin")
+ assertEqual(target.ProxyJumps[1].host, "192.168.50.2")
+end
+
+function tests.test_toCommand()
+ resetHosts()
+ local cmd = yoshi.ssh{
+ HostName = "localhost",
+ User = "me",
+ Port = 2222,
+ DynamicForward = 8080,
+ LocalForward = 8000,
+ SessionType = "none",
+ RemoteCommand = "ls"
+ }
+
+ local text = cmd:toCommand()
+ assert(text:match("ssh"))
+ assert(text:match("-D 8080"))
+ assert(text:match("-L 8000:127.0.0.1:8000"))
+ assert(text:match("-N"))
+ assert(text:match("me@localhost"))
+ assert(text:match("ls"))
+end
+
+function tests.test_LocalForward_tuple()
+ local cmd = yoshi.ssh{
+ HostName="localhost",
+ LocalForward={9000,9001}
+ }
+ local text = cmd:toCommand()
+ assert(text:match("-L 9000:127.0.0.1:9001"))
+end
+
+function tests.test_DynamicForward()
+ local cmd = yoshi.ssh{
+ HostName="localhost",
+ DynamicForward=9999
+ }
+ local text = cmd:toCommand()
+ assert(text:match("-D 9999"))
+end
+
+function tests.test_RemoteCommand()
+ local cmd = yoshi.ssh{HostName="localhost", RemoteCommand="echo hello"}
+ local text = cmd:toCommand()
+ assert(text:match("echo hello"))
+end
+
+----------------------------------------------------------------
+-- Run tests
+----------------------------------------------------------------
+
+local function main()
+ tests.test_constructor()
+ tests.test_proxyjump()
+ tests.test_nested_proxyjump()
+ tests.test_cli_hop_override()
+ tests.test_toCommand()
+ tests.test_LocalForward_tuple()
+ tests.test_DynamicForward()
+ tests.test_RemoteCommand ()
+end
diff --git a/pkgs/yossh/yoshi.lua b/pkgs/yossh/yoshi.lua
new file mode 100644
index 0000000..ce2086a
--- /dev/null
+++ b/pkgs/yossh/yoshi.lua
@@ -0,0 +1,276 @@
+local SSHTarget = {}
+
+function SSHTarget:clone()
+ return {
+ user = self.user,
+ host = self.host,
+ port = self.port
+ }
+end
+
+local function parse_user_host_port(input)
+ local user, rest = input:match("^(.-)@(.*)$")
+ if not rest then rest = input end
+
+ local host, port_str = rest:match("^(.-):(%d+)$")
+ local port
+ if not host then
+ host = rest
+ else
+ port = tonumber(port_str)
+ end
+
+ return {
+ user = user,
+ host = host,
+ port = port,
+ }
+end
+
+local function split_path(path)
+ local t = {}
+ for seg in path:gmatch("[^/]+") do
+ table.insert(t, seg)
+ end
+ return t
+end
+
+local yoshi = {
+ hosts = {}
+}
+
+local SSHConfig = {}
+local SSHCommand = {}
+
+function SSHCommand:clone()
+ local new_proxy_jumps
+ if self.proxy_jumps then
+ new_proxy_jumps = {}
+ for _, j in ipairs(self.proxy_jumps) do
+ table.insert(new_proxy_jumps, {
+ user = j.user,
+ host = j.host,
+ port = j.port,
+ })
+ end
+ end
+
+ return setmetatable({
+ target = {
+ user = self.target.user,
+ host = self.target.host,
+ port = self.target.port,
+ },
+ session_type = self.session_type,
+ dynamic_forward = self.dynamic_forward,
+ local_forward = self.local_forward and { self.local_forward[1], self.local_forward[2] } or nil,
+ remote_command = self.remote_command,
+ proxy_jumps = new_proxy_jumps,
+ }, { __index = SSHCommand })
+end
+
+function SSHCommand:overlayTarget(target)
+ local new = self:clone()
+ if target.user then
+ new.target.user = target.user
+ end
+
+ if target.port then
+ new.target.port = target.port
+ end
+
+ return new
+end
+
+function yoshi.getHost(hostname)
+ local base = yoshi.hosts[hostname]
+ if not base then
+ error("Unknown host: " .. hostname)
+ end
+
+ local cmd = base()
+ if cmd then
+ return cmd
+ else
+ error("Calling host returned nil")
+ end
+end
+
+function yoshi.ssh(o)
+ local local_forward
+ local l = o.LocalForward
+ if type(l) == "number" then
+ local_forward = { l, l }
+ else
+ local_forward = l
+ end
+
+ local proxy_jumps = {}
+ if o.ProxyJump then
+ local u = parse_user_host_port(o.ProxyJump)
+ local jmp = yoshi.getHost(u.host):overlayTarget(u)
+
+ table.insert(proxy_jumps, jmp.target)
+ if #jmp.proxy_jumps ~= 0 then
+ for _, p in ipairs(jmp.proxy_jumps) do
+ table.insert(proxy_jumps, p)
+ end
+ end
+ end
+
+ local self = setmetatable({
+ target = {
+ user = o.User,
+ host = o.HostName,
+ port = o.Port or 22,
+ },
+ session_type = o.SessionType or "default",
+ dynamic_forward = o.DynamicForward,
+ local_forward = local_forward,
+ remote_command = o.RemoteCommand,
+
+ proxy_jumps = proxy_jumps,
+ }, { __index = SSHCommand })
+
+ return self
+end
+
+function SSHCommand:toCommand()
+ local ret = { "ssh" }
+
+ if self.dynamic_forward then
+ table.insert(ret, "-D")
+ table.insert(ret, tostring(self.dynamic_forward))
+ end
+
+ if self.local_forward then
+ table.insert(ret, "-L")
+ local lf = self.local_forward
+ table.insert(ret,
+ lf[1] .. ":127.0.0.1:" .. lf[2])
+
+ end
+
+ if self.session_type == "none" then
+ table.insert(ret, "-N")
+ end
+
+ if self.proxy_jumps and #self.proxy_jumps > 0 then
+ local js = {}
+ for _, j in ipairs(self.proxy_jumps) do
+ local s = (j.user and j.user .. "@" or "") .. j.host
+ if j.port and j.port ~= 22 then s = s .. ":" .. j.port end
+ table.insert(js, s)
+ end
+ table.insert(ret, "-J")
+ table.insert(ret, table.concat(js, ","))
+ end
+
+ if self.target.port and self.target.port ~= 22 then
+ table.insert(ret, "-p")
+ table.insert(ret, tostring(self.target.port))
+ end
+
+ table.insert(ret, (self.target.user and self.target.user .. "@" or "") .. self.target.host)
+
+ if self.remote_command then
+ table.insert(ret, "-t")
+ table.insert(ret, "'" .. self.remote_command .. "'")
+ end
+
+ return table.concat(ret, " ")
+end
+
+function SSHCommand:overlayCliOpts(cli_args)
+ local new = self:clone()
+
+ local i = 2
+ while i <= #cli_args do
+ local a = cli_args[i]
+
+ if a == "-D" then
+ new.dynamic_forward = tonumber(cli_args[i + 1])
+ i = i + 2
+
+ elseif a == "-L" then
+ local spec = cli_args[i + 1]
+
+ local port = tonumber(spec)
+ if port then
+ new.local_forward = { port, port }
+ i = i + 2
+
+ else
+ local l, r = spec:match("^(%d+):.*:(%d+)$")
+ if not l or not r then
+ error("Invalid -L spec: " .. spec)
+ end
+ new.local_forward = { tonumber(l), tonumber(r) }
+ i = i + 2
+ end
+
+ elseif a == "-N" then
+ new.session_type = "none"
+ i = i + 1
+
+ elseif a == "--" then
+ local parts = {}
+ for j = i + 1, #cli_args do
+ table.insert(parts, cli_args[j])
+ end
+ new.remote_command = table.concat(parts, " ")
+ break
+
+ else
+ error("Unknown or unsupported ssh option: " .. a)
+ end
+ end
+
+ return new
+end
+
+local function show_help()
+ print("Usage: ssh <[jumphost/...]host> [options] [-- command]")
+ print("\nAvailable hosts:")
+ for h, _ in pairs(yoshi.hosts) do
+ print(" - " .. h)
+ end
+end
+
+function yoshi.run(cli_opts, opts)
+ if not cli_opts then
+ error("Please pass `arg` to yoshi.run (ex. `yoshi.run(arg)`)")
+ end
+
+ if cli_opts[1] == nil then
+ show_help()
+ os.exit(0)
+ end
+
+ local path = split_path(cli_opts[1])
+ local destName = path[#path]
+
+ local target = yoshi.getHost(destName)
+
+ local acc = {}
+ if #path > 1 then
+ for i = 1, #path - 1 do
+ local hop = parse_user_host_port(path[i])
+ table.insert(acc, {
+ user = hop.user,
+ host = hop.host,
+ port = hop.port,
+ })
+ end
+ target.proxy_jumps = acc
+ end
+
+ local cmd = target:overlayCliOpts(cli_opts)
+ print("+ " .. cmd:toCommand())
+
+ if not opts or not opts.dry_run then
+ os.execute(cmd)
+ end
+end
+
+return yoshi