photoncloud-monorepo/nix/modules/coronafs.nix

247 lines
8.2 KiB
Nix

{ 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 -"
];
};
}