diff --git a/nixos/doc/manual/release-notes/rl-2105.xml b/nixos/doc/manual/release-notes/rl-2105.xml
index e3e6dc48433..4aadd417a09 100644
--- a/nixos/doc/manual/release-notes/rl-2105.xml
+++ b/nixos/doc/manual/release-notes/rl-2105.xml
@@ -715,6 +715,13 @@ environment.systemPackages = [
The yadm 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.
+
+
+ Instead of determining
+ automatically based on , the latest
+ version is always used because old versions are not officially supported.
+
+
diff --git a/nixos/modules/services/networking/radicale.nix b/nixos/modules/services/networking/radicale.nix
index 5af035fd59e..17a42abc0b7 100644
--- a/nixos/modules/services/networking/radicale.nix
+++ b/nixos/modules/services/networking/radicale.nix
@@ -3,56 +3,101 @@
with lib;
let
-
cfg = config.services.radicale;
- confFile = pkgs.writeText "radicale.conf" cfg.config;
-
- 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";
+ format = pkgs.formats.ini {
+ listToValue = concatMapStringsSep ", " (generators.mkValueStringDefault { });
};
-in
-{
+ pkg = if isNull cfg.package then
+ pkgs.radicale
+ else
+ cfg.package;
- options = {
- services.radicale.enable = mkOption {
- type = types.bool;
- default = false;
- description = ''
- Enable Radicale CalDAV and CardDAV server.
- '';
+ confFile = if cfg.settings == { } then
+ pkgs.writeText "radicale.conf" cfg.config
+ else
+ format.generate "radicale.conf" cfg.settings;
+
+ 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 {
- type = types.package;
- default = defaultPackage.pkg;
- defaultText = defaultPackage.text;
- description = ''
- Radicale package to use. This defaults to version 1.x if
- system.stateVersion < 17.09, version 2.x if
- 17.09 ≤ system.stateVersion < 20.09, and
- version 3.x otherwise.
- '';
- };
-
- services.radicale.config = mkOption {
+ config = mkOption {
type = types.str;
default = "";
description = ''
Radicale configuration, this will set the service
configuration file.
+ This option is mutually exclusive with .
+ This option is deprecated. Use instead.
'';
};
- services.radicale.extraArgs = mkOption {
+ settings = mkOption {
+ type = format.type;
+ default = { };
+ description = ''
+ Configuration for Radicale. See
+ .
+ This option is mutually exclusive with .
+ '';
+ 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
+ .
+ This option only works in conjunction with .
+ Setting this will also set and
+ 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;
default = [];
description = "Extra arguments passed to the Radicale daemon.";
@@ -60,7 +105,38 @@ in
};
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 =
{ uid = config.ids.uids.radicale;
@@ -75,10 +151,11 @@ in
systemd.services.radicale = {
description = "A Simple Calendar and Contact Server";
after = [ "network.target" ];
+ requires = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = concatStringsSep " " ([
- "${cfg.package}/bin/radicale" "-C" confFile
+ "${pkg}/bin/radicale" "-C" confFile
] ++ (
map escapeShellArg cfg.extraArgs
));
@@ -88,5 +165,5 @@ in
};
};
- meta.maintainers = with lib.maintainers; [ aneeshusa infinisil ];
+ meta.maintainers = with lib.maintainers; [ aneeshusa infinisil dotlambda ];
}
diff --git a/nixos/tests/radicale.nix b/nixos/tests/radicale.nix
index 1d3679c82a2..8fa71898ee7 100644
--- a/nixos/tests/radicale.nix
+++ b/nixos/tests/radicale.nix
@@ -1,140 +1,90 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }:
+
let
user = "someuser";
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 = {
enable = true;
- config = ''
- [auth]
- type = htpasswd
- htpasswd_filename = /etc/radicale/htpasswd
- htpasswd_encryption = bcrypt
-
- [storage]
- filesystem_folder = /tmp/collections
- '';
+ settings = {
+ auth = {
+ type = "htpasswd";
+ htpasswd_filename = "/etc/radicale/users";
+ htpasswd_encryption = "bcrypt";
+ };
+ storage = {
+ 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!
# 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}
'';
};
+ 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: {
- name = "radicale";
- meta.maintainers = with lib.maintainers; [ aneeshusa infinisil ];
+ with subtest("Test calendar and event creation"):
+ machine.succeed(
+ "${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 {
- radicale = radicale1; # Make the test script read more nicely
- radicale1 = lib.recursiveUpdate (common args) {
- nixpkgs.overlays = [
- (self: super: {
- radicale1 = super.radicale1.overrideAttrs (oldAttrs: {
- 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";
- };
- };
+ with subtest("Test rights file"):
+ machine.fail(
+ "${cli} --caldav-url http://localhost:${port}/${user} calendar create sub/cal"
+ )
+ machine.fail(
+ "${cli} --caldav-url http://localhost:${port}/otheruser calendar create cal"
+ )
- # This tests whether the web interface is accessible to an authenticated user
- testScript = { nodes }: let
- 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/")
- '';
+ with subtest("Test web interface"):
+ machine.succeed("curl --fail http://${user}:${password}@localhost:${port}/.web/")
+ '';
})