Skip to content

Commit

Permalink
feature: Support sudo & SSH connection options
Browse files Browse the repository at this point in the history
These changes (based on @arouzing's initial work, thank you!) introduce
configuration options for SSH connections and add support for using sudo.
  • Loading branch information
cmacrae committed Jan 4, 2023
1 parent d5706df commit 78b920b
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 97 deletions.
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
};
```

Expand All @@ -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
Expand Down
191 changes: 107 additions & 84 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -85,83 +69,122 @@
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
''
{{.REMOTE_COMMAND}} {{.REMOTE_OPTS}} {{.REMOTE_USER}}@{{.REMOTE_HOST}} \
'${optionalString useSudo "{{.REMOTE_SUDO_COMMAND}} {{.REMOTE_SUDO_OPTS}} "} install -d -m 077 "$(dirname ${path})"'
''

# Copy file
''
${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
''
{{.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}}
''
# --rsync-path="${optionalString useSudo "{{.REMOTE_SUDO_COMMAND}} {{.REMOTE_SUDO_OPTS}}"} mkdir -p {{.REMOTE_CONFIG_DIR}} \
];
};
};
};
});

# Taskfile passed to go-task
Expand Down
57 changes: 47 additions & 10 deletions module.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<config.networking.hostName>";
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 ];
};
}

0 comments on commit 78b920b

Please sign in to comment.