Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

modules/supervisord: init, modules/openssh: init #203

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
dc61ae1
modules/supervisord: init
zhaofengli Sep 21, 2022
3a02d09
modules/openssh: init
zhaofengli Sep 21, 2022
88b5731
fixup svd: remove cfg.configFile
zhaofengli Sep 21, 2022
a00dc02
fixup svd: mdDoc-ify everything
zhaofengli Sep 21, 2022
7834088
fixup svd: make script-generated command have normal priority
zhaofengli Sep 21, 2022
16107f5
fixup svd: add more docs
zhaofengli Sep 21, 2022
86248da
fixup svd: make environment.PATH compose better
zhaofengli Sep 21, 2022
4d93571
fixup svd: start svd during activation
zhaofengli Sep 21, 2022
3278a9c
fixup ssh: just hardcode password auth to no
zhaofengli Sep 21, 2022
269f6d1
fixup svd: use enabledPrograms for numPrograms
zhaofengli Sep 21, 2022
6e9389c
fixup svd: license header
zhaofengli Sep 21, 2022
0faa47c
fixup ssh: license header
zhaofengli Sep 21, 2022
bd44a10
fixup svd: add message when reloading
zhaofengli Sep 21, 2022
c39cb39
fixup ssh: reduce lib inherits
zhaofengli Sep 21, 2022
42b95bd
fixup svd: dont do set +e
zhaofengli Sep 21, 2022
06d245c
fixup svd: support dry-run
zhaofengli Sep 21, 2022
5ea968c
fixup ssh: don't write to stdout
zhaofengli Sep 21, 2022
f861f45
fixup svd: add autostart option
zhaofengli Sep 24, 2022
b416b58
fixup ssh: add autostart option
zhaofengli Sep 24, 2022
2f92f24
fixup ssh: also add coreutils to path
zhaofengli Sep 24, 2022
5278b3d
fixup svd: add set -e to job script
zhaofengli Sep 24, 2022
edb3247
fixup svd: only accept the typed boolean form in nix config
zhaofengli Sep 24, 2022
82c42b3
fixup svd: also accept boolean in extraConfig
zhaofengli Sep 24, 2022
56f4e44
fixup svd: disallow double quotes in environment
zhaofengli Sep 24, 2022
ca13b29
fixup ssh: Remove mdDoc for now
zhaofengli Oct 1, 2022
01a7447
fixup svd: Remove mdDoc for now
zhaofengli Oct 1, 2022
9222c03
fixup svd: use module assertions for double quote check
zhaofengli Oct 1, 2022
ba1526a
fixup svd: start supervisord in background
zhaofengli Oct 1, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions modules/environment/login/login-inner.nix
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ writeText "login-inner" ''
echo "If nothing works, open an issue at https://github.com/t184256/nix-on-droid/issues or try the rescue shell."
fi

${lib.optionalString config.supervisord.enable ''
if [ ! -e "${config.supervisord.socketPath}" ]; then
${config.supervisord.package}/bin/supervisord -c /etc/supervisord.conf || echo "Warning: supervisord failed to start"
fi
''}

${lib.optionalString config.build.initialBuild ''
if [ -e /etc/UNINTIALISED ]; then
export HOME="${config.user.home}"
Expand Down
2 changes: 2 additions & 0 deletions modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
./environment/shell.nix
./home-manager.nix
./nixpkgs/options.nix
./services/openssh.nix
./supervisord.nix
./terminal.nix
./time.nix
./upgrade.nix
Expand Down
141 changes: 141 additions & 0 deletions modules/services/openssh.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Copyright (c) 2019-2022, see AUTHORS. Licensed under MIT License, see LICENSE.

# Parts from nixpkgs/nixos/modules/services/networking/ssh/sshd.nix
# MIT Licensed. Copyright (c) 2003-2022 Eelco Dolstra and the Nixpkgs/NixOS contributors

{ pkgs, lib, config, ... }:
zhaofengli marked this conversation as resolved.
Show resolved Hide resolved
let
inherit (lib) types;

cfg = config.services.openssh;

uncheckedConf = ''
${lib.concatMapStrings (port: ''
Port ${toString port}
'') cfg.ports}
PasswordAuthentication no
${lib.flip lib.concatMapStrings cfg.hostKeys (k: ''
HostKey ${k.path}
'')}
${lib.optionalString cfg.allowSFTP ''
Subsystem sftp ${cfg.package}/libexec/sftp-server
''}
SetEnv PATH=${config.user.home}/.nix-profile/bin:/usr/bin:/bin
${cfg.extraConfig}
'';

sshdConf = pkgs.runCommand "sshd.conf-validated" {
nativeBuildInputs = [ cfg.package ];
} ''
cat >$out <<EOL
${uncheckedConf}
EOL

ssh-keygen -q -f mock-hostkey -N ""
sshd -t -f $out -h mock-hostkey
'';
in {
options = {
services.openssh = {
zhaofengli marked this conversation as resolved.
Show resolved Hide resolved
enable = lib.mkOption {
description = lib.mdDoc ''
Whether to enable the OpenSSH secure shell daemon, which
allows secure remote logins.
'';
type = types.bool;
default = false;
};
autostart = lib.mkOption {
description = ''
Whether to automatically start the OpenSSH daemon.

If false, the server has to be manually started using
`supervisorctl`.
'';
type = types.bool;
default = true;
};
package = lib.mkOption {
description = ''
The package to use for OpenSSH.
'';
type = types.package;
default = pkgs.openssh;
defaultText = lib.literalExpression "pkgs.openssh";
};
ports = lib.mkOption {
description = lib.mdDoc ''
Specifies on which ports the SSH daemon listens.
'';
type = types.listOf types.port;
default = [ 8022 ];
};
allowSFTP = lib.mkOption {
description = lib.mdDoc ''
Whether to enable the SFTP subsystem in the SSH daemon. This
enables the use of commands such as {command}`sftp` and
{command}`sshfs`.
'';
type = types.bool;
default = true;
};
hostKeys = lib.mkOption {
description = lib.mdDoc ''
Nix-on-Droid can automatically generate SSH host keys. This option
specifies the path, type and size of each key. See
{manpage}`ssh-keygen(1)` for supported types
and sizes.
'';
type = types.listOf types.attrs;
default =
[ { type = "rsa"; bits = 4096; path = "/etc/ssh/ssh_host_rsa_key"; }
{ type = "ed25519"; path = "/etc/ssh/ssh_host_ed25519_key"; }
];
example =
[ { type = "rsa"; bits = 4096; path = "/etc/ssh/ssh_host_rsa_key"; rounds = 100; openSSHFormat = true; }
{ type = "ed25519"; path = "/etc/ssh/ssh_host_ed25519_key"; rounds = 100; comment = "key comment"; }
];
};
extraConfig = lib.mkOption {
description = lib.mdDoc "Verbatim contents of {file}`sshd_config`.";
type = types.lines;
default = "";
};
};
};
config = lib.mkIf cfg.enable {
environment.etc = {
"ssh/sshd_config".source = sshdConf;
"ssh/moduli".source = "${cfg.package}/etc/ssh/moduli";
};

supervisord.programs.sshd = {
inherit (cfg) autostart;
path = [ cfg.package pkgs.coreutils ];
autoRestart = true;
script = ''
Gerschtli marked this conversation as resolved.
Show resolved Hide resolved
# don't write to stdout
exec >&2

${lib.flip lib.concatMapStrings cfg.hostKeys (k: ''
if ! [ -s "${k.path}" ]; then
if ! [ -h "${k.path}" ]; then
rm -f "${k.path}"
fi
mkdir -m 0755 -p "$(dirname '${k.path}')"
ssh-keygen \
-t "${k.type}" \
${if k ? bits then "-b ${toString k.bits}" else ""} \
${if k ? rounds then "-a ${toString k.rounds}" else ""} \
${if k ? comment then "-C '${k.comment}'" else ""} \
${if k ? openSSHFormat && k.openSSHFormat then "-o" else ""} \
-f "${k.path}" \
-N ""
fi
'')}

exec ${cfg.package}/bin/sshd -D -f /etc/ssh/sshd_config
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably add the -e flag so log output goes to stderr, which supervisord can actually pick up (unlike syslog).

'';
};
};
}
204 changes: 204 additions & 0 deletions modules/supervisord.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# Copyright (c) 2019-2022, see AUTHORS. Licensed under MIT License, see LICENSE.

{ pkgs, lib, config, ... }:
zhaofengli marked this conversation as resolved.
Show resolved Hide resolved
let
inherit (lib) types;
Gerschtli marked this conversation as resolved.
Show resolved Hide resolved

cfg = config.supervisord;

format = pkgs.formats.ini {};

programType = types.submodule ({ name, config, ... }: {
options = {
enable = lib.mkOption {
description = lib.mdDoc ''
Whether to enable this program.
'';
type = types.bool;
default = true;
};
command = lib.mkOption {
description = lib.mdDoc ''
The command that will be run as the service's main process.
'';
type = types.str;
};
script = lib.mkOption {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just thinking out loud: Does it make sense to expose script and command options like this? The command option looks to me like an unnecessary option if there is already script. Also not sure if types.lines is a good fit for script because that means that its values will be merged instead of overwritten when this option is defined multiple times.

Another idea to be more user-friendly and more error-prone (because I am sure there will be a lot of people forgetting to use exec in front of their commands): How about a preStart/initScript/.. and a command option and the module builds a script like

''
  ${cfg.preStart}
  exec ${cfg.command}
''

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

command is the direct way of specifying what to run (systemd.services.<name>.serviceConfig.ExecStart) and works fine for simple services that don't require much setup. I think there's no need to involve a script when setting command directly is sufficient (also avoid the exec pitfalls).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do think about splitting set up logic from the actual command? I like the concept of ExecPreStart and similar lifecycle hooks of systemd that we could just implement for the user.

If you don't like it, please put a note about the necessity of exec in the description of script and clarify when each of the options are recommended to use and that not both options can be set simultaneously, ideally throwing an assertion error there to avoid unexpected behaviour.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep it simple and not create illusions of lifecycle hooks that don't actually exist in supervisord. Scripts don't have to include an exec (like some kind of wait setup with custom cleanup logic) so I'm hesitant to add assertions to enforce this incorrect assumption.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But if you set e.g.

script = ''
  some-command
``;

signals like SIGTERM will not be forwarded by default to this process or is supervisord doing some magic there? These signals need to be forwarded to the actual main process.

Regarding lifecycle hooks: I agree that that this looks like an illusion but I am just thinking about improving UX through providing useful abstractions

description = lib.mdDoc ''
Shell commands executed as the service's main process.
'';
type = types.lines;
default = "";
};
path = lib.mkOption {
description = lib.mdDoc ''
Packages added to the service's PATH environment variable.
'';
type = types.listOf (types.either types.package types.str);
zhaofengli marked this conversation as resolved.
Show resolved Hide resolved
default = [];
};
autostart = lib.mkOption {
description = lib.mdDoc ''
Whether to automatically start the process.

If false, the process has to be manually started using
`supervisorctl`.
'';
type = types.bool;
default = true;
};
autoRestart = lib.mkOption {
description = lib.mdDoc ''
Whether to automatically restart the process if it exits.
zhaofengli marked this conversation as resolved.
Show resolved Hide resolved

If `unexpected`, the process will be restarted if it exits
with an exit code not listed in the programs's `exitcodes`
configuration.
'';
type = types.either types.bool (types.enum [ "unexpected" ]);
default = "unexpected";
};
environment = lib.mkOption {
description = lib.mdDoc ''
Environment variables passed to the service's process.
'';
type = types.attrsOf types.str;
default = {};
};
extraConfig = lib.mkOption {
description = lib.mdDoc ''
Extra structured configurations to add to the [program:x] section.
'';
type = types.attrsOf (types.either types.str types.bool);
default = {};
};
};
config = {
command = lib.mkIf (config.script != "")
(toString (pkgs.writeShellScript "${name}-script.sh" ''
set -e
${config.script}
''));

environment.PATH = lib.mkDefault (lib.makeBinPath config.path);
};
});

renderAtom = val:
if builtins.isBool val then if val then "true" else "false"
else toString val;

renderProgram = program: let
section = {
inherit (program) command autostart;
autorestart = program.autoRestart;
zhaofengli marked this conversation as resolved.
Show resolved Hide resolved
environment = let
# FIXME: Make more robust
escape = s:
assert lib.assertMsg (!(lib.hasInfix "\"" s)) "supervisord.programs.<name>.environment: Values cannot have double quotes at the moment (${s})";
zhaofengli marked this conversation as resolved.
Show resolved Hide resolved
builtins.replaceStrings [ "%" ] [ "%%" ] s;
envs = lib.mapAttrsToList (k: v: "${k}=\"${escape v}\"") program.environment;
in builtins.concatStringsSep "," envs;
} // program.extraConfig;
in lib.mapAttrs (_: v: renderAtom v) section;

numPrograms = builtins.length (builtins.attrNames enabledPrograms);
enabledPrograms = lib.filterAttrs (_: program: program.enable) cfg.programs;

structuredConfig = {
supervisord = {
logfile = cfg.logPath;
pidfile = cfg.pidPath;
};
supervisorctl = {
serverurl = "unix://${cfg.socketPath}";
};
unix_http_server = {
file = cfg.socketPath;
};
"rpcinterface:supervisor" = {
"supervisor.rpcinterface_factory" = "supervisor.rpcinterface:make_main_rpcinterface";
};
} // (lib.mapAttrs' (k: v: {
name = "program:${k}";
value = renderProgram v;
}) enabledPrograms);

configFile = format.generate "supervisord.conf" structuredConfig;

# Only expose the "supervisorctl" executable
zhaofengli marked this conversation as resolved.
Show resolved Hide resolved
supervisorctl = pkgs.runCommand "supervisorctl" {} ''
mkdir -p $out/bin
ln -s ${cfg.package}/bin/supervisorctl $out/bin/supervisorctl
'';
in {
options = {
supervisord = {
enable = lib.mkOption {
description = lib.mdDoc ''
Whether to enable the supervisord process control system.

This allows you to define long-running services in Nix-on-Droid.
'';
type = types.bool;
default = numPrograms != 0;
Gerschtli marked this conversation as resolved.
Show resolved Hide resolved
};
package = lib.mkOption {
description = lib.mdDoc ''
The supervisord package to use.
'';
type = types.package;
default = pkgs.python3Packages.supervisor;
defaultText = lib.literalExpression "pkgs.python3Packages.supervisor";
};
socketPath = lib.mkOption {
description = lib.mdDoc ''
Path to the UNIX domain socket on which supervisord will listen on.
'';
type = types.path;
default = "/tmp/supervisor.sock";
};
pidPath = lib.mkOption {
description = lib.mdDoc ''
Path to the file in which supervisord saves its PID.
'';
type = types.path;
default = "/tmp/supervisor.pid";
};
logPath = lib.mkOption {
description = ''
Path to the log file.
'';
type = types.path;
default = "/tmp/supervisor.log";
};
programs = lib.mkOption {
description = lib.mdDoc ''
Definition of supervisord programs.

Upstream documentations are available at <http://supervisord.org/configuration.html#program-x-section-settings>.
'';
type = types.attrsOf programType;
default = {};
};
};
};

config = lib.mkIf cfg.enable {
environment.etc."supervisord.conf" = {
source = configFile;
};

environment.packages = [ supervisorctl ];

build.activationAfter.reloadSupervisord = ''
if [ ! -e "${config.supervisord.socketPath}" ]; then
echo "Starting supervisord..."
$DRY_RUN_CMD ${cfg.package}/bin/supervisord -c /etc/supervisord.conf
else
echo "Reloading supervisord..."
$DRY_RUN_CMD ${cfg.package}/bin/supervisorctl -c /etc/supervisord.conf update
fi
'';
};
}