diff --git a/Cargo.lock b/Cargo.lock index dc08fa9c..0515801c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1257,6 +1257,7 @@ dependencies = [ "serde", "serde_bytes", "serde_json", + "systemstat", "tokio", "tokio-stream", "tracing", @@ -3549,6 +3550,20 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "systemstat" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a24aec24a9312c83999a28e3ef9db7e2afd5c64bf47725b758cdc1cafd5b0bd2" +dependencies = [ + "bytesize", + "lazy_static", + "libc", + "nom", + "time 0.3.17", + "winapi", +] + [[package]] name = "tempfile" version = "3.3.0" diff --git a/Cargo.nix b/Cargo.nix index 6d7a4fd1..35a4e176 100644 --- a/Cargo.nix +++ b/Cargo.nix @@ -32,7 +32,7 @@ args@{ ignoreLockHash, }: let - nixifiedLockHash = "456bca1fe75cfe5c26a9b56401a40b5b205c0096a0dc7287c7853c35498bc5c0"; + nixifiedLockHash = "cf836c01a9c668bab5f9a09d468f47aa24c50abec92855503624d706721335ef"; workspaceSrc = if args.workspaceSrc == null then ./. else args.workspaceSrc; currentLockHash = builtins.hashFile "sha256" (workspaceSrc + /Cargo.lock); lockHashIgnored = if ignoreLockHash @@ -1790,6 +1790,7 @@ in serde = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde."1.0.152" { inherit profileName; }).out; serde_bytes = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_bytes."0.11.8" { inherit profileName; }).out; serde_json = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".serde_json."1.0.91" { inherit profileName; }).out; + systemstat = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".systemstat."0.2.3" { inherit profileName; }).out; tokio = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio."1.24.2" { inherit profileName; }).out; tokio_stream = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tokio-stream."0.1.11" { inherit profileName; }).out; tracing = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".tracing."0.1.37" { inherit profileName; }).out; @@ -4938,6 +4939,21 @@ in }; }); + "registry+https://github.com/rust-lang/crates.io-index".systemstat."0.2.3" = overridableMkRustCrate (profileName: rec { + name = "systemstat"; + version = "0.2.3"; + registry = "registry+https://github.com/rust-lang/crates.io-index"; + src = fetchCratesIo { inherit name version; sha256 = "a24aec24a9312c83999a28e3ef9db7e2afd5c64bf47725b758cdc1cafd5b0bd2"; }; + dependencies = { + bytesize = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".bytesize."1.1.0" { inherit profileName; }).out; + lazy_static = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".lazy_static."1.4.0" { inherit profileName; }).out; + libc = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".libc."0.2.139" { inherit profileName; }).out; + ${ if hostPlatform.parsed.kernel.name == "linux" || hostPlatform.parsed.kernel.name == "android" then "nom" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".nom."7.1.3" { inherit profileName; }).out; + time = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".time."0.3.17" { inherit profileName; }).out; + ${ if hostPlatform.isWindows then "winapi" else null } = (rustPackages."registry+https://github.com/rust-lang/crates.io-index".winapi."0.3.9" { inherit profileName; }).out; + }; + }); + "registry+https://github.com/rust-lang/crates.io-index".tempfile."3.3.0" = overridableMkRustCrate (profileName: rec { name = "tempfile"; version = "3.3.0"; @@ -5961,6 +5977,7 @@ in [ "ntsecapi" ] [ "ntstatus" ] [ "objbase" ] + [ "pdh" ] [ "processenv" ] [ "processthreadsapi" ] [ "profileapi" ] @@ -5977,6 +5994,7 @@ in (lib.optional (rootFeatures' ? "garage/consul-discovery" || rootFeatures' ? "garage_rpc/consul-discovery" || rootFeatures' ? "garage_rpc/reqwest") "winreg") [ "winsock2" ] [ "winstring" ] + [ "ws2def" ] [ "ws2ipdef" ] [ "ws2tcpip" ] [ "wtypesbase" ] diff --git a/Makefile b/Makefile index 23e10f78..55fa16dd 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ all: clear; cargo build release: - nix-build --arg release true + nix-build --attr pkgs.amd64.release --no-build-output shell: nix-shell diff --git a/default.nix b/default.nix index 1791b3c6..ecdf6436 100644 --- a/default.nix +++ b/default.nix @@ -1,7 +1,4 @@ -{ - system ? builtins.currentSystem, - git_version ? null, -}: +{ system ? builtins.currentSystem, git_version ? null, }: with import ./nix/common.nix; @@ -13,22 +10,21 @@ let debug = (compile { inherit system target git_version pkgsSrc cargo2nixOverlay; release = false; - }).workspace.garage { - compileMode = "build"; - }; + }).workspace.garage { compileMode = "build"; }; release = (compile { inherit system target git_version pkgsSrc cargo2nixOverlay; release = true; - }).workspace.garage { - compileMode = "build"; - }; + }).workspace.garage { compileMode = "build"; }; }); - test = (rustPkgs: pkgs.symlinkJoin { - name ="garage-tests"; - paths = builtins.map (key: rustPkgs.workspace.${key} { compileMode = "test"; }) (builtins.attrNames rustPkgs.workspace); - }); + test = (rustPkgs: + pkgs.symlinkJoin { + name = "garage-tests"; + paths = + builtins.map (key: rustPkgs.workspace.${key} { compileMode = "test"; }) + (builtins.attrNames rustPkgs.workspace); + }); in { pkgs = { @@ -55,8 +51,6 @@ in { inherit system git_version pkgsSrc cargo2nixOverlay; target = "x86_64-unknown-linux-musl"; compiler = "clippy"; - }).workspace.garage { - compileMode = "build"; - }; + }).workspace.garage { compileMode = "build"; }; }; } diff --git a/flake.nix b/flake.nix index c1d772bb..7eb9e33b 100644 --- a/flake.nix +++ b/flake.nix @@ -1,7 +1,9 @@ { - description = "Garage, an S3-compatible distributed object store for self-hosted deployments"; + description = + "Garage, an S3-compatible distributed object store for self-hosted deployments"; - inputs.nixpkgs.url = "github:NixOS/nixpkgs/a3073c49bc0163fea6a121c276f526837672b555"; + inputs.nixpkgs.url = + "github:NixOS/nixpkgs/a3073c49bc0163fea6a121c276f526837672b555"; inputs.cargo2nix = { # As of 2022-10-18: two small patches over unstable branch, one for clippy and one to fix feature detection url = "github:Alexis211/cargo2nix/a7a61179b66054904ef6a195d8da736eaaa06c36"; @@ -24,13 +26,11 @@ release = true; }).workspace.garage { compileMode = "build"; }; }; - devShell = ((compile { + devShell = (compile { inherit system git_version; pkgsSrc = nixpkgs; cargo2nixOverlay = cargo2nix.overlays.default; release = false; - }).workspaceShell { - packages = [ pkgs.rustfmt cargo2nix.packages.${system}.default ]; - }); + }).workspaceShell { packages = [ pkgs.rustfmt ]; }; }); } diff --git a/nix/build_index.nix b/nix/build_index.nix index 4625e3ae..7cc4f62c 100644 --- a/nix/build_index.nix +++ b/nix/build_index.nix @@ -1,10 +1,8 @@ -{ - path ? "/../aws-list.txt", -}: +{ path ? "/../aws-list.txt", }: with import ./common.nix; -let - pkgs = import pkgsSrc {}; +let + pkgs = import pkgsSrc { }; lib = pkgs.lib; /* Converts a key list and a value list to a set @@ -13,139 +11,182 @@ let listToSet [ "name" "version" ] [ "latex" "3.14" ] => { name = "latex"; version = "3.14"; } */ - listToSet = keys: values: - builtins.listToAttrs - (lib.zipListsWith - (a: b: { name = a; value = b; }) - keys - values); + listToSet = keys: values: + builtins.listToAttrs (lib.zipListsWith (a: b: { + name = a; + value = b; + }) keys values); /* Says if datetime a is more recent than datetime b - Example: - cmpDate { date = "2021-09-10"; time = "22:12:15"; } { date = "2021-02-03"; time = "23:54:12"; } - => true + Example: + cmpDate { date = "2021-09-10"; time = "22:12:15"; } { date = "2021-02-03"; time = "23:54:12"; } + => true */ - cmpDate = a: b: - let da = (builtins.head a.builds).date; - db = (builtins.head b.builds).date; - in - if da == db then (builtins.head a.builds).time > (builtins.head b.builds).time - else da > db; + cmpDate = a: b: + let + da = (builtins.head a.builds).date; + db = (builtins.head b.builds).date; + in if da == db then + (builtins.head a.builds).time > (builtins.head b.builds).time + else + da > db; - /* Pretty platforms */ + # Pretty platforms prettyPlatform = name: - if name == "aarch64-unknown-linux-musl" then "linux/arm64" - else if name == "armv6l-unknown-linux-musleabihf" then "linux/arm" - else if name == "x86_64-unknown-linux-musl" then "linux/amd64" - else if name == "i686-unknown-linux-musl" then "linux/386" - else name; + if name == "aarch64-unknown-linux-musl" then + "linux/arm64" + else if name == "armv6l-unknown-linux-musleabihf" then + "linux/arm" + else if name == "x86_64-unknown-linux-musl" then + "linux/amd64" + else if name == "i686-unknown-linux-musl" then + "linux/386" + else + name; - /* Parsing */ + # Parsing list = builtins.readFile (./. + path); entries = lib.splitString "\n" list; - elems = builtins.filter - (e: (builtins.length e) == 4) - (map - (x: builtins.filter (e: e != "") (lib.splitString " " x)) - entries); + elems = builtins.filter (e: (builtins.length e) == 4) + (map (x: builtins.filter (e: e != "") (lib.splitString " " x)) entries); - keys = ["date" "time" "size" "path"]; + keys = [ "date" "time" "size" "path" ]; parsed = map (entry: listToSet keys entry) elems; - subkeys = ["root" "version" "platform" "binary" ]; - builds = map (entry: entry // listToSet subkeys (lib.splitString "/" entry.path) // { url = "https://garagehq.deuxfleurs.fr/" + entry.path; }) parsed; + subkeys = [ "root" "version" "platform" "binary" ]; + builds = map (entry: + entry // listToSet subkeys (lib.splitString "/" entry.path) // { + url = "https://garagehq.deuxfleurs.fr/" + entry.path; + }) parsed; - /* Aggregation */ - builds_per_version = lib.foldl (acc: v: acc // { ${v.version} = if builtins.hasAttr v.version acc then acc.${v.version} ++ [ v ] else [ v ]; }) {} builds; + # Aggregation + builds_per_version = lib.foldl (acc: v: + acc // { + ${v.version} = if builtins.hasAttr v.version acc then + acc.${v.version} ++ [ v ] + else + [ v ]; + }) { } builds; versions = builtins.attrNames builds_per_version; - versions_release = builtins.filter (x: builtins.match "v[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?" x != null) versions; - versions_commit = builtins.filter (x: builtins.match "[0-9a-f]{40}" x != null) versions; - versions_extra = lib.subtractLists (versions_release ++ versions_commit) versions; + versions_release = builtins.filter + (x: builtins.match "v[0-9]+.[0-9]+.[0-9]+(.[0-9]+)?" x != null) versions; + versions_commit = + builtins.filter (x: builtins.match "[0-9a-f]{40}" x != null) versions; + versions_extra = + lib.subtractLists (versions_release ++ versions_commit) versions; sorted_builds = [ { name = "Release"; hide = false; type = "tag"; - description = "Release builds are the official builds, they are tailored for productions and are the most tested."; - builds = builtins.sort (a: b: a.version > b.version) (map (x: { version = x; builds = builtins.getAttr x builds_per_version; }) versions_release); + description = + "Release builds are the official builds, they are tailored for productions and are the most tested."; + builds = builtins.sort (a: b: a.version > b.version) (map (x: { + version = x; + builds = builtins.getAttr x builds_per_version; + }) versions_release); } { name = "Extra"; hide = true; type = "tag"; - description = "Extra builds are built on demand to test a specific feature or a specific need."; - builds = builtins.sort cmpDate (map (x: { version = x; builds = builtins.getAttr x builds_per_version; }) versions_extra); + description = + "Extra builds are built on demand to test a specific feature or a specific need."; + builds = builtins.sort cmpDate (map (x: { + version = x; + builds = builtins.getAttr x builds_per_version; + }) versions_extra); } { name = "Development"; hide = true; type = "commit"; - description = "Development builds are built periodically. Use them if you want to test a specific feature that is not yet released."; - builds = builtins.sort cmpDate (map (x: { version = x; builds = builtins.getAttr x builds_per_version; }) versions_commit); + description = + "Development builds are built periodically. Use them if you want to test a specific feature that is not yet released."; + builds = builtins.sort cmpDate (map (x: { + version = x; + builds = builtins.getAttr x builds_per_version; + }) versions_commit); } ]; - json = pkgs.writeTextDir "share/_releases.json" (builtins.toJSON sorted_builds); + json = + pkgs.writeTextDir "share/_releases.json" (builtins.toJSON sorted_builds); html = pkgs.writeTextDir "share/_releases.html" '' - - - - - Garage releases - - - - ${ builtins.toString (lib.forEach sorted_builds (r: '' -
-

${r.name} builds

+ + + + + Garage releases + + + + ${ + builtins.toString (lib.forEach sorted_builds (r: '' +
+

${r.name} builds

-

${r.description}

+

${r.description}

- ${if r.hide then "
Show ${r.name} builds" else ""} - ${ builtins.toString (lib.forEach r.builds (x: '' -

${x.version} (${(builtins.head x.builds).date})

-

See this build on

-

Binaries: -

-

Sources: -

- '')) } - ${ if builtins.length r.builds == 0 then "There is no build for this category" else "" } - ${if r.hide then "
" else ""} -
- ''))} - - -''; -in - pkgs.symlinkJoin { - name = "releases"; - paths = [ json html ]; - } + ${ + if r.hide then + "
Show ${r.name} builds" + else + "" + } + ${ + builtins.toString (lib.forEach r.builds (x: '' +

${x.version} (${(builtins.head x.builds).date})

+

See this build on

+

Binaries: +

+

Sources: +

+ '')) + } + ${ + if builtins.length r.builds == 0 then + "There is no build for this category" + else + "" + } + ${if r.hide then "
" else ""} +
+ '')) + } + + + ''; +in pkgs.symlinkJoin { + name = "releases"; + paths = [ json html ]; +} diff --git a/nix/common.nix b/nix/common.nix index 90e3afaf..57f354dd 100644 --- a/nix/common.nix +++ b/nix/common.nix @@ -1,10 +1,9 @@ rec { - /* - * Fixed dependencies - */ + # * Fixed dependencies pkgsSrc = fetchTarball { # As of 2022-10-13 - url = "https://github.com/NixOS/nixpkgs/archive/a3073c49bc0163fea6a121c276f526837672b555.zip"; + url = + "https://github.com/NixOS/nixpkgs/archive/a3073c49bc0163fea6a121c276f526837672b555.zip"; sha256 = "1bz632psfbpmicyzjb8b4265y50shylccvfm6ry6mgnv5hvz324s"; }; cargo2nixSrc = fetchGit { @@ -14,9 +13,7 @@ rec { rev = "a7a61179b66054904ef6a195d8da736eaaa06c36"; }; - /* - * Shared objects - */ + # * Shared objects cargo2nix = import cargo2nixSrc; cargo2nixOverlay = cargo2nix.overlays.default; } diff --git a/nix/compile.nix b/nix/compile.nix index 3ea5035e..54e920db 100644 --- a/nix/compile.nix +++ b/nix/compile.nix @@ -1,227 +1,240 @@ -{ - system, - target ? null, - pkgsSrc, - cargo2nixOverlay, - compiler ? "rustc", - release ? false, - git_version ? null, - features ? null, -}: +{ system, target ? null, pkgsSrc, cargo2nixOverlay, compiler ? "rustc" +, release ? false, git_version ? null, features ? null, }: let log = v: builtins.trace v v; - pkgs = - if target != null then - import pkgsSrc { - inherit system; - crossSystem = { - config = target; - isStatic = true; - }; - overlays = [ cargo2nixOverlay ]; - } - else - import pkgsSrc { - inherit system; - overlays = [ cargo2nixOverlay ]; - }; - - /* - Cargo2nix is built for rustOverlay which installs Rust from Mozilla releases. - This is fine for 64-bit platforms, but for 32-bit platforms, we need our own Rust - to avoid incompatibilities with time_t between different versions of musl - (>= 1.2.0 shipped by NixOS, < 1.2.0 with which rustc was built), which lead to compilation breakage. - So we want a Rust release that is bound to our Nix repository to avoid these problems. - See here for more info: https://musl.libc.org/time64.html - Because Cargo2nix does not support the Rust environment shipped by NixOS, - we emulate the structure of the Rust object created by rustOverlay. - In practise, rustOverlay ships rustc+cargo in a single derivation while - NixOS ships them in separate ones. We reunite them with symlinkJoin. - */ - toolchainOptions = - if target == null || target == "x86_64-unknown-linux-musl" || target == "aarch64-unknown-linux-musl" then { - rustVersion = "1.63.0"; - extraRustComponents = [ "clippy" ]; - } else { - rustToolchain = pkgs.symlinkJoin { - name = "rust-static-toolchain-${target}"; - paths = [ - pkgs.rustPlatform.rust.cargo - pkgs.rustPlatform.rust.rustc - # clippy not needed, it only runs on amd64 - ]; + pkgs = if target != null then + import pkgsSrc { + inherit system; + crossSystem = { + config = target; + isStatic = true; }; + overlays = [ cargo2nixOverlay ]; + } + else + import pkgsSrc { + inherit system; + overlays = [ cargo2nixOverlay ]; }; - - buildEnv = (drv: { - rustc = drv.setBuildEnv; - clippy = '' - ${drv.setBuildEnv or "" } - echo - echo --- BUILDING WITH CLIPPY --- - echo - - export NIX_RUST_BUILD_FLAGS="''${NIX_RUST_BUILD_FLAGS} --deny warnings" - export RUSTC="''${CLIPPY_DRIVER}" - ''; - }.${compiler}); - - /* - Cargo2nix provides many overrides by default, you can take inspiration from them: - https://github.com/cargo2nix/cargo2nix/blob/master/overlay/overrides.nix - - You can have a complete list of the available options by looking at the overriden object, mkcrate: - https://github.com/cargo2nix/cargo2nix/blob/master/overlay/mkcrate.nix + /* Cargo2nix is built for rustOverlay which installs Rust from Mozilla releases. + This is fine for 64-bit platforms, but for 32-bit platforms, we need our own Rust + to avoid incompatibilities with time_t between different versions of musl + (>= 1.2.0 shipped by NixOS, < 1.2.0 with which rustc was built), which lead to compilation breakage. + So we want a Rust release that is bound to our Nix repository to avoid these problems. + See here for more info: https://musl.libc.org/time64.html + Because Cargo2nix does not support the Rust environment shipped by NixOS, + we emulate the structure of the Rust object created by rustOverlay. + In practise, rustOverlay ships rustc+cargo in a single derivation while + NixOS ships them in separate ones. We reunite them with symlinkJoin. */ - packageOverrides = pkgs: pkgs.rustBuilder.overrides.all ++ [ - /* - [1] We add some logic to compile our crates with clippy, it provides us many additional lints + toolchainOptions = if target == null || target == "x86_64-unknown-linux-musl" + || target == "aarch64-unknown-linux-musl" then { + rustVersion = "1.63.0"; + extraRustComponents = [ "clippy" ]; + } else { + rustToolchain = pkgs.symlinkJoin { + name = "rust-static-toolchain-${target}"; + paths = [ + pkgs.rustPlatform.rust.cargo + pkgs.rustPlatform.rust.rustc + # clippy not needed, it only runs on amd64 + ]; + }; + }; - [2] We need to alter Nix hardening to make static binaries: PIE, - Position Independent Executables seems to be supported only on amd64. Having - this flag set either 1. make our executables crash or 2. compile as dynamic on some platforms. - Here, we deactivate it. Later (find `codegenOpts`), we reactivate it for supported targets - (only amd64 curently) through the `-static-pie` flag. - PIE is a feature used by ASLR, which helps mitigate security issues. - Learn more about Nix Hardening at: https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/cc-wrapper/add-hardening.sh + buildEnv = (drv: + { + rustc = drv.setBuildEnv; + clippy = '' + ${drv.setBuildEnv or ""} + echo + echo --- BUILDING WITH CLIPPY --- + echo - [3] We want to inject the git version while keeping the build deterministic. - As we do not want to consider the .git folder as part of the input source, - we ask the user (the CI often) to pass the value to Nix. + export NIX_RUST_BUILD_FLAGS="''${NIX_RUST_BUILD_FLAGS} --deny warnings" + export RUSTC="''${CLIPPY_DRIVER}" + ''; + }.${compiler}); - [4] We don't want libsodium-sys and zstd-sys to try to use pkgconfig to build against a system library. - However the features to do so get activated for some reason (due to a bug in cargo2nix?), - so disable them manually here. - */ - (pkgs.rustBuilder.rustLib.makeOverride { - name = "garage"; - overrideAttrs = drv: - (if git_version != null then { - /* [3] */ preConfigure = '' - ${drv.preConfigure or ""} - export GIT_VERSION="${git_version}" - ''; - } else {}) - // - { - /* [1] */ setBuildEnv = (buildEnv drv); - /* [2] */ hardeningDisable = [ "pie" ]; - }; - }) + /* Cargo2nix provides many overrides by default, you can take inspiration from them: + https://github.com/cargo2nix/cargo2nix/blob/master/overlay/overrides.nix - (pkgs.rustBuilder.rustLib.makeOverride { - name = "garage_rpc"; - overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; - }) - - (pkgs.rustBuilder.rustLib.makeOverride { - name = "garage_db"; - overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; - }) - - (pkgs.rustBuilder.rustLib.makeOverride { - name = "garage_util"; - overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; - }) - - (pkgs.rustBuilder.rustLib.makeOverride { - name = "garage_table"; - overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; - }) - - (pkgs.rustBuilder.rustLib.makeOverride { - name = "garage_block"; - overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; - }) - - (pkgs.rustBuilder.rustLib.makeOverride { - name = "garage_model"; - overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; - }) - - (pkgs.rustBuilder.rustLib.makeOverride { - name = "garage_api"; - overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; - }) - - (pkgs.rustBuilder.rustLib.makeOverride { - name = "garage_web"; - overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; - }) - - (pkgs.rustBuilder.rustLib.makeOverride { - name = "k2v-client"; - overrideAttrs = drv: { /* [1] */ setBuildEnv = (buildEnv drv); }; - }) - - (pkgs.rustBuilder.rustLib.makeOverride { - name = "libsodium-sys"; - overrideArgs = old: { - features = [ ]; /* [4] */ - }; - }) - - (pkgs.rustBuilder.rustLib.makeOverride { - name = "zstd-sys"; - overrideArgs = old: { - features = [ ]; /* [4] */ - }; - }) - ]; - - /* - We ship some parts of the code disabled by default by putting them behind a flag. - It speeds up the compilation (when the feature is not required) and released crates have less dependency by default (less attack surface, disk space, etc.). - But we want to ship these additional features when we release Garage. - In the end, we chose to exclude all features from debug builds while putting (all of) them in the release builds. + You can have a complete list of the available options by looking at the overriden object, mkcrate: + https://github.com/cargo2nix/cargo2nix/blob/master/overlay/mkcrate.nix */ - rootFeatures = if features != null then features else - ([ - "garage/bundled-libs" - "garage/sled" - "garage/k2v" - ] ++ (if release then [ - "garage/consul-discovery" - "garage/kubernetes-discovery" - "garage/metrics" - "garage/telemetry-otlp" - "garage/lmdb" - "garage/sqlite" - ] else [])); + packageOverrides = pkgs: + pkgs.rustBuilder.overrides.all ++ [ + /* [1] We add some logic to compile our crates with clippy, it provides us many additional lints + [2] We need to alter Nix hardening to make static binaries: PIE, + Position Independent Executables seems to be supported only on amd64. Having + this flag set either 1. make our executables crash or 2. compile as dynamic on some platforms. + Here, we deactivate it. Later (find `codegenOpts`), we reactivate it for supported targets + (only amd64 curently) through the `-static-pie` flag. + PIE is a feature used by ASLR, which helps mitigate security issues. + Learn more about Nix Hardening at: https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/cc-wrapper/add-hardening.sh + + [3] We want to inject the git version while keeping the build deterministic. + As we do not want to consider the .git folder as part of the input source, + we ask the user (the CI often) to pass the value to Nix. + + [4] We don't want libsodium-sys and zstd-sys to try to use pkgconfig to build against a system library. + However the features to do so get activated for some reason (due to a bug in cargo2nix?), + so disable them manually here. + */ + (pkgs.rustBuilder.rustLib.makeOverride { + name = "garage"; + overrideAttrs = drv: + (if git_version != null then { + # [3] + preConfigure = '' + ${drv.preConfigure or ""} + export GIT_VERSION="${git_version}" + ''; + } else + { }) // { + # [1] + setBuildEnv = (buildEnv drv); + # [2] + hardeningDisable = [ "pie" ]; + }; + }) + + (pkgs.rustBuilder.rustLib.makeOverride { + name = "garage_rpc"; + overrideAttrs = drv: { # [1] + setBuildEnv = (buildEnv drv); + }; + }) + + (pkgs.rustBuilder.rustLib.makeOverride { + name = "garage_db"; + overrideAttrs = drv: { # [1] + setBuildEnv = (buildEnv drv); + }; + }) + + (pkgs.rustBuilder.rustLib.makeOverride { + name = "garage_util"; + overrideAttrs = drv: { # [1] + setBuildEnv = (buildEnv drv); + }; + }) + + (pkgs.rustBuilder.rustLib.makeOverride { + name = "garage_table"; + overrideAttrs = drv: { # [1] + setBuildEnv = (buildEnv drv); + }; + }) + + (pkgs.rustBuilder.rustLib.makeOverride { + name = "garage_block"; + overrideAttrs = drv: { # [1] + setBuildEnv = (buildEnv drv); + }; + }) + + (pkgs.rustBuilder.rustLib.makeOverride { + name = "garage_model"; + overrideAttrs = drv: { # [1] + setBuildEnv = (buildEnv drv); + }; + }) + + (pkgs.rustBuilder.rustLib.makeOverride { + name = "garage_api"; + overrideAttrs = drv: { # [1] + setBuildEnv = (buildEnv drv); + }; + }) + + (pkgs.rustBuilder.rustLib.makeOverride { + name = "garage_web"; + overrideAttrs = drv: { # [1] + setBuildEnv = (buildEnv drv); + }; + }) + + (pkgs.rustBuilder.rustLib.makeOverride { + name = "k2v-client"; + overrideAttrs = drv: { # [1] + setBuildEnv = (buildEnv drv); + }; + }) + + (pkgs.rustBuilder.rustLib.makeOverride { + name = "libsodium-sys"; + overrideArgs = old: { + features = [ ]; # [4] + }; + }) + + (pkgs.rustBuilder.rustLib.makeOverride { + name = "zstd-sys"; + overrideArgs = old: { + features = [ ]; # [4] + }; + }) + ]; + + /* We ship some parts of the code disabled by default by putting them behind a flag. + It speeds up the compilation (when the feature is not required) and released crates have less dependency by default (less attack surface, disk space, etc.). + But we want to ship these additional features when we release Garage. + In the end, we chose to exclude all features from debug builds while putting (all of) them in the release builds. + */ + rootFeatures = if features != null then + features + else + ([ "garage/bundled-libs" "garage/sled" "garage/k2v" ] ++ (if release then [ + "garage/consul-discovery" + "garage/kubernetes-discovery" + "garage/metrics" + "garage/telemetry-otlp" + "garage/lmdb" + "garage/sqlite" + ] else + [ ])); packageFun = import ../Cargo.nix; - /* - We compile fully static binaries with musl to simplify deployment on most systems. - When possible, we reactivate PIE hardening (see above). + /* We compile fully static binaries with musl to simplify deployment on most systems. + When possible, we reactivate PIE hardening (see above). - Also, if you set the RUSTFLAGS environment variable, the following parameters will - be ignored. + Also, if you set the RUSTFLAGS environment variable, the following parameters will + be ignored. - For more information on static builds, please refer to Rust's RFC 1721. - https://rust-lang.github.io/rfcs/1721-crt-static.html#specifying-dynamicstatic-c-runtime-linkage + For more information on static builds, please refer to Rust's RFC 1721. + https://rust-lang.github.io/rfcs/1721-crt-static.html#specifying-dynamicstatic-c-runtime-linkage */ codegenOpts = { - "armv6l-unknown-linux-musleabihf" = [ "target-feature=+crt-static" "link-arg=-static" ]; /* compile as dynamic with static-pie */ - "aarch64-unknown-linux-musl" = [ "target-feature=+crt-static" "link-arg=-static" ]; /* segfault with static-pie */ - "i686-unknown-linux-musl" = [ "target-feature=+crt-static" "link-arg=-static" ]; /* segfault with static-pie */ - "x86_64-unknown-linux-musl" = [ "target-feature=+crt-static" "link-arg=-static-pie" ]; + "armv6l-unknown-linux-musleabihf" = [ + "target-feature=+crt-static" + "link-arg=-static" + ]; # compile as dynamic with static-pie + "aarch64-unknown-linux-musl" = [ + "target-feature=+crt-static" + "link-arg=-static" + ]; # segfault with static-pie + "i686-unknown-linux-musl" = [ + "target-feature=+crt-static" + "link-arg=-static" + ]; # segfault with static-pie + "x86_64-unknown-linux-musl" = + [ "target-feature=+crt-static" "link-arg=-static-pie" ]; }; - /* - NixOS and Rust/Cargo triples do not match for ARM, fix it here. - */ - rustTarget = if target == "armv6l-unknown-linux-musleabihf" - then "arm-unknown-linux-musleabihf" - else target; + # NixOS and Rust/Cargo triples do not match for ARM, fix it here. + rustTarget = if target == "armv6l-unknown-linux-musleabihf" then + "arm-unknown-linux-musleabihf" + else + target; -in - pkgs.rustBuilder.makePackageSet ({ - inherit release packageFun packageOverrides codegenOpts rootFeatures; - target = rustTarget; - } // toolchainOptions) +in pkgs.rustBuilder.makePackageSet ({ + inherit release packageFun packageOverrides codegenOpts rootFeatures; + target = rustTarget; +} // toolchainOptions) diff --git a/nix/kaniko.nix b/nix/kaniko.nix index 140328b8..64cadd14 100644 --- a/nix/kaniko.nix +++ b/nix/kaniko.nix @@ -15,7 +15,8 @@ pkgs.buildGoModule rec { checkPhase = "true"; meta = with pkgs.lib; { - description = "kaniko is a tool to build container images from a Dockerfile, inside a container or Kubernetes cluster."; + description = + "kaniko is a tool to build container images from a Dockerfile, inside a container or Kubernetes cluster."; homepage = "https://github.com/GoogleContainerTools/kaniko"; license = licenses.asl20; platforms = platforms.linux; diff --git a/nix/manifest-tool.nix b/nix/manifest-tool.nix index 182ccc0e..1090a6ef 100644 --- a/nix/manifest-tool.nix +++ b/nix/manifest-tool.nix @@ -15,7 +15,8 @@ pkgs.buildGoModule rec { checkPhase = "true"; meta = with pkgs.lib; { - description = "Command line tool to create and query container image manifest list/indexes"; + description = + "Command line tool to create and query container image manifest list/indexes"; homepage = "https://github.com/estesp/manifest-tool"; license = licenses.asl20; platforms = platforms.linux; diff --git a/nix/toolchain.nix b/nix/toolchain.nix index 079fcf13..532db74e 100644 --- a/nix/toolchain.nix +++ b/nix/toolchain.nix @@ -1,6 +1,4 @@ -{ - system ? builtins.currentSystem, -}: +{ system ? builtins.currentSystem, }: with import ./common.nix; @@ -11,27 +9,23 @@ let #"aarch64-unknown-linux-musl" "armv6l-unknown-linux-musleabihf" ]; - pkgsList = builtins.map (target: import pkgsSrc { - inherit system; - crossSystem = { - config = target; - isStatic = true; - }; - overlays = [ cargo2nixOverlay ]; - }) platforms; - pkgsHost = import pkgsSrc {}; + pkgsList = builtins.map (target: + import pkgsSrc { + inherit system; + crossSystem = { + config = target; + isStatic = true; + }; + overlays = [ cargo2nixOverlay ]; + }) platforms; + pkgsHost = import pkgsSrc { }; lib = pkgsHost.lib; kaniko = (import ./kaniko.nix) pkgsHost; winscp = (import ./winscp.nix) pkgsHost; manifestTool = (import ./manifest-tool.nix) pkgsHost; -in - lib.flatten (builtins.map (pkgs: [ - pkgs.rustPlatform.rust.rustc - pkgs.rustPlatform.rust.cargo - pkgs.buildPackages.stdenv.cc - ]) pkgsList) ++ [ - kaniko - winscp - manifestTool - ] +in lib.flatten (builtins.map (pkgs: [ + pkgs.rustPlatform.rust.rustc + pkgs.rustPlatform.rust.cargo + pkgs.buildPackages.stdenv.cc +]) pkgsList) ++ [ kaniko winscp manifestTool ] diff --git a/nix/winscp.nix b/nix/winscp.nix index 10d3cb28..113f4506 100644 --- a/nix/winscp.nix +++ b/nix/winscp.nix @@ -11,12 +11,12 @@ pkgs.stdenv.mkDerivation rec { }; buildPhase = '' - cat > winscp < winscp < /tmp/nix-signing-key.sec - nix copy \ - --to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/tmp/nix-signing-key.sec' \ - $(nix-store -qR \ - $(nix-build --no-build-output --no-out-link nix/toolchain.nix)) - rm /tmp/nix-signing-key.sec + # --- Release shell --- + # A shell built to make releasing easier + release = pkgs.mkShell { + shellHook = '' + function refresh_toolchain { + pass show deuxfleurs/nix_priv_key > /tmp/nix-signing-key.sec + nix copy \ + --to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/tmp/nix-signing-key.sec' \ + $(nix-store -qR \ + $(nix-build --no-build-output --no-out-link nix/toolchain.nix)) + rm /tmp/nix-signing-key.sec + } + + function refresh_cache { + pass show deuxfleurs/nix_priv_key > /tmp/nix-signing-key.sec + for attr in clippy.amd64 test.amd64 pkgs.{amd64,i386,arm,arm64}.{debug,release}; do + echo "Updating cache for ''${attr}" + derivation=$(nix-instantiate --attr ''${attr}) + nix copy -j8 \ + --to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/tmp/nix-signing-key.sec' \ + $(nix-store -qR ''${derivation%\!bin}) + done + rm /tmp/nix-signing-key.sec + } + + function refresh_flake_cache { + pass show deuxfleurs/nix_priv_key > /tmp/nix-signing-key.sec + for attr in packages.x86_64-linux.default devShell.x86_64-linux; do + echo "Updating cache for ''${attr}" + derivation=$(nix path-info --derivation ".#''${attr}") + nix copy -j8 \ + --to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/tmp/nix-signing-key.sec' \ + $(nix-store -qR ''${derivation}) + done + rm /tmp/nix-signing-key.sec + } + + function to_s3 { + aws \ + --endpoint-url https://garage.deuxfleurs.fr \ + --region garage \ + s3 cp \ + ./result-bin/bin/garage \ + s3://garagehq.deuxfleurs.fr/_releases/''${DRONE_TAG:-$DRONE_COMMIT}/''${TARGET}/garage + } + + function to_docker { + executor \ + --force \ + --customPlatform="''${DOCKER_PLATFORM}" \ + --destination "''${CONTAINER_NAME}:''${CONTAINER_TAG}" \ + --context dir://`pwd` \ + --verbosity=debug + } + + function multiarch_docker { + manifest-tool push from-spec <(cat < aws-list.txt + + nix-build nix/build_index.nix + + aws \ + --endpoint-url https://garage.deuxfleurs.fr \ + --region garage \ + s3 cp \ + result/share/_releases.json \ + s3://garagehq.deuxfleurs.fr/ + + aws \ + --endpoint-url https://garage.deuxfleurs.fr \ + --region garage \ + s3 cp \ + result/share/_releases.html \ + s3://garagehq.deuxfleurs.fr/ + } + ''; + nativeBuildInputs = [ pkgs.awscli2 kaniko manifest-tool ]; + }; } -function refresh_cache { - pass show deuxfleurs/nix_priv_key > /tmp/nix-signing-key.sec - for attr in clippy.amd64 test.amd64 pkgs.{amd64,i386,arm,arm64}.{debug,release}; do - echo "Updating cache for ''${attr}" - derivation=$(nix-instantiate --attr ''${attr}) - nix copy -j8 \ - --to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/tmp/nix-signing-key.sec' \ - $(nix-store -qR ''${derivation%\!bin}) - done - rm /tmp/nix-signing-key.sec -} - -function refresh_flake_cache { - pass show deuxfleurs/nix_priv_key > /tmp/nix-signing-key.sec - for attr in packages.x86_64-linux.default; do - echo "Updating cache for ''${attr}" - derivation=$(nix path-info --derivation ".#''${attr}") - nix copy -j8 \ - --to 's3://nix?endpoint=garage.deuxfleurs.fr®ion=garage&secret-key=/tmp/nix-signing-key.sec' \ - $(nix-store -qR ''${derivation}) - done - rm /tmp/nix-signing-key.sec -} - -function to_s3 { - aws \ - --endpoint-url https://garage.deuxfleurs.fr \ - --region garage \ - s3 cp \ - ./result-bin/bin/garage \ - s3://garagehq.deuxfleurs.fr/_releases/''${DRONE_TAG:-$DRONE_COMMIT}/''${TARGET}/garage -} - -function to_docker { - executor \ - --force \ - --customPlatform="''${DOCKER_PLATFORM}" \ - --destination "''${CONTAINER_NAME}:''${CONTAINER_TAG}" \ - --context dir://`pwd` \ - --verbosity=debug -} - -function multiarch_docker { - manifest-tool push from-spec <(cat < aws-list.txt - - nix-build nix/build_index.nix - - aws \ - --endpoint-url https://garage.deuxfleurs.fr \ - --region garage \ - s3 cp \ - result/share/_releases.json \ - s3://garagehq.deuxfleurs.fr/ - - aws \ - --endpoint-url https://garage.deuxfleurs.fr \ - --region garage \ - s3 cp \ - result/share/_releases.html \ - s3://garagehq.deuxfleurs.fr/ -} - ''; - nativeBuildInputs = [ - pkgs.awscli2 - kaniko - manifest-tool - ]; - }; - } - - diff --git a/src/garage/admin.rs b/src/garage/admin.rs index 305c5c65..4eabebca 100644 --- a/src/garage/admin.rs +++ b/src/garage/admin.rs @@ -15,6 +15,7 @@ use garage_util::time::*; use garage_table::replication::*; use garage_table::*; +use garage_rpc::ring::PARTITION_BITS; use garage_rpc::*; use garage_block::manager::BlockResyncErrorInfo; @@ -783,6 +784,7 @@ impl AdminRpcHandler { for node in ring.layout.node_ids().iter() { let mut opt = opt.clone(); opt.all_nodes = false; + opt.skip_global = true; writeln!(&mut ret, "\n======================").unwrap(); writeln!(&mut ret, "Stats for node {:?}:", node).unwrap(); @@ -799,6 +801,15 @@ impl AdminRpcHandler { Err(e) => writeln!(&mut ret, "Network error: {}", e).unwrap(), } } + + writeln!(&mut ret, "\n======================").unwrap(); + write!( + &mut ret, + "Cluster statistics:\n\n{}", + self.gather_cluster_stats() + ) + .unwrap(); + Ok(AdminRpc::Ok(ret)) } else { Ok(AdminRpc::Ok(self.gather_stats_local(opt)?)) @@ -819,22 +830,6 @@ impl AdminRpcHandler { writeln!(&mut ret, "\nDatabase engine: {}", self.garage.db.engine()).unwrap(); - // Gather ring statistics - let ring = self.garage.system.ring.borrow().clone(); - let mut ring_nodes = HashMap::new(); - for (_i, loc) in ring.partitions().iter() { - for n in ring.get_nodes(loc, ring.replication_factor).iter() { - if !ring_nodes.contains_key(n) { - ring_nodes.insert(*n, 0usize); - } - *ring_nodes.get_mut(n).unwrap() += 1; - } - } - writeln!(&mut ret, "\nRing nodes & partition count:").unwrap(); - for (n, c) in ring_nodes.iter() { - writeln!(&mut ret, " {:?} {}", n, c).unwrap(); - } - // Gather table statistics let mut table = vec![" Table\tItems\tMklItems\tMklTodo\tGcTodo".into()]; table.push(self.gather_table_stats(&self.garage.bucket_table, opt.detailed)?); @@ -881,12 +876,108 @@ impl AdminRpcHandler { .unwrap(); if !opt.detailed { - writeln!(&mut ret, "\nIf values are missing (marked as NC), consider adding the --detailed flag - this will be slow.").unwrap(); + writeln!(&mut ret, "\nIf values are missing above (marked as NC), consider adding the --detailed flag (this will be slow).").unwrap(); + } + + if !opt.skip_global { + write!(&mut ret, "\n{}", self.gather_cluster_stats()).unwrap(); } Ok(ret) } + fn gather_cluster_stats(&self) -> String { + let mut ret = String::new(); + + // Gather storage node and free space statistics + let layout = &self.garage.system.ring.borrow().layout; + let mut node_partition_count = HashMap::::new(); + for short_id in layout.ring_assignation_data.iter() { + let id = layout.node_id_vec[*short_id as usize]; + *node_partition_count.entry(id).or_default() += 1; + } + let node_info = self + .garage + .system + .get_known_nodes() + .into_iter() + .map(|n| (n.id, n)) + .collect::>(); + + let mut table = vec![" ID\tHostname\tZone\tCapacity\tPart.\tDataAvail\tMetaAvail".into()]; + for (id, parts) in node_partition_count.iter() { + let info = node_info.get(id); + let status = info.map(|x| &x.status); + let role = layout.roles.get(id).and_then(|x| x.0.as_ref()); + let hostname = status.map(|x| x.hostname.as_str()).unwrap_or("?"); + let zone = role.map(|x| x.zone.as_str()).unwrap_or("?"); + let capacity = role.map(|x| x.capacity_string()).unwrap_or("?".into()); + let avail_str = |x| match x { + Some((avail, total)) => { + let pct = (avail as f64) / (total as f64) * 100.; + let avail = bytesize::ByteSize::b(avail); + let total = bytesize::ByteSize::b(total); + format!("{}/{} ({:.1}%)", avail, total, pct) + } + None => "?".into(), + }; + let data_avail = avail_str(status.and_then(|x| x.data_disk_avail)); + let meta_avail = avail_str(status.and_then(|x| x.meta_disk_avail)); + table.push(format!( + " {:?}\t{}\t{}\t{}\t{}\t{}\t{}", + id, hostname, zone, capacity, parts, data_avail, meta_avail + )); + } + write!( + &mut ret, + "Storage nodes:\n{}", + format_table_to_string(table) + ) + .unwrap(); + + let meta_part_avail = node_partition_count + .iter() + .filter_map(|(id, parts)| { + node_info + .get(id) + .and_then(|x| x.status.meta_disk_avail) + .map(|c| c.0 / *parts) + }) + .collect::>(); + let data_part_avail = node_partition_count + .iter() + .filter_map(|(id, parts)| { + node_info + .get(id) + .and_then(|x| x.status.data_disk_avail) + .map(|c| c.0 / *parts) + }) + .collect::>(); + if !meta_part_avail.is_empty() && !data_part_avail.is_empty() { + let meta_avail = + bytesize::ByteSize(meta_part_avail.iter().min().unwrap() * (1 << PARTITION_BITS)); + let data_avail = + bytesize::ByteSize(data_part_avail.iter().min().unwrap() * (1 << PARTITION_BITS)); + writeln!( + &mut ret, + "\nEstimated available storage space cluster-wide (might be lower in practice):" + ) + .unwrap(); + if meta_part_avail.len() < node_partition_count.len() + || data_part_avail.len() < node_partition_count.len() + { + writeln!(&mut ret, " data: < {}", data_avail).unwrap(); + writeln!(&mut ret, " metadata: < {}", meta_avail).unwrap(); + writeln!(&mut ret, "A precise estimate could not be given as information is missing for some storage nodes.").unwrap(); + } else { + writeln!(&mut ret, " data: {}", data_avail).unwrap(); + writeln!(&mut ret, " metadata: {}", meta_avail).unwrap(); + } + } + + ret + } + fn gather_table_stats( &self, t: &Arc>, diff --git a/src/garage/cli/cmd.rs b/src/garage/cli/cmd.rs index 46e9113c..af7f1aa1 100644 --- a/src/garage/cli/cmd.rs +++ b/src/garage/cli/cmd.rs @@ -59,18 +59,29 @@ pub async fn cmd_status(rpc_cli: &Endpoint, rpc_host: NodeID) -> let layout = fetch_layout(rpc_cli, rpc_host).await?; println!("==== HEALTHY NODES ===="); - let mut healthy_nodes = vec!["ID\tHostname\tAddress\tTags\tZone\tCapacity".to_string()]; + let mut healthy_nodes = + vec!["ID\tHostname\tAddress\tTags\tZone\tCapacity\tDataAvail\tMetaAvail".to_string()]; for adv in status.iter().filter(|adv| adv.is_up) { match layout.roles.get(&adv.id) { Some(NodeRoleV(Some(cfg))) => { + let data_avail = match &adv.status.data_disk_avail { + _ if cfg.capacity.is_none() => "N/A".into(), + Some((avail, total)) => { + let pct = (*avail as f64) / (*total as f64) * 100.; + let avail = bytesize::ByteSize::b(*avail); + format!("{} ({:.1}%)", avail, pct) + } + None => "?".into(), + }; healthy_nodes.push(format!( - "{id:?}\t{host}\t{addr}\t[{tags}]\t{zone}\t{capacity}", + "{id:?}\t{host}\t{addr}\t[{tags}]\t{zone}\t{capacity}\t{data_avail}", id = adv.id, host = adv.status.hostname, addr = adv.addr, tags = cfg.tags.join(","), zone = cfg.zone, capacity = cfg.capacity_string(), + data_avail = data_avail, )); } _ => { diff --git a/src/garage/cli/structs.rs b/src/garage/cli/structs.rs index 661a71f0..01ae92da 100644 --- a/src/garage/cli/structs.rs +++ b/src/garage/cli/structs.rs @@ -504,6 +504,11 @@ pub struct StatsOpt { /// Gather detailed statistics (this can be long) #[structopt(short = "d", long = "detailed")] pub detailed: bool, + + /// Don't show global cluster stats (internal use in RPC) + #[structopt(skip)] + #[serde(default)] + pub skip_global: bool, } #[derive(Serialize, Deserialize, StructOpt, Debug, Eq, PartialEq, Clone)] diff --git a/src/rpc/Cargo.toml b/src/rpc/Cargo.toml index 22b15679..87ae15ac 100644 --- a/src/rpc/Cargo.toml +++ b/src/rpc/Cargo.toml @@ -23,6 +23,7 @@ hex = "0.4" tracing = "0.1" rand = "0.8" sodiumoxide = { version = "0.2.5-0", package = "kuska-sodiumoxide" } +systemstat = "0.2.3" async-trait = "0.1.7" serde = { version = "1.0", default-features = false, features = ["derive", "rc"] } diff --git a/src/rpc/lib.rs b/src/rpc/lib.rs index a8cc0030..5aec92c0 100644 --- a/src/rpc/lib.rs +++ b/src/rpc/lib.rs @@ -3,6 +3,9 @@ #[macro_use] extern crate tracing; +mod metrics; +mod system_metrics; + #[cfg(feature = "consul-discovery")] mod consul; #[cfg(feature = "kubernetes-discovery")] @@ -13,9 +16,6 @@ pub mod replication_mode; pub mod ring; pub mod system; -mod metrics; pub mod rpc_helper; pub use rpc_helper::*; - -pub mod system_metrics; diff --git a/src/rpc/system.rs b/src/rpc/system.rs index 90f6a4c2..e0ced8cc 100644 --- a/src/rpc/system.rs +++ b/src/rpc/system.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use std::io::{Read, Write}; use std::net::{IpAddr, SocketAddr}; use std::path::{Path, PathBuf}; +use std::sync::atomic::Ordering; use std::sync::{Arc, RwLock}; use std::time::{Duration, Instant}; @@ -38,7 +39,6 @@ use crate::replication_mode::*; use crate::ring::*; use crate::rpc_helper::*; -#[cfg(feature = "metrics")] use crate::system_metrics::*; const DISCOVERY_INTERVAL: Duration = Duration::from_secs(60); @@ -106,7 +106,7 @@ pub struct System { consul_discovery: Option, #[cfg(feature = "kubernetes-discovery")] kubernetes_discovery: Option, - #[cfg(feature = "metrics")] + metrics: SystemMetrics, replication_mode: ReplicationMode, @@ -118,18 +118,28 @@ pub struct System { /// Path to metadata directory pub metadata_dir: PathBuf, + /// Path to data directory + pub data_dir: PathBuf, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NodeStatus { /// Hostname of the node pub hostname: String, + /// Replication factor configured on the node pub replication_factor: usize, /// Cluster layout version pub cluster_layout_version: u64, /// Hash of cluster layout staging data pub cluster_layout_staging_hash: Hash, + + /// Disk usage on partition containing metadata directory (tuple: `(avail, total)`) + #[serde(default)] + pub meta_disk_avail: Option<(u64, u64)>, + /// Disk usage on partition containing data directory (tuple: `(avail, total)`) + #[serde(default)] + pub data_disk_avail: Option<(u64, u64)>, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -271,18 +281,11 @@ impl System { } }; - let local_status = NodeStatus { - hostname: gethostname::gethostname() - .into_string() - .unwrap_or_else(|_| "".to_string()), - replication_factor, - cluster_layout_version: cluster_layout.version, - cluster_layout_staging_hash: cluster_layout.staging_hash, - }; - - #[cfg(feature = "metrics")] let metrics = SystemMetrics::new(replication_factor); + let mut local_status = NodeStatus::initial(replication_factor, &cluster_layout); + local_status.update_disk_usage(&config.metadata_dir, &config.data_dir, &metrics); + let ring = Ring::new(cluster_layout, replication_factor); let (update_ring, ring) = watch::channel(Arc::new(ring)); @@ -373,12 +376,12 @@ impl System { consul_discovery, #[cfg(feature = "kubernetes-discovery")] kubernetes_discovery: config.kubernetes_discovery.clone(), - #[cfg(feature = "metrics")] metrics, ring, update_ring: Mutex::new(update_ring), metadata_dir: config.metadata_dir.clone(), + data_dir: config.data_dir.clone(), }); sys.system_endpoint.set_handler(sys.clone()); Ok(sys) @@ -416,12 +419,7 @@ impl System { .get(&n.id.into()) .cloned() .map(|(_, st)| st) - .unwrap_or(NodeStatus { - hostname: "?".to_string(), - replication_factor: 0, - cluster_layout_version: 0, - cluster_layout_staging_hash: Hash::from([0u8; 32]), - }), + .unwrap_or(NodeStatus::unknown()), }) .collect::>(); known_nodes @@ -600,6 +598,9 @@ impl System { let ring = self.ring.borrow(); new_si.cluster_layout_version = ring.layout.version; new_si.cluster_layout_staging_hash = ring.layout.staging_hash; + + new_si.update_disk_usage(&self.metadata_dir, &self.data_dir, &self.metrics); + self.local_status.swap(Arc::new(new_si)); } @@ -864,6 +865,69 @@ impl EndpointHandler for System { } } +impl NodeStatus { + fn initial(replication_factor: usize, layout: &ClusterLayout) -> Self { + NodeStatus { + hostname: gethostname::gethostname() + .into_string() + .unwrap_or_else(|_| "".to_string()), + replication_factor, + cluster_layout_version: layout.version, + cluster_layout_staging_hash: layout.staging_hash, + meta_disk_avail: None, + data_disk_avail: None, + } + } + + fn unknown() -> Self { + NodeStatus { + hostname: "?".to_string(), + replication_factor: 0, + cluster_layout_version: 0, + cluster_layout_staging_hash: Hash::from([0u8; 32]), + meta_disk_avail: None, + data_disk_avail: None, + } + } + + fn update_disk_usage(&mut self, meta_dir: &Path, data_dir: &Path, metrics: &SystemMetrics) { + use systemstat::{Platform, System}; + let mounts = System::new().mounts().unwrap_or_default(); + + let mount_avail = |path: &Path| { + mounts + .iter() + .filter(|x| path.starts_with(&x.fs_mounted_on)) + .max_by_key(|x| x.fs_mounted_on.len()) + .map(|x| (x.avail.as_u64(), x.total.as_u64())) + }; + + self.meta_disk_avail = mount_avail(meta_dir); + self.data_disk_avail = mount_avail(data_dir); + + if let Some((avail, total)) = self.meta_disk_avail { + metrics + .values + .meta_disk_avail + .store(avail, Ordering::Relaxed); + metrics + .values + .meta_disk_total + .store(total, Ordering::Relaxed); + } + if let Some((avail, total)) = self.data_disk_avail { + metrics + .values + .data_disk_avail + .store(avail, Ordering::Relaxed); + metrics + .values + .data_disk_total + .store(total, Ordering::Relaxed); + } + } +} + fn get_default_ip() -> Option { pnet_datalink::interfaces() .iter() diff --git a/src/rpc/system_metrics.rs b/src/rpc/system_metrics.rs index d96b67e4..83f5fa97 100644 --- a/src/rpc/system_metrics.rs +++ b/src/rpc/system_metrics.rs @@ -1,14 +1,31 @@ +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; + use opentelemetry::{global, metrics::*, KeyValue}; /// TableMetrics reference all counter used for metrics pub struct SystemMetrics { pub(crate) _garage_build_info: ValueObserver, pub(crate) _replication_factor: ValueObserver, + pub(crate) _disk_avail: ValueObserver, + pub(crate) _disk_total: ValueObserver, + pub(crate) values: Arc, +} + +#[derive(Default)] +pub struct SystemMetricsValues { + pub(crate) data_disk_total: AtomicU64, + pub(crate) data_disk_avail: AtomicU64, + pub(crate) meta_disk_total: AtomicU64, + pub(crate) meta_disk_avail: AtomicU64, } impl SystemMetrics { pub fn new(replication_factor: usize) -> Self { let meter = global::meter("garage_system"); + let values = Arc::new(SystemMetricsValues::default()); + let values1 = values.clone(); + let values2 = values.clone(); Self { _garage_build_info: meter .u64_value_observer("garage_build_info", move |observer| { @@ -28,6 +45,33 @@ impl SystemMetrics { }) .with_description("Garage replication factor setting") .init(), + _disk_avail: meter + .u64_value_observer("garage_local_disk_avail", move |observer| { + match values1.data_disk_avail.load(Ordering::Relaxed) { + 0 => (), + x => observer.observe(x, &[KeyValue::new("volume", "data")]), + }; + match values1.meta_disk_avail.load(Ordering::Relaxed) { + 0 => (), + x => observer.observe(x, &[KeyValue::new("volume", "metadata")]), + }; + }) + .with_description("Garage available disk space on each node") + .init(), + _disk_total: meter + .u64_value_observer("garage_local_disk_total", move |observer| { + match values2.data_disk_total.load(Ordering::Relaxed) { + 0 => (), + x => observer.observe(x, &[KeyValue::new("volume", "data")]), + }; + match values2.meta_disk_total.load(Ordering::Relaxed) { + 0 => (), + x => observer.observe(x, &[KeyValue::new("volume", "metadata")]), + }; + }) + .with_description("Garage total disk space on each node") + .init(), + values, } } }