summary refs log tree commit diff
path: root/nixos/modules/services/networking
diff options
context:
space:
mode:
authorSilvan Mosberger <infinisil@icloud.com>2018-08-21 22:16:48 +0200
committerSilvan Mosberger <infinisil@icloud.com>2018-10-14 20:39:42 +0200
commit81c3ae949281a7d027dd5a1e9e61a1df5745eabe (patch)
treee3b657158ff83c541ec0e2829a93200af2dce5e2 /nixos/modules/services/networking
parent4eee2cd0e03657dfb39e3e7c93460442e3d2d86c (diff)
downloadnixlib-81c3ae949281a7d027dd5a1e9e61a1df5745eabe.tar
nixlib-81c3ae949281a7d027dd5a1e9e61a1df5745eabe.tar.gz
nixlib-81c3ae949281a7d027dd5a1e9e61a1df5745eabe.tar.bz2
nixlib-81c3ae949281a7d027dd5a1e9e61a1df5745eabe.tar.lz
nixlib-81c3ae949281a7d027dd5a1e9e61a1df5745eabe.tar.xz
nixlib-81c3ae949281a7d027dd5a1e9e61a1df5745eabe.tar.zst
nixlib-81c3ae949281a7d027dd5a1e9e61a1df5745eabe.zip
nixos/znc: add config option
This option represents the ZNC configuration as a Nix value. It will be
converted to a syntactically valid file. This provides:
- Flexibility: Any ZNC option can be used
- Modularity: These values can be set from any NixOS module and will be
merged correctly
- Overridability: Default values can be overridden

Also done:
Remove unused/unneeded options, mkRemovedOptionModule unfortunately doesn't work
inside submodules (yet). The options userName and modulePackages were never used
to begin with
Diffstat (limited to 'nixos/modules/services/networking')
-rw-r--r--nixos/modules/services/networking/znc/default.nix222
-rw-r--r--nixos/modules/services/networking/znc/options.nix190
2 files changed, 265 insertions, 147 deletions
diff --git a/nixos/modules/services/networking/znc/default.nix b/nixos/modules/services/networking/znc/default.nix
index e2526550caf3..bce5b15a19ec 100644
--- a/nixos/modules/services/networking/znc/default.nix
+++ b/nixos/modules/services/networking/znc/default.nix
@@ -3,51 +3,102 @@
 with lib;
 
 let
+
   cfg = config.services.znc;
 
-  defaultUser = "znc"; # Default user to own process.
+  defaultUser = "znc";
 
   modules = pkgs.buildEnv {
     name = "znc-modules";
     paths = cfg.modulePackages;
   };
 
+  listenerPorts = concatMap (l: optional (l ? Port) l.Port)
+    (attrValues (cfg.config.Listener or {}));
+
+  # Converts the config option to a string
+  semanticString = let
+
+      sortedAttrs = set: sort (l: r:
+        if l == "extraConfig" then false # Always put extraConfig last
+        else if isAttrs set.${l} == isAttrs set.${r} then l < r
+        else isAttrs set.${r} # Attrsets should be last, makes for a nice config
+        # This last case occurs when any side (but not both) is an attrset
+        # The order of these is correct when the attrset is on the right
+        # which we're just returning
+      ) (attrNames set);
+
+      # Specifies an attrset that encodes the value according to its type
+      encode = name: value: {
+          null = [];
+          bool = [ "${name} = ${boolToString value}" ];
+          int = [ "${name} = ${toString value}" ];
+
+          # extraConfig should be inserted verbatim
+          string = [ (if name == "extraConfig" then value else "${name} = ${value}") ];
+
+          # Values like `Foo = [ "bar" "baz" ];` should be transformed into
+          #   Foo=bar
+          #   Foo=baz
+          list = concatMap (encode name) value;
+
+          # Values like `Foo = { bar = { Baz = "baz"; Qux = "qux"; Florps = null; }; };` should be transmed into
+          #   <Foo bar>
+          #     Baz=baz
+          #     Qux=qux
+          #   </Foo>
+          set = concatMap (subname: [
+              "<${name} ${subname}>"
+            ] ++ map (line: "\t${line}") (toLines value.${subname}) ++ [
+              "</${name}>"
+            ]) (filter (v: v != null) (attrNames value));
+
+        }.${builtins.typeOf value};
+
+      # One level "above" encode, acts upon a set and uses encode on each name,value pair
+      toLines = set: concatMap (name: encode name set.${name}) (sortedAttrs set);
+
+    in
+      concatStringsSep "\n" (toLines cfg.config);
+
+  semanticTypes = with types; rec {
+    zncAtom = nullOr (either (either int bool) str);
+    zncAttr = attrsOf (nullOr zncConf);
+    zncAll = either (either zncAtom (listOf zncAtom)) zncAttr;
+    zncConf = attrsOf (zncAll // {
+      # Since this is a recursive type and the description by default contains
+      # the description of its subtypes, infinite recursion would occur without
+      # explicitly breaking this cycle
+      description = "znc values (null, atoms (str, int, bool), list of atoms, or attrsets of znc values)";
+    });
+  };
+
 in
 
 {
 
-  imports = [
-    ./options.nix
-  ];
-
-  ###### Interface
+  imports = [ ./options.nix ];
 
   options = {
     services.znc = {
-      enable = mkOption {
-        default = false;
-        type = types.bool;
-        description = ''
-          Enable a ZNC service for a user.
-        '';
-      };
+      enable = mkEnableOption "ZNC";
 
       user = mkOption {
         default = "znc";
         example = "john";
-        type = types.string;
+        type = types.str;
         description = ''
-          The name of an existing user account to use to own the ZNC server process.
-          If not specified, a default user will be created to own the process.
+          The name of an existing user account to use to own the ZNC server
+          process. If not specified, a default user will be created.
         '';
       };
 
       group = mkOption {
-        default = "";
+        default = defaultUser;
         example = "users";
-        type = types.string;
+        type = types.str;
         description = ''
-          Group to own the ZNCserver process.
+          Group to own the ZNC process.
         '';
       };
 
@@ -56,7 +107,8 @@ in
         example = "/home/john/.znc/";
         type = types.path;
         description = ''
-          The data directory. Used for configuration files and modules.
+          The state directory for ZNC. The config and the modules will be linked
+          to from this directory as well.
         '';
       };
 
@@ -64,7 +116,79 @@ in
         type = types.bool;
         default = false;
         description = ''
-          Whether to open ports in the firewall for ZNC.
+          Whether to open ports in the firewall for ZNC. Does work with
+          ports for listeners specified in
+          <option>services.znc.config.Listener</option>.
+        '';
+      };
+
+      config = mkOption {
+        type = semanticTypes.zncConf;
+        default = {};
+        example = literalExample ''
+          {
+            LoadModule = [ "webadmin" "adminlog" ];
+            User.paul = {
+              Admin = true;
+              Nick = "paul";
+              AltNick = "paul1";
+              LoadModule = [ "chansaver" "controlpanel" ];
+              Network.freenode = {
+                Server = "chat.freenode.net +6697";
+                LoadModule = [ "simple_away" ];
+                Chan = {
+                  "#nixos" = { Detached = false; };
+                  "##linux" = { Disabled = true; };
+                };
+              };
+              Pass.password = {
+                Method = "sha256";
+                Hash = "e2ce303c7ea75c571d80d8540a8699b46535be6a085be3414947d638e48d9e93";
+                Salt = "l5Xryew4g*!oa(ECfX2o";
+              };
+            };
+          }
+        '';
+        description = ''
+          Configuration for ZNC, see
+          <literal>https://wiki.znc.in/Configuration</literal> for details. The
+          Nix value declared here will be translated directly to the xml-like
+          format ZNC expects. This is much more flexible than the legacy options
+          under <option>services.znc.confOptions.*</option>, but also can't do
+          any type checking.
+          </para>
+          <para>
+          You can use <command>nix-instantiate --eval --strict '&lt;nixpkgs/nixos&gt;' -A config.services.znc.config</command>
+          to view the current value. By default it contains a listener for port
+          5000 with SSL enabled.
+          </para>
+          <para>
+          Nix attributes called <literal>extraConfig</literal> will be inserted
+          verbatim into the resulting config file.
+          </para>
+          <para>
+          If <option>services.znc.useLegacyConfig</option> is turned on, the
+          option values in <option>services.znc.confOptions.*</option> will be
+          gracefully be applied to this option.
+          </para>
+          <para>
+          If you intend to update the configuration through this option, be sure
+          to enable <option>services.znc.mutable</option>, otherwise none of the
+          changes here will be applied after the initial deploy.
+        '';
+      };
+
+      configFile = mkOption {
+        type = types.path;
+        example = "~/.znc/configs/znc.conf";
+        description = ''
+          Configuration file for ZNC. It is recommended to use the
+          <option>config</option> option instead.
+          </para>
+          <para>
+          Setting this option will override any auto-generated config file
+          through the <option>confOptions</option> or <option>config</option>
+          options.
         '';
       };
 
@@ -78,16 +202,21 @@ in
       };
 
       mutable = mkOption {
-        default = true;
+        default = true; # TODO: Default to true when config is set, make sure to not delete the old config if present
         type = types.bool;
         description = ''
-          Indicates whether to allow the contents of the `dataDir` directory to be changed
-          by the user at run-time.
-          If true, modifications to the ZNC configuration after its initial creation are not
-            overwritten by a NixOS system rebuild.
-          If false, the ZNC configuration is rebuilt by every system rebuild.
-          If the user wants to manage the ZNC service using the web admin interface, this value
-            should be set to true.
+          Indicates whether to allow the contents of the
+          <literal>dataDir</literal> directory to be changed by the user at
+          run-time.
+          </para>
+          <para>
+          If enabled, modifications to the ZNC configuration after its initial
+          creation are not overwritten by a NixOS rebuild. If disabled, the
+          ZNC configuration is rebuilt on every NixOS rebuild.
+          </para>
+          <para>
+          If the user wants to manage the ZNC service using the web admin
+          interface, this option should be enabled.
         '';
       };
 
@@ -96,7 +225,7 @@ in
         example = [ "--debug" ];
         type = types.listOf types.str;
         description = ''
-          Extra flags to use when executing znc command.
+          Extra arguments to use for executing znc.
         '';
       };
     };
@@ -107,40 +236,48 @@ in
 
   config = mkIf cfg.enable {
 
-    networking.firewall = mkIf cfg.openFirewall {
-      allowedTCPPorts = [ ]; # TODO: Add port
+    services.znc = {
+      configFile = mkDefault (pkgs.writeText "znc-generated.conf" semanticString);
+      config = {
+        Version = (builtins.parseDrvName pkgs.znc.name).version;
+        Listener.l.Port = mkDefault 5000;
+        Listener.l.SSL = mkDefault true;
+      };
     };
 
+    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall listenerPorts;
+
     systemd.services.znc = {
       description = "ZNC Server";
       wantedBy = [ "multi-user.target" ];
-      after = [ "network.service" ];
+      after = [ "network-online.target" ];
       serviceConfig = {
         User = cfg.user;
         Group = cfg.group;
         Restart = "always";
+        ExecStart = "${pkgs.znc}/bin/znc --foreground --datadir ${cfg.dataDir} ${escapeShellArgs cfg.extraFlags}";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
-        ExecStop   = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
+        ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
       };
       preStart = ''
-        ${pkgs.coreutils}/bin/mkdir -p ${cfg.dataDir}/configs
+        mkdir -p ${cfg.dataDir}/configs
 
         # If mutable, regenerate conf file every time.
         ${optionalString (!cfg.mutable) ''
-          ${pkgs.coreutils}/bin/echo "znc is set to be system-managed. Now deleting old znc.conf file to be regenerated."
-          ${pkgs.coreutils}/bin/rm -f ${cfg.dataDir}/configs/znc.conf
+          echo "znc is set to be system-managed. Now deleting old znc.conf file to be regenerated."
+          rm -f ${cfg.dataDir}/configs/znc.conf
         ''}
 
         # Ensure essential files exist.
         if [[ ! -f ${cfg.dataDir}/configs/znc.conf ]]; then
-            ${pkgs.coreutils}/bin/echo "No znc.conf file found in ${cfg.dataDir}. Creating one now."
-            ${pkgs.coreutils}/bin/cp --no-clobber ${/* TODO */"zncConfFile"} ${cfg.dataDir}/configs/znc.conf
-            ${pkgs.coreutils}/bin/chmod u+rw ${cfg.dataDir}/configs/znc.conf
-            ${pkgs.coreutils}/bin/chown ${cfg.user} ${cfg.dataDir}/configs/znc.conf
+            echo "No znc.conf file found in ${cfg.dataDir}. Creating one now."
+            cp --no-clobber ${cfg.configFile} ${cfg.dataDir}/configs/znc.conf
+            chmod u+rw ${cfg.dataDir}/configs/znc.conf
+            chown ${cfg.user} ${cfg.dataDir}/configs/znc.conf
         fi
 
         if [[ ! -f ${cfg.dataDir}/znc.pem ]]; then
-          ${pkgs.coreutils}/bin/echo "No znc.pem file found in ${cfg.dataDir}. Creating one now."
+          echo "No znc.pem file found in ${cfg.dataDir}. Creating one now."
           ${pkgs.znc}/bin/znc --makepem --datadir ${cfg.dataDir}
         fi
 
@@ -148,7 +285,6 @@ in
         rm ${cfg.dataDir}/modules || true
         ln -fs ${modules}/lib/znc ${cfg.dataDir}/modules
       '';
-      script = "${pkgs.znc}/bin/znc --foreground --datadir ${cfg.dataDir} ${toString cfg.extraFlags}";
     };
 
     users.users = optional (cfg.user == defaultUser)
diff --git a/nixos/modules/services/networking/znc/options.nix b/nixos/modules/services/networking/znc/options.nix
index ce5ca0a9a3b6..505ebb3bf0ad 100644
--- a/nixos/modules/services/networking/znc/options.nix
+++ b/nixos/modules/services/networking/znc/options.nix
@@ -6,66 +6,9 @@ let
 
   cfg = config.services.znc;
 
-  # Default user and pass:
-  # un=znc
-  # pw=nixospass
-
-  defaultUserName = "znc";
-  defaultPassBlock = "
-        <Pass password>
-                Method = sha256
-                Hash = e2ce303c7ea75c571d80d8540a8699b46535be6a085be3414947d638e48d9e93
-                Salt = l5Xryew4g*!oa(ECfX2o
-        </Pass>
-  ";
-
-  # Keep znc.conf in nix store, then symlink or copy into `dataDir`, depending on `mutable`.
-  mkZncConf = confOpts: ''
-    Version = 1.6.3
-    ${concatMapStrings (n: "LoadModule = ${n}\n") confOpts.modules}
-
-    <Listener l>
-            Port = ${toString confOpts.port}
-            IPv4 = true
-            IPv6 = true
-            SSL = ${boolToString confOpts.useSSL}
-            ${lib.optionalString (confOpts.uriPrefix != null) "URIPrefix = ${confOpts.uriPrefix}"}
-    </Listener>
-
-    <User ${confOpts.userName}>
-            ${confOpts.passBlock}
-            Admin = true
-            Nick = ${confOpts.nick}
-            AltNick = ${confOpts.nick}_
-            Ident = ${confOpts.nick}
-            RealName = ${confOpts.nick}
-            ${concatMapStrings (n: "LoadModule = ${n}\n") confOpts.userModules}
-
-            ${ lib.concatStringsSep "\n" (lib.mapAttrsToList (name: net: ''
-              <Network ${name}>
-                  ${concatMapStrings (m: "LoadModule = ${m}\n") net.modules}
-                  Server = ${net.server} ${lib.optionalString net.useSSL "+"}${toString net.port} ${net.password}
-                  ${concatMapStrings (c: "<Chan #${c}>\n</Chan>\n") net.channels}
-                  ${lib.optionalString net.hasBitlbeeControlChannel ''
-                    <Chan &bitlbee>
-                    </Chan>
-                  ''}
-                  ${net.extraConf}
-              </Network>
-              '') confOpts.networks) }
-    </User>
-    ${confOpts.extraZncConf}
-  '';
-
-  zncConfFile = pkgs.writeTextFile {
-    name = "znc.conf";
-    text = if cfg.zncConf != ""
-      then cfg.zncConf
-      else mkZncConf cfg.confOptions;
-  };
-
   networkOpts = {
     options = {
+
       server = mkOption {
         type = types.str;
         example = "chat.freenode.net";
@@ -75,23 +18,13 @@ let
       };
 
       port = mkOption {
-        type = types.int;
+        type = types.ints.u16;
         default = 6697;
-        example = 6697;
         description = ''
           IRC server port.
         '';
       };
 
-      userName = mkOption {
-        default = "";
-        example = "johntron";
-        type = types.string;
-        description = ''
-          A nick identity specific to the IRC server.
-        '';
-      };
-
       password = mkOption {
         type = types.str;
         default = "";
@@ -108,21 +41,12 @@ let
         '';
       };
 
-      modulePackages = mkOption {
-        type = types.listOf types.package;
-        default = [];
-        example = [ "pkgs.zncModules.push" "pkgs.zncModules.fish" ];
-        description = ''
-          External ZNC modules to build.
-        '';
-      };
-
       modules = mkOption {
         type = types.listOf types.str;
         default = [ "simple_away" ];
         example = literalExample "[ simple_away sasl ]";
         description = ''
-          ZNC modules to load.
+          ZNC network modules to load.
         '';
       };
 
@@ -156,7 +80,8 @@ let
           Nick = johntron
         '';
         description = ''
-          Extra config for the network.
+          Extra config for the network. Consider using
+          <option>services.znc.config</option> instead.
         '';
       };
     };
@@ -166,23 +91,29 @@ in
 
 {
 
-  ###### Interface
-
   options = {
     services.znc = {
 
-      zncConf = mkOption {
-        default = "";
-        example = "See: http://wiki.znc.in/Configuration";
-        type = types.lines;
+      useLegacyConfig = mkOption {
+        default = true;
+        type = types.bool;
         description = ''
-          Config file as generated with `znc --makeconf` to use for the whole ZNC configuration.
-          If specified, `confOptions` will be ignored, and this value, as-is, will be used.
-          If left empty, a conf file with default values will be used.
+          Whether to propagate the legacy options under
+          <option>services.znc.confOptions.*</option> to the znc config. If this
+          is turned on, the znc config will contain a user with the default name
+          "znc", global modules "webadmin" and "adminlog" will be enabled by
+          default, and more, all controlled through the
+          <option>services.znc.confOptions.*</option> options.
+          You can use <command>nix-instantiate --eval --strict '&lt;nixpkgs/nixos&gt;' -A config.services.znc.config</command>
+          to view the current value of the config.
+          </para>
+          <para>
+          In any case, if you need more flexibility,
+          <option>services.znc.config</option> can be used to override/add to
+          all of the legacy options.
         '';
       };
 
-
       confOptions = {
         modules = mkOption {
           type = types.listOf types.str;
@@ -203,9 +134,9 @@ in
         };
 
         userName = mkOption {
-          default = defaultUserName;
+          default = "znc";
           example = "johntron";
-          type = types.string;
+          type = types.str;
           description = ''
             The user name used to log in to the ZNC web admin interface.
           '';
@@ -217,37 +148,47 @@ in
           description = ''
             IRC networks to connect the user to.
           '';
-          example = {
-            "freenode" = {
-              server = "chat.freenode.net";
-              port = 6697;
-              useSSL = true;
-              modules = [ "simple_away" ];
+          example = literalExample ''
+            {
+              "freenode" = {
+                server = "chat.freenode.net";
+                port = 6697;
+                useSSL = true;
+                modules = [ "simple_away" ];
+              };
             };
-          };
+          '';
         };
 
         nick = mkOption {
           default = "znc-user";
           example = "john";
-          type = types.string;
+          type = types.str;
           description = ''
             The IRC nick.
           '';
         };
 
         passBlock = mkOption {
-          example = defaultPassBlock;
-          type = types.string;
+          example = literalExample ''
+            &lt;Pass password&gt;
+               Method = sha256
+               Hash = e2ce303c7ea75c571d80d8540a8699b46535be6a085be3414947d638e48d9e93
+               Salt = l5Xryew4g*!oa(ECfX2o
+            &lt;/Pass&gt;
+          '';
+          type = types.str;
           description = ''
             Generate with `nix-shell -p znc --command "znc --makepass"`.
             This is the password used to log in to the ZNC web admin interface.
+            You can also set this through
+            <option>services.znc.config.User.&lt;username&gt;.Pass.Method</option>
+            and co.
           '';
         };
 
         port = mkOption {
           default = 5000;
-          example = 5000;
           type = types.int;
           description = ''
             Specifies the port on which to listen.
@@ -258,7 +199,8 @@ in
           default = true;
           type = types.bool;
           description = ''
-            Indicates whether the ZNC server should use SSL when listening on the specified port. A self-signed certificate will be generated.
+            Indicates whether the ZNC server should use SSL when listening on
+            the specified port. A self-signed certificate will be generated.
           '';
         };
 
@@ -283,4 +225,44 @@ in
 
     };
   };
+
+  config = mkIf cfg.useLegacyConfig {
+
+    services.znc.config = let
+      c = cfg.confOptions;
+      # defaults here should override defaults set in the non-legacy part
+      mkDefault = mkOverride 900;
+    in {
+      LoadModule = mkDefault c.modules;
+      Listener.l = {
+        Port = mkDefault c.port;
+        IPv4 = mkDefault true;
+        IPv6 = mkDefault true;
+        SSL = mkDefault c.useSSL;
+      };
+      User.${c.userName} = {
+        Admin = mkDefault true;
+        Nick = mkDefault c.nick;
+        AltNick = mkDefault "${c.nick}_";
+        Ident = mkDefault c.nick;
+        RealName = mkDefault c.nick;
+        LoadModule = mkDefault c.userModules;
+        Network = mapAttrs (name: net: {
+          LoadModule = mkDefault net.modules;
+          Server = mkDefault "${net.server} ${optionalString net.useSSL "+"}${toString net.port} ${net.password}";
+          Chan = optionalAttrs net.hasBitlbeeControlChannel { "&bitlbee" = mkDefault {}; } //
+            listToAttrs (map (n: nameValuePair "#${n}" (mkDefault {})) net.channels);
+          extraConfig = if net.extraConf == "" then mkDefault null else net.extraConf;
+        }) c.networks;
+        extraConfig = [ c.passBlock ] ++ optional (c.extraZncConf != "") c.extraZncConf;
+      };
+    };
+  };
+
+  imports = [
+    (mkRemovedOptionModule ["services" "znc" "zncConf"] ''
+      Instead of `services.znc.zncConf = "... foo ...";`, use
+      `services.znc.configFile = pkgs.writeText "znc.conf" "... foo ...";`.
+    '')
+  ];
 }