push sheeet
Some checks failed
Periodic Merges (6h) / master → staging-nixos (push) Failing after 12m50s
Periodic Merges (6h) / master → staging-next (push) Failing after 12m54s
Periodic Merges (24h) / merge-base(master,staging) → haskell-updates (push) Failing after 11m54s
Periodic Merges (6h) / staging-next → staging (push) Failing after 12m13s
Periodic Merges (24h) / staging-next-25.05 → staging-25.05 (push) Failing after 13m24s
Periodic Merges (24h) / release-25.05 → staging-next-25.05 (push) Failing after 14m28s

This commit is contained in:
Dark Steveneq
2025-10-09 14:15:47 +02:00
commit 646b892680
49168 changed files with 5897842 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.cachefilesd;
cfgFile = pkgs.writeText "cachefilesd.conf" ''
dir ${cfg.cacheDir}
${cfg.extraConfig}
'';
in
{
options = {
services.cachefilesd = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to enable cachefilesd network filesystems caching daemon.";
};
cacheDir = lib.mkOption {
type = lib.types.str;
default = "/var/cache/fscache";
description = "Directory to contain filesystem cache.";
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
example = "brun 10%";
description = "Additional configuration file entries. See {manpage}`cachefilesd.conf(5)` for more information.";
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
boot.kernelModules = [ "cachefiles" ];
systemd.services.cachefilesd = {
description = "Local network file caching management daemon";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "exec";
ExecStart = "${pkgs.cachefilesd}/bin/cachefilesd -n -f ${cfgFile}";
Restart = "on-failure";
PrivateTmp = true;
};
};
systemd.tmpfiles.settings."10-cachefilesd".${cfg.cacheDir}.d = {
user = "root";
group = "root";
mode = "0700";
};
};
}

View File

@@ -0,0 +1,492 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.ceph;
# function that translates "camelCaseOptions" to "camel case options", credits to tilpner in #nixos@freenode
expandCamelCase = lib.replaceStrings lib.upperChars (map (s: " ${s}") lib.lowerChars);
expandCamelCaseAttrs = lib.mapAttrs' (name: value: lib.nameValuePair (expandCamelCase name) value);
makeServices =
daemonType: daemonIds:
lib.mkMerge (
map (daemonId: {
"ceph-${daemonType}-${daemonId}" =
makeService daemonType daemonId cfg.global.clusterName
cfg.${daemonType}.package;
}) daemonIds
);
makeService =
daemonType: daemonId: clusterName: ceph:
let
stateDirectory = "ceph/${
if daemonType == "rgw" then "radosgw" else daemonType
}/${clusterName}-${daemonId}";
in
{
enable = true;
description = "Ceph ${
builtins.replaceStrings lib.lowerChars lib.upperChars daemonType
} daemon ${daemonId}";
after = [
"network-online.target"
"time-sync.target"
]
++ lib.optional (daemonType == "osd") "ceph-mon.target";
wants = [
"network-online.target"
"time-sync.target"
];
partOf = [ "ceph-${daemonType}.target" ];
wantedBy = [ "ceph-${daemonType}.target" ];
path = [ pkgs.getopt ];
# Don't start services that are not yet initialized
unitConfig.ConditionPathExists = "/var/lib/${stateDirectory}/keyring";
startLimitBurst =
if daemonType == "osd" then
30
else if
lib.elem daemonType [
"mgr"
"mds"
]
then
3
else
5;
startLimitIntervalSec = 60 * 30; # 30 mins
serviceConfig = {
LimitNOFILE = 1048576;
LimitNPROC = 1048576;
Environment = "CLUSTER=${clusterName}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
PrivateDevices = "yes";
PrivateTmp = "true";
ProtectHome = "true";
ProtectSystem = "full";
Restart = "on-failure";
StateDirectory = stateDirectory;
User = "ceph";
Group = if daemonType == "osd" then "disk" else "ceph";
ExecStart = ''
${ceph.out}/bin/${if daemonType == "rgw" then "radosgw" else "ceph-${daemonType}"} \
-f --cluster ${clusterName} --id ${daemonId}'';
}
// lib.optionalAttrs (daemonType == "osd") {
ExecStartPre = "${ceph.lib}/libexec/ceph/ceph-osd-prestart.sh --id ${daemonId} --cluster ${clusterName}";
RestartSec = "20s";
PrivateDevices = "no"; # osd needs disk access
}
// lib.optionalAttrs (daemonType == "mon") {
RestartSec = "10";
};
};
makeTarget = daemonType: {
"ceph-${daemonType}" = {
description = "Ceph target allowing to start/stop all ceph-${daemonType} services at once";
partOf = [ "ceph.target" ];
wantedBy = [ "ceph.target" ];
before = [ "ceph.target" ];
unitConfig.StopWhenUnneeded = true;
};
};
in
{
options.services.ceph = {
# Ceph has a monolithic configuration file but different sections for
# each daemon, a separate client section and a global section
enable = lib.mkEnableOption "Ceph global configuration";
global = {
fsid = lib.mkOption {
type = lib.types.str;
example = ''
433a2193-4f8a-47a0-95d2-209d7ca2cca5
'';
description = ''
Filesystem ID, a generated uuid, its must be generated and set before
attempting to start a cluster
'';
};
clusterName = lib.mkOption {
type = lib.types.str;
default = "ceph";
description = ''
Name of cluster
'';
};
mgrModulePath = lib.mkOption {
type = lib.types.path;
default = "${pkgs.ceph.lib}/lib/ceph/mgr";
defaultText = lib.literalExpression ''"''${pkgs.ceph.lib}/lib/ceph/mgr"'';
description = ''
Path at which to find ceph-mgr modules.
'';
};
monInitialMembers = lib.mkOption {
type = with lib.types; nullOr commas;
default = null;
example = ''
node0, node1, node2
'';
description = ''
List of hosts that will be used as monitors at startup.
'';
};
monHost = lib.mkOption {
type = with lib.types; nullOr commas;
default = null;
example = ''
10.10.0.1, 10.10.0.2, 10.10.0.3
'';
description = ''
List of hostname shortnames/IP addresses of the initial monitors.
'';
};
maxOpenFiles = lib.mkOption {
type = lib.types.int;
default = 131072;
description = ''
Max open files for each OSD daemon.
'';
};
authClusterRequired = lib.mkOption {
type = lib.types.enum [
"cephx"
"none"
];
default = "cephx";
description = ''
Enables requiring daemons to authenticate with eachother in the cluster.
'';
};
authServiceRequired = lib.mkOption {
type = lib.types.enum [
"cephx"
"none"
];
default = "cephx";
description = ''
Enables requiring clients to authenticate with the cluster to access services in the cluster (e.g. radosgw, mds or osd).
'';
};
authClientRequired = lib.mkOption {
type = lib.types.enum [
"cephx"
"none"
];
default = "cephx";
description = ''
Enables requiring the cluster to authenticate itself to the client.
'';
};
publicNetwork = lib.mkOption {
type = with lib.types; nullOr commas;
default = null;
example = ''
10.20.0.0/24, 192.168.1.0/24
'';
description = ''
A comma-separated list of subnets that will be used as public networks in the cluster.
'';
};
clusterNetwork = lib.mkOption {
type = with lib.types; nullOr commas;
default = null;
example = ''
10.10.0.0/24, 192.168.0.0/24
'';
description = ''
A comma-separated list of subnets that will be used as cluster networks in the cluster.
'';
};
rgwMimeTypesFile = lib.mkOption {
type = with lib.types; nullOr path;
default = "${pkgs.mailcap}/etc/mime.types";
defaultText = lib.literalExpression ''"''${pkgs.mailcap}/etc/mime.types"'';
description = ''
Path to mime types used by radosgw.
'';
};
};
extraConfig = lib.mkOption {
type = with lib.types; attrsOf str;
default = { };
example = {
"ms bind ipv6" = "true";
};
description = ''
Extra configuration to add to the global section. Use for setting values that are common for all daemons in the cluster.
'';
};
mgr = {
enable = lib.mkEnableOption "Ceph MGR daemon";
daemons = lib.mkOption {
type = with lib.types; listOf str;
default = [ ];
example = [
"name1"
"name2"
];
description = ''
A list of names for manager daemons that should have a service created. The names correspond
to the id part in ceph i.e. [ "name1" ] would result in mgr.name1
'';
};
package = lib.mkPackageOption pkgs "ceph" { };
extraConfig = lib.mkOption {
type = with lib.types; attrsOf str;
default = { };
description = ''
Extra configuration to add to the global section for manager daemons.
'';
};
};
mon = {
enable = lib.mkEnableOption "Ceph MON daemon";
daemons = lib.mkOption {
type = with lib.types; listOf str;
default = [ ];
example = [
"name1"
"name2"
];
description = ''
A list of monitor daemons that should have a service created. The names correspond
to the id part in ceph i.e. [ "name1" ] would result in mon.name1
'';
};
package = lib.mkPackageOption pkgs "ceph" { };
extraConfig = lib.mkOption {
type = with lib.types; attrsOf str;
default = { };
description = ''
Extra configuration to add to the monitor section.
'';
};
};
osd = {
enable = lib.mkEnableOption "Ceph OSD daemon";
daemons = lib.mkOption {
type = with lib.types; listOf str;
default = [ ];
example = [
"name1"
"name2"
];
description = ''
A list of OSD daemons that should have a service created. The names correspond
to the id part in ceph i.e. [ "name1" ] would result in osd.name1
'';
};
package = lib.mkPackageOption pkgs "ceph" { };
extraConfig = lib.mkOption {
type = with lib.types; attrsOf str;
default = {
"osd journal size" = "10000";
"osd pool default size" = "3";
"osd pool default min size" = "2";
"osd pool default pg num" = "200";
"osd pool default pgp num" = "200";
"osd crush chooseleaf type" = "1";
};
description = ''
Extra configuration to add to the OSD section.
'';
};
};
mds = {
enable = lib.mkEnableOption "Ceph MDS daemon";
daemons = lib.mkOption {
type = with lib.types; listOf str;
default = [ ];
example = [
"name1"
"name2"
];
description = ''
A list of metadata service daemons that should have a service created. The names correspond
to the id part in ceph i.e. [ "name1" ] would result in mds.name1
'';
};
package = lib.mkPackageOption pkgs "ceph" { };
extraConfig = lib.mkOption {
type = with lib.types; attrsOf str;
default = { };
description = ''
Extra configuration to add to the MDS section.
'';
};
};
rgw = {
enable = lib.mkEnableOption "Ceph RadosGW daemon";
package = lib.mkPackageOption pkgs "ceph" { };
daemons = lib.mkOption {
type = with lib.types; listOf str;
default = [ ];
example = [
"name1"
"name2"
];
description = ''
A list of rados gateway daemons that should have a service created. The names correspond
to the id part in ceph i.e. [ "name1" ] would result in client.name1, radosgw daemons
aren't daemons to cluster in the sense that OSD, MGR or MON daemons are. They are simply
daemons, from ceph, that uses the cluster as a backend.
'';
};
};
client = {
enable = lib.mkEnableOption "Ceph client configuration";
extraConfig = lib.mkOption {
type = with lib.types; attrsOf (attrsOf str);
default = { };
example = lib.literalExpression ''
{
# This would create a section for a radosgw daemon named node0 and related
# configuration for it
"client.radosgw.node0" = { "some config option" = "true"; };
};
'';
description = ''
Extra configuration to add to the client section. Configuration for rados gateways
would be added here, with their own sections, see example.
'';
};
};
};
config = lib.mkIf config.services.ceph.enable {
assertions = [
{
assertion = cfg.global.fsid != "";
message = "fsid has to be set to a valid uuid for the cluster to function";
}
{
assertion = cfg.mon.enable -> cfg.mon.daemons != [ ];
message = "have to set id of atleast one MON if you're going to enable Monitor";
}
{
assertion = cfg.mds.enable -> cfg.mds.daemons != [ ];
message = "have to set id of atleast one MDS if you're going to enable Metadata Service";
}
{
assertion = cfg.osd.enable -> cfg.osd.daemons != [ ];
message = "have to set id of atleast one OSD if you're going to enable OSD";
}
{
assertion = cfg.mgr.enable -> cfg.mgr.daemons != [ ];
message = "have to set id of atleast one MGR if you're going to enable MGR";
}
];
warnings =
lib.optional (cfg.global.monInitialMembers == null)
"Not setting up a list of members in monInitialMembers requires that you set the host variable for each mon daemon or else the cluster won't function";
environment.etc."ceph/ceph.conf".text =
let
# Merge the extraConfig set for mgr daemons, as mgr don't have their own section
globalSection = expandCamelCaseAttrs (
cfg.global // cfg.extraConfig // lib.optionalAttrs cfg.mgr.enable cfg.mgr.extraConfig
);
# Remove all name-value pairs with null values from the attribute set to avoid making empty sections in the ceph.conf
globalSection' = lib.filterAttrs (name: value: value != null) globalSection;
totalConfig = {
global = globalSection';
}
// lib.optionalAttrs (cfg.mon.enable && cfg.mon.extraConfig != { }) { mon = cfg.mon.extraConfig; }
// lib.optionalAttrs (cfg.mds.enable && cfg.mds.extraConfig != { }) { mds = cfg.mds.extraConfig; }
// lib.optionalAttrs (cfg.osd.enable && cfg.osd.extraConfig != { }) { osd = cfg.osd.extraConfig; }
// lib.optionalAttrs (cfg.client.enable && cfg.client.extraConfig != { }) cfg.client.extraConfig;
in
lib.generators.toINI { } totalConfig;
users.users.ceph = {
uid = config.ids.uids.ceph;
description = "Ceph daemon user";
group = "ceph";
extraGroups = [ "disk" ];
};
users.groups.ceph = {
gid = config.ids.gids.ceph;
};
systemd.services =
let
services =
[ ]
++ lib.optional cfg.mon.enable (makeServices "mon" cfg.mon.daemons)
++ lib.optional cfg.mds.enable (makeServices "mds" cfg.mds.daemons)
++ lib.optional cfg.osd.enable (makeServices "osd" cfg.osd.daemons)
++ lib.optional cfg.rgw.enable (makeServices "rgw" cfg.rgw.daemons)
++ lib.optional cfg.mgr.enable (makeServices "mgr" cfg.mgr.daemons);
in
lib.mkMerge services;
systemd.targets =
let
targets = [
{
ceph = {
description = "Ceph target allowing to start/stop all ceph service instances at once";
wantedBy = [ "multi-user.target" ];
unitConfig.StopWhenUnneeded = true;
};
}
]
++ lib.optional cfg.mon.enable (makeTarget "mon")
++ lib.optional cfg.mds.enable (makeTarget "mds")
++ lib.optional cfg.osd.enable (makeTarget "osd")
++ lib.optional cfg.rgw.enable (makeTarget "rgw")
++ lib.optional cfg.mgr.enable (makeTarget "mgr");
in
lib.mkMerge targets;
systemd.tmpfiles.settings."10-ceph" =
let
defaultConfig = {
user = "ceph";
group = "ceph";
};
in
{
"/etc/ceph".d = defaultConfig;
"/run/ceph".d = defaultConfig // {
mode = "0770";
};
"/var/lib/ceph".d = defaultConfig;
"/var/lib/ceph/mgr".d = lib.mkIf (cfg.mgr.enable) defaultConfig;
"/var/lib/ceph/mon".d = lib.mkIf (cfg.mon.enable) defaultConfig;
"/var/lib/ceph/osd".d = lib.mkIf (cfg.osd.enable) defaultConfig;
};
};
}

View File

@@ -0,0 +1,162 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib.attrsets) optionalAttrs;
inherit (lib.generators) toINIWithGlobalSection;
inherit (lib.lists) optional;
inherit (lib.modules) mkIf mkRemovedOptionModule;
inherit (lib.options) literalExpression mkEnableOption mkOption;
inherit (lib.strings) escape;
inherit (lib.types)
attrsOf
bool
int
lines
oneOf
str
submodule
;
cfg = config.services.davfs2;
escapeString = escape [
"\""
"\\"
];
formatValue =
value:
if true == value then
"1"
else if false == value then
"0"
else if builtins.isString value then
"\"${escapeString value}\""
else
toString value;
configFile = pkgs.writeText "davfs2.conf" (
toINIWithGlobalSection {
mkSectionName = escapeString;
mkKeyValue = k: v: "${k} ${formatValue v}";
} cfg.settings
);
in
{
imports = [
(mkRemovedOptionModule [ "services" "davfs2" "extraConfig" ] ''
The option extraConfig got removed, please migrate to
services.davfs2.settings instead.
'')
];
options.services.davfs2 = {
enable = mkEnableOption "davfs2";
davUser = mkOption {
type = str;
default = "davfs2";
description = ''
When invoked by root the mount.davfs daemon will run as this user.
Value must be given as name, not as numerical id.
'';
};
davGroup = mkOption {
type = str;
default = "davfs2";
description = ''
The group of the running mount.davfs daemon. Ordinary users must be
member of this group in order to mount a davfs2 file system. Value must
be given as name, not as numerical id.
'';
};
settings = mkOption {
type = submodule {
freeformType =
let
valueTypes = [
bool
int
str
];
in
attrsOf (attrsOf (oneOf (valueTypes ++ [ (attrsOf (oneOf valueTypes)) ])));
};
default = { };
example = literalExpression ''
{
globalSection = {
proxy = "foo.bar:8080";
use_locks = false;
};
sections = {
"/media/dav" = {
use_locks = true;
};
"/home/otto/mywebspace" = {
gui_optimize = true;
};
};
}
'';
description = ''
Extra settings appended to the configuration of davfs2.
See {manpage}`davfs2.conf(5)` for available settings.
'';
};
};
config = mkIf cfg.enable {
environment.systemPackages = [ pkgs.davfs2 ];
environment.etc."davfs2/davfs2.conf".source = configFile;
services.davfs2.settings = {
globalSection = {
dav_user = cfg.davUser;
dav_group = cfg.davGroup;
};
};
users.groups = optionalAttrs (cfg.davGroup == "davfs2") {
davfs2.gid = config.ids.gids.davfs2;
};
users.users = optionalAttrs (cfg.davUser == "davfs2") {
davfs2 = {
createHome = false;
group = cfg.davGroup;
uid = config.ids.uids.davfs2;
description = "davfs2 user";
};
};
security.wrappers."mount.davfs" = {
program = "mount.davfs";
source = "${pkgs.davfs2}/bin/mount.davfs";
owner = "root";
group = cfg.davGroup;
setuid = true;
permissions = "u+rx,g+x";
};
security.wrappers."umount.davfs" = {
program = "umount.davfs";
source = "${pkgs.davfs2}/bin/umount.davfs";
owner = "root";
group = cfg.davGroup;
setuid = true;
permissions = "u+rx,g+x";
};
};
}

View File

@@ -0,0 +1,162 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.diod;
diodBool = b: if b then "1" else "0";
diodConfig = pkgs.writeText "diod.conf" ''
allsquash = ${diodBool cfg.allsquash}
auth_required = ${diodBool cfg.authRequired}
exportall = ${diodBool cfg.exportall}
exportopts = "${lib.concatStringsSep "," cfg.exportopts}"
exports = { ${lib.concatStringsSep ", " (map (s: ''"${s}"'') cfg.exports)} }
listen = { ${lib.concatStringsSep ", " (map (s: ''"${s}"'') cfg.listen)} }
logdest = "${cfg.logdest}"
nwthreads = ${toString cfg.nwthreads}
squashuser = "${cfg.squashuser}"
statfs_passthru = ${diodBool cfg.statfsPassthru}
userdb = ${diodBool cfg.userdb}
${cfg.extraConfig}
'';
in
{
options = {
services.diod = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to enable the diod 9P file server.";
};
listen = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "0.0.0.0:564" ];
description = ''
[ "IP:PORT" [,"IP:PORT",...] ]
List the interfaces and ports that diod should listen on.
'';
};
exports = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
List the file systems that clients will be allowed to mount. All paths should
be fully qualified. The exports table can include two types of element:
a string element (as above),
or an alternate table element form { path="/path", opts="ro" }.
In the alternate form, the (optional) opts attribute is a comma-separated list
of export options. The two table element forms can be mixed in the exports
table. Note that although diod will not traverse file system boundaries for a
given mount due to inode uniqueness constraints, subdirectories of a file
system can be separately exported.
'';
};
exportall = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Export all file systems listed in /proc/mounts. If new file systems are mounted
after diod has started, they will become immediately mountable. If there is a
duplicate entry for a file system in the exports list, any options listed in
the exports entry will apply.
'';
};
exportopts = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Establish a default set of export options. These are overridden, not appended
to, by opts attributes in an "exports" entry.
'';
};
nwthreads = lib.mkOption {
type = lib.types.int;
default = 16;
description = ''
Sets the (fixed) number of worker threads created to handle 9P
requests for a unique aname.
'';
};
authRequired = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Allow clients to connect without authentication, i.e. without a valid MUNGE credential.
'';
};
userdb = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
This option disables password/group lookups. It allows any uid to attach and
assumes gid=uid, and supplementary groups contain only the primary gid.
'';
};
allsquash = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Remap all users to "nobody". The attaching user need not be present in the
password file.
'';
};
squashuser = lib.mkOption {
type = lib.types.str;
default = "nobody";
description = ''
Change the squash user. The squash user must be present in the password file.
'';
};
logdest = lib.mkOption {
type = lib.types.str;
default = "syslog:daemon:err";
description = ''
Set the destination for logging.
The value has the form of "syslog:facility:level" or "filename".
'';
};
statfsPassthru = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
This option configures statfs to return the host file system's type
rather than V9FS_MAGIC.
'';
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Extra configuration options for diod.conf.";
};
};
};
config = lib.mkIf config.services.diod.enable {
environment.systemPackages = [ pkgs.diod ];
systemd.services.diod = {
description = "diod 9P file server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
ExecStart = "${pkgs.diod}/sbin/diod -f -c ${diodConfig}";
};
};
};
}

View File

@@ -0,0 +1,68 @@
# Support for DRBD, the Distributed Replicated Block Device.
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.drbd;
in
{
###### interface
options = {
services.drbd.enable = lib.mkOption {
default = false;
type = lib.types.bool;
description = ''
Whether to enable support for DRBD, the Distributed Replicated
Block Device.
'';
};
services.drbd.config = lib.mkOption {
default = "";
type = lib.types.lines;
description = ''
Contents of the {file}`drbd.conf` configuration file.
'';
};
};
###### implementation
config = lib.mkIf cfg.enable {
environment.systemPackages = [ pkgs.drbd ];
services.udev.packages = [ pkgs.drbd ];
boot.kernelModules = [ "drbd" ];
boot.extraModprobeConfig = ''
options drbd usermode_helper=/run/current-system/sw/bin/drbdadm
'';
environment.etc."drbd.conf" = {
source = pkgs.writeText "drbd.conf" cfg.config;
};
systemd.services.drbd = {
after = [
"systemd-udev.settle.service"
"network.target"
];
wants = [ "systemd-udev.settle.service" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${pkgs.drbd}/bin/drbdadm up all";
ExecStop = "${pkgs.drbd}/bin/drbdadm down all";
};
};
};
}

View File

@@ -0,0 +1,227 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (pkgs) glusterfs rsync;
tlsCmd =
if (cfg.tlsSettings != null) then
''
mkdir -p /var/lib/glusterd
touch /var/lib/glusterd/secure-access
''
else
''
rm -f /var/lib/glusterd/secure-access
'';
restartTriggers = lib.optionals (cfg.tlsSettings != null) [
config.environment.etc."ssl/glusterfs.pem".source
config.environment.etc."ssl/glusterfs.key".source
config.environment.etc."ssl/glusterfs.ca".source
];
cfg = config.services.glusterfs;
in
{
###### interface
options = {
services.glusterfs = {
enable = lib.mkEnableOption "GlusterFS Daemon";
logLevel = lib.mkOption {
type = lib.types.enum [
"DEBUG"
"INFO"
"WARNING"
"ERROR"
"CRITICAL"
"TRACE"
"NONE"
];
description = "Log level used by the GlusterFS daemon";
default = "INFO";
};
useRpcbind = lib.mkOption {
type = lib.types.bool;
description = ''
Enable use of rpcbind. This is required for Gluster's NFS functionality.
You may want to turn it off to reduce the attack surface for DDoS reflection attacks.
See <https://davelozier.com/glusterfs-and-rpcbind-portmap-ddos-reflection-attacks/>
and <https://bugzilla.redhat.com/show_bug.cgi?id=1426842> for details.
'';
default = true;
};
enableGlustereventsd = lib.mkOption {
type = lib.types.bool;
description = "Whether to enable the GlusterFS Events Daemon";
default = true;
};
killMode = lib.mkOption {
type = lib.types.enum [
"control-group"
"process"
"mixed"
"none"
];
description = ''
The systemd KillMode to use for glusterd.
glusterd spawns other daemons like gsyncd.
If you want these to stop when glusterd is stopped (e.g. to ensure
that NixOS config changes are reflected even for these sub-daemons),
set this to 'control-group'.
If however you want running volume processes (glusterfsd) and thus
gluster mounts not be interrupted when glusterd is restarted
(for example, when you want to restart them manually at a later time),
set this to 'process'.
'';
default = "control-group";
};
stopKillTimeout = lib.mkOption {
type = lib.types.str;
description = ''
The systemd TimeoutStopSec to use.
After this time after having been asked to shut down, glusterd
(and depending on the killMode setting also its child processes)
are killed by systemd.
The default is set low because GlusterFS (as of 3.10) is known to
not tell its children (like gsyncd) to terminate at all.
'';
default = "5s";
};
extraFlags = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "Extra flags passed to the GlusterFS daemon";
default = [ ];
};
tlsSettings = lib.mkOption {
description = ''
Make the server communicate via TLS.
This means it will only connect to other gluster
servers having certificates signed by the same CA.
Enabling this will create a file {file}`/var/lib/glusterd/secure-access`.
Disabling will delete this file again.
See also: <https://gluster.readthedocs.io/en/latest/Administrator%20Guide/SSL/>
'';
default = null;
type = lib.types.nullOr (
lib.types.submodule {
options = {
tlsKeyPath = lib.mkOption {
type = lib.types.str;
description = "Path to the private key used for TLS.";
};
tlsPem = lib.mkOption {
type = lib.types.path;
description = "Path to the certificate used for TLS.";
};
caCert = lib.mkOption {
type = lib.types.path;
description = "Path certificate authority used to sign the cluster certificates.";
};
};
}
);
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
environment.systemPackages = [ pkgs.glusterfs ];
services.rpcbind.enable = cfg.useRpcbind;
environment.etc = lib.mkIf (cfg.tlsSettings != null) {
"ssl/glusterfs.pem".source = cfg.tlsSettings.tlsPem;
"ssl/glusterfs.key".source = cfg.tlsSettings.tlsKeyPath;
"ssl/glusterfs.ca".source = cfg.tlsSettings.caCert;
};
systemd.services.glusterd = {
inherit restartTriggers;
description = "GlusterFS, a clustered file-system server";
wantedBy = [ "multi-user.target" ];
requires = lib.optional cfg.useRpcbind "rpcbind.service";
after = [ "network.target" ] ++ lib.optional cfg.useRpcbind "rpcbind.service";
preStart = ''
install -m 0755 -d /var/log/glusterfs
''
# The copying of hooks is due to upstream bug https://bugzilla.redhat.com/show_bug.cgi?id=1452761
# Excludes one hook due to missing SELinux binaries.
+ ''
mkdir -p /var/lib/glusterd/hooks/
${rsync}/bin/rsync -a --exclude="S10selinux-label-brick.sh" ${glusterfs}/var/lib/glusterd/hooks/ /var/lib/glusterd/hooks/
${tlsCmd}
''
# `glusterfind` needs dirs that upstream installs at `make install` phase
# https://github.com/gluster/glusterfs/blob/v3.10.2/tools/glusterfind/Makefile.am#L16-L17
+ ''
mkdir -p /var/lib/glusterd/glusterfind/.keys
mkdir -p /var/lib/glusterd/hooks/1/delete/post/
'';
serviceConfig = {
LimitNOFILE = 65536;
ExecStart = "${glusterfs}/sbin/glusterd --no-daemon --log-level=${cfg.logLevel} ${toString cfg.extraFlags}";
KillMode = cfg.killMode;
TimeoutStopSec = cfg.stopKillTimeout;
};
};
systemd.services.glustereventsd = lib.mkIf cfg.enableGlustereventsd {
inherit restartTriggers;
description = "Gluster Events Notifier";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
preStart = ''
install -m 0755 -d /var/log/glusterfs
'';
# glustereventsd uses the `gluster` executable
path = [ glusterfs ];
serviceConfig = {
Type = "simple";
PIDFile = "/run/glustereventsd.pid";
ExecStart = "${glusterfs}/sbin/glustereventsd --pid-file /run/glustereventsd.pid";
ExecReload = "/bin/kill -SIGUSR2 $MAINPID";
KillMode = "control-group";
};
};
};
}

View File

@@ -0,0 +1,126 @@
{
config,
lib,
pkgs,
options,
...
}:
let
cfg = config.services.ipfs-cluster;
# secret is by envvar, not flag
initFlags = toString [
(lib.optionalString (cfg.initPeers != [ ]) "--peers")
(lib.strings.concatStringsSep "," cfg.initPeers)
];
in
{
options = {
services.ipfs-cluster = {
enable = lib.mkEnableOption "Pinset orchestration for IPFS - requires ipfs daemon to be useful";
consensus = lib.mkOption {
type = lib.types.enum [
"raft"
"crdt"
];
description = "Consensus protocol - 'raft' or 'crdt'. <https://cluster.ipfs.io/documentation/guides/consensus/>";
};
dataDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/ipfs-cluster";
description = "The data dir for ipfs-cluster.";
};
initPeers = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Peer addresses to initialize with on first run.";
};
openSwarmPort = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Open swarm port, secured by the cluster secret. This does not expose the API or proxy. <https://cluster.ipfs.io/documentation/guides/security/>";
};
secretFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
File containing the cluster secret in the format of EnvironmentFile as described by
{manpage}`systemd.exec(5)`. For example:
<programlisting>
CLUSTER_SECRET=<replaceable>...</replaceable>
</programlisting>
If null, a new secret will be generated on first run and stored in the data directory.
A secret in the correct format can also be generated by: `openssl rand -hex 32`
'';
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.enable -> config.services.kubo.enable;
message = "ipfs-cluster requires ipfs - configure and enable services.kubo";
}
];
environment.systemPackages = [ pkgs.ipfs-cluster ];
systemd.tmpfiles.rules = [
"d '${cfg.dataDir}' - ${config.services.kubo.user} ${config.services.kubo.group} - -"
];
systemd.services.ipfs-cluster-init = {
path = [
"/run/wrappers"
pkgs.ipfs-cluster
];
environment.IPFS_CLUSTER_PATH = cfg.dataDir;
wantedBy = [ "default.target" ];
serviceConfig = {
ExecStart = [
"${lib.getExe' pkgs.ipfs-cluster "ipfs-cluster-service"} init --consensus ${cfg.consensus} ${initFlags}"
];
Type = "oneshot";
RemainAfterExit = true;
User = config.services.kubo.user;
Group = config.services.kubo.group;
EnvironmentFile = lib.mkIf (cfg.secretFile != null) cfg.secretFile;
};
# only run once (= when the data directory is empty)
unitConfig.ConditionDirectoryNotEmpty = "!${cfg.dataDir}";
};
systemd.services.ipfs-cluster = {
environment.IPFS_CLUSTER_PATH = cfg.dataDir;
wantedBy = [ "multi-user.target" ];
wants = [ "ipfs-cluster-init.service" ];
after = [ "ipfs-cluster-init.service" ];
serviceConfig = {
Type = "notify";
ExecStart = [ "${lib.getExe' pkgs.ipfs-cluster "ipfs-cluster-service"} daemon" ];
User = config.services.kubo.user;
Group = config.services.kubo.group;
};
};
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openSwarmPort [ 9096 ];
};
meta = {
maintainers = with lib.maintainers; [
sorki
];
};
}

View File

@@ -0,0 +1,130 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (config.security) wrapperDir;
cfg = config.services.kbfs;
in
{
###### interface
options = {
services.kbfs = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to mount the Keybase filesystem.";
};
enableRedirector = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable the Keybase root redirector service, allowing
any user to access KBFS files via `/keybase`,
which will show different contents depending on the requester.
'';
};
mountPoint = lib.mkOption {
type = lib.types.str;
default = "%h/keybase";
example = "/keybase";
description = "Mountpoint for the Keybase filesystem.";
};
extraFlags = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
example = [
"-label kbfs"
"-mount-type normal"
];
description = ''
Additional flags to pass to the Keybase filesystem on launch.
'';
};
};
};
###### implementation
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
# Upstream: https://github.com/keybase/client/blob/master/packaging/linux/systemd/kbfs.service
systemd.user.services.kbfs = {
description = "Keybase File System";
# Note that the "Requires" directive will cause a unit to be restarted whenever its dependency is restarted.
# Do not issue a hard dependency on keybase, because kbfs can reconnect to a restarted service.
# Do not issue a hard dependency on keybase-redirector, because it's ok if it fails (e.g., if it is disabled).
wants = [ "keybase.service" ] ++ lib.optional cfg.enableRedirector "keybase-redirector.service";
path = [ "/run/wrappers" ];
unitConfig.ConditionUser = "!@system";
serviceConfig = {
Type = "notify";
# Keybase notifies from a forked process
EnvironmentFile = [
"-%E/keybase/keybase.autogen.env"
"-%E/keybase/keybase.env"
];
ExecStartPre = [
"${pkgs.coreutils}/bin/mkdir -p \"${cfg.mountPoint}\""
"-${wrapperDir}/fusermount -uz \"${cfg.mountPoint}\""
];
ExecStart = "${pkgs.kbfs}/bin/kbfsfuse ${toString cfg.extraFlags} \"${cfg.mountPoint}\"";
ExecStop = "${wrapperDir}/fusermount -uz \"${cfg.mountPoint}\"";
Restart = "on-failure";
PrivateTmp = true;
};
wantedBy = [ "default.target" ];
};
services.keybase.enable = true;
environment.systemPackages = [ pkgs.kbfs ];
}
(lib.mkIf cfg.enableRedirector {
security.wrappers."keybase-redirector".source = "${pkgs.kbfs}/bin/redirector";
systemd.tmpfiles.settings."10-kbfs"."/keybase".d = {
user = "root";
group = "root";
mode = "0755";
age = "0";
};
# Upstream: https://github.com/keybase/client/blob/master/packaging/linux/systemd/keybase-redirector.service
systemd.user.services.keybase-redirector = {
description = "Keybase Root Redirector for KBFS";
wants = [ "keybase.service" ];
unitConfig.ConditionUser = "!@system";
serviceConfig = {
EnvironmentFile = [
"-%E/keybase/keybase.autogen.env"
"-%E/keybase/keybase.env"
];
# Note: The /keybase mount point is not currently configurable upstream.
ExecStart = "${wrapperDir}/keybase-redirector /keybase";
Restart = "on-failure";
PrivateTmp = true;
};
wantedBy = [ "default.target" ];
};
})
]
);
}

View File

@@ -0,0 +1,532 @@
{
config,
lib,
pkgs,
utils,
...
}:
let
cfg = config.services.kubo;
settingsFormat = pkgs.formats.json { };
rawDefaultConfig = lib.importJSON (
pkgs.runCommand "kubo-default-config"
{
nativeBuildInputs = [ cfg.package ];
}
''
export IPFS_PATH="$TMPDIR"
ipfs init --empty-repo --profile=${profile}
ipfs --offline config show > "$out"
''
);
# Remove the PeerID (an attribute of "Identity") of the temporary Kubo repo.
# The "Pinning" section contains the "RemoteServices" section, which would prevent
# the daemon from starting as that setting can't be changed via ipfs config replace.
defaultConfig = builtins.removeAttrs rawDefaultConfig [
"Identity"
"Pinning"
];
customizedConfig = lib.recursiveUpdate defaultConfig cfg.settings;
configFile = settingsFormat.generate "kubo-config.json" customizedConfig;
# Create a fake repo containing only the file "api".
# $IPFS_PATH will point to this directory instead of the real one.
# For some reason the Kubo CLI tools insist on reading the
# config file when it exists. But the Kubo daemon sets the file
# permissions such that only the ipfs user is allowed to read
# this file. This prevents normal users from talking to the daemon.
# To work around this terrible design, create a fake repo with no
# config file, only an api file and everything should work as expected.
fakeKuboRepo = pkgs.writeTextDir "api" ''
/unix/run/ipfs.sock
'';
kuboFlags = utils.escapeSystemdExecArgs (
lib.optional cfg.autoMount "--mount"
++ lib.optional cfg.enableGC "--enable-gc"
++ lib.optional (cfg.serviceFdlimit != null) "--manage-fdlimit=false"
++ lib.optional (cfg.defaultMode == "offline") "--offline"
++ lib.optional (cfg.defaultMode == "norouting") "--routing=none"
++ cfg.extraFlags
);
profile = if cfg.localDiscovery then "local-discovery" else "server";
splitMulitaddr = addrRaw: lib.tail (lib.splitString "/" addrRaw);
multiaddrsToListenStreams =
addrIn:
let
addrs = if builtins.isList addrIn then addrIn else [ addrIn ];
unfilteredResult = map multiaddrToListenStream addrs;
in
builtins.filter (addr: addr != null) unfilteredResult;
multiaddrsToListenDatagrams =
addrIn:
let
addrs = if builtins.isList addrIn then addrIn else [ addrIn ];
unfilteredResult = map multiaddrToListenDatagram addrs;
in
builtins.filter (addr: addr != null) unfilteredResult;
multiaddrToListenStream =
addrRaw:
let
addr = splitMulitaddr addrRaw;
s = builtins.elemAt addr;
in
if s 0 == "ip4" && s 2 == "tcp" then
"${s 1}:${s 3}"
else if s 0 == "ip6" && s 2 == "tcp" then
"[${s 1}]:${s 3}"
else if s 0 == "unix" then
"/${lib.concatStringsSep "/" (lib.tail addr)}"
else
null; # not valid for listen stream, skip
multiaddrToListenDatagram =
addrRaw:
let
addr = splitMulitaddr addrRaw;
s = builtins.elemAt addr;
in
if s 0 == "ip4" && s 2 == "udp" then
"${s 1}:${s 3}"
else if s 0 == "ip6" && s 2 == "udp" then
"[${s 1}]:${s 3}"
else
null; # not valid for listen datagram, skip
in
{
###### interface
options = {
services.kubo = {
enable = lib.mkEnableOption ''
the Interplanetary File System (WARNING: may cause severe network degradation).
NOTE: after enabling this option and rebuilding your system, you need to log out
and back in for the `IPFS_PATH` environment variable to be present in your shell.
Until you do that, the CLI tools won't be able to talk to the daemon by default
'';
package = lib.mkPackageOption pkgs "kubo" { };
user = lib.mkOption {
type = lib.types.str;
default = "ipfs";
description = "User under which the Kubo daemon runs";
};
group = lib.mkOption {
type = lib.types.str;
default = "ipfs";
description = "Group under which the Kubo daemon runs";
};
dataDir = lib.mkOption {
type = lib.types.str;
default =
if lib.versionAtLeast config.system.stateVersion "17.09" then
"/var/lib/ipfs"
else
"/var/lib/ipfs/.ipfs";
defaultText = lib.literalExpression ''
if lib.versionAtLeast config.system.stateVersion "17.09"
then "/var/lib/ipfs"
else "/var/lib/ipfs/.ipfs"
'';
description = "The data dir for Kubo";
};
defaultMode = lib.mkOption {
type = lib.types.enum [
"online"
"offline"
"norouting"
];
default = "online";
description = "systemd service that is enabled by default";
};
autoMount = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether Kubo should try to mount /ipfs, /ipns and /mfs at startup.";
};
autoMigrate = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether Kubo should try to run the fs-repo-migration at startup.";
};
enableGC = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to enable automatic garbage collection";
};
emptyRepo = lib.mkOption {
type = lib.types.bool;
default = true;
description = "If set to false, the repo will be initialized with help files";
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
options = {
Addresses.API = lib.mkOption {
type = lib.types.oneOf [
lib.types.str
(lib.types.listOf lib.types.str)
];
default = [ ];
description = ''
Multiaddr or array of multiaddrs describing the address to serve the local HTTP API on.
In addition to the multiaddrs listed here, the daemon will also listen on a Unix domain socket.
To allow the ipfs CLI tools to communicate with the daemon over that socket,
add your user to the correct group, e.g. `users.users.alice.extraGroups = [ config.services.kubo.group ];`
'';
};
Addresses.Gateway = lib.mkOption {
type = lib.types.oneOf [
lib.types.str
(lib.types.listOf lib.types.str)
];
default = "/ip4/127.0.0.1/tcp/8080";
description = "Where the IPFS Gateway can be reached";
};
Addresses.Swarm = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [
"/ip4/0.0.0.0/tcp/4001"
"/ip6/::/tcp/4001"
"/ip4/0.0.0.0/udp/4001/quic-v1"
"/ip4/0.0.0.0/udp/4001/quic-v1/webtransport"
"/ip4/0.0.0.0/udp/4001/webrtc-direct"
"/ip6/::/udp/4001/quic-v1"
"/ip6/::/udp/4001/quic-v1/webtransport"
"/ip6/::/udp/4001/webrtc-direct"
];
description = "Where Kubo listens for incoming p2p connections";
};
Mounts.IPFS = lib.mkOption {
type = lib.types.str;
default = "/ipfs";
description = "Where to mount the IPFS namespace to";
};
Mounts.IPNS = lib.mkOption {
type = lib.types.str;
default = "/ipns";
description = "Where to mount the IPNS namespace to";
};
Mounts.MFS = lib.mkOption {
type = lib.types.str;
default = "/mfs";
description = "Where to mount the MFS namespace to";
};
};
};
description = ''
Attrset of daemon configuration.
See [https://github.com/ipfs/kubo/blob/master/docs/config.md](https://github.com/ipfs/kubo/blob/master/docs/config.md) for reference.
You can't set `Identity` or `Pinning`.
'';
default = { };
example = {
Datastore.StorageMax = "100GB";
Discovery.MDNS.Enabled = false;
Bootstrap = [
"/ip4/128.199.219.111/tcp/4001/ipfs/QmSoLSafTMBsPKadTEgaXctDQVcqN88CNLHXMkTNwMKPnu"
"/ip4/162.243.248.213/tcp/4001/ipfs/QmSoLueR4xBeUbY9WZ9xGUUxunbKWcrNFTDAadQJmocnWm"
];
Swarm.AddrFilters = null;
};
};
extraFlags = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "Extra flags passed to the Kubo daemon";
default = [ ];
};
localDiscovery = lib.mkOption {
type = lib.types.bool;
description = ''
Whether to enable local discovery for the Kubo daemon.
This will allow Kubo to scan ports on your local network. Some hosting services will ban you if you do this.
'';
default = false;
};
serviceFdlimit = lib.mkOption {
type = lib.types.nullOr lib.types.int;
default = null;
description = "The fdlimit for the Kubo systemd unit or `null` to have the daemon attempt to manage it";
example = 64 * 1024;
};
startWhenNeeded = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to use socket activation to start Kubo when needed.";
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = !builtins.hasAttr "Identity" cfg.settings;
message = ''
You can't set services.kubo.settings.Identity because the ``config replace`` subcommand used at startup does not support modifying any of the Identity settings.
'';
}
{
assertion =
!(
(builtins.hasAttr "Pinning" cfg.settings)
&& (builtins.hasAttr "RemoteServices" cfg.settings.Pinning)
);
message = ''
You can't set services.kubo.settings.Pinning.RemoteServices because the ``config replace`` subcommand used at startup does not work with it.
'';
}
{
assertion =
!(
(lib.versionAtLeast cfg.package.version "0.21")
&& (builtins.hasAttr "Experimental" cfg.settings)
&& (builtins.hasAttr "AcceleratedDHTClient" cfg.settings.Experimental)
);
message = ''
The `services.kubo.settings.Experimental.AcceleratedDHTClient` option was renamed to `services.kubo.settings.Routing.AcceleratedDHTClient` in Kubo 0.21.
'';
}
];
environment.systemPackages = [ cfg.package ];
environment.variables.IPFS_PATH = fakeKuboRepo;
# https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes
boot.kernel.sysctl."net.core.rmem_max" = lib.mkDefault 2500000;
boot.kernel.sysctl."net.core.wmem_max" = lib.mkDefault 2500000;
programs.fuse = lib.mkIf cfg.autoMount {
userAllowOther = true;
};
users.users = lib.mkIf (cfg.user == "ipfs") {
ipfs = {
group = cfg.group;
home = cfg.dataDir;
createHome = false;
uid = config.ids.uids.ipfs;
description = "IPFS daemon user";
packages = [
pkgs.kubo-migrator
];
};
};
users.groups = lib.mkIf (cfg.group == "ipfs") {
ipfs.gid = config.ids.gids.ipfs;
};
systemd.tmpfiles.settings."10-kubo" =
let
defaultConfig = { inherit (cfg) user group; };
in
{
${cfg.dataDir}.d = defaultConfig;
${cfg.settings.Mounts.IPFS}.d = lib.mkIf (cfg.autoMount) defaultConfig;
${cfg.settings.Mounts.IPNS}.d = lib.mkIf (cfg.autoMount) defaultConfig;
${cfg.settings.Mounts.MFS}.d = lib.mkIf (cfg.autoMount) defaultConfig;
};
# The hardened systemd unit breaks the fuse-mount function according to documentation in the unit file itself
systemd.packages =
if cfg.autoMount then [ cfg.package.systemd_unit ] else [ cfg.package.systemd_unit_hardened ];
services.kubo.settings = lib.mkIf cfg.autoMount {
Mounts.FuseAllowOther = lib.mkDefault true;
};
systemd.services.ipfs = {
path = [
"/run/wrappers"
cfg.package
];
environment.IPFS_PATH = cfg.dataDir;
preStart = ''
if [[ ! -f "$IPFS_PATH/config" ]]; then
ipfs init --empty-repo=${lib.boolToString cfg.emptyRepo}
else
# After an unclean shutdown this file may exist which will cause the config command to attempt to talk to the daemon. This will hang forever if systemd is holding our sockets open.
rm -vf "$IPFS_PATH/api"
''
+ lib.optionalString cfg.autoMigrate ''
'${lib.getExe pkgs.kubo-migrator}' -to '${cfg.package.repoVersion}' -y
''
+ ''
fi
ipfs --offline config show |
${pkgs.jq}/bin/jq -s '.[0].Pinning as $Pinning | .[0].Identity as $Identity | .[1] + {$Identity,$Pinning}' - '${configFile}' |
# This command automatically injects the private key and other secrets from
# the old config file back into the new config file.
# Unfortunately, it doesn't keep the original `Identity.PeerID`,
# so we need `ipfs config show` and jq above.
# See https://github.com/ipfs/kubo/issues/8993 for progress on fixing this problem.
# Kubo also wants a specific version of the original "Pinning.RemoteServices"
# section (redacted by `ipfs config show`), such that that section doesn't
# change when the changes are applied. Whyyyyyy.....
ipfs --offline config replace -
'';
postStop = lib.mkIf cfg.autoMount ''
# After an unclean shutdown the fuse mounts at cfg.settings.Mounts.IPFS, cfg.settings.Mounts.IPNS and cfg.settings.Mounts.MFS are locked
umount --quiet '${cfg.settings.Mounts.IPFS}' '${cfg.settings.Mounts.IPNS}' '${cfg.settings.Mounts.MFS}' || true
'';
serviceConfig = {
ExecStart = [
""
"${cfg.package}/bin/ipfs daemon ${kuboFlags}"
];
User = cfg.user;
Group = cfg.group;
StateDirectory = "";
ReadWritePaths = lib.optionals (!cfg.autoMount) [
""
cfg.dataDir
];
# Make sure the socket units are started before ipfs.service
Sockets = [
"ipfs-gateway.socket"
"ipfs-api.socket"
];
}
// lib.optionalAttrs (cfg.serviceFdlimit != null) { LimitNOFILE = cfg.serviceFdlimit; };
}
// lib.optionalAttrs (!cfg.startWhenNeeded) {
wantedBy = [ "default.target" ];
};
systemd.sockets.ipfs-gateway = {
wantedBy = [ "sockets.target" ];
socketConfig = {
ListenStream = [ "" ] ++ (multiaddrsToListenStreams cfg.settings.Addresses.Gateway);
ListenDatagram = [ "" ] ++ (multiaddrsToListenDatagrams cfg.settings.Addresses.Gateway);
};
};
systemd.sockets.ipfs-api = {
wantedBy = [ "sockets.target" ];
socketConfig = {
# We also include "%t/ipfs.sock" because there is no way to put the "%t"
# in the multiaddr.
ListenStream = [
""
"%t/ipfs.sock"
]
++ (multiaddrsToListenStreams cfg.settings.Addresses.API);
SocketMode = "0660";
SocketUser = cfg.user;
SocketGroup = cfg.group;
};
};
};
meta = {
maintainers = with lib.maintainers; [ Luflosi ];
};
imports = [
(lib.mkRenamedOptionModule [ "services" "ipfs" "enable" ] [ "services" "kubo" "enable" ])
(lib.mkRenamedOptionModule [ "services" "ipfs" "package" ] [ "services" "kubo" "package" ])
(lib.mkRenamedOptionModule [ "services" "ipfs" "user" ] [ "services" "kubo" "user" ])
(lib.mkRenamedOptionModule [ "services" "ipfs" "group" ] [ "services" "kubo" "group" ])
(lib.mkRenamedOptionModule [ "services" "ipfs" "dataDir" ] [ "services" "kubo" "dataDir" ])
(lib.mkRenamedOptionModule [ "services" "ipfs" "defaultMode" ] [ "services" "kubo" "defaultMode" ])
(lib.mkRenamedOptionModule [ "services" "ipfs" "autoMount" ] [ "services" "kubo" "autoMount" ])
(lib.mkRenamedOptionModule [ "services" "ipfs" "autoMigrate" ] [ "services" "kubo" "autoMigrate" ])
(lib.mkRenamedOptionModule
[ "services" "ipfs" "ipfsMountDir" ]
[ "services" "kubo" "settings" "Mounts" "IPFS" ]
)
(lib.mkRenamedOptionModule
[ "services" "ipfs" "ipnsMountDir" ]
[ "services" "kubo" "settings" "Mounts" "IPNS" ]
)
(lib.mkRenamedOptionModule
[ "services" "ipfs" "gatewayAddress" ]
[ "services" "kubo" "settings" "Addresses" "Gateway" ]
)
(lib.mkRenamedOptionModule
[ "services" "ipfs" "apiAddress" ]
[ "services" "kubo" "settings" "Addresses" "API" ]
)
(lib.mkRenamedOptionModule
[ "services" "ipfs" "swarmAddress" ]
[ "services" "kubo" "settings" "Addresses" "Swarm" ]
)
(lib.mkRenamedOptionModule [ "services" "ipfs" "enableGC" ] [ "services" "kubo" "enableGC" ])
(lib.mkRenamedOptionModule [ "services" "ipfs" "emptyRepo" ] [ "services" "kubo" "emptyRepo" ])
(lib.mkRenamedOptionModule [ "services" "ipfs" "extraConfig" ] [ "services" "kubo" "settings" ])
(lib.mkRenamedOptionModule [ "services" "ipfs" "extraFlags" ] [ "services" "kubo" "extraFlags" ])
(lib.mkRenamedOptionModule
[ "services" "ipfs" "localDiscovery" ]
[ "services" "kubo" "localDiscovery" ]
)
(lib.mkRenamedOptionModule
[ "services" "ipfs" "serviceFdlimit" ]
[ "services" "kubo" "serviceFdlimit" ]
)
(lib.mkRenamedOptionModule
[ "services" "ipfs" "startWhenNeeded" ]
[ "services" "kubo" "startWhenNeeded" ]
)
(lib.mkRenamedOptionModule [ "services" "kubo" "extraConfig" ] [ "services" "kubo" "settings" ])
(lib.mkRenamedOptionModule
[ "services" "kubo" "gatewayAddress" ]
[ "services" "kubo" "settings" "Addresses" "Gateway" ]
)
(lib.mkRenamedOptionModule
[ "services" "kubo" "apiAddress" ]
[ "services" "kubo" "settings" "Addresses" "API" ]
)
(lib.mkRenamedOptionModule
[ "services" "kubo" "swarmAddress" ]
[ "services" "kubo" "settings" "Addresses" "Swarm" ]
)
(lib.mkRenamedOptionModule
[ "services" "kubo" "ipfsMountDir" ]
[ "services" "kubo" "settings" "Mounts" "IPFS" ]
)
(lib.mkRenamedOptionModule
[ "services" "kubo" "ipnsMountDir" ]
[ "services" "kubo" "settings" "Mounts" "IPNS" ]
)
];
}

View File

@@ -0,0 +1,52 @@
# Litestream {#module-services-litestream}
[Litestream](https://litestream.io/) is a standalone streaming
replication tool for SQLite.
## Configuration {#module-services-litestream-configuration}
Litestream service is managed by a dedicated user named `litestream`
which needs permission to the database file. Here's an example config which gives
required permissions to access [grafana database](#opt-services.grafana.settings.database.path):
```nix
{ pkgs, ... }:
{
users.users.litestream.extraGroups = [ "grafana" ];
systemd.services.grafana.serviceConfig.ExecStartPost =
"+"
+ pkgs.writeShellScript "grant-grafana-permissions" ''
timeout=10
while [ ! -f /var/lib/grafana/data/grafana.db ];
do
if [ "$timeout" == 0 ]; then
echo "ERROR: Timeout while waiting for /var/lib/grafana/data/grafana.db."
exit 1
fi
sleep 1
((timeout--))
done
find /var/lib/grafana -type d -exec chmod -v 775 {} \;
find /var/lib/grafana -type f -exec chmod -v 660 {} \;
'';
services.litestream = {
enable = true;
environmentFile = "/run/secrets/litestream";
settings = {
dbs = [
{
path = "/var/lib/grafana/data/grafana.db";
replicas = [ { url = "s3://mybkt.litestream.io/grafana"; } ];
}
];
};
};
}
```

View File

@@ -0,0 +1,96 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.litestream;
settingsFormat = pkgs.formats.yaml { };
in
{
options.services.litestream = {
enable = lib.mkEnableOption "litestream";
package = lib.mkPackageOption pkgs "litestream" { };
settings = lib.mkOption {
description = ''
See the [documentation](https://litestream.io/reference/config/).
'';
type = settingsFormat.type;
example = {
dbs = [
{
path = "/var/lib/db1";
replicas = [
{
url = "s3://mybkt.litestream.io/db1";
}
];
}
];
};
};
environmentFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/run/secrets/litestream";
description = ''
Environment file as defined in {manpage}`systemd.exec(5)`.
Secrets may be passed to the service without adding them to the
world-readable Nix store, by specifying placeholder variables as
the option value in Nix and setting these variables accordingly in the
environment file.
By default, Litestream will perform environment variable expansion
within the config file before reading it. Any references to ''$VAR or
''${VAR} formatted variables will be replaced with their environment
variable values. If no value is set then it will be replaced with an
empty string.
```
# Content of the environment file
LITESTREAM_ACCESS_KEY_ID=AKIAxxxxxxxxxxxxxxxx
LITESTREAM_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/xxxxxxxxx
```
Note that this file needs to be available on the host on which
this exporter is running.
'';
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
environment.etc = {
"litestream.yml" = {
source = settingsFormat.generate "litestream-config.yaml" cfg.settings;
};
};
systemd.services.litestream = {
description = "Litestream";
wantedBy = [ "multi-user.target" ];
after = [ "networking.target" ];
serviceConfig = {
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
ExecStart = "${cfg.package}/bin/litestream replicate";
Restart = "always";
User = "litestream";
Group = "litestream";
};
};
users.users.litestream = {
description = "Litestream user";
group = "litestream";
isSystemUser = true;
};
users.groups.litestream = { };
};
meta.doc = ./default.md;
}

View File

@@ -0,0 +1,376 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.moosefs;
mfsUser = if cfg.runAsUser then "moosefs" else "root";
settingsFormat =
let
listSep = " ";
allowedTypes = with lib.types; [
bool
int
float
str
];
valueToString =
val:
if lib.isList val then
lib.concatStringsSep listSep (map (x: valueToString x) val)
else if lib.isBool val then
(if val then "1" else "0")
else
toString val;
in
{
type =
with lib.types;
let
valueType =
oneOf (
[
(listOf valueType)
]
++ allowedTypes
)
// {
description = "Flat key-value file";
};
in
attrsOf valueType;
generate =
name: value:
pkgs.writeText name (
lib.concatStringsSep "\n" (lib.mapAttrsToList (key: val: "${key} = ${valueToString val}") value)
);
};
# Manual initialization tool
initTool = pkgs.writeShellScriptBin "mfsmaster-init" ''
if [ ! -e ${cfg.master.settings.DATA_PATH}/metadata.mfs ]; then
cp ${pkgs.moosefs}/var/mfs/metadata.mfs.empty ${cfg.master.settings.DATA_PATH}
chmod +w ${cfg.master.settings.DATA_PATH}/metadata.mfs.empty
${pkgs.moosefs}/bin/mfsmaster -a -c ${masterCfg} start
${pkgs.moosefs}/bin/mfsmaster -c ${masterCfg} stop
rm ${cfg.master.settings.DATA_PATH}/metadata.mfs.empty
fi
'';
masterCfg = settingsFormat.generate "mfsmaster.cfg" cfg.master.settings;
metaloggerCfg = settingsFormat.generate "mfsmetalogger.cfg" cfg.metalogger.settings;
chunkserverCfg = settingsFormat.generate "mfschunkserver.cfg" cfg.chunkserver.settings;
systemdService = name: extraConfig: configFile: {
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [
"network.target"
"network-online.target"
];
serviceConfig = {
Type = "forking";
ExecStart = "${pkgs.moosefs}/bin/mfs${name} -c ${configFile} start";
ExecStop = "${pkgs.moosefs}/bin/mfs${name} -c ${configFile} stop";
ExecReload = "${pkgs.moosefs}/bin/mfs${name} -c ${configFile} reload";
PIDFile = "${cfg."${name}".settings.DATA_PATH}/.mfs${name}.lock";
}
// extraConfig;
};
in
{
###### interface
options = {
services.moosefs = {
masterHost = lib.mkOption {
type = lib.types.str;
default = null;
description = "IP or DNS name of the MooseFS master server.";
};
runAsUser = lib.mkOption {
type = lib.types.bool;
default = true;
example = true;
description = "Run daemons as moosefs user instead of root for better security.";
};
client.enable = lib.mkEnableOption "MooseFS client";
master = {
enable = lib.mkOption {
type = lib.types.bool;
description = ''
Enable MooseFS master daemon.
The master server coordinates all MooseFS operations and stores metadata.
'';
default = false;
};
autoInit = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to automatically initialize the master's metadata directory on first run. Use with caution.";
};
exports = lib.mkOption {
type = with lib.types; listOf str;
default = null;
description = "Export definitions for MooseFS (see mfsexports.cfg).";
example = [
"* / rw,alldirs,admin,maproot=0:0"
"* . rw"
];
};
openFirewall = lib.mkOption {
type = lib.types.bool;
description = "Whether to automatically open required firewall ports for master service.";
default = false;
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
options.DATA_PATH = lib.mkOption {
type = lib.types.str;
default = "/var/lib/mfs";
description = "Directory for storing master metadata.";
};
};
description = "Master configuration options (mfsmaster.cfg).";
};
};
metalogger = {
enable = lib.mkEnableOption "MooseFS metalogger daemon that maintains a backup copy of the master's metadata";
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
options.DATA_PATH = lib.mkOption {
type = lib.types.str;
default = "/var/lib/mfs";
description = "Directory for storing metalogger data.";
};
};
description = "Metalogger configuration options (mfsmetalogger.cfg).";
};
};
chunkserver = {
enable = lib.mkEnableOption "MooseFS chunkserver daemon that stores file data";
openFirewall = lib.mkOption {
type = lib.types.bool;
description = "Whether to automatically open required firewall ports for chunkserver service.";
default = false;
};
hdds = lib.mkOption {
type = with lib.types; listOf str;
default = null;
description = "Mount points used by chunkserver for data storage (see mfshdd.cfg).";
example = [
"/mnt/hdd1"
"/mnt/hdd2"
];
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
options.DATA_PATH = lib.mkOption {
type = lib.types.str;
default = "/var/lib/mfs";
description = "Directory for lock files and other runtime data.";
};
};
description = "Chunkserver configuration options (mfschunkserver.cfg).";
};
};
cgiserver = {
enable = lib.mkEnableOption ''
MooseFS CGI server for web interface.
Warning: The CGI server interface should be properly secured from unauthorized access,
as it provides full control over your MooseFS installation.
'';
openFirewall = lib.mkOption {
type = lib.types.bool;
description = "Whether to automatically open the web interface port.";
default = false;
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
options = {
BIND_HOST = lib.mkOption {
type = lib.types.str;
default = "0.0.0.0";
description = "IP address to bind CGI server to.";
};
PORT = lib.mkOption {
type = lib.types.port;
default = 9425;
description = "Port for CGI server to listen on.";
};
};
};
default = { };
description = "CGI server configuration options.";
};
};
};
};
###### implementation
config =
lib.mkIf
(
cfg.client.enable
|| cfg.master.enable
|| cfg.metalogger.enable
|| cfg.chunkserver.enable
|| cfg.cgiserver.enable
)
{
warnings = [ (lib.mkIf (!cfg.runAsUser) "Running MooseFS services as root is not recommended.") ];
services.moosefs = {
master.settings = lib.mkIf cfg.master.enable (
lib.mkMerge [
{
WORKING_USER = mfsUser;
EXPORTS_FILENAME = toString (
pkgs.writeText "mfsexports.cfg" (lib.concatStringsSep "\n" cfg.master.exports)
);
}
(lib.mkIf cfg.cgiserver.enable {
MFSCGISERV = toString cfg.cgiserver.settings.PORT;
})
]
);
metalogger.settings = lib.mkIf cfg.metalogger.enable {
WORKING_USER = mfsUser;
MASTER_HOST = cfg.masterHost;
};
chunkserver.settings = lib.mkIf cfg.chunkserver.enable {
WORKING_USER = mfsUser;
MASTER_HOST = cfg.masterHost;
HDD_CONF_FILENAME = toString (
pkgs.writeText "mfshdd.cfg" (lib.concatStringsSep "\n" cfg.chunkserver.hdds)
);
};
};
users =
lib.mkIf
(
cfg.runAsUser
&& (cfg.master.enable || cfg.metalogger.enable || cfg.chunkserver.enable || cfg.cgiserver.enable)
)
{
users.moosefs = {
isSystemUser = true;
description = "MooseFS daemon user";
group = "moosefs";
};
groups.moosefs = { };
};
environment.systemPackages =
(lib.optional cfg.client.enable pkgs.moosefs) ++ (lib.optional cfg.master.enable initTool);
networking.firewall.allowedTCPPorts = lib.mkMerge [
(lib.optionals cfg.master.openFirewall [
9419
9420
9421
])
(lib.optional cfg.chunkserver.openFirewall 9422)
(lib.optional (cfg.cgiserver.enable && cfg.cgiserver.openFirewall) cfg.cgiserver.settings.PORT)
];
systemd.tmpfiles.rules = [
# Master directories
(lib.optionalString cfg.master.enable "d ${cfg.master.settings.DATA_PATH} 0700 ${mfsUser} ${mfsUser} -")
# Metalogger directories
(lib.optionalString cfg.metalogger.enable "d ${cfg.metalogger.settings.DATA_PATH} 0700 ${mfsUser} ${mfsUser} -")
# Chunkserver directories
(lib.optionalString cfg.chunkserver.enable "d ${cfg.chunkserver.settings.DATA_PATH} 0700 ${mfsUser} ${mfsUser} -")
]
++ lib.optionals (cfg.chunkserver.enable && cfg.chunkserver.hdds != null) (
map (dir: "d ${dir} 0755 ${mfsUser} ${mfsUser} -") cfg.chunkserver.hdds
);
systemd.services = lib.mkMerge [
(lib.mkIf cfg.master.enable {
mfs-master = (
lib.mkMerge [
(systemdService "master" {
TimeoutStartSec = 1800;
TimeoutStopSec = 1800;
Restart = "on-failure";
User = mfsUser;
} masterCfg)
{
preStart = lib.mkIf cfg.master.autoInit "${initTool}/bin/mfsmaster-init";
}
]
);
})
(lib.mkIf cfg.metalogger.enable {
mfs-metalogger = systemdService "metalogger" {
Restart = "on-abnormal";
User = mfsUser;
} metaloggerCfg;
})
(lib.mkIf cfg.chunkserver.enable {
mfs-chunkserver = systemdService "chunkserver" {
Restart = "on-abnormal";
User = mfsUser;
} chunkserverCfg;
})
(lib.mkIf cfg.cgiserver.enable {
mfs-cgiserv = {
description = "MooseFS CGI Server";
wantedBy = [ "multi-user.target" ];
after = [ "mfs-master.service" ];
serviceConfig = {
Type = "simple";
ExecStart = "${pkgs.moosefs}/bin/mfscgiserv -D /var/lib/mfs -f start";
ExecStop = "${pkgs.moosefs}/bin/mfscgiserv -D /var/lib/mfs stop";
Restart = "on-failure";
RestartSec = "30s";
User = mfsUser;
Group = mfsUser;
WorkingDirectory = "/var/lib/mfs";
};
};
})
];
};
}

View File

@@ -0,0 +1,111 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.netatalk;
settingsFormat = pkgs.formats.ini { };
afpConfFile = settingsFormat.generate "afp.conf" cfg.settings;
in
{
options = {
services.netatalk = {
enable = lib.mkEnableOption "the Netatalk AFP fileserver";
port = lib.mkOption {
type = lib.types.port;
default = 548;
description = "TCP port to be used for AFP.";
};
settings = lib.mkOption {
inherit (settingsFormat) type;
default = { };
example = {
Global = {
"uam list" = "uams_guest.so";
};
Homes = {
path = "afp-data";
"basedir regex" = "/home";
};
example-volume = {
path = "/srv/volume";
"read only" = true;
};
};
description = ''
Configuration for Netatalk. See
{manpage}`afp.conf(5)`.
'';
};
extmap = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
File name extension mappings.
See {manpage}`extmap.conf(5)`. for more information.
'';
};
};
};
imports = (
map
(
option:
lib.mkRemovedOptionModule [
"services"
"netatalk"
option
] "This option was removed in favor of `services.netatalk.settings`."
)
[
"extraConfig"
"homes"
"volumes"
]
);
config = lib.mkIf cfg.enable {
services.netatalk.settings.Global = {
"afp port" = toString cfg.port;
"extmap file" = "${pkgs.writeText "extmap.conf" cfg.extmap}";
};
systemd.services.netatalk = {
description = "Netatalk AFP fileserver for Macintosh clients";
unitConfig.Documentation = "man:afp.conf(5) man:netatalk(8) man:afpd(8) man:cnid_metad(8) man:cnid_dbd(8)";
after = [
"network.target"
"avahi-daemon.service"
];
wantedBy = [ "multi-user.target" ];
path = [ pkgs.netatalk ];
serviceConfig = {
Type = "forking";
GuessMainPID = "no";
PIDFile = "/run/lock/netatalk";
ExecStart = "${pkgs.netatalk}/sbin/netatalk -F ${afpConfFile}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
ExecStop = "${pkgs.coreutils}/bin/kill -TERM $MAINPID";
Restart = "always";
RestartSec = 1;
StateDirectory = [ "netatalk/CNID" ];
};
};
security.pam.services.netatalk.unixAuth = true;
};
}

View File

@@ -0,0 +1,157 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.nfs.server;
exports = pkgs.writeText "exports" cfg.exports;
in
{
imports = [
(lib.mkRenamedOptionModule
[ "services" "nfs" "lockdPort" ]
[ "services" "nfs" "server" "lockdPort" ]
)
(lib.mkRenamedOptionModule
[ "services" "nfs" "statdPort" ]
[ "services" "nfs" "server" "statdPort" ]
)
];
###### interface
options = {
services.nfs = {
server = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable the kernel's NFS server.
'';
};
extraNfsdConfig = lib.mkOption {
type = lib.types.str;
default = "";
description = ''
Extra configuration options for the [nfsd] section of /etc/nfs.conf.
'';
};
exports = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
Contents of the /etc/exports file. See
{manpage}`exports(5)` for the format.
'';
};
hostName = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Hostname or address on which NFS requests will be accepted.
Default is all. See the {option}`-H` option in
{manpage}`nfsd(8)`.
'';
};
nproc = lib.mkOption {
type = lib.types.int;
default = 8;
description = ''
Number of NFS server threads. Defaults to the recommended value of 8.
'';
};
createMountPoints = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to create the mount points in the exports file at startup time.";
};
mountdPort = lib.mkOption {
type = lib.types.nullOr lib.types.port;
default = null;
example = 4002;
description = ''
Use fixed port for rpc.mountd, useful if server is behind firewall.
'';
};
lockdPort = lib.mkOption {
type = lib.types.nullOr lib.types.port;
default = null;
example = 4001;
description = ''
Use a fixed port for the NFS lock manager kernel module
(`lockd/nlockmgr`). This is useful if the
NFS server is behind a firewall.
'';
};
statdPort = lib.mkOption {
type = lib.types.nullOr lib.types.port;
default = null;
example = 4000;
description = ''
Use a fixed port for {command}`rpc.statd`. This is
useful if the NFS server is behind a firewall.
'';
};
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
services.rpcbind.enable = true;
boot.supportedFilesystems = [ "nfs" ]; # needed for statd and idmapd
environment.etc.exports.source = exports;
systemd.services.nfs-server = {
enable = true;
wantedBy = [ "multi-user.target" ];
preStart = ''
mkdir -p /var/lib/nfs/v4recovery
'';
};
systemd.services.nfs-mountd = {
enable = true;
restartTriggers = [ exports ];
preStart = ''
mkdir -p /var/lib/nfs
${lib.optionalString cfg.createMountPoints ''
# create export directories:
# skip comments, take first col which may either be a quoted
# "foo bar" or just foo (-> man export)
sed '/^#.*/d;s/^"\([^"]*\)".*/\1/;t;s/[ ].*//' ${exports} \
| xargs -d '\n' mkdir -p
''}
'';
};
};
}

View File

@@ -0,0 +1,281 @@
{
config,
lib,
pkgs,
...
}:
# openafsMod, openafsBin, mkCellServDB
with import ./lib.nix { inherit config lib pkgs; };
let
inherit (lib)
getBin
literalExpression
mkOption
mkIf
optionalString
singleton
types
;
cfg = config.services.openafsClient;
cellServDB = pkgs.fetchurl {
url = "http://dl.central.org/dl/cellservdb/CellServDB.2018-05-14";
sha256 = "1wmjn6mmyy2r8p10nlbdzs4nrqxy8a9pjyrdciy5nmppg4053rk2";
};
clientServDB = pkgs.writeText "client-cellServDB-${cfg.cellName}" (
mkCellServDB cfg.cellName cfg.cellServDB
);
afsConfig = pkgs.runCommand "afsconfig" { preferLocalBuild = true; } ''
mkdir -p $out
echo ${cfg.cellName} > $out/ThisCell
cat ${cellServDB} ${clientServDB} > $out/CellServDB
echo "${cfg.mountPoint}:${cfg.cache.directory}:${toString cfg.cache.blocks}" > $out/cacheinfo
'';
in
{
###### interface
options = {
services.openafsClient = {
enable = mkOption {
default = false;
type = types.bool;
description = "Whether to enable the OpenAFS client.";
};
afsdb = mkOption {
default = true;
type = types.bool;
description = "Resolve cells via AFSDB DNS records.";
};
cellName = mkOption {
default = "";
type = types.str;
description = "Cell name.";
example = "grand.central.org";
};
cellServDB = mkOption {
default = [ ];
type =
with types;
listOf (submodule {
options = cellServDBConfig;
});
description = ''
This cell's database server records, added to the global
CellServDB. See {manpage}`CellServDB(5)` man page for syntax. Ignored when
`afsdb` is set to `true`.
'';
example = [
{
ip = "1.2.3.4";
dnsname = "first.afsdb.server.dns.fqdn.org";
}
{
ip = "2.3.4.5";
dnsname = "second.afsdb.server.dns.fqdn.org";
}
];
};
cache = {
blocks = mkOption {
default = 100000;
type = types.int;
description = "Cache size in 1KB blocks.";
};
chunksize = mkOption {
default = 0;
type = types.ints.between 0 30;
description = ''
Size of each cache chunk given in powers of
2. `0` resets the chunk size to its default
values (13 (8 KB) for memcache, 18-20 (256 KB to 1 MB) for
diskcache). Maximum value is 30. Important performance
parameter. Set to higher values when dealing with large files.
'';
};
directory = mkOption {
default = "/var/cache/openafs";
type = types.str;
description = "Cache directory.";
};
diskless = mkOption {
default = false;
type = types.bool;
description = ''
Use in-memory cache for diskless machines. Has no real
performance benefit anymore.
'';
};
};
crypt = mkOption {
default = true;
type = types.bool;
description = "Whether to enable (weak) protocol encryption.";
};
daemons = mkOption {
default = 2;
type = types.int;
description = ''
Number of daemons to serve user requests. Numbers higher than 6
usually do no increase performance. Default is sufficient for up
to five concurrent users.
'';
};
fakestat = mkOption {
default = false;
type = types.bool;
description = ''
Return fake data on stat() calls. If `true`,
always do so. If `false`, only do so for
cross-cell mounts (as these are potentially expensive).
'';
};
inumcalc = mkOption {
default = "compat";
type = types.strMatching "compat|md5";
description = ''
Inode calculation method. `compat` is
computationally less expensive, but `md5` greatly
reduces the likelihood of inode collisions in larger scenarios
involving multiple cells mounted into one AFS space.
'';
};
mountPoint = mkOption {
default = "/afs";
type = types.str;
description = ''
Mountpoint of the AFS file tree, conventionally
`/afs`. When set to a different value, only
cross-cells that use the same value can be accessed.
'';
};
packages = {
module = mkOption {
default = config.boot.kernelPackages.openafs;
defaultText = literalExpression "config.boot.kernelPackages.openafs";
type = types.package;
description = "OpenAFS kernel module package. MUST match the userland package!";
};
programs = mkOption {
default = getBin pkgs.openafs;
defaultText = literalExpression "getBin pkgs.openafs";
type = types.package;
description = "OpenAFS programs package. MUST match the kernel module package!";
};
};
sparse = mkOption {
default = true;
type = types.bool;
description = "Minimal cell list in /afs.";
};
startDisconnected = mkOption {
default = false;
type = types.bool;
description = ''
Start up in disconnected mode. You need to execute
`fs disco online` (as root) to switch to
connected mode. Useful for roaming devices.
'';
};
};
};
###### implementation
config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.afsdb || cfg.cellServDB != [ ];
message = "You should specify all cell-local database servers in config.services.openafsClient.cellServDB or set config.services.openafsClient.afsdb.";
}
{
assertion = cfg.cellName != "";
message = "You must specify the local cell name in config.services.openafsClient.cellName.";
}
];
environment.systemPackages = [ openafsBin ];
environment.etc = {
clientCellServDB = {
source = pkgs.runCommand "CellServDB" { preferLocalBuild = true; } ''
cat ${cellServDB} ${clientServDB} > $out
'';
target = "openafs/CellServDB";
mode = "0644";
};
clientCell = {
text = ''
${cfg.cellName}
'';
target = "openafs/ThisCell";
mode = "0644";
};
};
systemd.services.afsd = {
description = "AFS client";
wantedBy = [ "multi-user.target" ];
wants = lib.optional (!cfg.startDisconnected) "network-online.target";
after = singleton (if cfg.startDisconnected then "network.target" else "network-online.target");
serviceConfig = {
RemainAfterExit = true;
};
restartIfChanged = false;
preStart = ''
mkdir -p -m 0755 ${cfg.mountPoint}
mkdir -m 0700 -p ${cfg.cache.directory}
${pkgs.kmod}/bin/insmod ${openafsMod}/lib/modules/*/extra/openafs/libafs.ko.xz
${openafsBin}/sbin/afsd \
-mountdir ${cfg.mountPoint} \
-confdir ${afsConfig} \
${optionalString (!cfg.cache.diskless) "-cachedir ${cfg.cache.directory}"} \
-blocks ${toString cfg.cache.blocks} \
-chunksize ${toString cfg.cache.chunksize} \
${optionalString cfg.cache.diskless "-memcache"} \
-inumcalc ${cfg.inumcalc} \
${if cfg.fakestat then "-fakestat-all" else "-fakestat"} \
${if cfg.sparse then "-dynroot-sparse" else "-dynroot"} \
${optionalString cfg.afsdb "-afsdb"}
${openafsBin}/bin/fs setcrypt ${if cfg.crypt then "on" else "off"}
${optionalString cfg.startDisconnected "${openafsBin}/bin/fs discon offline"}
'';
# Doing this in preStop, because after these commands AFS is basically
# stopped, so systemd has nothing to do, just noticing it. If done in
# postStop, then we get a hang + kernel oops, because AFS can't be
# stopped simply by sending signals to processes.
preStop = ''
${pkgs.util-linux}/bin/umount ${cfg.mountPoint}
${openafsBin}/sbin/afsd -shutdown
${pkgs.kmod}/sbin/rmmod libafs
'';
};
};
}

View File

@@ -0,0 +1,43 @@
{ config, lib, ... }:
let
inherit (lib)
concatStringsSep
mkOption
types
optionalString
;
in
{
mkCellServDB =
cellName: db:
''
>${cellName}
''
+ (concatStringsSep "\n" (
map (dbm: optionalString (dbm.ip != "" && dbm.dnsname != "") "${dbm.ip} #${dbm.dnsname}") db
))
+ "\n";
# CellServDB configuration type
cellServDBConfig = {
ip = mkOption {
type = types.str;
default = "";
example = "1.2.3.4";
description = "IP Address of a database server";
};
dnsname = mkOption {
type = types.str;
default = "";
example = "afs.example.org";
description = "DNS full-qualified domain name of a database server";
};
};
openafsMod = config.services.openafsClient.packages.module;
openafsBin = config.services.openafsClient.packages.programs;
openafsSrv = config.services.openafsServer.package;
}

View File

@@ -0,0 +1,348 @@
{
config,
lib,
pkgs,
...
}:
# openafsBin, openafsSrv, mkCellServDB
with import ./lib.nix { inherit config lib pkgs; };
let
inherit (lib)
concatStringsSep
literalExpression
mkIf
mkOption
mkEnableOption
mkPackageOption
optionalString
types
;
bosConfig = pkgs.writeText "BosConfig" (
''
restrictmode 1
restarttime 16 0 0 0 0
checkbintime 3 0 5 0 0
''
+ (optionalString cfg.roles.database.enable ''
bnode simple vlserver 1
parm ${openafsSrv}/libexec/openafs/vlserver ${optionalString cfg.dottedPrincipals "-allow-dotted-principals"} ${cfg.roles.database.vlserverArgs}
end
bnode simple ptserver 1
parm ${openafsSrv}/libexec/openafs/ptserver ${optionalString cfg.dottedPrincipals "-allow-dotted-principals"} ${cfg.roles.database.ptserverArgs}
end
'')
+ (optionalString cfg.roles.fileserver.enable ''
bnode dafs dafs 1
parm ${openafsSrv}/libexec/openafs/dafileserver ${optionalString cfg.dottedPrincipals "-allow-dotted-principals"} -udpsize ${udpSizeStr} ${cfg.roles.fileserver.fileserverArgs}
parm ${openafsSrv}/libexec/openafs/davolserver ${optionalString cfg.dottedPrincipals "-allow-dotted-principals"} -udpsize ${udpSizeStr} ${cfg.roles.fileserver.volserverArgs}
parm ${openafsSrv}/libexec/openafs/salvageserver ${cfg.roles.fileserver.salvageserverArgs}
parm ${openafsSrv}/libexec/openafs/dasalvager ${cfg.roles.fileserver.salvagerArgs}
end
'')
+ (optionalString
(cfg.roles.database.enable && cfg.roles.backup.enable && (!cfg.roles.backup.enableFabs))
''
bnode simple buserver 1
parm ${openafsSrv}/libexec/openafs/buserver ${cfg.roles.backup.buserverArgs} ${optionalString useBuCellServDB "-cellservdb /etc/openafs/backup/"}
end
''
)
+ (optionalString
(cfg.roles.database.enable && cfg.roles.backup.enable && cfg.roles.backup.enableFabs)
''
bnode simple buserver 1
parm ${lib.getBin pkgs.fabs}/bin/fabsys server --config ${fabsConfFile} ${cfg.roles.backup.fabsArgs}
end
''
)
);
netInfo =
if (cfg.advertisedAddresses != [ ]) then
pkgs.writeText "NetInfo" ((concatStringsSep "\nf " cfg.advertisedAddresses) + "\n")
else
null;
buCellServDB = pkgs.writeText "backup-cellServDB-${cfg.cellName}" (
mkCellServDB cfg.cellName cfg.roles.backup.cellServDB
);
useBuCellServDB = (cfg.roles.backup.cellServDB != [ ]) && (!cfg.roles.backup.enableFabs);
cfg = config.services.openafsServer;
udpSizeStr = toString cfg.udpPacketSize;
fabsConfFile = pkgs.writeText "fabs.yaml" (
builtins.toJSON (
{
afs = {
aklog = cfg.package + "/bin/aklog";
cell = cfg.cellName;
dumpscan = cfg.package + "/bin/afsdump_scan";
fs = cfg.package + "/bin/fs";
pts = cfg.package + "/bin/pts";
vos = cfg.package + "/bin/vos";
};
k5start.command = (lib.getBin pkgs.kstart) + "/bin/k5start";
}
// cfg.roles.backup.fabsExtraConfig
)
);
in
{
options = {
services.openafsServer = {
enable = mkOption {
default = false;
type = types.bool;
description = ''
Whether to enable the OpenAFS server. An OpenAFS server needs a
complex setup. So, be aware that enabling this service and setting
some options does not give you a turn-key-ready solution. You need
at least a running Kerberos 5 setup, as OpenAFS relies on it for
authentication. See the Guide "QuickStartUnix" coming with
`pkgs.openafs.doc` for complete setup
instructions.
'';
};
advertisedAddresses = mkOption {
type = types.listOf types.str;
default = [ ];
description = "List of IP addresses this server is advertised under. See {manpage}`NetInfo(5)`";
};
cellName = mkOption {
default = "";
type = types.str;
description = "Cell name, this server will serve.";
example = "grand.central.org";
};
cellServDB = mkOption {
default = [ ];
type = with types; listOf (submodule [ { options = cellServDBConfig; } ]);
description = "Definition of all cell-local database server machines.";
};
package = mkPackageOption pkgs "openafs" { };
roles = {
fileserver = {
enable = mkOption {
default = true;
type = types.bool;
description = "Fileserver role, serves files and volumes from its local storage.";
};
fileserverArgs = mkOption {
default = "-vattachpar 128 -vhashsize 11 -L -rxpck 400 -cb 1000000";
type = types.str;
description = "Arguments to the dafileserver process. See its man page.";
};
volserverArgs = mkOption {
default = "";
type = types.str;
description = "Arguments to the davolserver process. See its man page.";
example = "-sync never";
};
salvageserverArgs = mkOption {
default = "";
type = types.str;
description = "Arguments to the salvageserver process. See its man page.";
example = "-showlog";
};
salvagerArgs = mkOption {
default = "";
type = types.str;
description = "Arguments to the dasalvager process. See its man page.";
example = "-showlog -showmounts";
};
};
database = {
enable = mkOption {
default = true;
type = types.bool;
description = ''
Database server role, maintains the Volume Location Database,
Protection Database (and Backup Database, see
`backup` role). There can be multiple
servers in the database role for replication, which then need
reliable network connection to each other.
Servers in this role appear in AFSDB DNS records or the
CellServDB.
'';
};
vlserverArgs = mkOption {
default = "";
type = types.str;
description = "Arguments to the vlserver process. See its man page.";
example = "-rxbind";
};
ptserverArgs = mkOption {
default = "";
type = types.str;
description = "Arguments to the ptserver process. See its man page.";
example = "-restricted -default_access S---- S-M---";
};
};
backup = {
enable = mkEnableOption ''
the backup server role. When using OpenAFS built-in buserver, use in conjunction with the
`database` role to maintain the Backup
Database. Normally only used in conjunction with tape storage
or IBM's Tivoli Storage Manager.
For a modern backup server, enable this role and see
{option}`enableFabs`
'';
enableFabs = mkEnableOption ''
FABS, the flexible AFS backup system. It stores volumes as dump files, relying on other
pre-existing backup solutions for handling them
'';
buserverArgs = mkOption {
default = "";
type = types.str;
description = "Arguments to the buserver process. See its man page.";
example = "-p 8";
};
cellServDB = mkOption {
default = [ ];
type = with types; listOf (submodule [ { options = cellServDBConfig; } ]);
description = ''
Definition of all cell-local backup database server machines.
Use this when your cell uses less backup database servers than
other database server machines.
'';
};
fabsArgs = mkOption {
default = "";
type = types.str;
description = ''
Arguments to the fabsys process. See
{manpage}`fabsys_server(1)` and
{manpage}`fabsys_config(1)`.
'';
};
fabsExtraConfig = mkOption {
default = { };
type = types.attrs;
description = ''
Additional configuration parameters for the FABS backup server.
'';
example = literalExpression ''
{
afs.localauth = true;
afs.keytab = config.sops.secrets.fabsKeytab.path;
}
'';
};
};
};
dottedPrincipals = mkOption {
default = false;
type = types.bool;
description = ''
If enabled, allow principal names containing (.) dots. Enabling
this has security implications!
'';
};
udpPacketSize = mkOption {
default = 1310720;
type = types.int;
description = ''
UDP packet size to use in Bytes. Higher values can speed up
communications. The default of 1 MB is a sufficient in most
cases. Make sure to increase the kernel's UDP buffer size
accordingly via `net.core(w|r|opt)mem_max`
sysctl.
'';
};
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.cellServDB != [ ];
message = "You must specify all cell-local database servers in config.services.openafsServer.cellServDB.";
}
{
assertion = cfg.cellName != "";
message = "You must specify the local cell name in config.services.openafsServer.cellName.";
}
];
environment.systemPackages = [ openafsBin ];
environment.etc = {
bosConfig = {
source = bosConfig;
target = "openafs/BosConfig";
mode = "0644";
};
cellServDB = {
text = mkCellServDB cfg.cellName cfg.cellServDB;
target = "openafs/server/CellServDB";
mode = "0644";
};
thisCell = {
text = cfg.cellName;
target = "openafs/server/ThisCell";
mode = "0644";
};
buCellServDB = {
enable = useBuCellServDB;
text = mkCellServDB cfg.cellName cfg.roles.backup.cellServDB;
target = "openafs/backup/CellServDB";
};
};
systemd.services = {
openafs-server = {
description = "OpenAFS server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
restartIfChanged = false;
unitConfig.ConditionPathExists = [
"|/etc/openafs/server/KeyFileExt"
];
preStart = ''
mkdir -m 0755 -p /var/openafs
${optionalString (netInfo != null) "cp ${netInfo} /var/openafs/netInfo"}
${optionalString useBuCellServDB "cp ${buCellServDB}"}
'';
serviceConfig = {
ExecStart = "${openafsBin}/bin/bosserver -nofork";
ExecStop = "${openafsBin}/bin/bos shutdown localhost -wait -localauth";
};
};
};
};
}

View File

@@ -0,0 +1,106 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.orangefs.client;
in
{
###### interface
options = {
services.orangefs.client = {
enable = lib.mkEnableOption "OrangeFS client daemon";
extraOptions = lib.mkOption {
type = with lib.types; listOf str;
default = [ ];
description = "Extra command line options for pvfs2-client.";
};
fileSystems = lib.mkOption {
description = ''
The orangefs file systems to be mounted.
This option is preferred over using {option}`fileSystems` directly since
the pvfs client service needs to be running for it to be mounted.
'';
example = [
{
mountPoint = "/orangefs";
target = "tcp://server:3334/orangefs";
}
];
type =
with lib.types;
listOf (
submodule (
{ ... }:
{
options = {
mountPoint = lib.mkOption {
type = lib.types.str;
default = "/orangefs";
description = "Mount point.";
};
options = lib.mkOption {
type = with lib.types; listOf str;
default = [ ];
description = "Mount options";
};
target = lib.mkOption {
type = lib.types.str;
example = "tcp://server:3334/orangefs";
description = "Target URL";
};
};
}
)
);
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
environment.systemPackages = [ pkgs.orangefs ];
boot.supportedFilesystems = [ "pvfs2" ];
boot.kernelModules = [ "orangefs" ];
systemd.services.orangefs-client = {
requires = [ "network-online.target" ];
after = [ "network-online.target" ];
serviceConfig = {
Type = "simple";
ExecStart = ''
${pkgs.orangefs}/bin/pvfs2-client-core \
--logtype=syslog ${lib.concatStringsSep " " cfg.extraOptions}
'';
TimeoutStopSec = "120";
};
};
systemd.mounts = map (fs: {
requires = [ "orangefs-client.service" ];
after = [ "orangefs-client.service" ];
bindsTo = [ "orangefs-client.service" ];
wantedBy = [ "remote-fs.target" ];
type = "pvfs2";
options = lib.concatStringsSep "," fs.options;
what = fs.target;
where = fs.mountPoint;
}) cfg.fileSystems;
};
}

View File

@@ -0,0 +1,251 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.orangefs.server;
aliases = lib.mapAttrsToList (alias: url: alias) cfg.servers;
# Maximum handle number is 2^63
maxHandle = 9223372036854775806;
# One range of handles for each meta/data instance
handleStep = maxHandle / (lib.length aliases) / 2;
fileSystems = lib.mapAttrsToList (name: fs: ''
<FileSystem>
Name ${name}
ID ${toString fs.id}
RootHandle ${toString fs.rootHandle}
${fs.extraConfig}
<MetaHandleRanges>
${lib.concatStringsSep "\n" (
lib.imap0 (
i: alias:
let
begin = i * handleStep + 3;
end = begin + handleStep - 1;
in
"Range ${alias} ${toString begin}-${toString end}"
) aliases
)}
</MetaHandleRanges>
<DataHandleRanges>
${lib.concatStringsSep "\n" (
lib.imap0 (
i: alias:
let
begin = i * handleStep + 3 + (lib.length aliases) * handleStep;
end = begin + handleStep - 1;
in
"Range ${alias} ${toString begin}-${toString end}"
) aliases
)}
</DataHandleRanges>
<StorageHints>
TroveSyncMeta ${if fs.troveSyncMeta then "yes" else "no"}
TroveSyncData ${if fs.troveSyncData then "yes" else "no"}
${fs.extraStorageHints}
</StorageHints>
</FileSystem>
'') cfg.fileSystems;
configFile = ''
<Defaults>
LogType ${cfg.logType}
DataStorageSpace ${cfg.dataStorageSpace}
MetaDataStorageSpace ${cfg.metadataStorageSpace}
BMIModules ${lib.concatStringsSep "," cfg.BMIModules}
${cfg.extraDefaults}
</Defaults>
${cfg.extraConfig}
<Aliases>
${lib.concatStringsSep "\n" (lib.mapAttrsToList (alias: url: "Alias ${alias} ${url}") cfg.servers)}
</Aliases>
${lib.concatStringsSep "\n" fileSystems}
'';
in
{
###### interface
options = {
services.orangefs.server = {
enable = lib.mkEnableOption "OrangeFS server";
logType = lib.mkOption {
type =
with lib.types;
enum [
"file"
"syslog"
];
default = "syslog";
description = "Destination for log messages.";
};
dataStorageSpace = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/data/storage";
description = "Directory for data storage.";
};
metadataStorageSpace = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "/data/meta";
description = "Directory for meta data storage.";
};
BMIModules = lib.mkOption {
type = with lib.types; listOf str;
default = [ "bmi_tcp" ];
example = [
"bmi_tcp"
"bmi_ib"
];
description = "List of BMI modules to load.";
};
extraDefaults = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Extra config for `<Defaults>` section.";
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Extra config for the global section.";
};
servers = lib.mkOption {
type = with lib.types; attrsOf lib.types.str;
default = { };
example = {
node1 = "tcp://node1:3334";
node2 = "tcp://node2:3334";
};
description = "URLs for storage server including port. The attribute names define the server alias.";
};
fileSystems = lib.mkOption {
description = ''
These options will create the `<FileSystem>` sections of config file.
'';
default = {
orangefs = { };
};
example = lib.literalExpression ''
{
fs1 = {
id = 101;
};
fs2 = {
id = 102;
};
}
'';
type =
with lib.types;
attrsOf (
submodule (
{ ... }:
{
options = {
id = lib.mkOption {
type = lib.types.int;
default = 1;
description = "File system ID (must be unique within configuration).";
};
rootHandle = lib.mkOption {
type = lib.types.int;
default = 3;
description = "File system root ID.";
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Extra config for `<FileSystem>` section.";
};
troveSyncMeta = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Sync meta data.";
};
troveSyncData = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Sync data.";
};
extraStorageHints = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Extra config for `<StorageHints>` section.";
};
};
}
)
);
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
environment.systemPackages = [ pkgs.orangefs ];
# orangefs daemon will run as user
users.users.orangefs = {
isSystemUser = true;
group = "orangefs";
};
users.groups.orangefs = { };
# To format the file system the config file is needed.
environment.etc."orangefs/server.conf" = {
text = configFile;
user = "orangefs";
group = "orangefs";
};
systemd.services.orangefs-server = {
wantedBy = [ "multi-user.target" ];
requires = [ "network-online.target" ];
after = [ "network-online.target" ];
serviceConfig = {
# Run as "simple" in foreground mode.
# This is more reliable
ExecStart = ''
${pkgs.orangefs}/bin/pvfs2-server -d \
/etc/orangefs/server.conf
'';
TimeoutStopSec = "120";
User = "orangefs";
Group = "orangefs";
};
};
};
}

View File

@@ -0,0 +1,146 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.rsyncd;
settingsFormat = pkgs.formats.iniWithGlobalSection { };
configFile = settingsFormat.generate "rsyncd.conf" cfg.settings;
in
{
options = {
services.rsyncd = {
enable = lib.mkEnableOption "the rsync daemon";
port = lib.mkOption {
default = 873;
type = lib.types.port;
description = "TCP port the daemon will listen on.";
};
settings = lib.mkOption {
inherit (settingsFormat) type;
default = { };
example = {
globalSection = {
uid = "nobody";
gid = "nobody";
"use chroot" = true;
"max connections" = 4;
address = "0.0.0.0";
};
sections = {
ftp = {
path = "/var/ftp/./pub";
comment = "whole ftp area";
};
cvs = {
path = "/data/cvs";
comment = "CVS repository (requires authentication)";
"auth users" = [
"tridge"
"susan"
];
"secrets file" = "/etc/rsyncd.secrets";
};
};
};
description = ''
Configuration for rsyncd. See
{manpage}`rsyncd.conf(5)`.
'';
};
socketActivated = lib.mkOption {
default = false;
type = lib.types.bool;
description = "If enabled Rsync will be socket-activated rather than run persistently.";
};
};
};
imports = (
map
(
option:
lib.mkRemovedOptionModule [
"services"
"rsyncd"
option
] "This option was removed in favor of `services.rsyncd.settings`."
)
[
"address"
"extraConfig"
"motd"
"user"
"group"
]
);
config = lib.mkIf cfg.enable {
services.rsyncd.settings.globalSection.port = toString cfg.port;
systemd =
let
serviceConfigSecurity = {
ProtectSystem = "full";
PrivateDevices = "on";
NoNewPrivileges = "on";
};
in
{
services.rsync = {
enable = !cfg.socketActivated;
aliases = [ "rsyncd.service" ];
description = "fast remote file copy program daemon";
after = [ "network.target" ];
documentation = [
"man:rsync(1)"
"man:rsyncd.conf(5)"
];
serviceConfig = serviceConfigSecurity // {
ExecStart = "${pkgs.rsync}/bin/rsync --daemon --no-detach --config=${configFile}";
RestartSec = 1;
};
wantedBy = [ "multi-user.target" ];
};
services."rsync@" = {
description = "fast remote file copy program daemon";
after = [ "network.target" ];
serviceConfig = serviceConfigSecurity // {
ExecStart = "${pkgs.rsync}/bin/rsync --daemon --config=${configFile}";
StandardInput = "socket";
StandardOutput = "inherit";
StandardError = "journal";
};
};
sockets.rsync = {
enable = cfg.socketActivated;
description = "socket for fast remote file copy program daemon";
conflicts = [ "rsync.service" ];
listenStreams = [ (toString cfg.port) ];
socketConfig.Accept = true;
wantedBy = [ "sockets.target" ];
};
};
};
# TODO: socket activated rsyncd
}

View File

@@ -0,0 +1,148 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.samba-wsdd;
in
{
options = {
services.samba-wsdd = {
enable = lib.mkEnableOption ''
Web Services Dynamic Discovery host daemon. This enables (Samba) hosts, like your local NAS device,
to be found by Web Service Discovery Clients like Windows
'';
interface = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "eth0";
description = "Interface or address to use.";
};
hoplimit = lib.mkOption {
type = lib.types.nullOr lib.types.int;
default = null;
example = 2;
description = "Hop limit for multicast packets (default = 1).";
};
openFirewall = lib.mkOption {
description = ''
Whether to open the required firewall ports in the firewall.
'';
default = false;
type = lib.types.bool;
};
workgroup = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "HOME";
description = "Set workgroup name (default WORKGROUP).";
};
hostname = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
example = "FILESERVER";
description = "Override (NetBIOS) hostname to be used (default hostname).";
};
domain = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Set domain name (disables workgroup).";
};
discovery = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable discovery operation mode.";
};
listen = lib.mkOption {
type = lib.types.str;
default = "/run/wsdd/wsdd.sock";
description = "Listen on path or localhost port in discovery mode.";
};
extraOptions = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "--shortlog" ];
example = [
"--verbose"
"--no-http"
"--ipv4only"
"--no-host"
];
description = "Additional wsdd options.";
};
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ pkgs.wsdd ];
systemd.services.samba-wsdd = {
description = "Web Services Dynamic Discovery host daemon";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
DynamicUser = true;
Type = "simple";
ExecStart = ''
${pkgs.wsdd}/bin/wsdd ${
lib.optionalString (cfg.interface != null) "--interface '${cfg.interface}'"
} \
${
lib.optionalString (cfg.hoplimit != null) "--hoplimit '${toString cfg.hoplimit}'"
} \
${
lib.optionalString (cfg.workgroup != null) "--workgroup '${cfg.workgroup}'"
} \
${lib.optionalString (cfg.hostname != null) "--hostname '${cfg.hostname}'"} \
${lib.optionalString (cfg.domain != null) "--domain '${cfg.domain}'"} \
${lib.optionalString cfg.discovery "--discovery --listen '${cfg.listen}'"} \
${lib.escapeShellArgs cfg.extraOptions}
'';
# Runtime directory and mode
RuntimeDirectory = "wsdd";
RuntimeDirectoryMode = "0750";
# Access write directories
UMask = "0027";
# Capabilities
CapabilityBoundingSet = "";
# Security
NoNewPrivileges = true;
# Sandboxing
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
PrivateUsers = false;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
"AF_NETLINK"
];
RestrictNamespaces = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
PrivateMounts = true;
# System Call Filtering
SystemCallArchitectures = "native";
SystemCallFilter = "~@cpu-emulation @debug @mount @obsolete @privileged @resources";
};
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ 5357 ];
allowedUDPPorts = [ 3702 ];
};
};
}

View File

@@ -0,0 +1,39 @@
# Samba {#module-services-samba}
[Samba](https://www.samba.org/), a SMB/CIFS file, print, and login server for Unix.
## Basic Usage {#module-services-samba-basic-usage}
A minimal configuration looks like this:
```nix
{ services.samba.enable = true; }
```
This configuration automatically enables `smbd`, `nmbd` and `winbindd` services by default.
## Configuring {#module-services-samba-configuring}
Samba configuration is located in the `/etc/samba/smb.conf` file.
### File share {#module-services-samba-configuring-file-share}
This configuration will configure Samba to serve a `public` file share
which is read-only and accessible without authentication:
```nix
{
services.samba = {
enable = true;
settings = {
"public" = {
"path" = "/public";
"read only" = "yes";
"browseable" = "yes";
"guest ok" = "yes";
"comment" = "Public samba share.";
};
};
};
}
```

View File

@@ -0,0 +1,398 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.samba;
settingsFormat = pkgs.formats.ini {
listToValue = lib.concatMapStringsSep " " (lib.generators.mkValueStringDefault { });
};
# Ensure the global section is always first
globalConfigFile = settingsFormat.generate "smb-global.conf" { global = cfg.settings.global; };
sharesConfigFile = settingsFormat.generate "smb-shares.conf" (
lib.removeAttrs cfg.settings [ "global" ]
);
configFile = pkgs.concatText "smb.conf" [
globalConfigFile
sharesConfigFile
];
in
{
meta = {
doc = ./samba.md;
maintainers = [ lib.maintainers.anthonyroussel ];
};
imports = [
(lib.mkRemovedOptionModule [ "services" "samba" "defaultShare" ] "")
(lib.mkRemovedOptionModule [ "services" "samba" "syncPasswordsByPam" ]
"This option has been removed by upstream, see https://bugzilla.samba.org/show_bug.cgi?id=10669#c10"
)
(lib.mkRemovedOptionModule [ "services" "samba" "configText" ] ''
Use services.samba.settings instead.
This is part of the general move to use structured settings instead of raw
text for config as introduced by RFC0042:
https://github.com/NixOS/rfcs/blob/master/rfcs/0042-config-option.md
'')
(lib.mkRemovedOptionModule [
"services"
"samba"
"extraConfig"
] "Use services.samba.settings instead.")
(lib.mkRenamedOptionModule
[ "services" "samba" "invalidUsers" ]
[ "services" "samba" "settings" "global" "invalid users" ]
)
(lib.mkRenamedOptionModule
[ "services" "samba" "securityType" ]
[ "services" "samba" "settings" "global" "security" ]
)
(lib.mkRenamedOptionModule [ "services" "samba" "shares" ] [ "services" "samba" "settings" ])
(lib.mkRenamedOptionModule
[ "services" "samba" "enableWinbindd" ]
[ "services" "samba" "winbindd" "enable" ]
)
(lib.mkRenamedOptionModule
[ "services" "samba" "enableNmbd" ]
[ "services" "samba" "nmbd" "enable" ]
)
];
###### interface
options = {
services.samba = {
enable = lib.mkEnableOption "Samba, the SMB/CIFS protocol";
package = lib.mkPackageOption pkgs "samba" {
example = "samba4Full";
};
openFirewall = lib.mkEnableOption "opening the default ports in the firewall for Samba";
smbd = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to enable Samba's smbd daemon.";
};
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Extra arguments to pass to the smbd service.";
};
};
nmbd = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to enable Samba's nmbd, which replies to NetBIOS over IP name
service requests. It also participates in the browsing protocols
which make up the Windows "Network Neighborhood" view.
'';
};
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Extra arguments to pass to the nmbd service.";
};
};
winbindd = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to enable Samba's winbindd, which provides a number of services
to the Name Service Switch capability found in most modern C libraries,
to arbitrary applications via PAM and ntlm_auth and to Samba itself.
'';
};
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = "Extra arguments to pass to the winbindd service.";
};
};
usershares = {
enable = lib.mkEnableOption "user-configurable Samba shares";
group = lib.mkOption {
type = lib.types.str;
default = "samba";
description = ''
Name of the group members of which will be allowed to create usershares.
The group will be created automatically.
'';
};
};
nsswins = lib.mkEnableOption ''
WINS NSS (Name Service Switch) plug-in.
Enabling it allows applications to resolve WINS/NetBIOS names (a.k.a.
Windows machine names) by transparently querying the winbindd daemon
'';
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
options = {
global.security = lib.mkOption {
type = lib.types.enum [
"auto"
"user"
"domain"
"ads"
];
default = "user";
description = "Samba security type.";
};
global."invalid users" = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "root" ];
description = "List of users who are denied to login via Samba.";
};
global."passwd program" = lib.mkOption {
type = lib.types.str;
default = "/run/wrappers/bin/passwd %u";
description = "Path to a program that can be used to set UNIX user passwords.";
};
};
};
default = {
"global" = {
"security" = "user";
"passwd program" = "/run/wrappers/bin/passwd %u";
"invalid users" = [ "root" ];
};
};
example = {
"global" = {
"security" = "user";
"passwd program" = "/run/wrappers/bin/passwd %u";
"invalid users" = [ "root" ];
};
"public" = {
"path" = "/srv/public";
"read only" = "yes";
"browseable" = "yes";
"guest ok" = "yes";
"comment" = "Public samba share.";
};
};
description = ''
Configuration file for the Samba suite in ini format.
This file is located in /etc/samba/smb.conf
Refer to <https://www.samba.org/samba/docs/current/man-html/smb.conf.5.html>
for all available options.
'';
};
};
};
###### implementation
config = lib.mkMerge [
{
assertions = [
{
assertion = cfg.nsswins -> cfg.winbindd.enable;
message = "If services.samba.nsswins is enabled, then services.samba.winbindd.enable must also be enabled";
}
];
}
(lib.mkIf cfg.enable {
environment.etc."samba/smb.conf".source = configFile;
system.nssModules = lib.optional cfg.nsswins cfg.package;
system.nssDatabases.hosts = lib.optional cfg.nsswins "wins";
systemd = {
slices.system-samba = {
description = "Samba (SMB Networking Protocol) Slice";
};
targets.samba = {
description = "Samba Server";
after = [ "network.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
};
tmpfiles.rules = [
"d /var/lock/samba - - - - -"
"d /var/log/samba - - - - -"
"d /var/cache/samba - - - - -"
"d /var/lib/samba/private - - - - -"
];
};
security.pam.services.samba = { };
environment.systemPackages = [ cfg.package ];
# Like other mount* related commands that need the setuid bit, this is
# required too.
security.wrappers."mount.cifs" = {
program = "mount.cifs";
source = "${lib.getBin pkgs.cifs-utils}/bin/mount.cifs";
owner = "root";
group = "root";
setuid = true;
};
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [
139
445
];
networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall [
137
138
];
})
(lib.mkIf (cfg.enable && cfg.nmbd.enable) {
systemd.services.samba-nmbd = {
description = "Samba NMB Daemon";
documentation = [
"man:nmbd(8)"
"man:samba(7)"
"man:smb.conf(5)"
];
after = [
"network.target"
"network-online.target"
];
partOf = [ "samba.target" ];
wantedBy = [ "samba.target" ];
wants = [ "network-online.target" ];
environment.LD_LIBRARY_PATH = config.system.nssModules.path;
serviceConfig = {
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
ExecStart = "${cfg.package}/sbin/nmbd --foreground --no-process-group ${lib.escapeShellArgs cfg.nmbd.extraArgs}";
LimitCORE = "infinity";
PIDFile = "/run/samba/nmbd.pid";
Slice = "system-samba.slice";
Type = "notify";
};
unitConfig.RequiresMountsFor = "/var/lib/samba";
restartTriggers = [ configFile ];
};
})
(lib.mkIf (cfg.enable && cfg.smbd.enable) {
systemd.services.samba-smbd = {
description = "Samba SMB Daemon";
documentation = [
"man:smbd(8)"
"man:samba(7)"
"man:smb.conf(5)"
];
after = [
"network.target"
"network-online.target"
]
++ lib.optionals (cfg.nmbd.enable) [
"samba-nmbd.service"
]
++ lib.optionals (cfg.winbindd.enable) [
"samba-winbindd.service"
];
partOf = [ "samba.target" ];
wantedBy = [ "samba.target" ];
wants = [ "network-online.target" ];
environment.LD_LIBRARY_PATH = config.system.nssModules.path;
serviceConfig = {
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
ExecStart = "${cfg.package}/sbin/smbd --foreground --no-process-group ${lib.escapeShellArgs cfg.smbd.extraArgs}";
LimitCORE = "infinity";
LimitNOFILE = 16384;
PIDFile = "/run/samba/smbd.pid";
Slice = "system-samba.slice";
Type = "notify";
};
unitConfig.RequiresMountsFor = "/var/lib/samba";
restartTriggers = [ configFile ];
};
})
(lib.mkIf (cfg.enable && cfg.winbindd.enable) {
systemd.services.samba-winbindd = {
description = "Samba Winbind Daemon";
documentation = [
"man:winbindd(8)"
"man:samba(7)"
"man:smb.conf(5)"
];
after = [
"network.target"
]
++ lib.optionals (cfg.nmbd.enable) [
"samba-nmbd.service"
];
partOf = [ "samba.target" ];
wantedBy = [ "samba.target" ];
environment.LD_LIBRARY_PATH = config.system.nssModules.path;
serviceConfig = {
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
ExecStart = "${cfg.package}/sbin/winbindd --foreground --no-process-group ${lib.escapeShellArgs cfg.winbindd.extraArgs}";
LimitCORE = "infinity";
PIDFile = "/run/samba/winbindd.pid";
Slice = "system-samba.slice";
Type = "notify";
};
unitConfig.RequiresMountsFor = "/var/lib/samba";
restartTriggers = [ configFile ];
};
})
(lib.mkIf (cfg.enable && cfg.usershares.enable) {
users.groups.${cfg.usershares.group} = { };
systemd.tmpfiles.settings."50-samba-usershares"."/var/lib/samba/usershares".d = {
user = "root";
group = cfg.usershares.group;
mode = "1775"; # sticky so users can't delete others' shares
};
# set some reasonable defaults
services.samba.settings.global = lib.mkDefault {
"usershare path" = "/var/lib/samba/usershares";
"usershare max shares" = 100; # high enough to be considered ~unlimited
"usershare allow guests" = true;
};
})
];
}

View File

@@ -0,0 +1,288 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.saunafs;
settingsFormat =
let
listSep = " ";
allowedTypes = with lib.types; [
bool
int
float
str
];
valueToString =
val:
if lib.isList val then
lib.concatStringsSep listSep (map (x: valueToString x) val)
else if lib.isBool val then
(if val then "1" else "0")
else
toString val;
in
{
type =
let
valueType =
lib.types.oneOf (
[
(lib.types.listOf valueType)
]
++ allowedTypes
)
// {
description = "Flat key-value file";
};
in
lib.types.attrsOf valueType;
generate =
name: value:
pkgs.writeText name (
lib.concatStringsSep "\n" (lib.mapAttrsToList (key: val: "${key} = ${valueToString val}") value)
);
};
initTool = pkgs.writeShellScriptBin "sfsmaster-init" ''
if [ ! -e ${cfg.master.settings.DATA_PATH}/metadata.sfs ]; then
cp --update=none ${pkgs.saunafs}/var/lib/saunafs/metadata.sfs.empty ${cfg.master.settings.DATA_PATH}/metadata.sfs
chmod +w ${cfg.master.settings.DATA_PATH}/metadata.sfs
fi
'';
# master config file
masterCfg = settingsFormat.generate "sfsmaster.cfg" cfg.master.settings;
# metalogger config file
metaloggerCfg = settingsFormat.generate "sfsmetalogger.cfg" cfg.metalogger.settings;
# chunkserver config file
chunkserverCfg = settingsFormat.generate "sfschunkserver.cfg" cfg.chunkserver.settings;
# generic template for all daemons
systemdService = name: extraConfig: configFile: {
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [
"network.target"
"network-online.target"
];
serviceConfig = {
Type = "forking";
ExecStart = "${pkgs.saunafs}/bin/sfs${name} -c ${configFile} start";
ExecStop = "${pkgs.saunafs}/bin/sfs${name} -c ${configFile} stop";
ExecReload = "${pkgs.saunafs}/bin/sfs${name} -c ${configFile} reload";
}
// extraConfig;
};
in
{
###### interface
options = {
services.saunafs = {
masterHost = lib.mkOption {
type = lib.types.str;
default = null;
description = "IP or hostname name of master host.";
};
sfsUser = lib.mkOption {
type = lib.types.str;
default = "saunafs";
description = "Run daemons as user.";
};
client.enable = lib.mkEnableOption "Saunafs client";
master = {
enable = lib.mkOption {
type = lib.types.bool;
description = ''
Enable Saunafs master daemon.
You need to run `sfsmaster-init` on a freshly installed master server to
initialize the `DATA_PATH` directory.
'';
default = false;
};
exports = lib.mkOption {
type = with lib.types; listOf str;
default = null;
description = "Paths to exports file (see {manpage}`sfsexports.cfg(5)`).";
example = lib.literalExpression ''
[ "* / rw,alldirs,admin,maproot=0:0" ];
'';
};
openFirewall = lib.mkOption {
type = lib.types.bool;
description = "Whether to automatically open the necessary ports in the firewall.";
default = false;
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
options.DATA_PATH = lib.mkOption {
type = lib.types.str;
default = "/var/lib/saunafs/master";
description = "Data storage directory.";
};
};
description = "Contents of config file ({manpage}`sfsmaster.cfg(5)`).";
};
};
metalogger = {
enable = lib.mkEnableOption "Saunafs metalogger daemon";
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
options.DATA_PATH = lib.mkOption {
type = lib.types.str;
default = "/var/lib/saunafs/metalogger";
description = "Data storage directory";
};
};
description = "Contents of metalogger config file (see {manpage}`sfsmetalogger.cfg(5)`).";
};
};
chunkserver = {
enable = lib.mkEnableOption "Saunafs chunkserver daemon";
openFirewall = lib.mkOption {
type = lib.types.bool;
description = "Whether to automatically open the necessary ports in the firewall.";
default = false;
};
hdds = lib.mkOption {
type = with lib.types; listOf str;
default = null;
example = lib.literalExpression ''
[ "/mnt/hdd1" ];
'';
description = ''
Mount points to be used by chunkserver for storage (see {manpage}`sfshdd.cfg(5)`).
Note, that these mount points must writeable by the user defined by the saunafs user.
'';
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
options.DATA_PATH = lib.mkOption {
type = lib.types.str;
default = "/var/lib/saunafs/chunkserver";
description = "Directory for chunck meta data";
};
};
description = "Contents of chunkserver config file (see {manpage}`sfschunkserver.cfg(5)`).";
};
};
};
};
###### implementation
config =
lib.mkIf (cfg.client.enable || cfg.master.enable || cfg.metalogger.enable || cfg.chunkserver.enable)
{
warnings = [
(lib.mkIf (cfg.sfsUser == "root") "Running saunafs services as root is not recommended.")
];
# Service settings
services.saunafs = {
master.settings = lib.mkIf cfg.master.enable {
WORKING_USER = cfg.sfsUser;
EXPORTS_FILENAME = toString (
pkgs.writeText "sfsexports.cfg" (lib.concatStringsSep "\n" cfg.master.exports)
);
};
metalogger.settings = lib.mkIf cfg.metalogger.enable {
WORKING_USER = cfg.sfsUser;
MASTER_HOST = cfg.masterHost;
};
chunkserver.settings = lib.mkIf cfg.chunkserver.enable {
WORKING_USER = cfg.sfsUser;
MASTER_HOST = cfg.masterHost;
HDD_CONF_FILENAME = toString (
pkgs.writeText "sfshdd.cfg" (lib.concatStringsSep "\n" cfg.chunkserver.hdds)
);
};
};
# Create system user account for daemons
users =
lib.mkIf
(cfg.sfsUser != "root" && (cfg.master.enable || cfg.metalogger.enable || cfg.chunkserver.enable))
{
users."${cfg.sfsUser}" = {
isSystemUser = true;
description = "saunafs daemon user";
group = "saunafs";
};
groups."${cfg.sfsUser}" = { };
};
environment.systemPackages =
(lib.optional cfg.client.enable pkgs.saunafs) ++ (lib.optional cfg.master.enable initTool);
networking.firewall.allowedTCPPorts =
(lib.optionals cfg.master.openFirewall [
9419
9420
9421
])
++ (lib.optional cfg.chunkserver.openFirewall 9422);
# Ensure storage directories exist
systemd.tmpfiles.rules =
lib.optional cfg.master.enable "d ${cfg.master.settings.DATA_PATH} 0700 ${cfg.sfsUser} ${cfg.sfsUser} -"
++ lib.optional cfg.metalogger.enable "d ${cfg.metalogger.settings.DATA_PATH} 0700 ${cfg.sfsUser} ${cfg.sfsUser} -"
++ lib.optional cfg.chunkserver.enable "d ${cfg.chunkserver.settings.DATA_PATH} 0700 ${cfg.sfsUser} ${cfg.sfsUser} -";
# Service definitions
systemd.services.sfs-master = lib.mkIf cfg.master.enable (
systemdService "master" {
TimeoutStartSec = 1800;
TimeoutStopSec = 1800;
Restart = "no";
} masterCfg
);
systemd.services.sfs-metalogger = lib.mkIf cfg.metalogger.enable (
systemdService "metalogger" { Restart = "on-abort"; } metaloggerCfg
);
systemd.services.sfs-chunkserver = lib.mkIf cfg.chunkserver.enable (
systemdService "chunkserver" { Restart = "on-abort"; } chunkserverCfg
);
};
}

View File

@@ -0,0 +1,374 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.tahoe;
in
{
options.services.tahoe = {
introducers = lib.mkOption {
default = { };
type =
with lib.types;
attrsOf (submodule {
options = {
nickname = lib.mkOption {
type = lib.types.str;
description = ''
The nickname of this Tahoe introducer.
'';
};
tub.port = lib.mkOption {
default = 3458;
type = lib.types.port;
description = ''
The port on which the introducer will listen.
'';
};
tub.location = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = ''
The external location that the introducer should listen on.
If specified, the port should be included.
'';
};
package = lib.mkPackageOption pkgs "tahoelafs" { };
};
});
description = ''
The Tahoe introducers.
'';
};
nodes = lib.mkOption {
default = { };
type =
with lib.types;
attrsOf (submodule {
options = {
nickname = lib.mkOption {
type = lib.types.str;
description = ''
The nickname of this Tahoe node.
'';
};
tub.port = lib.mkOption {
default = 3457;
type = lib.types.port;
description = ''
The port on which the tub will listen.
This is the correct setting to tweak if you want Tahoe's storage
system to listen on a different port.
'';
};
tub.location = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = ''
The external location that the node should listen on.
This is the setting to tweak if there are multiple interfaces
and you want to alter which interface Tahoe is advertising.
If specified, the port should be included.
'';
};
web.port = lib.mkOption {
default = 3456;
type = lib.types.port;
description = ''
The port on which the Web server will listen.
This is the correct setting to tweak if you want Tahoe's WUI to
listen on a different port.
'';
};
client.introducer = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = ''
The furl for a Tahoe introducer node.
Like all furls, keep this safe and don't share it.
'';
};
client.helper = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = ''
The furl for a Tahoe helper node.
Like all furls, keep this safe and don't share it.
'';
};
client.shares.needed = lib.mkOption {
default = 3;
type = lib.types.int;
description = ''
The number of shares required to reconstitute a file.
'';
};
client.shares.happy = lib.mkOption {
default = 7;
type = lib.types.int;
description = ''
The number of distinct storage nodes required to store
a file.
'';
};
client.shares.total = lib.mkOption {
default = 10;
type = lib.types.int;
description = ''
The number of shares required to store a file.
'';
};
storage.enable = lib.mkEnableOption "storage service";
storage.reservedSpace = lib.mkOption {
default = "1G";
type = lib.types.str;
description = ''
The amount of filesystem space to not use for storage.
'';
};
helper.enable = lib.mkEnableOption "helper service";
sftpd.enable = lib.mkEnableOption "SFTP service";
sftpd.port = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.port;
description = ''
The port on which the SFTP server will listen.
This is the correct setting to tweak if you want Tahoe's SFTP
daemon to listen on a different port.
'';
};
sftpd.hostPublicKeyFile = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.path;
description = ''
Path to the SSH host public key.
'';
};
sftpd.hostPrivateKeyFile = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.path;
description = ''
Path to the SSH host private key.
'';
};
sftpd.accounts.file = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.path;
description = ''
Path to the accounts file.
'';
};
sftpd.accounts.url = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = ''
URL of the accounts server.
'';
};
package = lib.mkPackageOption pkgs "tahoelafs" { };
};
});
description = ''
The Tahoe nodes.
'';
};
};
config = lib.mkMerge [
(lib.mkIf (cfg.introducers != { }) {
environment = {
etc = lib.flip lib.mapAttrs' cfg.introducers (
node: settings:
lib.nameValuePair "tahoe-lafs/introducer-${node}.cfg" {
mode = "0444";
text = ''
# This configuration is generated by Nix. Edit at your own
# peril; here be dragons.
[node]
nickname = ${settings.nickname}
tub.port = ${toString settings.tub.port}
${lib.optionalString (settings.tub.location != null) "tub.location = ${settings.tub.location}"}
'';
}
);
# Actually require Tahoe, so that we will have it installed.
systemPackages = lib.flip lib.mapAttrsToList cfg.introducers (node: settings: settings.package);
};
# Open up the firewall.
# networking.firewall.allowedTCPPorts = lib.flip lib.mapAttrsToList cfg.introducers
# (node: settings: settings.tub.port);
systemd.services = lib.flip lib.mapAttrs' cfg.introducers (
node: settings:
let
pidfile = "/run/tahoe.introducer-${node}.pid";
# This is a directory, but it has no trailing slash. Tahoe commands
# get antsy when there's a trailing slash.
nodedir = "/var/db/tahoe-lafs/introducer-${node}";
in
lib.nameValuePair "tahoe.introducer-${node}" {
description = "Tahoe LAFS node ${node}";
documentation = [ "info:tahoe-lafs" ];
wantedBy = [ "multi-user.target" ];
path = [ settings.package ];
restartTriggers = [
config.environment.etc."tahoe-lafs/introducer-${node}.cfg".source
];
serviceConfig = {
Type = "simple";
PIDFile = pidfile;
# Believe it or not, Tahoe is very brittle about the order of
# arguments to $(tahoe run). The node directory must come first,
# and arguments which alter Twisted's behavior come afterwards.
ExecStart = ''
${settings.package}/bin/tahoe run ${lib.escapeShellArg nodedir} --pidfile=${lib.escapeShellArg pidfile}
'';
};
preStart = ''
if [ ! -d ${lib.escapeShellArg nodedir} ]; then
mkdir -p /var/db/tahoe-lafs
# See https://github.com/NixOS/nixpkgs/issues/25273
tahoe create-introducer \
--hostname="${config.networking.hostName}" \
${lib.escapeShellArg nodedir}
fi
# Tahoe has created a predefined tahoe.cfg which we must now
# scribble over.
# XXX I thought that a symlink would work here, but it doesn't, so
# we must do this on every prestart. Fixes welcome.
# rm ${nodedir}/tahoe.cfg
# ln -s /etc/tahoe-lafs/introducer-${node}.cfg ${nodedir}/tahoe.cfg
cp /etc/tahoe-lafs/introducer-"${node}".cfg ${lib.escapeShellArg nodedir}/tahoe.cfg
'';
}
);
users.users = lib.flip lib.mapAttrs' cfg.introducers (
node: _:
lib.nameValuePair "tahoe.introducer-${node}" {
description = "Tahoe node user for introducer ${node}";
isSystemUser = true;
}
);
})
(lib.mkIf (cfg.nodes != { }) {
environment = {
etc = lib.flip lib.mapAttrs' cfg.nodes (
node: settings:
lib.nameValuePair "tahoe-lafs/${node}.cfg" {
mode = "0444";
text = ''
# This configuration is generated by Nix. Edit at your own
# peril; here be dragons.
[node]
nickname = ${settings.nickname}
tub.port = ${toString settings.tub.port}
${lib.optionalString (settings.tub.location != null) "tub.location = ${settings.tub.location}"}
# This is a Twisted endpoint. Twisted Web doesn't work on
# non-TCP. ~ C.
web.port = tcp:${toString settings.web.port}
[client]
${lib.optionalString (
settings.client.introducer != null
) "introducer.furl = ${settings.client.introducer}"}
${lib.optionalString (settings.client.helper != null) "helper.furl = ${settings.client.helper}"}
shares.needed = ${toString settings.client.shares.needed}
shares.happy = ${toString settings.client.shares.happy}
shares.total = ${toString settings.client.shares.total}
[storage]
enabled = ${lib.boolToString settings.storage.enable}
reserved_space = ${settings.storage.reservedSpace}
[helper]
enabled = ${lib.boolToString settings.helper.enable}
[sftpd]
enabled = ${lib.boolToString settings.sftpd.enable}
${lib.optionalString (settings.sftpd.port != null) "port = ${toString settings.sftpd.port}"}
${lib.optionalString (
settings.sftpd.hostPublicKeyFile != null
) "host_pubkey_file = ${settings.sftpd.hostPublicKeyFile}"}
${lib.optionalString (
settings.sftpd.hostPrivateKeyFile != null
) "host_privkey_file = ${settings.sftpd.hostPrivateKeyFile}"}
${lib.optionalString (
settings.sftpd.accounts.file != null
) "accounts.file = ${settings.sftpd.accounts.file}"}
${lib.optionalString (
settings.sftpd.accounts.url != null
) "accounts.url = ${settings.sftpd.accounts.url}"}
'';
}
);
# Actually require Tahoe, so that we will have it installed.
systemPackages = lib.flip lib.mapAttrsToList cfg.nodes (node: settings: settings.package);
};
# Open up the firewall.
# networking.firewall.allowedTCPPorts = lib.flip lib.mapAttrsToList cfg.nodes
# (node: settings: settings.tub.port);
systemd.services = lib.flip lib.mapAttrs' cfg.nodes (
node: settings:
let
pidfile = "/run/tahoe.${node}.pid";
# This is a directory, but it has no trailing slash. Tahoe commands
# get antsy when there's a trailing slash.
nodedir = "/var/db/tahoe-lafs/${node}";
in
lib.nameValuePair "tahoe.${node}" {
description = "Tahoe LAFS node ${node}";
documentation = [ "info:tahoe-lafs" ];
wantedBy = [ "multi-user.target" ];
path = [ settings.package ];
restartTriggers = [
config.environment.etc."tahoe-lafs/${node}.cfg".source
];
serviceConfig = {
Type = "simple";
PIDFile = pidfile;
# Believe it or not, Tahoe is very brittle about the order of
# arguments to $(tahoe run). The node directory must come first,
# and arguments which alter Twisted's behavior come afterwards.
ExecStart = ''
${settings.package}/bin/tahoe run ${lib.escapeShellArg nodedir} --pidfile=${lib.escapeShellArg pidfile}
'';
};
preStart = ''
if [ ! -d ${lib.escapeShellArg nodedir} ]; then
mkdir -p /var/db/tahoe-lafs
tahoe create-node --hostname=localhost ${lib.escapeShellArg nodedir}
fi
# Tahoe has created a predefined tahoe.cfg which we must now
# scribble over.
# XXX I thought that a symlink would work here, but it doesn't, so
# we must do this on every prestart. Fixes welcome.
# rm ${nodedir}/tahoe.cfg
# ln -s /etc/tahoe-lafs/${lib.escapeShellArg node}.cfg ${nodedir}/tahoe.cfg
cp /etc/tahoe-lafs/${lib.escapeShellArg node}.cfg ${lib.escapeShellArg nodedir}/tahoe.cfg
'';
}
);
users.users = lib.flip lib.mapAttrs' cfg.nodes (
node: _:
lib.nameValuePair "tahoe.${node}" {
description = "Tahoe node user for node ${node}";
isSystemUser = true;
}
);
})
];
}

View File

@@ -0,0 +1,78 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.u9fs;
in
{
options = {
services.u9fs = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to run the u9fs 9P server for Unix.";
};
listenStreams = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "564" ];
example = [ "192.168.16.1:564" ];
description = ''
Sockets to listen for clients on.
See {command}`man 5 systemd.socket` for socket syntax.
'';
};
user = lib.mkOption {
type = lib.types.str;
default = "nobody";
description = "User to run u9fs under.";
};
extraArgs = lib.mkOption {
type = lib.types.str;
default = "";
example = "-a none";
description = ''
Extra arguments to pass on invocation,
see {command}`man 4 u9fs`
'';
};
};
};
config = lib.mkIf cfg.enable {
systemd = {
sockets.u9fs = {
description = "U9fs Listening Socket";
wantedBy = [ "sockets.target" ];
after = [ "network.target" ];
inherit (cfg) listenStreams;
socketConfig.Accept = "yes";
};
services."u9fs@" = {
description = "9P Protocol Server";
reloadIfChanged = true;
requires = [ "u9fs.socket" ];
serviceConfig = {
ExecStart = "-${pkgs.u9fs}/bin/u9fs ${cfg.extraArgs}";
StandardInput = "socket";
StandardError = "journal";
User = cfg.user;
AmbientCapabilities = "cap_setuid cap_setgid";
};
};
};
};
}

View File

@@ -0,0 +1,152 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.webdav-server-rs;
format = pkgs.formats.toml { };
settings = lib.recursiveUpdate {
server.uid = config.users.users."${cfg.user}".uid;
server.gid = config.users.groups."${cfg.group}".gid;
} cfg.settings;
in
{
options = {
services.webdav-server-rs = {
enable = lib.mkEnableOption "WebDAV server";
user = lib.mkOption {
type = lib.types.str;
default = "webdav";
description = "User to run under when setuid is not enabled.";
};
group = lib.mkOption {
type = lib.types.str;
default = "webdav";
description = "Group to run under when setuid is not enabled.";
};
debug = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable debug mode.";
};
settings = lib.mkOption {
type = format.type;
default = { };
description = ''
Attrset that is converted and passed as config file. Available
options can be found at
[here](https://github.com/miquels/webdav-server-rs/blob/master/webdav-server.toml).
'';
example = lib.literalExpression ''
{
server.listen = [ "0.0.0.0:4918" "[::]:4918" ];
accounts = {
auth-type = "htpasswd.default";
acct-type = "unix";
};
htpasswd.default = {
htpasswd = "/etc/htpasswd";
};
location = [
{
route = [ "/public/*path" ];
directory = "/srv/public";
handler = "filesystem";
methods = [ "webdav-ro" ];
autoindex = true;
auth = "false";
}
{
route = [ "/user/:user/*path" ];
directory = "~";
handler = "filesystem";
methods = [ "webdav-rw" ];
autoindex = true;
auth = "true";
setuid = true;
}
];
}
'';
};
configFile = lib.mkOption {
type = lib.types.path;
default = format.generate "webdav-server.toml" settings;
defaultText = "Config file generated from services.webdav-server-rs.settings";
description = ''
Path to config file. If this option is set, it will override any
configuration done in services.webdav-server-rs.settings.
'';
example = "/etc/webdav-server.toml";
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = lib.hasAttr cfg.user config.users.users && config.users.users."${cfg.user}".uid != null;
message = "users.users.${cfg.user} and users.users.${cfg.user}.uid must be defined.";
}
{
assertion =
lib.hasAttr cfg.group config.users.groups && config.users.groups."${cfg.group}".gid != null;
message = "users.groups.${cfg.group} and users.groups.${cfg.group}.gid must be defined.";
}
];
users.users = lib.optionalAttrs (cfg.user == "webdav") {
webdav = {
description = "WebDAV user";
group = cfg.group;
uid = config.ids.uids.webdav;
};
};
users.groups = lib.optionalAttrs (cfg.group == "webdav") {
webdav.gid = config.ids.gids.webdav;
};
systemd.services.webdav-server-rs = {
description = "WebDAV server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${pkgs.webdav-server-rs}/bin/webdav-server ${lib.optionalString cfg.debug "--debug"} -c ${cfg.configFile}";
CapabilityBoundingSet = [
"CAP_SETUID"
"CAP_SETGID"
];
NoExecPaths = [ "/" ];
ExecPaths = [ "/nix/store" ];
# This program actively detects if it is running in root user account
# when it starts and uses root privilege to switch process uid to
# respective unix user when a user logs in. Maybe we can enable
# DynamicUser in the future when it's able to detect CAP_SETUID and
# CAP_SETGID capabilities.
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = true;
};
};
};
meta.maintainers = with lib.maintainers; [ pmy ];
}

View File

@@ -0,0 +1,110 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.webdav;
format = pkgs.formats.yaml { };
in
{
options = {
services.webdav = {
enable = lib.mkEnableOption "WebDAV server";
package = lib.mkPackageOption pkgs "webdav" { };
user = lib.mkOption {
type = lib.types.str;
default = "webdav";
description = "User account under which WebDAV runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "webdav";
description = "Group under which WebDAV runs.";
};
settings = lib.mkOption {
type = format.type;
default = { };
description = ''
Attrset that is converted and passed as config file. Available options
can be found at
[here](https://github.com/hacdias/webdav).
This program supports reading username and password configuration
from environment variables, so it's strongly recommended to store
username and password in a separate
[EnvironmentFile](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#EnvironmentFile=).
This prevents adding secrets to the world-readable Nix store.
'';
example = lib.literalExpression ''
{
address = "0.0.0.0";
port = 8080;
scope = "/srv/public";
modify = true;
auth = true;
users = [
{
username = "{env}ENV_USERNAME";
password = "{env}ENV_PASSWORD";
}
];
}
'';
};
configFile = lib.mkOption {
type = lib.types.path;
default = format.generate "webdav.yaml" cfg.settings;
defaultText = "Config file generated from services.webdav.settings";
description = ''
Path to config file. If this option is set, it will override any
configuration done in options.services.webdav.settings.
'';
example = "/etc/webdav/config.yaml";
};
environmentFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Environment file as defined in {manpage}`systemd.exec(5)`.
'';
};
};
};
config = lib.mkIf cfg.enable {
users.users = lib.mkIf (cfg.user == "webdav") {
webdav = {
description = "WebDAV daemon user";
group = cfg.group;
uid = config.ids.uids.webdav;
};
};
users.groups = lib.mkIf (cfg.group == "webdav") {
webdav.gid = config.ids.gids.webdav;
};
systemd.services.webdav = {
description = "WebDAV server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${lib.getExe cfg.package} -c ${cfg.configFile}";
Restart = "on-failure";
User = cfg.user;
Group = cfg.group;
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
};
};
};
meta.maintainers = with lib.maintainers; [ pmy ];
}

View File

@@ -0,0 +1,516 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.xtreemfs;
xtreemfs = pkgs.xtreemfs;
home = cfg.homeDir;
startupScript =
class: configPath:
pkgs.writeScript "xtreemfs-osd.sh" ''
#! ${pkgs.runtimeShell}
JAVA_HOME="${pkgs.jdk}"
JAVADIR="${xtreemfs}/share/java"
JAVA_CALL="$JAVA_HOME/bin/java -ea -cp $JAVADIR/XtreemFS.jar:$JAVADIR/BabuDB.jar:$JAVADIR/Flease.jar:$JAVADIR/protobuf-java-2.5.0.jar:$JAVADIR/Foundation.jar:$JAVADIR/jdmkrt.jar:$JAVADIR/jdmktk.jar:$JAVADIR/commons-codec-1.3.jar"
$JAVA_CALL ${class} ${configPath}
'';
dirReplicationConfig = pkgs.writeText "xtreemfs-dir-replication-plugin.properties" ''
babudb.repl.backupDir = ${home}/server-repl-dir
plugin.jar = ${xtreemfs}/share/java/BabuDB_replication_plugin.jar
babudb.repl.dependency.0 = ${xtreemfs}/share/java/Flease.jar
${cfg.dir.replication.extraConfig}
'';
dirConfig = pkgs.writeText "xtreemfs-dir-config.properties" ''
uuid = ${cfg.dir.uuid}
listen.port = ${toString cfg.dir.port}
${lib.optionalString (cfg.dir.address != "") "listen.address = ${cfg.dir.address}"}
http_port = ${toString cfg.dir.httpPort}
babudb.baseDir = ${home}/dir/database
babudb.logDir = ${home}/dir/db-log
babudb.sync = ${if cfg.dir.replication.enable then "FDATASYNC" else cfg.dir.syncMode}
${lib.optionalString cfg.dir.replication.enable "babudb.plugin.0 = ${dirReplicationConfig}"}
${cfg.dir.extraConfig}
'';
mrcReplicationConfig = pkgs.writeText "xtreemfs-mrc-replication-plugin.properties" ''
babudb.repl.backupDir = ${home}/server-repl-mrc
plugin.jar = ${xtreemfs}/share/java/BabuDB_replication_plugin.jar
babudb.repl.dependency.0 = ${xtreemfs}/share/java/Flease.jar
${cfg.mrc.replication.extraConfig}
'';
mrcConfig = pkgs.writeText "xtreemfs-mrc-config.properties" ''
uuid = ${cfg.mrc.uuid}
listen.port = ${toString cfg.mrc.port}
${lib.optionalString (cfg.mrc.address != "") "listen.address = ${cfg.mrc.address}"}
http_port = ${toString cfg.mrc.httpPort}
babudb.baseDir = ${home}/mrc/database
babudb.logDir = ${home}/mrc/db-log
babudb.sync = ${if cfg.mrc.replication.enable then "FDATASYNC" else cfg.mrc.syncMode}
${lib.optionalString cfg.mrc.replication.enable "babudb.plugin.0 = ${mrcReplicationConfig}"}
${cfg.mrc.extraConfig}
'';
osdConfig = pkgs.writeText "xtreemfs-osd-config.properties" ''
uuid = ${cfg.osd.uuid}
listen.port = ${toString cfg.osd.port}
${lib.optionalString (cfg.osd.address != "") "listen.address = ${cfg.osd.address}"}
http_port = ${toString cfg.osd.httpPort}
object_dir = ${home}/osd/
${cfg.osd.extraConfig}
'';
optionalDir = lib.optionals cfg.dir.enable [ "xtreemfs-dir.service" ];
systemdOptionalDependencies = {
after = [ "network.target" ] ++ optionalDir;
wantedBy = [ "multi-user.target" ] ++ optionalDir;
};
in
{
###### interface
options = {
services.xtreemfs = {
enable = lib.mkEnableOption "XtreemFS";
homeDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/xtreemfs";
description = ''
XtreemFS home dir for the xtreemfs user.
'';
};
dir = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to enable XtreemFS DIR service.
'';
};
uuid = lib.mkOption {
example = "eacb6bab-f444-4ebf-a06a-3f72d7465e40";
type = lib.types.str;
description = ''
Must be set to a unique identifier, preferably a UUID according to
RFC 4122. UUIDs can be generated with `uuidgen` command, found in
the `util-linux` package.
'';
};
port = lib.mkOption {
default = 32638;
type = lib.types.port;
description = ''
The port to listen on for incoming connections (TCP).
'';
};
address = lib.mkOption {
type = lib.types.str;
example = "127.0.0.1";
default = "";
description = ''
If specified, it defines the interface to listen on. If not
specified, the service will listen on all interfaces (any).
'';
};
httpPort = lib.mkOption {
default = 30638;
type = lib.types.port;
description = ''
Specifies the listen port for the HTTP service that returns the
status page.
'';
};
syncMode = lib.mkOption {
type = lib.types.enum [
"ASYNC"
"SYNC_WRITE_METADATA"
"SYNC_WRITE"
"FDATASYNC"
"FSYNC"
];
default = "FSYNC";
example = "FDATASYNC";
description = ''
The sync mode influences how operations are committed to the disk
log before the operation is acknowledged to the caller.
-ASYNC mode the writes to the disk log are buffered in memory by the operating system. This is the fastest mode but will lead to data loss in case of a crash, kernel panic or power failure.
-SYNC_WRITE_METADATA opens the file with O_SYNC, the system will not buffer any writes. The operation will be acknowledged when data has been safely written to disk. This mode is slow but offers maximum data safety. However, BabuDB cannot influence the disk drive caches, this depends on the OS and hard disk model.
-SYNC_WRITE similar to SYNC_WRITE_METADATA but opens file with O_DSYNC which means that only the data is commit to disk. This can lead to some data loss depending on the implementation of the underlying file system. Linux does not implement this mode.
-FDATASYNC is similar to SYNC_WRITE but opens the file in asynchronous mode and calls fdatasync() after writing the data to disk.
-FSYNC is similar to SYNC_WRITE_METADATA but opens the file in asynchronous mode and calls fsync() after writing the data to disk.
For best throughput use ASYNC, for maximum data safety use FSYNC.
(If xtreemfs.dir.replication.enable is true then FDATASYNC is forced)
'';
};
extraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
example = ''
# specify whether SSL is required
ssl.enabled = true
ssl.service_creds.pw = passphrase
ssl.service_creds.container = pkcs12
ssl.service_creds = /etc/xos/xtreemfs/truststore/certs/dir.p12
ssl.trusted_certs = /etc/xos/xtreemfs/truststore/certs/trusted.jks
ssl.trusted_certs.pw = jks_passphrase
ssl.trusted_certs.container = jks
'';
description = ''
Configuration of XtreemFS DIR service.
WARNING: configuration is saved as plaintext inside nix store.
For more options: <https://www.xtreemfs.org/xtfs-guide-1.5.1/index.html>
'';
};
replication = {
enable = lib.mkEnableOption "XtreemFS DIR replication plugin";
extraConfig = lib.mkOption {
type = lib.types.lines;
example = ''
# participants of the replication including this replica
babudb.repl.participant.0 = 192.168.0.10
babudb.repl.participant.0.port = 35676
babudb.repl.participant.1 = 192.168.0.11
babudb.repl.participant.1.port = 35676
babudb.repl.participant.2 = 192.168.0.12
babudb.repl.participant.2.port = 35676
# number of servers that at least have to be up to date
# To have a fault-tolerant system, this value has to be set to the
# majority of nodes i.e., if you have three replicas, set this to 2
# Please note that a setup with two nodes provides no fault-tolerance.
babudb.repl.sync.n = 2
# specify whether SSL is required
babudb.ssl.enabled = true
babudb.ssl.protocol = tlsv12
# server credentials for SSL handshakes
babudb.ssl.service_creds = /etc/xos/xtreemfs/truststore/certs/osd.p12
babudb.ssl.service_creds.pw = passphrase
babudb.ssl.service_creds.container = pkcs12
# trusted certificates for SSL handshakes
babudb.ssl.trusted_certs = /etc/xos/xtreemfs/truststore/certs/trusted.jks
babudb.ssl.trusted_certs.pw = jks_passphrase
babudb.ssl.trusted_certs.container = jks
babudb.ssl.authenticationWithoutEncryption = false
'';
description = ''
Configuration of XtreemFS DIR replication plugin.
WARNING: configuration is saved as plaintext inside nix store.
For more options: <https://www.xtreemfs.org/xtfs-guide-1.5.1/index.html>
'';
};
};
};
mrc = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to enable XtreemFS MRC service.
'';
};
uuid = lib.mkOption {
example = "eacb6bab-f444-4ebf-a06a-3f72d7465e41";
type = lib.types.str;
description = ''
Must be set to a unique identifier, preferably a UUID according to
RFC 4122. UUIDs can be generated with `uuidgen` command, found in
the `util-linux` package.
'';
};
port = lib.mkOption {
default = 32636;
type = lib.types.port;
description = ''
The port to listen on for incoming connections (TCP).
'';
};
address = lib.mkOption {
example = "127.0.0.1";
type = lib.types.str;
default = "";
description = ''
If specified, it defines the interface to listen on. If not
specified, the service will listen on all interfaces (any).
'';
};
httpPort = lib.mkOption {
default = 30636;
type = lib.types.port;
description = ''
Specifies the listen port for the HTTP service that returns the
status page.
'';
};
syncMode = lib.mkOption {
default = "FSYNC";
type = lib.types.enum [
"ASYNC"
"SYNC_WRITE_METADATA"
"SYNC_WRITE"
"FDATASYNC"
"FSYNC"
];
example = "FDATASYNC";
description = ''
The sync mode influences how operations are committed to the disk
log before the operation is acknowledged to the caller.
-ASYNC mode the writes to the disk log are buffered in memory by the operating system. This is the fastest mode but will lead to data loss in case of a crash, kernel panic or power failure.
-SYNC_WRITE_METADATA opens the file with O_SYNC, the system will not buffer any writes. The operation will be acknowledged when data has been safely written to disk. This mode is slow but offers maximum data safety. However, BabuDB cannot influence the disk drive caches, this depends on the OS and hard disk model.
-SYNC_WRITE similar to SYNC_WRITE_METADATA but opens file with O_DSYNC which means that only the data is commit to disk. This can lead to some data loss depending on the implementation of the underlying file system. Linux does not implement this mode.
-FDATASYNC is similar to SYNC_WRITE but opens the file in asynchronous mode and calls fdatasync() after writing the data to disk.
-FSYNC is similar to SYNC_WRITE_METADATA but opens the file in asynchronous mode and calls fsync() after writing the data to disk.
For best throughput use ASYNC, for maximum data safety use FSYNC.
(If xtreemfs.mrc.replication.enable is true then FDATASYNC is forced)
'';
};
extraConfig = lib.mkOption {
type = lib.types.lines;
example = ''
osd_check_interval = 300
no_atime = true
local_clock_renewal = 0
remote_time_sync = 30000
authentication_provider = org.xtreemfs.common.auth.NullAuthProvider
# shared secret between the MRC and all OSDs
capability_secret = iNG8UuQJrJ6XVDTe
dir_service.host = 192.168.0.10
dir_service.port = 32638
# if replication is enabled
dir_service.1.host = 192.168.0.11
dir_service.1.port = 32638
dir_service.2.host = 192.168.0.12
dir_service.2.port = 32638
# specify whether SSL is required
ssl.enabled = true
ssl.protocol = tlsv12
ssl.service_creds.pw = passphrase
ssl.service_creds.container = pkcs12
ssl.service_creds = /etc/xos/xtreemfs/truststore/certs/mrc.p12
ssl.trusted_certs = /etc/xos/xtreemfs/truststore/certs/trusted.jks
ssl.trusted_certs.pw = jks_passphrase
ssl.trusted_certs.container = jks
'';
description = ''
Configuration of XtreemFS MRC service.
WARNING: configuration is saved as plaintext inside nix store.
For more options: <https://www.xtreemfs.org/xtfs-guide-1.5.1/index.html>
'';
};
replication = {
enable = lib.mkEnableOption "XtreemFS MRC replication plugin";
extraConfig = lib.mkOption {
type = lib.types.lines;
example = ''
# participants of the replication including this replica
babudb.repl.participant.0 = 192.168.0.10
babudb.repl.participant.0.port = 35678
babudb.repl.participant.1 = 192.168.0.11
babudb.repl.participant.1.port = 35678
babudb.repl.participant.2 = 192.168.0.12
babudb.repl.participant.2.port = 35678
# number of servers that at least have to be up to date
# To have a fault-tolerant system, this value has to be set to the
# majority of nodes i.e., if you have three replicas, set this to 2
# Please note that a setup with two nodes provides no fault-tolerance.
babudb.repl.sync.n = 2
# specify whether SSL is required
babudb.ssl.enabled = true
babudb.ssl.protocol = tlsv12
# server credentials for SSL handshakes
babudb.ssl.service_creds = /etc/xos/xtreemfs/truststore/certs/osd.p12
babudb.ssl.service_creds.pw = passphrase
babudb.ssl.service_creds.container = pkcs12
# trusted certificates for SSL handshakes
babudb.ssl.trusted_certs = /etc/xos/xtreemfs/truststore/certs/trusted.jks
babudb.ssl.trusted_certs.pw = jks_passphrase
babudb.ssl.trusted_certs.container = jks
babudb.ssl.authenticationWithoutEncryption = false
'';
description = ''
Configuration of XtreemFS MRC replication plugin.
WARNING: configuration is saved as plaintext inside nix store.
For more options: <https://www.xtreemfs.org/xtfs-guide-1.5.1/index.html>
'';
};
};
};
osd = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Whether to enable XtreemFS OSD service.
'';
};
uuid = lib.mkOption {
example = "eacb6bab-f444-4ebf-a06a-3f72d7465e42";
type = lib.types.str;
description = ''
Must be set to a unique identifier, preferably a UUID according to
RFC 4122. UUIDs can be generated with `uuidgen` command, found in
the `util-linux` package.
'';
};
port = lib.mkOption {
default = 32640;
type = lib.types.port;
description = ''
The port to listen on for incoming connections (TCP and UDP).
'';
};
address = lib.mkOption {
example = "127.0.0.1";
type = lib.types.str;
default = "";
description = ''
If specified, it defines the interface to listen on. If not
specified, the service will listen on all interfaces (any).
'';
};
httpPort = lib.mkOption {
default = 30640;
type = lib.types.port;
description = ''
Specifies the listen port for the HTTP service that returns the
status page.
'';
};
extraConfig = lib.mkOption {
type = lib.types.lines;
example = ''
local_clock_renewal = 0
remote_time_sync = 30000
report_free_space = true
capability_secret = iNG8UuQJrJ6XVDTe
dir_service.host = 192.168.0.10
dir_service.port = 32638
# if replication is used
dir_service.1.host = 192.168.0.11
dir_service.1.port = 32638
dir_service.2.host = 192.168.0.12
dir_service.2.port = 32638
# specify whether SSL is required
ssl.enabled = true
ssl.service_creds.pw = passphrase
ssl.service_creds.container = pkcs12
ssl.service_creds = /etc/xos/xtreemfs/truststore/certs/osd.p12
ssl.trusted_certs = /etc/xos/xtreemfs/truststore/certs/trusted.jks
ssl.trusted_certs.pw = jks_passphrase
ssl.trusted_certs.container = jks
'';
description = ''
Configuration of XtreemFS OSD service.
WARNING: configuration is saved as plaintext inside nix store.
For more options: <https://www.xtreemfs.org/xtfs-guide-1.5.1/index.html>
'';
};
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
environment.systemPackages = [ xtreemfs ];
users.users.xtreemfs = {
uid = config.ids.uids.xtreemfs;
description = "XtreemFS user";
createHome = true;
home = home;
};
users.groups.xtreemfs = {
gid = config.ids.gids.xtreemfs;
};
systemd.services.xtreemfs-dir = lib.mkIf cfg.dir.enable {
description = "XtreemFS-DIR Server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = "xtreemfs";
ExecStart = "${startupScript "org.xtreemfs.dir.DIR" dirConfig}";
};
};
systemd.services.xtreemfs-mrc = lib.mkIf cfg.mrc.enable (
{
description = "XtreemFS-MRC Server";
serviceConfig = {
User = "xtreemfs";
ExecStart = "${startupScript "org.xtreemfs.mrc.MRC" mrcConfig}";
};
}
// systemdOptionalDependencies
);
systemd.services.xtreemfs-osd = lib.mkIf cfg.osd.enable (
{
description = "XtreemFS-OSD Server";
serviceConfig = {
User = "xtreemfs";
ExecStart = "${startupScript "org.xtreemfs.osd.OSD" osdConfig}";
};
}
// systemdOptionalDependencies
);
};
}

View File

@@ -0,0 +1,118 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.yandex-disk;
dir = "/var/lib/yandex-disk";
u = if cfg.user != null then cfg.user else "yandexdisk";
in
{
###### interface
options = {
services.yandex-disk = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to enable Yandex-disk client. See <https://disk.yandex.ru/>
'';
};
username = lib.mkOption {
default = "";
type = lib.types.str;
description = ''
Your yandex.com login name.
'';
};
password = lib.mkOption {
default = "";
type = lib.types.str;
description = ''
Your yandex.com password. Warning: it will be world-readable in /nix/store.
'';
};
user = lib.mkOption {
default = null;
type = lib.types.nullOr lib.types.str;
description = ''
The user the yandex-disk daemon should run as.
'';
};
directory = lib.mkOption {
type = lib.types.path;
default = "/home/Yandex.Disk";
description = "The directory to use for Yandex.Disk storage";
};
excludes = lib.mkOption {
default = "";
type = lib.types.commas;
example = "data,backup";
description = ''
Comma-separated list of directories which are excluded from synchronization.
'';
};
};
};
###### implementation
config = lib.mkIf cfg.enable {
users.users = lib.mkIf (cfg.user == null) [
{
name = u;
uid = config.ids.uids.yandexdisk;
group = "nogroup";
home = dir;
}
];
systemd.services.yandex-disk = {
description = "Yandex-disk server";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
# FIXME: have to specify ${directory} here as well
unitConfig.RequiresMountsFor = dir;
script = ''
mkdir -p -m 700 ${dir}
chown ${u} ${dir}
if ! test -d "${cfg.directory}" ; then
(mkdir -p -m 755 ${cfg.directory} && chown ${u} ${cfg.directory}) ||
exit 1
fi
${pkgs.su}/bin/su -s ${pkgs.runtimeShell} ${u} \
-c '${pkgs.yandex-disk}/bin/yandex-disk token -p ${cfg.password} ${cfg.username} ${dir}/token'
${pkgs.su}/bin/su -s ${pkgs.runtimeShell} ${u} \
-c '${pkgs.yandex-disk}/bin/yandex-disk start --no-daemon -a ${dir}/token -d ${cfg.directory} --exclude-dirs=${cfg.excludes}'
'';
};
};
}