From c29a14135736c2516bb5d951e6e97dc8143598db Mon Sep 17 00:00:00 2001 From: Collin Williams <96917990+bluedragon1221@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:45:26 -0600 Subject: changes --- flake.lock | 98 +--------- flake.nix | 53 ++---- hosts/ganymede/nixos.nix | 26 ++- hosts/mercury/hjem.nix | 6 +- hosts/mercury/nixos.nix | 2 - lib/nix-furnace/mkSystem.nix | 1 - modules/desktop/hjem/wm/sway.nix | 1 - modules/desktop/options.nix | 1 + modules/services/nixos/cgit/default.nix | 4 +- modules/services/nixos/goaccess.nix | 4 +- modules/services/nixos/qbittorrent.nix | 7 +- modules/services/options.nix | 2 + modules/system/nixos/networking/default.nix | 2 +- modules/wireguard/nixos/default.nix | 50 ----- modules/wireguard/options.nix | 41 ----- pkgs/util/default.nix | 2 +- pkgs/yo.nix | 93 ---------- pkgs/yo/README.md | 2 + pkgs/yo/default.nix | 59 ++++++ pkgs/yo/nixos.rb | 37 ++++ pkgs/yoshi.nix | 52 ------ pkgs/yossh/README.md | 129 +++++++++++++ pkgs/yossh/default.nix | 52 ++++++ pkgs/yossh/doc/lua_doc.md | 118 ++++++++++++ pkgs/yossh/doc/nix.md | 44 +++++ pkgs/yossh/examples/home_wifi.lua | 33 ++++ pkgs/yossh/examples/mega_bastion.lua | 61 ++++++ pkgs/yossh/examples/test.lua | 18 ++ pkgs/yossh/tests/unit_tests.lua | 190 +++++++++++++++++++ pkgs/yossh/yoshi.lua | 276 ++++++++++++++++++++++++++++ 30 files changed, 1077 insertions(+), 387 deletions(-) delete mode 100644 modules/wireguard/nixos/default.nix delete mode 100644 modules/wireguard/options.nix delete mode 100644 pkgs/yo.nix create mode 100644 pkgs/yo/README.md create mode 100644 pkgs/yo/default.nix create mode 100644 pkgs/yo/nixos.rb delete mode 100644 pkgs/yoshi.nix create mode 100644 pkgs/yossh/README.md create mode 100644 pkgs/yossh/default.nix create mode 100644 pkgs/yossh/doc/lua_doc.md create mode 100644 pkgs/yossh/doc/nix.md create mode 100755 pkgs/yossh/examples/home_wifi.lua create mode 100644 pkgs/yossh/examples/mega_bastion.lua create mode 100644 pkgs/yossh/examples/test.lua create mode 100644 pkgs/yossh/tests/unit_tests.lua create mode 100644 pkgs/yossh/yoshi.lua 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": [ @@ -156,22 +134,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, @@ -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 {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/yoshi.nix b/pkgs/yoshi.nix deleted file mode 100644 index 9e261df..0000000 --- a/pkgs/yoshi.nix +++ /dev/null @@ -1,52 +0,0 @@ -{ - pkgs, - inputs, - ... -}: let - yossh = - pkgs.writeText "yossh.lua" - # lua - '' - yoshi = dofile'${inputs.yoshi-lua}/yoshi.lua' - - 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", - DynamicForward = 9090, - Port = 2222 - } - else - return yoshi.ssh{ - HostName = "williamsfam.us.com", - DynamicForward = 9090 - } - end - end - - yoshi.hosts["io"] = function() - if isHome() then - return yoshi.ssh{ - HostName = "192.168.50.3", - User = "admin", - } - else - return yoshi.ssh{ - HostName = "192.168.50.3", - User = "admin", - ProxyJump = "collin@ganymede", - } - end - end - - yoshi.run(arg) - ''; -in - pkgs.writeShellScriptBin "yossh" '' - exec ${pkgs.lua}/bin/lua ${yossh} "$@" - '' 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 + yossh + yossh + yossh + +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/yossh/default.nix b/pkgs/yossh/default.nix new file mode 100644 index 0000000..4ea0127 --- /dev/null +++ b/pkgs/yossh/default.nix @@ -0,0 +1,52 @@ +{ + pkgs, + inputs, + ... +}: let + yossh = + pkgs.writeText "yossh.lua" + # lua + '' + yoshi = dofile'${./yoshi.lua}' + + 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", + DynamicForward = 9090, + Port = 2222 + } + else + return yoshi.ssh{ + HostName = "williamsfam.us.com", + DynamicForward = 9090 + } + end + end + + yoshi.hosts["io"] = function() + if isHome() then + return yoshi.ssh{ + HostName = "192.168.50.3", + User = "admin", + } + else + return yoshi.ssh{ + HostName = "192.168.50.3", + User = "admin", + ProxyJump = "collin@ganymede", + } + end + end + + yoshi.run(arg) + ''; +in + pkgs.writeShellScriptBin "yossh" '' + exec ${pkgs.lua}/bin/lua ${yossh} "$@" + '' 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 -- cgit v1.3.1