diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..38d03ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.* +\#* +*~ +result +!.gitignore diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..edd2349 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2022 James Andariese + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..51dff63 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# `cascade` + +The next iteration of the cascade network. + +Should it have been thunder? + +Probably. + +It's cascade anyway. + +## Usage + +From a [Proxmox VE][PVE] machine with [Nix][NIX] installed: + +```bash +nix-shell +``` + +Guidance will be printed which should be followed. + +## Prerequisites + +In order to discover your network and configure hosts, this configuration management system requires the following: +* Samba DC with functioning DNS and DHCP server. +* You must be able to auth with Kerberos to Samba. +* Proxmox running on the local host +* Proxmox should have a local filesystem called hdd-fs + + +NIX: https://www.nixos.org +PVE: https://www.proxmox.com/en/proxmox-ve diff --git a/common/bash-is-needed.nix b/common/bash-is-needed.nix new file mode 100644 index 0000000..a61f779 --- /dev/null +++ b/common/bash-is-needed.nix @@ -0,0 +1,37 @@ +{ config, lib, pkgs, ... }: + +with lib; +{ + options = { + environment.binbash.enable = mkOption { + default = true; + type = types.bool; + description = '' + Include a /bin/bash in the system. + ''; + }; + environment.binbash.bash = mkOption { + type = types.package; + default = pkgs.bashInteractive; + defaultText = literalExpression "pkgs.bashInteractive"; + example = literalExpression "pkgs.bash"; + description = lib.mdDoc '' + The bash implementation that will be present in + `/bin/bash` after enabling this option. + ''; + }; + }; + + config = { + system.activationScripts.binbash = if config.environment.binbash.enable + then '' + mkdir -m 0755 -p /bin + ln -sfn ${config.environment.binbash.bash}/bin/bash /bin/.bash.tmp + mv /bin/.bash.tmp /bin/bash # atomically replace /usr/bin/env + '' + else '' + rm -f /bin/bash + ''; + }; +} + diff --git a/common/cascade-networking.nix b/common/cascade-networking.nix new file mode 100644 index 0000000..25e88ca --- /dev/null +++ b/common/cascade-networking.nix @@ -0,0 +1,21 @@ +{config, lib, ...}: { + +options = with lib; with types; { + cascade.bridge-interface = mkOption { + type = str; + description = "interface on which to create primary bridge (br0)"; + }; +}; + +config = with lib; { + networking = { + useNetworkd = mkForce true; + + bridges.br0.interfaces = [config.cascade.bridge-interface]; + interfaces.br0.useDHCP = mkImageMediaOverride true; + }; + #systemd.network.links."05-br0".matchConfig.Name = "br0"; + #systemd.network.links."05-br0".linkConfig.MACAddressPolicy = "none"; +}; + +} diff --git a/common/default.nix b/common/default.nix new file mode 100644 index 0000000..f816717 --- /dev/null +++ b/common/default.nix @@ -0,0 +1,11 @@ +let _ = builtins.trace "${toString ./.}/default.nix"; in +{...}: +with builtins; +with import (toString ../functions); + +let folder = import-folder {path = "${toString ./.}"; filenameMatch = ".*[.]nix"; filenameBadMatch = ".*_.*";}; +goodNames = attrNames folder; +goodPaths = map (n: "${toString ./.}/${n}.nix") goodNames; +in + +{ imports = goodPaths; } diff --git a/common/ssh.nix b/common/ssh.nix new file mode 100644 index 0000000..947c277 --- /dev/null +++ b/common/ssh.nix @@ -0,0 +1,7 @@ +{ pkgs, config, ... }: { + +config.programs.ssh.package = pkgs.openssh_gssapi; +config.services.openssh.enable = true; +config.networking.firewall.allowedTCPPorts = [ 22 ]; + +} diff --git a/common/users.nix b/common/users.nix new file mode 100644 index 0000000..d683408 --- /dev/null +++ b/common/users.nix @@ -0,0 +1,24 @@ +{pkgs, ...}: { + config = { + users.users = rec { + james = { + createHome = true; + description = "James Andariese"; + extraGroups = [ "wheel" ]; + group = "users"; + home = "/home/james"; + shell = pkgs.bashInteractive; + uid = 1982; + isNormalUser = true; + openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBDEj6S+ISygrn6D7a5GBsrYaUMWjcReyMmrlgRdDUGx james@chimecho" + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBW+t2we/HTgV9ykgcQWiHqiA+vEehVhwOcbsLr4jJvL james@Jamess-MBP.cascade.strudelline.net" + ]; + }; + root.shell = james.shell; + root.openssh.authorizedKeys.keys = james.openssh.authorizedKeys.keys; + }; + security.sudo.wheelNeedsPassword = false; + nix.settings.trusted-users = [ "root" "@wheel" ]; + }; +} diff --git a/custom-image-configuration.nix b/custom-image-configuration.nix new file mode 100644 index 0000000..44876b3 --- /dev/null +++ b/custom-image-configuration.nix @@ -0,0 +1,20 @@ +{ config, lib, pkgs, ... }: +{ + imports = [ + ./common + ]; + + config = { + system.stateVersion = "22.11"; + + nix.nixPath = [ + "nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixos" + "nixos-config=/etc/nixos/configuration.nix" + "hardware-config=/etc/nixos/hardware-configuration.nix" + "/nix/var/nix/profiles/per-user/root/channels" + ]; + + } + # also pull in all of the qemu-vm profile which will be the basis of the live config + // (builtins.removeAttrs (import ./profiles/qemu-vm {inherit lib;}) [ "deployment" ]); +} diff --git a/custom-image.nix b/custom-image.nix new file mode 100644 index 0000000..15a9342 --- /dev/null +++ b/custom-image.nix @@ -0,0 +1,21 @@ +{}: +let + myisoconfig = import ./custom-image-configuration.nix; + + copyChannel = true; + system = "x86_64-linux"; + evalNixos = configuration: import { + inherit system configuration; + }; + resultOfEval = (evalNixos myisoconfig); + lib = resultOfEval.pkgs.lib; + pkgs = resultOfEval.pkgs; + config = resultOfEval.config; +in +import { + inherit lib copyChannel pkgs config; + diskSize = "20480"; + format = "raw"; + installBootLoader = true; + partitionTableType = "hybrid"; +} diff --git a/functions/default.nix b/functions/default.nix new file mode 100644 index 0000000..a94bd83 --- /dev/null +++ b/functions/default.nix @@ -0,0 +1,5 @@ +let _ = builtins.trace "${toString ./.}/default.nix"; in + +let import-folder = import (toString ./import-folder.nix); +in + import-folder {path = (toString ./.);} diff --git a/functions/import-folder.nix b/functions/import-folder.nix new file mode 100644 index 0000000..9c4d8e2 --- /dev/null +++ b/functions/import-folder.nix @@ -0,0 +1,17 @@ +with builtins; +let mkIsFilenameAMatch = {filenameMatch ? "[^_].*", filenameBadMatch ? null, ...}@opt: + {name, type, ...}: + if name == "default.nix" then false + else if builtins.match filenameMatch name == null then false + else if filenameBadMatch != null && builtins.match filenameBadMatch name != null then false + else if ! elem type ["symlink" "regular"] then false + else if builtins.match ".*[.]nix" name == null then false + else true; + readDirItems = import ./readDirItems.nix; + extractName = fn: let m = match "(.*[/])?([a-zA-Z0-9-]+)[.]nix" fn; in if m == null then throw "${fn} does not seem to have a correct filename" else head (tail m); + matches = {path, ...}@opt: + let isFilenameAMatch = mkIsFilenameAMatch opt; + in + listToAttrs (map ({name,...}: let pname = name; in {name = "${extractName name}"; value = import "${path}/${name}";}) (filter isFilenameAMatch (readDirItems "${path}" ))); +in + matches diff --git a/functions/mkAttrItemsFunction.nix b/functions/mkAttrItemsFunction.nix new file mode 100644 index 0000000..afea593 --- /dev/null +++ b/functions/mkAttrItemsFunction.nix @@ -0,0 +1,3 @@ +with builtins; + +kname: vname: aset: attrValues (mapAttrs (k: v: {"${kname}" = k; "${vname}" = v;}) aset) diff --git a/functions/readDirItems.nix b/functions/readDirItems.nix new file mode 100644 index 0000000..1a7390d --- /dev/null +++ b/functions/readDirItems.nix @@ -0,0 +1,3 @@ +with builtins; + +p: (import ./mkAttrItemsFunction.nix) "name" "type" (readDir p) diff --git a/hosts/_basic.nix b/hosts/_basic.nix new file mode 100644 index 0000000..1aa2571 --- /dev/null +++ b/hosts/_basic.nix @@ -0,0 +1,8 @@ +{ nodes, config, pkgs, ... }: { + imports = [ + (toString ../profiles/qemu-vm) + ]; + config = { + deployment.tags = [ ]; + }; +} diff --git a/hosts/default.nix b/hosts/default.nix new file mode 100644 index 0000000..e439f1a --- /dev/null +++ b/hosts/default.nix @@ -0,0 +1,5 @@ +let _ = builtins.trace "${toString ./.}/default.nix"; in +with builtins; +with import (toString ../functions); + +import-folder {path = "${toString ./.}"; filenameMatch = ".*[.]nix"; filenameBadMatch = ".*_.*";} diff --git a/network.nix b/network.nix new file mode 100644 index 0000000..eee1a40 --- /dev/null +++ b/network.nix @@ -0,0 +1,13 @@ +with builtins; + +let pkgs = import {}; + network = { + inherit pkgs; + description = "cascade"; + }; + hosts = import (toString ./hosts); +in + +hosts // { + inherit network; +} diff --git a/profiles/api/default.nix b/profiles/api/default.nix new file mode 100644 index 0000000..7f631a1 --- /dev/null +++ b/profiles/api/default.nix @@ -0,0 +1,48 @@ +# Do not modify this file! It was generated by ‘nixos-generate-config’ +# and may be overwritten by future invocations. Please make changes +# to /etc/nixos/configuration.nix instead. +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = [ (toString ../base) ]; + + config = { + deployment.targetUser = "root"; + boot.initrd.availableKernelModules = [ "xhci_pci" "dwc3_pci" "usbhid" "usb_storage" "uas" "sd_mod" "sdhci_acpi" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-intel" ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = + { device = "/dev/mmcblk1p1"; + fsType = "xfs"; + }; + + fileSystems."/boot" = + { device = "/dev/mmcblk1p3"; + fsType = "vfat"; + }; + + swapDevices = + [ { device = "/dev/mmcblk1p2"; } + ]; + + # Enables DHCP on each ethernet and wireless interface. In case of scripted networking + # (the default) this is the recommended approach. When using systemd-networkd it's + # still possible to use this option, but it's recommended to use it in conjunction + # with explicit per-interface declarations with `networking.interfaces..useDHCP`. + networking.interfaces.wlp0s20u3.useDHCP = lib.mkDefault false; + + hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; + # high-resolution display + hardware.video.hidpi.enable = lib.mkDefault true; + + # Use the systemd-boot EFI boot loader. + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + + #networking.meth.meth0.replaceInterface = "enp1s0"; + # + cascade.bridge-interface = "enp1s0"; + }; +} diff --git a/profiles/base/default.nix b/profiles/base/default.nix new file mode 100644 index 0000000..03e1c1f --- /dev/null +++ b/profiles/base/default.nix @@ -0,0 +1,14 @@ +{pkgs,...}: +{ + imports = [ + (toString ../../common) + ]; + config = { + environment.systemPackages = with pkgs; [ bridge-utils ]; + programs.neovim.enable = true; + programs.neovim.vimAlias = true; + programs.neovim.viAlias = true; + + system.stateVersion = "22.11"; + }; +} diff --git a/profiles/qemu-vm/default.nix b/profiles/qemu-vm/default.nix new file mode 100644 index 0000000..22580d6 --- /dev/null +++ b/profiles/qemu-vm/default.nix @@ -0,0 +1,51 @@ +{lib, ...}: + +with lib; +{ + imports = [ (toString ../base) ]; + + config = { + deployment.targetUser = "root"; + fileSystems."/" = { + device = "/dev/disk/by-label/nixos"; + fsType = "ext4"; + autoResize = true; + }; + fileSystems."/boot" = { + device = "/dev/disk/by-label/ESP"; + fsType = "vfat"; + }; + swapDevices = [ + {device = "/swap"; size = 1024;} # make sure we always have enough memory to rebuild nixos. + ]; + + # boot.initrd.network.enable = true; + # networking.useDHCP = true; + + networking.useNetworkd = mkForce true; + networking.networkmanager.enable = false; + #networking.interfaces.ens18.useDHCP = false; + #networking.bridges.br0.interfaces = [ "ens18" ]; + #networking.interfaces.br0.useDHCP = lib.mkDefault true; + + # hardware.cpu.amd.updateMicrocode = lib.config.hardware.enableRedistributableFirmware; + boot.initrd.availableKernelModules = [ "virtio_net" "virtio_pci" "virtio_mmio" "virtio_blk" "virtio_scsi" "9p" "9pnet_virtio" ]; + boot.initrd.kernelModules = [ "virtio_balloon" "virtio_console" "virtio_rng" ]; + + boot.growPartition = true; + boot.kernelParams = [ "console=ttyS0" ]; + boot.loader.grub.device = "/dev/vda"; + boot.loader.timeout = mkDefault 3; + boot.consoleLogLevel = 3; + boot.initrd.verbose = true; + + services.getty.autologinUser = "root"; + + services.sshd.enable = true; + networking.firewall.allowedTCPPorts = [ 22 ]; + + services.qemuGuest.enable = true; + + cascade.bridge-interface = "ens18"; + }; +} diff --git a/shell.nix b/shell.nix new file mode 100755 index 0000000..b050109 --- /dev/null +++ b/shell.nix @@ -0,0 +1,227 @@ +{ pkgs ? import {}, ... }: + +let + vault_addr = "http://vault:8200"; +in + +with pkgs; +with lib; + +stdenv.mkDerivation { + name = "commands-nix"; + + buildInputs = [ terraform vault coreutils-full dig bash samba4Full morph ]; + + shellHook = '' + export NIX_PATH="nixpkgs=${toString }" + export LD_LIBRARY_PATH="${libvirt}/lib:$LD_LIBRARY_PATH" + export VAULT_ADDR=${escapeShellArg vault_addr} + export DOMAIN="$(hostname -d)" + + +MEMORY=2048 +name="$1" + +rebuild-nixos-image() { + echo "$(nix-build \ + -I nixos-config=custom-image-configuration.nix \ + -I hardware-config=profiles/qemu-vm/default.nix \ + custom-image.nix)/nixos".img +} + +mkvirt() { + eval "$(env_cascade)" || return 3 + eval "$(env_name "$1")" || return 4 + + if [ -f "./hosts/$name"".nix" ];then + 1>&2 echo "there is already a file at ./hosts/$name"".nix. move it or remove it." + return 1 + fi + cp "./hosts/_basic.nix" "./hosts/""$name"".nix" + morph build network.nix --on="$name" & + + [ -f "result/nixos.img" ] || ( + 1>&2 echo "you do not seem to have a result/nixos.img file. building one now." + rebuild-nixos-image || errcho "could not build result/nixos.img." || return $? + ) + [ -f "result/nixos.img" ] || errcho "there must be a file in result/nixos.img. make one with rebuild-nixos-image." || return $? + IMG="$(realpath result/nixos.img)" + [ -f "$IMG" ] || errcho "result/nixos.img must be resolveable to a file. rebuild the image link with rebuild-nixos-image." || return $? + + [ $? -eq 0 ] || return 5 + VMID=$(sudo pvesh get /cluster/nextid) + [ $? -eq 0 ] || return 6 + sudo qm create $VMID \ + --memory "$MEMORY" \ + --net0 virtio,bridge=vmbr0 \ + --ipconfig0 ip=dhcp \ + --agent enabled=1,type=virtio \ + --virtio0 hdd-fs:0,import-from="$IMG",discard=on,format=raw \ + --boot c \ + --bootdisk virtio0 \ + --vga serial0 \ + --serial0 socket \ + --name "$name" \ + --start 1 + [ $? -eq 0 ] || return 7 + + IP="$(wait-for-vm-ipv4 $VMID)" + [ $? -eq 0 ] || return 8 + samba-create-ipv4-records "$IP" "$name" "$DOMAIN" + [ $? -eq 0 ] || return 9 + 1>&2 echo "removing any stale ssh keys" + ssh-keygen -f "/home/james/.ssh/known_hosts" -R "$name" + ssh-keygen -f "/home/james/.ssh/known_hosts" -R "$name.$DOMAIN" + ssh-keygen -f "/home/james/.ssh/known_hosts" -R "$IP" + 1>&2 echo "scanning for new ssh keys" + # head -1 allows us to grab the ed25519 key before the rsa key. + # at worst, it will yield a single key of some sort which is also fine. + ( + echo + ssh-keyscan "$IP" | sort | head -1 + echo + ssh-keyscan "$name" | sort | head -1 + echo + ssh-keyscan "$name.$DOMAIN" | sort | head -1 + echo + ) 2> /dev/null >> ~/.ssh/known_hosts + + 1>&2 echo "waiting for morph build to finish" + wait + 1>&2 echo "morphing host" + morph deploy network.nix switch --on="$name" +} + +samba-create-ipv4-records() { + eval "$(env_cascade)" + IP="$1" + HOST="$2" + :;:;:;:;:;:;:;:;: ;_IP_R="$IP" + IP1="''${_IP_R%%.*}";_IP_R="''${_IP_R#*.}" + IP2="''${_IP_R%%.*}";_IP_R="''${_IP_R#*.}" + IP3="''${_IP_R%%.*}";_IP_R="''${_IP_R#*.}" + IP4="''${_IP_R}" ;_IP_R="''${_IP_R#*.}" + # at the end, the last fragment has no . so if we try to remove another dotted-quad/4, + # we simply get no changes. the _desired end state_ is to have _IP_R = IP4. + [ x"$IP4" = x"$_IP_R" ] || errcho "error parsing IPv4" + + samba-tool dns cleanup -k yes "$DC" "$HOST"."$DOMAIN" + samba-tool dns add -k yes "$DC" "$DOMAIN" "$HOST" A "$IP" + samba-tool dns zonecreate "$DC" "$IP3.$IP2.$IP1.in-addr.arpa." -k yes || true + samba-tool dns add -k yes "$DC" "$IP3.$IP2.$IP1.in-addr.arpa." "$IP4.$IP3.$IP2.$IP1.in-addr.arpa." PTR "$HOST.$DOMAIN." +} + +wait-for-vm-ipv4() { +set -x + VMID="$1" + while true;do + IP="$( sudo qm guest cmd $VMID network-get-interfaces | \ + jq -r ' + .[] + |select(.["hardware-address"] != "00:00:00:00:00:00") + |.["ip-addresses"] | select(.) | .[] + |select(.["ip-address-type"] == "ipv4") + |.["ip-address"]' \ + 2> /dev/null \ + | grep -E '172|10|192' || true)" + if [ x != x"$IP" ];then + echo "$IP" + break + fi + sleep 1 + done + set +x +} + +env_cascade() { + DOMAIN="$(hostname -d)" + #calculate some other variables + REALM="$(echo "$DOMAIN" | tr a-z A-Z)" + WORKGROUP="$(echo "$DOMAIN" | cut -d . -f 1)" + + #and recalculate others to fix caps + DOMAIN="$(echo "$REALM" | tr A-Z a-z)" + DC="$(dig +short -x "$(dig +short "$DOMAIN")")" + DC="''${DC%.}" # just in case, let's drop the absolution dot + workgroup="$(echo "$WORKGROUP" | tr A-Z a-z)" + WORKGROUP="$(echo "$workgroup" | tr a-z A-Z)" + printf '%q=%q\n' \ + DOMAIN "$DOMAIN" \ + REALM "$REALM" \ + WORKGROUP "$WORKGROUP" \ + DC "$DC" \ + workgroup "$workgroup" +} + +env_name() { + # protect thy stdout + ( + exec 18>&1- + exec 1>&2 + NAME="$1" + [ x"$NAME" != x ] || errcho "you need to provide a hostname" + + exec 1>&18 + printf '%q=%q\n' name "$(echo "$NAME" | tr A-Z a-z)" + printf '%q=%q\n' NAME "$(echo "$NAME" | tr a-z A-Z)" + ) +} + +errcho() { + RC=$? + if [ $RC -ne 0 ];then + 1>&2 printf '%s\n' "$@" + return $RC + fi +} + +guestbash() { + VMID="$1" + CMD="$2" + shift;shift + echo -n "$CMD" | sudo qm guest exec --pass-stdin=1 --timeout=300 "$VMID" -- /run/wrappers/bin/sudo -i bash -s "$@" +} + +deploy() { + nix-build network.nix && morph build network.nix && morph deploy network.nix "$@" +} + +destroy-host() { + while [ $# -gt 0 ];do + + + VMID="$(sudo qm list | awk -v N="$1" '$2 == N {print $1}')" + + echo "destroying hosts/$1.nix and VM #$VMID" 1>&2 + sleep 1 + + sudo qm stop $VMID + sudo qm destroy $VMID + rm -f hosts/"$1".nix + + shift + done +} + +cat << EOF +Welcome! You've entered the cascade shell. + +You may make a virt on the local proxmox host using: + + mkvirt hostname-here + +You may also register an existing host in DNS using: + + samba-create-ipv4-records IP hostname-here + +Then morph may be used to deploy: + + # Create a hosts/hostname-here.nix file based on hosts/basic.nix.sample + + deploy test # this is an alias which runs morph on network.nix. + deploy switch # the argument is the same as to morph deploy and nixos-rebuild +EOF + + ''; +} +