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
This commit is contained in:
Silvan Mosberger 2018-08-21 22:16:48 +02:00
parent 4eee2cd0e0
commit 81c3ae9492
No known key found for this signature in database
GPG key ID: 9424360B4B85C9E7
2 changed files with 265 additions and 147 deletions

View file

@ -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)

View file

@ -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 ...";`.
'')
];
}