1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7 8let 9 cfg = config.services.saunafs; 10 11 settingsFormat = 12 let 13 listSep = " "; 14 allowedTypes = with lib.types; [ 15 bool 16 int 17 float 18 str 19 ]; 20 valueToString = 21 val: 22 if lib.isList val then 23 lib.concatStringsSep listSep (map (x: valueToString x) val) 24 else if lib.isBool val then 25 (if val then "1" else "0") 26 else 27 toString val; 28 29 in 30 { 31 type = 32 let 33 valueType = 34 lib.types.oneOf ( 35 [ 36 (lib.types.listOf valueType) 37 ] 38 ++ allowedTypes 39 ) 40 // { 41 description = "Flat key-value file"; 42 }; 43 in 44 lib.types.attrsOf valueType; 45 46 generate = 47 name: value: 48 pkgs.writeText name ( 49 lib.concatStringsSep "\n" (lib.mapAttrsToList (key: val: "${key} = ${valueToString val}") value) 50 ); 51 }; 52 53 initTool = pkgs.writeShellScriptBin "sfsmaster-init" '' 54 if [ ! -e ${cfg.master.settings.DATA_PATH}/metadata.sfs ]; then 55 cp --update=none ${pkgs.saunafs}/var/lib/saunafs/metadata.sfs.empty ${cfg.master.settings.DATA_PATH}/metadata.sfs 56 chmod +w ${cfg.master.settings.DATA_PATH}/metadata.sfs 57 fi 58 ''; 59 60 # master config file 61 masterCfg = settingsFormat.generate "sfsmaster.cfg" cfg.master.settings; 62 63 # metalogger config file 64 metaloggerCfg = settingsFormat.generate "sfsmetalogger.cfg" cfg.metalogger.settings; 65 66 # chunkserver config file 67 chunkserverCfg = settingsFormat.generate "sfschunkserver.cfg" cfg.chunkserver.settings; 68 69 # generic template for all daemons 70 systemdService = name: extraConfig: configFile: { 71 wantedBy = [ "multi-user.target" ]; 72 wants = [ "network-online.target" ]; 73 after = [ 74 "network.target" 75 "network-online.target" 76 ]; 77 78 serviceConfig = { 79 Type = "forking"; 80 ExecStart = "${pkgs.saunafs}/bin/sfs${name} -c ${configFile} start"; 81 ExecStop = "${pkgs.saunafs}/bin/sfs${name} -c ${configFile} stop"; 82 ExecReload = "${pkgs.saunafs}/bin/sfs${name} -c ${configFile} reload"; 83 } // extraConfig; 84 }; 85 86in 87{ 88 ###### interface 89 90 options = { 91 services.saunafs = { 92 masterHost = lib.mkOption { 93 type = lib.types.str; 94 default = null; 95 description = "IP or hostname name of master host."; 96 }; 97 98 sfsUser = lib.mkOption { 99 type = lib.types.str; 100 default = "saunafs"; 101 description = "Run daemons as user."; 102 }; 103 104 client.enable = lib.mkEnableOption "Saunafs client"; 105 106 master = { 107 enable = lib.mkOption { 108 type = lib.types.bool; 109 description = '' 110 Enable Saunafs master daemon. 111 112 You need to run `sfsmaster-init` on a freshly installed master server to 113 initialize the `DATA_PATH` directory. 114 ''; 115 default = false; 116 }; 117 118 exports = lib.mkOption { 119 type = with lib.types; listOf str; 120 default = null; 121 description = "Paths to exports file (see {manpage}`sfsexports.cfg(5)`)."; 122 example = lib.literalExpression '' 123 [ "* / rw,alldirs,admin,maproot=0:0" ]; 124 ''; 125 }; 126 127 openFirewall = lib.mkOption { 128 type = lib.types.bool; 129 description = "Whether to automatically open the necessary ports in the firewall."; 130 default = false; 131 }; 132 133 settings = lib.mkOption { 134 type = lib.types.submodule { 135 freeformType = settingsFormat.type; 136 137 options.DATA_PATH = lib.mkOption { 138 type = lib.types.str; 139 default = "/var/lib/saunafs/master"; 140 description = "Data storage directory."; 141 }; 142 }; 143 144 description = "Contents of config file ({manpage}`sfsmaster.cfg(5)`)."; 145 }; 146 }; 147 148 metalogger = { 149 enable = lib.mkEnableOption "Saunafs metalogger daemon"; 150 151 settings = lib.mkOption { 152 type = lib.types.submodule { 153 freeformType = settingsFormat.type; 154 155 options.DATA_PATH = lib.mkOption { 156 type = lib.types.str; 157 default = "/var/lib/saunafs/metalogger"; 158 description = "Data storage directory"; 159 }; 160 }; 161 162 description = "Contents of metalogger config file (see {manpage}`sfsmetalogger.cfg(5)`)."; 163 }; 164 }; 165 166 chunkserver = { 167 enable = lib.mkEnableOption "Saunafs chunkserver daemon"; 168 169 openFirewall = lib.mkOption { 170 type = lib.types.bool; 171 description = "Whether to automatically open the necessary ports in the firewall."; 172 default = false; 173 }; 174 175 hdds = lib.mkOption { 176 type = with lib.types; listOf str; 177 default = null; 178 179 example = lib.literalExpression '' 180 [ "/mnt/hdd1" ]; 181 ''; 182 183 description = '' 184 Mount points to be used by chunkserver for storage (see {manpage}`sfshdd.cfg(5)`). 185 186 Note, that these mount points must writeable by the user defined by the saunafs user. 187 ''; 188 }; 189 190 settings = lib.mkOption { 191 type = lib.types.submodule { 192 freeformType = settingsFormat.type; 193 194 options.DATA_PATH = lib.mkOption { 195 type = lib.types.str; 196 default = "/var/lib/saunafs/chunkserver"; 197 description = "Directory for chunck meta data"; 198 }; 199 }; 200 201 description = "Contents of chunkserver config file (see {manpage}`sfschunkserver.cfg(5)`)."; 202 }; 203 }; 204 }; 205 }; 206 207 ###### implementation 208 209 config = 210 lib.mkIf (cfg.client.enable || cfg.master.enable || cfg.metalogger.enable || cfg.chunkserver.enable) 211 { 212 213 warnings = [ 214 (lib.mkIf (cfg.sfsUser == "root") "Running saunafs services as root is not recommended.") 215 ]; 216 217 # Service settings 218 services.saunafs = { 219 master.settings = lib.mkIf cfg.master.enable { 220 WORKING_USER = cfg.sfsUser; 221 EXPORTS_FILENAME = toString ( 222 pkgs.writeText "sfsexports.cfg" (lib.concatStringsSep "\n" cfg.master.exports) 223 ); 224 }; 225 226 metalogger.settings = lib.mkIf cfg.metalogger.enable { 227 WORKING_USER = cfg.sfsUser; 228 MASTER_HOST = cfg.masterHost; 229 }; 230 231 chunkserver.settings = lib.mkIf cfg.chunkserver.enable { 232 WORKING_USER = cfg.sfsUser; 233 MASTER_HOST = cfg.masterHost; 234 HDD_CONF_FILENAME = toString ( 235 pkgs.writeText "sfshdd.cfg" (lib.concatStringsSep "\n" cfg.chunkserver.hdds) 236 ); 237 }; 238 }; 239 240 # Create system user account for daemons 241 users = 242 lib.mkIf 243 (cfg.sfsUser != "root" && (cfg.master.enable || cfg.metalogger.enable || cfg.chunkserver.enable)) 244 { 245 users."${cfg.sfsUser}" = { 246 isSystemUser = true; 247 description = "saunafs daemon user"; 248 group = "saunafs"; 249 }; 250 groups."${cfg.sfsUser}" = { }; 251 }; 252 253 environment.systemPackages = 254 (lib.optional cfg.client.enable pkgs.saunafs) ++ (lib.optional cfg.master.enable initTool); 255 256 networking.firewall.allowedTCPPorts = 257 (lib.optionals cfg.master.openFirewall [ 258 9419 259 9420 260 9421 261 ]) 262 ++ (lib.optional cfg.chunkserver.openFirewall 9422); 263 264 # Ensure storage directories exist 265 systemd.tmpfiles.rules = 266 lib.optional cfg.master.enable "d ${cfg.master.settings.DATA_PATH} 0700 ${cfg.sfsUser} ${cfg.sfsUser} -" 267 ++ lib.optional cfg.metalogger.enable "d ${cfg.metalogger.settings.DATA_PATH} 0700 ${cfg.sfsUser} ${cfg.sfsUser} -" 268 ++ lib.optional cfg.chunkserver.enable "d ${cfg.chunkserver.settings.DATA_PATH} 0700 ${cfg.sfsUser} ${cfg.sfsUser} -"; 269 270 # Service definitions 271 systemd.services.sfs-master = lib.mkIf cfg.master.enable ( 272 systemdService "master" { 273 TimeoutStartSec = 1800; 274 TimeoutStopSec = 1800; 275 Restart = "no"; 276 } masterCfg 277 ); 278 279 systemd.services.sfs-metalogger = lib.mkIf cfg.metalogger.enable ( 280 systemdService "metalogger" { Restart = "on-abort"; } metaloggerCfg 281 ); 282 283 systemd.services.sfs-chunkserver = lib.mkIf cfg.chunkserver.enable ( 284 systemdService "chunkserver" { Restart = "on-abort"; } chunkserverCfg 285 ); 286 }; 287}