From 05ed7ba128a2643e25e68229361efc4ab01ac492 Mon Sep 17 00:00:00 2001 From: Calum MacRae Date: Wed, 4 Jan 2023 14:20:42 +0000 Subject: [PATCH] feature: Support `sudo` & SSH connection options These changes (based on @arouzing's initial work, thank you!) introduce configuration options for SSH connections and add support for using sudo. --- README.md | 16 ++++- flake.nix | 190 ++++++++++++++++++++++++++++++----------------------- module.nix | 57 +++++++++++++--- 3 files changed, 166 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index cf893bd..bd0eaa1 100644 --- a/README.md +++ b/README.md @@ -198,9 +198,16 @@ lollypops.deployment = { # Where on the remote the configuration (system flake) is placed config-dir = "/var/src/lollypops"; - # Ssh connection parameters - host = "${config.networking.hostName}"; - user = "root"; + # SSH connection parameters + ssh.host = "${config.networking.hostName}"; + ssh.user = "root"; + ssh.command = "ssh"; + ssh.opts = []; + + # sudo options + sudo.enable = false; + sudo.command = "sudo"; + sudo.opts = []; }; ``` @@ -214,6 +221,9 @@ be missing still on the first deployment. To fix this, either add it to your $PATH on the remote side or do your first deployment with `lollypops.deployment.local-evaluation` set to `true`. +**Note:** If your flake includes remote Git repositories in its inputs, `git` is +required to be installed on the remote host. + ### Secrets Secrets are specified as attribute set under `lollypops.secrets.files`. All diff --git a/flake.nix b/flake.nix index 2dc5d3f..ab5a84b 100644 --- a/flake.nix +++ b/flake.nix @@ -55,23 +55,7 @@ ]) homeUsers)); - mkSeclist = config: pkgs.lib.lists.flatten (map - (x: [ - "echo 'Deploying ${x.name} to ${pkgs.lib.escapeShellArg x.path}'" - # Create parent directory if it does not exist - '' - set -o pipefail -e; ssh {{.REMOTE_USER}}@{{.REMOTE_HOST}} 'umask 077; mkdir -p "$(dirname ${pkgs.lib.escapeShellArg x.path})"' - '' - # Copy file - '' - set -o pipefail -e; ${x.cmd} | ssh {{.REMOTE_USER}}@{{.REMOTE_HOST}} "umask 077; cat > ${pkgs.lib.escapeShellArg x.path}" - '' - # # Set group and owner - '' - set -o pipefail -e; ssh {{.REMOTE_USER}}@{{.REMOTE_HOST}} "chown ${x.owner}:${x.group-name} ${pkgs.lib.escapeShellArg x.path}" - '' - ]) - (builtins.attrValues config.lollypops.secrets.files)); + in { @@ -85,83 +69,121 @@ output = "prefixed"; vars = with hostConfig.config.lollypops; { - REMOTE_USER = deployment.user; - REMOTE_HOST = deployment.host; + REMOTE_USER = deployment.ssh.user; + REMOTE_HOST = deployment.ssh.host; + REMOTE_COMMAND = deployment.ssh.command; + REMOTE_SSH_OPTS = pkgs.lib.concatStrings deployment.ssh.opts; + REMOTE_SUDO_COMMAND = deployment.sudo.command; + REMOTE_SUDO_OPTS = pkgs.lib.concatStrings deployment.sudo.opts; REBUILD_ACTION = ''{{default "switch" .REBUILD_ACTION}}''; REMOTE_CONFIG_DIR = deployment.config-dir; LOCAL_FLAKE_SOURCE = configFlake; HOSTNAME = hostName; }; - tasks = { - - check-vars.preconditions = [{ - sh = ''[ ! -z "{{.HOSTNAME}}" ]''; - msg = "HOSTNAME not set: {{.HOSTNAME}}"; - }]; - - deploy-secrets = { - deps = [ "check-vars" ]; - - desc = "Deploy secrets to: ${hostName}"; - - cmds = [ - ''echo "Deploying secrets to: {{.HOSTNAME}}"'' - ] - ++ mkSeclist hostConfig.config - ++ (if builtins.hasAttr "home-manager" hostConfig.config then - mkSeclistUser hostConfig.config.home-manager.users else [ ]); - }; - - rebuild = { - dir = self; - - desc = "Rebuild configuration of: ${hostName}"; - deps = [ "check-vars" ]; - cmds = [ - ''echo "Rebuilding: {{.HOSTNAME}}"'' - # For dry-running use `nixos-rebuild dry-activate` - ( - if hostConfig.config.lollypops.deployment.local-evaluation then + tasks = + let + useSudo = hostConfig.config.lollypops.deployment.sudo.enable; + in + with pkgs.lib; { + + check-vars.preconditions = [{ + sh = ''[ ! -z "{{.HOSTNAME}}" ]''; + msg = "HOSTNAME not set: {{.HOSTNAME}}"; + }]; + + deploy-secrets = + let mkSeclist = config: lists.flatten (map + (x: + let + path = escapeShellArg x.path; + in + [ + "echo 'Deploying ${x.name} to ${path}'" + + # Create parent directory if it does not exist + '' + set -o pipefail -e; {{.REMOTE_COMMAND}} {{.REMOTE_OPTS}} {{.REMOTE_USER}}@{{.REMOTE_HOST}} \ + '${optionalString useSudo "{{.REMOTE_SUDO_COMMAND}} {{.REMOTE_SUDO_OPTS}} "} install -d -m 077 "$(dirname ${path})"' + '' + + # Copy file + '' + set -o pipefail -e; ${x.cmd} | {{.REMOTE_COMMAND}} {{.REMOTE_OPTS}} {{.REMOTE_USER}}@{{.REMOTE_HOST}} \ + "${optionalString useSudo "{{.REMOTE_SUDO_COMMAND}} {{.REMOTE_SUDO_OPTS}}"} \ + install -m 077 /dev/null ${path}; \ + ${optionalString useSudo "{{.REMOTE_SUDO_COMMAND}} {{.REMOTE_SUDO_OPTS}}"} \ + cat > ${path}" + '' + + # Set group and owner + '' + set -o pipefail -e; {{.REMOTE_COMMAND}} {{.REMOTE_OPTS}} {{.REMOTE_USER}}@{{.REMOTE_HOST}} \ + "${optionalString useSudo "{{.REMOTE_SUDO_COMMAND}} {{.REMOTE_SUDO_OPTS}}"} \ + chown ${x.owner}:${x.group-name} ${path}" + '' + ]) + (builtins.attrValues config.lollypops.secrets.files)); in + { + deps = [ "check-vars" ]; + + desc = "Deploy secrets to: ${hostName}"; + + cmds = [ + ''echo "Deploying secrets to: {{.HOSTNAME}}"'' + ] + ++ mkSeclist hostConfig.config + ++ (if builtins.hasAttr "home-manager" hostConfig.config then + mkSeclistUser hostConfig.config.home-manager.users else [ ]); + }; + + rebuild = { + dir = self; + + desc = "Rebuild configuration of: ${hostName}"; + deps = [ "check-vars" ]; + cmds = [ + (if hostConfig.config.lollypops.deployment.local-evaluation then '' - nixos-rebuild {{.REBUILD_ACTION}} --flake '{{.REMOTE_CONFIG_DIR}}#{{.HOSTNAME}}' \ + ${optionalString useSudo ''NIX_SSHOPTS="{{.REMOTE_SSH_OPTS}}"''} nixos-rebuild {{.REBUILD_ACTION}} \ + --flake '{{.LOCAL_CONFIG_DIR}}#{{.HOSTNAME}}' \ --target-host {{.REMOTE_USER}}@{{.REMOTE_HOST}} \ - --build-host root@{{.REMOTE_HOST}} - '' - else - '' - ssh {{.REMOTE_USER}}@{{.REMOTE_HOST}} "nixos-rebuild {{.REBUILD_ACTION}} --flake '{{.REMOTE_CONFIG_DIR}}#{{.HOSTNAME}}'" - '' - ) - ]; - }; + ${optionalString useSudo "--use-remote-sudo"} + '' else '' + {{.REMOTE_COMMAND}} {{.REMOTE_OPTS}} {{.REMOTE_USER}}@{{.REMOTE_HOST}} \ + "${optionalString useSudo "{{.REMOTE_SUDO_COMMAND}} {{.REMOTE_SUDO_OPTS}}"} nixos-rebuild {{.REBUILD_ACTION}} \ + --flake '{{.REMOTE_CONFIG_DIR}}#{{.HOSTNAME}}'" + '') + ]; + }; - deploy-flake = { - - deps = [ "check-vars" ]; - desc = "Deploy flake repository to: ${hostName}"; - cmds = [ - ''echo "Deploying flake to: {{.HOSTNAME}}"'' - '' - source_path={{.LOCAL_FLAKE_SOURCE}} - if test -d "$source_path"; then - source_path=$source_path/ - fi - ${pkgs.rsync}/bin/rsync \ - --checksum \ - --verbose \ - -e ssh\ -l\ {{.REMOTE_USER}}\ -T \ - -FD \ - --times \ - --perms \ - --recursive \ - --links \ - --delete-excluded \ - $source_path {{.REMOTE_USER}}\@{{.REMOTE_HOST}}:{{.REMOTE_CONFIG_DIR}} - '' - ]; + deploy-flake = { + + deps = [ "check-vars" ]; + desc = "Deploy flake repository to: ${hostName}"; + cmds = [ + ''echo "Deploying flake to: {{.HOSTNAME}}"'' + '' + source_path={{.LOCAL_FLAKE_SOURCE}} + if test -d "$source_path"; then + source_path=$source_path/ + fi + ${pkgs.rsync}/bin/rsync \ + --verbose \ + -e {{.REMOTE_COMMAND}}\ -l\ {{.REMOTE_USER}}\ -T \ + -FD \ + --times \ + --perms \ + --recursive \ + --links \ + --delete-excluded \ + --mkpath \ + ${optionalString useSudo ''--rsync-path="{{.REMOTE_SUDO_COMMAND}} {{.REMOTE_SUDO_OPTS}} rsync" \''} + $source_path {{.REMOTE_USER}}\@{{.REMOTE_HOST}}:{{.REMOTE_CONFIG_DIR}} + '' + ]; + }; }; - }; }); # Taskfile passed to go-task diff --git a/module.nix b/module.nix index 14fe986..eab81db 100644 --- a/module.nix +++ b/module.nix @@ -95,22 +95,59 @@ in description = "Path to place the configuration on the remote host"; }; - host = mkOption { - type = types.str; - default = "${config.networking.hostName}"; - description = "Host to deploy to"; - defaultText = ""; + sudo = { + + enable = mkOption { + type = types.bool; + default = false; + description = "Enables the use of sudo for deployment on remote servers"; + }; + + command = mkOption { + type = types.str; + default = "sudo"; + description = "Command to run for permission elevation"; + }; + + opts = mkOption { + type = types.listOf types.str; + default = [ "" ]; + example = [ "--user=user" ]; + description = "Options to pass to the configured sudo command"; + }; }; - user = mkOption { - type = types.str; - default = "root"; - description = "User to deploy as"; + ssh = { + + command = mkOption { + type = types.str; + default = "ssh"; + description = "Command to run for connection to another server"; + }; + + opts = mkOption { + type = types.listOf types.str; + default = [ "" ]; + example = [ "-A" ]; + description = "Options to pass to the configured SSH command"; + }; + + host = mkOption { + type = types.str; + default = "${config.networking.hostName}"; + description = "Host to deploy to"; + }; + + user = mkOption { + type = types.str; + default = "root"; + description = "User to deploy as"; + }; }; }; }; config = { - environment.systemPackages = with pkgs; [ rsync ]; + environment.systemPackages = with pkgs; [ rsync ]; }; }