diff --git a/.gitignore b/.gitignore index 35568021..eaf85810 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ target bench/bench_workspace zerofs.toml TODO.md +result* diff --git a/README.md b/README.md index b80b71ec..37fdbb9b 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,39 @@ cargo install zerofs docker pull ghcr.io/barre/zerofs:latest ``` +#### Via Nix +```nix +# flake.nix +{ + inputs.zerofs.url = "github:Barre/ZeroFS"; + + outputs = { nixpkgs, zerofs, ... }: { + nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { + modules = [ + { nixpkgs.overlays = [ zerofs.overlays.default ]; } + zerofs.nixosModules.default + { + services.zerofs = { + enable = true; + settings = { + cache.dir = "/var/cache/zerofs"; + cache.disk_size_gb = 10.0; + storage.url = "s3://my-bucket/zerofs-data"; + storage.encryption_password = "\${ZEROFS_PASSWORD}"; + servers.nfs.addresses = [ "127.0.0.1:2049" ]; + aws = { + access_key_id = "\${AWS_ACCESS_KEY_ID}"; + secret_access_key = "\${AWS_SECRET_ACCESS_KEY}"; + }; + }; + }; + } + ]; + }; + }; +} +``` + ### Getting Started ```bash diff --git a/documentation/src/app/quickstart/page.mdx b/documentation/src/app/quickstart/page.mdx index b57c5a42..b7307e5b 100644 --- a/documentation/src/app/quickstart/page.mdx +++ b/documentation/src/app/quickstart/page.mdx @@ -48,6 +48,23 @@ docker pull ghcr.io/barre/zerofs:latest encryption-password: ${{ secrets.ZEROFS_PASSWORD }} ``` +```nix {{ title: 'NixOS Module' }} +# flake.nix +{ + inputs.zerofs.url = "github:Barre/ZeroFS"; + + outputs = { nixpkgs, zerofs, ... }: { + nixosConfigurations.myhost = nixpkgs.lib.nixosSystem { + modules = [ + { nixpkgs.overlays = [ zerofs.overlays.default ]; } + zerofs.nixosModules.default + { services.zerofs.enable = true; /* ... */ } + ]; + }; + }; +} +``` + ## Configuration @@ -139,6 +156,24 @@ allow_http = "true" # For HTTP endpoints addresses = ["127.0.0.1:2049"] ``` +```nix {{ title: 'NixOS' }} +services.zerofs = { + enable = true; + settings = { + cache.dir = "/var/cache/zerofs"; + cache.disk_size_gb = 10.0; + cache.memory_size_gb = 2.0; + storage.url = "s3://my-bucket/zerofs-data"; + storage.encryption_password = "\${ZEROFS_PASSWORD}"; + servers.nfs.addresses = [ "127.0.0.1:2049" ]; + aws = { + access_key_id = "\${AWS_ACCESS_KEY_ID}"; + secret_access_key = "\${AWS_SECRET_ACCESS_KEY}"; + }; + }; +}; +``` +
diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..0f2da68b --- /dev/null +++ b/flake.lock @@ -0,0 +1,64 @@ +{ + "nodes": { + "crane": { + "locked": { + "lastModified": 1766194365, + "narHash": "sha256-4AFsUZ0kl6MXSm4BaQgItD0VGlEKR3iq7gIaL7TjBvc=", + "owner": "ipetkov", + "repo": "crane", + "rev": "7d8ec2c71771937ab99790b45e6d9b93d15d9379", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1765835352, + "narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "a34fae9c08a15ad73f295041fec82323541400a9", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1766653575, + "narHash": "sha256-TPgxCS7+hWc4kPhzkU5dD2M5UuPhLuuaMNZ/IpwKQvI=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "3c1016e6acd16ad96053116d0d3043029c9e2649", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "crane": "crane", + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..b72947cf --- /dev/null +++ b/flake.nix @@ -0,0 +1,33 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + + flake-parts = { + url = "github:hercules-ci/flake-parts"; + inputs.nixpkgs-lib.follows = "nixpkgs"; + }; + + crane.url = "github:ipetkov/crane"; + }; + + outputs = inputs @ { self, flake-parts, ... }: let + inherit (inputs.nixpkgs) lib; + + modules = builtins.foldl' (acc: f: f acc) ./nix/flake [ + builtins.readDir + (lib.filterAttrs (name: type: + type == "regular" && lib.hasSuffix ".nix" name + )) + (lib.mapAttrsToList (name: _: + lib.path.append ./nix/flake name + )) + ]; + + in flake-parts.lib.mkFlake { inherit inputs; } { + imports = modules; + systems = [ + "x86_64-linux" + "aarch64-linux" + ]; + }; +} diff --git a/nix/flake/nixos-modules.nix b/nix/flake/nixos-modules.nix new file mode 100644 index 00000000..fac6776a --- /dev/null +++ b/nix/flake/nixos-modules.nix @@ -0,0 +1,7 @@ +{ inputs, ... }: +{ + flake.nixosModules = { + zerofs = import ../nixos/zerofs.nix; + default = inputs.self.nixosModules.zerofs; + }; +} diff --git a/nix/flake/overlays.nix b/nix/flake/overlays.nix new file mode 100644 index 00000000..ad404bc2 --- /dev/null +++ b/nix/flake/overlays.nix @@ -0,0 +1,12 @@ +{ inputs, ... }: +{ + flake.overlays = { + zerofs = final: prev: { + zerofs = final.callPackage "${inputs.self}/nix/package.nix" { + craneLib = inputs.crane.mkLib final; + }; + }; + + default = inputs.self.overlays.zerofs; + }; +} diff --git a/nix/flake/packages.nix b/nix/flake/packages.nix new file mode 100644 index 00000000..c2e84e4f --- /dev/null +++ b/nix/flake/packages.nix @@ -0,0 +1,13 @@ +{ inputs, ... }: +{ + perSystem = { pkgs, ... }: + let + craneLib = inputs.crane.mkLib pkgs; + zerofs = pkgs.callPackage "${inputs.self}/nix/package.nix" { inherit craneLib; }; + in { + packages = { + inherit zerofs; + default = zerofs; + }; + }; +} diff --git a/nix/flake/tests.nix b/nix/flake/tests.nix new file mode 100644 index 00000000..20237262 --- /dev/null +++ b/nix/flake/tests.nix @@ -0,0 +1,33 @@ +{ inputs, lib, ... }: +{ + perSystem = + { pkgs, system, ... }: + let + mkTest = + name: + lib.nixos.evalTest { + imports = [ (../nixos/tests + "/${name}.nix") ]; + hostPkgs = pkgs; + defaults = { + imports = [ inputs.self.nixosModules.default ]; + services.zerofs.package = lib.mkDefault inputs.self.packages.${system}.default; + }; + }; + + tests = { + nfs = mkTest "nfs"; + "9p-tcp" = mkTest "9p-tcp"; + "9p-uds" = mkTest "9p-uds"; + "nbd-zfs" = mkTest "nbd-zfs"; + }; + in + { + apps = lib.mapAttrs' ( + name: test: + lib.nameValuePair "test-${name}" { + type = "app"; + program = "${test.config.result.driver}/bin/nixos-test-driver"; + } + ) tests; + }; +} diff --git a/nix/nixos/tests/9p-tcp.nix b/nix/nixos/tests/9p-tcp.nix new file mode 100644 index 00000000..e899a066 --- /dev/null +++ b/nix/nixos/tests/9p-tcp.nix @@ -0,0 +1,65 @@ +{ pkgs, ... }: +{ + name = "9p-tcp"; + + nodes.machine = { pkgs, ... }: { + services.minio = { + enable = true; + rootCredentialsFile = pkgs.writeText "minio-credentials" '' + MINIO_ROOT_USER=minioadmin + MINIO_ROOT_PASSWORD=minioadmin + ''; + }; + + services.zerofs = { + enable = true; + settings = { + cache = { + dir = "/var/cache/zerofs"; + disk_size_gb = 1.0; + }; + storage = { + url = "s3://zerofs-test/data"; + encryption_password = "test-password"; + }; + servers.ninep.addresses = [ "127.0.0.1:5564" ]; + aws = { + access_key_id = "minioadmin"; + secret_access_key = "minioadmin"; + endpoint = "http://127.0.0.1:9000"; + region = "us-east-1"; + allow_http = "true"; + }; + }; + }; + }; + + testScript = '' + machine.wait_for_unit("minio.service") + machine.wait_for_open_port(9000) + + machine.succeed( + "${pkgs.minio-client}/bin/mc alias set local http://127.0.0.1:9000 minioadmin minioadmin", + "${pkgs.minio-client}/bin/mc mb local/zerofs-test", + ) + + machine.wait_for_unit("zerofs.service") + machine.wait_for_open_port(5564) + + # Mount zerofs via 9P over TCP + machine.succeed("mkdir -p /mnt/zerofs") + machine.succeed("mount -t 9p -o trans=tcp,port=5564,version=9p2000.L,cache=mmap 127.0.0.1 /mnt/zerofs") + + # Test basic file operations + machine.succeed("echo 'Hello from ZeroFS 9P!' > /mnt/zerofs/test.txt") + output = machine.succeed("cat /mnt/zerofs/test.txt") + assert "Hello from ZeroFS 9P!" in output, f"Unexpected content: {output}" + + machine.succeed("ls -la /mnt/zerofs/") + + machine.succeed("rm /mnt/zerofs/test.txt") + machine.succeed("test ! -f /mnt/zerofs/test.txt") + + machine.succeed("umount /mnt/zerofs") + ''; +} diff --git a/nix/nixos/tests/9p-uds.nix b/nix/nixos/tests/9p-uds.nix new file mode 100644 index 00000000..8890bcba --- /dev/null +++ b/nix/nixos/tests/9p-uds.nix @@ -0,0 +1,67 @@ +{ pkgs, ... }: +{ + name = "9p-uds"; + + nodes.machine = { pkgs, ... }: { + services.minio = { + enable = true; + rootCredentialsFile = pkgs.writeText "minio-credentials" '' + MINIO_ROOT_USER=minioadmin + MINIO_ROOT_PASSWORD=minioadmin + ''; + }; + + services.zerofs = { + enable = true; + settings = { + cache = { + dir = "/var/cache/zerofs"; + disk_size_gb = 1.0; + }; + storage = { + url = "s3://zerofs-test/data"; + encryption_password = "test-password"; + }; + servers.ninep.unix_socket = "/run/zerofs/zerofs.9p.sock"; + aws = { + access_key_id = "minioadmin"; + secret_access_key = "minioadmin"; + endpoint = "http://127.0.0.1:9000"; + region = "us-east-1"; + allow_http = "true"; + }; + }; + }; + }; + + testScript = '' + machine.wait_for_unit("minio.service") + machine.wait_for_open_port(9000) + + machine.succeed( + "${pkgs.minio-client}/bin/mc alias set local http://127.0.0.1:9000 minioadmin minioadmin", + "${pkgs.minio-client}/bin/mc mb local/zerofs-test", + ) + + machine.wait_for_unit("zerofs.service") + + # Wait for Unix socket to be created + machine.wait_for_file("/run/zerofs/zerofs.9p.sock") + + # Mount zerofs via 9P over Unix domain socket + machine.succeed("mkdir -p /mnt/zerofs") + machine.succeed("mount -t 9p -o trans=unix,version=9p2000.L,cache=mmap,access=user /run/zerofs/zerofs.9p.sock /mnt/zerofs") + + # Test basic file operations + machine.succeed("echo 'Hello from ZeroFS 9P UDS!' > /mnt/zerofs/test.txt") + output = machine.succeed("cat /mnt/zerofs/test.txt") + assert "Hello from ZeroFS 9P UDS!" in output, f"Unexpected content: {output}" + + machine.succeed("ls -la /mnt/zerofs/") + + machine.succeed("rm /mnt/zerofs/test.txt") + machine.succeed("test ! -f /mnt/zerofs/test.txt") + + machine.succeed("umount /mnt/zerofs") + ''; +} diff --git a/nix/nixos/tests/nbd-zfs.nix b/nix/nixos/tests/nbd-zfs.nix new file mode 100644 index 00000000..be28700e --- /dev/null +++ b/nix/nixos/tests/nbd-zfs.nix @@ -0,0 +1,126 @@ +{ pkgs, ... }: +{ + name = "nbd-zfs"; + + nodes.machine = { pkgs, ... }: { + services.minio = { + enable = true; + rootCredentialsFile = pkgs.writeText "minio-credentials" '' + MINIO_ROOT_USER=minioadmin + MINIO_ROOT_PASSWORD=minioadmin + ''; + }; + + services.zerofs = { + enable = true; + settings = { + cache = { + dir = "/var/cache/zerofs"; + disk_size_gb = 2.0; + memory_size_gb = 2.0; + }; + storage = { + url = "s3://zerofs-test/data"; + encryption_password = "test-password"; + }; + servers.ninep.addresses = [ "127.0.0.1:5564" ]; + servers.nbd.addresses = [ "127.0.0.1:10809" ]; + # Higher L0 limits needed for slow VM environments where compaction can't keep up + lsm = { + l0_max_ssts = 64; + max_concurrent_compactions = 32; + }; + aws = { + access_key_id = "minioadmin"; + secret_access_key = "minioadmin"; + endpoint = "http://127.0.0.1:9000"; + region = "us-east-1"; + allow_http = "true"; + }; + }; + }; + + boot.supportedFilesystems = [ "zfs" ]; + boot.zfs.forceImportRoot = false; + boot.kernelModules = [ "nbd" ]; + networking.hostId = "deadbeef"; + + environment.systemPackages = with pkgs; [ + nbd + zfs + ]; + }; + + testScript = '' + machine.wait_for_unit("minio.service") + machine.wait_for_open_port(9000) + + machine.succeed( + "${pkgs.minio-client}/bin/mc alias set local http://127.0.0.1:9000 minioadmin minioadmin", + "${pkgs.minio-client}/bin/mc mb local/zerofs-test", + ) + + machine.wait_for_unit("zerofs.service") + machine.wait_for_open_port(5564) + machine.wait_for_open_port(10809) + + # Mount 9P and create NBD device file + machine.succeed("mkdir -p /mnt/9p") + machine.succeed("mount -t 9p -o trans=tcp,port=5564,version=9p2000.L 127.0.0.1 /mnt/9p") + machine.succeed("mkdir -p /mnt/9p/.nbd") + machine.succeed("truncate -s 1G /mnt/9p/.nbd/test-device") + machine.succeed("umount /mnt/9p") + + # Connect NBD device + machine.succeed("nbd-client 127.0.0.1 10809 /dev/nbd0 -N test-device") + machine.succeed("blockdev --getsize64 /dev/nbd0") + + # Create ZFS pool and filesystem + machine.succeed("zpool create testpool /dev/nbd0") + machine.succeed("zfs create testpool/data") + machine.succeed("zfs set mountpoint=/mnt/zfsdata testpool/data") + + # Write test data + machine.succeed("echo 'Hello from ZeroFS NBD+ZFS!' > /mnt/zfsdata/test.txt") + machine.succeed("dd if=/dev/urandom of=/mnt/zfsdata/random.dat bs=1M count=10") + original_checksum = machine.succeed("sha256sum /mnt/zfsdata/random.dat").strip() + + # Create snapshot + machine.succeed("zfs snapshot testpool/data@snap1") + + # Sync and export + machine.succeed("sync") + machine.succeed("zpool sync testpool") + machine.succeed("zfs unmount testpool/data") + machine.succeed("zpool export testpool") + + # Disconnect NBD and wait for full disconnect + machine.succeed("nbd-client -d /dev/nbd0") + machine.wait_until_succeeds("test $(cat /sys/block/nbd0/size) = 0") + machine.wait_until_succeeds("! nbd-client -c /dev/nbd0") + + # Restart zerofs + machine.succeed("systemctl stop zerofs.service") + machine.succeed("systemctl start zerofs.service") + machine.wait_for_unit("zerofs.service") + machine.wait_for_open_port(10809) + + # Reconnect NBD and import pool + machine.succeed("nbd-client 127.0.0.1 10809 /dev/nbd0 -N test-device") + machine.succeed("zpool import testpool") + + # Verify data integrity + output = machine.succeed("cat /mnt/zfsdata/test.txt") + assert "Hello from ZeroFS NBD+ZFS!" in output, f"Unexpected content: {output}" + + restored_checksum = machine.succeed("sha256sum /mnt/zfsdata/random.dat").strip() + assert original_checksum == restored_checksum, f"Checksum mismatch: {original_checksum} vs {restored_checksum}" + + # Verify snapshot exists + machine.succeed("zfs list -t snapshot testpool/data@snap1") + + # Cleanup + machine.succeed("zpool export testpool") + machine.succeed("nbd-client -d /dev/nbd0") + ''; +} diff --git a/nix/nixos/tests/nfs.nix b/nix/nixos/tests/nfs.nix new file mode 100644 index 00000000..3d509dad --- /dev/null +++ b/nix/nixos/tests/nfs.nix @@ -0,0 +1,74 @@ +{ pkgs, ... }: +{ + name = "nfs"; + + nodes.machine = { pkgs, ... }: { + # Minio for S3-compatible storage + services.minio = { + enable = true; + rootCredentialsFile = pkgs.writeText "minio-credentials" '' + MINIO_ROOT_USER=minioadmin + MINIO_ROOT_PASSWORD=minioadmin + ''; + }; + + services.zerofs = { + enable = true; + settings = { + cache = { + dir = "/var/cache/zerofs"; + disk_size_gb = 1.0; + }; + storage = { + url = "s3://zerofs-test/data"; + encryption_password = "test-password"; + }; + servers.nfs.addresses = [ "127.0.0.1:2049" ]; + aws = { + access_key_id = "minioadmin"; + secret_access_key = "minioadmin"; + endpoint = "http://127.0.0.1:9000"; + region = "us-east-1"; + allow_http = "true"; + }; + }; + }; + + # NFS client for mounting + environment.systemPackages = [ pkgs.nfs-utils ]; + }; + + testScript = '' + machine.wait_for_unit("minio.service") + machine.wait_for_open_port(9000) + + # Create the bucket for zerofs + machine.succeed( + "${pkgs.minio-client}/bin/mc alias set local http://127.0.0.1:9000 minioadmin minioadmin", + "${pkgs.minio-client}/bin/mc mb local/zerofs-test", + ) + + machine.wait_for_unit("zerofs.service") + + # Wait for NFS to be ready + machine.wait_for_open_port(2049) + + # Mount zerofs via NFS + machine.succeed("mkdir -p /mnt/zerofs") + machine.succeed("mount -t nfs -o async,nolock,rsize=1048576,wsize=1048576,tcp,port=2049,mountport=2049,hard 127.0.0.1:/ /mnt/zerofs") + + # Test basic file operations + machine.succeed("echo 'Hello from ZeroFS!' > /mnt/zerofs/test.txt") + output = machine.succeed("cat /mnt/zerofs/test.txt") + assert "Hello from ZeroFS!" in output, f"Unexpected content: {output}" + + # Test file listing + machine.succeed("ls -la /mnt/zerofs/") + + # Test file deletion + machine.succeed("rm /mnt/zerofs/test.txt") + machine.succeed("test ! -f /mnt/zerofs/test.txt") + + machine.succeed("umount /mnt/zerofs") + ''; +} diff --git a/nix/nixos/zerofs.nix b/nix/nixos/zerofs.nix new file mode 100644 index 00000000..b937723e --- /dev/null +++ b/nix/nixos/zerofs.nix @@ -0,0 +1,285 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.zerofs; + settingsFormat = pkgs.formats.toml { }; + + # Collect unix socket directories that need to be created + socketDirs = lib.unique (lib.filter (x: x != null) [ + (if cfg.settings.servers.ninep != null then lib.mapNullable dirOf cfg.settings.servers.ninep.unix_socket else null) + (if cfg.settings.servers.nbd != null then lib.mapNullable dirOf cfg.settings.servers.nbd.unix_socket else null) + (if cfg.settings.servers.rpc != null then lib.mapNullable dirOf cfg.settings.servers.rpc.unix_socket else null) + ]); + + # Filter out null values from the settings + filterNulls = + attrs: + lib.filterAttrsRecursive (_: v: v != null) ( + lib.mapAttrs ( + _: v: + if lib.isAttrs v then filterNulls v else v + ) attrs + ); + + # Build the configuration from module options + configFile = settingsFormat.generate "zerofs.toml" (filterNulls cfg.settings); + +in { + options.services.zerofs = { + enable = lib.mkEnableOption "ZeroFS, a filesystem that makes S3 your primary storage"; + + package = lib.mkPackageOption pkgs "zerofs" { }; + + readOnly = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Run ZeroFS in read-only mode. Read-only instances automatically see + updates from the writer and return EROFS errors for write operations. + ''; + }; + + environmentFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = '' + Environment file to load before starting ZeroFS. Useful for secrets + like encryption passwords and cloud credentials that can be referenced + in the config using `''${VAR}` syntax. + ''; + }; + + settings = lib.mkOption { + type = lib.types.submodule { + freeformType = settingsFormat.type; + + options = { + cache = { + dir = lib.mkOption { + type = lib.types.path; + default = "/var/cache/zerofs"; + description = "Directory for caching data."; + }; + + disk_size_gb = lib.mkOption { + type = lib.types.numbers.positive; + default = 10.0; + description = "Maximum disk cache size in GB."; + }; + + memory_size_gb = lib.mkOption { + type = lib.types.nullOr lib.types.numbers.positive; + default = null; + description = "Memory cache size in GB."; + }; + }; + + storage = { + url = lib.mkOption { + type = lib.types.str; + example = "s3://my-bucket/zerofs-data"; + description = '' + Storage backend URL. Supports: + - S3: `s3://bucket/path` + - Azure: `azure://container/path` + - GCS: `gs://bucket/path` + - Local: `file:///path/to/storage` + ''; + }; + + encryption_password = lib.mkOption { + type = lib.types.str; + example = "\${ZEROFS_PASSWORD}"; + description = '' + Encryption password for data at rest. Use environment variable + substitution (e.g., `''${ZEROFS_PASSWORD}`) to avoid storing + secrets in the Nix store. + ''; + }; + }; + + filesystem = lib.mkOption { + type = lib.types.nullOr (lib.types.submodule { + options = { + max_size_gb = lib.mkOption { + type = lib.types.nullOr lib.types.numbers.positive; + default = null; + description = '' + Filesystem size limit in GB. When reached, writes return ENOSPC. + If not specified, defaults to 16 EiB (effectively unlimited). + ''; + }; + + compression = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "zstd-3"; + description = '' + Compression algorithm. Options: + - `lz4` (default): Fast compression + - `zstd-{1-22}`: Zstandard with configurable level + ''; + }; + }; + }); + default = null; + description = "Filesystem options."; + }; + + servers = lib.mkOption { + type = lib.types.submodule { + options = { + nfs = lib.mkOption { + type = lib.types.nullOr (lib.types.submodule { + options.addresses = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "127.0.0.1:2049" ]; + description = "NFS server bind addresses."; + }; + }); + default = null; + description = "NFS server configuration."; + }; + + ninep = lib.mkOption { + type = lib.types.nullOr (lib.types.submodule { + options = { + addresses = lib.mkOption { + type = lib.types.nullOr (lib.types.listOf lib.types.str); + default = null; + description = "9P server bind addresses."; + }; + + unix_socket = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Unix socket path for 9P server."; + }; + }; + }); + default = null; + description = "9P server configuration."; + }; + + nbd = lib.mkOption { + type = lib.types.nullOr (lib.types.submodule { + options = { + addresses = lib.mkOption { + type = lib.types.nullOr (lib.types.listOf lib.types.str); + default = null; + description = "NBD server bind addresses."; + }; + + unix_socket = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Unix socket path for NBD server."; + }; + }; + }); + default = null; + description = "NBD server configuration."; + }; + + rpc = lib.mkOption { + type = lib.types.nullOr (lib.types.submodule { + options = { + addresses = lib.mkOption { + type = lib.types.nullOr (lib.types.listOf lib.types.str); + default = null; + description = "RPC server bind addresses."; + }; + + unix_socket = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Unix socket path for RPC server."; + }; + }; + }); + default = null; + description = "RPC server configuration for admin commands."; + }; + }; + }; + default = { }; + description = "Server configurations for NFS, 9P, NBD, and RPC."; + }; + + lsm = lib.mkOption { + type = lib.types.nullOr (lib.types.submodule { + options = { + l0_max_ssts = lib.mkOption { + type = lib.types.nullOr lib.types.ints.positive; + default = null; + description = "Max SST files in L0 before compaction (default: 16, min: 4)."; + }; + + max_unflushed_gb = lib.mkOption { + type = lib.types.nullOr lib.types.numbers.positive; + default = null; + description = "Max unflushed data before forcing flush in GB (default: 1.0)."; + }; + + max_concurrent_compactions = lib.mkOption { + type = lib.types.nullOr lib.types.ints.positive; + default = null; + description = "Max concurrent compaction operations (default: 8)."; + }; + + flush_interval_secs = lib.mkOption { + type = lib.types.nullOr lib.types.ints.positive; + default = null; + description = "Interval between periodic flushes in seconds (default: 30)."; + }; + }; + }); + default = null; + description = "LSM tree performance tuning options."; + }; + }; + }; + default = { }; + description = '' + Configuration for ZeroFS in TOML format. See + for available options. + + Cloud provider credentials (aws, azure, gcp sections) can be added + using freeform attributes. Example: + + ```nix + services.zerofs.settings = { + aws = { + access_key_id = "''${AWS_ACCESS_KEY_ID}"; + secret_access_key = "''${AWS_SECRET_ACCESS_KEY}"; + region = "us-east-1"; + }; + }; + ``` + ''; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.zerofs = { + description = "ZeroFS S3 Filesystem"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + Type = "simple"; + ExecStartPre = lib.optionals (socketDirs != []) [ + "+${pkgs.coreutils}/bin/mkdir -p ${lib.escapeShellArgs socketDirs}" + ]; + ExecStart = + "${cfg.package}/bin/zerofs run --config ${configFile}" + + lib.optionalString cfg.readOnly " --read-only"; + Restart = "always"; + RestartSec = 5; + } // lib.optionalAttrs (cfg.environmentFile != null) { + EnvironmentFile = cfg.environmentFile; + }; + }; + }; +} diff --git a/nix/package.nix b/nix/package.nix new file mode 100644 index 00000000..f9a2c6ec --- /dev/null +++ b/nix/package.nix @@ -0,0 +1,39 @@ +{ + lib, + craneLib, + cmake, + pkg-config, + openssl, +}: + +let + protoFilter = path: _type: builtins.match ".*\\.proto$" path != null; + src = lib.cleanSourceWith { + src = ./..; + filter = path: type: + (protoFilter path type) || (craneLib.filterCargoSources path type); + }; + + inherit (craneLib.crateNameFromCargoToml { cargoToml = ../zerofs/Cargo.toml; }) pname version; + + commonArgs = { + inherit src pname version; + cargoLock = ../zerofs/Cargo.lock; + # Hashes for git dependencies (required when commit is not on default branch) + outputHashes = { + "git+https://github.com/slatedb/slatedb.git?rev=22e8070316a3897f4eb5b52a3e5831a8af4485b7#22e8070316a3897f4eb5b52a3e5831a8af4485b7" = "sha256-XSA/t9AF6zC/N6i1IzPTaYJg2Fuo+3Q202tyuNDM2g8="; + }; + postUnpack = '' + cd $sourceRoot/zerofs + sourceRoot="." + ''; + strictDeps = true; + nativeBuildInputs = [ cmake pkg-config ]; + buildInputs = [ openssl ]; + }; + + cargoArtifacts = craneLib.buildDepsOnly commonArgs; +in +craneLib.buildPackage (commonArgs // { + inherit cargoArtifacts; +})