{ config, lib, pkgs, ... }: let cfg = config.services.lightningstor; tomlFormat = pkgs.formats.toml { }; serverEnabled = cfg.mode != "data"; nodeEnabled = cfg.mode != "metadata"; serverDataDir = if cfg.mode == "all-in-one" then "${toString cfg.dataDir}/server" else toString cfg.dataDir; nodeDataDir = if cfg.mode == "all-in-one" then "${toString cfg.dataDir}/node" else toString cfg.dataDir; nodeListenPort = if cfg.mode == "data" then cfg.port else cfg.nodePort; localDependencies = lib.optionals (serverEnabled && (config.services ? iam) && config.services.iam.enable) [ "iam.service" ] ++ lib.optionals (serverEnabled && (config.services ? flaredb) && config.services.flaredb.enable) [ "flaredb.service" ]; effectiveNodeEndpoints = lib.unique ( cfg.distributedNodeEndpoints ++ lib.optionals (serverEnabled && nodeEnabled && cfg.objectStorageBackend == "distributed") [ "http://127.0.0.1:${toString nodeListenPort}" ] ); distributedRedundancy = if cfg.redundancyMode == "replicated" then { type = "replicated"; replica_count = cfg.replicaCount; read_quorum = cfg.readQuorum; write_quorum = cfg.writeQuorum; } else { type = "erasure_coded"; data_shards = cfg.dataShards; parity_shards = cfg.parityShards; }; serverBaseConfig = { grpc_addr = "0.0.0.0:${toString cfg.port}"; s3_addr = "0.0.0.0:${toString cfg.s3Port}"; log_level = "info"; data_dir = serverDataDir; sync_on_write = cfg.syncOnWrite; object_storage_backend = cfg.objectStorageBackend; auth = { iam_server_addr = if cfg.iamAddr != null then cfg.iamAddr else "127.0.0.1:50080"; }; distributed = { node_endpoints = effectiveNodeEndpoints; connection_timeout_ms = cfg.distributedConnectionTimeoutMs; request_timeout_ms = cfg.distributedRequestTimeoutMs; redundancy = distributedRedundancy; } // lib.optionalAttrs (cfg.distributedRegistryEndpoint != null) { registry_endpoint = cfg.distributedRegistryEndpoint; }; } // lib.optionalAttrs (cfg.flaredbAddr != null) { flaredb_endpoint = cfg.flaredbAddr; } // { metadata_backend = cfg.metadataBackend; single_node = cfg.singleNode; } // lib.optionalAttrs (cfg.databaseUrl != null) { metadata_database_url = cfg.databaseUrl; } // lib.optionalAttrs (cfg.chainfireAddr != null) { chainfire_endpoint = "http://${cfg.chainfireAddr}"; }; lightningstorConfigFile = tomlFormat.generate "lightningstor.toml" (lib.recursiveUpdate serverBaseConfig cfg.settings); lightningstorNodeConfigFile = tomlFormat.generate "lightningstor-node.toml" { node_id = cfg.nodeId; grpc_addr = "0.0.0.0:${toString nodeListenPort}"; data_dir = nodeDataDir; zone = cfg.zone; region = cfg.region; log_level = "info"; max_capacity_bytes = cfg.maxCapacityBytes; metrics_port = cfg.nodeMetricsPort; sync_on_write = cfg.syncOnWrite; }; serverCommand = lib.escapeShellArgs [ "${cfg.package}/bin/lightningstor-server" "--config" "${lightningstorConfigFile}" ]; nodeCommand = lib.escapeShellArgs [ "${cfg.nodePackage}/bin/lightningstor-node" "--config" "${lightningstorNodeConfigFile}" ]; allInOneLauncher = pkgs.writeShellScript "lightningstor-all-in-one" '' set -euo pipefail ${nodeCommand} & node_pid=$! cleanup() { if kill -0 "$node_pid" 2>/dev/null; then kill "$node_pid" 2>/dev/null || true wait "$node_pid" 2>/dev/null || true fi } trap cleanup EXIT INT TERM exec ${serverCommand} ''; execStart = if cfg.mode == "data" then nodeCommand else if cfg.mode == "metadata" then serverCommand else allInOneLauncher; in { options.services.lightningstor = { enable = lib.mkEnableOption "lightningstor service"; mode = lib.mkOption { type = lib.types.enum [ "data" "metadata" "all-in-one" ]; default = "all-in-one"; description = "LightningStor operating mode: data (storage node), metadata (coordinator), or all-in-one"; }; port = lib.mkOption { type = lib.types.port; default = 50086; description = "Port for lightningstor gRPC API. In mode=data this is reused as the node daemon gRPC port."; }; nodePort = lib.mkOption { type = lib.types.port; default = 50090; description = "Port for lightningstor-node when mode=all-in-one."; }; s3Port = lib.mkOption { type = lib.types.port; default = 9000; description = "Port for S3-compatible HTTP API"; }; iamAddr = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "IAM service endpoint address (host:port)"; example = "10.0.0.1:50080"; }; chainfireAddr = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "ChainFire endpoint address (host:port) for cluster coordination only"; example = "10.0.0.1:2379"; }; flaredbAddr = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "FlareDB endpoint address (host:port) for metadata/user data"; example = "10.0.0.1:2479"; }; metadataBackend = lib.mkOption { type = lib.types.enum [ "flaredb" "postgres" "sqlite" ]; default = "flaredb"; description = "Metadata backend for LightningStor."; }; objectStorageBackend = lib.mkOption { type = lib.types.enum [ "local_fs" "distributed" ]; default = "local_fs"; description = "Object data backend for metadata/all-in-one modes."; }; distributedNodeEndpoints = lib.mkOption { type = lib.types.listOf lib.types.str; default = []; description = "Static LightningStor node gRPC endpoints for distributed object storage."; example = [ "http://10.0.0.21:50086" "http://10.0.0.22:50086" ]; }; distributedRegistryEndpoint = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "Registry endpoint for future dynamic node discovery. Currently informational only."; }; distributedConnectionTimeoutMs = lib.mkOption { type = lib.types.int; default = 5000; description = "Connection timeout in milliseconds for distributed LightningStor node RPCs."; }; distributedRequestTimeoutMs = lib.mkOption { type = lib.types.int; default = 300000; description = "Request timeout in milliseconds for distributed LightningStor node RPCs."; }; redundancyMode = lib.mkOption { type = lib.types.enum [ "replicated" "erasure_coded" ]; default = "replicated"; description = "Redundancy strategy for distributed object storage."; }; replicaCount = lib.mkOption { type = lib.types.int; default = 3; description = "Replica count when redundancyMode=replicated."; }; readQuorum = lib.mkOption { type = lib.types.int; default = 1; description = "Read quorum when redundancyMode=replicated."; }; writeQuorum = lib.mkOption { type = lib.types.int; default = 2; description = "Write quorum when redundancyMode=replicated."; }; dataShards = lib.mkOption { type = lib.types.int; default = 4; description = "Data shard count when redundancyMode=erasure_coded."; }; parityShards = lib.mkOption { type = lib.types.int; default = 2; description = "Parity shard count when redundancyMode=erasure_coded."; }; nodeId = lib.mkOption { type = lib.types.str; default = config.networking.hostName; description = "Node ID for lightningstor-node."; }; zone = lib.mkOption { type = lib.types.str; default = ""; description = "Placement zone for lightningstor-node."; }; region = lib.mkOption { type = lib.types.str; default = ""; description = "Placement region for lightningstor-node."; }; maxCapacityBytes = lib.mkOption { type = lib.types.int; default = 0; description = "Maximum capacity for lightningstor-node (0 = unlimited)."; }; syncOnWrite = lib.mkOption { type = lib.types.bool; default = false; description = "Whether filesystem-backed LightningStor writes are flushed before success is returned."; }; nodeMetricsPort = lib.mkOption { type = lib.types.port; default = 9098; description = "Prometheus metrics port for lightningstor-node."; }; s3StreamingPutThresholdBytes = lib.mkOption { type = lib.types.int; default = 64 * 1024 * 1024; description = "Streaming PUT multipart threshold for the S3 frontend."; }; s3InlinePutMaxBytes = lib.mkOption { type = lib.types.int; default = 128 * 1024 * 1024; description = "Maximum inline single-PUT size for the S3 frontend."; }; s3MultipartPutConcurrency = lib.mkOption { type = lib.types.int; default = 4; description = "Maximum in-flight multipart PUT part uploads."; }; s3MultipartFetchConcurrency = lib.mkOption { type = lib.types.int; default = 4; description = "Maximum concurrent multipart GET part fetches."; }; databaseUrl = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "SQL database URL for metadata (required when metadataBackend is postgres/sqlite)."; example = "postgres://lightningstor:secret@10.0.0.10:5432/lightningstor"; }; singleNode = lib.mkOption { type = lib.types.bool; default = false; description = "Enable single-node mode (required when metadata backend is SQLite)"; }; dataDir = lib.mkOption { type = lib.types.path; default = "/var/lib/lightningstor"; description = "Data directory for lightningstor"; }; settings = lib.mkOption { type = lib.types.attrs; default = {}; description = "Additional configuration settings merged into the lightningstor-server config."; }; package = lib.mkOption { type = lib.types.package; default = pkgs.lightningstor-server or (throw "lightningstor-server package not found"); description = "Package to use for lightningstor-server."; }; nodePackage = lib.mkOption { type = lib.types.package; default = pkgs.lightningstor-node or (throw "lightningstor-node package not found"); description = "Package to use for lightningstor-node."; }; }; config = lib.mkIf cfg.enable { users.users.lightningstor = { isSystemUser = true; group = "lightningstor"; description = "LightningStor service user"; home = cfg.dataDir; }; users.groups.lightningstor = { }; systemd.services.lightningstor = { description = "LightningStor Object Storage Service"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ] ++ localDependencies; requires = localDependencies; serviceConfig = { Type = "simple"; User = "lightningstor"; Group = "lightningstor"; Restart = "on-failure"; RestartSec = "10s"; StateDirectory = "lightningstor"; StateDirectoryMode = "0750"; NoNewPrivileges = true; PrivateTmp = true; ProtectSystem = "strict"; ProtectHome = true; ReadWritePaths = [ cfg.dataDir ]; ExecStart = execStart; }; environment = { RUST_LOG = "info"; LIGHTNINGSTOR_S3_STREAMING_PUT_THRESHOLD_BYTES = toString cfg.s3StreamingPutThresholdBytes; LIGHTNINGSTOR_S3_INLINE_PUT_MAX_BYTES = toString cfg.s3InlinePutMaxBytes; LIGHTNINGSTOR_S3_MULTIPART_PUT_CONCURRENCY = toString cfg.s3MultipartPutConcurrency; LIGHTNINGSTOR_S3_MULTIPART_FETCH_CONCURRENCY = toString cfg.s3MultipartFetchConcurrency; }; }; }; }