nixos/radicale: add settings option

The radicale version is no longer chosen automatically based on
system.stateVersion because that gave the impression that old versions
are still supported.
This commit is contained in:
Robert Schütz 2021-04-23 20:23:24 +02:00
parent 870b3a3054
commit 022c5b0922
3 changed files with 195 additions and 161 deletions

View file

@ -715,6 +715,13 @@ environment.systemPackages = [
The <package>yadm</package> dotfile manager has been updated from 2.x to 3.x, which has new (XDG) default locations for some data/state files. Most yadm commands will fail and print a legacy path warning (which describes how to upgrade/migrate your repository). If you have scripts, daemons, scheduled jobs, shell profiles, etc. that invoke yadm, expect them to fail or misbehave until you perform this migration and prepare accordingly. The <package>yadm</package> dotfile manager has been updated from 2.x to 3.x, which has new (XDG) default locations for some data/state files. Most yadm commands will fail and print a legacy path warning (which describes how to upgrade/migrate your repository). If you have scripts, daemons, scheduled jobs, shell profiles, etc. that invoke yadm, expect them to fail or misbehave until you perform this migration and prepare accordingly.
</para> </para>
</listitem> </listitem>
<listitem>
<para>
Instead of determining <option>services.radicale.package</option>
automatically based on <option>system.stateVersion</option>, the latest
version is always used because old versions are not officially supported.
</para>
</listitem>
</itemizedlist> </itemizedlist>
</section> </section>

View file

@ -3,56 +3,101 @@
with lib; with lib;
let let
cfg = config.services.radicale; cfg = config.services.radicale;
confFile = pkgs.writeText "radicale.conf" cfg.config; format = pkgs.formats.ini {
listToValue = concatMapStringsSep ", " (generators.mkValueStringDefault { });
defaultPackage = if versionAtLeast config.system.stateVersion "20.09" then {
pkg = pkgs.radicale3;
text = "pkgs.radicale3";
} else if versionAtLeast config.system.stateVersion "17.09" then {
pkg = pkgs.radicale2;
text = "pkgs.radicale2";
} else {
pkg = pkgs.radicale1;
text = "pkgs.radicale1";
}; };
in
{ pkg = if isNull cfg.package then
pkgs.radicale
else
cfg.package;
options = { confFile = if cfg.settings == { } then
services.radicale.enable = mkOption { pkgs.writeText "radicale.conf" cfg.config
type = types.bool; else
default = false; format.generate "radicale.conf" cfg.settings;
description = ''
Enable Radicale CalDAV and CardDAV server. rightsFile = format.generate "radicale.rights" cfg.rights;
'';
in {
options.services.radicale = {
enable = mkEnableOption "Radicale CalDAV and CardDAV server";
package = mkOption {
description = "Radicale package to use.";
# Default cannot be pkgs.radicale because non-null values suppress
# warnings about incompatible configuration and storage formats.
type = with types; nullOr package // { inherit (package) description; };
default = null;
defaultText = "pkgs.radicale";
}; };
services.radicale.package = mkOption { config = mkOption {
type = types.package;
default = defaultPackage.pkg;
defaultText = defaultPackage.text;
description = ''
Radicale package to use. This defaults to version 1.x if
<literal>system.stateVersion &lt; 17.09</literal>, version 2.x if
<literal>17.09 system.stateVersion &lt; 20.09</literal>, and
version 3.x otherwise.
'';
};
services.radicale.config = mkOption {
type = types.str; type = types.str;
default = ""; default = "";
description = '' description = ''
Radicale configuration, this will set the service Radicale configuration, this will set the service
configuration file. configuration file.
This option is mutually exclusive with <option>settings</option>.
This option is deprecated. Use <option>settings</option> instead.
''; '';
}; };
services.radicale.extraArgs = mkOption { settings = mkOption {
type = format.type;
default = { };
description = ''
Configuration for Radicale. See
<link xlink:href="https://radicale.org/3.0.html#documentation/configuration" />.
This option is mutually exclusive with <option>config</option>.
'';
example = literalExample ''
server = {
hosts = [ "0.0.0.0:5232" "[::]:5232" ];
};
auth = {
type = "htpasswd";
htpasswd_filename = "/etc/radicale/users";
htpasswd_encryption = "bcrypt";
};
storage = {
filesystem_folder = "/var/lib/radicale/collections";
};
'';
};
rights = mkOption {
type = format.type;
description = ''
Configuration for Radicale's rights file. See
<link xlink:href="https://radicale.org/3.0.html#documentation/authentication-and-rights" />.
This option only works in conjunction with <option>settings</option>.
Setting this will also set <option>settings.rights.type</option> and
<option>settings.rights.file</option> to approriate values.
'';
default = { };
example = literalExample ''
root = {
user = ".+";
collection = "";
permissions = "R";
};
principal = {
user = ".+";
collection = "{user}";
permissions = "RW";
};
calendars = {
user = ".+";
collection = "{user}/[^/]+";
permissions = "rw";
};
'';
};
extraArgs = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
default = []; default = [];
description = "Extra arguments passed to the Radicale daemon."; description = "Extra arguments passed to the Radicale daemon.";
@ -60,7 +105,38 @@ in
}; };
config = mkIf cfg.enable { config = mkIf cfg.enable {
environment.systemPackages = [ cfg.package ]; assertions = [
{
assertion = cfg.settings == { } || cfg.config == "";
message = ''
The options services.radicale.config and services.radicale.settings
are mutually exclusive.
'';
}
];
warnings = optional (isNull cfg.package && versionOlder config.system.stateVersion "17.09") ''
The configuration and storage formats of your existing Radicale
installation might be incompatible with the newest version.
For upgrade instructions see
https://radicale.org/2.1.html#documentation/migration-from-1xx-to-2xx.
Set services.radicale.package to suppress this warning.
'' ++ optional (isNull cfg.package && versionOlder config.system.stateVersion "20.09") ''
The configuration format of your existing Radicale installation might be
incompatible with the newest version. For upgrade instructions see
https://github.com/Kozea/Radicale/blob/3.0.6/NEWS.md#upgrade-checklist.
Set services.radicale.package to suppress this warning.
'' ++ optional (cfg.config != "") ''
The option services.radicale.config is deprecated.
Use services.radicale.settings instead.
'';
services.radicale.settings.rights = mkIf (cfg.rights != { }) {
type = "from_file";
file = toString rightsFile;
};
environment.systemPackages = [ pkg ];
users.users.radicale = users.users.radicale =
{ uid = config.ids.uids.radicale; { uid = config.ids.uids.radicale;
@ -75,10 +151,11 @@ in
systemd.services.radicale = { systemd.services.radicale = {
description = "A Simple Calendar and Contact Server"; description = "A Simple Calendar and Contact Server";
after = [ "network.target" ]; after = [ "network.target" ];
requires = [ "network.target" ];
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
serviceConfig = { serviceConfig = {
ExecStart = concatStringsSep " " ([ ExecStart = concatStringsSep " " ([
"${cfg.package}/bin/radicale" "-C" confFile "${pkg}/bin/radicale" "-C" confFile
] ++ ( ] ++ (
map escapeShellArg cfg.extraArgs map escapeShellArg cfg.extraArgs
)); ));
@ -88,5 +165,5 @@ in
}; };
}; };
meta.maintainers = with lib.maintainers; [ aneeshusa infinisil ]; meta.maintainers = with lib.maintainers; [ aneeshusa infinisil dotlambda ];
} }

View file

@ -1,140 +1,90 @@
import ./make-test-python.nix ({ lib, pkgs, ... }:
let let
user = "someuser"; user = "someuser";
password = "some_password"; password = "some_password";
port = builtins.toString 5232; port = "5232";
filesystem_folder = "/data/radicale";
common = { pkgs, ... }: { cli = "${pkgs.calendar-cli}/bin/calendar-cli --caldav-user ${user} --caldav-pass ${password}";
in {
name = "radicale3";
meta.maintainers = with lib.maintainers; [ dotlambda ];
machine = { pkgs, ... }: {
services.radicale = { services.radicale = {
enable = true; enable = true;
config = '' settings = {
[auth] auth = {
type = htpasswd type = "htpasswd";
htpasswd_filename = /etc/radicale/htpasswd htpasswd_filename = "/etc/radicale/users";
htpasswd_encryption = bcrypt htpasswd_encryption = "bcrypt";
};
[storage] storage = {
filesystem_folder = /tmp/collections inherit filesystem_folder;
''; hook = "git add -A && (git diff --cached --quiet || git commit -m 'Changes by '%(user)s)";
};
logging.level = "info";
};
rights = {
principal = {
user = ".+";
collection = "{user}";
permissions = "RW";
};
calendars = {
user = ".+";
collection = "{user}/[^/]+";
permissions = "rw";
};
};
}; };
systemd.services.radicale.path = [ pkgs.git ];
environment.systemPackages = [ pkgs.git ];
systemd.tmpfiles.rules = [ "d ${filesystem_folder} 0750 radicale radicale -" ];
# WARNING: DON'T DO THIS IN PRODUCTION! # WARNING: DON'T DO THIS IN PRODUCTION!
# This puts unhashed secrets directly into the Nix store for ease of testing. # This puts unhashed secrets directly into the Nix store for ease of testing.
environment.etc."radicale/htpasswd".source = pkgs.runCommand "htpasswd" {} '' environment.etc."radicale/users".source = pkgs.runCommand "htpasswd" {} ''
${pkgs.apacheHttpd}/bin/htpasswd -bcB "$out" ${user} ${password} ${pkgs.apacheHttpd}/bin/htpasswd -bcB "$out" ${user} ${password}
''; '';
}; };
testScript = ''
machine.wait_for_unit("radicale.service")
machine.wait_for_open_port(${port})
in machine.succeed("sudo -u radicale git -C ${filesystem_folder} init")
machine.succeed(
"sudo -u radicale git -C ${filesystem_folder} config --local user.email radicale@example.com"
)
machine.succeed(
"sudo -u radicale git -C ${filesystem_folder} config --local user.name radicale"
)
import ./make-test-python.nix ({ lib, ... }@args: { with subtest("Test calendar and event creation"):
name = "radicale"; machine.succeed(
meta.maintainers = with lib.maintainers; [ aneeshusa infinisil ]; "${cli} --caldav-url http://localhost:${port}/${user} calendar create cal"
)
machine.succeed("test -d ${filesystem_folder}/collection-root/${user}/cal")
machine.succeed('test -z "$(ls ${filesystem_folder}/collection-root/${user}/cal)"')
machine.succeed(
"${cli} --caldav-url http://localhost:${port}/${user}/cal calendar add 2021-04-23 testevent"
)
machine.succeed('test -n "$(ls ${filesystem_folder}/collection-root/${user}/cal)"')
(status, stdout) = machine.execute(
"sudo -u radicale git -C ${filesystem_folder} log --format=oneline | wc -l"
)
assert status == 0, "git log failed"
assert stdout == "3\n", "there should be exactly 3 commits"
nodes = rec { with subtest("Test rights file"):
radicale = radicale1; # Make the test script read more nicely machine.fail(
radicale1 = lib.recursiveUpdate (common args) { "${cli} --caldav-url http://localhost:${port}/${user} calendar create sub/cal"
nixpkgs.overlays = [ )
(self: super: { machine.fail(
radicale1 = super.radicale1.overrideAttrs (oldAttrs: { "${cli} --caldav-url http://localhost:${port}/otheruser calendar create cal"
propagatedBuildInputs = with self.pythonPackages; )
(oldAttrs.propagatedBuildInputs or []) ++ [ passlib ];
});
})
];
system.stateVersion = "17.03";
};
radicale1_export = lib.recursiveUpdate radicale1 {
services.radicale.extraArgs = [
"--export-storage" "/tmp/collections-new"
];
system.stateVersion = "17.03";
};
radicale2_verify = lib.recursiveUpdate radicale2 {
services.radicale.extraArgs = [ "--debug" "--verify-storage" ];
system.stateVersion = "17.09";
};
radicale2 = lib.recursiveUpdate (common args) {
system.stateVersion = "17.09";
};
radicale3 = lib.recursiveUpdate (common args) {
system.stateVersion = "20.09";
};
};
# This tests whether the web interface is accessible to an authenticated user with subtest("Test web interface"):
testScript = { nodes }: let machine.succeed("curl --fail http://${user}:${password}@localhost:${port}/.web/")
switchToConfig = nodeName: let '';
newSystem = nodes.${nodeName}.config.system.build.toplevel;
in "${newSystem}/bin/switch-to-configuration test";
in ''
with subtest("Check Radicale 1 functionality"):
radicale.succeed(
"${switchToConfig "radicale1"} >&2"
)
radicale.wait_for_unit("radicale.service")
radicale.wait_for_open_port(${port})
radicale.succeed(
"curl --fail http://${user}:${password}@localhost:${port}/someuser/calendar.ics/"
)
with subtest("Export data in Radicale 2 format"):
radicale.succeed("systemctl stop radicale")
radicale.succeed("ls -al /tmp/collections")
radicale.fail("ls -al /tmp/collections-new")
with subtest("Radicale exits immediately after exporting storage"):
radicale.succeed(
"${switchToConfig "radicale1_export"} >&2"
)
radicale.wait_until_fails("systemctl status radicale")
radicale.succeed("ls -al /tmp/collections")
radicale.succeed("ls -al /tmp/collections-new")
with subtest("Verify data in Radicale 2 format"):
radicale.succeed("rm -r /tmp/collections/${user}")
radicale.succeed("mv /tmp/collections-new/collection-root /tmp/collections")
radicale.succeed(
"${switchToConfig "radicale2_verify"} >&2"
)
radicale.wait_until_fails("systemctl status radicale")
(retcode, logs) = radicale.execute("journalctl -u radicale -n 10")
assert (
retcode == 0 and "Verifying storage" in logs
), "Radicale 2 didn't verify storage"
assert (
"failed" not in logs and "exception" not in logs
), "storage verification failed"
with subtest("Check Radicale 2 functionality"):
radicale.succeed(
"${switchToConfig "radicale2"} >&2"
)
radicale.wait_for_unit("radicale.service")
radicale.wait_for_open_port(${port})
(retcode, output) = radicale.execute(
"curl --fail http://${user}:${password}@localhost:${port}/someuser/calendar.ics/"
)
assert (
retcode == 0 and "VCALENDAR" in output
), "Could not read calendar from Radicale 2"
radicale.succeed("curl --fail http://${user}:${password}@localhost:${port}/.web/")
with subtest("Check Radicale 3 functionality"):
radicale.succeed(
"${switchToConfig "radicale3"} >&2"
)
radicale.wait_for_unit("radicale.service")
radicale.wait_for_open_port(${port})
(retcode, output) = radicale.execute(
"curl --fail http://${user}:${password}@localhost:${port}/someuser/calendar.ics/"
)
assert (
retcode == 0 and "VCALENDAR" in output
), "Could not read calendar from Radicale 3"
radicale.succeed("curl --fail http://${user}:${password}@localhost:${port}/.web/")
'';
}) })