Merge branch 'master' into pkgs/blake3-1.7.0
This commit is contained in:
@@ -20,7 +20,7 @@ jobs:
|
||||
lib
|
||||
maintainers
|
||||
|
||||
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
|
||||
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
with:
|
||||
extra_nix_config: sandbox = true
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
rev=$(jq -r .rev ci/pinned-nixpkgs.json)
|
||||
echo "url=https://github.com/NixOS/nixpkgs/archive/$rev.tar.gz" >> "$GITHUB_ENV"
|
||||
|
||||
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
|
||||
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
with:
|
||||
extra_nix_config: sandbox = true
|
||||
nix_path: nixpkgs=${{ env.url }}
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
rev=$(jq -r .rev ci/pinned-nixpkgs.json)
|
||||
echo "url=https://github.com/NixOS/nixpkgs/archive/$rev.tar.gz" >> "$GITHUB_ENV"
|
||||
|
||||
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
|
||||
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
with:
|
||||
extra_nix_config: sandbox = true
|
||||
nix_path: nixpkgs=${{ env.url }}
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
|
||||
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
|
||||
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
|
||||
- name: Build shell
|
||||
run: nix-build shell.nix
|
||||
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
needs: get-merge-commit
|
||||
if: github.repository_owner == 'NixOS' && needs.get-merge-commit.outputs.mergedSha
|
||||
steps:
|
||||
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
|
||||
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
with:
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
if: github.repository_owner == 'NixOS'
|
||||
steps:
|
||||
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
|
||||
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
|
||||
# Important: Because we use pull_request_target, this checks out the base branch of the PR, not the PR head.
|
||||
# This is intentional, because we need to request the review of owners as declared in the base branch.
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
|
||||
|
||||
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
|
||||
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
with:
|
||||
# nixpkgs commit is pinned so that it doesn't break
|
||||
# editorconfig-checker 2.4.0
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
|
||||
|
||||
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
|
||||
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
with:
|
||||
extra_nix_config: sandbox = true
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
echo "targetSha=$targetSha" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
|
||||
uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
with:
|
||||
extra_nix_config: sandbox = true
|
||||
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
path: nixpkgs
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
|
||||
uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
with:
|
||||
extra_nix_config: sandbox = true
|
||||
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
path: nixpkgs
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
|
||||
uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
with:
|
||||
extra_nix_config: sandbox = true
|
||||
|
||||
@@ -149,7 +149,7 @@ jobs:
|
||||
path: nixpkgs
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
|
||||
uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
with:
|
||||
extra_nix_config: sandbox = true
|
||||
|
||||
@@ -254,7 +254,7 @@ jobs:
|
||||
path: comparison
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
|
||||
uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
|
||||
# Important: This workflow job runs with extra permissions,
|
||||
# so we need to make sure to not run untrusted code from PRs
|
||||
|
||||
@@ -19,13 +19,21 @@ permissions: {}
|
||||
jobs:
|
||||
nixos:
|
||||
name: nixos-manual-build
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
system:
|
||||
- x86_64-linux
|
||||
- aarch64-linux
|
||||
runs-on: >-
|
||||
${{ (matrix.system == 'x86_64-linux' && 'ubuntu-24.04')
|
||||
|| (matrix.system == 'aarch64-linux' && 'ubuntu-24.04-arm') }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
|
||||
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
|
||||
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
with:
|
||||
extra_nix_config: sandbox = true
|
||||
|
||||
@@ -34,7 +42,15 @@ jobs:
|
||||
with:
|
||||
# This cache is for the nixpkgs repo checks and should not be trusted or used elsewhere.
|
||||
name: nixpkgs-ci
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Building NixOS manual
|
||||
run: NIX_PATH=nixpkgs=$(pwd) nix-build --option restrict-eval true nixos/release.nix -A manual.x86_64-linux
|
||||
- name: Build NixOS manual
|
||||
id: build-manual
|
||||
run: NIX_PATH=nixpkgs=$(pwd) nix-build --option restrict-eval true nixos/release.nix -A manual.${{ matrix.system }}
|
||||
|
||||
- name: Upload NixOS manual
|
||||
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
|
||||
with:
|
||||
name: nixos-manual-${{ matrix.system }}
|
||||
path: result/
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.pull_request.number }}/merge
|
||||
|
||||
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
|
||||
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
with:
|
||||
extra_nix_config: sandbox = true
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
ref: ${{ needs.get-merge-commit.outputs.mergedSha }}
|
||||
if: ${{ env.CHANGED_FILES && env.CHANGED_FILES != '' }}
|
||||
|
||||
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
|
||||
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
with:
|
||||
extra_nix_config: sandbox = true
|
||||
nix_path: nixpkgs=channel:nixpkgs-unstable
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
git worktree add "$target" "$(git rev-parse HEAD^1)"
|
||||
echo "target=$target" >> "$GITHUB_ENV"
|
||||
|
||||
- uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
|
||||
- uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31
|
||||
|
||||
- name: Fetching the pinned tool
|
||||
# Update the pinned version using ci/nixpkgs-vet/update-pinned-tool.sh
|
||||
|
||||
@@ -149,10 +149,13 @@ nixos/modules/installer/tools/nix-fallback-paths.nix @NixOS/nix-team @raitobeza
|
||||
/nixos/modules/services/monitoring/amazon-cloudwatch-agent.nix @philipmw
|
||||
/nixos/tests/amazon-cloudwatch-agent.nix @philipmw
|
||||
|
||||
# Monitoring
|
||||
/nixos/modules/services/monitoring/fluent-bit.nix @arianvp
|
||||
/nixos/tests/fluent-bit.nix @arianvp
|
||||
|
||||
# nixos-rebuild-ng
|
||||
/pkgs/by-name/ni/nixos-rebuild-ng @thiagokokada
|
||||
|
||||
|
||||
# Updaters
|
||||
## update.nix
|
||||
/maintainers/scripts/update.nix @jtojnar
|
||||
@@ -399,6 +402,11 @@ pkgs/by-name/fo/forgejo/ @adamcstephens @bendlas @emilylange
|
||||
/pkgs/development/compilers/ocaml @ulrikstrid
|
||||
/pkgs/development/ocaml-modules @ulrikstrid
|
||||
|
||||
# ZFS
|
||||
/nixos/modules/tasks/filesystems/zfs.nix @adamcstephens @amarshall
|
||||
/nixos/tests/zfs.nix @adamcstephens @amarshall
|
||||
/pkgs/os-specific/linux/zfs @adamcstephens @amarshall
|
||||
|
||||
# Zig
|
||||
/pkgs/development/compilers/zig @figsoda @RossComputerGuy
|
||||
/doc/hooks/zig.section.md @figsoda @RossComputerGuy
|
||||
|
||||
@@ -71,7 +71,15 @@ let
|
||||
getLabels
|
||||
;
|
||||
|
||||
getAttrs = dir: builtins.fromJSON (builtins.readFile "${dir}/outpaths.json");
|
||||
getAttrs =
|
||||
dir:
|
||||
let
|
||||
raw = builtins.readFile "${dir}/outpaths.json";
|
||||
# The file contains Nix paths; we need to ignore them for evaluation purposes,
|
||||
# else there will be a "is not allowed to refer to a store path" error.
|
||||
data = builtins.unsafeDiscardStringContext raw;
|
||||
in
|
||||
builtins.fromJSON data;
|
||||
beforeAttrs = getAttrs beforeResultDir;
|
||||
afterAttrs = getAttrs afterResultDir;
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ A few markups for other kinds of literals are also available:
|
||||
These literal kinds are used mostly in NixOS option documentation.
|
||||
|
||||
This syntax is taken from [MyST](https://myst-parser.readthedocs.io/en/latest/syntax/syntax.html#roles-an-in-line-extension-point). Though, the feature originates from [reStructuredText](https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-manpage) with slightly different syntax.
|
||||
They are handled by `myst_role` defined per renderer. <!-- reverse references in code -->
|
||||
|
||||
#### Admonitions
|
||||
|
||||
|
||||
@@ -99,12 +99,17 @@ failure. To prevent this, guard the completion generation commands.
|
||||
```nix
|
||||
{
|
||||
nativeBuildInputs = [ installShellFiles ];
|
||||
postInstall = lib.optionalString (stdenv.buildPlatform.canExecute stdenv.hostPlatform) ''
|
||||
# using named fd
|
||||
installShellCompletion --cmd foobar \
|
||||
--bash <($out/bin/foobar --bash-completion) \
|
||||
--fish <($out/bin/foobar --fish-completion) \
|
||||
--zsh <($out/bin/foobar --zsh-completion)
|
||||
'';
|
||||
postInstall = lib.optionalString (stdenv.hostPlatform.emulatorAvailable buildPackages) (
|
||||
let
|
||||
emulator = stdenv.hostPlatform.emulator buildPackages;
|
||||
in
|
||||
''
|
||||
# using named fd
|
||||
installShellCompletion --cmd foobar \
|
||||
--bash <(${emulator} $out/bin/foobar --bash-completion) \
|
||||
--fish <(${emulator} $out/bin/foobar --fish-completion) \
|
||||
--zsh <(${emulator} $out/bin/foobar --zsh-completion)
|
||||
''
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -14,6 +14,7 @@ fish.section.md
|
||||
fuse.section.md
|
||||
geant4.section.md
|
||||
ibus.section.md
|
||||
inkscape.section.md
|
||||
kakoune.section.md
|
||||
krita.section.md
|
||||
linux.section.md
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# Inkscape {#sec-inkscape}
|
||||
|
||||
[Inkscape](https://inkscape.org) is a powerful vector graphics editor.
|
||||
|
||||
## Plugins {#inkscape-plugins}
|
||||
Inkscape plugins are collected in the [`inkscape-extensions`](https://search.nixos.org/packages?channel=unstable&type=packages&query=cudaPackages) package set.
|
||||
To enable them, use an override on `inkscape-with-extensions`:
|
||||
|
||||
```nix
|
||||
inkscape-with-extensions.override {
|
||||
inkscapeExtensions = with inkscape-extensions; [
|
||||
inkstitch
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
Similarly, this works in the shell:
|
||||
|
||||
```bash
|
||||
$ nix-shell -p 'inkscape-with-extensions.override { inkscapeExtensions = with inkscape-extensions; [inkstitch]; }'
|
||||
[nix-shell:~]$ # Ink/Stitch is now available via the extension menu
|
||||
[nix-shell:~]$ inkscape
|
||||
```
|
||||
|
||||
All available extension can be enabled by passing `inkscapeExtensions = null;`.
|
||||
|
||||
::: {.note}
|
||||
Loading the Inkscape extensions stand-alone (without using `override`) does not affect Inkscape at all.
|
||||
:::
|
||||
@@ -17,6 +17,9 @@
|
||||
"ex-testEqualArrayOrMap-test-function-add-cowbell": [
|
||||
"index.html#ex-testEqualArrayOrMap-test-function-add-cowbell"
|
||||
],
|
||||
"inkscape-plugins": [
|
||||
"index.html#inkscape-plugins"
|
||||
],
|
||||
"neovim": [
|
||||
"index.html#neovim"
|
||||
],
|
||||
@@ -62,6 +65,9 @@
|
||||
"sec-build-helper-extendMkDerivation": [
|
||||
"index.html#sec-build-helper-extendMkDerivation"
|
||||
],
|
||||
"sec-inkscape": [
|
||||
"index.html#sec-inkscape"
|
||||
],
|
||||
"sec-language-cosmic": [
|
||||
"index.html#sec-language-cosmic"
|
||||
],
|
||||
|
||||
@@ -75,6 +75,11 @@
|
||||
- `functor.wrapped` is now deprecated for some types and using it will give a warning with migration instructions. It is deprecated for these types:
|
||||
- `lib.types.attrsWith`
|
||||
- `lib.types.listOf`
|
||||
- `lib.types.unique` and `lib.types.uniq`
|
||||
- `lib.types.nullOr`
|
||||
- `lib.types.functionTo`
|
||||
- `lib.types.coercedTo`
|
||||
- `lib.types.either`
|
||||
|
||||
- Plasma 5 and Qt 5 based versions of associated software are deprecated in NixOS 25.05, and will be removed in NixOS 25.11. Users are encouraged to upgrade to Plasma 6.
|
||||
|
||||
|
||||
@@ -156,6 +156,7 @@ let
|
||||
makeScope makeScopeWithSplicing makeScopeWithSplicing'
|
||||
extendMkDerivation;
|
||||
inherit (self.derivations) lazyDerivation optionalDrvAttr warnOnInstantiate;
|
||||
inherit (self.generators) mkLuaInline;
|
||||
inherit (self.meta) addMetaAttrs dontDistribute setName updateName
|
||||
appendToName mapDerivationAttrset setPrio lowPrio lowPrioSet hiPrio
|
||||
hiPrioSet licensesSpdx getLicenseFromSpdxId getLicenseFromSpdxIdOr
|
||||
|
||||
+2
-2
@@ -743,6 +743,8 @@ in rec {
|
||||
"nil"
|
||||
else if isInt v || isFloat v || isString v || isBool v then
|
||||
toJSON v
|
||||
else if isPath v || isDerivation v then
|
||||
toJSON "${v}"
|
||||
else if isList v then
|
||||
(if v == [ ] then "{}" else
|
||||
"{${introSpace}${concatItems (map (value: "${toLua innerArgs value}") v)}${outroSpace}}")
|
||||
@@ -752,8 +754,6 @@ in rec {
|
||||
"(${v.expr})"
|
||||
else if v == { } then
|
||||
"{}"
|
||||
else if isDerivation v then
|
||||
''"${toString v}"''
|
||||
else
|
||||
"{${introSpace}${concatItems (
|
||||
mapAttrsToList (key: value: "[${toJSON key}] = ${toLua innerArgs value}") v
|
||||
|
||||
+5
-5
@@ -140,11 +140,6 @@ lib.mapAttrs mkLicense ({
|
||||
fullName = "Apache License 2.0";
|
||||
};
|
||||
|
||||
asl20-llvm = {
|
||||
spdxId = "Apache-2.0 WITH LLVM-exception";
|
||||
fullName = "Apache License 2.0 with LLVM Exceptions";
|
||||
};
|
||||
|
||||
bitstreamVera = {
|
||||
spdxId = "Bitstream-Vera";
|
||||
fullName = "Bitstream Vera Font License";
|
||||
@@ -859,6 +854,11 @@ lib.mapAttrs mkLicense ({
|
||||
url = "https://opensource.franz.com/preamble.html";
|
||||
};
|
||||
|
||||
llvm-exception = {
|
||||
spdxId = "LLVM-exception";
|
||||
fullName = "LLVM Exception"; # LLVM exceptions to the Apache 2.0 License
|
||||
};
|
||||
|
||||
lppl1 = {
|
||||
spdxId = "LPPL-1.0";
|
||||
fullName = "LaTeX Project Public License v1.0";
|
||||
|
||||
@@ -415,6 +415,38 @@ NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribu
|
||||
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.listOf.type.nestedTypes.elemType.functor.wrapped ./deprecated-wrapped.nix
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.mergedListOf.type.nestedTypes.elemType.functor.wrapped ./deprecated-wrapped.nix
|
||||
# unique / uniq
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.unique.type.functor.wrapped ./deprecated-wrapped.nix
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.mergedUnique.type.functor.wrapped ./deprecated-wrapped.nix
|
||||
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.unique.type.nestedTypes.elemType.functor.wrapped ./deprecated-wrapped.nix
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.mergedUnique.type.nestedTypes.elemType.functor.wrapped ./deprecated-wrapped.nix
|
||||
# nullOr
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.nullOr.type.functor.wrapped ./deprecated-wrapped.nix
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.mergedNullOr.type.functor.wrapped ./deprecated-wrapped.nix
|
||||
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.nullOr.type.nestedTypes.elemType.functor.wrapped ./deprecated-wrapped.nix
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.mergedNullOr.type.nestedTypes.elemType.functor.wrapped ./deprecated-wrapped.nix
|
||||
# functionTo
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.functionTo.type.functor.wrapped ./deprecated-wrapped.nix
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.mergedFunctionTo.type.functor.wrapped ./deprecated-wrapped.nix
|
||||
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.functionTo.type.nestedTypes.elemType.functor.wrapped ./deprecated-wrapped.nix
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.mergedFunctionTo.type.nestedTypes.elemType.functor.wrapped ./deprecated-wrapped.nix
|
||||
|
||||
# coercedTo
|
||||
# Note: test 'nestedTypes.finalType' and 'nestedTypes.coercedType'
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.coercedTo.type.functor.wrapped ./deprecated-wrapped.nix
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.coercedTo.type.nestedTypes.finalType.functor.wrapped ./deprecated-wrapped.nix
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.coercedTo.type.nestedTypes.coercedType.functor.wrapped ./deprecated-wrapped.nix
|
||||
# either
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.either.type.functor.wrapped ./deprecated-wrapped.nix
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.mergedEither.type.functor.wrapped ./deprecated-wrapped.nix
|
||||
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.either.type.nestedTypes.left.functor.wrapped ./deprecated-wrapped.nix
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.either.type.nestedTypes.right.functor.wrapped ./deprecated-wrapped.nix
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.mergedEither.type.nestedTypes.left.functor.wrapped ./deprecated-wrapped.nix
|
||||
NIX_ABORT_ON_WARN=1 checkConfigError 'The deprecated `.*functor.wrapped` attribute .*is accessed, use `.*nestedTypes.elemType` instead.' options.mergedEither.type.nestedTypes.right.functor.wrapped ./deprecated-wrapped.nix
|
||||
|
||||
# Even with multiple assignments, a type error should be thrown if any of them aren't valid
|
||||
checkConfigError 'A definition for option .* is not of type .*' \
|
||||
|
||||
@@ -6,6 +6,11 @@ let
|
||||
# attrsOf uses attrsWith internally
|
||||
attrsOf
|
||||
listOf
|
||||
unique
|
||||
nullOr
|
||||
functionTo
|
||||
coercedTo
|
||||
either
|
||||
;
|
||||
in
|
||||
{
|
||||
@@ -26,6 +31,38 @@ in
|
||||
options.mergedListOf = mkOption {
|
||||
type = listOf (listOf types.str);
|
||||
};
|
||||
# unique
|
||||
options.unique = mkOption {
|
||||
type = unique { message = ""; } (listOf types.str);
|
||||
};
|
||||
options.mergedUnique = mkOption {
|
||||
type = unique { message = ""; } (listOf types.str);
|
||||
};
|
||||
# nullOr
|
||||
options.nullOr = mkOption {
|
||||
type = nullOr (listOf types.str);
|
||||
};
|
||||
options.mergedNullOr = mkOption {
|
||||
type = nullOr (listOf types.str);
|
||||
};
|
||||
# functionTo
|
||||
options.functionTo = mkOption {
|
||||
type = functionTo (listOf types.str);
|
||||
};
|
||||
options.mergedFunctionTo = mkOption {
|
||||
type = functionTo (listOf types.str);
|
||||
};
|
||||
# coercedTo
|
||||
# Note: coercedTo is a non-mergeable option-type
|
||||
options.coercedTo = mkOption {
|
||||
type = coercedTo (listOf types.str) lib.id (listOf types.str);
|
||||
};
|
||||
options.either = mkOption {
|
||||
type = either (listOf types.str) (listOf types.str);
|
||||
};
|
||||
options.mergedEither = mkOption {
|
||||
type = either (listOf types.str) (listOf types.str);
|
||||
};
|
||||
}
|
||||
)
|
||||
# Module B
|
||||
@@ -38,6 +75,18 @@ in
|
||||
options.mergedListOf = mkOption {
|
||||
type = listOf (listOf types.str);
|
||||
};
|
||||
options.mergedUnique = mkOption {
|
||||
type = unique { message = ""; } (listOf types.str);
|
||||
};
|
||||
options.mergedNullOr = mkOption {
|
||||
type = nullOr (listOf types.str);
|
||||
};
|
||||
options.mergedFunctionTo = mkOption {
|
||||
type = functionTo (listOf types.str);
|
||||
};
|
||||
options.mergedEither = mkOption {
|
||||
type = either (listOf types.str) (listOf types.str);
|
||||
};
|
||||
}
|
||||
)
|
||||
];
|
||||
|
||||
+28
-10
@@ -75,6 +75,7 @@ let
|
||||
# Note that individual attributes can be overriden if needed.
|
||||
elemTypeFunctor = name: { elemType, ... }@payload: {
|
||||
inherit name payload;
|
||||
wrappedDeprecationMessage = makeWrappedDeprecationMessage payload;
|
||||
type = outer_types.types.${name};
|
||||
binOp = a: b:
|
||||
let
|
||||
@@ -85,10 +86,10 @@ let
|
||||
null
|
||||
else
|
||||
{ elemType = merged; };
|
||||
wrappedDeprecationMessage = { loc }: lib.warn ''
|
||||
The deprecated `${lib.optionalString (loc != null) "type."}functor.wrapped` attribute ${lib.optionalString (loc != null) "of the option `${showOption loc}` "}is accessed, use `${lib.optionalString (loc != null) "type."}nestedTypes.elemType` instead.
|
||||
'' payload.elemType;
|
||||
};
|
||||
makeWrappedDeprecationMessage = payload: { loc }: lib.warn ''
|
||||
The deprecated `${lib.optionalString (loc != null) "type."}functor.wrapped` attribute ${lib.optionalString (loc != null) "of the option `${showOption loc}` "}is accessed, use `${lib.optionalString (loc != null) "type."}nestedTypes.elemType` instead.
|
||||
'' payload.elemType;
|
||||
|
||||
|
||||
outer_types =
|
||||
@@ -834,6 +835,15 @@ rec {
|
||||
};
|
||||
};
|
||||
|
||||
# A value produced by `lib.mkLuaInline`
|
||||
luaInline = mkOptionType {
|
||||
name = "luaInline";
|
||||
description = "inline lua";
|
||||
descriptionClass = "noun";
|
||||
check = x: x._type or null == "lua-inline";
|
||||
merge = mergeEqualOption;
|
||||
};
|
||||
|
||||
uniq = unique { message = ""; };
|
||||
|
||||
unique = { message }: type: mkOptionType rec {
|
||||
@@ -844,7 +854,9 @@ rec {
|
||||
getSubOptions = type.getSubOptions;
|
||||
getSubModules = type.getSubModules;
|
||||
substSubModules = m: uniq (type.substSubModules m);
|
||||
functor = (defaultFunctor name) // { wrapped = type; };
|
||||
functor = elemTypeFunctor name { elemType = type; } // {
|
||||
type = payload: types.unique { inherit message; } payload.elemType;
|
||||
};
|
||||
nestedTypes.elemType = type;
|
||||
};
|
||||
|
||||
@@ -864,7 +876,9 @@ rec {
|
||||
getSubOptions = elemType.getSubOptions;
|
||||
getSubModules = elemType.getSubModules;
|
||||
substSubModules = m: nullOr (elemType.substSubModules m);
|
||||
functor = (defaultFunctor name) // { wrapped = elemType; };
|
||||
functor = (elemTypeFunctor name { inherit elemType; }) // {
|
||||
type = payload: types.nullOr payload.elemType;
|
||||
};
|
||||
nestedTypes.elemType = elemType;
|
||||
};
|
||||
|
||||
@@ -883,7 +897,9 @@ rec {
|
||||
getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "<function body>" ]);
|
||||
getSubModules = elemType.getSubModules;
|
||||
substSubModules = m: functionTo (elemType.substSubModules m);
|
||||
functor = (defaultFunctor "functionTo") // { wrapped = elemType; };
|
||||
functor = (elemTypeFunctor "functionTo" { inherit elemType; }) // {
|
||||
type = payload: types.functionTo payload.elemType;
|
||||
};
|
||||
nestedTypes.elemType = elemType;
|
||||
};
|
||||
|
||||
@@ -1118,13 +1134,13 @@ rec {
|
||||
then t2.merge loc defs
|
||||
else mergeOneOption loc defs;
|
||||
typeMerge = f':
|
||||
let mt1 = t1.typeMerge (elemAt f'.wrapped 0).functor;
|
||||
mt2 = t2.typeMerge (elemAt f'.wrapped 1).functor;
|
||||
let mt1 = t1.typeMerge (elemAt f'.payload.elemType 0).functor;
|
||||
mt2 = t2.typeMerge (elemAt f'.payload.elemType 1).functor;
|
||||
in
|
||||
if (name == f'.name) && (mt1 != null) && (mt2 != null)
|
||||
then functor.type mt1 mt2
|
||||
else null;
|
||||
functor = (defaultFunctor name) // { wrapped = [ t1 t2 ]; };
|
||||
functor = elemTypeFunctor name { elemType = [ t1 t2 ]; };
|
||||
nestedTypes.left = t1;
|
||||
nestedTypes.right = t2;
|
||||
};
|
||||
@@ -1157,7 +1173,9 @@ rec {
|
||||
getSubModules = finalType.getSubModules;
|
||||
substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m);
|
||||
typeMerge = t: null;
|
||||
functor = (defaultFunctor name) // { wrapped = finalType; };
|
||||
functor = (defaultFunctor name) // {
|
||||
wrappedDeprecationMessage = makeWrappedDeprecationMessage { elemType = finalType; };
|
||||
};
|
||||
nestedTypes.coercedType = coercedType;
|
||||
nestedTypes.finalType = finalType;
|
||||
};
|
||||
|
||||
@@ -579,6 +579,17 @@
|
||||
githubId = 50264672;
|
||||
name = "Adam Freeth";
|
||||
};
|
||||
adamperkowski = {
|
||||
name = "Adam Perkowski";
|
||||
email = "adas1per@protonmail.com";
|
||||
matrix = "@xx0a_q:matrix.org";
|
||||
github = "adamperkowski";
|
||||
githubId = 75480869;
|
||||
keys = [
|
||||
{ fingerprint = "00F6 1623 FB56 BC5B B709 4E63 4CE6 C117 2DF6 BE79"; }
|
||||
{ fingerprint = "5A53 0832 DA91 20B0 CA57 DDB6 7CBD B58E CF1D 3478"; }
|
||||
];
|
||||
};
|
||||
adamt = {
|
||||
email = "mail@adamtulinius.dk";
|
||||
github = "adamtulinius";
|
||||
@@ -1081,6 +1092,12 @@
|
||||
githubId = 45104896;
|
||||
name = "Alexandru Nechita";
|
||||
};
|
||||
alexandrutocar = {
|
||||
email = "at@myquiet.place";
|
||||
github = "alexandrutocar";
|
||||
githubId = 65486851;
|
||||
name = "Alexandru Tocar";
|
||||
};
|
||||
alexarice = {
|
||||
email = "alexrice999@hotmail.co.uk";
|
||||
github = "alexarice";
|
||||
@@ -1185,6 +1202,18 @@
|
||||
githubId = 30437811;
|
||||
name = "Alex Andrews";
|
||||
};
|
||||
alikindsys = {
|
||||
email = "alice@blocovermelho.org";
|
||||
github = "alikindsys";
|
||||
githubId = 36565196;
|
||||
name = "Alikind System";
|
||||
|
||||
keys = [
|
||||
{
|
||||
fingerprint = "7D31 15DC D912 C15A 2781 F7BB 511C B44B C752 2A89";
|
||||
}
|
||||
];
|
||||
};
|
||||
alirezameskin = {
|
||||
email = "alireza.meskin@gmail.com";
|
||||
github = "alirezameskin";
|
||||
@@ -6387,7 +6416,7 @@
|
||||
name = "Sebastian Krohn";
|
||||
};
|
||||
drawbu = {
|
||||
email = "clement2104.boillot@gmail.com";
|
||||
email = "nixpkgs@drawbu.dev";
|
||||
github = "drawbu";
|
||||
githubId = 69208565;
|
||||
name = "Clément Boillot";
|
||||
@@ -9103,6 +9132,12 @@
|
||||
githubId = 39066502;
|
||||
name = "Guekka";
|
||||
};
|
||||
guelakais = {
|
||||
email = "koroyeldiores@gmail.com";
|
||||
github = "Guelakais";
|
||||
githubId = 76840985;
|
||||
name = "GueLaKais";
|
||||
};
|
||||
guibert = {
|
||||
email = "david.guibert@gmail.com";
|
||||
github = "dguibert";
|
||||
@@ -9176,6 +9211,11 @@
|
||||
github = "gytis-ivaskevicius";
|
||||
githubId = 23264966;
|
||||
};
|
||||
GZGavinZhao = {
|
||||
name = "Gavin Zhao";
|
||||
github = "GZGavinZhao";
|
||||
githubId = 74938940;
|
||||
};
|
||||
h3cth0r = {
|
||||
name = "Hector Miranda";
|
||||
email = "hector.miranda@tec.mx";
|
||||
@@ -10514,6 +10554,12 @@
|
||||
githubId = 94313;
|
||||
name = "Xianyi Lin";
|
||||
};
|
||||
izelnakri = {
|
||||
email = "contact@izelnakri.com";
|
||||
github = "izelnakri";
|
||||
githubId = 1190931;
|
||||
name = "Izel Nakri";
|
||||
};
|
||||
izorkin = {
|
||||
email = "Izorkin@gmail.com";
|
||||
github = "Izorkin";
|
||||
@@ -11415,6 +11461,13 @@
|
||||
githubId = 22916782;
|
||||
name = "Joan Massachs";
|
||||
};
|
||||
joaomoreira = {
|
||||
matrix = "@joaomoreira:matrix.org";
|
||||
github = "joaoymoreira";
|
||||
githubId = 151087767;
|
||||
name = "João Moreira";
|
||||
keys = [ { fingerprint = "F457 0A3A 5F89 22F8 F572 E075 EF8B F2C8 C5F4 097D"; } ];
|
||||
};
|
||||
joaquintrinanes = {
|
||||
email = "hi@joaquint.io";
|
||||
github = "JoaquinTrinanes";
|
||||
@@ -13647,6 +13700,13 @@
|
||||
githubId = 10554636;
|
||||
name = "Braian A. Diez";
|
||||
};
|
||||
linuxwhata = {
|
||||
email = "linuxwhata@qq.com";
|
||||
matrix = "@lwa:envs.net";
|
||||
github = "linuxwhata";
|
||||
githubId = 68576488;
|
||||
name = "Zhou Ke";
|
||||
};
|
||||
lionello = {
|
||||
email = "lio@lunesu.com";
|
||||
github = "lionello";
|
||||
@@ -14202,6 +14262,17 @@
|
||||
githubId = 7910815;
|
||||
name = "Alex McGrath";
|
||||
};
|
||||
lykos153 = {
|
||||
email = "silvio.ankermann@cloudandheat.com";
|
||||
github = "Lykos153";
|
||||
githubId = 6453662;
|
||||
name = "Silvio Ankermann";
|
||||
keys = [
|
||||
{
|
||||
fingerprint = "8D47 6294 7205 541C 62A4 9C88 F422 6CA3 971C 4E97";
|
||||
}
|
||||
];
|
||||
};
|
||||
lyn = {
|
||||
name = "Lyn";
|
||||
matrix = "@lynatic:catgirl.cloud";
|
||||
@@ -15098,6 +15169,13 @@
|
||||
githubId = 158568;
|
||||
name = "Matthias C. M. Troffaes";
|
||||
};
|
||||
mcparland = {
|
||||
email = "john@mcpar.land";
|
||||
github = "mcpar-land";
|
||||
githubId = 55669980;
|
||||
name = "John McParland";
|
||||
keys = [ { fingerprint = "39D2 171D D733 C718 DD21 285E B326 E14B 05D8 7A4E"; } ];
|
||||
};
|
||||
McSinyx = {
|
||||
email = "cnx@loang.net";
|
||||
github = "McSinyx";
|
||||
@@ -18610,6 +18688,12 @@
|
||||
githubId = 88469;
|
||||
name = "Jaime Breva";
|
||||
};
|
||||
phodina = {
|
||||
email = "phodina@protonmail.com";
|
||||
github = "phodina";
|
||||
githubId = 2997905;
|
||||
name = "Petr Hodina";
|
||||
};
|
||||
photex = {
|
||||
email = "photex@gmail.com";
|
||||
github = "photex";
|
||||
@@ -20651,6 +20735,13 @@
|
||||
keys = [ { fingerprint = "7DCA 5615 8AB2 621F 2F32 9FF4 1C7C E491 479F A273"; } ];
|
||||
name = "Rahul Butani";
|
||||
};
|
||||
rseichter = {
|
||||
email = "nixos.org@seichter.de";
|
||||
github = "rseichter";
|
||||
githubId = 30873939;
|
||||
keys = [ { fingerprint = "6AE2 A847 23D5 6D98 5B34 0BC0 8E5F A470 9F69 E911"; } ];
|
||||
name = "Ralph Seichter";
|
||||
};
|
||||
rski = {
|
||||
name = "rski";
|
||||
email = "rom.skiad+nix@gmail.com";
|
||||
@@ -20927,6 +21018,13 @@
|
||||
githubId = 7309170;
|
||||
name = "Ryota Kameoka";
|
||||
};
|
||||
rypervenche = {
|
||||
email = "git@ryper.org";
|
||||
github = "rypervenche";
|
||||
githubId = 1411504;
|
||||
name = "rypervenche";
|
||||
keys = [ { fingerprint = "1198 7A9F 03AE 47F0 4919 E334 6A41 2C4A ECE1 66EF"; } ];
|
||||
};
|
||||
rytone = {
|
||||
email = "max@ryt.one";
|
||||
github = "rastertail";
|
||||
@@ -21399,6 +21497,11 @@
|
||||
githubId = 19472270;
|
||||
name = "Sebastian";
|
||||
};
|
||||
sebaguardian = {
|
||||
name = "Sebaguardian";
|
||||
github = "Sebaguardian";
|
||||
githubId = 68247013;
|
||||
};
|
||||
sebastianblunt = {
|
||||
name = "Sebastian Blunt";
|
||||
email = "nix@sebastianblunt.com";
|
||||
@@ -21532,6 +21635,12 @@
|
||||
githubId = 529649;
|
||||
name = "Raffael Mancini";
|
||||
};
|
||||
sepiabrown = {
|
||||
email = "bboxone@gmail.com";
|
||||
github = "sepiabrown";
|
||||
githubId = 35622998;
|
||||
name = "Suwon Park";
|
||||
};
|
||||
seppeljordan = {
|
||||
email = "sebastian.jordan.mail@googlemail.com";
|
||||
github = "seppeljordan";
|
||||
@@ -21986,6 +22095,11 @@
|
||||
githubId = 91412114;
|
||||
keys = [ { fingerprint = "C1DA A551 B422 7A6F 3FD9 6B3A 467B 7D12 9EA7 3AC9"; } ];
|
||||
};
|
||||
silvanshade = {
|
||||
github = "silvanshade";
|
||||
githubId = 11022302;
|
||||
name = "silvanshade";
|
||||
};
|
||||
Silver-Golden = {
|
||||
name = "Brendan Golden";
|
||||
email = "github+nixpkgs@brendan.ie";
|
||||
@@ -22510,6 +22624,13 @@
|
||||
githubId = 47164123;
|
||||
name = "Spoonbaker";
|
||||
};
|
||||
sportshead = {
|
||||
email = "me@sportshead.dev";
|
||||
github = "sportshead";
|
||||
githubId = 32637656;
|
||||
name = "sportshead";
|
||||
keys = [ { fingerprint = "A6B6 D031 782E BDF7 631A 8E7E A874 DB2C BFD3 CFD0"; } ];
|
||||
};
|
||||
sprock = {
|
||||
email = "rmason@mun.ca";
|
||||
github = "sprock";
|
||||
|
||||
@@ -1018,8 +1018,9 @@ with lib.maintainers;
|
||||
|
||||
rocm = {
|
||||
members = [
|
||||
Madouura
|
||||
Flakebi
|
||||
GZGavinZhao
|
||||
LunNova
|
||||
mschwaig
|
||||
];
|
||||
githubTeams = [ "rocm-maintainers" ];
|
||||
|
||||
@@ -232,6 +232,13 @@ merging is handled.
|
||||
definitions cannot be merged. The regular expression is processed
|
||||
using `builtins.match`.
|
||||
|
||||
### Specialised types {#sec-option-types-specialised}
|
||||
|
||||
`types.luaInline`
|
||||
|
||||
: A string wrapped using `lib.mkLuaInline`. Allows embedding lua expressions
|
||||
inline within generated lua. Multiple definitions cannot be merged.
|
||||
|
||||
## Submodule types {#sec-option-types-submodule}
|
||||
|
||||
Submodules are detailed in [Submodule](#section-option-types-submodule).
|
||||
|
||||
@@ -402,6 +402,31 @@ have a predefined type and string generator already declared under
|
||||
: Outputs the given attribute set as an Elixir map, instead of the
|
||||
default Elixir keyword list
|
||||
|
||||
`pkgs.formats.lua { asBindings ? false, multiline ? true, columnWidth ? 100, indentWidth ? 2, indentUsingTabs ? false }`
|
||||
|
||||
: A function taking an attribute set with values
|
||||
|
||||
`asBindings` (default `false`)
|
||||
|
||||
: Whether to treat attributes as variable bindings
|
||||
|
||||
`multiline` (default `true`)
|
||||
|
||||
: Whether to procude a multiline output. The output may still wrap across
|
||||
multiple lines if it would otherwise exceed `columnWidth`.
|
||||
|
||||
`columnWidth` (default `100`)
|
||||
|
||||
: The column width to use to attempt to wrap lines.
|
||||
|
||||
`indentWidth` (default `2`)
|
||||
|
||||
: The width of a single indentation level.
|
||||
|
||||
`indentUsingTabs` (default `false`)
|
||||
|
||||
: Whether the indentation should use tabs instead of spaces.
|
||||
|
||||
`pkgs.formats.php { finalVariable }` []{#pkgs-formats-php}
|
||||
|
||||
: A function taking an attribute set with values
|
||||
|
||||
@@ -1592,6 +1592,9 @@
|
||||
"sec-option-types-string": [
|
||||
"index.html#sec-option-types-string"
|
||||
],
|
||||
"sec-option-types-specialised": [
|
||||
"index.html#sec-option-types-specialised"
|
||||
],
|
||||
"sec-option-types-submodule": [
|
||||
"index.html#sec-option-types-submodule"
|
||||
],
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
- The `nixos-generate-config` command now supports a optional `--flake` option, which will generate a flake.nix file alongside the `configuration.nix` and `hardware-configuration.nix`, providing an easy instroduction into flake-based system configurations.
|
||||
|
||||
- A `nixos-rebuild build-image` sub-command has been added.
|
||||
It allows users to build platform-specific (disk) images from their NixOS configurations. `nixos-rebuild build-image` works similar to the popular [nix-community/nixos-generators](https://github.com/nix-community/nixos-generators) project. See new [section on image building in the nixpkgs manual](https://nixos.org/manual/nixpkgs/unstable/#sec-image-nixos-rebuild-build-image). It is also available for `nixos-rebuild-ng`.
|
||||
It allows users to build platform-specific (disk) images from their NixOS configurations. `nixos-rebuild build-image` works similar to the popular [nix-community/nixos-generators](https://github.com/nix-community/nixos-generators) project. See new [section on image building in the NixOS manual](https://nixos.org/manual/nixos/unstable/#sec-image-nixos-rebuild-build-image). It is also available for `nixos-rebuild-ng`.
|
||||
|
||||
- `nixos-option` has been rewritten to a Nix expression called by a simple bash script. This lowers our maintenance threshold, makes eval errors less verbose, adds support for flake-based configurations, descending into `attrsOf` and `listOf` submodule options, and `--show-trace`.
|
||||
|
||||
@@ -108,12 +108,16 @@
|
||||
|
||||
- [Schroot](https://codeberg.org/shelter/reschroot), a lightweight virtualisation tool. Securely enter a chroot and run a command or login shell. Available as [programs.schroot](#opt-programs.schroot.enable).
|
||||
|
||||
- [Firezone](https://firezone.dev), an enterprise-ready zero-trust access platform built on WireGuard. This includes the server stack as [services.firezone.server.enable](#opt-services.firezone.server.enable), a TURN/STUN relay service as [services.firezone.relay.enable](#opt-services.firezone.relay.enable), a gateway service as [services.firezone.gateway.enable](#opt-services.firezone.gateway.enable), a headless client as [services.firezone.headless-client.enable](#opt-services.firezone.headless-client.enable) and a GUI client as [services.firezone.gui-client.enable](#opt-services.firezone.gui-client.enable).
|
||||
|
||||
- [crab-hole](https://github.com/LuckyTurtleDev/crab-hole), a cross platform Pi-hole clone written in Rust using hickory-dns/trust-dns. Available as [services.crab-hole](#opt-services.crab-hole.enable).
|
||||
|
||||
- [zwave-js-ui](https://zwave-js.github.io/zwave-js-ui/), a full featured Z-Wave Control Panel and MQTT Gateway. Available as [services.zwave-js-ui](#opt-services.zwave-js-ui.enable).
|
||||
|
||||
- [Amazon CloudWatch Agent](https://github.com/aws/amazon-cloudwatch-agent), the official telemetry collector for AWS CloudWatch and AWS X-Ray. Available as [services.amazon-cloudwatch-agent](options.html#opt-services.amazon-cloudwatch-agent.enable).
|
||||
|
||||
- [Fluent Bit](https://github.com/fluent/fluent-bit), a fast Log, Metrics and Traces Processor and Forwarder. Available as [services.fluent-bit](#opt-services.fluent-bit.enable).
|
||||
|
||||
- [Bat](https://github.com/sharkdp/bat), a {manpage}`cat(1)` clone with wings. Available as [programs.bat](options.html#opt-programs.bat).
|
||||
|
||||
- [Autotier](https://github.com/45Drives/autotier), a passthrough FUSE filesystem. Available as [services.autotierfs](options.html#opt-services.autotierfs.enable).
|
||||
@@ -214,6 +218,8 @@
|
||||
|
||||
- `python3Packages.beancount` was updated to 3.1.0. Previous major version remains available as `python3Packages.beancount_2`.
|
||||
|
||||
- `wastebin` has been updated to 3.0.0. See the [Changelog](https://github.com/matze/wastebin/blob/master/CHANGELOG.md#300) for breaking changes to the configuration.
|
||||
|
||||
- `binwalk` was updated to 3.1.0, which has been rewritten in rust. The python module is no longer available.
|
||||
See the release notes of [3.1.0](https://github.com/ReFirmLabs/binwalk/releases/tag/v3.1.0) for more information.
|
||||
|
||||
@@ -225,6 +231,11 @@
|
||||
|
||||
- `pkgs.nextcloud28` has been removed since it's out of support upstream.
|
||||
|
||||
- `services.cloudflare-dyndns.apiTokenFile` now must be just your Cloudflare api token. Previously it was supposed to be a file of the form `CLOUDFLARE_API_TOKEN=...`.
|
||||
|
||||
- Emacs lisp build helpers, such as `emacs.pkgs.melpaBuild`, now enables `__structuredAttrs` by default.
|
||||
Environment variables have to be passed via the `env` attribute.
|
||||
|
||||
- `buildGoModule` now passes environment variables via the `env` attribute. `CGO_ENABLED` should now be specified with `env.CGO_ENABLED` when passing to buildGoModule. Direct specification of `CGO_ENABLED` is now redirected by a compatibility layer with a warning, but will become an error in future releases.
|
||||
|
||||
Go-related environment variables previously shadowed by `buildGoModule` now results in errors when specified directly. Such variables include `GOOS` and `GOARCH`.
|
||||
@@ -603,6 +614,8 @@
|
||||
|
||||
- `services.soft-serve` now restarts upon config change.
|
||||
|
||||
- `services.keycloak` now provides a `realmFiles` option that allows to import realms during startup. See https://www.keycloak.org/server/importExport
|
||||
|
||||
- `bind.cacheNetworks` now only controls access for recursive queries, where it previously controlled access for all queries.
|
||||
|
||||
- [`services.mongodb.enableAuth`](#opt-services.mongodb.enableAuth) now uses the newer [mongosh](https://github.com/mongodb-js/mongosh) shell instead of the legacy shell to configure the initial superuser. You can configure the mongosh package to use through the [`services.mongodb.mongoshPackage`](#opt-services.mongodb.mongoshPackage) option.
|
||||
|
||||
@@ -58,7 +58,7 @@ let
|
||||
<fontconfig>
|
||||
<!-- Font directories -->
|
||||
${lib.concatStringsSep "\n" (map (font: "<dir>${font}</dir>") config.fonts.packages)}
|
||||
${lib.optionalString (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) ''
|
||||
${lib.optionalString (pkgs.stdenv.hostPlatform.emulatorAvailable pkgs.buildPackages) ''
|
||||
<!-- Pre-generated font caches -->
|
||||
<cachedir>${cache}</cachedir>
|
||||
${lib.optionalString (pkgs.stdenv.hostPlatform.isx86_64 && cfg.cache32Bit) ''
|
||||
|
||||
@@ -376,4 +376,4 @@ foreach my $u (values %usersOut) {
|
||||
|
||||
updateFile("/etc/subuid", join("\n", @subUids) . "\n");
|
||||
updateFile("/etc/subgid", join("\n", @subGids) . "\n");
|
||||
updateFile($subUidMapFile, encode_json($subUidMap) . "\n");
|
||||
updateFile($subUidMapFile, to_json($subUidMap) . "\n");
|
||||
|
||||
@@ -951,6 +951,21 @@ in {
|
||||
}
|
||||
] ++ flatten (flip mapAttrsToList cfg.users (name: user:
|
||||
[
|
||||
(
|
||||
let
|
||||
# Things fail in various ways with especially non-ascii usernames.
|
||||
# This regex mirrors the one from shadow's is_valid_name:
|
||||
# https://github.com/shadow-maint/shadow/blob/bee77ffc291dfed2a133496db465eaa55e2b0fec/lib/chkname.c#L68
|
||||
# though without the trailing $, because Samba 3 got its last release
|
||||
# over 10 years ago and is not in Nixpkgs anymore,
|
||||
# while later versions don't appear to require anything like that.
|
||||
nameRegex = "[a-zA-Z0-9_.][a-zA-Z0-9_.-]*";
|
||||
in
|
||||
{
|
||||
assertion = builtins.match nameRegex user.name != null;
|
||||
message = "The username \"${user.name}\" is not valid, it does not match the regex \"${nameRegex}\".";
|
||||
}
|
||||
)
|
||||
{
|
||||
assertion = (user.hashedPassword != null)
|
||||
-> (match ".*:.*" user.hashedPassword == null);
|
||||
|
||||
@@ -47,7 +47,10 @@ let
|
||||
let
|
||||
module = ../. + "/installer/sd-card/sd-image-${pkgs.targetPlatform.qemuArch}.nix";
|
||||
in
|
||||
if builtins.pathExists module then [ module ] else throw "The module ${module} does not exist.";
|
||||
if builtins.pathExists module then
|
||||
[ module ]
|
||||
else
|
||||
throw "The module ${toString module} does not exist.";
|
||||
};
|
||||
kexec = ../installer/netboot/netboot-minimal.nix;
|
||||
};
|
||||
|
||||
@@ -560,6 +560,7 @@
|
||||
./services/desktops/system76-scheduler.nix
|
||||
./services/desktops/telepathy.nix
|
||||
./services/desktops/tumbler.nix
|
||||
./services/desktops/wlock.nix
|
||||
./services/desktops/zeitgeist.nix
|
||||
./services/development/athens.nix
|
||||
./services/development/blackfire.nix
|
||||
@@ -934,6 +935,7 @@
|
||||
./services/monitoring/das_watchdog.nix
|
||||
./services/monitoring/datadog-agent.nix
|
||||
./services/monitoring/do-agent.nix
|
||||
./services/monitoring/fluent-bit.nix
|
||||
./services/monitoring/fusion-inventory.nix
|
||||
./services/monitoring/gatus.nix
|
||||
./services/monitoring/gitwatch.nix
|
||||
@@ -1101,6 +1103,11 @@
|
||||
./services/networking/firewall.nix
|
||||
./services/networking/firewall-iptables.nix
|
||||
./services/networking/firewall-nftables.nix
|
||||
./services/networking/firezone/gateway.nix
|
||||
./services/networking/firezone/gui-client.nix
|
||||
./services/networking/firezone/headless-client.nix
|
||||
./services/networking/firezone/relay.nix
|
||||
./services/networking/firezone/server.nix
|
||||
./services/networking/flannel.nix
|
||||
./services/networking/freenet.nix
|
||||
./services/networking/freeradius.nix
|
||||
@@ -1364,6 +1371,7 @@
|
||||
./services/scheduling/atd.nix
|
||||
./services/scheduling/cron.nix
|
||||
./services/scheduling/fcron.nix
|
||||
./services/scheduling/prefect.nix
|
||||
./services/scheduling/scx.nix
|
||||
./services/search/elasticsearch-curator.nix
|
||||
./services/search/elasticsearch.nix
|
||||
|
||||
@@ -102,10 +102,10 @@ with lib;
|
||||
stdenv
|
||||
stdenvNoCC # for runCommand
|
||||
busybox
|
||||
jq # for closureInfo
|
||||
# For boot.initrd.systemd
|
||||
makeInitrdNGTool
|
||||
];
|
||||
]
|
||||
++ jq.all; # for closureInfo
|
||||
|
||||
boot.swraid.enable = true;
|
||||
# remove warning about unset mail
|
||||
|
||||
@@ -118,8 +118,15 @@ in
|
||||
Some of these might be able to be configured more ergonomically
|
||||
using policies.
|
||||
|
||||
See [here](https://mozilla.github.io/policy-templates/#preferences) for allowed preferences.
|
||||
|
||||
${organisationInfo}
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
"browser.tabs.tabmanager.enabled" = false;
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
preferencesStatus = lib.mkOption {
|
||||
|
||||
@@ -10,7 +10,14 @@ in
|
||||
{
|
||||
options.programs.streamcontroller = {
|
||||
enable = lib.mkEnableOption "StreamController";
|
||||
package = lib.mkPackageOption pkgs "streamcontroller" { default = [ "streamcontroller" ]; };
|
||||
package = lib.mkOption {
|
||||
default = pkgs.streamcontroller.override { isKde = config.services.desktopManager.plasma6.enable; };
|
||||
defaultText = lib.literalExpression "pkgs.streamcontroller";
|
||||
type = lib.types.package;
|
||||
description = ''
|
||||
The StreamController package to use
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
||||
@@ -79,8 +79,7 @@ in
|
||||
description = ''
|
||||
The package which contains the `yabar` binary.
|
||||
|
||||
Nixpkgs provides the `yabar` and `yabar-unstable`,
|
||||
so it's possible to choose.
|
||||
Nixpkgs offers both a stable (`yabar`) and unstable (`yabar-unstable`) version of Yabar.
|
||||
'';
|
||||
};
|
||||
|
||||
|
||||
@@ -202,8 +202,9 @@ in
|
||||
|
||||
systemd.user.services =
|
||||
let
|
||||
lomiriService = "lomiri.service";
|
||||
lomiriServiceNames = [
|
||||
"lomiri.service"
|
||||
lomiriService
|
||||
"lomiri-full-greeter.service"
|
||||
"lomiri-full-shell.service"
|
||||
"lomiri-greeter.service"
|
||||
@@ -225,9 +226,9 @@ in
|
||||
|
||||
"lomiri-polkit-agent" = {
|
||||
description = "Lomiri Polkit agent";
|
||||
wantedBy = lomiriServiceNames;
|
||||
after = [ "graphical-session.target" ];
|
||||
partOf = lomiriServiceNames;
|
||||
wantedBy = [ lomiriService ];
|
||||
after = [ lomiriService ];
|
||||
partOf = [ lomiriService ];
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
Restart = "always";
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.services.wlock;
|
||||
in
|
||||
{
|
||||
options = {
|
||||
services.wlock = {
|
||||
enable = lib.mkEnableOption "wlock, a Wayland sessionlocker using the ext-session-lock-v1 protocol";
|
||||
|
||||
package = lib.mkPackageOption pkgs "wlock" { };
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
security.wrappers.wlock = {
|
||||
owner = "root";
|
||||
group = "root";
|
||||
# mirror upstream chmod of 4755
|
||||
setuid = true;
|
||||
setgid = false;
|
||||
source = lib.getExe cfg.package;
|
||||
};
|
||||
};
|
||||
|
||||
meta.maintainers = [ lib.maintainers.fliegendewurst ];
|
||||
}
|
||||
@@ -9,7 +9,8 @@ let
|
||||
registrationFile = "${dataDir}/telegram-registration.yaml";
|
||||
cfg = config.services.mautrix-telegram;
|
||||
settingsFormat = pkgs.formats.json { };
|
||||
settingsFile = settingsFormat.generate "mautrix-telegram-config.json" cfg.settings;
|
||||
settingsFileUnsubstituted = settingsFormat.generate "mautrix-telegram-config.json" cfg.settings;
|
||||
settingsFile = "${dataDir}/config.json";
|
||||
|
||||
in
|
||||
{
|
||||
@@ -132,10 +133,37 @@ in
|
||||
List of Systemd services to require and wait for when starting the application service.
|
||||
'';
|
||||
};
|
||||
|
||||
registerToSynapse = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = config.services.matrix-synapse.enable;
|
||||
defaultText = lib.literalExpression "config.services.matrix-synapse.enable";
|
||||
description = ''
|
||||
Whether to add the bridge's app service registration file to
|
||||
`services.matrix-synapse.settings.app_service_config_files`.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
||||
users.users.mautrix-telegram = {
|
||||
isSystemUser = true;
|
||||
group = "mautrix-telegram";
|
||||
home = dataDir;
|
||||
description = "Mautrix-Telegram bridge user";
|
||||
};
|
||||
|
||||
users.groups.mautrix-telegram = { };
|
||||
|
||||
services.matrix-synapse = lib.mkIf cfg.registerToSynapse {
|
||||
settings.app_service_config_files = [ registrationFile ];
|
||||
};
|
||||
systemd.services.matrix-synapse = lib.mkIf cfg.registerToSynapse {
|
||||
serviceConfig.SupplementaryGroups = [ "mautrix-telegram" ];
|
||||
};
|
||||
|
||||
systemd.services.mautrix-telegram = {
|
||||
description = "Mautrix-Telegram, a Matrix-Telegram hybrid puppeting/relaybot bridge.";
|
||||
|
||||
@@ -161,6 +189,16 @@ in
|
||||
|
||||
preStart =
|
||||
''
|
||||
# substitute the settings file by environment variables
|
||||
# in this case read from EnvironmentFile
|
||||
test -f '${settingsFile}' && rm -f '${settingsFile}'
|
||||
old_umask=$(umask)
|
||||
umask 0177
|
||||
${pkgs.envsubst}/bin/envsubst \
|
||||
-o '${settingsFile}' \
|
||||
-i '${settingsFileUnsubstituted}'
|
||||
umask $old_umask
|
||||
|
||||
# generate the appservice's registration file if absent
|
||||
if [ ! -f '${registrationFile}' ]; then
|
||||
${pkgs.mautrix-telegram}/bin/mautrix-telegram \
|
||||
@@ -168,6 +206,19 @@ in
|
||||
--config='${settingsFile}' \
|
||||
--registration='${registrationFile}'
|
||||
fi
|
||||
|
||||
old_umask=$(umask)
|
||||
umask 0177
|
||||
# 1. Overwrite registration tokens in config
|
||||
# is set, set it as the login shared secret value for the configured
|
||||
# homeserver domain.
|
||||
${pkgs.yq}/bin/yq -s '.[0].appservice.as_token = .[1].as_token
|
||||
| .[0].appservice.hs_token = .[1].hs_token
|
||||
| .[0]' \
|
||||
'${settingsFile}' '${registrationFile}' > '${settingsFile}.tmp'
|
||||
mv '${settingsFile}.tmp' '${settingsFile}'
|
||||
|
||||
umask $old_umask
|
||||
''
|
||||
+ lib.optionalString (pkgs.mautrix-telegram ? alembic) ''
|
||||
# run automatic database init and migration scripts
|
||||
@@ -175,6 +226,8 @@ in
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
User = "mautrix-telegram";
|
||||
Group = "mautrix-telegram";
|
||||
Type = "simple";
|
||||
Restart = "always";
|
||||
|
||||
@@ -184,7 +237,6 @@ in
|
||||
ProtectKernelModules = true;
|
||||
ProtectControlGroups = true;
|
||||
|
||||
DynamicUser = true;
|
||||
PrivateTmp = true;
|
||||
WorkingDirectory = pkgs.mautrix-telegram; # necessary for the database migration scripts to be found
|
||||
StateDirectory = baseNameOf dataDir;
|
||||
|
||||
@@ -192,80 +192,54 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
config =
|
||||
let
|
||||
# If homepage-dashboard is enabled, but none of the configuration values have been updated,
|
||||
# then default to "unmanaged" configuration which is manually updated in
|
||||
# var/lib/homepage-dashboard. This is to maintain backwards compatibility, and should be
|
||||
# deprecated in a future release.
|
||||
managedConfig =
|
||||
!(
|
||||
cfg.bookmarks == [ ]
|
||||
&& cfg.customCSS == ""
|
||||
&& cfg.customJS == ""
|
||||
&& cfg.docker == { }
|
||||
&& cfg.kubernetes == { }
|
||||
&& cfg.services == [ ]
|
||||
&& cfg.settings == { }
|
||||
&& cfg.widgets == [ ]
|
||||
);
|
||||
|
||||
configDir = if managedConfig then "/etc/homepage-dashboard" else "/var/lib/homepage-dashboard";
|
||||
|
||||
msg =
|
||||
"using unmanaged configuration for homepage-dashboard is deprecated and will be removed"
|
||||
+ " in 24.05. please see the NixOS documentation for `services.homepage-dashboard' and add"
|
||||
+ " your bookmarks, services, widgets, and other configuration using the options provided.";
|
||||
in
|
||||
lib.mkIf cfg.enable {
|
||||
warnings = lib.optional (!managedConfig) msg;
|
||||
|
||||
environment.etc = lib.mkIf managedConfig {
|
||||
"homepage-dashboard/custom.css".text = cfg.customCSS;
|
||||
"homepage-dashboard/custom.js".text = cfg.customJS;
|
||||
|
||||
"homepage-dashboard/bookmarks.yaml".source = settingsFormat.generate "bookmarks.yaml" cfg.bookmarks;
|
||||
"homepage-dashboard/docker.yaml".source = settingsFormat.generate "docker.yaml" cfg.docker;
|
||||
"homepage-dashboard/kubernetes.yaml".source =
|
||||
settingsFormat.generate "kubernetes.yaml" cfg.kubernetes;
|
||||
"homepage-dashboard/services.yaml".source = settingsFormat.generate "services.yaml" cfg.services;
|
||||
"homepage-dashboard/settings.yaml".source = settingsFormat.generate "settings.yaml" cfg.settings;
|
||||
"homepage-dashboard/widgets.yaml".source = settingsFormat.generate "widgets.yaml" cfg.widgets;
|
||||
};
|
||||
|
||||
systemd.services.homepage-dashboard = {
|
||||
description = "Homepage Dashboard";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
environment = {
|
||||
HOMEPAGE_CONFIG_DIR = configDir;
|
||||
NIXPKGS_HOMEPAGE_CACHE_DIR = "/var/cache/homepage-dashboard";
|
||||
PORT = toString cfg.listenPort;
|
||||
LOG_TARGETS = lib.mkIf managedConfig "stdout";
|
||||
};
|
||||
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
DynamicUser = true;
|
||||
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
|
||||
StateDirectory = lib.mkIf (!managedConfig) "homepage-dashboard";
|
||||
CacheDirectory = "homepage-dashboard";
|
||||
ExecStart = lib.getExe cfg.package;
|
||||
Restart = "on-failure";
|
||||
};
|
||||
|
||||
preStart = ''
|
||||
# Related:
|
||||
# * https://github.com/NixOS/nixpkgs/issues/346016 ("homepage-dashboard: cache dir is not cleared upon version upgrade")
|
||||
# * https://github.com/gethomepage/homepage/discussions/4560 ("homepage NixOS package does not clear cache on upgrade leaving broken state")
|
||||
# * https://github.com/vercel/next.js/discussions/58864 ("Feature Request: Allow configuration of cache dir")
|
||||
rm -rf "$NIXPKGS_HOMEPAGE_CACHE_DIR"/*
|
||||
'';
|
||||
};
|
||||
|
||||
networking.firewall = lib.mkIf cfg.openFirewall {
|
||||
allowedTCPPorts = [ cfg.listenPort ];
|
||||
};
|
||||
config = lib.mkIf cfg.enable {
|
||||
environment.etc = {
|
||||
"homepage-dashboard/custom.css".text = cfg.customCSS;
|
||||
"homepage-dashboard/custom.js".text = cfg.customJS;
|
||||
"homepage-dashboard/bookmarks.yaml".source = settingsFormat.generate "bookmarks.yaml" cfg.bookmarks;
|
||||
"homepage-dashboard/docker.yaml".source = settingsFormat.generate "docker.yaml" cfg.docker;
|
||||
"homepage-dashboard/kubernetes.yaml".source =
|
||||
settingsFormat.generate "kubernetes.yaml" cfg.kubernetes;
|
||||
"homepage-dashboard/services.yaml".source = settingsFormat.generate "services.yaml" cfg.services;
|
||||
"homepage-dashboard/settings.yaml".source = settingsFormat.generate "settings.yaml" cfg.settings;
|
||||
"homepage-dashboard/widgets.yaml".source = settingsFormat.generate "widgets.yaml" cfg.widgets;
|
||||
};
|
||||
|
||||
systemd.services.homepage-dashboard = {
|
||||
description = "Homepage Dashboard";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
environment = {
|
||||
HOMEPAGE_CONFIG_DIR = "/etc/homepage-dashboard";
|
||||
NIXPKGS_HOMEPAGE_CACHE_DIR = "/var/cache/homepage-dashboard";
|
||||
PORT = toString cfg.listenPort;
|
||||
LOG_TARGETS = "stdout";
|
||||
};
|
||||
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
DynamicUser = true;
|
||||
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
|
||||
StateDirectory = "homepage-dashboard";
|
||||
CacheDirectory = "homepage-dashboard";
|
||||
ExecStart = lib.getExe cfg.package;
|
||||
Restart = "on-failure";
|
||||
};
|
||||
|
||||
enableStrictShellChecks = true;
|
||||
|
||||
# Related:
|
||||
# * https://github.com/NixOS/nixpkgs/issues/346016 ("homepage-dashboard: cache dir is not cleared upon version upgrade")
|
||||
# * https://github.com/gethomepage/homepage/discussions/4560 ("homepage NixOS package does not clear cache on upgrade leaving broken state")
|
||||
# * https://github.com/vercel/next.js/discussions/58864 ("Feature Request: Allow configuration of cache dir")
|
||||
preStart = ''
|
||||
rm -rf "''${NIXPKGS_HOMEPAGE_CACHE_DIR:?}"/*
|
||||
'';
|
||||
};
|
||||
|
||||
networking.firewall = lib.mkIf cfg.openFirewall {
|
||||
allowedTCPPorts = [ cfg.listenPort ];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ in
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
services.orthanc.settings = options.services.orthanc.settings.default;
|
||||
services.orthanc.settings = opt.settings.default;
|
||||
|
||||
systemd.services.orthanc = {
|
||||
description = "Orthanc is a lightweight, RESTful DICOM server for healthcare and medical research";
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
utils,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.services.fluent-bit;
|
||||
|
||||
yamlFormat = pkgs.formats.yaml { };
|
||||
in
|
||||
{
|
||||
options.services.fluent-bit = {
|
||||
enable = lib.mkEnableOption "Fluent Bit";
|
||||
package = lib.mkPackageOption pkgs "fluent-bit" { };
|
||||
configurationFile = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
default = yamlFormat.generate "fluent-bit.yaml" cfg.settings;
|
||||
defaultText = lib.literalExpression ''yamlFormat.generate "fluent-bit.yaml" cfg.settings'';
|
||||
description = ''
|
||||
Fluent Bit configuration. See
|
||||
<https://docs.fluentbit.io/manual/administration/configuring-fluent-bit/yaml>
|
||||
for supported values.
|
||||
|
||||
{option}`configurationFile` takes precedence over {option}`settings`.
|
||||
|
||||
Note: Restricted evaluation blocks access to paths outside the Nix store.
|
||||
This means detecting content changes for mutable paths (i.e. not input or content-addressed) can't be done.
|
||||
As a result, `nixos-rebuild` won't reload/restart the systemd unit when mutable path contents change.
|
||||
`systemctl restart fluent-bit.service` must be used instead.
|
||||
'';
|
||||
example = "/etc/fluent-bit/fluent-bit.yaml";
|
||||
};
|
||||
settings = lib.mkOption {
|
||||
type = yamlFormat.type;
|
||||
default = { };
|
||||
description = ''
|
||||
See {option}`configurationFile`.
|
||||
|
||||
{option}`configurationFile` takes precedence over {option}`settings`.
|
||||
'';
|
||||
example = {
|
||||
service = {
|
||||
grace = 30;
|
||||
};
|
||||
pipeline = {
|
||||
inputs = [
|
||||
{
|
||||
name = "systemd";
|
||||
systemd_filter = "_SYSTEMD_UNIT=fluent-bit.service";
|
||||
}
|
||||
];
|
||||
outputs = [
|
||||
{
|
||||
name = "file";
|
||||
path = "/var/log/fluent-bit";
|
||||
file = "fluent-bit.out";
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
# See https://docs.fluentbit.io/manual/administration/configuring-fluent-bit/yaml/service-section.
|
||||
graceLimit = lib.mkOption {
|
||||
type = lib.types.nullOr (
|
||||
lib.types.oneOf [
|
||||
lib.types.ints.positive
|
||||
lib.types.str
|
||||
]
|
||||
);
|
||||
default = null;
|
||||
description = ''
|
||||
The grace time limit. Sets the systemd unit's `TimeoutStopSec`.
|
||||
|
||||
The `service.grace` option in the Fluent Bit configuration should be ≤ this option.
|
||||
'';
|
||||
example = 30;
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
# See https://github.com/fluent/fluent-bit/blob/v3.2.6/init/systemd.in.
|
||||
systemd.services.fluent-bit = {
|
||||
description = "Fluent Bit";
|
||||
after = [ "network.target" ];
|
||||
requires = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
DynamicUser = true;
|
||||
# See https://nixos.org/manual/nixos/stable#sec-logging.
|
||||
SupplementaryGroups = "systemd-journal";
|
||||
ExecStart = utils.escapeSystemdExecArgs [
|
||||
(lib.getExe cfg.package)
|
||||
"--config"
|
||||
cfg.configurationFile
|
||||
];
|
||||
Restart = "always";
|
||||
TimeoutStopSec = lib.mkIf (cfg.graceLimit != null) cfg.graceLimit;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
{ config, lib, pkgs, utils, ... }:
|
||||
let
|
||||
inherit (lib) maintainers;
|
||||
inherit (lib.meta) getExe;
|
||||
inherit (lib.modules) mkIf mkMerge;
|
||||
inherit (lib.options) literalExpression mkEnableOption mkOption mkPackageOption;
|
||||
inherit (lib.types) bool enum nullOr port str submodule;
|
||||
inherit (utils) genJqSecretsReplacementSnippet;
|
||||
|
||||
cfg = config.services.scrutiny;
|
||||
# Define the settings format used for this program
|
||||
@@ -36,6 +37,11 @@ in
|
||||
Scrutiny settings to be rendered into the configuration file.
|
||||
|
||||
See <https://github.com/AnalogJ/scrutiny/blob/master/example.scrutiny.yaml>.
|
||||
|
||||
Options containing secret data should be set to an attribute set
|
||||
containing the attribute `_secret`. This attribute should be a string
|
||||
or structured JSON with `quote = false;`, pointing to a file that
|
||||
contains the value the option should be set to.
|
||||
'';
|
||||
default = { };
|
||||
type = submodule {
|
||||
@@ -130,6 +136,11 @@ in
|
||||
Collector settings to be rendered into the collector configuration file.
|
||||
|
||||
See <https://github.com/AnalogJ/scrutiny/blob/master/example.collector.yaml>.
|
||||
|
||||
Options containing secret data should be set to an attribute set
|
||||
containing the attribute `_secret`. This attribute should be a string
|
||||
or structured JSON with `quote = false;`, pointing to a file that
|
||||
contains the value the option should be set to.
|
||||
'';
|
||||
default = { };
|
||||
type = submodule {
|
||||
@@ -177,6 +188,9 @@ in
|
||||
SCRUTINY_WEB_DATABASE_LOCATION = "/var/lib/scrutiny/scrutiny.db";
|
||||
SCRUTINY_WEB_SRC_FRONTEND_PATH = "${cfg.package}/share/scrutiny";
|
||||
};
|
||||
preStart = ''
|
||||
${genJqSecretsReplacementSnippet cfg.settings "/run/scrutiny/config.yaml"}
|
||||
'';
|
||||
postStart = ''
|
||||
for i in $(seq 300); do
|
||||
if "${lib.getExe pkgs.curl}" --fail --silent --head "http://${cfg.settings.web.listen.host}:${toString cfg.settings.web.listen.port}" >/dev/null; then
|
||||
@@ -191,8 +205,10 @@ in
|
||||
'';
|
||||
serviceConfig = {
|
||||
DynamicUser = true;
|
||||
ExecStart = "${getExe cfg.package} start --config ${settingsFormat.generate "scrutiny.yaml" cfg.settings}";
|
||||
ExecStart = "${getExe cfg.package} start --config /run/scrutiny/config.yaml";
|
||||
Restart = "always";
|
||||
RuntimeDirectory = "scrutiny";
|
||||
RuntimeDirectoryMode = "0700";
|
||||
StateDirectory = "scrutiny";
|
||||
StateDirectoryMode = "0750";
|
||||
};
|
||||
@@ -216,9 +232,14 @@ in
|
||||
COLLECTOR_VERSION = "1";
|
||||
COLLECTOR_API_ENDPOINT = cfg.collector.settings.api.endpoint;
|
||||
};
|
||||
preStart = ''
|
||||
${genJqSecretsReplacementSnippet cfg.collector.settings "/run/scrutiny-collector/config.yaml"}
|
||||
'';
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${getExe cfg.collector.package} run --config ${settingsFormat.generate "scrutiny-collector.yaml" cfg.collector.settings}";
|
||||
ExecStart = "${getExe cfg.collector.package} run --config /run/scrutiny-collector/config.yaml";
|
||||
RuntimeDirectory = "scrutiny-collector";
|
||||
RuntimeDirectoryMode = "0700";
|
||||
};
|
||||
startAt = cfg.collector.schedule;
|
||||
};
|
||||
|
||||
@@ -75,6 +75,7 @@ in
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
"AF_NETLINK"
|
||||
"AF_UNIX"
|
||||
];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
|
||||
@@ -15,12 +15,13 @@ in
|
||||
package = lib.mkPackageOption pkgs "cloudflare-dyndns" { };
|
||||
|
||||
apiTokenFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
type = lib.types.pathWith {
|
||||
absolute = true;
|
||||
inStore = false;
|
||||
};
|
||||
|
||||
description = ''
|
||||
The path to a file containing the CloudFlare API token.
|
||||
|
||||
The file must have the form `CLOUDFLARE_API_TOKEN=...`
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -91,19 +92,35 @@ in
|
||||
Type = "simple";
|
||||
DynamicUser = true;
|
||||
StateDirectory = "cloudflare-dyndns";
|
||||
EnvironmentFile = cfg.apiTokenFile;
|
||||
Environment = [ "XDG_CACHE_HOME=%S/cloudflare-dyndns/.cache" ];
|
||||
ExecStart =
|
||||
let
|
||||
args =
|
||||
[ "--cache-file /var/lib/cloudflare-dyndns/ip.cache" ]
|
||||
++ (if cfg.ipv4 then [ "-4" ] else [ "-no-4" ])
|
||||
++ (if cfg.ipv6 then [ "-6" ] else [ "-no-6" ])
|
||||
++ lib.optional cfg.deleteMissing "--delete-missing"
|
||||
++ lib.optional cfg.proxied "--proxied";
|
||||
in
|
||||
"${lib.getExe cfg.package} ${toString args}";
|
||||
LoadCredential = [
|
||||
"apiToken:${cfg.apiTokenFile}"
|
||||
];
|
||||
};
|
||||
|
||||
script =
|
||||
let
|
||||
args =
|
||||
[ "--cache-file /var/lib/cloudflare-dyndns/ip.cache" ]
|
||||
++ (if cfg.ipv4 then [ "-4" ] else [ "-no-4" ])
|
||||
++ (if cfg.ipv6 then [ "-6" ] else [ "-no-6" ])
|
||||
++ lib.optional cfg.deleteMissing "--delete-missing"
|
||||
++ lib.optional cfg.proxied "--proxied";
|
||||
in
|
||||
''
|
||||
export CLOUDFLARE_API_TOKEN=$(< "''${CREDENTIALS_DIRECTORY}/apiToken")
|
||||
|
||||
# Added 2025-03-10: `cfg.apiTokenFile` used to be passed as an
|
||||
# `EnvironmentFile` to the service, which required it to be of
|
||||
# the form "CLOUDFLARE_API_TOKEN=" rather than just the secret.
|
||||
# If we detect this legacy usage, error out.
|
||||
if [[ $CLOUDFLARE_API_TOKEN == CLOUDFLARE_API_TOKEN* ]]; then
|
||||
echo "Error: your api token starts with 'CLOUDFLARE_API_TOKEN='. Remove that, and instead specify just the token." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec ${lib.getExe cfg.package} ${toString args}
|
||||
'';
|
||||
}
|
||||
// lib.optionalAttrs (cfg.frequency != null) {
|
||||
startAt = cfg.frequency;
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
boolToString
|
||||
getExe
|
||||
mkEnableOption
|
||||
mkIf
|
||||
mkOption
|
||||
mkPackageOption
|
||||
types
|
||||
;
|
||||
|
||||
cfg = config.services.firezone.gateway;
|
||||
in
|
||||
{
|
||||
options = {
|
||||
services.firezone.gateway = {
|
||||
enable = mkOption {
|
||||
default = false;
|
||||
example = true;
|
||||
description = ''
|
||||
Whether to enable the firezone gateway.
|
||||
|
||||
You have to manually masquerade and forward traffic from the
|
||||
tun-firezone interface to your resource! Refer to the
|
||||
[upstream setup script](https://github.com/firezone/firezone/blob/8c7c0a9e8e33ae790aeb75fdb5a15432c2870b79/scripts/gateway-systemd-install.sh#L154-L168)
|
||||
for a list of iptable commands.
|
||||
|
||||
See the firezone nixos test in this repository for an nftables based example.
|
||||
'';
|
||||
type = lib.types.bool;
|
||||
};
|
||||
package = mkPackageOption pkgs "firezone-gateway" { };
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
description = "The name of this gateway as shown in firezone";
|
||||
};
|
||||
|
||||
apiUrl = mkOption {
|
||||
type = types.strMatching "^wss://.+/$";
|
||||
example = "wss://firezone.example.com/api/";
|
||||
description = ''
|
||||
The URL of your firezone server's API. This should be the same
|
||||
as your server's setting for {option}`services.firezone.server.settings.api.externalUrl`,
|
||||
but with `wss://` instead of `https://`.
|
||||
'';
|
||||
};
|
||||
|
||||
tokenFile = mkOption {
|
||||
type = types.path;
|
||||
example = "/run/secrets/firezone-gateway-token";
|
||||
description = ''
|
||||
A file containing the firezone gateway token. Do not use a nix-store path here
|
||||
as it will make the token publicly readable!
|
||||
|
||||
This file will be passed via systemd credentials, it should only be accessible
|
||||
by the root user.
|
||||
'';
|
||||
};
|
||||
|
||||
logLevel = mkOption {
|
||||
type = types.str;
|
||||
default = "info";
|
||||
description = ''
|
||||
The log level for the firezone application. See
|
||||
[RUST_LOG](https://docs.rs/env_logger/latest/env_logger/#enabling-logging)
|
||||
for the format.
|
||||
'';
|
||||
};
|
||||
|
||||
enableTelemetry = mkEnableOption "telemetry";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services.firezone-gateway = {
|
||||
description = "Gateway service for the Firezone zero-trust access platform";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
path = [ pkgs.util-linux ];
|
||||
script = ''
|
||||
# If FIREZONE_ID is not given by the user, use a persisted (or newly generated) uuid.
|
||||
if [[ -z "''${FIREZONE_ID:-}" ]]; then
|
||||
if [[ ! -e gateway_id ]]; then
|
||||
uuidgen -r > gateway_id
|
||||
fi
|
||||
export FIREZONE_ID=$(< gateway_id)
|
||||
fi
|
||||
|
||||
export FIREZONE_TOKEN=$(< "$CREDENTIALS_DIRECTORY/firezone-token")
|
||||
exec ${getExe cfg.package}
|
||||
'';
|
||||
|
||||
environment = {
|
||||
FIREZONE_API_URL = cfg.apiUrl;
|
||||
FIREZONE_NAME = cfg.name;
|
||||
FIREZONE_NO_TELEMETRY = boolToString (!cfg.enableTelemetry);
|
||||
RUST_LOG = cfg.logLevel;
|
||||
};
|
||||
|
||||
serviceConfig = {
|
||||
Type = "exec";
|
||||
DynamicUser = true;
|
||||
User = "firezone-gateway";
|
||||
LoadCredential = [ "firezone-token:${cfg.tokenFile}" ];
|
||||
|
||||
DeviceAllow = "/dev/net/tun";
|
||||
AmbientCapabilities = [ "CAP_NET_ADMIN" ];
|
||||
CapabilityBoundingSet = [ "CAP_NET_ADMIN" ];
|
||||
|
||||
StateDirectory = "firezone-gateway";
|
||||
WorkingDirectory = "/var/lib/firezone-gateway";
|
||||
|
||||
Restart = "on-failure";
|
||||
RestartSec = 10;
|
||||
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateMounts = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = false;
|
||||
ProcSubset = "pid";
|
||||
ProtectClock = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectProc = "invisible";
|
||||
ProtectSystem = "strict";
|
||||
RestrictAddressFamilies = [
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
"AF_NETLINK"
|
||||
];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = "@system-service";
|
||||
UMask = "077";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
oddlama
|
||||
patrickdag
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
boolToString
|
||||
getExe'
|
||||
mkEnableOption
|
||||
mkIf
|
||||
mkOption
|
||||
mkPackageOption
|
||||
types
|
||||
;
|
||||
|
||||
cfg = config.services.firezone.gui-client;
|
||||
in
|
||||
{
|
||||
options = {
|
||||
services.firezone.gui-client = {
|
||||
enable = mkEnableOption "the firezone gui client";
|
||||
package = mkPackageOption pkgs "firezone-gui-client" { };
|
||||
|
||||
allowedUsers = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = ''
|
||||
All listed users will become part of the `firezone-client` group so
|
||||
they can control the IPC service. This is a convenience option.
|
||||
'';
|
||||
};
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
description = "The name of this client as shown in firezone";
|
||||
};
|
||||
|
||||
logLevel = mkOption {
|
||||
type = types.str;
|
||||
default = "info";
|
||||
description = ''
|
||||
The log level for the firezone application. See
|
||||
[RUST_LOG](https://docs.rs/env_logger/latest/env_logger/#enabling-logging)
|
||||
for the format.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
users.groups.firezone-client.members = cfg.allowedUsers;
|
||||
|
||||
# Required for deep-link mimetype registration
|
||||
environment.systemPackages = [ cfg.package ];
|
||||
|
||||
# Required for the token store in the gui application
|
||||
services.gnome.gnome-keyring.enable = true;
|
||||
|
||||
systemd.services.firezone-ipc-service = {
|
||||
description = "GUI IPC service for the Firezone zero-trust access platform";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
path = [ pkgs.util-linux ];
|
||||
script = ''
|
||||
# If FIREZONE_ID is not given by the user, use a persisted (or newly generated) uuid.
|
||||
if [[ -z "''${FIREZONE_ID:-}" ]]; then
|
||||
if [[ ! -e client_id ]]; then
|
||||
uuidgen -r > client_id
|
||||
fi
|
||||
export FIREZONE_ID=$(< client_id)
|
||||
fi
|
||||
|
||||
exec ${getExe' cfg.package "firezone-client-ipc"} run
|
||||
'';
|
||||
|
||||
environment = {
|
||||
FIREZONE_NAME = cfg.name;
|
||||
LOG_DIR = "%L/dev.firezone.client";
|
||||
RUST_LOG = cfg.logLevel;
|
||||
};
|
||||
|
||||
serviceConfig = {
|
||||
Type = "notify";
|
||||
|
||||
DeviceAllow = "/dev/net/tun";
|
||||
AmbientCapabilities = [ "CAP_NET_ADMIN" ];
|
||||
CapabilityBoundingSet = [ "CAP_NET_ADMIN" ];
|
||||
|
||||
# This block contains hardcoded values in the client, we cannot change these :(
|
||||
Group = "firezone-client";
|
||||
RuntimeDirectory = "dev.firezone.client";
|
||||
StateDirectory = "dev.firezone.client";
|
||||
WorkingDirectory = "/var/lib/dev.firezone.client";
|
||||
LogsDirectory = "dev.firezone.client";
|
||||
|
||||
Restart = "on-failure";
|
||||
RestartSec = 10;
|
||||
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateMounts = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = false;
|
||||
ProcSubset = "pid";
|
||||
ProtectClock = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectProc = "invisible";
|
||||
ProtectSystem = "strict";
|
||||
RestrictAddressFamilies = [
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
"AF_NETLINK"
|
||||
"AF_UNIX"
|
||||
];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = "@system-service";
|
||||
UMask = "077";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
oddlama
|
||||
patrickdag
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
boolToString
|
||||
getExe
|
||||
mkEnableOption
|
||||
mkIf
|
||||
mkOption
|
||||
mkPackageOption
|
||||
types
|
||||
;
|
||||
|
||||
cfg = config.services.firezone.headless-client;
|
||||
in
|
||||
{
|
||||
options = {
|
||||
services.firezone.headless-client = {
|
||||
enable = mkEnableOption "the firezone headless client";
|
||||
package = mkPackageOption pkgs "firezone-headless-client" { };
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
description = "The name of this client as shown in firezone";
|
||||
};
|
||||
|
||||
apiUrl = mkOption {
|
||||
type = types.strMatching "^wss://.+/$";
|
||||
example = "wss://firezone.example.com/api/";
|
||||
description = ''
|
||||
The URL of your firezone server's API. This should be the same
|
||||
as your server's setting for {option}`services.firezone.server.settings.api.externalUrl`,
|
||||
but with `wss://` instead of `https://`.
|
||||
'';
|
||||
};
|
||||
|
||||
tokenFile = mkOption {
|
||||
type = types.path;
|
||||
example = "/run/secrets/firezone-client-token";
|
||||
description = ''
|
||||
A file containing the firezone client token. Do not use a nix-store path here
|
||||
as it will make the token publicly readable!
|
||||
|
||||
This file will be passed via systemd credentials, it should only be accessible
|
||||
by the root user.
|
||||
'';
|
||||
};
|
||||
|
||||
logLevel = mkOption {
|
||||
type = types.str;
|
||||
default = "info";
|
||||
description = ''
|
||||
The log level for the firezone application. See
|
||||
[RUST_LOG](https://docs.rs/env_logger/latest/env_logger/#enabling-logging)
|
||||
for the format.
|
||||
'';
|
||||
};
|
||||
|
||||
enableTelemetry = mkEnableOption "telemetry";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
systemd.services.firezone-headless-client = {
|
||||
description = "headless client service for the Firezone zero-trust access platform";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
path = [ pkgs.util-linux ];
|
||||
script = ''
|
||||
# If FIREZONE_ID is not given by the user, use a persisted (or newly generated) uuid.
|
||||
if [[ -z "''${FIREZONE_ID:-}" ]]; then
|
||||
if [[ ! -e client_id ]]; then
|
||||
uuidgen -r > client_id
|
||||
fi
|
||||
export FIREZONE_ID=$(< client_id)
|
||||
fi
|
||||
|
||||
exec ${getExe cfg.package}
|
||||
'';
|
||||
|
||||
environment = {
|
||||
FIREZONE_API_URL = cfg.apiUrl;
|
||||
FIREZONE_NAME = cfg.name;
|
||||
FIREZONE_NO_TELEMETRY = boolToString (!cfg.enableTelemetry);
|
||||
FIREZONE_TOKEN_PATH = "%d/firezone-token";
|
||||
LOG_DIR = "%L/dev.firezone.client";
|
||||
RUST_LOG = cfg.logLevel;
|
||||
};
|
||||
|
||||
serviceConfig = {
|
||||
Type = "exec";
|
||||
LoadCredential = [ "firezone-token:${cfg.tokenFile}" ];
|
||||
|
||||
DeviceAllow = "/dev/net/tun";
|
||||
AmbientCapabilities = [ "CAP_NET_ADMIN" ];
|
||||
CapabilityBoundingSet = [ "CAP_NET_ADMIN" ];
|
||||
|
||||
# Hardcoded values in the client :(
|
||||
RuntimeDirectory = "dev.firezone.client";
|
||||
StateDirectory = "dev.firezone.client";
|
||||
WorkingDirectory = "/var/lib/dev.firezone.client";
|
||||
LogsDirectory = "dev.firezone.client";
|
||||
|
||||
Restart = "on-failure";
|
||||
RestartSec = 10;
|
||||
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateMounts = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = false;
|
||||
ProcSubset = "pid";
|
||||
ProtectClock = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectProc = "invisible";
|
||||
ProtectSystem = "strict";
|
||||
RestrictAddressFamilies = [
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
"AF_NETLINK"
|
||||
"AF_UNIX"
|
||||
];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = "@system-service";
|
||||
UMask = "077";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
oddlama
|
||||
patrickdag
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,709 @@
|
||||
defmodule Provision do
|
||||
alias Domain.{Repo, Accounts, Auth, Actors, Resources, Tokens, Gateways, Relays, Policies}
|
||||
require Logger
|
||||
|
||||
# UUID Mapping handling
|
||||
defmodule UuidMapping do
|
||||
@mapping_file "provision-uuids.json"
|
||||
|
||||
# Loads the mapping from file
|
||||
def load do
|
||||
mappings = case File.read(@mapping_file) do
|
||||
{:ok, content} ->
|
||||
case Jason.decode(content) do
|
||||
{:ok, mapping} -> mapping
|
||||
_ -> %{"accounts" => %{}}
|
||||
end
|
||||
|
||||
_ -> %{"accounts" => %{}}
|
||||
end
|
||||
Process.put(:uuid_mappings, mappings)
|
||||
mappings
|
||||
end
|
||||
|
||||
# Saves the current mapping (defaulting to the one in the process dictionary)
|
||||
def save(mapping \\ Process.get(:uuid_mappings)) do
|
||||
File.write!(@mapping_file, Jason.encode!(mapping))
|
||||
end
|
||||
|
||||
# Retrieves the account-level mapping from a given mapping (or from Process)
|
||||
def get_account(mapping \\ Process.get(:uuid_mappings), account_slug) do
|
||||
get_in(mapping, ["accounts", account_slug]) || %{}
|
||||
end
|
||||
|
||||
# Retrieves the entity mapping for a specific account and type
|
||||
def get_entities(mapping \\ Process.get(:uuid_mappings), account_slug, type) do
|
||||
get_in(mapping, ["accounts", account_slug, type]) || %{}
|
||||
end
|
||||
|
||||
# Retrieves an entity mapping for a specific account, type and external_id
|
||||
def get_entity(mapping \\ Process.get(:uuid_mappings), account_slug, type, external_id) do
|
||||
get_in(mapping, ["accounts", account_slug, type, external_id])
|
||||
end
|
||||
|
||||
# Updates (or creates) the account UUID mapping and stores it in the process dictionary.
|
||||
def update_account(account_slug, uuid) do
|
||||
mapping = Process.get(:uuid_mappings) || load()
|
||||
mapping = ensure_account_exists(mapping, account_slug)
|
||||
mapping = put_in(mapping, ["accounts", account_slug, "id"], uuid)
|
||||
Process.put(:uuid_mappings, mapping)
|
||||
mapping
|
||||
end
|
||||
|
||||
# Ensures that the given account exists in the mapping.
|
||||
def ensure_account_exists(mapping, account_slug) do
|
||||
if not Map.has_key?(mapping["accounts"], account_slug) do
|
||||
put_in(mapping, ["accounts", account_slug], %{})
|
||||
else
|
||||
mapping
|
||||
end
|
||||
end
|
||||
|
||||
# Updates (or creates) the mapping for entities of a given type for the account.
|
||||
def update_entities(account_slug, type, new_entries) do
|
||||
mapping = Process.get(:uuid_mappings) || load()
|
||||
mapping = ensure_account_exists(mapping, account_slug)
|
||||
current = get_entities(mapping, account_slug, type)
|
||||
mapping = put_in(mapping, ["accounts", account_slug, type], Map.merge(current, new_entries))
|
||||
Process.put(:uuid_mappings, mapping)
|
||||
mapping
|
||||
end
|
||||
|
||||
# Removes an entire account from the mapping.
|
||||
def remove_account(account_slug) do
|
||||
mapping = Process.get(:uuid_mappings) || load()
|
||||
mapping = update_in(mapping, ["accounts"], fn accounts ->
|
||||
Map.delete(accounts, account_slug)
|
||||
end)
|
||||
Process.put(:uuid_mappings, mapping)
|
||||
mapping
|
||||
end
|
||||
|
||||
# Removes a specific entity mapping for the account.
|
||||
def remove_entity(account_slug, type, key) do
|
||||
mapping = Process.get(:uuid_mappings) || load()
|
||||
mapping = update_in(mapping, ["accounts", account_slug, type], fn entities ->
|
||||
Map.delete(entities || %{}, key)
|
||||
end)
|
||||
Process.put(:uuid_mappings, mapping)
|
||||
mapping
|
||||
end
|
||||
end
|
||||
|
||||
defp resolve_references(value) when is_map(value) do
|
||||
Enum.into(value, %{}, fn {k, v} -> {k, resolve_references(v)} end)
|
||||
end
|
||||
|
||||
defp resolve_references(value) when is_list(value) do
|
||||
Enum.map(value, &resolve_references/1)
|
||||
end
|
||||
|
||||
defp resolve_references(value) when is_binary(value) do
|
||||
Regex.replace(~r/\{env:([^}]+)\}/, value, fn _, var ->
|
||||
System.get_env(var) || raise "Environment variable #{var} not set"
|
||||
end)
|
||||
end
|
||||
|
||||
defp resolve_references(value), do: value
|
||||
|
||||
defp atomize_keys(map) when is_map(map) do
|
||||
Enum.into(map, %{}, fn {k, v} ->
|
||||
{
|
||||
if(is_binary(k), do: String.to_atom(k), else: k),
|
||||
if(is_map(v), do: atomize_keys(v), else: v)
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
defp cleanup_account(uuid) do
|
||||
case Accounts.fetch_account_by_id_or_slug(uuid) do
|
||||
{:ok, value} when value.deleted_at == nil ->
|
||||
Logger.info("Deleting removed account #{value.slug}")
|
||||
value |> Ecto.Changeset.change(%{ deleted_at: DateTime.utc_now() }) |> Repo.update!()
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp cleanup_actor(uuid, subject) do
|
||||
case Actors.fetch_actor_by_id(uuid, subject) do
|
||||
{:ok, value} ->
|
||||
Logger.info("Deleting removed actor #{value.name}")
|
||||
{:ok, _} = Actors.delete_actor(value, subject)
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp cleanup_provider(uuid, subject) do
|
||||
case Auth.fetch_provider_by_id(uuid, subject) do
|
||||
{:ok, value} ->
|
||||
Logger.info("Deleting removed provider #{value.name}")
|
||||
{:ok, _} = Auth.delete_provider(value, subject)
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp cleanup_gateway_group(uuid, subject) do
|
||||
case Gateways.fetch_group_by_id(uuid, subject) do
|
||||
{:ok, value} ->
|
||||
Logger.info("Deleting removed gateway group #{value.name}")
|
||||
{:ok, _} = Gateways.delete_group(value, subject)
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp cleanup_relay_group(uuid, subject) do
|
||||
case Relays.fetch_group_by_id(uuid, subject) do
|
||||
{:ok, value} ->
|
||||
Logger.info("Deleting removed relay group #{value.name}")
|
||||
{:ok, _} = Relays.delete_group(value, subject)
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp cleanup_actor_group(uuid, subject) do
|
||||
case Actors.fetch_group_by_id(uuid, subject) do
|
||||
{:ok, value} ->
|
||||
Logger.info("Deleting removed actor group #{value.name}")
|
||||
{:ok, _} = Actors.delete_group(value, subject)
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
# Fetch resource by uuid, but follow the chain of replacements if any
|
||||
defp fetch_resource(uuid, subject) do
|
||||
case Resources.fetch_resource_by_id(uuid, subject) do
|
||||
{:ok, resource} when resource.replaced_by_resource_id != nil -> fetch_resource(resource.replaced_by_resource_id, subject)
|
||||
v -> v
|
||||
end
|
||||
end
|
||||
|
||||
defp cleanup_resource(uuid, subject) do
|
||||
case fetch_resource(uuid, subject) do
|
||||
{:ok, value} when value.deleted_at == nil ->
|
||||
Logger.info("Deleting removed resource #{value.name}")
|
||||
{:ok, _} = Resources.delete_resource(value, subject)
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
# Fetch policy by uuid, but follow the chain of replacements if any
|
||||
defp fetch_policy(uuid, subject) do
|
||||
case Policies.fetch_policy_by_id(uuid, subject) do
|
||||
{:ok, policy} when policy.replaced_by_policy_id != nil -> fetch_policy(policy.replaced_by_policy_id, subject)
|
||||
v -> v
|
||||
end
|
||||
end
|
||||
|
||||
defp cleanup_policy(uuid, subject) do
|
||||
case fetch_policy(uuid, subject) do
|
||||
{:ok, value} when value.deleted_at == nil ->
|
||||
Logger.info("Deleting removed policy #{value.description}")
|
||||
{:ok, _} = Policies.delete_policy(value, subject)
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp cleanup_entity_type(account_slug, entity_type, cleanup_fn, temp_admin_subject) do
|
||||
# Get mapping for this entity type
|
||||
existing_entities = UuidMapping.get_entities(account_slug, entity_type)
|
||||
# Get current entities from account data
|
||||
current_entities = Process.get(:current_entities)
|
||||
# Determine which ones to remove
|
||||
removed_entity_ids = Map.keys(existing_entities) -- (current_entities[entity_type] || [])
|
||||
|
||||
# Process each entity to remove
|
||||
Enum.each(removed_entity_ids, fn entity_id ->
|
||||
case existing_entities[entity_id] do
|
||||
nil -> :ok
|
||||
uuid ->
|
||||
cleanup_fn.(uuid, temp_admin_subject)
|
||||
UuidMapping.remove_entity(account_slug, entity_type, entity_id)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp collect_current_entities(account_data) do
|
||||
%{
|
||||
"actors" => Map.keys(account_data["actors"] || %{}),
|
||||
"providers" => Map.keys(account_data["auth"] || %{}),
|
||||
"gateway_groups" => Map.keys(account_data["gatewayGroups"] || %{}),
|
||||
"relay_groups" => Map.keys(account_data["relayGroups"] || %{}),
|
||||
"actor_groups" => Map.keys(account_data["groups"] || %{}) ++ ["everyone"],
|
||||
"resources" => Map.keys(account_data["resources"] || %{}),
|
||||
"policies" => Map.keys(account_data["policies"] || %{})
|
||||
}
|
||||
end
|
||||
|
||||
defp nil_if_deleted_or_not_found(value) do
|
||||
case value do
|
||||
nil -> nil
|
||||
{:error, :not_found} -> nil
|
||||
{:ok, value} when value.deleted_at != nil -> nil
|
||||
v -> v
|
||||
end
|
||||
end
|
||||
|
||||
defp create_temp_admin(account, email_provider) do
|
||||
temp_admin_actor_email = "firezone-provision@localhost.local"
|
||||
temp_admin_actor_context = %Auth.Context{
|
||||
type: :browser,
|
||||
user_agent: "Unspecified/0.0",
|
||||
remote_ip: {127, 0, 0, 1},
|
||||
remote_ip_location_region: "N/A",
|
||||
remote_ip_location_city: "N/A",
|
||||
remote_ip_location_lat: 0.0,
|
||||
remote_ip_location_lon: 0.0
|
||||
}
|
||||
|
||||
{:ok, temp_admin_actor} =
|
||||
Actors.create_actor(account, %{
|
||||
type: :account_admin_user,
|
||||
name: "Provisioning"
|
||||
})
|
||||
|
||||
{:ok, temp_admin_actor_email_identity} =
|
||||
Auth.create_identity(temp_admin_actor, email_provider, %{
|
||||
provider_identifier: temp_admin_actor_email,
|
||||
provider_identifier_confirmation: temp_admin_actor_email
|
||||
})
|
||||
|
||||
{:ok, temp_admin_actor_token} =
|
||||
Auth.create_token(temp_admin_actor_email_identity, temp_admin_actor_context, "temporarynonce", DateTime.utc_now() |> DateTime.add(1, :hour))
|
||||
|
||||
{:ok, temp_admin_subject} =
|
||||
Auth.build_subject(temp_admin_actor_token, temp_admin_actor_context)
|
||||
|
||||
{temp_admin_subject, temp_admin_actor, temp_admin_actor_email_identity, temp_admin_actor_token}
|
||||
end
|
||||
|
||||
defp cleanup_temp_admin(temp_admin_actor, temp_admin_actor_email_identity, temp_admin_actor_token, subject) do
|
||||
Logger.info("Cleaning up temporary admin actor")
|
||||
{:ok, _} = Tokens.delete_token(temp_admin_actor_token, subject)
|
||||
{:ok, _} = Auth.delete_identity(temp_admin_actor_email_identity, subject)
|
||||
{:ok, _} = Actors.delete_actor(temp_admin_actor, subject)
|
||||
end
|
||||
|
||||
def provision() do
|
||||
Logger.info("Starting provisioning")
|
||||
|
||||
# Load desired state
|
||||
json_file = "provision-state.json"
|
||||
{:ok, raw_json} = File.read(json_file)
|
||||
{:ok, %{"accounts" => accounts}} = Jason.decode(raw_json)
|
||||
accounts = resolve_references(accounts)
|
||||
|
||||
# Load existing UUID mappings into the process dictionary.
|
||||
UuidMapping.load()
|
||||
|
||||
# Clean up removed accounts first
|
||||
current_account_slugs = Map.keys(accounts)
|
||||
existing_accounts = Map.keys(Process.get(:uuid_mappings)["accounts"])
|
||||
removed_accounts = existing_accounts -- current_account_slugs
|
||||
|
||||
Enum.each(removed_accounts, fn slug ->
|
||||
if uuid = get_in(Process.get(:uuid_mappings), ["accounts", slug, "id"]) do
|
||||
cleanup_account(uuid)
|
||||
# Remove the account from the UUID mapping.
|
||||
UuidMapping.remove_account(slug)
|
||||
end
|
||||
end)
|
||||
|
||||
multi = Enum.reduce(accounts, Ecto.Multi.new(), fn {slug, account_data}, multi ->
|
||||
account_attrs = atomize_keys(%{
|
||||
name: account_data["name"],
|
||||
slug: slug,
|
||||
features: Map.get(account_data, "features", %{}),
|
||||
metadata: Map.get(account_data, "metadata", %{}),
|
||||
limits: Map.get(account_data, "limits", %{})
|
||||
})
|
||||
|
||||
multi = multi
|
||||
|> Ecto.Multi.run({:account, slug}, fn repo, _changes ->
|
||||
case Accounts.fetch_account_by_id_or_slug(slug) do
|
||||
{:ok, acc} ->
|
||||
Logger.info("Updating existing account #{slug}")
|
||||
updated_acc = acc |> Ecto.Changeset.change(account_attrs) |> repo.update!()
|
||||
{:ok, {:existing, updated_acc}}
|
||||
_ ->
|
||||
Logger.info("Creating new account #{slug}")
|
||||
{:ok, account} = Accounts.create_account(account_attrs)
|
||||
|
||||
Logger.info("Creating internet gateway group")
|
||||
{:ok, internet_site} = Gateways.create_internet_group(account)
|
||||
|
||||
Logger.info("Creating internet resource")
|
||||
{:ok, _internet_resource} = Resources.create_internet_resource(account, internet_site)
|
||||
|
||||
# Store mapping of slug to UUID
|
||||
UuidMapping.update_account(slug, account.id)
|
||||
{:ok, {:new, account}}
|
||||
end
|
||||
end)
|
||||
|> Ecto.Multi.run({:everyone_group, slug}, fn _repo, changes ->
|
||||
case Map.get(changes, {:account, slug}) do
|
||||
{:new, account} ->
|
||||
Logger.info("Creating everyone group for new account")
|
||||
{:ok, actor_group} = Actors.create_managed_group(account, %{name: "Everyone", membership_rules: [%{operator: true}]})
|
||||
UuidMapping.update_entities(slug, "actor_groups", %{"everyone" => actor_group.id})
|
||||
{:ok, actor_group}
|
||||
{:existing, _account} ->
|
||||
{:ok, :skipped}
|
||||
end
|
||||
end)
|
||||
|> Ecto.Multi.run({:email_provider, slug}, fn _repo, changes ->
|
||||
case Map.get(changes, {:account, slug}) do
|
||||
{:new, account} ->
|
||||
Logger.info("Creating default email provider for new account")
|
||||
Auth.create_provider(account, %{name: "Email", adapter: :email, adapter_config: %{}})
|
||||
{:existing, account} ->
|
||||
Auth.Provider.Query.not_disabled()
|
||||
|> Auth.Provider.Query.by_adapter(:email)
|
||||
|> Auth.Provider.Query.by_account_id(account.id)
|
||||
|> Repo.fetch(Auth.Provider.Query, [])
|
||||
end
|
||||
end)
|
||||
|> Ecto.Multi.run({:temp_admin, slug}, fn _repo, changes ->
|
||||
{_, account} = changes[{:account, slug}]
|
||||
email_provider = changes[{:email_provider, slug}]
|
||||
{:ok, create_temp_admin(account, email_provider)}
|
||||
end)
|
||||
|
||||
# Clean up removed entities for this account after we have an admin subject
|
||||
multi = multi
|
||||
|> Ecto.Multi.run({:cleanup_entities, slug}, fn _repo, changes ->
|
||||
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
|
||||
|
||||
# Store current entities in process dictionary for our helper function
|
||||
current_entities = collect_current_entities(account_data)
|
||||
Process.put(:current_entities, current_entities)
|
||||
|
||||
# Define entity types and their cleanup functions
|
||||
entity_types = [
|
||||
{"actors", &cleanup_actor/2},
|
||||
{"providers", &cleanup_provider/2},
|
||||
{"gateway_groups", &cleanup_gateway_group/2},
|
||||
{"relay_groups", &cleanup_relay_group/2},
|
||||
{"actor_groups", &cleanup_actor_group/2},
|
||||
{"resources", &cleanup_resource/2},
|
||||
{"policies", &cleanup_policy/2}
|
||||
]
|
||||
|
||||
# Clean up each entity type
|
||||
Enum.each(entity_types, fn {entity_type, cleanup_fn} ->
|
||||
cleanup_entity_type(slug, entity_type, cleanup_fn, temp_admin_subject)
|
||||
end)
|
||||
|
||||
{:ok, :cleaned}
|
||||
end)
|
||||
|
||||
# Create or update actors
|
||||
multi = Enum.reduce(account_data["actors"] || %{}, multi, fn {external_id, actor_data}, multi ->
|
||||
actor_attrs = atomize_keys(%{
|
||||
name: actor_data["name"],
|
||||
type: String.to_atom(actor_data["type"])
|
||||
})
|
||||
|
||||
Ecto.Multi.run(multi, {:actor, slug, external_id}, fn _repo, changes ->
|
||||
{_, account} = changes[{:account, slug}]
|
||||
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
|
||||
uuid = UuidMapping.get_entity(slug, "actors", external_id)
|
||||
case uuid && Actors.fetch_actor_by_id(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
|
||||
nil ->
|
||||
Logger.info("Creating new actor #{actor_data["name"]}")
|
||||
{:ok, actor} = Actors.create_actor(account, actor_attrs)
|
||||
# Update the mapping without manually handling Process.get/put.
|
||||
UuidMapping.update_entities(slug, "actors", %{external_id => actor.id})
|
||||
{:ok, {:new, actor}}
|
||||
{:ok, existing_actor} ->
|
||||
Logger.info("Updating existing actor #{actor_data["name"]}")
|
||||
{:ok, updated_act} = Actors.update_actor(existing_actor, actor_attrs, temp_admin_subject)
|
||||
{:ok, {:existing, updated_act}}
|
||||
end
|
||||
end)
|
||||
|> Ecto.Multi.run({:actor_identity, slug, external_id}, fn repo, changes ->
|
||||
email_provider = changes[{:email_provider, slug}]
|
||||
case Map.get(changes, {:actor, slug, external_id}) do
|
||||
{:new, actor} ->
|
||||
Logger.info("Creating actor email identity")
|
||||
Auth.create_identity(actor, email_provider, %{
|
||||
provider_identifier: actor_data["email"],
|
||||
provider_identifier_confirmation: actor_data["email"]
|
||||
})
|
||||
{:existing, actor} ->
|
||||
Logger.info("Updating actor email identity")
|
||||
{:ok, identity} = Auth.Identity.Query.not_deleted()
|
||||
|> Auth.Identity.Query.by_actor_id(actor.id)
|
||||
|> Auth.Identity.Query.by_provider_id(email_provider.id)
|
||||
|> Repo.fetch(Auth.Identity.Query, [])
|
||||
|
||||
{:ok, identity |> Ecto.Changeset.change(%{
|
||||
provider_identifier: actor_data["email"]
|
||||
}) |> repo.update!()}
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
# Create or update providers
|
||||
multi = Enum.reduce(account_data["auth"] || %{}, multi, fn {external_id, provider_data}, multi ->
|
||||
Ecto.Multi.run(multi, {:provider, slug, external_id}, fn repo, changes ->
|
||||
provider_attrs = %{
|
||||
name: provider_data["name"],
|
||||
adapter: String.to_atom(provider_data["adapter"]),
|
||||
adapter_config: provider_data["adapter_config"]
|
||||
}
|
||||
|
||||
{_, account} = changes[{:account, slug}]
|
||||
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
|
||||
uuid = UuidMapping.get_entity(slug, "providers", external_id)
|
||||
case uuid && Auth.fetch_provider_by_id(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
|
||||
nil ->
|
||||
Logger.info("Creating new provider #{provider_data["name"]}")
|
||||
{:ok, provider} = Auth.create_provider(account, provider_attrs)
|
||||
UuidMapping.update_entities(slug, "providers", %{external_id => provider.id})
|
||||
{:ok, provider}
|
||||
{:ok, existing} ->
|
||||
Logger.info("Updating existing provider #{provider_data["name"]}")
|
||||
{:ok, existing |> Ecto.Changeset.change(provider_attrs) |> repo.update!()}
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
# Create or update gateway_groups
|
||||
multi = Enum.reduce(account_data["gatewayGroups"] || %{}, multi, fn {external_id, gateway_group_data}, multi ->
|
||||
Ecto.Multi.run(multi, {:gateway_group, slug, external_id}, fn _repo, changes ->
|
||||
gateway_group_attrs = %{
|
||||
name: gateway_group_data["name"],
|
||||
tokens: [%{}]
|
||||
}
|
||||
|
||||
{_, account} = changes[{:account, slug}]
|
||||
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
|
||||
uuid = UuidMapping.get_entity(slug, "gateway_groups", external_id)
|
||||
case uuid && Gateways.fetch_group_by_id(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
|
||||
nil ->
|
||||
Logger.info("Creating new gateway group #{gateway_group_data["name"]}")
|
||||
gateway_group = account
|
||||
|> Gateways.Group.Changeset.create(gateway_group_attrs, temp_admin_subject)
|
||||
|> Repo.insert!()
|
||||
UuidMapping.update_entities(slug, "gateway_groups", %{external_id => gateway_group.id})
|
||||
{:ok, gateway_group}
|
||||
{:ok, existing} ->
|
||||
# Nothing to update
|
||||
{:ok, existing}
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
# Create or update relay_groups
|
||||
multi = Enum.reduce(account_data["relayGroups"] || %{}, multi, fn {external_id, relay_group_data}, multi ->
|
||||
Ecto.Multi.run(multi, {:relay_group, slug, external_id}, fn _repo, changes ->
|
||||
relay_group_attrs = %{
|
||||
name: relay_group_data["name"]
|
||||
}
|
||||
|
||||
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
|
||||
uuid = UuidMapping.get_entity(slug, "relay_groups", external_id)
|
||||
existing_relay_group = uuid && Relays.fetch_group_by_id(uuid, temp_admin_subject)
|
||||
case existing_relay_group do
|
||||
v when v in [nil, {:error, :not_found}] ->
|
||||
Logger.info("Creating new relay group #{relay_group_data["name"]}")
|
||||
{:ok, relay_group} = Relays.create_group(relay_group_attrs, temp_admin_subject)
|
||||
UuidMapping.update_entities(slug, "relay_groups", %{external_id => relay_group.id})
|
||||
{:ok, relay_group}
|
||||
{:ok, existing} ->
|
||||
# Nothing to update
|
||||
{:ok, existing}
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
# Create or update actor_groups
|
||||
multi = Enum.reduce(account_data["groups"] || %{}, multi, fn {external_id, actor_group_data}, multi ->
|
||||
Ecto.Multi.run(multi, {:actor_group, slug, external_id}, fn _repo, changes ->
|
||||
actor_group_attrs = %{
|
||||
name: actor_group_data["name"],
|
||||
type: :static
|
||||
}
|
||||
|
||||
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
|
||||
uuid = UuidMapping.get_entity(slug, "actor_groups", external_id)
|
||||
case uuid && Actors.fetch_group_by_id(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
|
||||
nil ->
|
||||
Logger.info("Creating new actor group #{actor_group_data["name"]}")
|
||||
{:ok, actor_group} = Actors.create_group(actor_group_attrs, temp_admin_subject)
|
||||
UuidMapping.update_entities(slug, "actor_groups", %{external_id => actor_group.id})
|
||||
{:ok, actor_group}
|
||||
{:ok, existing} ->
|
||||
# Nothing to update
|
||||
{:ok, existing}
|
||||
end
|
||||
end)
|
||||
|> Ecto.Multi.run({:actor_group_members, slug, external_id}, fn repo, changes ->
|
||||
{_, account} = changes[{:account, slug}]
|
||||
group_uuid = UuidMapping.get_entity(slug, "actor_groups", external_id)
|
||||
|
||||
memberships =
|
||||
Actors.Membership.Query.all()
|
||||
|> Actors.Membership.Query.by_group_id(group_uuid)
|
||||
|> Actors.Membership.Query.returning_all()
|
||||
|> Repo.all()
|
||||
|
||||
existing_members = Enum.map(memberships, fn membership -> membership.actor_id end)
|
||||
desired_members = Enum.map(actor_group_data["members"] || [], fn member ->
|
||||
uuid = UuidMapping.get_entity(slug, "actors", member)
|
||||
if uuid == nil do
|
||||
raise "Cannot find provisioned actor #{member} to add to group"
|
||||
end
|
||||
uuid
|
||||
end)
|
||||
|
||||
missing_members = desired_members -- existing_members
|
||||
untracked_members = existing_members -- desired_members
|
||||
|
||||
Logger.info("Updating members for actor group #{external_id}")
|
||||
Enum.each(missing_members || [], fn actor_uuid ->
|
||||
Logger.info("Adding member #{external_id}")
|
||||
Actors.Membership.Changeset.upsert(account.id, %Actors.Membership{}, %{
|
||||
group_id: group_uuid,
|
||||
actor_id: actor_uuid
|
||||
})
|
||||
|> repo.insert!()
|
||||
end)
|
||||
|
||||
if actor_group_data["forceMembers"] == true do
|
||||
# Remove untracked members
|
||||
to_delete = Enum.map(untracked_members, fn actor_uuid -> {group_uuid, actor_uuid} end)
|
||||
if to_delete != [] do
|
||||
Actors.Membership.Query.by_group_id_and_actor_id({:in, to_delete})
|
||||
|> repo.delete_all()
|
||||
end
|
||||
end
|
||||
|
||||
{:ok, nil}
|
||||
end)
|
||||
end)
|
||||
|
||||
# Create or update resources
|
||||
multi = Enum.reduce(account_data["resources"] || %{}, multi, fn {external_id, resource_data}, multi ->
|
||||
Ecto.Multi.run(multi, {:resource, slug, external_id}, fn _repo, changes ->
|
||||
resource_attrs = %{
|
||||
type: String.to_atom(resource_data["type"]),
|
||||
name: resource_data["name"],
|
||||
address: resource_data["address"],
|
||||
address_description: resource_data["address_description"],
|
||||
connections: Enum.map(resource_data["gatewayGroups"] || [], fn group ->
|
||||
%{gateway_group_id: UuidMapping.get_entity(slug, "gateway_groups", group)}
|
||||
end),
|
||||
filters: Enum.map(resource_data["filters"] || [], fn filter ->
|
||||
%{
|
||||
ports: filter["ports"] || [],
|
||||
protocol: String.to_atom(filter["protocol"])
|
||||
}
|
||||
end)
|
||||
}
|
||||
|
||||
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
|
||||
uuid = UuidMapping.get_entity(slug, "resources", external_id)
|
||||
case uuid && fetch_resource(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
|
||||
nil ->
|
||||
Logger.info("Creating new resource #{resource_data["name"]}")
|
||||
{:ok, resource} = Resources.create_resource(resource_attrs, temp_admin_subject)
|
||||
UuidMapping.update_entities(slug, "resources", %{external_id => resource.id})
|
||||
{:ok, resource}
|
||||
{:ok, existing} ->
|
||||
existing = Repo.preload(existing, :connections)
|
||||
Logger.info("Updating existing resource #{resource_data["name"]}")
|
||||
only_updated_attrs = resource_attrs
|
||||
|> Enum.reject(fn {key, value} ->
|
||||
case key do
|
||||
# Compare connections by gateway_group_id only
|
||||
:connections -> value == Enum.map(existing.connections || [], fn conn -> Map.take(conn, [:gateway_group_id]) end)
|
||||
# Compare filters by ports and protocol only
|
||||
:filters -> value == Enum.map(existing.filters || [], fn filter -> Map.take(filter, [:ports, :protocol]) end)
|
||||
_ -> Map.get(existing, key) == value
|
||||
end
|
||||
end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
if only_updated_attrs == %{} do
|
||||
{:ok, existing}
|
||||
else
|
||||
resource = case existing |> Resources.update_resource(resource_attrs, temp_admin_subject) do
|
||||
{:replaced, _old, new} ->
|
||||
UuidMapping.update_entities(slug, "resources", %{external_id => new.id})
|
||||
new
|
||||
{:updated, value} -> value
|
||||
x -> x
|
||||
end
|
||||
|
||||
{:ok, resource}
|
||||
end
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
# Create or update policies
|
||||
multi = Enum.reduce(account_data["policies"] || %{}, multi, fn {external_id, policy_data}, multi ->
|
||||
Ecto.Multi.run(multi, {:policy, slug, external_id}, fn _repo, changes ->
|
||||
policy_attrs = %{
|
||||
description: policy_data["description"],
|
||||
actor_group_id: UuidMapping.get_entity(slug, "actor_groups", policy_data["group"]),
|
||||
resource_id: UuidMapping.get_entity(slug, "resources", policy_data["resource"])
|
||||
}
|
||||
|
||||
{temp_admin_subject, _, _, _} = changes[{:temp_admin, slug}]
|
||||
uuid = UuidMapping.get_entity(slug, "policies", external_id)
|
||||
case uuid && fetch_policy(uuid, temp_admin_subject) |> nil_if_deleted_or_not_found() do
|
||||
nil ->
|
||||
Logger.info("Creating new policy #{policy_data["name"]}")
|
||||
{:ok, policy} = Policies.create_policy(policy_attrs, temp_admin_subject)
|
||||
UuidMapping.update_entities(slug, "policies", %{external_id => policy.id})
|
||||
{:ok, policy}
|
||||
{:ok, existing} ->
|
||||
Logger.info("Updating existing policy #{policy_data["name"]}")
|
||||
only_updated_attrs = policy_attrs
|
||||
|> Enum.reject(fn {key, value} -> Map.get(existing, key) == value end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
if only_updated_attrs == %{} do
|
||||
{:ok, existing}
|
||||
else
|
||||
policy = case existing |> Policies.update_policy(policy_attrs, temp_admin_subject) do
|
||||
{:replaced, _old, new} ->
|
||||
UuidMapping.update_entities(slug, "policies", %{external_id => new.id})
|
||||
new
|
||||
{:updated, value} -> value
|
||||
x -> x
|
||||
end
|
||||
|
||||
{:ok, policy}
|
||||
end
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
# Clean up temporary admin after all operations
|
||||
multi |> Ecto.Multi.run({:cleanup_temp_admin, slug}, fn _repo, changes ->
|
||||
{temp_admin_subject, temp_admin_actor, temp_admin_actor_email_identity, temp_admin_actor_token} =
|
||||
changes[{:temp_admin, slug}]
|
||||
|
||||
cleanup_temp_admin(temp_admin_actor, temp_admin_actor_email_identity, temp_admin_actor_token, temp_admin_subject)
|
||||
{:ok, :cleaned}
|
||||
end)
|
||||
end)
|
||||
|> Ecto.Multi.run({:save_state}, fn _repo, _changes ->
|
||||
# Save all UUID mappings to disk.
|
||||
UuidMapping.save()
|
||||
{:ok, :saved}
|
||||
end)
|
||||
|
||||
case Repo.transaction(multi) do
|
||||
{:ok, _result} ->
|
||||
Logger.info("Provisioning completed successfully")
|
||||
{:error, step, reason, _changes} ->
|
||||
Logger.error("Provisioning failed at step #{inspect(step)}, no changes were applied: #{inspect(reason)}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Provision.provision()
|
||||
@@ -0,0 +1,202 @@
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
boolToString
|
||||
getExe
|
||||
mkEnableOption
|
||||
mkIf
|
||||
mkOption
|
||||
mkPackageOption
|
||||
types
|
||||
;
|
||||
|
||||
cfg = config.services.firezone.relay;
|
||||
in
|
||||
{
|
||||
options = {
|
||||
services.firezone.relay = {
|
||||
enable = mkEnableOption "the firezone relay server";
|
||||
package = mkPackageOption pkgs "firezone-relay" { };
|
||||
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
example = "My relay";
|
||||
description = "The name of this gateway as shown in firezone";
|
||||
};
|
||||
|
||||
publicIpv4 = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "The public ipv4 address of this relay";
|
||||
};
|
||||
|
||||
publicIpv6 = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "The public ipv6 address of this relay";
|
||||
};
|
||||
|
||||
openFirewall = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Opens up the main STUN port and the TURN allocation range.";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 3478;
|
||||
description = "The port to listen on for STUN messages";
|
||||
};
|
||||
|
||||
lowestPort = mkOption {
|
||||
type = types.port;
|
||||
default = 49152;
|
||||
description = "The lowest port to use in TURN allocation";
|
||||
};
|
||||
|
||||
highestPort = mkOption {
|
||||
type = types.port;
|
||||
default = 65535;
|
||||
description = "The highest port to use in TURN allocation";
|
||||
};
|
||||
|
||||
apiUrl = mkOption {
|
||||
type = types.strMatching "^wss://.+/$";
|
||||
example = "wss://firezone.example.com/api/";
|
||||
description = ''
|
||||
The URL of your firezone server's API. This should be the same
|
||||
as your server's setting for {option}`services.firezone.server.settings.api.externalUrl`,
|
||||
but with `wss://` instead of `https://`.
|
||||
'';
|
||||
};
|
||||
|
||||
tokenFile = mkOption {
|
||||
type = types.path;
|
||||
example = "/run/secrets/firezone-relay-token";
|
||||
description = ''
|
||||
A file containing the firezone relay token. Do not use a nix-store path here
|
||||
as it will make the token publicly readable!
|
||||
|
||||
This file will be passed via systemd credentials, it should only be accessible
|
||||
by the root user.
|
||||
'';
|
||||
};
|
||||
|
||||
logLevel = mkOption {
|
||||
type = types.str;
|
||||
default = "info";
|
||||
description = ''
|
||||
The log level for the firezone application. See
|
||||
[RUST_LOG](https://docs.rs/env_logger/latest/env_logger/#enabling-logging)
|
||||
for the format.
|
||||
'';
|
||||
};
|
||||
|
||||
enableTelemetry = mkEnableOption "telemetry";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.publicIpv4 != null || cfg.publicIpv6 != null;
|
||||
message = "At least one of `services.firezone.relay.publicIpv4` and `services.firezone.relay.publicIpv6` must be set";
|
||||
}
|
||||
];
|
||||
|
||||
networking.firewall.allowedUDPPorts = mkIf cfg.openFirewall [ cfg.port ];
|
||||
networking.firewall.allowedUDPPortRanges = mkIf cfg.openFirewall [
|
||||
{
|
||||
from = cfg.lowestPort;
|
||||
to = cfg.highestPort;
|
||||
}
|
||||
];
|
||||
|
||||
systemd.services.firezone-relay = {
|
||||
description = "relay service for the Firezone zero-trust access platform";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
path = [ pkgs.util-linux ];
|
||||
script = ''
|
||||
# If FIREZONE_ID is not given by the user, use a persisted (or newly generated) uuid.
|
||||
if [[ -z "''${FIREZONE_ID:-}" ]]; then
|
||||
if [[ ! -e relay_id ]]; then
|
||||
uuidgen -r > relay_id
|
||||
fi
|
||||
export FIREZONE_ID=$(< relay_id)
|
||||
fi
|
||||
|
||||
export FIREZONE_TOKEN=$(< "$CREDENTIALS_DIRECTORY/firezone-token")
|
||||
exec ${getExe cfg.package}
|
||||
'';
|
||||
|
||||
environment = {
|
||||
FIREZONE_API_URL = cfg.apiUrl;
|
||||
FIREZONE_NAME = cfg.name;
|
||||
FIREZONE_TELEMETRY = boolToString cfg.enableTelemetry;
|
||||
|
||||
PUBLIC_IP4_ADDR = cfg.publicIpv4;
|
||||
PUBLIC_IP6_ADDR = cfg.publicIpv6;
|
||||
|
||||
LISTEN_PORT = toString cfg.port;
|
||||
LOWEST_PORT = toString cfg.lowestPort;
|
||||
HIGHEST_PORT = toString cfg.highestPort;
|
||||
|
||||
RUST_LOG = cfg.logLevel;
|
||||
LOG_FORMAT = "human";
|
||||
};
|
||||
|
||||
serviceConfig = {
|
||||
Type = "exec";
|
||||
DynamicUser = true;
|
||||
User = "firezone-relay";
|
||||
LoadCredential = [ "firezone-token:${cfg.tokenFile}" ];
|
||||
|
||||
StateDirectory = "firezone-relay";
|
||||
WorkingDirectory = "/var/lib/firezone-relay";
|
||||
|
||||
Restart = "on-failure";
|
||||
RestartSec = 10;
|
||||
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateMounts = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = false;
|
||||
ProcSubset = "pid";
|
||||
ProtectClock = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectProc = "invisible";
|
||||
ProtectSystem = "strict";
|
||||
RestrictAddressFamilies = [
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
"AF_NETLINK"
|
||||
];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = "@system-service";
|
||||
UMask = "077";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
oddlama
|
||||
patrickdag
|
||||
];
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,14 +13,13 @@ let
|
||||
'';
|
||||
};
|
||||
zone_type = mkOption {
|
||||
type = types.enum [ "Primary" "Secondary" "Hint" "Forward" ];
|
||||
type = types.enum [ "Primary" "Secondary" "External" ];
|
||||
default = "Primary";
|
||||
description = ''
|
||||
One of:
|
||||
- "Primary" (the master, authority for the zone).
|
||||
- "Secondary" (the slave, replicated from the primary).
|
||||
- "Hint" (a cached zone with recursive resolver abilities).
|
||||
- "Forward" (a cached zone where all requests are forwarded to another resolver).
|
||||
- "External" (a cached zone that queries other nameservers).
|
||||
|
||||
For more details about these zone types, consult the documentation for BIND,
|
||||
though note that hickory-dns supports only a subset of BIND's zone types:
|
||||
@@ -28,13 +27,13 @@ let
|
||||
'';
|
||||
};
|
||||
file = mkOption {
|
||||
type = types.either types.path types.str;
|
||||
default = "${config.zone}.zone";
|
||||
defaultText = literalExpression ''"''${config.zone}.zone"'';
|
||||
type = types.nullOr (types.either types.path types.str);
|
||||
default = if config.zone_type != "External" then "${config.zone}.zone" else null;
|
||||
defaultText = literalExpression ''if config.zone_type != "External" then "''${config.zone}.zone" else null'';
|
||||
description = ''
|
||||
Path to the .zone file.
|
||||
If not fully-qualified, this path will be interpreted relative to the `directory` option.
|
||||
If omitted, defaults to the value of the `zone` option suffixed with ".zone".
|
||||
If omitted, defaults to the value of the `zone` option suffixed with ".zone" when `zone_type` isn't External; otherwise, defaults to `null`.
|
||||
'';
|
||||
};
|
||||
};
|
||||
@@ -82,7 +81,7 @@ in
|
||||
configFile = mkOption {
|
||||
type = types.path;
|
||||
default = toml.generate "hickory-dns.toml" (
|
||||
lib.filterAttrsRecursive (_: v: v != null) cfg.settings
|
||||
lib.mapAttrs (_: v: if builtins.isList v then map (v: if builtins.isAttrs v then lib.filterAttrs (_: v: v != null) v else v) v else v) (lib.filterAttrsRecursive (_: v: v != null) cfg.settings)
|
||||
);
|
||||
defaultText = lib.literalExpression ''
|
||||
let toml = pkgs.formats.toml { }; in toml.generate "hickory-dns.toml" cfg.settings
|
||||
|
||||
@@ -34,7 +34,7 @@ in
|
||||
mode = "0600";
|
||||
};
|
||||
|
||||
environment.systemPackages = with pkgs; [ targetcli ];
|
||||
environment.systemPackages = with pkgs; [ targetcli-fb ];
|
||||
|
||||
boot.kernelModules = [
|
||||
"configfs"
|
||||
@@ -52,8 +52,8 @@ in
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${pkgs.python3.pkgs.rtslib}/bin/targetctl restore";
|
||||
ExecStop = "${pkgs.python3.pkgs.rtslib}/bin/targetctl clear";
|
||||
ExecStart = "${lib.getExe pkgs.python3Packages.rtslib-fb} restore";
|
||||
ExecStop = "${lib.getExe pkgs.python3Packages.rtslib-fb} clear";
|
||||
RemainAfterExit = "yes";
|
||||
};
|
||||
};
|
||||
|
||||
@@ -119,6 +119,14 @@ in
|
||||
are mutually exclusive.
|
||||
'';
|
||||
}
|
||||
{
|
||||
assertion = cfg.config != "" || (cfg.settings ? auth && cfg.settings.auth ? type);
|
||||
message = ''
|
||||
Radicale 3.5.0 changed the default value for `auth.type` from `none` to `denyall`.
|
||||
You probably don't want `denyall`, so please set this explicitly.
|
||||
https://github.com/Kozea/Radicale/blob/v3.5.0/CHANGELOG.md
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
warnings =
|
||||
|
||||
@@ -5,7 +5,8 @@ with lib;
|
||||
let
|
||||
|
||||
cfg = config.services.shairport-sync;
|
||||
|
||||
configFormat = pkgs.formats.libconfig {};
|
||||
configFile = configFormat.generate "shairport-sync.conf" cfg.settings;
|
||||
in
|
||||
|
||||
{
|
||||
@@ -29,9 +30,44 @@ in
|
||||
|
||||
package = lib.options.mkPackageOption pkgs "shairport-sync" { };
|
||||
|
||||
settings = mkOption {
|
||||
type = configFormat.type;
|
||||
default = {
|
||||
general.output_backend = "pa";
|
||||
diagnostics.log_verbosity = 1;
|
||||
};
|
||||
example = {
|
||||
general = {
|
||||
name = "NixOS Shairport";
|
||||
output_backend = "pw";
|
||||
};
|
||||
metadata = {
|
||||
enabled = "yes";
|
||||
include_cover_art = "yes";
|
||||
cover_art_cache_directory = "/tmp/shairport-sync/.cache/coverart";
|
||||
pipe_name = "/tmp/shairport-sync-metadata";
|
||||
pipe_timeout = 5000;
|
||||
};
|
||||
mqtt = {
|
||||
enabled = "yes";
|
||||
hostname = "mqtt.server.domain.example";
|
||||
port = 1883;
|
||||
publish_parsed = "yes";
|
||||
publish_cover = "yes";
|
||||
};
|
||||
};
|
||||
description = ''
|
||||
Configuration options for Shairport-Sync.
|
||||
|
||||
See the example [shairport-sync.conf][example-file] for possible options.
|
||||
|
||||
[example-file]: https://github.com/mikebrady/shairport-sync/blob/master/scripts/shairport-sync.conf
|
||||
'';
|
||||
};
|
||||
|
||||
arguments = mkOption {
|
||||
type = types.str;
|
||||
default = "-v -o pa";
|
||||
default = "";
|
||||
description = ''
|
||||
Arguments to pass to the daemon. Defaults to a local pulseaudio
|
||||
server.
|
||||
@@ -77,6 +113,11 @@ in
|
||||
services.avahi.publish.enable = true;
|
||||
services.avahi.publish.userServices = true;
|
||||
|
||||
services.shairport-sync.settings = {
|
||||
general.output_backend = lib.mkDefault "pa";
|
||||
diagnostics.log_verbosity = lib.mkDefault 1;
|
||||
};
|
||||
|
||||
users = {
|
||||
users.${cfg.user} = {
|
||||
description = "Shairport user";
|
||||
@@ -108,8 +149,10 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
environment.systemPackages = [ cfg.package ];
|
||||
|
||||
environment = {
|
||||
systemPackages = [ cfg.package ];
|
||||
etc."shairport-sync.conf".source = configFile;
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ let
|
||||
>"$RUNTIME_DIRECTORY/api_key"
|
||||
do sleep 1; done
|
||||
(printf "X-API-Key: "; cat "$RUNTIME_DIRECTORY/api_key") >"$RUNTIME_DIRECTORY/headers"
|
||||
${pkgs.curl}/bin/curl --fail -sSLk -H "@$RUNTIME_DIRECTORY/headers" \
|
||||
${pkgs.curl}/bin/curl -sSLk -H "@$RUNTIME_DIRECTORY/headers" \
|
||||
--retry 1000 --retry-delay 1 --retry-all-errors \
|
||||
"$@"
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ let
|
||||
}
|
||||
// (optionalAttrs (cfg'.user == null) {
|
||||
DynamicUser = true;
|
||||
ProtectHome = true;
|
||||
})
|
||||
// (optionalAttrs (cfg'.additionalGroups != [ ]) {
|
||||
SupplementaryGroups = cfg'.additionalGroups;
|
||||
@@ -63,7 +64,6 @@ let
|
||||
PrivateTmp = true;
|
||||
NoNewPrivileges = true;
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectControlGroups = true;
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.services.prefect;
|
||||
inherit (lib.types)
|
||||
bool
|
||||
str
|
||||
enum
|
||||
path
|
||||
attrsOf
|
||||
nullOr
|
||||
submodule
|
||||
port
|
||||
;
|
||||
|
||||
in
|
||||
{
|
||||
options.services.prefect = {
|
||||
enable = lib.mkOption {
|
||||
type = bool;
|
||||
default = false;
|
||||
description = "enable prefect server and worker services";
|
||||
};
|
||||
|
||||
package = lib.mkPackageOption pkgs "prefect" { };
|
||||
|
||||
host = lib.mkOption {
|
||||
type = str;
|
||||
default = "127.0.0.1";
|
||||
example = "0.0.0.0";
|
||||
description = "Prefect server host";
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = port;
|
||||
default = 4200;
|
||||
description = "Prefect server port";
|
||||
};
|
||||
|
||||
dataDir = lib.mkOption {
|
||||
type = path;
|
||||
default = "/var/lib/prefect-server";
|
||||
description = ''
|
||||
Specify the directory for Prefect.
|
||||
'';
|
||||
};
|
||||
|
||||
database = lib.mkOption {
|
||||
type = enum [
|
||||
"sqlite"
|
||||
"postgres"
|
||||
];
|
||||
default = "sqlite";
|
||||
description = "which database to use for prefect server: sqlite or postgres";
|
||||
};
|
||||
|
||||
databaseHost = lib.mkOption {
|
||||
type = str;
|
||||
default = "localhost";
|
||||
description = "database host for postgres only";
|
||||
};
|
||||
|
||||
databasePort = lib.mkOption {
|
||||
type = str;
|
||||
default = "5432";
|
||||
description = "database port for postgres only";
|
||||
};
|
||||
|
||||
databaseName = lib.mkOption {
|
||||
type = str;
|
||||
default = "prefect";
|
||||
description = "database name for postgres only";
|
||||
};
|
||||
|
||||
databaseUser = lib.mkOption {
|
||||
type = str;
|
||||
default = "postgres";
|
||||
description = "database user for postgres only";
|
||||
};
|
||||
|
||||
databasePasswordFile = lib.mkOption {
|
||||
type = nullOr str;
|
||||
default = null;
|
||||
description = ''
|
||||
path to a file containing e.g.:
|
||||
DBPASSWORD=supersecret
|
||||
|
||||
stored outside the nix store, read by systemd as EnvironmentFile.
|
||||
'';
|
||||
};
|
||||
|
||||
# now define workerPools as an attribute set of submodules,
|
||||
# each key is the pool name, and the submodule has an installPolicy
|
||||
workerPools = lib.mkOption {
|
||||
type = attrsOf (submodule {
|
||||
options = {
|
||||
installPolicy = lib.mkOption {
|
||||
type = enum [
|
||||
"always"
|
||||
"if-not-present"
|
||||
"never"
|
||||
"prompt"
|
||||
];
|
||||
default = "always";
|
||||
description = "install policy for the worker (always, if-not-present, never, prompt)";
|
||||
};
|
||||
};
|
||||
});
|
||||
default = { };
|
||||
description = ''
|
||||
define a set of worker pools with submodule config. example:
|
||||
workerPools.my-pool = {
|
||||
installPolicy = "never";
|
||||
};
|
||||
'';
|
||||
};
|
||||
|
||||
baseUrl = lib.mkOption {
|
||||
type = nullOr str;
|
||||
default = null;
|
||||
description = "external url when served by a reverse proxy, e.g. https://example.com/prefect";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
# define systemd.services as the server plus any worker definitions
|
||||
systemd.services =
|
||||
{
|
||||
"prefect-server" = {
|
||||
description = "prefect server";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
DynamicUser = true;
|
||||
StateDirectory = "prefect-server";
|
||||
# TODO all my efforts to setup the database url
|
||||
# have failed with some unable to open file
|
||||
Environment = [
|
||||
"PREFECT_HOME=%S/prefect-server"
|
||||
"PREFECT_UI_STATIC_DIRECTORY=%S/prefect-server"
|
||||
"PREFECT_SERVER_ANALYTICS_ENABLED=off"
|
||||
"PREFECT_UI_API_URL=${cfg.baseUrl}/api"
|
||||
"PREFECT_UI_URL=${cfg.baseUrl}"
|
||||
];
|
||||
EnvironmentFile =
|
||||
if cfg.database == "postgres" && cfg.databasePasswordFile != null then
|
||||
[ cfg.databasePasswordFile ]
|
||||
else
|
||||
[ ];
|
||||
|
||||
# ReadWritePaths = [ cfg.dataDir ];
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
PrivateTmp = true;
|
||||
NoNewPrivileges = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
LockPersonality = true;
|
||||
CapabilityBoundingSet = [ ];
|
||||
AmbientCapabilities = [ ];
|
||||
RestrictSUIDSGID = true;
|
||||
RestrictAddressFamilies = [
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
"AF_UNIX"
|
||||
];
|
||||
ProtectKernelTunables = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectControlGroups = true;
|
||||
MemoryAccounting = true;
|
||||
CPUAccounting = true;
|
||||
|
||||
ExecStart = "${pkgs.prefect}/bin/prefect server start --host ${cfg.host} --port ${toString cfg.port}";
|
||||
Restart = "always";
|
||||
WorkingDirectory = cfg.dataDir;
|
||||
};
|
||||
};
|
||||
}
|
||||
// lib.concatMapAttrs (poolName: poolCfg: {
|
||||
# return a partial attr set with one key: "prefect-worker-..."
|
||||
"prefect-worker-${poolName}" = {
|
||||
description = "prefect worker for pool '${poolName}'";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network.target" ];
|
||||
|
||||
environment.systemPackages = cfg.package;
|
||||
|
||||
serviceConfig = {
|
||||
DynamicUser = true;
|
||||
StateDirectory = "prefect-worker-${poolName}";
|
||||
Environment = [
|
||||
"PREFECT_HOME=%S/prefect-worker-${poolName}"
|
||||
"PREFECT_API_URL=${cfg.baseUrl}/api"
|
||||
];
|
||||
ProtectSystem = "strict";
|
||||
ProtectHome = true;
|
||||
PrivateTmp = true;
|
||||
NoNewPrivileges = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
LockPersonality = true;
|
||||
CapabilityBoundingSet = [ ];
|
||||
AmbientCapabilities = [ ];
|
||||
RestrictSUIDSGID = true;
|
||||
RestrictAddressFamilies = [
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
"AF_UNIX"
|
||||
];
|
||||
ProtectKernelTunables = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectControlGroups = true;
|
||||
MemoryAccounting = true;
|
||||
CPUAccounting = true;
|
||||
ExecStart = ''
|
||||
${pkgs.prefect}/bin/prefect worker start \
|
||||
--pool ${poolName} \
|
||||
--type process \
|
||||
--install-policy ${poolCfg.installPolicy}
|
||||
'';
|
||||
Restart = "always";
|
||||
};
|
||||
};
|
||||
}) cfg.workerPools;
|
||||
};
|
||||
}
|
||||
@@ -51,6 +51,7 @@ let
|
||||
cfg.provision.idmAdminPasswordFile
|
||||
cfg.provision.adminPasswordFile
|
||||
]
|
||||
++ optional (cfg.provision.extraJsonFile != null) cfg.provision.extraJsonFile
|
||||
++ mapAttrsToList (_: x: x.basicSecretFile) cfg.provision.systems.oauth2
|
||||
);
|
||||
secretDirectories = unique (
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
options.services.paretosecurity = {
|
||||
enable = lib.mkEnableOption "[ParetoSecurity](https://paretosecurity.com) [agent](https://github.com/ParetoSecurity/agent) and its root helper";
|
||||
package = lib.mkPackageOption pkgs "paretosecurity" { };
|
||||
trayIcon = lib.mkEnableOption "tray icon for ParetoSecurity";
|
||||
};
|
||||
|
||||
config = lib.mkIf config.services.paretosecurity.enable {
|
||||
@@ -39,5 +40,12 @@
|
||||
};
|
||||
};
|
||||
|
||||
systemd.user.services."paretosecurity-trayicon" = lib.mkIf config.services.paretosecurity.trayIcon {
|
||||
wantedBy = [ "graphical-session.target" ];
|
||||
serviceConfig = {
|
||||
ExecStart = "${config.services.paretosecurity.package}/bin/paretosecurity trayicon";
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@@ -128,10 +128,29 @@ let
|
||||
|
||||
siteOpts = { options, config, lib, name, ... }:
|
||||
{
|
||||
# TODO: Remove in time for 25.11 and/or simplify once https://github.com/NixOS/nixpkgs/issues/96006 is fixed
|
||||
imports = [
|
||||
({config, options, ... }: let
|
||||
removalNote = "The option has had no effect for 3+ years. There is no replacement available.";
|
||||
optPath = lib.options.showOption [ "services" "dokuwiki" "sites" name "enable" ];
|
||||
in {
|
||||
options.enable = mkOption {
|
||||
visible = false;
|
||||
apply = x: throw "The option `${optPath}' can no longer be used since it's been removed. ${removalNote}";
|
||||
};
|
||||
config.assertions = [
|
||||
{
|
||||
assertion = !options.enable.isDefined;
|
||||
message = ''
|
||||
The option definition `${optPath}' in ${showFiles options.enable.files} no longer has any effect; please remove it.
|
||||
${removalNote}
|
||||
'';
|
||||
}
|
||||
];
|
||||
})
|
||||
];
|
||||
|
||||
options = {
|
||||
enable = mkEnableOption "DokuWiki web application";
|
||||
|
||||
package = mkPackageOption pkgs "dokuwiki" { };
|
||||
|
||||
stateDir = mkOption {
|
||||
@@ -342,6 +361,13 @@ let
|
||||
'';
|
||||
};
|
||||
|
||||
# TODO: Remove when no submodule-level assertions are needed anymore
|
||||
assertions = mkOption {
|
||||
type = types.listOf types.unspecified;
|
||||
default = [ ];
|
||||
visible = false;
|
||||
internal = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
@@ -374,6 +400,8 @@ in
|
||||
|
||||
# implementation
|
||||
config = mkIf (eachSite != {}) (mkMerge [{
|
||||
# TODO: Remove when no submodule-level assertions are needed anymore
|
||||
assertions = flatten (mapAttrsToList (_: cfg: cfg.assertions) eachSite);
|
||||
|
||||
services.phpfpm.pools = mapAttrs' (hostName: cfg: (
|
||||
nameValuePair "dokuwiki-${hostName}" {
|
||||
|
||||
@@ -75,9 +75,12 @@ in
|
||||
);
|
||||
|
||||
# Hardening
|
||||
AmbientCapabilities = "";
|
||||
CapabilityBoundingSet = [ "" ];
|
||||
DeviceAllow = [ "" ];
|
||||
DevicePolicy = "closed";
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateDevices = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = true;
|
||||
@@ -91,15 +94,19 @@ in
|
||||
ProtectKernelTunables = true;
|
||||
ProtectProc = "invisible";
|
||||
ProtectSystem = "strict";
|
||||
RestrictAddressFamilies = [
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
"AF_UNIX"
|
||||
];
|
||||
RemoveIPC = true;
|
||||
RestrictAddressFamilies = [ "AF_INET AF_INET6 AF_UNIX" ];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = [
|
||||
"@system-service"
|
||||
"~@privileged"
|
||||
"~@resources"
|
||||
"setrlimit"
|
||||
];
|
||||
UMask = "0077";
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -102,24 +102,12 @@ in
|
||||
phpPackage = cfg.phpPackage.buildEnv {
|
||||
extensions =
|
||||
{ all, enabled }:
|
||||
with all;
|
||||
[
|
||||
enabled
|
||||
++ (with all; [
|
||||
apcu
|
||||
ctype
|
||||
curl
|
||||
dom
|
||||
exif
|
||||
filter
|
||||
gd
|
||||
mbstring
|
||||
opcache
|
||||
openssl
|
||||
session
|
||||
simplexml
|
||||
xml
|
||||
yaml
|
||||
zip
|
||||
];
|
||||
]);
|
||||
|
||||
extraConfig = generators.toKeyValue { mkKeyValue = generators.mkKeyValueDefault { } " = "; } {
|
||||
output_buffering = "0";
|
||||
|
||||
@@ -90,6 +90,7 @@ in
|
||||
enum
|
||||
package
|
||||
port
|
||||
listOf
|
||||
;
|
||||
|
||||
assertStringPath =
|
||||
@@ -288,6 +289,25 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
realmFiles = mkOption {
|
||||
type = listOf path;
|
||||
example = lib.literalExpression ''
|
||||
[
|
||||
./some/realm.json
|
||||
./another/realm.json
|
||||
]
|
||||
'';
|
||||
default = [ ];
|
||||
description = ''
|
||||
Realm files that the server is going to import during startup.
|
||||
If a realm already exists in the server, the import operation is
|
||||
skipped. Importing the master realm is not supported. All files are
|
||||
expected to be in `json` format. See the
|
||||
[documentation](https://www.keycloak.org/server/importExport) for
|
||||
further information.
|
||||
'';
|
||||
};
|
||||
|
||||
settings = mkOption {
|
||||
type = lib.types.submodule {
|
||||
freeformType = attrsOf (
|
||||
@@ -644,6 +664,24 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.tmpfiles.settings."10-keycloak" =
|
||||
let
|
||||
mkTarget =
|
||||
file:
|
||||
let
|
||||
baseName = builtins.baseNameOf file;
|
||||
name = if lib.hasSuffix ".json" baseName then baseName else "${baseName}.json";
|
||||
in
|
||||
"/run/keycloak/data/import/${name}";
|
||||
settingsList = map (f: {
|
||||
name = mkTarget f;
|
||||
value = {
|
||||
"L+".argument = "${f}";
|
||||
};
|
||||
}) cfg.realmFiles;
|
||||
in
|
||||
builtins.listToAttrs settingsList;
|
||||
|
||||
systemd.services.keycloak =
|
||||
let
|
||||
databaseServices =
|
||||
@@ -725,7 +763,7 @@ in
|
||||
cp $CREDENTIALS_DIRECTORY/ssl_{cert,key} /run/keycloak/ssl/
|
||||
''
|
||||
+ ''
|
||||
kc.sh --verbose start --optimized
|
||||
kc.sh --verbose start --optimized ${lib.optionalString (cfg.realmFiles != [ ]) "--import-realm"}
|
||||
'';
|
||||
};
|
||||
|
||||
|
||||
@@ -320,9 +320,17 @@ in
|
||||
|
||||
umask $oldUmask
|
||||
|
||||
# Ensure that our local.yaml is valid (see kimai:reload command).
|
||||
${pkg hostName cfg}/bin/console lint:yaml --parse-tags \
|
||||
${pkg hostName cfg}/share/php/kimai/config
|
||||
|
||||
# Run kimai:install to ensure database is created or updated.
|
||||
# Note that kimai:update is an alias to kimai:install.
|
||||
${pkg hostName cfg}/bin/console kimai:install
|
||||
${pkg hostName cfg}/bin/console kimai:install --no-cache
|
||||
# Clear cache and warmup cache separately, to avoid "Cannot declare
|
||||
# class App\Entity\Timesheet" error on first init after upgrade.
|
||||
${pkg hostName cfg}/bin/console cache:clear --env=prod
|
||||
${pkg hostName cfg}/bin/console cache:warmup --env=prod
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
|
||||
@@ -398,6 +398,7 @@ in
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/lib/mobilizon/sitemap 700 mobilizon mobilizon - -"
|
||||
"d /var/lib/mobilizon/uploads/exports/csv 700 mobilizon mobilizon - -"
|
||||
"Z /var/lib/mobilizon 700 mobilizon mobilizon - -"
|
||||
];
|
||||
|
||||
@@ -159,18 +159,25 @@ in
|
||||
Group = config.users.users.wakapi.group;
|
||||
|
||||
DynamicUser = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = true;
|
||||
PrivateDevices = true;
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
ProtectClock = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectControlGroups = true;
|
||||
NoNewPrivileges = true;
|
||||
ProtectProc = "invisible";
|
||||
ProtectSystem = "strict";
|
||||
ProtectSystem = "full";
|
||||
RestrictAddressFamilies = [
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
"AF_UNIX"
|
||||
];
|
||||
CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
|
||||
@@ -107,9 +107,11 @@ in
|
||||
ExecStart = lib.getExe cfg.package;
|
||||
|
||||
# Hardening
|
||||
AmbientCapabilities = "";
|
||||
CapabilityBoundingSet = [ "" ];
|
||||
DeviceAllow = [ "" ];
|
||||
DevicePolicy = "closed";
|
||||
LockPersonality = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateDevices = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = true;
|
||||
@@ -123,15 +125,18 @@ in
|
||||
ProtectKernelTunables = true;
|
||||
ProtectProc = "invisible";
|
||||
ProtectSystem = "strict";
|
||||
RestrictAddressFamilies = [
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
"AF_UNIX"
|
||||
];
|
||||
RemoveIPC = true;
|
||||
RestrictAddressFamilies = [ "AF_INET AF_INET6 AF_UNIX AF_NETLINK" ];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = [
|
||||
"@system-service"
|
||||
"~@privileged"
|
||||
"~@resources"
|
||||
];
|
||||
UMask = "0077";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -29,10 +29,12 @@ def config(*path: List[str]) -> Optional[Any]:
|
||||
|
||||
|
||||
def get_system_path(profile: str = 'system', gen: Optional[str] = None, spec: Optional[str] = None) -> str:
|
||||
basename = f'{profile}-{gen}-link' if gen is not None else profile
|
||||
profiles_dir = '/nix/var/nix/profiles'
|
||||
if profile == 'system':
|
||||
result = os.path.join('/nix', 'var', 'nix', 'profiles', 'system')
|
||||
result = os.path.join(profiles_dir, basename)
|
||||
else:
|
||||
result = os.path.join('/nix', 'var', 'nix', 'profiles', 'system-profiles', profile + f'-{gen}-link' if gen is not None else '')
|
||||
result = os.path.join(profiles_dir, 'system-profiles', basename)
|
||||
|
||||
if spec is not None:
|
||||
result = os.path.join(result, 'specialisation', spec)
|
||||
@@ -169,8 +171,8 @@ def generate_config_entry(profile: str, gen: str) -> str:
|
||||
boot_spec = bootjson_to_bootspec(boot_json)
|
||||
|
||||
entry = config_entry(2, boot_spec, f'Generation {gen}', time)
|
||||
for spec in boot_spec.specialisations:
|
||||
entry += config_entry(2, boot_spec, f'Generation {gen}, Specialisation {spec}', str(time))
|
||||
for spec, spec_boot_spec in boot_spec.specialisations.items():
|
||||
entry += config_entry(2, spec_boot_spec, f'Generation {gen}, Specialisation {spec}', str(time))
|
||||
return entry
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ let
|
||||
kernel = config.system.modulesTree;
|
||||
firmware = config.hardware.firmware;
|
||||
allowMissing = false;
|
||||
inherit (config.boot.initrd) extraFirmwarePaths;
|
||||
};
|
||||
|
||||
|
||||
@@ -484,6 +485,14 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
boot.initrd.extraFirmwarePaths = mkOption {
|
||||
default = [ ];
|
||||
type = types.listOf types.str;
|
||||
description = ''
|
||||
Other firmware files (relative to `"''${config.hardware.firmware}/lib/firmware"`) to include in the final initrd we are building.
|
||||
'';
|
||||
};
|
||||
|
||||
boot.initrd.checkJournalingFS = mkOption {
|
||||
default = true;
|
||||
type = types.bool;
|
||||
|
||||
@@ -104,7 +104,7 @@ let
|
||||
"sleep.target"
|
||||
"hybrid-sleep.target"
|
||||
"systemd-hibernate.service"
|
||||
"systemd-hibernate-clear.service"
|
||||
] ++ (lib.optional cfg.package.withEfi "systemd-hibernate-clear.service") ++ [
|
||||
"systemd-hybrid-sleep.service"
|
||||
"systemd-suspend.service"
|
||||
"systemd-suspend-then-hibernate.service"
|
||||
@@ -130,11 +130,13 @@ let
|
||||
"systemd-ask-password-wall.service"
|
||||
|
||||
# Varlink APIs
|
||||
] ++ lib.optionals cfg.package.withBootloader [
|
||||
"systemd-bootctl@.service"
|
||||
"systemd-bootctl.socket"
|
||||
] ++ [
|
||||
"systemd-creds@.service"
|
||||
"systemd-creds.socket"
|
||||
] ++ lib.optional cfg.package.withTpm2Tss [
|
||||
] ++ lib.optional cfg.package.withTpm2Units [
|
||||
"systemd-pcrlock@.service"
|
||||
"systemd-pcrlock.socket"
|
||||
] ++ [
|
||||
|
||||
@@ -105,6 +105,7 @@ let
|
||||
kernel = config.system.modulesTree;
|
||||
firmware = config.hardware.firmware;
|
||||
allowMissing = false;
|
||||
inherit (config.boot.initrd) extraFirmwarePaths;
|
||||
};
|
||||
|
||||
initrdBinEnv = pkgs.buildEnv {
|
||||
@@ -703,10 +704,11 @@ in
|
||||
"|stage1panic"
|
||||
];
|
||||
};
|
||||
script = ''
|
||||
echo c > /proc/sysrq-trigger
|
||||
'';
|
||||
serviceConfig.Type = "oneshot";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${pkgs.coreutils}/bin/echo c";
|
||||
StandardOutput = "file:/proc/sysrq-trigger";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ let
|
||||
cfg = config.systemd.sysusers;
|
||||
userCfg = config.users;
|
||||
|
||||
systemUsers = lib.filterAttrs (_username: opts: !opts.isNormalUser) userCfg.users;
|
||||
systemUsers = lib.filterAttrs (_username: opts: opts.enable && !opts.isNormalUser) userCfg.users;
|
||||
|
||||
sysusersConfig = pkgs.writeTextDir "00-nixos.conf" ''
|
||||
# Type Name ID GECOS Home directory Shell
|
||||
@@ -89,7 +89,7 @@ in
|
||||
}
|
||||
]
|
||||
++ (lib.mapAttrsToList (username: opts: {
|
||||
assertion = !opts.isNormalUser;
|
||||
assertion = opts.enable -> !opts.isNormalUser;
|
||||
message = "${username} is a normal user. systemd-sysusers doesn't create normal users, only system users.";
|
||||
}) userCfg.users)
|
||||
++ lib.mapAttrsToList (username: opts: {
|
||||
|
||||
@@ -27,13 +27,13 @@
|
||||
|
||||
options = {
|
||||
systemd.tpm2.enable = lib.mkEnableOption "systemd TPM2 support" // {
|
||||
default = config.systemd.package.withTpm2Tss;
|
||||
defaultText = "systemd.package.withTpm2Tss";
|
||||
default = config.systemd.package.withTpm2Units;
|
||||
defaultText = "systemd.package.withTpm2Units";
|
||||
};
|
||||
|
||||
boot.initrd.systemd.tpm2.enable = lib.mkEnableOption "systemd initrd TPM2 support" // {
|
||||
default = config.boot.initrd.systemd.package.withTpm2Tss;
|
||||
defaultText = "boot.initrd.systemd.package.withTpm2Tss";
|
||||
default = config.boot.initrd.systemd.package.withTpm2Units;
|
||||
defaultText = "boot.initrd.systemd.package.withTpm2Units";
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -58,6 +58,18 @@ in
|
||||
vmWithBootLoader = lib.mkDefault config.virtualisation.vmVariantWithBootLoader.system.build.vm;
|
||||
};
|
||||
|
||||
virtualisation.vmVariant = {
|
||||
options = {
|
||||
virtualisation.vmVariant = lib.mkOption {
|
||||
apply = _: throw "virtualisation.vmVariant*.virtualisation.vmVariant is not supported";
|
||||
};
|
||||
virtualisation.vmVariantWithBootLoader = lib.mkOption {
|
||||
apply =
|
||||
_: throw "virtualisation.vmVariant*.virtualisation.vmVariantWithBootloader is not supported";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
# uses extendModules
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Systemd services for docker.
|
||||
|
||||
{ config, lib, pkgs, ... }:
|
||||
{ config, lib, utils, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
@@ -271,11 +271,15 @@ in
|
||||
restartIfChanged = false;
|
||||
unitConfig.X-StopOnRemoval = false;
|
||||
|
||||
serviceConfig.Type = "oneshot";
|
||||
|
||||
script = ''
|
||||
${cfg.package}/bin/docker system prune -f ${toString cfg.autoPrune.flags}
|
||||
'';
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = utils.escapeSystemdExecArgs ([
|
||||
(lib.getExe cfg.package)
|
||||
"system"
|
||||
"prune"
|
||||
"-f"
|
||||
] ++ cfg.autoPrune.flags);
|
||||
};
|
||||
|
||||
startAt = optional cfg.autoPrune.enable cfg.autoPrune.dates;
|
||||
after = [ "docker.service" ];
|
||||
|
||||
@@ -306,6 +306,7 @@ in
|
||||
};
|
||||
|
||||
boot.kernelModules = [
|
||||
"br_netfilter"
|
||||
"veth"
|
||||
"xt_comment"
|
||||
"xt_CHECKSUM"
|
||||
|
||||
@@ -178,7 +178,6 @@ let
|
||||
exec ${config.systemd.package}/bin/systemd-nspawn \
|
||||
--keep-unit \
|
||||
-M "$INSTANCE" -D "$root" "''${extraFlags[@]}" \
|
||||
$EXTRA_NSPAWN_FLAGS \
|
||||
--notify-ready=yes \
|
||||
--kill-signal=SIGRTMIN+3 \
|
||||
--bind-ro=/nix/store:/nix/store$NIX_BIND_OPT \
|
||||
@@ -203,6 +202,7 @@ let
|
||||
${optionalString (cfg.tmpfs != null && cfg.tmpfs != [])
|
||||
''--tmpfs=${concatStringsSep " --tmpfs=" cfg.tmpfs}''
|
||||
} \
|
||||
$EXTRA_NSPAWN_FLAGS \
|
||||
${containerInit cfg} "''${SYSTEM_PATH:-/nix/var/nix/profiles/system}/init"
|
||||
'';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
{ config, lib, utils, pkgs, ... }:
|
||||
let
|
||||
cfg = config.virtualisation.podman;
|
||||
json = pkgs.formats.json { };
|
||||
@@ -202,11 +202,15 @@ in
|
||||
restartIfChanged = false;
|
||||
unitConfig.X-StopOnRemoval = false;
|
||||
|
||||
serviceConfig.Type = "oneshot";
|
||||
|
||||
script = ''
|
||||
${cfg.package}/bin/podman system prune -f ${toString cfg.autoPrune.flags}
|
||||
'';
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = utils.escapeSystemdExecArgs ([
|
||||
(lib.getExe cfg.package)
|
||||
"system"
|
||||
"prune"
|
||||
"-f"
|
||||
] ++ cfg.autoPrune.flags);
|
||||
};
|
||||
|
||||
startAt = lib.optional cfg.autoPrune.enable cfg.autoPrune.dates;
|
||||
after = [ "podman.service" ];
|
||||
|
||||
@@ -85,7 +85,6 @@ rec {
|
||||
(onFullSupported "nixos.tests.containers-imperative")
|
||||
(onFullSupported "nixos.tests.containers-ip")
|
||||
(onSystems [ "x86_64-linux" ] "nixos.tests.docker")
|
||||
(onFullSupported "nixos.tests.ecryptfs")
|
||||
(onFullSupported "nixos.tests.env")
|
||||
|
||||
# Way too many manual retries required on Hydra.
|
||||
|
||||
@@ -264,7 +264,7 @@ in {
|
||||
cinnamon = handleTest ./cinnamon.nix {};
|
||||
cinnamon-wayland = handleTest ./cinnamon-wayland.nix {};
|
||||
cjdns = handleTest ./cjdns.nix {};
|
||||
clatd = handleTest ./clatd.nix {};
|
||||
clatd = runTest ./clatd.nix;
|
||||
clickhouse = handleTest ./clickhouse.nix {};
|
||||
cloud-init = handleTest ./cloud-init.nix {};
|
||||
cloud-init-hostname = handleTest ./cloud-init-hostname.nix {};
|
||||
@@ -353,7 +353,7 @@ in {
|
||||
early-mount-options = handleTest ./early-mount-options.nix {};
|
||||
ec2-config = (handleTestOn ["x86_64-linux"] ./ec2.nix {}).boot-ec2-config or {};
|
||||
ec2-nixops = (handleTestOn ["x86_64-linux"] ./ec2.nix {}).boot-ec2-nixops or {};
|
||||
echoip = handleTest ./echoip.nix {};
|
||||
echoip = runTest ./echoip.nix;
|
||||
ecryptfs = handleTest ./ecryptfs.nix {};
|
||||
fscrypt = handleTest ./fscrypt.nix {};
|
||||
fastnetmon-advanced = runTest ./fastnetmon-advanced.nix;
|
||||
@@ -425,6 +425,7 @@ in {
|
||||
firewall = handleTest ./firewall.nix { nftables = false; };
|
||||
firewall-nftables = handleTest ./firewall.nix { nftables = true; };
|
||||
fish = runTest ./fish.nix;
|
||||
firezone = handleTest ./firezone/firezone.nix {};
|
||||
flannel = handleTestOn ["x86_64-linux"] ./flannel.nix {};
|
||||
flaresolverr = handleTest ./flaresolverr.nix {};
|
||||
flood = handleTest ./flood.nix {};
|
||||
@@ -432,6 +433,7 @@ in {
|
||||
imports = [ ./firefox.nix ] ;
|
||||
_module.args.firefoxPackage = pkgs.floorp;
|
||||
};
|
||||
fluent-bit = handleTest ./fluent-bit.nix {};
|
||||
fluentd = handleTest ./fluentd.nix {};
|
||||
fluidd = handleTest ./fluidd.nix {};
|
||||
fontconfig-default-fonts = handleTest ./fontconfig-default-fonts.nix {};
|
||||
@@ -969,6 +971,7 @@ in {
|
||||
pppd = handleTest ./pppd.nix {};
|
||||
predictable-interface-names = handleTest ./predictable-interface-names.nix {};
|
||||
pretalx = runTest ./web-apps/pretalx.nix;
|
||||
prefect = runTest ./prefect.nix;
|
||||
pretix = runTest ./web-apps/pretix.nix;
|
||||
printing-socket = handleTest ./printing.nix { socket = true; listenTcp = true; };
|
||||
printing-service = handleTest ./printing.nix { socket = false; listenTcp = true; };
|
||||
@@ -996,7 +999,7 @@ in {
|
||||
qgis = handleTest ./qgis.nix { package = pkgs.qgis; };
|
||||
qgis-ltr = handleTest ./qgis.nix { package = pkgs.qgis-ltr; };
|
||||
qownnotes = handleTest ./qownnotes.nix {};
|
||||
qtile = handleTestOn ["x86_64-linux" "aarch64-linux"] ./qtile/default.nix {};
|
||||
qtile = runTestOn ["x86_64-linux" "aarch64-linux"] ./qtile/default.nix;
|
||||
quake3 = handleTest ./quake3.nix {};
|
||||
quicktun = handleTest ./quicktun.nix {};
|
||||
quickwit = handleTest ./quickwit.nix {};
|
||||
@@ -1005,7 +1008,7 @@ in {
|
||||
radarr = handleTest ./radarr.nix {};
|
||||
radicale = handleTest ./radicale.nix {};
|
||||
radicle = runTest ./radicle.nix;
|
||||
ragnarwm = handleTest ./ragnarwm.nix {};
|
||||
ragnarwm = runTestOn ["x86_64-linux" "aarch64-linux"] ./ragnarwm.nix;
|
||||
rasdaemon = handleTest ./rasdaemon.nix {};
|
||||
rathole = runTest ./rathole.nix;
|
||||
readarr = handleTest ./readarr.nix {};
|
||||
@@ -1049,7 +1052,7 @@ in {
|
||||
scaphandre = handleTest ./scaphandre.nix {};
|
||||
schleuder = handleTest ./schleuder.nix {};
|
||||
scion-freestanding-deployment = handleTest ./scion/freestanding-deployment {};
|
||||
scrutiny = handleTest ./scrutiny.nix {};
|
||||
scrutiny = runTest ./scrutiny.nix;
|
||||
sddm = handleTest ./sddm.nix {};
|
||||
sdl3 = handleTest ./sdl3.nix { };
|
||||
seafile = handleTest ./seafile.nix {};
|
||||
@@ -1273,12 +1276,14 @@ in {
|
||||
vault-dev = handleTest ./vault-dev.nix {};
|
||||
vault-postgresql = handleTest ./vault-postgresql.nix {};
|
||||
vaultwarden = discoverTests (import ./vaultwarden.nix);
|
||||
vdirsyncer = handleTest ./vdirsyncer.nix {};
|
||||
vector = handleTest ./vector {};
|
||||
velocity = runTest ./velocity.nix;
|
||||
vengi-tools = handleTest ./vengi-tools.nix {};
|
||||
victoriametrics = handleTest ./victoriametrics {};
|
||||
vikunja = handleTest ./vikunja.nix {};
|
||||
virtualbox = handleTestOn ["x86_64-linux"] ./virtualbox.nix {};
|
||||
vm-variant = handleTest ./vm-variant.nix {};
|
||||
vscode-remote-ssh = handleTestOn ["x86_64-linux"] ./vscode-remote-ssh.nix {};
|
||||
vscodium = discoverTests (import ./vscodium.nix);
|
||||
vsftpd = handleTest ./vsftpd.nix {};
|
||||
|
||||
+166
-184
@@ -26,160 +26,160 @@
|
||||
# | Route: 192.0.2.0/24 via 100.64.0.1
|
||||
# +------
|
||||
|
||||
import ./make-test-python.nix (
|
||||
{ pkgs, lib, ... }:
|
||||
{ lib, ... }:
|
||||
|
||||
{
|
||||
name = "clatd";
|
||||
meta = with pkgs.lib.maintainers; {
|
||||
maintainers = [
|
||||
hax404
|
||||
jmbaur
|
||||
{
|
||||
name = "clatd";
|
||||
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
hax404
|
||||
jmbaur
|
||||
];
|
||||
|
||||
nodes = {
|
||||
# The server is configured with static IPv4 addresses. RFC 6052 Section 3.1
|
||||
# disallows the mapping of non-global IPv4 addresses like RFC 1918 into the
|
||||
# Well-Known Prefix 64:ff9b::/96. TAYGA also does not allow the mapping of
|
||||
# documentation space (RFC 5737). To circumvent this, 100.64.0.2/24 from
|
||||
# RFC 6589 (Carrier Grade NAT) is used here.
|
||||
# To reach the IPv4 address pool of the NAT64 gateway, there is a static
|
||||
# route configured. In normal cases, where the router would also source NAT
|
||||
# the pool addresses to one IPv4 addresses, this would not be needed.
|
||||
server = {
|
||||
virtualisation.vlans = [
|
||||
2 # towards router
|
||||
];
|
||||
networking = {
|
||||
useDHCP = false;
|
||||
interfaces.eth1 = lib.mkForce { };
|
||||
};
|
||||
systemd.network = {
|
||||
enable = true;
|
||||
networks."vlan1" = {
|
||||
matchConfig.Name = "eth1";
|
||||
address = [
|
||||
"100.64.0.2/24"
|
||||
];
|
||||
routes = [
|
||||
{
|
||||
Destination = "192.0.2.0/24";
|
||||
Gateway = "100.64.0.1";
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
nodes = {
|
||||
# The server is configured with static IPv4 addresses. RFC 6052 Section 3.1
|
||||
# disallows the mapping of non-global IPv4 addresses like RFC 1918 into the
|
||||
# Well-Known Prefix 64:ff9b::/96. TAYGA also does not allow the mapping of
|
||||
# documentation space (RFC 5737). To circumvent this, 100.64.0.2/24 from
|
||||
# RFC 6589 (Carrier Grade NAT) is used here.
|
||||
# To reach the IPv4 address pool of the NAT64 gateway, there is a static
|
||||
# route configured. In normal cases, where the router would also source NAT
|
||||
# the pool addresses to one IPv4 addresses, this would not be needed.
|
||||
server = {
|
||||
virtualisation.vlans = [
|
||||
2 # towards router
|
||||
];
|
||||
networking = {
|
||||
useDHCP = false;
|
||||
interfaces.eth1 = lib.mkForce { };
|
||||
};
|
||||
systemd.network = {
|
||||
enable = true;
|
||||
networks."vlan1" = {
|
||||
matchConfig.Name = "eth1";
|
||||
address = [
|
||||
"100.64.0.2/24"
|
||||
];
|
||||
routes = [
|
||||
# The router is configured with static IPv4 addresses towards the server
|
||||
# and IPv6 addresses towards the client. DNS64 is exposed towards the
|
||||
# client so clatd is able to auto-discover the PLAT prefix. For NAT64, the
|
||||
# Well-Known prefix 64:ff9b::/96 is used. NAT64 is done with TAYGA which
|
||||
# provides the tun-interface nat64 and does the translation over it. The
|
||||
# IPv6 packets are sent to this interfaces and received as IPv4 packets and
|
||||
# vice versa. As TAYGA only translates IPv6 addresses to dedicated IPv4
|
||||
# addresses, it needs a pool of IPv4 addresses which must be at least as
|
||||
# big as the expected amount of clients. In this test, the packets from the
|
||||
# pool are directly routed towards the client. In normal cases, there would
|
||||
# be a second source NAT44 to map all clients behind one IPv4 address.
|
||||
router = {
|
||||
boot.kernel.sysctl = {
|
||||
"net.ipv4.conf.all.forwarding" = 1;
|
||||
"net.ipv6.conf.all.forwarding" = 1;
|
||||
};
|
||||
|
||||
virtualisation.vlans = [
|
||||
2 # towards server
|
||||
3 # towards client
|
||||
];
|
||||
|
||||
networking = {
|
||||
useDHCP = false;
|
||||
useNetworkd = true;
|
||||
firewall.enable = false;
|
||||
interfaces.eth1 = lib.mkForce {
|
||||
ipv4 = {
|
||||
addresses = [
|
||||
{
|
||||
Destination = "192.0.2.0/24";
|
||||
Gateway = "100.64.0.1";
|
||||
address = "100.64.0.1";
|
||||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
interfaces.eth2 = lib.mkForce {
|
||||
ipv6 = {
|
||||
addresses = [
|
||||
{
|
||||
address = "2001:db8::1";
|
||||
prefixLength = 64;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# The router is configured with static IPv4 addresses towards the server
|
||||
# and IPv6 addresses towards the client. DNS64 is exposed towards the
|
||||
# client so clatd is able to auto-discover the PLAT prefix. For NAT64, the
|
||||
# Well-Known prefix 64:ff9b::/96 is used. NAT64 is done with TAYGA which
|
||||
# provides the tun-interface nat64 and does the translation over it. The
|
||||
# IPv6 packets are sent to this interfaces and received as IPv4 packets and
|
||||
# vice versa. As TAYGA only translates IPv6 addresses to dedicated IPv4
|
||||
# addresses, it needs a pool of IPv4 addresses which must be at least as
|
||||
# big as the expected amount of clients. In this test, the packets from the
|
||||
# pool are directly routed towards the client. In normal cases, there would
|
||||
# be a second source NAT44 to map all clients behind one IPv4 address.
|
||||
router = {
|
||||
boot.kernel.sysctl = {
|
||||
"net.ipv4.conf.all.forwarding" = 1;
|
||||
"net.ipv6.conf.all.forwarding" = 1;
|
||||
};
|
||||
|
||||
virtualisation.vlans = [
|
||||
2 # towards server
|
||||
3 # towards client
|
||||
];
|
||||
|
||||
networking = {
|
||||
useDHCP = false;
|
||||
useNetworkd = true;
|
||||
firewall.enable = false;
|
||||
interfaces.eth1 = lib.mkForce {
|
||||
ipv4 = {
|
||||
addresses = [
|
||||
{
|
||||
address = "100.64.0.1";
|
||||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
interfaces.eth2 = lib.mkForce {
|
||||
ipv6 = {
|
||||
addresses = [
|
||||
{
|
||||
address = "2001:db8::1";
|
||||
prefixLength = 64;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.network.networks."40-eth2" = {
|
||||
networkConfig.IPv6SendRA = true;
|
||||
ipv6Prefixes = [ { Prefix = "2001:db8::/64"; } ];
|
||||
ipv6PREF64Prefixes = [ { Prefix = "64:ff9b::/96"; } ];
|
||||
ipv6SendRAConfig = {
|
||||
EmitDNS = true;
|
||||
DNS = "_link_local";
|
||||
};
|
||||
};
|
||||
|
||||
services.resolved.extraConfig = ''
|
||||
DNSStubListener=no
|
||||
'';
|
||||
|
||||
networking.extraHosts = ''
|
||||
192.0.0.171 ipv4only.arpa
|
||||
192.0.0.170 ipv4only.arpa
|
||||
'';
|
||||
|
||||
services.coredns = {
|
||||
enable = true;
|
||||
config = ''
|
||||
.:53 {
|
||||
bind ::
|
||||
hosts /etc/hosts
|
||||
dns64 64:ff9b::/96
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
services.tayga = {
|
||||
enable = true;
|
||||
ipv4 = {
|
||||
address = "192.0.2.0";
|
||||
router = {
|
||||
address = "192.0.2.1";
|
||||
};
|
||||
pool = {
|
||||
address = "192.0.2.0";
|
||||
prefixLength = 24;
|
||||
};
|
||||
};
|
||||
ipv6 = {
|
||||
address = "2001:db8::1";
|
||||
router = {
|
||||
address = "64:ff9b::1";
|
||||
};
|
||||
pool = {
|
||||
address = "64:ff9b::";
|
||||
prefixLength = 96;
|
||||
};
|
||||
};
|
||||
systemd.network.networks."40-eth2" = {
|
||||
networkConfig.IPv6SendRA = true;
|
||||
ipv6Prefixes = [ { Prefix = "2001:db8::/64"; } ];
|
||||
ipv6PREF64Prefixes = [ { Prefix = "64:ff9b::/96"; } ];
|
||||
ipv6SendRAConfig = {
|
||||
EmitDNS = true;
|
||||
DNS = "_link_local";
|
||||
};
|
||||
};
|
||||
|
||||
# The client uses SLAAC to assign IPv6 addresses. To reach the IPv4-only
|
||||
# server, the client starts the clat daemon which starts and configures the
|
||||
# local IPv4 -> IPv6 translation via Tayga after discovering the PLAT
|
||||
# prefix via DNS64.
|
||||
client = {
|
||||
services.resolved.extraConfig = ''
|
||||
DNSStubListener=no
|
||||
'';
|
||||
|
||||
networking.extraHosts = ''
|
||||
192.0.0.171 ipv4only.arpa
|
||||
192.0.0.170 ipv4only.arpa
|
||||
'';
|
||||
|
||||
services.coredns = {
|
||||
enable = true;
|
||||
config = ''
|
||||
.:53 {
|
||||
bind ::
|
||||
hosts /etc/hosts
|
||||
dns64 64:ff9b::/96
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
services.tayga = {
|
||||
enable = true;
|
||||
ipv4 = {
|
||||
address = "192.0.2.0";
|
||||
router = {
|
||||
address = "192.0.2.1";
|
||||
};
|
||||
pool = {
|
||||
address = "192.0.2.0";
|
||||
prefixLength = 24;
|
||||
};
|
||||
};
|
||||
ipv6 = {
|
||||
address = "2001:db8::1";
|
||||
router = {
|
||||
address = "64:ff9b::1";
|
||||
};
|
||||
pool = {
|
||||
address = "64:ff9b::";
|
||||
prefixLength = 96;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# The client uses SLAAC to assign IPv6 addresses. To reach the IPv4-only
|
||||
# server, the client starts the clat daemon which starts and configures the
|
||||
# local IPv4 -> IPv6 translation via Tayga after discovering the PLAT
|
||||
# prefix via DNS64.
|
||||
client =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
virtualisation.vlans = [
|
||||
3 # towards router
|
||||
];
|
||||
@@ -193,62 +193,44 @@ import ./make-test-python.nix (
|
||||
enable = true;
|
||||
networks."vlan1" = {
|
||||
matchConfig.Name = "eth1";
|
||||
|
||||
# NOTE: clatd does not actually use the PREF64 prefix discovered by
|
||||
# systemd-networkd (nor does systemd-networkd do anything with it,
|
||||
# yet), but we set this to confirm it works. See the test script
|
||||
# below.
|
||||
ipv6AcceptRAConfig.UsePREF64 = true;
|
||||
};
|
||||
};
|
||||
|
||||
services.clatd = {
|
||||
enable = true;
|
||||
# NOTE: Perl's Net::DNS resolver does not seem to work well querying
|
||||
# for AAAA records to systemd-resolved's default IPv4 bind address
|
||||
# (127.0.0.53), so we add an IPv6 listener address to systemd-resolved
|
||||
# and tell clatd to use that instead.
|
||||
settings.dns64-servers = "::1";
|
||||
};
|
||||
|
||||
# Allow clatd to find dns server. See comment above.
|
||||
services.resolved.extraConfig = ''
|
||||
DNSStubListenerExtra=::1
|
||||
'';
|
||||
services.clatd.enable = true;
|
||||
|
||||
environment.systemPackages = [ pkgs.mtr ];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
import json
|
||||
testScript = ''
|
||||
import json
|
||||
|
||||
start_all()
|
||||
start_all()
|
||||
|
||||
# wait for all machines to start up
|
||||
for machine in client, router, server:
|
||||
machine.wait_for_unit("network-online.target")
|
||||
# wait for all machines to start up
|
||||
for machine in client, router, server:
|
||||
machine.wait_for_unit("network.target")
|
||||
|
||||
with subtest("Wait for tayga and clatd"):
|
||||
router.wait_for_unit("tayga.service")
|
||||
client.wait_for_unit("clatd.service")
|
||||
# clatd checks if this system has IPv4 connectivity for 10 seconds
|
||||
client.wait_until_succeeds(
|
||||
'journalctl -u clatd -e | grep -q "Starting up TAYGA, using config file"'
|
||||
)
|
||||
with subtest("Wait for tayga and clatd"):
|
||||
router.wait_for_unit("tayga.service")
|
||||
client.wait_for_unit("clatd.service")
|
||||
# clatd checks if this system has IPv4 connectivity for 10 seconds
|
||||
client.wait_until_succeeds(
|
||||
'journalctl -u clatd -e | grep -q "Starting up TAYGA, using config file"'
|
||||
)
|
||||
|
||||
with subtest("networkd exports PREF64 prefix"):
|
||||
assert json.loads(client.succeed("networkctl status eth1 --json=short"))[
|
||||
"NDisc"
|
||||
]["PREF64"][0]["Prefix"] == [0x0, 0x64, 0xFF, 0x9B] + ([0] * 12)
|
||||
with subtest("networkd exports PREF64 prefix"):
|
||||
assert json.loads(client.succeed("networkctl status eth1 --json=short"))[
|
||||
"NDisc"
|
||||
]["PREF64"][0]["Prefix"] == [0x0, 0x64, 0xFF, 0x9B] + ([0] * 12)
|
||||
|
||||
with subtest("Test ICMP"):
|
||||
client.wait_until_succeeds("ping -c 3 100.64.0.2 >&2")
|
||||
with subtest("Test ICMP"):
|
||||
client.wait_until_succeeds("ping -c3 100.64.0.2 >&2")
|
||||
|
||||
with subtest("Test ICMP and show a traceroute"):
|
||||
client.wait_until_succeeds("mtr --show-ips --report-wide 100.64.0.2 >&2")
|
||||
with subtest("Test ICMP and show a traceroute"):
|
||||
client.wait_until_succeeds("mtr --show-ips --report-wide 100.64.0.2 >&2")
|
||||
|
||||
client.log(client.execute("systemd-analyze security clatd.service")[1])
|
||||
'';
|
||||
}
|
||||
)
|
||||
client.log(client.execute("systemd-analyze security clatd.service")[1])
|
||||
'';
|
||||
}
|
||||
|
||||
+23
-24
@@ -1,29 +1,28 @@
|
||||
import ./make-test-python.nix (
|
||||
{ lib, ... }:
|
||||
{
|
||||
name = "echoip";
|
||||
meta.maintainers = with lib.maintainers; [ defelo ];
|
||||
{ lib, ... }:
|
||||
|
||||
nodes.machine = {
|
||||
services.echoip = {
|
||||
enable = true;
|
||||
virtualHost = "echoip.local";
|
||||
};
|
||||
{
|
||||
name = "echoip";
|
||||
meta.maintainers = with lib.maintainers; [ defelo ];
|
||||
|
||||
networking.hosts = {
|
||||
"127.0.0.1" = [ "echoip.local" ];
|
||||
"::1" = [ "echoip.local" ];
|
||||
};
|
||||
nodes.machine = {
|
||||
services.echoip = {
|
||||
enable = true;
|
||||
virtualHost = "echoip.local";
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.wait_for_unit("echoip.service")
|
||||
machine.wait_for_open_port(8080)
|
||||
networking.hosts = {
|
||||
"127.0.0.1" = [ "echoip.local" ];
|
||||
"::1" = [ "echoip.local" ];
|
||||
};
|
||||
};
|
||||
|
||||
resp = machine.succeed("curl -4 http://echoip.local/ip")
|
||||
assert resp.strip() == "127.0.0.1"
|
||||
resp = machine.succeed("curl -6 http://echoip.local/ip")
|
||||
assert resp.strip() == "::1"
|
||||
'';
|
||||
}
|
||||
)
|
||||
testScript = ''
|
||||
machine.wait_for_unit("echoip.service")
|
||||
machine.wait_for_open_port(8080)
|
||||
|
||||
resp = machine.succeed("curl -4 http://echoip.local/ip")
|
||||
assert resp.strip() == "127.0.0.1"
|
||||
resp = machine.succeed("curl -6 http://echoip.local/ip")
|
||||
assert resp.strip() == "::1"
|
||||
'';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
alias Domain.{Repo, Accounts, Auth, Actors, Tokens}
|
||||
|
||||
mappings = case File.read("provision-uuids.json") do
|
||||
{:ok, content} ->
|
||||
case Jason.decode(content) do
|
||||
{:ok, mapping} -> mapping
|
||||
_ -> %{"accounts" => %{}}
|
||||
end
|
||||
_ -> %{"accounts" => %{}}
|
||||
end
|
||||
|
||||
IO.puts("INFO: Fetching account")
|
||||
{:ok, account} = Accounts.fetch_account_by_id_or_slug("main")
|
||||
|
||||
IO.puts("INFO: Fetching email provider")
|
||||
{:ok, email_provider} = Auth.Provider.Query.not_disabled()
|
||||
|> Auth.Provider.Query.by_adapter(:email)
|
||||
|> Auth.Provider.Query.by_account_id(account.id)
|
||||
|> Repo.fetch(Auth.Provider.Query, [])
|
||||
|
||||
temp_admin_actor_email = "firezone-provision@localhost.local"
|
||||
temp_admin_actor_context = %Auth.Context{
|
||||
type: :browser,
|
||||
user_agent: "Unspecified/0.0",
|
||||
remote_ip: {127, 0, 0, 1},
|
||||
remote_ip_location_region: "N/A",
|
||||
remote_ip_location_city: "N/A",
|
||||
remote_ip_location_lat: 0.0,
|
||||
remote_ip_location_lon: 0.0
|
||||
}
|
||||
|
||||
{:ok, temp_admin_actor} =
|
||||
Actors.create_actor(account, %{
|
||||
type: :account_admin_user,
|
||||
name: "Token Provisioning"
|
||||
})
|
||||
|
||||
{:ok, temp_admin_actor_email_identity} =
|
||||
Auth.create_identity(temp_admin_actor, email_provider, %{
|
||||
provider_identifier: temp_admin_actor_email,
|
||||
provider_identifier_confirmation: temp_admin_actor_email
|
||||
})
|
||||
|
||||
{:ok, temp_admin_actor_token} =
|
||||
Auth.create_token(temp_admin_actor_email_identity, temp_admin_actor_context, "temporarynonce", DateTime.utc_now() |> DateTime.add(1, :hour))
|
||||
|
||||
{:ok, temp_admin_subject} =
|
||||
Auth.build_subject(temp_admin_actor_token, temp_admin_actor_context)
|
||||
|
||||
{:ok, relay_group_token} =
|
||||
Tokens.create_token(%{
|
||||
"type" => :relay_group,
|
||||
"expires_at" => DateTime.utc_now() |> DateTime.add(1, :hour),
|
||||
"secret_fragment" => Domain.Crypto.random_token(32, encoder: :hex32),
|
||||
"relay_group_id" => get_in(mappings, ["accounts", "main", "relay_groups", "my-relays"])
|
||||
})
|
||||
|
||||
relay_group_encoded_token = Tokens.encode_fragment!(relay_group_token)
|
||||
IO.puts("Created relay token: #{relay_group_encoded_token}")
|
||||
File.write("relay_token.txt", relay_group_encoded_token)
|
||||
|
||||
{:ok, gateway_group_token} =
|
||||
Tokens.create_token(%{
|
||||
"type" => :gateway_group,
|
||||
"expires_at" => DateTime.utc_now() |> DateTime.add(1, :hour),
|
||||
"secret_fragment" => Domain.Crypto.random_token(32, encoder: :hex32),
|
||||
"account_id" => get_in(mappings, ["accounts", "main", "id"]),
|
||||
"gateway_group_id" => get_in(mappings, ["accounts", "main", "gateway_groups", "site"])
|
||||
}, temp_admin_subject)
|
||||
|
||||
gateway_group_encoded_token = Tokens.encode_fragment!(gateway_group_token)
|
||||
IO.puts("Created gateway group token: #{gateway_group_encoded_token}")
|
||||
File.write("gateway_token.txt", gateway_group_encoded_token)
|
||||
|
||||
{:ok, service_account_actor_token} =
|
||||
Tokens.create_token(%{
|
||||
"type" => :client,
|
||||
"expires_at" => DateTime.utc_now() |> DateTime.add(1, :hour),
|
||||
"secret_fragment" => Domain.Crypto.random_token(32, encoder: :hex32),
|
||||
"account_id" => get_in(mappings, ["accounts", "main", "id"]),
|
||||
"actor_id" => get_in(mappings, ["accounts", "main", "actors", "client"])
|
||||
})
|
||||
|
||||
service_account_actor_encoded_token = Tokens.encode_fragment!(service_account_actor_token)
|
||||
IO.puts("Created service actor token: #{service_account_actor_encoded_token}")
|
||||
File.write("client_token.txt", service_account_actor_encoded_token)
|
||||
@@ -0,0 +1,349 @@
|
||||
import ../make-test-python.nix (
|
||||
{ pkgs, ... }:
|
||||
let
|
||||
certs = import ../common/acme/server/snakeoil-certs.nix;
|
||||
domain = certs.domain;
|
||||
in
|
||||
{
|
||||
name = "firezone";
|
||||
meta.maintainers = with pkgs.lib.maintainers; [ oddlama ];
|
||||
|
||||
nodes = {
|
||||
server =
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
security.pki.certificateFiles = [ certs.ca.cert ];
|
||||
|
||||
networking.extraHosts = ''
|
||||
${config.networking.primaryIPAddress} ${domain}
|
||||
${config.networking.primaryIPv6Address} ${domain}
|
||||
'';
|
||||
|
||||
networking.firewall.allowedTCPPorts = [
|
||||
80
|
||||
443
|
||||
];
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts.${domain} = {
|
||||
sslCertificate = certs.${domain}.cert;
|
||||
sslCertificateKey = certs.${domain}.key;
|
||||
};
|
||||
};
|
||||
|
||||
services.firezone.server = {
|
||||
enable = true;
|
||||
enableLocalDB = true;
|
||||
nginx.enable = true;
|
||||
|
||||
# Doesn't need to work for this test, but needs to be configured
|
||||
# otherwise the server will not start.
|
||||
smtp = {
|
||||
from = "firezone@example.com";
|
||||
host = "mail.localhost";
|
||||
port = 465;
|
||||
implicitTls = true;
|
||||
username = "firezone@example.com";
|
||||
passwordFile = pkgs.writeText "tmpmailpasswd" "supermailpassword";
|
||||
};
|
||||
|
||||
provision = {
|
||||
enable = true;
|
||||
accounts.main = {
|
||||
name = "My Account";
|
||||
relayGroups.my-relays.name = "Relays";
|
||||
gatewayGroups.site.name = "Site";
|
||||
actors = {
|
||||
admin = {
|
||||
type = "account_admin_user";
|
||||
name = "Admin";
|
||||
email = "admin@example.com";
|
||||
};
|
||||
client = {
|
||||
type = "service_account";
|
||||
name = "A client";
|
||||
email = "client@example.com";
|
||||
};
|
||||
};
|
||||
resources.res1 = {
|
||||
type = "dns";
|
||||
name = "Dns Resource";
|
||||
address = "resource.example.com";
|
||||
gatewayGroups = [ "site" ];
|
||||
filters = [
|
||||
{ protocol = "icmp"; }
|
||||
{
|
||||
protocol = "tcp";
|
||||
ports = [ 80 ];
|
||||
}
|
||||
];
|
||||
};
|
||||
resources.res2 = {
|
||||
type = "ip";
|
||||
name = "Ip Resource";
|
||||
address = "172.20.2.1";
|
||||
gatewayGroups = [ "site" ];
|
||||
};
|
||||
resources.res3 = {
|
||||
type = "cidr";
|
||||
name = "Cidr Resource";
|
||||
address = "172.20.1.0/24";
|
||||
gatewayGroups = [ "site" ];
|
||||
};
|
||||
policies.pol1 = {
|
||||
description = "Allow anyone res1 access";
|
||||
group = "everyone";
|
||||
resource = "res1";
|
||||
};
|
||||
policies.pol2 = {
|
||||
description = "Allow anyone res2 access";
|
||||
group = "everyone";
|
||||
resource = "res2";
|
||||
};
|
||||
policies.pol3 = {
|
||||
description = "Allow anyone res3 access";
|
||||
group = "everyone";
|
||||
resource = "res3";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
api.externalUrl = "https://${domain}/api/";
|
||||
web.externalUrl = "https://${domain}/";
|
||||
};
|
||||
|
||||
systemd.services.firezone-server-domain.postStart = lib.mkAfter ''
|
||||
${lib.getExe config.services.firezone.server.domain.package} rpc 'Code.eval_file("${./create-tokens.exs}")'
|
||||
'';
|
||||
};
|
||||
|
||||
relay =
|
||||
{
|
||||
nodes,
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{
|
||||
security.pki.certificateFiles = [ certs.ca.cert ];
|
||||
networking.extraHosts = ''
|
||||
${nodes.server.networking.primaryIPAddress} ${domain}
|
||||
${nodes.server.networking.primaryIPv6Address} ${domain}
|
||||
'';
|
||||
|
||||
services.firezone.relay = {
|
||||
enable = true;
|
||||
logLevel = "debug";
|
||||
name = "test-relay";
|
||||
apiUrl = "wss://${domain}/api/";
|
||||
tokenFile = "/tmp/shared/relay_token.txt";
|
||||
publicIpv4 = config.networking.primaryIPAddress;
|
||||
publicIpv6 = config.networking.primaryIPv6Address;
|
||||
openFirewall = true;
|
||||
};
|
||||
|
||||
# Don't auto-start so we can wait until the token was provisioned
|
||||
systemd.services.firezone-relay.wantedBy = lib.mkForce [ ];
|
||||
};
|
||||
|
||||
# A resource that is only connected to the gateway,
|
||||
# allowing us to confirm the VPN works
|
||||
resource = {
|
||||
virtualisation.vlans = [
|
||||
1
|
||||
2
|
||||
];
|
||||
|
||||
networking.interfaces.eth1.ipv4.addresses = [
|
||||
{
|
||||
address = "172.20.1.1";
|
||||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
|
||||
networking.interfaces.eth2.ipv4.addresses = [
|
||||
{
|
||||
address = "172.20.2.1";
|
||||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
|
||||
networking.firewall.allowedTCPPorts = [
|
||||
80
|
||||
];
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts = {
|
||||
"localhost" = {
|
||||
default = true;
|
||||
locations."/".extraConfig = ''
|
||||
return 200 'greetings from the resource';
|
||||
add_header Content-Type text/plain;
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
gateway =
|
||||
{
|
||||
nodes,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{
|
||||
virtualisation.vlans = [
|
||||
1
|
||||
2
|
||||
];
|
||||
|
||||
networking = {
|
||||
interfaces.eth1.ipv4.addresses = [
|
||||
{
|
||||
address = "172.20.1.2";
|
||||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
|
||||
interfaces.eth2.ipv4.addresses = [
|
||||
{
|
||||
address = "172.20.2.2";
|
||||
prefixLength = 24;
|
||||
}
|
||||
];
|
||||
|
||||
firewall.enable = false;
|
||||
nftables.enable = true;
|
||||
nftables.tables."filter".family = "inet";
|
||||
nftables.tables."filter".content = ''
|
||||
chain incoming {
|
||||
type filter hook input priority 0; policy accept;
|
||||
}
|
||||
|
||||
chain postrouting {
|
||||
type nat hook postrouting priority srcnat; policy accept;
|
||||
meta protocol ip iifname "tun-firezone" oifname { "eth1", "eth2" } masquerade random
|
||||
}
|
||||
|
||||
chain forward {
|
||||
type filter hook forward priority 0; policy drop;
|
||||
iifname "tun-firezone" accept
|
||||
oifname "tun-firezone" accept
|
||||
}
|
||||
|
||||
chain output {
|
||||
type filter hook output priority 0; policy accept;
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
boot.kernel.sysctl."net.ipv4.ip_forward" = "1";
|
||||
# boot.kernel.sysctl."net.ipv4.conf.all.src_valid_mark" = "1";
|
||||
boot.kernel.sysctl."net.ipv6.conf.default.forwarding" = "1";
|
||||
boot.kernel.sysctl."net.ipv6.conf.all.forwarding" = "1";
|
||||
|
||||
security.pki.certificateFiles = [ certs.ca.cert ];
|
||||
networking.extraHosts = ''
|
||||
${nodes.server.networking.primaryIPAddress} ${domain}
|
||||
${nodes.server.networking.primaryIPv6Address} ${domain}
|
||||
172.20.1.1 resource.example.com
|
||||
'';
|
||||
|
||||
services.firezone.gateway = {
|
||||
enable = true;
|
||||
logLevel = "debug";
|
||||
name = "test-gateway";
|
||||
apiUrl = "wss://${domain}/api/";
|
||||
tokenFile = "/tmp/shared/gateway_token.txt";
|
||||
};
|
||||
|
||||
# Don't auto-start so we can wait until the token was provisioned
|
||||
systemd.services.firezone-gateway.wantedBy = lib.mkForce [ ];
|
||||
};
|
||||
|
||||
client =
|
||||
{
|
||||
nodes,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{
|
||||
security.pki.certificateFiles = [ certs.ca.cert ];
|
||||
networking.useNetworkd = true;
|
||||
networking.extraHosts = ''
|
||||
${nodes.server.networking.primaryIPAddress} ${domain}
|
||||
${nodes.server.networking.primaryIPv6Address} ${domain}
|
||||
'';
|
||||
|
||||
services.firezone.headless-client = {
|
||||
enable = true;
|
||||
logLevel = "debug";
|
||||
name = "test-client-somebody";
|
||||
apiUrl = "wss://${domain}/api/";
|
||||
tokenFile = "/tmp/shared/client_token.txt";
|
||||
};
|
||||
|
||||
# Don't auto-start so we can wait until the token was provisioned
|
||||
systemd.services.firezone-headless-client.wantedBy = lib.mkForce [ ];
|
||||
};
|
||||
};
|
||||
|
||||
testScript =
|
||||
{ ... }:
|
||||
''
|
||||
start_all()
|
||||
|
||||
with subtest("Start server"):
|
||||
server.wait_for_unit("firezone.target")
|
||||
server.wait_until_succeeds("curl -Lsf https://${domain} | grep 'Welcome to Firezone'")
|
||||
server.wait_until_succeeds("curl -Ls https://${domain}/api | grep 'Not Found'")
|
||||
|
||||
# Wait for tokens and copy them to shared folder
|
||||
server.wait_for_file("/var/lib/private/firezone/relay_token.txt")
|
||||
server.wait_for_file("/var/lib/private/firezone/gateway_token.txt")
|
||||
server.wait_for_file("/var/lib/private/firezone/client_token.txt")
|
||||
server.succeed("cp /var/lib/private/firezone/*_token.txt /tmp/shared")
|
||||
|
||||
with subtest("Connect relay"):
|
||||
relay.succeed("systemctl start firezone-relay")
|
||||
relay.wait_for_unit("firezone-relay.service")
|
||||
relay.wait_until_succeeds("journalctl --since -2m --unit firezone-relay.service --grep 'Connected to portal.*${domain}'", timeout=30)
|
||||
|
||||
with subtest("Connect gateway"):
|
||||
gateway.succeed("systemctl start firezone-gateway")
|
||||
gateway.wait_for_unit("firezone-gateway.service")
|
||||
gateway.wait_until_succeeds("journalctl --since -2m --unit firezone-gateway.service --grep 'Connected to portal.*${domain}'", timeout=30)
|
||||
relay.wait_until_succeeds("journalctl --since -2m --unit firezone-relay.service --grep 'Created allocation.*IPv4'", timeout=30)
|
||||
relay.wait_until_succeeds("journalctl --since -2m --unit firezone-relay.service --grep 'Created allocation.*IPv6'", timeout=30)
|
||||
|
||||
# Assert both relay ips are known
|
||||
gateway.wait_until_succeeds("journalctl --since -2m --unit firezone-gateway.service --grep 'Updated allocation.*relay_ip4.*Some.*relay_ip6.*Some'", timeout=30)
|
||||
|
||||
with subtest("Connect headless-client"):
|
||||
client.succeed("systemctl start firezone-headless-client")
|
||||
client.wait_for_unit("firezone-headless-client.service")
|
||||
client.wait_until_succeeds("journalctl --since -2m --unit firezone-headless-client.service --grep 'Connected to portal.*${domain}'", timeout=30)
|
||||
client.wait_until_succeeds("journalctl --since -2m --unit firezone-headless-client.service --grep 'Tunnel ready'", timeout=30)
|
||||
|
||||
with subtest("Check DNS based access"):
|
||||
# Check that we can access the resource through the VPN via DNS
|
||||
client.wait_until_succeeds("curl -4 -Lsf http://resource.example.com | grep 'greetings from the resource'")
|
||||
client.wait_until_succeeds("curl -6 -Lsf http://resource.example.com | grep 'greetings from the resource'")
|
||||
|
||||
with subtest("Check CIDR based access"):
|
||||
# Check that we can access the resource through the VPN via CIDR
|
||||
client.wait_until_succeeds("ping -c1 -W1 172.20.1.1")
|
||||
|
||||
with subtest("Check IP based access"):
|
||||
# Check that we can access the resource through the VPN via IP
|
||||
client.wait_until_succeeds("ping -c1 -W1 172.20.2.1")
|
||||
'';
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,40 @@
|
||||
import ./make-test-python.nix (
|
||||
{ lib, pkgs, ... }:
|
||||
{
|
||||
name = "fluent-bit";
|
||||
|
||||
nodes.machine =
|
||||
{ config, pkgs, ... }:
|
||||
{
|
||||
services.fluent-bit = {
|
||||
enable = true;
|
||||
settings = {
|
||||
pipeline = {
|
||||
inputs = [
|
||||
{
|
||||
name = "systemd";
|
||||
systemd_filter = "_SYSTEMD_UNIT=fluent-bit.service";
|
||||
}
|
||||
];
|
||||
outputs = [
|
||||
{
|
||||
name = "file";
|
||||
path = "/var/log/fluent-bit";
|
||||
file = "fluent-bit.out";
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.fluent-bit.serviceConfig.LogsDirectory = "fluent-bit";
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
|
||||
machine.wait_for_unit("fluent-bit.service")
|
||||
machine.wait_for_file("/var/log/fluent-bit/fluent-bit.out")
|
||||
'';
|
||||
}
|
||||
)
|
||||
@@ -40,7 +40,7 @@
|
||||
machine_custom_port.wait_for_open_port(5678)
|
||||
|
||||
soup = BeautifulSoup(machine_default.succeed("curl http://localhost:8080"))
|
||||
expected_version = "${config.nodes.machine_default.services.glance.package.version}"
|
||||
expected_version = "v${config.nodes.machine_default.services.glance.package.version}"
|
||||
assert any(a.text == expected_version for a in soup.select(".footer a"))
|
||||
'';
|
||||
|
||||
|
||||
@@ -25,7 +25,10 @@ import ./make-test-python.nix (
|
||||
machine.wait_for_open_port(80)
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
# This establishes _something_
|
||||
machine.succeed("curl -sSf http://localhost")
|
||||
# The second request creates the database, unsure why both are required
|
||||
machine.succeed("curl -sSf http://localhost/")
|
||||
|
||||
machine.succeed(
|
||||
"curl -c cookies -sSf -X POST http://localhost/login -d 'username=admin&password=admin'"
|
||||
|
||||
@@ -4,46 +4,25 @@ import ./make-test-python.nix (
|
||||
name = "homepage-dashboard";
|
||||
meta.maintainers = with lib.maintainers; [ jnsgruk ];
|
||||
|
||||
nodes.unmanaged_conf =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
services.homepage-dashboard.enable = true;
|
||||
};
|
||||
|
||||
nodes.managed_conf =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
services.homepage-dashboard = {
|
||||
enable = true;
|
||||
settings.title = "test title rodUsEagid"; # something random/unique
|
||||
};
|
||||
nodes.machine = _: {
|
||||
services.homepage-dashboard = {
|
||||
enable = true;
|
||||
settings.title = "test title rodUsEagid"; # something random/unique
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
# Ensure the services are started on unmanaged machine
|
||||
unmanaged_conf.wait_for_unit("homepage-dashboard.service")
|
||||
unmanaged_conf.wait_for_open_port(8082)
|
||||
unmanaged_conf.succeed("curl --fail http://localhost:8082/")
|
||||
|
||||
# Ensure that /etc/homepage-dashboard doesn't exist, and boilerplate
|
||||
# configs are copied into place.
|
||||
unmanaged_conf.fail("test -d /etc/homepage-dashboard")
|
||||
unmanaged_conf.succeed("test -f /var/lib/private/homepage-dashboard/settings.yaml")
|
||||
|
||||
# Ensure the services are started on managed machine
|
||||
managed_conf.wait_for_unit("homepage-dashboard.service")
|
||||
managed_conf.wait_for_open_port(8082)
|
||||
managed_conf.succeed("curl --fail http://localhost:8082/")
|
||||
machine.wait_for_unit("homepage-dashboard.service")
|
||||
machine.wait_for_open_port(8082)
|
||||
machine.succeed("curl --fail http://localhost:8082/")
|
||||
|
||||
# Ensure /etc/homepage-dashboard is created and unmanaged conf location isn't.
|
||||
managed_conf.succeed("test -d /etc/homepage-dashboard")
|
||||
managed_conf.fail("test -f /var/lib/private/homepage-dashboard/settings.yaml")
|
||||
# Ensure /etc/homepage-dashboard is created.
|
||||
machine.succeed("test -d /etc/homepage-dashboard")
|
||||
|
||||
# Ensure that we see the custom title *only in the managed config*
|
||||
page = managed_conf.succeed("curl --fail http://localhost:8082/")
|
||||
page = machine.succeed("curl --fail http://localhost:8082/")
|
||||
assert "test title rodUsEagid" in page, "Custom title not found"
|
||||
page = unmanaged_conf.succeed("curl --fail http://localhost:8082/")
|
||||
assert "test title rodUsEagid" not in page, "Custom title found where it shouldn't be"
|
||||
'';
|
||||
}
|
||||
)
|
||||
|
||||
@@ -306,6 +306,10 @@ import ./make-test-python.nix (
|
||||
provision.succeed('${specialisations}/credentialProvision/bin/switch-to-configuration test')
|
||||
provision_login("${provisionIdmAdminPassword}")
|
||||
|
||||
# Make sure neither password is logged
|
||||
provision.fail("journalctl --since -10m --unit kanidm.service --grep '${provisionAdminPassword}'")
|
||||
provision.fail("journalctl --since -10m --unit kanidm.service --grep '${provisionIdmAdminPassword}'")
|
||||
|
||||
# Test provisioned admin pw
|
||||
out = provision.succeed("KANIDM_PASSWORD=${provisionAdminPassword} kanidm login -D admin")
|
||||
assert_contains(out, "Login Success for admin")
|
||||
|
||||
@@ -3,14 +3,58 @@
|
||||
name = "paretosecurity";
|
||||
meta.maintainers = [ lib.maintainers.zupo ];
|
||||
|
||||
nodes.machine =
|
||||
nodes.terminal =
|
||||
{ config, pkgs, ... }:
|
||||
{
|
||||
imports = [ ./common/user-account.nix ];
|
||||
|
||||
services.paretosecurity.enable = true;
|
||||
};
|
||||
|
||||
# very basic test for now, need to add output asserts
|
||||
nodes.xfce =
|
||||
{ config, pkgs, ... }:
|
||||
{
|
||||
imports = [ ./common/user-account.nix ];
|
||||
|
||||
services.paretosecurity = {
|
||||
enable = true;
|
||||
trayIcon = true;
|
||||
};
|
||||
|
||||
services.xserver.enable = true;
|
||||
services.xserver.displayManager.lightdm.enable = true;
|
||||
services.xserver.desktopManager.xfce.enable = true;
|
||||
|
||||
services.displayManager.autoLogin = {
|
||||
enable = true;
|
||||
user = "alice";
|
||||
};
|
||||
|
||||
environment.systemPackages = [ pkgs.xdotool ];
|
||||
environment.variables.XAUTHORITY = "/home/alice/.Xauthority";
|
||||
|
||||
};
|
||||
|
||||
enableOCR = true;
|
||||
|
||||
testScript = ''
|
||||
machine.wait_until_succeeds("paretosecurity check")
|
||||
terminal.succeed(
|
||||
"su -- alice -c 'paretosecurity check"
|
||||
# Disable some checks that need intricate test setup so that this test
|
||||
# remains simple and fast. Tests for all checks and edge cases available
|
||||
# at https://github.com/ParetoSecurity/agent/tree/main/test/integration
|
||||
+ " --skip c96524f2-850b-4bb9-abc7-517051b6c14e" # SecureBoot
|
||||
+ " --skip 37dee029-605b-4aab-96b9-5438e5aa44d8" # Screen lock
|
||||
+ " --skip 21830a4e-84f1-48fe-9c5b-beab436b2cdb" # Disk encryption
|
||||
+ " --skip 44e4754a-0b42-4964-9cc2-b88b2023cb1e" # Pareto Security is up to date
|
||||
+ " --skip f962c423-fdf5-428a-a57a-827abc9b253e" # Password manager installed
|
||||
+ "'"
|
||||
)
|
||||
|
||||
xfce.wait_for_x()
|
||||
xfce.succeed("xdotool mousemove 850 10")
|
||||
xfce.wait_for_text("Pareto Security")
|
||||
xfce.succeed("xdotool click 1")
|
||||
xfce.wait_for_text("Run Checks")
|
||||
'';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{ lib, ... }:
|
||||
let
|
||||
mainPort = "4200";
|
||||
in
|
||||
{
|
||||
name = "prefect";
|
||||
|
||||
nodes = {
|
||||
machine =
|
||||
{ ... }:
|
||||
{
|
||||
services.prefect = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.start()
|
||||
machine.wait_for_unit("prefect-server.service")
|
||||
machine.wait_for_open_port("${mainPort}")
|
||||
'';
|
||||
|
||||
meta = with lib.maintainers; {
|
||||
maintainers = [ happysalada ];
|
||||
};
|
||||
}
|
||||
@@ -1,54 +1,52 @@
|
||||
import ../make-test-python.nix (
|
||||
{ lib, ... }:
|
||||
{
|
||||
name = "qtile";
|
||||
{ lib, ... }:
|
||||
{
|
||||
name = "qtile";
|
||||
|
||||
meta = {
|
||||
maintainers = with lib.maintainers; [ sigmanificient ];
|
||||
};
|
||||
meta = {
|
||||
maintainers = with lib.maintainers; [ sigmanificient ];
|
||||
};
|
||||
|
||||
nodes.machine =
|
||||
{ pkgs, lib, ... }:
|
||||
let
|
||||
# We create a custom Qtile configuration file that adds a widget from
|
||||
# qtile-extras to the bar. This ensure that the qtile-extras package
|
||||
# also works, and that extraPackages behave as expected.
|
||||
nodes.machine =
|
||||
{ pkgs, lib, ... }:
|
||||
let
|
||||
# We create a custom Qtile configuration file that adds a widget from
|
||||
# qtile-extras to the bar. This ensure that the qtile-extras package
|
||||
# also works, and that extraPackages behave as expected.
|
||||
|
||||
config-deriv = pkgs.callPackage ./config.nix { };
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
../common/x11.nix
|
||||
../common/user-account.nix
|
||||
];
|
||||
test-support.displayManager.auto.user = "alice";
|
||||
config-deriv = pkgs.callPackage ./config.nix { };
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
../common/x11.nix
|
||||
../common/user-account.nix
|
||||
];
|
||||
test-support.displayManager.auto.user = "alice";
|
||||
|
||||
services.xserver.windowManager.qtile = {
|
||||
enable = true;
|
||||
configFile = "${config-deriv}/config.py";
|
||||
extraPackages = ps: [ ps.qtile-extras ];
|
||||
};
|
||||
|
||||
services.displayManager.defaultSession = lib.mkForce "qtile";
|
||||
|
||||
environment.systemPackages = [ pkgs.kitty ];
|
||||
services.xserver.windowManager.qtile = {
|
||||
enable = true;
|
||||
configFile = "${config-deriv}/config.py";
|
||||
extraPackages = ps: [ ps.qtile-extras ];
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
with subtest("ensure x starts"):
|
||||
machine.wait_for_x()
|
||||
machine.wait_for_file("/home/alice/.Xauthority")
|
||||
machine.succeed("xauth merge ~alice/.Xauthority")
|
||||
services.displayManager.defaultSession = lib.mkForce "qtile";
|
||||
|
||||
with subtest("ensure client is available"):
|
||||
machine.succeed("qtile --version")
|
||||
environment.systemPackages = [ pkgs.kitty ];
|
||||
};
|
||||
|
||||
with subtest("ensure we can open a new terminal"):
|
||||
machine.sleep(2)
|
||||
machine.send_key("meta_l-ret")
|
||||
machine.wait_for_window(r"alice.*?machine")
|
||||
machine.sleep(2)
|
||||
machine.screenshot("terminal")
|
||||
'';
|
||||
}
|
||||
)
|
||||
testScript = ''
|
||||
with subtest("ensure x starts"):
|
||||
machine.wait_for_x()
|
||||
machine.wait_for_file("/home/alice/.Xauthority")
|
||||
machine.succeed("xauth merge ~alice/.Xauthority")
|
||||
|
||||
with subtest("ensure client is available"):
|
||||
machine.succeed("qtile --version")
|
||||
|
||||
with subtest("ensure we can open a new terminal"):
|
||||
machine.sleep(2)
|
||||
machine.send_key("meta_l-ret")
|
||||
machine.wait_for_window(r"alice.*?machine")
|
||||
machine.sleep(2)
|
||||
machine.screenshot("terminal")
|
||||
'';
|
||||
}
|
||||
|
||||
+34
-36
@@ -1,40 +1,38 @@
|
||||
import ./make-test-python.nix (
|
||||
{ lib, ... }:
|
||||
{
|
||||
name = "ragnarwm";
|
||||
{ lib, ... }:
|
||||
{
|
||||
name = "ragnarwm";
|
||||
|
||||
meta = {
|
||||
maintainers = with lib.maintainers; [ sigmanificient ];
|
||||
meta = {
|
||||
maintainers = with lib.maintainers; [ sigmanificient ];
|
||||
};
|
||||
|
||||
nodes.machine =
|
||||
{ pkgs, lib, ... }:
|
||||
{
|
||||
imports = [
|
||||
./common/x11.nix
|
||||
./common/user-account.nix
|
||||
];
|
||||
test-support.displayManager.auto.user = "alice";
|
||||
services.displayManager.defaultSession = lib.mkForce "ragnar";
|
||||
services.xserver.windowManager.ragnarwm.enable = true;
|
||||
|
||||
# Setup the default terminal of Ragnar
|
||||
environment.systemPackages = [ pkgs.alacritty ];
|
||||
};
|
||||
|
||||
nodes.machine =
|
||||
{ pkgs, lib, ... }:
|
||||
{
|
||||
imports = [
|
||||
./common/x11.nix
|
||||
./common/user-account.nix
|
||||
];
|
||||
test-support.displayManager.auto.user = "alice";
|
||||
services.displayManager.defaultSession = lib.mkForce "ragnar";
|
||||
services.xserver.windowManager.ragnarwm.enable = true;
|
||||
testScript = ''
|
||||
with subtest("ensure x starts"):
|
||||
machine.wait_for_x()
|
||||
machine.wait_for_file("/home/alice/.Xauthority")
|
||||
machine.succeed("xauth merge ~alice/.Xauthority")
|
||||
|
||||
# Setup the default terminal of Ragnar
|
||||
environment.systemPackages = [ pkgs.alacritty ];
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
with subtest("ensure x starts"):
|
||||
machine.wait_for_x()
|
||||
machine.wait_for_file("/home/alice/.Xauthority")
|
||||
machine.succeed("xauth merge ~alice/.Xauthority")
|
||||
|
||||
with subtest("ensure we can open a new terminal"):
|
||||
# Sleeping a bit before the test, as it may help for sending keys
|
||||
machine.sleep(2)
|
||||
machine.send_key("meta_l-ret")
|
||||
machine.wait_for_window(r"alice.*?machine")
|
||||
machine.sleep(2)
|
||||
machine.screenshot("terminal")
|
||||
'';
|
||||
}
|
||||
)
|
||||
with subtest("ensure we can open a new terminal"):
|
||||
# Sleeping a bit before the test, as it may help for sending keys
|
||||
machine.sleep(2)
|
||||
machine.send_key("meta_l-ret")
|
||||
machine.wait_for_window(r"alice.*?machine")
|
||||
machine.sleep(2)
|
||||
machine.screenshot("terminal")
|
||||
'';
|
||||
}
|
||||
|
||||
+90
-75
@@ -1,83 +1,98 @@
|
||||
import ./make-test-python.nix (
|
||||
{ lib, ... }:
|
||||
{ lib, ... }:
|
||||
|
||||
{
|
||||
name = "scrutiny";
|
||||
meta.maintainers = with lib.maintainers; [ jnsgruk ];
|
||||
{
|
||||
name = "scrutiny";
|
||||
meta.maintainers = with lib.maintainers; [ jnsgruk ];
|
||||
|
||||
nodes = {
|
||||
machine =
|
||||
{
|
||||
self,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{
|
||||
services = {
|
||||
scrutiny.enable = true;
|
||||
scrutiny.collector.enable = true;
|
||||
nodes = {
|
||||
machine =
|
||||
{
|
||||
self,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{
|
||||
services = {
|
||||
scrutiny = {
|
||||
enable = true;
|
||||
settings = {
|
||||
notify.urls = [
|
||||
{
|
||||
_secret = pkgs.writeText "notify-script" "script://${pkgs.writeShellScript "touch-test-file" ''
|
||||
echo "HelloWorld" > /run/scrutiny/hello
|
||||
''}";
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
environment.systemPackages =
|
||||
let
|
||||
seleniumScript =
|
||||
pkgs.writers.writePython3Bin "selenium-script"
|
||||
{
|
||||
libraries = with pkgs.python3Packages; [ selenium ];
|
||||
}
|
||||
''
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.firefox.options import Options
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
options = Options()
|
||||
options.add_argument("--headless")
|
||||
service = webdriver.FirefoxService(executable_path="${lib.getExe pkgs.geckodriver}") # noqa: E501
|
||||
|
||||
driver = webdriver.Firefox(options=options, service=service)
|
||||
driver.implicitly_wait(10)
|
||||
driver.get("http://localhost:8080/web/dashboard")
|
||||
|
||||
wait = WebDriverWait(driver, 10).until(
|
||||
EC.text_to_be_present_in_element(
|
||||
(By.TAG_NAME, "body"), "Drive health at a glance")
|
||||
)
|
||||
|
||||
body_text = driver.find_element(By.TAG_NAME, "body").text
|
||||
assert "Temperature history for each device" in body_text
|
||||
|
||||
driver.close()
|
||||
'';
|
||||
in
|
||||
with pkgs;
|
||||
[
|
||||
curl
|
||||
firefox-unwrapped
|
||||
geckodriver
|
||||
seleniumScript
|
||||
];
|
||||
scrutiny.collector.enable = true;
|
||||
};
|
||||
};
|
||||
# This is the test code that will check if our service is running correctly:
|
||||
testScript = ''
|
||||
start_all()
|
||||
|
||||
# Wait for Scrutiny to be available
|
||||
machine.wait_for_unit("scrutiny")
|
||||
machine.wait_for_open_port(8080)
|
||||
environment.systemPackages =
|
||||
let
|
||||
seleniumScript =
|
||||
pkgs.writers.writePython3Bin "selenium-script"
|
||||
{
|
||||
libraries = with pkgs.python3Packages; [ selenium ];
|
||||
}
|
||||
''
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.firefox.options import Options
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
|
||||
# Ensure the API responds as we expect
|
||||
output = machine.succeed("curl localhost:8080/api/health")
|
||||
assert output == '{"success":true}'
|
||||
options = Options()
|
||||
options.add_argument("--headless")
|
||||
service = webdriver.FirefoxService(executable_path="${lib.getExe pkgs.geckodriver}") # noqa: E501
|
||||
|
||||
# Start the collector service to send some metrics
|
||||
collect = machine.succeed("systemctl start scrutiny-collector.service")
|
||||
driver = webdriver.Firefox(options=options, service=service)
|
||||
driver.implicitly_wait(10)
|
||||
driver.get("http://localhost:8080/web/dashboard")
|
||||
|
||||
# Ensure the application is actually rendered by the Javascript
|
||||
machine.succeed("PYTHONUNBUFFERED=1 selenium-script")
|
||||
'';
|
||||
}
|
||||
)
|
||||
wait = WebDriverWait(driver, 10).until(
|
||||
EC.text_to_be_present_in_element(
|
||||
(By.TAG_NAME, "body"), "Drive health at a glance")
|
||||
)
|
||||
|
||||
body_text = driver.find_element(By.TAG_NAME, "body").text
|
||||
assert "Temperature history for each device" in body_text
|
||||
|
||||
driver.close()
|
||||
'';
|
||||
in
|
||||
with pkgs;
|
||||
[
|
||||
curl
|
||||
firefox-unwrapped
|
||||
geckodriver
|
||||
seleniumScript
|
||||
];
|
||||
};
|
||||
};
|
||||
# This is the test code that will check if our service is running correctly:
|
||||
testScript = ''
|
||||
start_all()
|
||||
|
||||
# Wait for Scrutiny to be available
|
||||
machine.wait_for_unit("scrutiny")
|
||||
machine.wait_for_open_port(8080)
|
||||
|
||||
# Ensure the API responds as we expect
|
||||
output = machine.succeed("curl localhost:8080/api/health")
|
||||
assert output == '{"success":true}'
|
||||
|
||||
# Start the collector service to send some metrics
|
||||
collect = machine.succeed("systemctl start scrutiny-collector.service")
|
||||
|
||||
# Ensure the application is actually rendered by the Javascript
|
||||
machine.succeed("PYTHONUNBUFFERED=1 selenium-script")
|
||||
|
||||
# Test notification and genJqSecretsReplacementSnippet
|
||||
machine.succeed("curl -X POST http://localhost:8080/api/health/notify")
|
||||
machine.wait_for_file("/run/scrutiny/hello")
|
||||
output = machine.succeed("cat /run/scrutiny/hello")
|
||||
assert "HelloWorld" in output
|
||||
'';
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user