247 lines
8.2 KiB
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 -"
|
|
];
|
|
};
|
|
}
|