From 81c3ae949281a7d027dd5a1e9e61a1df5745eabe Mon Sep 17 00:00:00 2001 From: Silvan Mosberger Date: Tue, 21 Aug 2018 22:16:48 +0200 Subject: 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 --- nixos/modules/services/networking/znc/default.nix | 222 +++++++++++++++++----- nixos/modules/services/networking/znc/options.nix | 190 +++++++++--------- 2 files changed, 265 insertions(+), 147 deletions(-) (limited to 'nixos/modules/services/networking') 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 + # + # Baz=baz + # Qux=qux + # + set = concatMap (subname: [ + "<${name} ${subname}>" + ] ++ map (line: "\t${line}") (toLines value.${subname}) ++ [ + "" + ]) (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 + . + ''; + }; + + 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 + https://wiki.znc.in/Configuration 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 , but also can't do + any type checking. + + + You can use nix-instantiate --eval --strict '<nixpkgs/nixos>' -A config.services.znc.config + to view the current value. By default it contains a listener for port + 5000 with SSL enabled. + + + Nix attributes called extraConfig will be inserted + verbatim into the resulting config file. + + + If is turned on, the + option values in will be + gracefully be applied to this option. + + + If you intend to update the configuration through this option, be sure + to enable , 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 instead. + + + Setting this option will override any auto-generated config file + through the or + 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 + dataDir directory to be changed by the user at + run-time. + + + 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. + + + 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 = " - - Method = sha256 - Hash = e2ce303c7ea75c571d80d8540a8699b46535be6a085be3414947d638e48d9e93 - Salt = l5Xryew4g*!oa(ECfX2o - - "; - - # 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} - - - Port = ${toString confOpts.port} - IPv4 = true - IPv6 = true - SSL = ${boolToString confOpts.useSSL} - ${lib.optionalString (confOpts.uriPrefix != null) "URIPrefix = ${confOpts.uriPrefix}"} - - - - ${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: '' - - ${concatMapStrings (m: "LoadModule = ${m}\n") net.modules} - Server = ${net.server} ${lib.optionalString net.useSSL "+"}${toString net.port} ${net.password} - ${concatMapStrings (c: "\n\n") net.channels} - ${lib.optionalString net.hasBitlbeeControlChannel '' - - - ''} - ${net.extraConf} - - '') confOpts.networks) } - - ${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 + 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 + 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 + options. + You can use nix-instantiate --eval --strict '<nixpkgs/nixos>' -A config.services.znc.config + to view the current value of the config. + + + In any case, if you need more flexibility, + 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 '' + <Pass password> + Method = sha256 + Hash = e2ce303c7ea75c571d80d8540a8699b46535be6a085be3414947d638e48d9e93 + Salt = l5Xryew4g*!oa(ECfX2o + </Pass> + ''; + 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 + + 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 ...";`. + '') + ]; } -- cgit 1.4.1