summary refs log tree commit diff
path: root/nixos/modules/services/logging/logrotate.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/services/logging/logrotate.nix')
-rw-r--r--nixos/modules/services/logging/logrotate.nix337
1 files changed, 203 insertions, 134 deletions
diff --git a/nixos/modules/services/logging/logrotate.nix b/nixos/modules/services/logging/logrotate.nix
index 082cf92ff4e..fd41b982678 100644
--- a/nixos/modules/services/logging/logrotate.nix
+++ b/nixos/modules/services/logging/logrotate.nix
@@ -5,166 +5,226 @@ with lib;
 let
   cfg = config.services.logrotate;
 
-  pathOpts = { name, ... }:  {
-    options = {
-      enable = mkOption {
-        type = types.bool;
-        default = true;
-        description = ''
-          Whether to enable log rotation for this path. This can be used to explicitly disable
-          logging that has been configured by NixOS.
-        '';
-      };
-
-      name = mkOption {
-        type = types.str;
-        internal = true;
-      };
-
-      path = mkOption {
-        type = with types; either str (listOf str);
-        default = name;
-        defaultText = "attribute name";
-        description = ''
-          The path to log files to be rotated.
-          Spaces are allowed and normal shell quoting rules apply,
-          with ', ", and \ characters supported.
-        '';
-      };
-
-      user = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = ''
-          The user account to use for rotation.
-        '';
-      };
-
-      group = mkOption {
-        type = with types; nullOr str;
-        default = null;
-        description = ''
-          The group to use for rotation.
-        '';
-      };
-
-      frequency = mkOption {
-        type = types.enum [ "hourly" "daily" "weekly" "monthly" "yearly" ];
-        default = "daily";
-        description = ''
-          How often to rotate the logs.
-        '';
-      };
-
-      keep = mkOption {
-        type = types.int;
-        default = 20;
-        description = ''
-          How many rotations to keep.
-        '';
-      };
-
-      extraConfig = mkOption {
-        type = types.lines;
-        default = "";
-        description = ''
-          Extra logrotate config options for this path. Refer to
-          <link xlink:href="https://linux.die.net/man/8/logrotate"/> for details.
-        '';
-      };
-
-      priority = mkOption {
-        type = types.int;
-        default = 1000;
-        description = ''
-          Order of this logrotate block in relation to the others. The semantics are
-          the same as with `lib.mkOrder`. Smaller values have a greater priority.
-        '';
-      };
-    };
+  generateLine = n: v:
+    if builtins.elem n [ "files" "priority" "enable" "global" ] || v == null then null
+    else if builtins.elem n [ "frequency" ] then "${v}\n"
+    else if builtins.elem n [ "firstaction" "lastaction" "prerotate" "postrotate" "preremove" ]
+         then "${n}\n    ${v}\n  endscript\n"
+    else if isInt v then "${n} ${toString v}\n"
+    else if v == true then "${n}\n"
+    else if v == false then "no${n}\n"
+    else "${n} ${v}\n";
+  generateSection = indent: settings: concatStringsSep (fixedWidthString indent " " "") (
+    filter (x: x != null) (mapAttrsToList generateLine settings)
+  );
 
-    config.name = name;
+  # generateSection includes a final newline hence weird closing brace
+  mkConf = settings:
+    if settings.global or false then generateSection 0 settings
+    else ''
+      ${concatMapStringsSep "\n" (files: ''"${files}"'') (toList settings.files)} {
+        ${generateSection 2 settings}}
+    '';
+
+  settings = sortProperties (attrValues (filterAttrs (_: settings: settings.enable) (
+    foldAttrs recursiveUpdate { } [
+      {
+        header = {
+          enable = true;
+          missingok = true;
+          notifempty = true;
+          frequency = "weekly";
+          rotate = 4;
+        };
+      }
+      cfg.settings
+      { header = { global = true; priority = 100; }; }
+    ]
+  )));
+  configFile = pkgs.writeTextFile {
+    name = "logrotate.conf";
+    text = concatStringsSep "\n" (
+      map mkConf settings
+    );
+    checkPhase = optionalString cfg.checkConfig ''
+      # logrotate --debug also checks that users specified in config
+      # file exist, but we only have sandboxed users here so brown these
+      # out. according to man page that means su, create and createolddir.
+      # files required to exist also won't be present, so missingok is forced.
+      user=$(${pkgs.buildPackages.coreutils}/bin/id -un)
+      group=$(${pkgs.buildPackages.coreutils}/bin/id -gn)
+      sed -e "s/\bsu\s.*/su $user $group/" \
+          -e "s/\b\(create\s\+[0-9]*\s*\|createolddir\s\+[0-9]*\s\+\).*/\1$user $group/" \
+          -e "1imissingok" -e "s/\bnomissingok\b//" \
+          $out > logrotate.conf
+      # Since this makes for very verbose builds only show real error.
+      # There is no way to control log level, but logrotate hardcodes
+      # 'error:' at common log level, so we can use grep, taking care
+      # to keep error codes
+      set -o pipefail
+      if ! ${pkgs.buildPackages.logrotate}/sbin/logrotate -s logrotate.status \
+                      --debug logrotate.conf 2>&1 \
+                  | ( ! grep "error:" ) > logrotate-error; then
+              echo "Logrotate configuration check failed."
+              echo "The failing configuration (after adjustments to pass tests in sandbox) was:"
+              printf "%s\n" "-------"
+              cat logrotate.conf
+              printf "%s\n" "-------"
+              echo "The error reported by logrotate was as follow:"
+              printf "%s\n" "-------"
+              cat logrotate-error
+              printf "%s\n" "-------"
+              echo "You can disable this check with services.logrotate.checkConfig = false,"
+              echo "but if you think it should work please report this failure along with"
+              echo "the config file being tested!"
+              false
+      fi
+    '';
   };
 
-  mkConf = pathOpts: ''
-    # generated by NixOS using the `services.logrotate.paths.${pathOpts.name}` attribute set
-    ${concatMapStringsSep " " (path: ''"${path}"'') (toList pathOpts.path)} {
-      ${optionalString (pathOpts.user != null || pathOpts.group != null) "su ${pathOpts.user} ${pathOpts.group}"}
-      ${pathOpts.frequency}
-      rotate ${toString pathOpts.keep}
-      ${pathOpts.extraConfig}
-    }
-  '';
-
-  paths = sortProperties (attrValues (filterAttrs (_: pathOpts: pathOpts.enable) cfg.paths));
-  configFile = pkgs.writeText "logrotate.conf" (
-    concatStringsSep "\n" (
-      [ "missingok" "notifempty" cfg.extraConfig ] ++ (map mkConf paths)
-    )
-  );
-
+  mailOption =
+    if foldr (n: a: a || (n.mail or false) != false) false (attrValues cfg.settings)
+    then "--mail=${pkgs.mailutils}/bin/mail"
+    else "";
 in
 {
   imports = [
-    (mkRenamedOptionModule [ "services" "logrotate" "config" ] [ "services" "logrotate" "extraConfig" ])
+    (mkRemovedOptionModule [ "services" "logrotate" "config" ] "Modify services.logrotate.settings.header instead")
+    (mkRemovedOptionModule [ "services" "logrotate" "extraConfig" ] "Modify services.logrotate.settings.header instead")
+    (mkRemovedOptionModule [ "services" "logrotate" "paths" ] "Add attributes to services.logrotate.settings instead")
   ];
 
   options = {
     services.logrotate = {
-      enable = mkEnableOption "the logrotate systemd service" // {
-        default = foldr (n: a: a || n.enable) false (attrValues cfg.paths);
-        defaultText = literalExpression "cfg.paths != {}";
+      enable = mkEnableOption (lib.mdDoc "the logrotate systemd service") // {
+        default = foldr (n: a: a || n.enable) false (attrValues cfg.settings);
+        defaultText = literalExpression "cfg.settings != {}";
       };
 
-      paths = mkOption {
-        type = with types; attrsOf (submodule pathOpts);
-        default = {};
-        description = ''
-          Attribute set of paths to rotate. The order each block appears in the generated configuration file
-          can be controlled by the <link linkend="opt-services.logrotate.paths._name_.priority">priority</link> option
-          using the same semantics as `lib.mkOrder`. Smaller values have a greater priority.
+      settings = mkOption {
+        default = { };
+        description = lib.mdDoc ''
+          logrotate freeform settings: each attribute here will define its own section,
+          ordered by priority, which can either define files to rotate with their settings
+          or settings common to all further files settings.
+          Refer to <https://linux.die.net/man/8/logrotate> for details.
         '';
         example = literalExpression ''
           {
-            httpd = {
-              path = "/var/log/httpd/*.log";
-              user = config.services.httpd.user;
-              group = config.services.httpd.group;
-              keep = 7;
+            # global options
+            header = {
+              dateext = true;
+            };
+            # example custom files
+            "/var/log/mylog.log" = {
+              frequency = "daily";
+              rotate = 3;
+            };
+            "multiple paths" = {
+               files = [
+                "/var/log/first*.log"
+                "/var/log/second.log"
+              ];
+            };
+          };
+          '';
+        type = types.attrsOf (types.submodule ({ name, ... }: {
+          freeformType = with types; attrsOf (nullOr (oneOf [ int bool str ]));
+
+          options = {
+            enable = mkEnableOption (lib.mdDoc "setting individual kill switch") // {
+              default = true;
+            };
+
+            global = mkOption {
+              type = types.bool;
+              default = false;
+              description = lib.mdDoc ''
+                Whether this setting is a global option or not: set to have these
+                settings apply to all files settings with a higher priority.
+              '';
+            };
+            files = mkOption {
+              type = with types; either str (listOf str);
+              default = name;
+              defaultText = ''
+                The attrset name if not specified
+              '';
+              description = lib.mdDoc ''
+                Single or list of files for which rules are defined.
+                The files are quoted with double-quotes in logrotate configuration,
+                so globs and spaces are supported.
+                Note this setting is ignored if globals is true.
+              '';
             };
 
-            myapp = {
-              path = "/var/log/myapp/*.log";
-              user = "myuser";
-              group = "mygroup";
-              frequency = "weekly";
-              keep = 5;
-              priority = 1;
+            frequency = mkOption {
+              type = types.nullOr types.str;
+              default = null;
+              description = lib.mdDoc ''
+                How often to rotate the logs. Defaults to previously set global setting,
+                which itself defauts to weekly.
+              '';
             };
-          }
+
+            priority = mkOption {
+              type = types.int;
+              default = 1000;
+              description = lib.mdDoc ''
+                Order of this logrotate block in relation to the others. The semantics are
+                the same as with `lib.mkOrder`. Smaller values are inserted first.
+              '';
+            };
+          };
+
+        }));
+      };
+
+      configFile = mkOption {
+        type = types.path;
+        default = configFile;
+        defaultText = ''
+          A configuration file automatically generated by NixOS.
+        '';
+        description = lib.mdDoc ''
+          Override the configuration file used by MySQL. By default,
+          NixOS generates one automatically from [](#opt-services.logrotate.settings).
+        '';
+        example = literalExpression ''
+          pkgs.writeText "logrotate.conf" '''
+            missingok
+            "/var/log/*.log" {
+              rotate 4
+              weekly
+            }
+          ''';
         '';
       };
 
-      extraConfig = mkOption {
-        default = "";
-        type = types.lines;
-        description = ''
-          Extra contents to append to the logrotate configuration file. Refer to
-          <link xlink:href="https://linux.die.net/man/8/logrotate"/> for details.
+      checkConfig = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc ''
+          Whether the config should be checked at build time.
+
+          Some options are not checkable at build time because of the build sandbox:
+          for example, the test does not know about existing files and system users are
+          not known.
+          These limitations mean we must adjust the file for tests (missingok is forced
+          and users are replaced by dummy users), so tests are complemented by a
+          logrotate-checkconf service that is enabled by default.
+          This extra check can be disabled by disabling it at the systemd level with the
+          {option}`services.systemd.services.logrotate-checkconf.enable` option.
+
+          Conversely there are still things that might make this check fail incorrectly
+          (e.g. a file path where we don't have access to intermediate directories):
+          in this case you can disable the failing check with this option.
         '';
       };
     };
   };
 
   config = mkIf cfg.enable {
-    assertions = mapAttrsToList (name: pathOpts:
-      { assertion = (pathOpts.user != null) == (pathOpts.group != null);
-        message = ''
-          If either of `services.logrotate.paths.${name}.user` or `services.logrotate.paths.${name}.group` are specified then *both* must be specified.
-        '';
-      }
-    ) cfg.paths;
-
     systemd.services.logrotate = {
       description = "Logrotate Service";
       startAt = "hourly";
@@ -172,7 +232,16 @@ in
       serviceConfig = {
         Restart = "no";
         User = "root";
-        ExecStart = "${pkgs.logrotate}/sbin/logrotate ${configFile}";
+        ExecStart = "${pkgs.logrotate}/sbin/logrotate ${mailOption} ${cfg.configFile}";
+      };
+    };
+    systemd.services.logrotate-checkconf = {
+      description = "Logrotate configuration check";
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+        RemainAfterExit = true;
+        ExecStart = "${pkgs.logrotate}/sbin/logrotate --debug ${cfg.configFile}";
       };
     };
   };