{ config, lib, pkgs, ... }: let cfg = config.services.coronafs; chainfireEnabled = lib.hasAttrByPath [ "services" "chainfire" "enable" ] config && config.services.chainfire.enable; chainfireApiUrls = if cfg.chainfireApiUrl != null then lib.filter (item: item != "") (map lib.strings.trim (lib.splitString "," cfg.chainfireApiUrl)) else [ ]; effectiveChainfireApiUrl = if cfg.chainfireApiUrl != null then cfg.chainfireApiUrl else if chainfireEnabled then "http://127.0.0.1:${toString config.services.chainfire.httpPort}" else null; localChainfireApiUrl = lib.any (url: lib.hasPrefix "http://127.0.0.1:" url || lib.hasPrefix "http://localhost:" url ) ( if effectiveChainfireApiUrl == null then [ ] else if cfg.chainfireApiUrl != null then chainfireApiUrls else [ effectiveChainfireApiUrl ] ); waitForChainfire = pkgs.writeShellScript "coronafs-wait-for-chainfire" '' set -eu deadline=$((SECONDS + 60)) urls='${lib.concatStringsSep " " ( if effectiveChainfireApiUrl == null then [ ] else if cfg.chainfireApiUrl != null then chainfireApiUrls else [ effectiveChainfireApiUrl ] )}' while true; do for url in $urls; do if curl -fsS "$url/health" >/dev/null 2>&1; then exit 0 fi done if [ "$SECONDS" -ge "$deadline" ]; then echo "timed out waiting for ChainFire at ${if effectiveChainfireApiUrl == null then "(none)" else effectiveChainfireApiUrl}" >&2 exit 1 fi sleep 1 done ''; tomlFormat = pkgs.formats.toml { }; coronafsConfigFile = tomlFormat.generate "coronafs.toml" ( { mode = cfg.mode; metadata_backend = cfg.metadataBackend; chainfire_key_prefix = cfg.chainfireKeyPrefix; listen_addr = "0.0.0.0:${toString cfg.port}"; advertise_host = cfg.advertiseHost; data_dir = toString cfg.dataDir; export_bind_addr = cfg.exportBindAddr; export_base_port = cfg.exportBasePort; export_port_count = cfg.exportPortCount; export_shared_clients = cfg.exportSharedClients; export_cache_mode = cfg.exportCacheMode; export_aio_mode = cfg.exportAioMode; export_discard_mode = cfg.exportDiscardMode; export_detect_zeroes_mode = cfg.exportDetectZeroesMode; preallocate = cfg.preallocate; sync_on_write = cfg.syncOnWrite; qemu_nbd_path = "${pkgs.qemu}/bin/qemu-nbd"; qemu_img_path = "${pkgs.qemu}/bin/qemu-img"; log_level = "info"; } // lib.optionalAttrs (effectiveChainfireApiUrl != null) { chainfire_api_url = effectiveChainfireApiUrl; } ); in { options.services.coronafs = { enable = lib.mkEnableOption "CoronaFS block volume service"; mode = lib.mkOption { type = lib.types.enum [ "combined" "controller" "node" ]; default = "combined"; description = "CoronaFS operating mode: combined compatibility mode, controller-only API, or node-local export mode."; }; metadataBackend = lib.mkOption { type = lib.types.enum [ "filesystem" "chainfire" ]; default = "filesystem"; description = "Metadata backend for CoronaFS volume metadata. Use chainfire on controller nodes to replicate volume metadata."; }; chainfireApiUrl = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "Optional ChainFire HTTP API URL used when metadataBackend = chainfire. Comma-separated endpoints are allowed for failover."; example = "http://127.0.0.1:8081"; }; chainfireKeyPrefix = lib.mkOption { type = lib.types.str; default = "/coronafs/volumes"; description = "ChainFire key prefix used to store CoronaFS metadata when metadataBackend = chainfire."; }; port = lib.mkOption { type = lib.types.port; default = 50088; description = "Port for the CoronaFS control API."; }; advertiseHost = lib.mkOption { type = lib.types.str; default = "127.0.0.1"; description = "Host or IP placed into exported NBD URIs."; example = "10.0.0.11"; }; exportBindAddr = lib.mkOption { type = lib.types.str; default = "0.0.0.0"; description = "Bind address for qemu-nbd exports."; }; exportBasePort = lib.mkOption { type = lib.types.port; default = 11000; description = "First TCP port reserved for CoronaFS NBD exports."; }; exportPortCount = lib.mkOption { type = lib.types.int; default = 512; description = "Number of NBD export ports reserved for CoronaFS volumes."; }; exportSharedClients = lib.mkOption { type = lib.types.int; default = 32; description = "Maximum number of concurrent clients per exported CoronaFS volume."; }; exportCacheMode = lib.mkOption { type = lib.types.enum [ "none" "writeback" "writethrough" "directsync" "unsafe" ]; default = "none"; description = "qemu-nbd cache mode for CoronaFS exports."; }; exportAioMode = lib.mkOption { type = lib.types.enum [ "native" "io_uring" "threads" ]; default = "threads"; description = "qemu-nbd AIO mode for CoronaFS exports."; }; exportDiscardMode = lib.mkOption { type = lib.types.enum [ "ignore" "unmap" ]; default = "unmap"; description = "qemu-nbd discard handling for CoronaFS exports."; }; exportDetectZeroesMode = lib.mkOption { type = lib.types.enum [ "off" "on" "unmap" ]; default = "unmap"; description = "qemu-nbd detect-zeroes mode for CoronaFS exports."; }; preallocate = lib.mkOption { type = lib.types.bool; default = true; description = "Preallocate blank CoronaFS volumes with fallocate when possible."; }; syncOnWrite = lib.mkOption { type = lib.types.bool; default = false; description = "Force sync_all after volume import writes."; }; dataDir = lib.mkOption { type = lib.types.path; default = "/var/lib/coronafs"; description = "Data directory for CoronaFS volumes, metadata, and export pid files."; }; package = lib.mkOption { type = lib.types.package; default = pkgs.coronafs-server or (throw "coronafs-server package not found"); description = "Package to use for CoronaFS."; }; }; config = lib.mkIf cfg.enable { assertions = [ { assertion = cfg.metadataBackend != "chainfire" || effectiveChainfireApiUrl != null; message = "services.coronafs.metadataBackend = \"chainfire\" requires services.coronafs.chainfireApiUrl or a local services.chainfire instance."; } ]; users.users.coronafs = { isSystemUser = true; group = "coronafs"; description = "CoronaFS service user"; home = cfg.dataDir; extraGroups = lib.optional (lib.hasAttrByPath [ "services" "plasmavmc" "enable" ] config && config.services.plasmavmc.enable) "plasmavmc"; }; users.groups.coronafs = { }; systemd.services.coronafs = { description = "CoronaFS Block Volume Service"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ] ++ lib.optionals chainfireEnabled [ "chainfire.service" ]; wants = lib.optionals chainfireEnabled [ "chainfire.service" ]; path = [ pkgs.qemu pkgs.util-linux pkgs.procps pkgs.coreutils pkgs.curl ]; serviceConfig = { Type = "simple"; User = "coronafs"; Group = "coronafs"; UMask = "0007"; Restart = "on-failure"; RestartSec = "5s"; StateDirectory = "coronafs"; StateDirectoryMode = "0750"; ReadWritePaths = [ cfg.dataDir ]; ExecStartPre = lib.optionals (cfg.metadataBackend == "chainfire" && localChainfireApiUrl) [ waitForChainfire ]; ExecStart = "${cfg.package}/bin/coronafs-server --config ${coronafsConfigFile}"; }; }; systemd.tmpfiles.rules = [ "d ${toString cfg.dataDir} 0750 coronafs coronafs -" "d ${toString cfg.dataDir}/volumes 2770 coronafs coronafs -" "d ${toString cfg.dataDir}/metadata 0750 coronafs coronafs -" "d ${toString cfg.dataDir}/pids 0750 coronafs coronafs -" ]; }; }