Merge remote-tracking branch 'upstream/staging-next' into staging

This commit is contained in:
Michael Daniels
2026-09-13 14:19:52 -04:00
1289 changed files with 30843 additions and 19605 deletions

View File

@@ -66,4 +66,4 @@ jobs:
Periodic merge from `${{ inputs.from }}` into [`${{ inputs.into }}`](https://github.com/NixOS/nixpkgs/tree/${{ inputs.into }}) has [failed](https://github.com/NixOS/nixpkgs/actions/runs/${{ github.run_id }}).
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
gh pr comment 105153 --body "$BODY_TEXT"
gh issue comment 562905 --body "$BODY_TEXT"

View File

@@ -3,6 +3,35 @@
Nixpkgs provides a variety of wrapper functions that help build commonly useful derivations.
Like [`stdenv.mkDerivation`](#sec-using-stdenv), each of these build helpers creates a derivation, but the arguments passed are different (usually simpler) from those required by `stdenv.mkDerivation`.
## Arguments with finalAttrs {#trivial-builder-finalAttrs}
In parameters that reference this section, you may either pass the value itself,
or a function that produces it.
When it's a function the argument value is [`finalAttrs`] from [`mkDerivation`].
Typically both the *attributes* and *script* arguments support this, simultaneously if needed.
::: {.example #ex-trivial-builder-finalAttrs}
# Using `finalAttrs` in a build helper
```nix
runCommand "hi" (finalAttrs: { passthru.exe = "${finalAttrs.finalPackage}/bin/hi"; }) ''
mkdir -p $out/bin
substitute ${./hi.foo} $out/bin/hi --replace-fail "@foo@" ${lib.getExe foo}
''
```
This creates a package with an executable script that's in the standard `bin/` directory,
but also convenient to interpolate without reliance on `$PATH`, e.g assuming the result of the above is in binding `hi`:
```nix
''
echo START_GREETING
${hi.exe} --rude
echo END_GREETING
''
```
:::
## `runCommandWith` {#trivial-builder-runCommandWith}
@@ -23,8 +52,10 @@ runCommandWith :: {
name :: name;
stdenv? :: Derivation;
runLocal? :: Bool;
derivationArgs? :: { ... };
} -> String -> Derivation
derivationArgs? :: { ... } | finalAttrs@{ finalPackage :: Derivation, ... } -> { ... };
}
-> (String | finalAttrs@{ finalPackage :: Derivation, ... } -> String)
-> Derivation
```
### Inputs {#trivial-builder-runCommandWith-Inputs}
@@ -47,10 +78,10 @@ runCommandWith :: {
`stdenv` (Derivation)
: The [standard environment](#chap-stdenv) to use, defaulting to `pkgs.stdenv`.
`derivationArgs` (Attribute set)
`derivationArgs` (Attribute set *or* [function from `finalAttrs`](#trivial-builder-finalAttrs))
: Additional arguments for [`mkDerivation`](#sec-using-stdenv).
`buildCommand` (String)
`buildCommand` (String *or* [function from `finalAttrs`](#trivial-builder-finalAttrs))
: Shell commands to run in the derivation builder.
::: {.note}
@@ -109,10 +140,10 @@ While the type signature(s) differ from [`runCommandWith`], individual arguments
`name` (String)
: The derivation's name
`derivationArgs` (Attribute set)
`derivationArgs` (Attribute set *or* [function from `finalAttrs`](#trivial-builder-finalAttrs))
: Additional parameters passed to [`mkDerivation`]
`buildCommand` (String)
`buildCommand` (String *or* [function from `finalAttrs`](#trivial-builder-finalAttrs))
: The command(s) run to build the derivation.
@@ -902,3 +933,6 @@ produces an output path `/nix/store/<hash>-runtime-references` containing
but none of `hello`'s dependencies because those are not referenced directly
by `hi`'s output.
[`finalAttrs`]: #mkderivation-recursive-attributes
[`mkDerivation`]: #sec-using-stdenv

View File

@@ -176,5 +176,7 @@ stdenvNoCC.mkDerivation (
manpage-urls = callPackage ../tests/manpage-urls.nix { };
};
};
meta.license = lib.licenses.mit;
}
)

View File

@@ -0,0 +1,47 @@
# `installAgentSkills` {#installAgentSkills}
This hook automatically installs LLM agent skills into the proper location in `$out/share/skills/($pname|$base)/$skill/`.
Agents do not scan package outputs themselves. Expose skills via `environment.pathsToLink = [ "/share/skills" ];` and symlink the wanted `share/skills/<pname>/<skill>` directories into the agent's skill directory (e.g. `~/.claude/skills/`).
The automatic behavior of the hook can be disabled by setting the `dontInstallAgentSkills` variable to true.
Additionally, it exposes the `installSkill` function that can be used from `postInstall`
## `installSkill` {#installAgentSkills-installSkill}
The `installSkill` function takes one or two arguments: a directory to copy to the install location, and an optional base directory.
NB: passing a SKILL.md file directly as the first argument will fail as skills often contain other examples and tooling within the same directory.
### Example Usage {#installAgentSkills-installSkill-exampleusage}
```nix
{
nativeBuildInputs = [ installAgentSkills ];
postInstall = ''
installSkill skills/skill-xyz
'';
# installs to $out/share/skills/$pname/skill-xyz
# OR
postInstall = ''
installSkill skills/skill-xyz random-base
'';
# installs to $out/share/skills/random-base/skill-xyz
}
```
Where `skills/skill-xyz` may look like:
```
skills/skill-xyz:
- SKILL.md
- scripts/
- references/
- assets/
- ...
```

File diff suppressed because it is too large Load Diff

View File

@@ -139,7 +139,7 @@ running NixOS integration tests on the builder (macOS 15+, M3 or newer).
```nix
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-22.11-darwin";
nixpkgs.url = "https://channels.nixos.org/nixpkgs-26.05-darwin/nixexprs.tar.zst";
darwin.url = "github:nix-darwin/nix-darwin/master";
darwin.inputs.nixpkgs.follows = "nixpkgs";
};

View File

@@ -140,6 +140,9 @@
"ex-testEqualArrayOrMap-test-function-add-cowbell": [
"index.html#ex-testEqualArrayOrMap-test-function-add-cowbell"
],
"ex-trivial-builder-finalAttrs": [
"index.html#ex-trivial-builder-finalAttrs"
],
"ex-writeShellApplication": [
"index.html#ex-writeShellApplication"
],
@@ -167,6 +170,15 @@
"inkscape-plugins": [
"index.html#inkscape-plugins"
],
"installAgentSkills": [
"index.html#installAgentSkills"
],
"installAgentSkills-installSkill": [
"index.html#installAgentSkills-installSkill"
],
"installAgentSkills-installSkill-exampleusage": [
"index.html#installAgentSkills-installSkill-exampleusage"
],
"installfonts": [
"index.html#installfonts"
],
@@ -999,6 +1011,9 @@
"treefmt": [
"index.html#treefmt"
],
"trivial-builder-finalAttrs": [
"index.html#trivial-builder-finalAttrs"
],
"typst": [
"index.html#typst",
"doc/languages-frameworks/typst.section.md#typst"

View File

@@ -47,6 +47,8 @@
- `bundlerApp` now sets `__structuredAttrs = true` for its result package.
Out-of-tree packages passing `postBuild` to `bundlerApp` should examine if their `postBuild` commands are compatible with structured attributes.
- `praat` has been updated to `7.x.x`, which changes the location of the settings file, buttons file, and plug-ins to standardized locations for each platform. This is a breaking change for plug-in writers and users of the sendpraat subroutine.
- `databricks-cli` has been updated from `0.290.2` to `1.x.x`, the first major release. OAuth tokens for interactive logins (`auth_type = databricks-cli`) are now stored in the OS-native secure store by default (Secret Service on Linux) instead of `~/.databricks/token-cache.json`; cached tokens from older versions are not migrated, so run `databricks auth login` once per profile after upgrading. To keep the previous file-backed storage, set `DATABRICKS_AUTH_STORAGE=plaintext` or add `auth_storage = plaintext` under `[__settings__]` in `~/.databrickscfg`. Additionally, the `vector_search_endpoints` DABs resource renamed `min_qps` to `target_qps` (and the `vector-search-endpoints` command renamed `--min-qps` to `--target-qps`). See the [upstream changelog](https://github.com/databricks/cli/blob/main/CHANGELOG.md) for details.
- Gradle 7 has been removed because it is end-of-life. Please [upgrade to a newer version of Gradle](https://docs.gradle.org/current/userguide/upgrading_version_7.html).
@@ -60,8 +62,13 @@
- `gotosocial` has been updated to 0.22.0. This release contains a very long database migration, which should not be cancelled or interrupted under any circumstances.
- Postgres users: Following the migration, if you encounter slowdown on Postgres specifically (ie., timing out while loading timelines) you may need to run some manual database maintenance steps. Please check https://docs.gotosocial.org/en/stable/admin/database_maintenance/#postgres.
- `lego` has been updated to 5.x, which has a changed CLI and state layout.
See [upstream's v4 to v5 CLI migration guide](https://go-acme.github.io/lego/migration/cli/) for details.
- `xsecurelock` no longer supports authentication via htaccess files (`~/.xsecurelock.pw`) or via the `pamtester` program by default. Only the recommended PAM module is supported unless rebuilt with `withHtaccess` or `withPamtester`.
- `keychain` has been updated to 3.x.x, which is a major and breaking rewrite of the program, including its CLI arguments. See [v3](https://github.com/danielrobbins/keychain/releases/tag/3.0.0) release notes for details and migration.
- `python3Packages.django-health-check` has been updated to major version 4. See its [migration guide](https://codingjoe.dev/django-health-check/migrate-to-v4/) and [changelog](https://github.com/codingjoe/django-health-check/releases/tag/4.0.0) for breaking changes.
- `jmtpfs` has been removed due to lack of maintenance and fuse3 support.
@@ -97,6 +104,8 @@
- `tengine` has been removed as it has seen seriously delayed responses to security vulnerabilities.
- `jellyfin` has been upgraded to major version 12, which contains breaking changes. See the [upstream blog post](https://jellyfin.org/posts/jellyfin-release-12.0) for more information on how to safely upgrade.
- `writeTextFile` now uses structured attributes and `passAsFile` is no longer used or supported.
- `nix-serve-ng` (and `haskellPackages.nix-serve-ng`) is now built against Lix instead of CppNix, following upstream which has switched to Lix as its supported Nix implementation.
@@ -155,6 +164,8 @@
- `pdfium` is now built from source instead of packaging prebuilt binaries. `pdfium-binaries` has been renamed to `pdfium`, and `pdfium-binaries-v8` has been removed.
- `iocaine` has been updated to `3.5.1`.
- `librest` providing 0.7 ABI was removed. `librest_1_0` providing 1.0 ABI was renamed to `librest` and `librest_1_0` was kept as an alias.
- `luaPackages.lrexlib-pcre` has been removed as part of the process to fully migrate from the end-of-life PRCE library to PCRE2. `luaPackages.lrexlib-pcre2` and multiple other versions of lrexlib can be used instead.
@@ -193,6 +204,10 @@
- netbox plugins have been moved from the python3Packages to the netboxPlugins package set.
- The `services.freenet` module has been renamed to `services.hyphanet`.
For more information, [see the project's website](https://www.hyphanet.org/freenet-renamed-to-hyphanet.html).
The `freenet` user, group and `/var/lib/freenet` data directory are renamed to `hyphanet` on systems with `system.stateVersion` at `26.11` or newer.
## Other Notable Changes {#sec-nixpkgs-release-26.11-notable-changes}
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->

View File

@@ -31,7 +31,6 @@ let
length
mapAttrs
mapAttrsToList
mapAttrsRecursiveCond
min
optional
optionalAttrs
@@ -254,7 +253,7 @@ let
# This function takes an empty attrset as an argument.
# It could theoretically be replaced with its body,
# but such a binding is avoided to allow for earlier grabage collection.
# but such a binding is avoided to allow for earlier garbage collection.
doCollect =
{ }:
collectModules class (specialArgs.modulesPath or "") (regularModules ++ [ internalModule ]) (
@@ -279,7 +278,11 @@ let
let
# For definitions that have an associated option
declaredConfig = mapAttrsRecursiveCond (v: !isOption v) (_: v: v.value) options;
declaredConfig =
let
recurse = mapAttrs (n: v: if isOption v then v.value else recurse v);
in
recurse options;
# If freeformType is set, this is for definitions that don't have an associated option
freeformConfig =
@@ -564,12 +567,23 @@ let
modulesPath:
{ disabled, modules }:
let
keyFilter = filter (attrs: !isDisabled modulesPath disabled attrs);
isDisabledModule = isDisabled modulesPath disabled;
keyFilter = filter (attrs: !isDisabledModule attrs);
in
catAttrs "module" (genericClosure {
startSet = keyFilter modules;
operator = attrs: keyFilter attrs.modules;
});
catAttrs "module" (
genericClosure (
if disabled == [ ] then
{
startSet = modules;
operator = attrs: attrs.modules;
}
else
{
startSet = keyFilter modules;
operator = attrs: keyFilter attrs.modules;
}
)
);
toGraph =
modulesPath:
@@ -631,6 +645,28 @@ let
: 3\. Function argument
*/
unifyModuleSyntax =
let
attrsToRemove = [
"_class"
"_file"
"key"
"disabledModules"
"imports"
"options"
"config"
"meta"
"freeformType"
];
shorthandAttrsToRemove = [
"_class"
"_file"
"key"
"disabledModules"
"require"
"imports"
"freeformType"
];
in
file: key: m:
let
addMeta =
@@ -654,17 +690,7 @@ let
in
if m ? config || m ? options then
let
badAttrs = removeAttrs m [
"_class"
"_file"
"key"
"disabledModules"
"imports"
"options"
"config"
"meta"
"freeformType"
];
badAttrs = removeAttrs m attrsToRemove;
in
if badAttrs != { } then
throw "Module `${key}' has an unsupported attribute `${head (attrNames badAttrs)}'. This is caused by introducing a top-level `config' or `options' attribute. Add configuration attributes immediately on the top level instead, or move all of them (namely: ${toString (attrNames badAttrs)}) into the explicit `config' attribute."
@@ -690,17 +716,7 @@ let
disabledModules = m.disabledModules or [ ];
imports = m.require or [ ] ++ m.imports or [ ];
options = { };
config = addFreeformType (
removeAttrs m [
"_class"
"_file"
"key"
"disabledModules"
"require"
"imports"
"freeformType"
]
);
config = addFreeformType (removeAttrs m shorthandAttrsToRemove);
};
applyModuleArgsIfFunction =
@@ -876,15 +892,13 @@ let
# We're descending into attribute ‘name’.
let
loc = prefix ++ [ name ];
defns = pushedDownDefinitionsByName.${name} or [ ];
defns' = rawDefinitionsByName.${name} or [ ];
optionDecls = filter (
isOptionDecl =
m:
m.options ? _type
&& (m.options._type == "option" || throwDeclarationTypeError loc m.options._type m._file)
) decls;
&& (m.options._type == "option" || throwDeclarationTypeError loc m.options._type m._file);
in
if length optionDecls == length decls then
if all isOptionDecl decls then
let
opt = fixupOptionType loc (mergeOptionDecls loc decls);
in
@@ -892,7 +906,35 @@ let
matchedOptions = evalOptionValue loc opt defns';
unmatchedDefns = [ ];
}
else if optionDecls != [ ] then
# this may look like duplicate computations are performed, but testing
# with a minimal NixOS config (defined below):
#
# 1. in 87% of cases, the above `all isOptionDecl decls` passes and we
# short-circuit
# 2. In 12% of cases, there's only one decl and we short-circuit
# 3. in another 1% of cases, we loop again and short-circuit when
# `any isOptionDecl decls` fails
# 4. In the final case, we pass the `any` and have to refilter. This
# only triggers in 1 of the 48000 calls to this function.
#
# The minimal config was defined with this expression:
#
# let nixos = import ./nixos/lib/eval-config.nix {
# modules = [
# ./nixos/modules/profiles/minimal.nix
# {
# fileSystems."/" = {
# device = "/dev/sda1";
# fsType = "ext4";
# };
# boot.loader.grub.devices = [ "/dev/sda" ];
# }
# ];
# }; in nixos.config.system.build.toplevel
else if length decls != 1 && any isOptionDecl decls then
let
optionDecls = filter isOptionDecl decls;
in
if
all (x: x.options.type.name or null == "submodule") optionDecls
# Raw options can only be merged into submodules. Merging into
@@ -920,7 +962,7 @@ let
(head optionDecls).options.type.description or "<no description>"
}' does not support nested options.\n${showRawDecls loc nonOptions}"
else
mergeModules' loc decls defns
mergeModules' loc decls (pushedDownDefinitionsByName.${name} or [ ])
) declsByName;
matchedOptions = mapAttrs (n: v: v.matchedOptions) resultsByName;
@@ -1009,19 +1051,17 @@ let
: 2\. Function argument
*/
mergeOptionDecls =
loc: opts:
loc:
foldl'
(
res: opt:
let
t = res.type;
t' = opt.options.type;
mergedType = t.typeMerge t'.functor;
typesMergeable = mergedType != null;
typeSet =
if opt.options ? type && res ? type then
if typesMergeable then
if res ? type && opt.options ? type then
let
mergedType = res.type.typeMerge opt.options.type.functor;
in
if mergedType != null then
{
type = mergedType;
}
@@ -1032,7 +1072,7 @@ let
else
{ };
bothHave = k: opt.options ? ${k} && res ? ${k};
bothHave = k: res ? ${k} && opt.options ? ${k};
in
if bothHave "default" || bothHave "example" || bothHave "description" || bothHave "apply" then
# Keep in sync with the same error above!
@@ -1077,8 +1117,7 @@ let
declarations = [ ];
declarationPositions = [ ];
options = [ ];
}
opts;
};
/**
Merge all the definitions of an option to produce the final
@@ -1131,21 +1170,19 @@ let
throw "The option `${showOption loc}' is read-only, but it's set multiple times. Definition values:${showDefs separateDefs}"
else
mergeDefinitions loc opt.type defs';
# Apply the 'apply' function to the merged value. This allows options to
# yield a value computed from the definitions
value = if opt ? apply then opt.apply res.mergedValue else res.mergedValue;
warnDeprecation =
if (opt.type.deprecationMessage != null) then
warn "The type `types.${opt.type.name}' of option `${showOption loc}' defined in ${showFiles opt.declarations} is deprecated. ${opt.type.deprecationMessage}"
else
x: x;
in
warnDeprecation opt
(
if opt.type.deprecationMessage != null then
warn "The type `types.${opt.type.name}' of option `${showOption loc}' defined in ${showFiles opt.declarations} is deprecated. ${opt.type.deprecationMessage}" opt
else
opt
)
// {
value = addErrorContext "while evaluating the option `${showOption loc}':" value;
value = addErrorContext "while evaluating the option `${showOption loc}':" (
# Apply the 'apply' function to the merged value. This allows options to
# yield a value computed from the definitions
if opt ? apply then opt.apply res.mergedValue else res.mergedValue
);
inherit (res.defsFinal') highestPrio;
definitions = catAttrs "value" res.defsFinal;
files = catAttrs "file" res.defsFinal;
@@ -1185,49 +1222,16 @@ let
# Merge definitions of a value of a given type.
mergeDefinitions = loc: type: defs: rec {
defsFinal' =
let
# Process mkMerge and mkIf properties.
defsNormalized = concatMap (
m:
map (
value:
if value._type or null == "definition" then
value
else
{
inherit (m) file;
inherit value;
}
) (addErrorContext "while evaluating definitions from `${m.file}':" (dischargeProperties m.value))
) defs;
# Process mkOverride properties.
defsFiltered = filterOverrides' defsNormalized;
# Sort mkOrder properties.
defsSorted =
# Avoid sorting if we don't have to.
if any (def: def.value._type or "" == "order") defsFiltered.values then
sortProperties defsFiltered.values
else
defsFiltered.values;
in
# Fast path: the overwhelming majority of options have exactly one
# definition whose value carries no property wrapper
# (mkIf/mkMerge/mkOverride/mkOrder/definition). In that case the
# discharge/filter/sort pipeline above is a no-op but still allocates
# several intermediate lists and closures. Detect it up front and hand
# the original singleton straight to the type merge. The let-bindings
# above are lazy and thus never forced on this branch.
# discharge/filter/sort pipeline below is a no-op but still allocates
# several intermediate lists and closures. Detect it up front and hand the
# original singleton straight to the type merge.
if
length defs == 1
&& (
let
d = head defs;
in
addErrorContext "while evaluating definitions from `${d.file}':" (
!(isAttrs d.value && d.value ? _type)
)
&& addErrorContext "while evaluating definitions from `${(head defs).file}':" (
!(head defs).value ? _type
)
then
{
@@ -1235,6 +1239,33 @@ let
highestPrio = defaultOverridePriority;
}
else
let
# Process mkMerge and mkIf properties.
defsNormalized = concatMap (
m:
map (
value:
if value._type or null == "definition" then
value
else
{
inherit (m) file;
inherit value;
}
) (addErrorContext "while evaluating definitions from `${m.file}':" (dischargeProperties m.value))
) defs;
# Process mkOverride properties.
defsFiltered = filterOverrides' defsNormalized;
# Sort mkOrder properties.
defsSorted =
# Avoid sorting if we don't have to.
if any (def: def.value._type or "" == "order") defsFiltered.values then
sortProperties defsFiltered.values
else
defsFiltered.values;
in
{
values = defsSorted;
inherit (defsFiltered) highestPrio;
@@ -1346,13 +1377,16 @@ let
val;
in
cfg:
if cfg._type or "" == "merge" then
concatMap pushDownProperties cfg.contents
else if cfg._type or "" == "if" then
map (mapAttrsIfAttrs (n: v: mkIf cfg.condition v)) (pushDownProperties cfg.content)
else if cfg._type or "" == "override" then
map (mapAttrsIfAttrs (n: v: mkOverride cfg.priority v)) (pushDownProperties cfg.content)
# FIXME: handle mkOrder?
if cfg ? _type then
if cfg._type == "if" then
map (mapAttrsIfAttrs (n: v: mkIf cfg.condition v)) (pushDownProperties cfg.content)
else if cfg._type == "merge" then
concatMap pushDownProperties cfg.contents
else if cfg._type == "override" then
map (mapAttrsIfAttrs (n: v: mkOverride cfg.priority v)) (pushDownProperties cfg.content)
# FIXME: handle mkOrder?
else
[ cfg ]
else
[ cfg ];
@@ -1375,13 +1409,16 @@ let
*/
dischargeProperties =
def:
if def._type or "" == "merge" then
concatMap dischargeProperties def.contents
else if def._type or "" == "if" then
if isBool def.condition then
if def.condition then dischargeProperties def.content else [ ]
if def ? _type then
if def._type == "if" then
if isBool def.condition then
if def.condition then dischargeProperties def.content else [ ]
else
throw "‘mkIf’ called with a non-Boolean condition"
else if def._type == "merge" then
concatMap dischargeProperties def.contents
else
throw "‘mkIf’ called with a non-Boolean condition"
[ def ]
else
[ def ];
@@ -1471,7 +1508,7 @@ let
fixupOptionType =
loc: opt:
if opt.type.getSubModules or null == null then
opt // { type = opt.type or types.unspecified; }
if opt ? type then opt else opt // { type = types.unspecified; }
else
opt
// {
@@ -1500,23 +1537,21 @@ let
mergeAttrDefinitionsWithPrio =
opt:
let
defsByAttr = zipAttrs (
concatLists (
concatMap (
{ value, ... }@def:
map (mapAttrsToList (
k: value: {
${k} = def // {
inherit value;
};
}
)) (pushDownProperties value)
) opt.definitionsWithLocations
)
defsByAttr = concatLists (
concatMap (
{ value, ... }@def:
map (mapAttrsToList (
k: value: {
${k} = def // {
inherit value;
};
}
)) (pushDownProperties value)
) opt.definitionsWithLocations
);
in
assert opt.type.name == "attrsOf" || opt.type.name == "lazyAttrsOf";
mapAttrs (
zipAttrsWith (
k: v:
let
merging = mergeDefinitions (opt.loc ++ [ k ]) opt.type.nestedTypes.elemType v;

View File

@@ -226,6 +226,9 @@ checkConfigError 'In attrTag, each tag value must be an option, but tag int was
# types
checkConfigOutput '"ok"' config.assertions ./types.nix
# types.nullOr
checkConfigError 'A definition for option .nullableValue\.mixed. is not of type .*\. TypeError: The option .nullableValue\.mixed. is defined both null and not null, in .* and .*\.' config.nullableValue.mixed ./types.nix
# types.pathInStore
checkConfigOutput '".*/store/0lz9p8xhf89kb1c1kk6jxrzskaiygnlh-bash-5.2-p15.drv"' config.pathInStore.ok1 ./types.nix
checkConfigOutput '".*/store/0fb3ykw9r5hpayd05sr0cizwadzq1d8q-bash-5.2-p15"' config.pathInStore.ok2 ./types.nix
@@ -742,6 +745,9 @@ checkConfigOutput '^38|27$' options.submoduleLine38.declarationPositions.1.line
# nested options work
checkConfigOutput '^34$' options.nested.nestedLine34.declarationPositions.0.line ./declaration-positions.nix
# types.pathWith in-module assertions
checkConfigOutput '"ok"' config.assertionsResult ./pathWith.nix
# types.pathWith { inStore = true; }
checkConfigOutput '".*/store/0lz9p8xhf89kb1c1kk6jxrzskaiygnlh-bash-5.2-p15.drv"' config.pathInStore.ok1 ./pathWith.nix
checkConfigOutput '".*/store/0fb3ykw9r5hpayd05sr0cizwadzq1d8q-bash-5.2-p15"' config.pathInStore.ok2 ./pathWith.nix

View File

@@ -1,4 +1,4 @@
{ lib, ... }:
{ config, lib, ... }:
let
inherit (builtins)
storeDir
@@ -12,6 +12,8 @@ in
imports = [
{
options = {
assertionsResult = mkOption { };
pathInStore = mkOption { type = types.lazyAttrsOf (types.pathWith { inStore = true; }); };
pathNotInStore = mkOption { type = types.lazyAttrsOf (types.pathWith { inStore = false; }); };
anyPath = mkOption { type = types.lazyAttrsOf (types.pathWith { }); };
@@ -23,6 +25,13 @@ in
}
);
};
absolutePath = mkOption {
type = types.lazyAttrsOf (
types.pathWith {
absolute = true;
}
);
};
# This conflicts with `conflictingPathOptionType` below.
conflictingPathOptionType = mkOption { type = types.pathWith { absolute = true; }; };
@@ -85,7 +94,21 @@ in
absolutePathNotInStore.bad1 = "./this/is/relative";
absolutePathNotInStore.bad2 = "${storeDir}/0fb3ykw9r5hpayd05sr0cizwadzq1d8q-bash-5.2-p15";
absolutePath.ok1 = "/var/lib/postgresql";
absolutePath.ok2 = {
type = "derivation";
outPath = abort "do not instantiate (via outPath)";
drvPath = abort "do not instantiate (via drvPath)";
meta.description = "just a test fixture";
};
conflictingPathOptionType = "/foo/bar";
impossiblePathOptionType = "/foo/bar";
assertionsResult =
assert config.absolutePath.ok1 == "/var/lib/postgresql";
assert config.absolutePath.ok2.type == "derivation";
assert config.absolutePath.ok2.meta.description == "just a test fixture";
"ok";
}

View File

@@ -60,6 +60,10 @@ in
};
nullableValue.list = [ { bar = [ 1 ]; } ]; # list
nullableValue.lambda = x: x; # Error
nullableValue.mixed = lib.mkMerge [
null
"foo"
]; # Error
# serializableValueWith { nullable = false; }
structuredValue.null = null; # Error

View File

@@ -695,7 +695,7 @@ rec {
else
/. + builtins.unsafeDiscardStringContext x
);
isAbsolute = substring 0 1 (toString x) == "/";
isAbsolute = x.type or null == "derivation" || substring 0 1 (toString x) == "/";
isExpectedType = (
if inStore == null || inStore then isStringLike x else isString x # Do not allow a true path, which could be copied to the store later on.
);
@@ -1242,18 +1242,44 @@ rec {
optionDescriptionPhrase (class: class == "noun" || class == "conjunction") elemType
}";
descriptionClass = "conjunction";
check = x: x == null || elemType.check x;
merge =
loc: defs:
let
nulls = filter (def: def.value == null) defs;
in
if nulls == [ ] then
elemType.merge loc defs
else if length nulls == length defs then
null
else
throw "The option `${showOption loc}` is defined both null and not null, in ${showFiles (getFiles defs)}.";
check = {
__functor = _self: x: x == null || elemType.check x;
isV2MergeCoherent = true;
};
merge = {
__functor =
self: loc: defs:
let
inherit (self.v2 { inherit loc defs; }) headError value;
in
if headError.causedByMixedNulls or false then throw headError.message else value;
v2 =
{ loc, defs }:
if all (def: def.value != null) defs then
# There are no null values
if elemType.merge ? v2 then
checkV2MergeCoherence loc elemType (elemType.merge.v2 { inherit loc defs; })
else
{
value = elemType.merge loc defs;
headError = checkDefsForError elemType.check loc defs;
valueMeta = { };
}
else
# There are some null values
{
headError =
if length defs == 1 || all (def: def.value == null) defs then
null
else
{
message = "The option `${showOption loc}` is defined both null and not null, in ${showFiles (getFiles defs)}.";
causedByMixedNulls = true;
};
value = null;
valueMeta = { };
};
};
emptyValue = {
value = null;
};

View File

@@ -1250,6 +1250,11 @@
githubId = 2545644;
name = "Alessandro Di Federico";
};
alejandroqh = {
github = "alejandroqh";
githubId = 40313108;
name = "Alejandro Quintanar";
};
alejandrosame = {
email = "alejandrosanchzmedina@gmail.com";
matrix = "@alejandrosame:matrix.org";
@@ -6229,6 +6234,12 @@
github = "dadada";
githubId = 7216772;
};
daimond113 = {
name = "daimond113";
github = "daimond113";
githubId = 72147841;
email = "contact@daimond113.com";
};
dalance = {
email = "dalance@gmail.com";
github = "dalance";
@@ -6766,12 +6777,6 @@
githubId = 25856103;
name = "DDoSolitary";
};
dduan = {
email = "daniel@duan.ca";
github = "dduan";
githubId = 75067;
name = "Daniel Duan";
};
de11n = {
email = "nixpkgs-commits@deshaw.com";
github = "de11n";
@@ -7437,6 +7442,12 @@
githubId = 93157285;
name = "Dmitrii Stepanidenko";
};
DmitrySkibitsky = {
email = "dmitryskibitsky@gmail.com";
github = "DmitrySkibitsky";
githubId = 20288556;
name = "Dmitry Skibitsky";
};
DmitryTsygankov = {
email = "dmitry.tsygankov@gmail.com";
github = "DmitryTsygankov";
@@ -8810,6 +8821,11 @@
githubId = 965612;
name = "York Wong";
};
etiennelescot = {
github = "EtienneLescot";
githubId = 215859519;
name = "Etienne Lescot";
};
Etjean = {
email = "et.jean@outlook.fr";
github = "Etjean";
@@ -10395,7 +10411,7 @@
};
gibbert = {
email = "gbjgms@gmail.com";
github = "zgibberish";
github = "2gibbert";
githubId = 67570424;
name = "gibbert";
};
@@ -14620,6 +14636,11 @@
githubId = 6544084;
name = "Kai Harries";
};
kaistarkk = {
github = "KaiStarkk";
githubId = 1722064;
name = "KaiStarkk";
};
kalbasit = {
email = "wael.nasreddine@gmail.com";
matrix = "@kalbasit:matrix.org";
@@ -25315,6 +25336,14 @@
github = "Ruixi-rebirth";
githubId = 75824585;
};
RumBugen = {
name = "Vladislav Wanner";
email = "vladislavwanner@gmail.com";
github = "RumBugen";
githubId = 43646118;
matrix = "@rumbugen:matrix.org";
keys = [ { fingerprint = "305D 2271 3C43 2E52 FB86 7C64 6107 6951 3194 1931"; } ];
};
rumpelsepp = {
name = "Stefan Tatschner";
email = "stefan@rumpelsepp.org";
@@ -31410,6 +31439,11 @@
githubId = 28888242;
name = "WORLDofPEACE";
};
wormt = {
github = "wormt";
githubId = 209373679;
name = "wormt";
};
WoutSwinkels = {
name = "Wout Swinkels";
email = "nixpkgs@woutswinkels.com";

View File

@@ -32,5 +32,6 @@ stdenv.mkDerivation {
description = "Utility to generate a Nix expression for a Perl package from CPAN";
mainProgram = "nix-generate-from-cpan";
platforms = lib.platforms.unix;
license = lib.licenses.mit;
};
}

View File

@@ -39,5 +39,6 @@ stdenv.mkDerivation {
description = "Utility for Nixpkgs contributors to check Nixpkgs for common errors";
mainProgram = "nixpkgs-lint";
platforms = lib.platforms.unix;
license = lib.licenses.mit;
};
}

View File

@@ -751,7 +751,6 @@ with lib.maintainers;
swift = {
members = [
dduan
samasaur
stephank
];

View File

@@ -15,6 +15,7 @@ An example of how to build an image:
imports = [ "${modulesPath}/image/repart.nix" ];
image.repart = {
enable = true;
name = "image";
partitions = {
"esp" = {
@@ -148,6 +149,7 @@ in
fileSystems."/".device = "/dev/disk/by-label/nixos";
image.repart = {
enable = true;
name = "image";
partitions = {
"esp" = {

View File

@@ -68,6 +68,18 @@
"module-boot-plymouth-tpm2-totp-quick-start-enable": [
"index.html#module-boot-plymouth-tpm2-totp-quick-start-enable"
],
"module-services-cliproxyapi": [
"index.html#module-services-cliproxyapi"
],
"module-services-cliproxyapi-authentication": [
"index.html#module-services-cliproxyapi-authentication"
],
"module-services-cliproxyapi-authentication-cli": [
"index.html#module-services-cliproxyapi-authentication-cli"
],
"module-services-cliproxyapi-authentication-management-api": [
"index.html#module-services-cliproxyapi-authentication-management-api"
],
"module-services-keycloak-unix-socket": [
"index.html#module-services-keycloak-unix-socket"
],

View File

@@ -161,6 +161,8 @@ Alongside many enhancements to NixOS modules and general system improvements, th
- [immich-public-proxy](https://github.com/alangrainger/immich-public-proxy), a proxy for sharing Immich albums without exposing the Immich API. Available as [services.immich-public-proxy](#opt-services.immich-public-proxy.enable).
- [Omada Software Controller](https://www.tp-link.com/us/business-networking/omada-sdn-controller/omada-software-controller), a professional and free on-premises controller for the Omada Software Defined Networking (SDN) platform, provides centralized management of Omada access points, switches and routers. Available as [services.omada](#opt-services.omada.enable).
- [Zipline](https://zipline.diced.sh/), a ShareX/file upload server that is easy to use, packed with features, and with an easy setup. Available as [services.zipline](#opt-services.zipline.enable).
- [GlitchTip](https://glitchtip.com/), an open source Sentry API compatible error tracking platform. Available as [services.glitchtip](#opt-services.glitchtip.enable).

View File

@@ -34,6 +34,8 @@
- [tranquil](https://tangled.org/tranquil.farm/tranquil-pds) is an ATProto PDS (personal data server) implementation in Rust. A featureful, spec conscious and community driven alternative to the Bluesky reference implementation PDS. Available as [services.tranquil-pds](#opt-services.tranquil-pds.enable).
- [Zerobyte](https://github.com/nicotsx/zerobyte), backup automation for self-hosters built on top of restic, with a web interface to schedule, manage and monitor encrypted backups. Available as [services.zerobyte](#opt-services.zerobyte.enable).
- [Cardwire](https://github.com/OpenGamingCollective/cardwire), a GPU manager for Linux that uses eBPF+LSM hooks to control GPUs. Available as [services.cardwired](#opt-services.cardwired.enable).
- [Moonlight Qt](https://moonlight-stream.org/), a client for playing your PC games on almost any device. Available as [programs.moonlight-qt](#opt-programs.moonlight-qt.enable).
@@ -60,6 +62,8 @@
- [feishin](https://github.com/jeffvli/feishin), a modern self-hosted music player. Available as [services.feishin](#opt-services.feishin.enable).
- [Aurral](https://aurral.org), a Lidarr companion for self-hosted music discovery. Available as [services.aurral](#opt-services.aurral.enable).
- [CastSponsorSkip](https://github.com/gabe565/CastSponsorSkip/), skips YouTube sponsorships (and sometimes ads) on all local Google Cast devices.
- [Stump](https://www.stumpapp.dev/), a free and open source comics, manga and digital book server with OPDS support. Available as [services.stump](#opt-services.stump.enable).
@@ -70,6 +74,8 @@
- [Freescout](https://freescout.net/), a free, open source Helpdesk and shared mailbox. Available as [services.freescout](#opt-services.freescout.enable).
- [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI), a proxy that provides OpenAI/Gemini/Claude/Codex/Grok compatible API interfaces from OAuth-based AI CLI subscriptions. Available as [services.cliproxyapi](#opt-services.cliproxyapi.enable).
- [Lix TOML remote builders](https://docs.lix.systems/manual/lix/stable/advanced-topics/distributed-builds.html#using-a-toml-configuration), remote builder configuration using lix's TOML format. Available as [lix.buildMachines](#opt-lix.buildMachines). Note: incompatible with `nix.buildMachines`.
- [Forgejo Runner](https://forgejo.org/docs/latest/admin/actions/), a daemon for Forgejo Actions. Available as [services.forgejo-runner](#opt-services.forgejo-runner.instances).
@@ -96,6 +102,8 @@
- [vellum](https://github.com/greyxp1/vellum) is a live screen annotation overlay for Wayland. Available as [programs.vellum](#opt-programs.vellum.enable).
- [iocaine](https://git.madhouse-project.org/iocaine/iocaine) is a defense mechanism against unwanted scrapers. Available as [services.iocaine](#opt-services.iocaine.enable).
- [stash-clipboard](https://github.com/NotAShelf/stash), a Wayland clipboard "manager" with fast persistent history and multi-media support. Available as [services.stash-clipboard](#opt-services.stash-clipboard.enable).
- [OO7](https://github.com/linux-credentials/oo7) is a desktop-agnostic Secret Service provider. Available as [services.oo7](#opt-services.oo7.enable)
@@ -150,6 +158,12 @@
- `boot.vesa` has been removed. It was deprecated in 2020 because Xorg now works better with kernel modesetting. If you still need the legacy VESA 800x600 fallback, set `boot.kernelParams = [ "vga=0x317" "nomodeset" ];` directly.
- `security.acme` now uses `lego` 5.x.
Existing ACME account state is migrated automatically.
If you pass raw `lego` flags through `security.acme.*.extraLegoFlags`, `security.acme.*.extraLegoRunFlags`, or `security.acme.*.extraLegoRenewFlags`, update them for the `lego` 5.x CLI.
In particular, `lego renew` has been replaced by the renewal mode of `lego run`.
The certificate identifiers formed by `domain` and `extraDomainNames` must be unique and canonical to avoid repeated renewals. Use IDNA A-labels for internationalized domain names and RFC 5952 notation for IPv6 addresses.
- `authentik` has been updated to 2026.5.3, which changes the default listen address from `0.0.0.0` to `[::]`.
IPv4-only deployments might need to adjust their listen settings.
Deployments running the server and worker in the same network namespace must also set at least the worker
@@ -169,6 +183,8 @@
- `services.pid-fan-controller` no longer provides deep configuration rewriting and adheres now fully to RFC42.
- `strongSwan` has been updated to 6.1.0, which defaults to only accepts IKEv2 connections as responder.
- The `extraArgs` and `check` arguments to `nixos/lib/eval-config.nix` (and therefore to `lib.nixosSystem`) have been removed after being deprecated with a warning since 2021. Passing them is now an evaluation error. Instead of `extraArgs`, set `config._module.args`; instead of `check = false`, set `config._module.check = false`. The `extraArgs` attribute on the resulting configuration has been removed as well.
- Rustical migrates from `settings.http.host` and `settings.http.port` to `settings.http.bind` to support UNIX domain sockets as well as TCP sockets in one setting.
@@ -242,6 +258,8 @@
- The papra NixOS module is now hardening the systemd unit by default. If this breaks any of the configured directories, please reconfigure them through `services.papra.environment` to enable sandbox passthrough.
- The `image/repart.nix` module now requires users to explicitly enable its use via `image.repart.enable`.
- `services.selfoss.extraConfig` and `services.selfoss.database` have been removed in favor of the structured [](#opt-services.selfoss.settings) option. When moving the `database` options to `settings`, you should also switch to the upstream naming:
- `type` → [`db_type`](#opt-services.selfoss.settings.db_type)
@@ -318,6 +336,9 @@
Migrating sites to Grav 2 is a [manual process](https://learn.getgrav.org/20/migration/manual-migration) with this package since the migration plugin cannot modify the Nix store.
The [`services.grav.package`](#opt-services.grav.package) option defaults to `pkgs.grav_2` if [`system.stateVersion`](#opt-system.stateVersion) >=26.11.
- The implementation of [`services.btrfs.autoScrub`](#opt-services.btrfs.autoScrub.enable) has been refactored to use systemd templates.
As part of this change, mountpoints to be scrubbed are now automatically mounted (if not already mounted) when the unit is started.
- `services.plausible` can now again seed an initial admin user declaratively via [`services.plausible.adminUser.email`](#opt-services.plausible.adminUser.email).
This makes fully declarative deployments safer: Otherwise the user needed to either accept Plausible's unauthenticated "first launch" setup wizard, which lets anyone reaching the instance create the first admin account, or do more work (deploying with NixOS's default binding to `localhost` without exposing it publicly, going through the wizard, and then deploying Plausible exposed to the Internet).
This option was previously removed with NixOS 25.05 due to an upstream Plausible change making declarative admin creation more difficult, but this change re-implements the admin creation directly.

View File

@@ -52,6 +52,7 @@ rec {
# Pin virt-11.0 to avoid gic-version=3 that works on MacOS 15+ only.
# FIXME: Revert to `virt` after minimal supported macos is 15+.
aarch64-linux = "${qemuPkg}/bin/qemu-system-aarch64 -machine virt-11.0,accel=${accel "hvf"} -cpu max";
x86_64-linux = "${qemuPkg}/bin/qemu-system-x86_64 -machine type=q35,accel=${accel "hvf"} -cpu max";
};
};

View File

@@ -0,0 +1,83 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
mkDefault
mkEnableOption
mkIf
mkOption
optional
types
;
cfg = config.hardware.ipu7;
in
{
options.hardware.ipu7 = {
enable = mkEnableOption "support for Intel IPU7/MIPI cameras";
platform = mkOption {
type = types.enum [
"ipu7x"
"ipu75xa"
];
description = ''
Choose the version for your hardware platform. The IPU reports which one
it is through its PCI ID, visible as the Multimedia controller in lspci.
- ipu7x (Lunar Lake, PCI 8086:645d)
Sensor list: https://github.com/intel/ipu7-camera-hal/tree/main/config/linux/ipu7x/sensors
- ipu75xa (Panther Lake, PCI 8086:b05d)
Sensor list: https://github.com/intel/ipu7-camera-hal/tree/main/config/linux/ipu75xa/sensors
'';
};
};
config = mkIf cfg.enable {
# Kernels >= 6.17 ship an IPU7 core and ISys in drivers/staging/media/ipu7,
# but no PSys, so they cannot drive the hardware ISP that the camera HAL
# needs. ipu7-drivers supplies just the PSys module (intel-ipu7-psys), which
# has no in-tree counterpart, and links it against the in-tree core and ISys
# that already enumerate the sensor.
boot.extraModulePackages = with config.boot.kernelPackages; [
ipu7-drivers
];
hardware.firmware = with pkgs; [
ipu7-camera-bins
ivsc-firmware
];
services.udev.extraRules = ''
SUBSYSTEM=="intel-ipu7-psys", MODE="0660", GROUP="video"
'';
services.v4l2-relayd.instances.ipu7 = {
enable = mkDefault true;
cardLabel = mkDefault "Intel MIPI Camera";
extraPackages =
with pkgs.gst_all_1;
optional (cfg.platform == "ipu7x") icamerasrc-ipu7x
++ optional (cfg.platform == "ipu75xa") icamerasrc-ipu75xa;
input = {
pipeline = "icamerasrc";
# REVIEW from https://edc.intel.com/content/www/us/en/secure/design/confidential/products/platforms/details/lunar-lake-mx/core-ultra-200v-series-processors-datasheet-volume-1-of-2/camera-integrated-isp/
# Output Formats - NV12, NV16, I420, M420, YUY2, YUYV, P010, P016
format = "NV12";
};
};
};
}

View File

@@ -131,6 +131,7 @@ in
];
options.image.repart = {
enable = lib.mkEnableOption "systemd-repart boot image";
name = lib.mkOption {
type = lib.types.str;
@@ -313,7 +314,7 @@ in
};
config = {
config = lib.mkIf cfg.enable {
image.baseName =
let
version = config.image.repart.version;

View File

@@ -124,7 +124,7 @@ in
datadog = 76;
lighttpd = 77;
lightdm = 78;
freenet = 79;
# hyphanet = 79; # was freenet; dynamically allocated as of 2026-09-02
ircd = 80;
bacula = 81;
#almir = 82; # removed 2018-03-25, the almir package was removed in 30291227f2411abaca097773eedb49b8f259e297 during 2017-08
@@ -466,7 +466,7 @@ in
datadog = 76;
lighttpd = 77;
lightdm = 78;
freenet = 79;
# hyphanet = 79; # was freenet; dynamically allocated as of 2026-09-02
ircd = 80;
bacula = 81;
#almir = 82; # removed 2018-03-25, the almir package was removed in 30291227f2411abaca097773eedb49b8f259e297 during 2017-08

View File

@@ -130,6 +130,7 @@
./hardware/video/virtualbox.nix
./hardware/video/webcam/facetimehd.nix
./hardware/video/webcam/ipu6.nix
./hardware/video/webcam/ipu7.nix
./hardware/wooting.nix
./hardware/xone.nix
./hardware/xpad-noone.nix
@@ -854,6 +855,7 @@
./services/misc/anki-sync-server.nix
./services/misc/apache-kafka.nix
./services/misc/atuin.nix
./services/misc/aurral.nix
./services/misc/autobrr.nix
./services/misc/autofs.nix
./services/misc/autorandr.nix
@@ -871,6 +873,7 @@
./services/misc/cgminer.nix
./services/misc/clipcat.nix
./services/misc/clipmenu.nix
./services/misc/cliproxyapi.nix
./services/misc/comfyui.nix
./services/misc/confd.nix
./services/misc/conman.nix
@@ -1243,7 +1246,6 @@
./services/networking/firezone/relay.nix
./services/networking/firezone/server.nix
./services/networking/flannel.nix
./services/networking/freenet.nix
./services/networking/freeradius.nix
./services/networking/frp.nix
./services/networking/frr.nix
@@ -1272,6 +1274,7 @@
./services/networking/htpdate.nix
./services/networking/https-dns-proxy.nix
./services/networking/hylafax/default.nix
./services/networking/hyphanet.nix
./services/networking/i2p.nix
./services/networking/i2pd.nix
./services/networking/icecream/daemon.nix
@@ -1280,6 +1283,7 @@
./services/networking/imaginary.nix
./services/networking/inadyn.nix
./services/networking/inspircd.nix
./services/networking/iocaine.nix
./services/networking/iodine.nix
./services/networking/iperf3.nix
./services/networking/ircd-hybrid/default.nix
@@ -1383,6 +1387,7 @@
./services/networking/ofono.nix
./services/networking/oidentd.nix
./services/networking/oink.nix
./services/networking/omada.nix
./services/networking/onedrive.nix
./services/networking/openconnect.nix
./services/networking/openvpn.nix
@@ -1707,6 +1712,7 @@
./services/web-apps/filebrowser.nix
./services/web-apps/firefly-iii-data-importer.nix
./services/web-apps/firefly-iii.nix
./services/web-apps/flame.nix
./services/web-apps/flarum.nix
./services/web-apps/fluidd.nix
./services/web-apps/freescout.nix
@@ -1876,6 +1882,7 @@
./services/web-apps/your_spotify.nix
./services/web-apps/youtrack.nix
./services/web-apps/zabbix.nix
./services/web-apps/zerobyte.nix
./services/web-apps/zipline.nix
./services/web-apps/zitadel.nix
./services/web-servers/agate.nix

View File

@@ -225,6 +225,7 @@ in
mainProgram = "create-builder";
description = "Create a Linux builder VM for macOS";
platforms = lib.platforms.darwin;
maintainers = with lib.maintainers; [ booxter ];
};
passthru = {
# Let users in the repl inspect the config

View File

@@ -15,10 +15,7 @@
Additionally to installing `k3b` enabling this will
add `setuid` wrappers in `/run/wrappers/bin`
for both `cdrdao` and `cdrecord`. On first
run you must manually configure the path of `cdrdae` and
`cdrecord` to correspond to the appropriate paths under
`/run/wrappers/bin` in the "Setup External Programs" menu.
for both `cdrdao` and `cdrecord`.
'';
};
};
@@ -48,6 +45,5 @@
source = "${pkgs.cdrtools}/bin/cdrecord";
};
};
};
}

View File

@@ -29,15 +29,13 @@ let
builtInRemovedMsg = "This is now built-in in DMS and doesn't need additional dependencies.";
optionalPackages =
optionals cfg.enableSystemMonitoring [ pkgs.dgop ]
++ optionals cfg.enableVPN [
optionals cfg.enableVPN [
pkgs.glib
pkgs.networkmanager
]
++ optional cfg.enableDynamicTheming pkgs.matugen
++ optional cfg.enableAudioWavelength pkgs.cava
++ optional cfg.enableCalendarEvents pkgs.khal
++ optional cfg.enableClipboardPaste pkgs.wtype;
++ optional cfg.enableCalendarEvents pkgs.khal;
in
{
imports = [
@@ -47,6 +45,8 @@ in
path ++ [ "enableSystemSound" ]
) "qtmultimedia is now included on dms-shell package.")
(lib.mkRemovedOptionModule (path ++ [ "enableClipboard" ]) builtInRemovedMsg)
(lib.mkRemovedOptionModule (path ++ [ "enableSystemMonitoring" ]) builtInRemovedMsg)
(lib.mkRemovedOptionModule (path ++ [ "enableClipboardPaste" ]) builtInRemovedMsg)
];
options.programs.dms-shell = {
@@ -86,17 +86,6 @@ in
};
};
enableSystemMonitoring = mkOption {
type = types.bool;
default = true;
description = ''
Whether to install dependencies required for system monitoring widgets.
This includes process list viewers and system resource monitors.
Requires: dgop
'';
};
enableVPN = mkOption {
type = types.bool;
default = true;
@@ -141,17 +130,6 @@ in
'';
};
enableClipboardPaste = mkOption {
type = types.bool;
default = true;
description = ''
Whether to install dependencies required for pasting directly from the clipboard history support.
This enables pressing Shift+Return for pasting entries from the clipboard history.
Requires: wtype
'';
};
quickshell = {
package = mkPackageOption pkgs "quickshell" { };
};

View File

@@ -278,7 +278,7 @@ in
''}
# Determine current fqdn hostname
HOST=$(hostname --fqdn)
HOST=$(${lib.getExe pkgs.unixtools.hostname} --fqdn)
# Setup command line history.
# Don't export these, otherwise other shells (bash) will try to use same HISTFILE.

View File

@@ -242,8 +242,6 @@ let
+ lib.optionalString (data.csr != null) " - ${data.csr}"
+ lib.optionalString (data.profile != null) " - ${data.profile}";
certDir = mkHash hashData;
# TODO remove domainHash usage entirely. Waiting on go-acme/lego#1532
domainHash = mkHash "${lib.concatStringsSep " " extraDomains} ${data.domain}";
accountHash = (mkAccountHash acmeServer data);
accountDir = accountDirRoot + accountHash;
@@ -254,7 +252,10 @@ let
"--dns"
data.dnsProvider
]
++ lib.optionals (!data.dnsPropagationCheck) [ "--dns.propagation-disable-ans" ]
++ lib.optionals (!data.dnsPropagationCheck) [
"--dns.propagation.disable-ans"
"--dns.propagation.disable-rns"
]
++ lib.optionals (data.dnsResolver != null) [
"--dns.resolvers"
data.dnsResolver
@@ -269,7 +270,7 @@ let
else if data.listenHTTP != null then
[
"--http"
"--http.port"
"--http.address"
data.listenHTTP
]
else
@@ -309,21 +310,27 @@ let
]) extraDomains
++ data.extraLegoFlags;
# Although --must-staple is common to both modes, it is not declared as a
# mode-agnostic argument in lego and thus must come after the mode.
# `lego run` renews when its state still contains a certificate resource. The full
# path is also a recovery path, so force renewal without an ARI `replaces`
# identifier that may refer to a stale certificate.
runOpts = lib.escapeShellArgs (
commonOpts
++ [ "run" ]
[ "run" ]
++ commonOpts
++ [
"--no-random-sleep"
"--renew-force"
"--ari-disable"
]
++ lib.optionals (data.csr == null) [ "--force-cert-domains" ]
++ lib.optionals data.ocspMustStaple [ "--must-staple" ]
++ lib.optionals (data.profile != null) [ "--profile=${data.profile}" ]
++ data.extraLegoRunFlags
);
renewOpts = lib.escapeShellArgs (
commonOpts
++ [
"renew"
"--no-random-sleep"
]
[ "run" ]
++ commonOpts
++ [ "--no-random-sleep" ]
++ lib.optionals (data.csr == null) [ "--force-cert-domains" ]
++ lib.optionals data.ocspMustStaple [ "--must-staple" ]
++ lib.optionals (data.profile != null) [ "--profile=${data.profile}" ]
++ data.extraLegoRenewFlags
@@ -575,13 +582,18 @@ let
}
}
echo '${domainHash}' > domainhash.txt
# Multiple certificates can share an account. Serialize the check and
# migration because lego v5 moves the shared key out of keys/.
exec {MIGRATION_LOCK_FD}> "${lockdir}migration-${accountHash}.lock"
${pkgs.flock}/bin/flock "$MIGRATION_LOCK_FD"
if [ -n "$(find accounts -path '*/keys/*.key' -print -quit)" ]; then
printf 'Y\n' | lego migrate --account-only --path .
fi
exec {MIGRATION_LOCK_FD}>&-
# Check if a new order is needed
# We can only renew if the list of domains has not changed.
# Check if the existing certificate can be renewed.
# We also need an account key. Avoids #190493
if cmp -s domainhash.txt certificates/domainhash.txt && [ -e '${certificateKey}' ] && \
[ -e 'certificates/${keyName}.crt' ] && \
if [ -e '${certificateKey}' ] && [ -e 'certificates/${keyName}.crt' ] && \
[ -n "$(find accounts -name '${
if (data.email != null) then data.email else placeholderEmail
}.key')" ];
@@ -590,7 +602,7 @@ let
# Try to renew, and silently fail if the cert is not expired.
# Avoids #85794 and resolves #129838
if ! lego ${renewOpts} ${
if data.validMinDays != null then "--days ${toString data.validMinDays}" else "--dynamic"
if data.validMinDays != null then "--renew-days ${toString data.validMinDays}" else ""
}; then
if is_expiration_skippable out/full.pem; then
echo 1>&2 "nixos-acme: Ignoring failed renewal because expiration isn't due yet"
@@ -610,8 +622,6 @@ let
exit 10
fi
mv domainhash.txt certificates/
touch out/acme-success
# Copy all certs to the "real" certs directory
@@ -872,7 +882,7 @@ let
type = lib.types.listOf lib.types.str;
inherit (defaultAndText "extraLegoFlags" [ ]) default defaultText;
description = ''
Additional global flags to pass to all lego commands.
Additional flags to pass to both `lego run` invocations.
'';
};
@@ -880,7 +890,7 @@ let
type = lib.types.listOf lib.types.str;
inherit (defaultAndText "extraLegoRenewFlags" [ ]) default defaultText;
description = ''
Additional flags to pass to lego renew.
Additional flags to pass to the non-forced renewal `lego run` invocation.
'';
};
@@ -888,7 +898,7 @@ let
type = lib.types.listOf lib.types.str;
inherit (defaultAndText "extraLegoRunFlags" [ ]) default defaultText;
description = ''
Additional flags to pass to lego run.
Additional flags to pass to the forced obtain/reissue `lego run` invocation.
'';
};
};

View File

@@ -236,7 +236,8 @@ in
# If DynamicUser= is enabled, PrivateTmp=true is implied (and cannot be turned off).
# so disable them unless PrivateTmp=true is explicitely set.
${lib.optionalString (!cfg.serviceConfig.PrivateTmp) ''
# We're explicitely comparing to `false` here, because PrivateTmp can also be "disconnected".
${lib.optionalString (cfg.serviceConfig.PrivateTmp == false) ''
echo "InaccessiblePaths=-+/tmp" >> "$serviceFile"
echo "InaccessiblePaths=-+/var/tmp" >> "$serviceFile"
''}

View File

@@ -560,6 +560,16 @@ in
++ lib.optional (cfg.dockerHost == null) "docker.service";
wantedBy = [ "multi-user.target" ];
# Periphery shells out to `docker`, `docker compose` and `git` through `sh -c`.
path = [
pkgs.git
config.virtualisation.docker.package
]
++ lib.optionals (!cfg.disableTerminals) [
"/run/current-system/sw"
"/run/wrappers"
];
serviceConfig = {
Type = "simple";
User = cfg.user;
@@ -604,11 +614,6 @@ in
// cfg.environment
);
ExecSearchPath = lib.mkIf (!cfg.disableTerminals) [
"/run/current-system/sw/bin"
"/run/wrappers/bin"
];
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
StateDirectory = "komodo-periphery";

View File

@@ -69,7 +69,8 @@ let
cassandraEnvPkg = "${cfg.package}/conf/cassandra-env.sh";
cassandraLogbackConfig = pkgs.writeText "logback.xml" cfg.logbackConfig;
passAsFile = [ "extraEnvSh" ];
strictDeps = true;
__structuredAttrs = true;
inherit (cfg) extraEnvSh package;
buildCommand = ''
@@ -80,7 +81,7 @@ let
( cat "$cassandraEnvPkg"
echo "# lines from services.cassandra.extraEnvSh: "
cat "$extraEnvShPath"
printf "%s" "$extraEnvSh"
) > "$out/cassandra-env.sh"
# Delete default JMX Port, otherwise we can't set it using env variable

View File

@@ -44,19 +44,17 @@ let
makeBinPath [
cfg.quickshell.package
compositorPkg
pkgs.glib # provides gdbus, used by the fprintd hardware probe and portal reads
]
}
${
escapeShellArgs (
[
"sh"
"${cfg.package}/share/quickshell/dms/Modules/Greetd/assets/dms-greeter"
"${cfg.package}/bin/dms-greeter"
"--cache-dir"
cacheDir
"--command"
cfg.compositor.name
"-p"
"${cfg.package}/share/quickshell/dms"
]
++ lib.optionals (cfg.compositor.customConfig != "") [
"-C"
@@ -114,21 +112,7 @@ in
options.services.displayManager.dms-greeter = {
enable = mkEnableOption "DankMaterialShell greeter";
package = mkOption {
type = types.package;
default = if cfgDms.enable then cfgDms.package else pkgs.dms-shell;
defaultText = literalExpression ''
if config.programs.dms-shell.enable
then config.programs.dms-shell.package
else pkgs.dms-shell;
'';
description = ''
The DankMaterialShell package to use for the greeter.
Defaults to the package from `programs.dms-shell` if it is enabled,
otherwise defaults to `pkgs.dms-shell`.
'';
};
package = lib.mkPackageOption pkgs "dms-greeter" { };
compositor = {
name = mkOption {

View File

@@ -0,0 +1,185 @@
{
config,
pkgs,
lib,
...
}:
let
cfg = config.services.aurral;
in
{
options = {
services.aurral = {
enable = lib.mkEnableOption "Aurral is the Lidarr companion for self-hosted music discovery";
package = lib.mkPackageOption pkgs "aurral" { };
directories = lib.mkOption {
type = lib.types.listOf lib.types.externalPath;
default = [ ];
description = ''
Directories that Aurral needs access to. Other directories won't be visible by the app.
Environment variable `DOWNLOAD_FOLDER` is added automatically.
See BindPaths in {manpage}`systemd.exec(5)`.
'';
};
dataDir = lib.mkOption {
type = lib.types.externalPath;
default = "/var/lib/aurral";
description = ''
The directory where Aurral stores its stateful data.
'';
};
port = lib.mkOption {
type = lib.types.port;
default = 3001;
description = "Port number";
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Open ports in the firewall for Aurral.
'';
};
user = lib.mkOption {
type = lib.types.str;
default = "aurral";
description = ''
User account under which Aurral runs.
'';
};
group = lib.mkOption {
type = lib.types.str;
default = "aurral";
description = ''
Group under which Aurral runs.
'';
};
environment = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
example = {
DOWNLOAD_FOLDER = "/media/downloads";
TRUST_PROXY = "true";
};
description = ''
Environment variables passed to the service.
'';
};
environmentFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Environment file as defined in {manpage}`systemd.exec(5)` passed to the service.
'';
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.aurral = {
description = "Aurral";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
environment = cfg.environment // {
AURRAL_DATA_DIR = cfg.dataDir;
PORT = toString cfg.port;
};
path = [ cfg.package ];
serviceConfig = {
Type = "simple";
ExecStart = lib.getExe cfg.package;
Restart = "on-failure";
User = cfg.user;
Group = cfg.group;
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/aurral") "aurral";
WorkingDirectory = cfg.dataDir;
ReadWritePaths = "";
ProtectSystem = "strict";
BindPaths = [
cfg.dataDir
]
++ (lib.map (x: "-" + x) cfg.directories)
++ lib.optional (cfg.environment ? DOWNLOAD_FOLDER) cfg.environment.DOWNLOAD_FOLDER;
BindReadOnlyPaths = [
builtins.storeDir
"${config.security.pki.caBundle}:/etc/ssl/certs/ca-certificates.crt"
"-/etc/resolv.conf"
]
++ lib.optionals config.services.resolved.enable [
"/run/systemd/resolve/stub-resolv.conf"
"/run/systemd/resolve/resolv.conf"
];
RestrictSUIDSGID = true;
CapabilityBoundingSet = "";
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
SocketBindDeny = "any";
SocketBindAllow = toString cfg.port;
SystemCallErrorNumber = "EPERM";
SystemCallFilter = [
"@system-service"
"~@privileged"
"~@resources"
];
UMask = "0007";
SystemCallArchitectures = "native";
ProtectProc = "invisible";
ProcSubset = "pid";
LockPersonality = true;
NoNewPrivileges = true;
DevicePolicy = "closed";
PrivateIPC = true;
PrivatePIDs = true;
ProtectClock = true;
ProtectHome = true;
ProtectKernelLogs = true;
ProtectHostname = true;
RemoveIPC = true;
RestrictRealtime = true;
RestrictNamespaces = true;
MemoryDenyWriteExecute = false;
};
confinement.enable = true;
};
systemd.tmpfiles.settings."10-aurral" = lib.mkIf (cfg.environment ? DOWNLOAD_FOLDER) {
${cfg.environment.DOWNLOAD_FOLDER}.d = {
inherit (cfg) user group;
mode = "0770";
};
};
users.users = lib.mkIf (cfg.user == "aurral") {
aurral = {
isSystemUser = true;
home = cfg.dataDir;
group = cfg.group;
};
};
users.groups = lib.mkIf (cfg.group == "aurral") {
aurral = { };
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.port ];
};
};
}

View File

@@ -0,0 +1,55 @@
# CLIProxyAPI {#module-services-cliproxyapi}
[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI) exposes OAuth-based subscription CLIs (Claude Code, Codex, Gemini, Qwen, Grok, Antigravity) behind OpenAI/Gemini/Anthropic-compatible HTTP APIs.
Enable it with:
```nix
{
services.cliproxyapi.enable = true;
}
```
The service runs as a dedicated `cliproxyapi` user and keeps its configuration and OAuth tokens under `/var/lib/cliproxyapi`.
## Authentication {#module-services-cliproxyapi-authentication}
Provider logins use OAuth and must land in the service's `auth-dir` (`/var/lib/cliproxyapi`), which is owned by the `cliproxyapi` user. Either of the approaches below writes the token with the correct ownership, and the running service picks it up without a restart.
### Management API {#module-services-cliproxyapi-authentication-management-api}
Set a management key in [](#opt-services.cliproxyapi.settings):
```nix
{
services.cliproxyapi.settings.remote-management.secret-key._secret =
"/run/secrets/cliproxyapi-mgmt-key";
}
```
Then request an authentication URL for the desired provider and open it in a browser:
```bash
curl -H "Authorization: Bearer <management-key>" \
http://127.0.0.1:8317/v0/management/anthropic-auth-url
```
The daemon completes the OAuth flow itself and stores the token in its `auth-dir`. Authentication endpoints are available for the `anthropic`, `codex`, `xai`, `antigravity`, and `kimi` providers.
### Command-line login {#module-services-cliproxyapi-authentication-cli}
Add the package so the `cliproxyapi` binary is on `PATH`:
```nix
{
environment.systemPackages = [ config.services.cliproxyapi.package ];
}
```
Then run the login as the service user, pointing at the managed configuration:
```bash
sudo -u cliproxyapi cliproxyapi -config /var/lib/cliproxyapi/config.yaml --claude-login
```
Other providers use their matching flags, for example `--codex-login` or `--xai-login`. On a headless host, pass `-no-browser` to print the OAuth URL instead of launching a browser.

View File

@@ -0,0 +1,153 @@
{
config,
lib,
pkgs,
utils,
...
}:
let
cfg = config.services.cliproxyapi;
format = pkgs.formats.yaml { };
stateDir = "/var/lib/cliproxyapi";
configPath = "${stateDir}/config.yaml";
settings = {
auth-dir = stateDir;
}
// cfg.settings;
secretsReplacement = utils.genJqSecretsReplacement {
loadCredential = true;
} settings configPath;
port = cfg.settings.port or 8317;
in
{
options.services.cliproxyapi = {
enable = lib.mkEnableOption "CLIProxyAPI";
package = lib.mkPackageOption pkgs "cliproxyapi" { };
settings = lib.mkOption {
type = format.type;
default = { };
example = lib.literalExpression ''
{
host = "127.0.0.1";
port = 8317;
api-keys = [ { _secret = "/run/secrets/cliproxyapi-api-key"; } ];
remote-management.secret-key._secret = "/run/secrets/cliproxyapi-management-key";
}
'';
description = ''
Configuration for CLIProxyAPI. See the
[example configuration](https://github.com/router-for-me/CLIProxyAPI/blob/main/config.example.yaml)
for available options. Secret values can be loaded from files using
`._secret = "/path/to/secret";`.
'';
};
environmentFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/run/secrets/cliproxyapi.env";
description = "Environment file as defined in {manpage}`systemd.exec(5)`.";
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to open the firewall for the specified port.";
};
user = lib.mkOption {
type = lib.types.str;
default = "cliproxyapi";
description = "User account under which CLIProxyAPI runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "cliproxyapi";
description = "Group under which CLIProxyAPI runs.";
};
};
config = lib.mkIf cfg.enable {
users.users = lib.mkIf (cfg.user == "cliproxyapi") {
cliproxyapi = {
isSystemUser = true;
group = cfg.group;
home = stateDir;
description = "CLIProxyAPI service user";
};
};
users.groups = lib.mkIf (cfg.group == "cliproxyapi") {
cliproxyapi = { };
};
systemd.services.cliproxyapi = {
description = "Proxy that provides OpenAI/Gemini/Claude/Codex/Grok compatible API interfaces";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
preStart = secretsReplacement.script;
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
StateDirectory = "cliproxyapi";
StateDirectoryMode = "0700";
WorkingDirectory = stateDir;
ExecStart = "${lib.getExe cfg.package} -config ${configPath}";
Restart = "on-failure";
RestartSec = 5;
LoadCredential = secretsReplacement.credentials;
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) [ cfg.environmentFile ];
# Hardening
CapabilityBoundingSet = "";
NoNewPrivileges = true;
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
PrivateUsers = true;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
ProtectProc = "invisible";
ProcSubset = "pid";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictSUIDSGID = true;
RestrictRealtime = true;
RemoveIPC = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
"~@resources"
];
UMask = "0077";
};
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ port ];
};
};
meta = {
doc = ./cliproxyapi.md;
maintainers = [ lib.maintainers.anish ];
};
}

View File

@@ -111,7 +111,13 @@ in
ExecStart = toString [
(lib.getExe' cfg.package "llama-server")
(lib.cli.toCommandLine (optionName: {
option = if builtins.stringLength optionName > 1 then "--${optionName}" else "-${optionName}";
option =
if lib.hasPrefix "-" optionName then
optionName
else if builtins.stringLength optionName > 1 then
"--${optionName}"
else
"-${optionName}";
sep = " ";
explicitBool = false;
formatArg = lib.generators.mkValueStringDefault { };

View File

@@ -1,51 +0,0 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.freenet;
varDir = "/var/lib/freenet";
in
{
options = {
services.freenet = {
enable = lib.mkEnableOption "Freenet daemon";
nice = lib.mkOption {
type = lib.types.ints.between (-20) 19;
default = 10;
description = "Set the nice level for the Freenet daemon";
};
};
};
config = lib.mkIf cfg.enable {
systemd.services.freenet = {
description = "Freenet daemon";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = lib.getExe pkgs.freenet;
User = "freenet";
UMask = "0007";
WorkingDirectory = varDir;
Nice = cfg.nice;
};
};
users.users.freenet = {
group = "freenet";
description = "Freenet daemon user";
home = varDir;
createHome = true;
uid = config.ids.uids.freenet;
};
users.groups.freenet.gid = config.ids.gids.freenet;
};
meta.maintainers = with lib.maintainers; [ nagy ];
}

View File

@@ -120,6 +120,93 @@ in
example = "/var/lib/headplane";
};
proxy_auth = mkOption {
type = types.nullOr (
types.submodule {
options = {
enabled = mkEnableOption ''
delegating Headplane authentication to a trusted reverse proxy
instead of logging in through Headplane directly. Identity
headers are only trusted on requests whose client IP matches
`allowed_cidrs`; all Headscale API calls then use
`headscale.api_key_path`
'';
allowed_cidrs = mkOption {
type = types.listOf types.str;
default = [
"127.0.0.1/32"
"::1/128"
];
description = ''
Client CIDRs allowed to authenticate via the configured proxy
headers.
'';
example = [ "10.0.0.0/8" ];
};
trusted_proxy_cidrs = mkOption {
type = types.listOf types.str;
default = [
"127.0.0.1/32"
"::1/128"
];
description = ''
Direct proxy CIDRs trusted to supply `ip_header`.
'';
example = [ "127.0.0.1/32" ];
};
ip_header = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Header containing the original client IP, such as
`X-Forwarded-For` or `X-Real-IP`. Only read when the direct
socket peer matches `trusted_proxy_cidrs`.
'';
example = "X-Forwarded-For";
};
user_header = mkOption {
type = types.str;
default = "Remote-User";
description = ''
Header containing the stable authenticated user identity.
'';
example = "Remote-User";
};
email_header = mkOption {
type = types.nullOr types.str;
default = null;
description = "Optional header containing the authenticated user's email address.";
example = "Remote-Email";
};
name_header = mkOption {
type = types.nullOr types.str;
default = null;
description = "Optional header containing the authenticated user's display name.";
example = "Remote-Name";
};
picture_header = mkOption {
type = types.nullOr types.str;
default = null;
description = "Optional header containing the authenticated user's profile picture URL.";
example = "Remote-Picture";
};
};
}
);
default = null;
description = ''
Delegate Headplane authentication to a trusted reverse proxy. See the
upstream [Proxy Authentication docs](https://github.com/tale/headplane/blob/main/docs/features/proxy-auth.md).
'';
};
};
};
default = { };
@@ -445,6 +532,18 @@ in
services.headplane.settings.headscale.api_key_path must be set when the agent is enabled.
'';
}
{
assertion =
cfg.settings.server.proxy_auth == null
|| !cfg.settings.server.proxy_auth.enabled
|| cfg.settings.headscale.api_key_path != null;
message = ''
services.headplane.settings.headscale.api_key_path must be set
when services.headplane.settings.server.proxy_auth.enabled is true.
Proxy authentication requires a Headscale API key to make Headscale
API calls on behalf of proxy-authenticated users.
'';
}
];
environment = {

View File

@@ -0,0 +1,56 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.hyphanet;
useNewNames = lib.versionAtLeast config.system.stateVersion "26.11";
userName = if useNewNames then "hyphanet" else "freenet";
varDir = "/var/lib/${userName}";
in
{
options = {
services.hyphanet = {
enable = lib.mkEnableOption "Hyphanet daemon";
nice = lib.mkOption {
type = lib.types.ints.between (-20) 19;
default = 10;
description = "Set the nice level for the Hyphanet daemon";
};
};
};
imports = [
(lib.mkRenamedOptionModule [ "services" "freenet" ] [ "services" "hyphanet" ])
];
config = lib.mkIf cfg.enable {
systemd.services.hyphanet = {
description = "Hyphanet daemon";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = lib.getExe pkgs.hyphanet;
User = userName;
UMask = "0007";
WorkingDirectory = varDir;
Nice = cfg.nice;
};
};
users.users.${userName} = {
group = userName;
description = "Hyphanet daemon user";
home = varDir;
createHome = true;
};
users.groups.${userName} = { };
};
meta.maintainers = with lib.maintainers; [ nagy ];
}

View File

@@ -263,6 +263,14 @@ in
types.submodule {
inherit freeformType;
options = {
type = lib.mkOption {
type = types.str;
default = "server";
description = ''
Type of server tunnel.
See <https://docs.i2pd.website/en/latest/user-guide/tunnels/#tunnel-types>.
'';
};
host = lib.mkOption {
type = types.either types.str credType;
description = "IP address of server (on this address i2pd will send data from I2P)";
@@ -289,6 +297,14 @@ in
types.submodule {
inherit freeformType;
options = {
type = lib.mkOption {
type = types.str;
default = "client";
description = ''
Type of client tunnel.
See <https://docs.i2pd.website/en/latest/user-guide/tunnels/#tunnel-types>.
'';
};
port = lib.mkOption {
type = types.port;
description = "Port of client tunnel (on this port i2pd will receive data)";
@@ -401,10 +417,8 @@ in
gen = attr: settings: {
conf = genConfig "i2pd.conf" (credSubstituteRec attr settings);
tunconf = genTunnels "i2pd-tunnels.conf" (
lib.mapAttrs' (k: v: lib.nameValuePair "client-${k}" (v // { "type" = "client"; })) (
credSubstituteRec attr cfg.clientTunnels
)
// lib.mapAttrs' (k: v: lib.nameValuePair "server-${k}" (v // { "type" = "server"; })) (
lib.mapAttrs' (k: v: lib.nameValuePair "client-${k}" v) (credSubstituteRec attr cfg.clientTunnels)
// lib.mapAttrs' (k: v: lib.nameValuePair "server-${k}" v) (
credSubstituteRec attr cfg.serverTunnels
)
);

View File

@@ -0,0 +1,226 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
any
getExe
hasPrefix
mapAttrsToList
mkIf
literalExpression
mkEnableOption
mkMerge
mkOption
mkPackageOption
optional
optionals
;
inherit (lib.types)
attrsOf
bool
listOf
nullOr
path
str
submodule
;
cfg = config.services.iocaine;
jsonFormat = pkgs.formats.json { };
hasUDSbind = any (hasPrefix "/") (
mapAttrsToList (_server: cfg: cfg.bind) (cfg.settings.server or { })
);
ifHasSettings = optional (cfg.settings != null);
hasFirewall = cfg.settings.firewall.enable;
description = "iocaine, the deadliest poison known to AI";
in
{
options.services.iocaine = {
enable = mkEnableOption description;
package = mkPackageOption pkgs "iocaine" { };
environment = mkOption {
default = { };
type = attrsOf str;
description = "Environment variables for iocaine.";
example = literalExpression ''
{
RUST_LOG = "info";
RUST_BACKTRACE = "1";
}
'';
};
settings = mkOption {
type = nullOr (submodule {
freeformType = jsonFormat.type;
options = {
firewall.enable = mkOption {
default = false;
type = bool;
description = "Enables the firewall";
example = true;
};
};
});
default = null;
description = ''
The configuration for iocaine.
See [the configuration reference](https://iocaine.madhouse-project.org/documentation/3/configuration/)
for full documentation on the fields.
'';
example = literalExpression ''
{
server.default = {
bind = "localhost:2137";
mode = "http";
use.handler-from = "default";
};
handler.default = {
settings = {
"ai-robots-txt-path" = "/etc/iocaine/data/ai.robots.txt-robots.json";
sources = {
training-corpus = [
"/data/corpus/1984.txt"
"/data/corpus/brave-new-world.txt"
];
wordlists = [ "/data/corpus/words.txt" ];
};
};
};
}
'';
};
extraSettingsPaths = mkOption {
type = listOf path;
default = [ ];
description = "Configuration paths to run iocaine with. Useful for secrets";
example = literalExpression ''
[
"/etc/iocaine/iocaine.json"
./iocaine.json
]
'';
};
};
config = mkIf cfg.enable {
environment.etc."iocaine/iocaine.json" = mkIf (cfg.settings != null) {
source = jsonFormat.generate "iocaine.json" cfg.settings;
};
systemd.services = mkMerge [
{
iocaine = {
inherit description;
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
environment = {
HOME = "%S/home";
}
// cfg.environment;
restartTriggers =
(ifHasSettings config.environment.etc."iocaine/iocaine.json".source) ++ cfg.extraSettingsPaths;
stopIfChanged = false;
serviceConfig = {
Type = "notify";
ExecStart = toString (
[
(getExe cfg.package)
]
++ (map (path: "--config-path=${path}") (
(ifHasSettings "/etc/iocaine/iocaine.json") ++ cfg.extraSettingsPaths
))
++ [ "start" ]
);
Restart = "on-failure";
DynamicUser = true;
UMask = "0077";
LimitNOFILE = 524288;
StateDirectory = "iocaine";
WorkingDirectory = "%S/iocaine";
RuntimeDirectory = "iocaine";
ProtectSystem = "strict";
ProtectClock = true;
ProtectHostname = true;
ProtectProc = "invisible";
ProtectControlGroups = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectKernelLogs = true;
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
PrivateUsers = !hasFirewall;
SystemCallArchitectures = "native";
DevicePolicy = "closed";
LockPersonality = true;
MemoryDenyWriteExecute = false;
NoNewPrivileges = true;
RestrictAddressFamilies =
(optionals hasUDSbind [
"AF_INET"
"AF_INET6"
"AF_UNIX"
])
++ (optionals hasFirewall [ "AF_NETLINK" ]);
RestrictNamespaces = true;
RestrictRealtime = true;
SystemCallFilter = [
"@system-service"
"~@privileged"
"~@resources"
];
CapabilityBoundingSet = mkIf hasFirewall [ "CAP_NET_ADMIN" ];
AmbientCapabilities = mkIf hasFirewall [ "CAP_NET_ADMIN" ];
};
};
}
(
let
iocaineDep = {
requires = [ "iocaine.service" ];
after = [ "iocaine.service" ];
serviceConfig.SupplementaryGroups = [ "iocaine" ];
};
in
{
nginx = mkIf (config.services.nginx.enable && hasUDSbind) iocaineDep;
caddy = mkIf (config.services.caddy.enable && hasUDSbind) iocaineDep;
}
)
];
};
meta = {
maintainers = with lib.maintainers; [ poz ];
};
}

View File

@@ -0,0 +1,140 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.omada;
in
{
options = {
services.omada = {
enable = lib.mkEnableOption "Enable the Omada Software Controller service.";
package = lib.mkPackageOption pkgs "omada-software-controller" { };
user = lib.mkOption {
type = lib.types.str;
default = "omada";
description = ''
User under which the Omada Software Controller service runs.
'';
};
group = lib.mkOption {
type = lib.types.str;
default = "omada";
description = ''
Group under which the Omada Software Controller service runs.
'';
};
dataDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/omada";
description = ''
The path where the Omada Software Controller stores all data. This path must
be in sync with the omada-software-controller package (where it is hardcoded
during the build in accordance with its own `dataDir` argument).
'';
};
openFirewallDevicePorts = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to open the firewall ports required for Omada devices to communicate
with the Omada Software Controller (discovery, adoption, management, etc.).
'';
};
openFirewallWebPorts = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Whether to open the firewall ports of the web interface (8043, 8088).
'';
};
};
};
config = lib.mkIf cfg.enable {
users.groups.${cfg.group} = { };
users.users.${cfg.user} = {
description = "Omada Software Controller user";
group = cfg.group;
home = cfg.dataDir;
createHome = true;
isSystemUser = true;
};
systemd.tmpfiles.settings."10-omada" =
lib.genAttrs
(map (dir: "${cfg.dataDir}/${dir}") [
"data"
"logs"
"properties"
"work"
])
(_: {
d = {
user = cfg.user;
group = cfg.group;
};
});
systemd.services.omada = {
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
description = "Omada Software Controller";
serviceConfig = {
ExecStart = "${lib.getExe cfg.package} start";
ExecStop = "${lib.getExe cfg.package} stop";
# The control script asks for confirmation (y/n) before starting when
# it detects that MongoDB was upgraded across major versions. Answer it,
# otherwise its read loop would spin forever on an EOF stdin.
StandardInput = "data";
StandardInputText = "y";
Type = "forking";
TimeoutSec = 300;
RuntimeDirectory = "omada";
RuntimeDirectoryMode = "0755";
PIDFile = "/run/omada/omada.pid";
WorkingDirectory = cfg.dataDir;
StateDirectory = baseNameOf cfg.dataDir;
User = cfg.user;
Group = cfg.group;
Environment = [
"OMADA_USER=${cfg.user}"
];
Restart = "on-failure";
};
};
networking.firewall = {
allowedUDPPorts = lib.optionals cfg.openFirewallDevicePorts [
19810 # discovery port
29810 # discovery port
];
allowedTCPPorts =
lib.optionals cfg.openFirewallDevicePorts [
29811 # management port
29812 # adoption port
29813 # upgrade port
29814 # management port
29815 # transfer port
29816 # rtty port
29817 # device management port
]
++ lib.optionals cfg.openFirewallWebPorts [
8043 # web port (HTTPS)
8088 # web port (HTTP)
];
};
};
}

View File

@@ -95,8 +95,8 @@ in
wantedBy = [ "network.target" ];
serviceConfig = {
Type = "forking";
ExecStart = "${package}/bin/vpnserver start";
ExecStop = "${package}/bin/vpnserver stop";
ExecStart = "${cfg.dataDir}/vpnserver/vpnserver start";
ExecStop = "${cfg.dataDir}/vpnserver/vpnserver stop";
};
preStart = ''
rm -rf ${cfg.dataDir}/vpnserver/vpnserver
@@ -116,8 +116,8 @@ in
wantedBy = [ "network.target" ];
serviceConfig = {
Type = "forking";
ExecStart = "${package}/bin/vpnbridge start";
ExecStop = "${package}/bin/vpnbridge stop";
ExecStart = "${cfg.dataDir}/vpnbridge/vpnbridge start";
ExecStop = "${cfg.dataDir}/vpnbridge/vpnbridge stop";
};
preStart = ''
rm -rf ${cfg.dataDir}/vpnbridge/vpnbridge
@@ -137,8 +137,8 @@ in
wantedBy = [ "network.target" ];
serviceConfig = {
Type = "forking";
ExecStart = "${package}/bin/vpnclient start";
ExecStop = "${package}/bin/vpnclient stop";
ExecStart = "${cfg.dataDir}/vpnclient/vpnclient start";
ExecStop = "${cfg.dataDir}/vpnclient/vpnclient stop";
};
preStart = ''
rm -rf ${cfg.dataDir}/vpnclient/vpnclient

View File

@@ -79,13 +79,13 @@ in
mkAttrsOfParams
{
version = mkIntParam 0 ''
version = mkIntParam 2 ''
IKE major version to use for connection.
- 1 uses IKEv1 aka ISAKMP,
- 2 uses IKEv2.
- A connection using the default of 0 accepts both IKEv1 and IKEv2 as
responder, and initiates the connection actively with IKEv2.
- 2 uses IKEv2 (the default).
- A connection using 0 accepts both IKEv1 and IKEv2 as responder,
and initiates the connection actively with IKEv2.
'';
local_addrs = mkCommaSepListParam [ ] ''

View File

@@ -149,7 +149,28 @@ in
"2270007"
];
description = ''
List of rules that should be disabled.
List of matchers specifying which rules should be disabled.
These can be raw SID numbers or something like "group:emerging-coinminer.rules".
'';
};
dropRules = mkOption {
type = types.listOf types.str;
default = [ ];
example = literalExpression ''
[ "2274852" "4327876" "902244405" ]
'';
description = ''
List of matchers specifying which rules should be converted to drop rules.
These can be raw SID numbers or something like "group:emerging-coinminer.rules".
'';
};
reloadOnRulesetUpdate = mkOption {
type = types.bool;
default = false;
description = ''
Whether to reload Suricata if it is running after an automated ruleset update.
This is a blocking reload, and may take some time depending on the number of rules and computational power of the host.
'';
};
};
@@ -213,11 +234,20 @@ in
};
systemd.services = {
suricata-blocking-reload = lib.mkIf cfg.reloadOnRulesetUpdate {
description = "Refresh Runtime Suricata Ruleset";
serviceConfig = {
Type = "oneshot";
ExecCondition = "systemctl is-active --quiet suricata.service";
ExecStart = "${pkg}/bin/suricatasc -c reload-rules";
};
};
suricata-update = {
description = "Update Suricata Rules";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
onSuccess = lib.mkIf cfg.reloadOnRulesetUpdate [ "suricata-blocking-reload.service" ];
script =
let
@@ -230,7 +260,8 @@ in
${concatStringsSep "\n" enabledSourcesCmds}
${python.interpreter} ${pkg}/bin/suricata-update update-sources
${python.interpreter} ${pkg}/bin/suricata-update update --suricata-conf ${cfg.configFile} --no-test \
--disable-conf ${pkgs.writeText "suricata-disable-conf" "${concatStringsSep "\n" cfg.disabledRules}"}
--disable-conf ${pkgs.writeText "suricata-disable-conf" "${concatStringsSep "\n" cfg.disabledRules}"} \
--drop-conf ${pkgs.writeText "suricata-drop.conf" "${concatStringsSep "\n" cfg.dropRules}"}
'';
serviceConfig = {
Type = "oneshot";

View File

@@ -18,6 +18,47 @@ let
port
;
# `host` is a bind address, and 0.0.0.0 means "every interface" to bind(2)
# but nothing at all to connect(2). Workers run beside the server, so they
# dial it directly rather than going back out through `baseUrl` - a reverse
# proxy that may terminate TLS, require auth, or simply not be up yet.
localHost = if cfg.host == "0.0.0.0" then "127.0.0.1" else cfg.host;
# Prefect builds the SQLAlchemy URL itself from these discrete settings, so
# the password never has to be interpolated into a string that would land in
# the store. It arrives separately as PREFECT_SERVER_DATABASE_PASSWORD, from
# `databasePasswordFile`.
postgresEnvironment = [
"PREFECT_SERVER_DATABASE_DRIVER=postgresql+asyncpg"
"PREFECT_SERVER_DATABASE_HOST=${cfg.databaseHost}"
"PREFECT_SERVER_DATABASE_PORT=${cfg.databasePort}"
"PREFECT_SERVER_DATABASE_NAME=${cfg.databaseName}"
"PREFECT_SERVER_DATABASE_USER=${cfg.databaseUser}"
];
# Identical for the server and every worker, so it is written once.
hardening = {
DynamicUser = true;
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;
};
in
{
options.services.prefect = {
@@ -46,7 +87,9 @@ in
type = path;
default = "/var/lib/prefect-server";
description = ''
Specify the directory for Prefect.
Working directory for the server. Note that Prefect's own state -
including the SQLite database - lives in the unit's `StateDirectory`
rather than here, since the service runs as a `DynamicUser`.
'';
};
@@ -86,11 +129,19 @@ in
databasePasswordFile = lib.mkOption {
type = nullOr str;
default = null;
example = "/run/secrets/prefect-database-password";
description = ''
path to a file containing e.g.:
DBPASSWORD=supersecret
Path to a file containing the postgres password as an environment
variable assignment:
stored outside the nix store, read by systemd as EnvironmentFile.
```
PREFECT_SERVER_DATABASE_PASSWORD=supersecret
```
Stored outside the nix store, read by systemd as an `EnvironmentFile`.
Leave `null` when postgres authenticates the server some other way,
such as peer authentication over a unix socket.
'';
};
@@ -121,9 +172,15 @@ in
};
baseUrl = lib.mkOption {
type = nullOr str;
default = null;
description = "external url when served by a reverse proxy, e.g. `https://example.com/prefect`";
type = str;
default = "http://${localHost}:${toString cfg.port}";
defaultText = lib.literalExpression ''"http://''${host}:''${toString port}"'';
example = "https://example.com/prefect";
description = ''
External url the UI is reached at, when served by a reverse proxy.
Defaults to the address the server itself binds, which is what you
want when there is no proxy in front of it.
'';
};
};
@@ -135,44 +192,20 @@ in
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
DynamicUser = true;
serviceConfig = hardening // {
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
[ ];
]
++ lib.optionals (cfg.database == "postgres") postgresEnvironment;
# 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;
EnvironmentFile = lib.optional (
cfg.database == "postgres" && cfg.databasePasswordFile != null
) cfg.databasePasswordFile;
ExecStart = "${lib.getExe cfg.package} server start --host ${cfg.host} --port ${toString cfg.port}";
Restart = "always";
@@ -185,36 +218,21 @@ in
"prefect-worker-${poolName}" = {
description = "prefect worker for pool '${poolName}'";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
after = [
"network.target"
"prefect-server.service"
];
environment.systemPackages = cfg.package;
# A process worker shells out to prefect for every flow run it picks
# up, so the package has to be on its PATH and not merely in ExecStart.
path = [ cfg.package ];
serviceConfig = {
DynamicUser = true;
serviceConfig = hardening // {
StateDirectory = "prefect-worker-${poolName}";
Environment = [
"PREFECT_HOME=%S/prefect-worker-${poolName}"
"PREFECT_API_URL=${cfg.baseUrl}/api"
"PREFECT_API_URL=http://${localHost}:${toString cfg.port}/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;
ExecStart = ''
${lib.getExe cfg.package} worker start \
--pool ${poolName} \
@@ -226,4 +244,6 @@ in
};
}) cfg.workerPools;
};
meta.maintainers = with lib.maintainers; [ happysalada ];
}

View File

@@ -45,6 +45,17 @@ in
default = null;
};
databaseEncryptionKeysFile = mkOption {
description = ''
Path to file containing encryption key(s) to encrypt target credentials stored in database.
Should be a env-like file: `WARPGATE_ENCRYPTION_KEY=$(openssl rand -base64 32)`.
If you are rotating key, move the old key to `WARPGATE_ENCRYPTION_KEY_OLD`.
See [Encrypting credentials at rest](https://warpgate.null.page/encryption/).
'';
type = nullOr str;
default = null;
};
settings = mkOption {
description = "Warpgate configuration.";
type = submodule {
@@ -120,18 +131,6 @@ in
]
'';
};
recordings = {
enable = mkOption {
description = "Whether to enable session recording.";
default = true;
type = bool;
};
path = mkOption {
description = "Path to store session recordings.";
default = "/var/lib/warpgate/recordings";
type = str;
};
};
external_host = mkOption {
description = ''
Configure the domain name of this Warpgate instance.
@@ -160,6 +159,11 @@ in
default = "[::]:2222";
type = str;
};
proxy_protocol = mkOption {
description = "Accept HAProxy PROXY protocol v1/v2 headers from upstream reverse proxy.";
default = false;
type = bool;
};
external_host = mkOption {
description = "The SSH listener is reachable via this domain name externally.";
default = null;
@@ -201,6 +205,11 @@ in
default = "[::]:8888";
type = str;
};
proxy_protocol = mkOption {
description = "Accept HAProxy PROXY protocol v1/v2 headers from upstream reverse proxy.";
default = false;
type = bool;
};
external_host = mkOption {
description = "The HTTP listener is reachable via this domain name externally.";
default = null;
@@ -270,6 +279,88 @@ in
type = str;
};
};
rdp = {
enable = mkOption {
description = "Whether to enable RDP listener.";
default = false;
type = bool;
};
listen = mkOption {
description = "Listen endpoint of RDP listener.";
default = "[::]:3389";
type = str;
};
proxy_protocol = mkOption {
description = "Accept HAProxy PROXY protocol v1/v2 headers from the listener's peer.";
default = false;
type = bool;
};
external_host = mkOption {
description = "The RDP listener is reachable via this domain name externally.";
default = null;
type = nullOr str;
};
external_port = mkOption {
description = "The RDP listener is reachable via this port externally.";
default = null;
type = nullOr str;
};
certificate = mkOption {
description = "Path to RDP listener certificate.";
default = "/var/lib/warpgate/tls.certificate.pem";
type = str;
};
key = mkOption {
description = "Path to RDP listener private key.";
default = "/var/lib/warpgate/tls.key.pem";
type = str;
};
};
vnc = {
enable = mkOption {
description = "Whether to enable VNC listener.";
default = false;
type = bool;
};
listen = mkOption {
description = "Listen endpoint of VNC listener.";
default = "[::]:5900";
type = str;
};
proxy_protocol = mkOption {
description = "Accept HAProxy PROXY protocol v1/v2 headers from the listener's peer.";
default = false;
type = bool;
};
external_host = mkOption {
description = "The VNC listener is reachable via this domain name externally.";
default = null;
type = nullOr str;
};
external_port = mkOption {
description = "The VNC listener is reachable via this port externally.";
default = null;
type = nullOr str;
};
certificate = mkOption {
description = "Path to VNC listener certificate.";
default = "/var/lib/warpgate/tls.certificate.pem";
type = str;
};
key = mkOption {
description = "Path to VNC listener private key.";
default = "/var/lib/warpgate/tls.key.pem";
type = str;
};
enable_ard_auth = mkOption {
description = ''
Enable Apple-DH (Apple Remote Desktop / type 30) auth, which is to ensure compatibility with Apple clients.
However [connections from macOS built-in VNC client with ARD auth is not supported](https://github.com/warp-tech/warpgate/blob/47e676969a0b1e0b8456f9a5f1474d6c58648c4f/warpgate-protocol-vnc/src/server/rfb.rs#L8-L10).
'';
default = false;
type = bool;
};
};
mysql = {
enable = mkOption {
description = "Whether to enable MySQL listener.";
@@ -281,6 +372,11 @@ in
default = "[::]:33306";
type = str;
};
proxy_protocol = mkOption {
description = "Accept HAProxy PROXY protocol v1/v2 headers from upstream reverse proxy.";
default = false;
type = bool;
};
external_host = mkOption {
description = "The MySQL listener is reachable via this domain name externally.";
default = null;
@@ -301,6 +397,14 @@ in
default = "/var/lib/warpgate/tls.key.pem";
type = str;
};
advertised_version = mkOption {
description = ''
The server version advertised to clients during the handshake.
Warpgate can't auto-match the target's version since the target is only known after the handshake, but Warpgate's clients use it to pick a protocol dialect.
'';
default = "8.0.3-Warpgate";
type = str;
};
};
postgres = {
enable = mkOption {
@@ -313,6 +417,11 @@ in
default = "[::]:55432";
type = str;
};
proxy_protocol = mkOption {
description = "Accept HAProxy PROXY protocol v1/v2 headers from upstream reverse proxy.";
default = false;
type = bool;
};
external_host = mkOption {
description = "The PostgreSQL listener is reachable via this domain name externally.";
default = null;
@@ -345,6 +454,11 @@ in
default = "[::]:8443";
type = str;
};
proxy_protocol = mkOption {
description = "Accept HAProxy PROXY protocol v1/v2 headers from upstream reverse proxy.";
default = false;
type = bool;
};
external_host = mkOption {
description = "The Kubernetes listener is reachable via this domain name externally.";
default = null;
@@ -420,36 +534,45 @@ in
any
map
head
optional
reverseList
;
inherit (lib.strings) splitString toIntBase10;
inherit (lib.strings)
optionalString
splitString
toIntBase10
;
preStartScript = pkgs.writers.writeBash "warpgate-init" ''
CFGFILE=/var/lib/warpgate/config.yaml
renderedYamlConfig = yaml.generate "warpgate-config" cfg.settings;
startupScript = pkgs.writeShellScript "warpgate-run" ''
CFGFILE=$STATE_DIRECTORY/config.yaml
if [ ! -O $CFGFILE ] || [ ! -s $CFGFILE ]; then
INITPWD=$(tr -dc 'A-Za-z0-9!?%=' </dev/urandom 2>/dev/null | head -c 16)
${lib.getExe cfg.package} \
--config $CFGFILE unattended-setup \
--data-path /var/lib/warpgate \
--data-path $STATE_DIRECTORY \
--http-port 8888 \
--admin-password $INITPWD
fi
${
if cfg.databaseUrlFile != null then
''
sed -e '/^database_url: null/d' ${yaml.generate "warpgate-config" cfg.settings} > $CFGFILE
cat /run/credentials/warpgate.service/databaseUrl >> $CFGFILE
''
else
"cp --no-preserve=ownership ${yaml.generate "warpgate-config" cfg.settings} $CFGFILE"
}
cp --no-preserve=ownership ${renderedYamlConfig} $CFGFILE
${optionalString (cfg.databaseUrlFile != null) ''
sed -e '/^database_url: null/d' ${renderedYamlConfig} > $CFGFILE
cat $CREDENTIALS_DIRECTORY/databaseUrl >> $CFGFILE
''}
${optionalString (cfg.databaseEncryptionKeysFile != null) ''
set -a
source $CREDENTIALS_DIRECTORY/dbEncryptionKeys
set +a
''}
${lib.getExe cfg.package} --config $CFGFILE run
'';
bindOnPrivilegedPorts = any (x: toIntBase10 x < 1025) (
map (x: head (reverseList (splitString ":" x))) (
[ cfg.settings.http.listen ]
++ lib.optional cfg.settings.ssh.enable cfg.settings.ssh.listen
++ lib.optional cfg.settings.mysql.enable cfg.settings.mysql.listen
++ lib.optional cfg.settings.postgres.enable cfg.settings.postgres.listen
++ optional cfg.settings.ssh.enable cfg.settings.ssh.listen
++ optional cfg.settings.mysql.enable cfg.settings.mysql.listen
++ optional cfg.settings.postgres.enable cfg.settings.postgres.listen
)
);
in
@@ -467,6 +590,10 @@ in
assertion = !(lib.hasAttr "config_provider" cfg.settings);
message = "`services.warpgate.settings.config_provider` is a legacy option that has been removed since 0.14.0. Please do not set this option.";
}
{
assertion = !(lib.hasAttr "recordings" cfg.settings);
message = "`services.warpgate.settings.recordings` has been deprecated by S3 recording storage support in 0.27.0. Please remove this section from your config and set it from admin UI.";
}
];
environment.systemPackages = [ cfg.package ];
@@ -474,14 +601,16 @@ in
systemd.services.warpgate = {
description = "Warpgate smart bastion";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
startLimitBurst = 5;
serviceConfig = {
LoadCredential = "${
if cfg.databaseUrlFile != null then "databaseUrl:${cfg.databaseUrlFile}" else ""
}";
ExecStartPre = preStartScript;
ExecStart = "${lib.getExe cfg.package} --config /var/lib/warpgate/config.yaml run";
LoadCredential =
optional (cfg.databaseUrlFile != null) "databaseUrl:${cfg.databaseUrlFile}"
++ optional (
cfg.databaseEncryptionKeysFile != null
) "dbEncryptionKeys:${cfg.databaseEncryptionKeysFile}";
ExecStart = startupScript;
DynamicUser = true;
RestartSec = 3;
Restart = "on-failure";

View File

@@ -11,6 +11,7 @@ with lib;
let
cfg = config.services.coder;
name = "coder";
configRefUrl = "https://coder.com/docs/@main/admin/setup/configuration-reference";
in
{
options = {
@@ -82,7 +83,10 @@ in
environment = {
extra = mkOption {
type = types.attrs;
description = "Extra environment variables to pass run Coder's server with. See Coder documentation.";
description = ''
Extra environment variables to pass run Coder's server with.
See [Coder configuration reference](${configRefUrl}).
'';
default = { };
example = {
CODER_OAUTH2_GITHUB_ALLOW_SIGNUPS = true;
@@ -91,7 +95,10 @@ in
};
file = mkOption {
type = types.nullOr types.path;
description = "Systemd environment file to add to Coder.";
description = ''
Systemd environment file to add to Coder.
See [Coder configuration reference](${configRefUrl}).
'';
default = null;
};
};

View File

@@ -0,0 +1,371 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.flame;
settingsFormat = pkgs.formats.json { };
# Accepts either a list of strings or a raw semicolon-separated string.
schemaToStr = x: if builtins.isList x then lib.concatStringsSep ";" x else x;
# Needed to prepopulate DB
sqlQuote = s: "'" + builtins.replaceStrings [ "'" ] [ "''" ] s + "'";
seedSql = pkgs.writeText "flame-seed.sql" ''
${lib.optionalString (cfg.apps != [ ] || cfg.categories != [ ]) ''
DELETE FROM bookmarks;
DELETE FROM categories;
DELETE FROM apps;
''}
${lib.concatMapStringsSep "\n" (app: ''
INSERT INTO apps (name, url, icon, description, isPinned, createdAt, updatedAt)
VALUES (${sqlQuote app.name}, ${sqlQuote app.url}, ${sqlQuote app.icon}, ${sqlQuote app.description}, ${
if app.isPinned then "1" else "0"
}, datetime('now'), datetime('now'));
'') cfg.apps}
${lib.concatMapStringsSep "\n" (cat: ''
INSERT INTO categories (name, isPinned, createdAt, updatedAt)
VALUES (${sqlQuote cat.name}, ${
if cat.isPinned then "1" else "0"
}, datetime('now'), datetime('now'));
${lib.concatMapStringsSep "\n" (bm: ''
INSERT INTO bookmarks (name, url, icon, categoryId, createdAt, updatedAt)
VALUES (${sqlQuote bm.name}, ${sqlQuote bm.url}, ${sqlQuote bm.icon}, (SELECT id FROM categories WHERE name = ${sqlQuote cat.name} ORDER BY id DESC LIMIT 1), datetime('now'), datetime('now'));
'') cat.bookmarks}
'') cfg.categories}
'';
cssFile = pkgs.writeText "flame-custom.css" cfg.customCSS;
# Build-time symlink farm of everything Flame ships except data/ and
# public/, which are left as empty placeholders here and populated at
# runtime (data/ is real state; public/ is refreshed from cfg.package
# on every start, since it holds built client assets).
appTree = pkgs.runCommand "flame-app-tree" { } ''
mkdir -p $out
for entry in ${cfg.package}/lib/flame/*; do
name=$(basename "$entry")
if [ "$name" != data ] && [ "$name" != public ]; then
ln -s "$entry" "$out/$name"
fi
done
mkdir -p $out/data $out/public
'';
# WEATHER_API_KEY is deliberately excluded here; it's injected at
# runtime from `weatherApiKeyFile` so it never touches the Nix store.
settingsFile = settingsFormat.generate "flame-settings.json" (
lib.filterAttrs (n: _: n != "weatherApiKeyFile") cfg.settings
// lib.optionalAttrs (cfg.settings ? greetingsSchema) {
greetingsSchema = schemaToStr cfg.settings.greetingsSchema;
}
// lib.optionalAttrs (cfg.settings ? daySchema) {
daySchema = schemaToStr cfg.settings.daySchema;
}
// lib.optionalAttrs (cfg.settings ? monthSchema) {
monthSchema = schemaToStr cfg.settings.monthSchema;
}
);
in
{
meta.maintainers = with lib.maintainers; [ DerGrumpf ];
options.services.flame = {
enable = lib.mkEnableOption "Flame, a self-hosted startpage for your server";
package = lib.mkPackageOption pkgs "flame" { };
port = lib.mkOption {
type = lib.types.port;
default = 5005;
description = "Port on which to serve the Flame web interface.";
};
passwordFile = lib.mkOption {
type = lib.types.path;
description = ''
Path to a file containing the password to log in to Flame's settings panel.
This is the recommended option as it avoids storing the password in the Nix store.
Compatible with sops-nix and agenix.
'';
example = "/run/secrets/flame-password";
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to open the firewall for the port used by Flame.";
};
customCSS = lib.mkOption {
type = lib.types.lines;
default = "";
description = ''
Custom CSS injected into Flame's UI, written to
{file}`public/flame.css` on every service start. Can also be used
to define a fully custom theme via CSS custom properties — see
[Flame's Custom CSS wiki page](https://github.com/pawelmalak/flame/wiki/Custom-CSS).
'';
example = ''
.Home_SettingsButton__Qvn8C {
border-radius: 0 !important;
}
'';
};
categories = lib.mkOption {
type = lib.types.listOf (
lib.types.submodule {
options = {
name = lib.mkOption {
type = lib.types.str;
description = "Name of the bookmark category.";
};
isPinned = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether the category is pinned by default.";
};
bookmarks = lib.mkOption {
default = [ ];
description = "Bookmarks belonging to this category.";
type = lib.types.listOf (
lib.types.submodule {
options = {
name = lib.mkOption {
type = lib.types.str;
description = "Name of the bookmark.";
};
url = lib.mkOption {
type = lib.types.str;
description = "URL of the bookmark.";
};
icon = lib.mkOption {
type = lib.types.str;
default = "";
description = "Icon name or URL for the bookmark.";
};
};
}
);
};
};
}
);
default = [ ];
description = ''
Bookmark categories and their bookmarks. When non-empty, this
fully replaces the contents of Flame's `categories` and
`bookmarks` tables on every service start — any bookmarks added
through the web UI will not persist across restarts.
'';
example = [
{
name = "Dev";
bookmarks = [
{
name = "GitHub";
url = "https://github.com";
}
];
}
];
};
apps = lib.mkOption {
type = lib.types.listOf (
lib.types.submodule {
options = {
name = lib.mkOption {
type = lib.types.str;
description = "Name of the app.";
};
url = lib.mkOption {
type = lib.types.str;
description = "URL of the app.";
};
icon = lib.mkOption {
type = lib.types.str;
default = "cancel";
description = "Icon name or URL for the app.";
};
description = lib.mkOption {
type = lib.types.str;
default = "";
description = "Short description shown for the app.";
};
isPinned = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether the app is pinned by default.";
};
};
}
);
default = [ ];
description = ''
Applications shown on the dashboard. When non-empty, this fully
replaces the contents of Flame's `apps` table on every service
start — any apps added through the web UI will not persist
across restarts.
'';
example = [
{
name = "Router";
url = "http://192.168.1.1";
}
];
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = settingsFormat.type;
options = {
weatherApiKeyFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Path to a file containing the API key obtained from https://www.weatherapi.com used for
Flame's weather widget.
Compatible with sops-nix and agenix.
'';
example = "/run/secrets/flame-weather-api-key";
};
};
};
default = { };
description = ''
Flame settings, written to Flame's settings JSON on every service
start. Accepts any key Flame's settings API supports; see
[Flame's source](https://github.com/pawelmalak/flame/blob/master/client/src/context/context.js)
for the current schema, since Flame does not publish separate
settings documentation.
`greetingsSchema`, `daySchema`, and `monthSchema` accept either a
list of strings or a single semicolon-separated string.
'';
example = {
lat = 52.52;
long = 13.405;
customTitle = "My Dashboard";
hideHeader = true;
};
};
};
config = lib.mkIf cfg.enable {
systemd.services = {
flame-seed = lib.mkIf (cfg.apps != [ ] || cfg.categories != [ ]) {
description = "Seed Flame apps and bookmarks";
after = [ "flame.service" ];
requires = [ "flame.service" ];
wantedBy = [ "flame.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
DynamicUser = true;
StateDirectory = "flame";
};
script = ''
for i in $(seq 1 30); do
if ${lib.getExe pkgs.sqlite} /var/lib/flame/app/data/db.sqlite \
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='apps';" | grep -q 1; then
break
fi
sleep 1
done
${lib.getExe pkgs.sqlite} /var/lib/flame/app/data/db.sqlite < ${seedSql}
'';
};
flame = {
description = "Flame, a self-hosted startpage for your server";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
preStart = ''
for entry in ${appTree}/*; do
name=$(basename "$entry")
if [ "$name" != data ] && [ "$name" != public ]; then
ln -sfn "$entry" /var/lib/flame/app/"$name"
fi
done
for entry in /var/lib/flame/app/data /var/lib/flame/app/public; do
if [ -L "$entry" ]; then
rm -f "$entry"
fi
done
mkdir -p /var/lib/flame/app/data/uploads /var/lib/flame/app/public
if [ ! -f /var/lib/flame/app/data/.secret ]; then
${lib.getExe pkgs.openssl} rand -hex 32 > /var/lib/flame/app/data/.secret
fi
chmod 644 /var/lib/flame/app/data/.secret
cp -r ${cfg.package}/lib/flame/public/. /var/lib/flame/app/public/
chmod -R u+w /var/lib/flame/app/public
install -m644 ${cssFile} /var/lib/flame/app/data/flame.css
${lib.getExe pkgs.jq} -n --slurpfile base ${cfg.package}/lib/flame/utils/init/initialConfig.json \
'$base[0]' > /var/lib/flame/app/data/config.json.tmp
${lib.optionalString (cfg.settings.weatherApiKeyFile != null) ''
weatherApiKey=$(cat ${cfg.settings.weatherApiKeyFile})
${lib.getExe pkgs.jq} --arg key "$weatherApiKey" '.WEATHER_API_KEY = $key' \
${settingsFile} > /var/lib/flame/app/data/settings-with-key.json
''}
${lib.getExe pkgs.jq} -s '.[0] * .[1]' \
/var/lib/flame/app/data/config.json.tmp \
${
if cfg.settings.weatherApiKeyFile != null then
"/var/lib/flame/app/data/settings-with-key.json"
else
settingsFile
} \
> /var/lib/flame/app/data/config.json
rm -f /var/lib/flame/app/data/config.json.tmp
chmod u+w /var/lib/flame/app/data/config.json
'';
serviceConfig = {
DynamicUser = true;
StateDirectory = [
"flame"
"flame/app"
];
WorkingDirectory = "/var/lib/flame/app";
Environment = [
"PORT=${toString cfg.port}"
"NODE_ENV=production"
"VERSION=${cfg.package.version}"
];
LoadCredential = [ "flame-password:${cfg.passwordFile}" ];
Restart = "always";
NoNewPrivileges = true;
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
CapabilityBoundingSet = "";
};
script = ''
export PASSWORD="$(cat "$CREDENTIALS_DIRECTORY/flame-password")"
exec ${lib.getExe pkgs.nodejs} --preserve-symlinks --preserve-symlinks-main server.js
'';
};
};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.port ];
};
};
}

View File

@@ -50,9 +50,6 @@ let
mkOption
mkEnableOption
;
postgresqlPackage =
if cfg.database.enable then config.services.postgresql.package else pkgs.postgresql;
in
{
imports = [
@@ -259,6 +256,29 @@ in
default = "immich";
description = "The database user for immich.";
};
package = mkOption {
type = types.package;
default =
if config.services.postgresql.enable then config.services.postgresql.package else pkgs.postgresql;
defaultText = lib.literalExpression ''
if config.services.postgresql.enable then
config.services.postgresql.package
else
pkgs.postgresql
'';
example = lib.literalExpression "pkgs.postgresql_18";
description = ''
The postgresql package providing the client programs that immich uses,
most notably the `pg_dumpall` of its database backup job.
These programs refuse to talk to a server that is newer than
themselves, so this must not be older than the server reachable at
{option}`services.immich.database.host`. It is derived from
{option}`services.postgresql.package` whenever the postgresql module
is enabled on this host; set it explicitly when the database lives on
another machine.
'';
};
};
redis = {
enable = mkEnableOption "a redis cache for use with immich" // {
@@ -337,7 +357,7 @@ in
in
[
''
${lib.getExe' postgresqlPackage "psql"} -d "${cfg.database.name}" -f "${sqlFile}"
${lib.getExe' cfg.database.package "psql"} -d "${cfg.database.name}" -f "${sqlFile}"
''
];
@@ -407,7 +427,7 @@ in
path = [
# gzip and pg_dumpall are used by the backup service
pkgs.gzip
postgresqlPackage
cfg.database.package
];
preStart = mkIf (cfg.settings != null) secretsReplacement.script;

View File

@@ -332,6 +332,18 @@ in
assertion = cfg.cron.enable -> cfg.cron.key != null;
message = ''services.invoiceplane.sites."${hostName}".cron.key must be set in order to use cron service.'';
}
{
assertion =
(lib.versionAtLeast (pkg hostName cfg).version "1.7.2" && cfg.invoiceTemplates != [ ])
-> cfg.settings ? CUSTOM_INVOICE_TEMPLATES_PDF;
message = ''services.invoiceplane.sites."${hostName}".invoiceTemplates is set but settings.CUSTOM_INVOICE_TEMPLATES_PDF is not. Since InvoicePlane >= 1.7.2 (current: ${cfg.package.version}), the filename of the custom invoice template PHP file must be explicitly whitelisted via settings.CUSTOM_INVOICE_TEMPLATES_PDF, otherwise it will not be picked up.'';
}
{
assertion =
(lib.versionAtLeast (pkg hostName cfg).version "1.7.2" && cfg.quoteTemplates != [ ])
-> cfg.settings ? CUSTOM_QUOTE_TEMPLATES_PDF;
message = ''services.invoiceplane.sites."${hostName}".quoteTemplates is set but settings.CUSTOM_QUOTE_TEMPLATES_PDF is not. Since InvoicePlane >= 1.7.2 (current: ${cfg.package.version}), the filename of the custom quote template PHP file must be explicitly whitelisted via settings.CUSTOM_QUOTE_TEMPLATES_PDF, otherwise it will not be picked up.'';
}
]) eachSite
);

View File

@@ -28,12 +28,12 @@ let
]}
}
'';
passAsFile = [ "serversList" ];
__structuredAttrs = true;
}
''
cp -r ${cfg.package}/assets $out
chmod +w "$out/servers_list.js"
cp "$serversListPath" "$out/servers_list.js"
printf "%s" "$serversList" > "$out/servers_list.js"
substitute ${cfg.package}/assets/index.html $out/index.html \
--replace-fail "s.setParameter(\"telemetry_level\",\"basic\"); //enable telemetry" "override_settings();" \
--replace-fail "LibreSpeed Example" ${lib.escapeShellArg (lib.escapeXML cfg.frontend.pageTitle)} \

View File

@@ -50,10 +50,28 @@ in
default = false;
description = ''
Disable HTTP token authentication with requests.
WARNING: This will make your instance vulnerable! Only turn this on if you are ONLY connecting from localhost.
WARNING: This will make your instance vulnerable!
Only turn this on if nothing but trusted local clients can reach the API.
Note that web pages open in a browser on this machine also count as local
callers; restrict allowed_origins if you disable auth.
'';
};
allowed_origins = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "*" ];
description = ''
Origins allowed to call the API from a browser.
This is a CORS allowlist, not an auth mechanism: it only governs which
web pages a browser will let read this API's responses.
The default "*" means any site open in your browser can send requests to
this instance, which matters most when disable_auth is on. Restrict this to
your own frontends to close that off, or use an empty list to block all
browser (cross-origin) callers.
'';
example = [ "http://localhost:8000" ];
};
disable_fetch_requests = lib.mkOption {
type = lib.types.bool;
default = false;
@@ -89,6 +107,16 @@ in
'';
example = 0;
};
access_log = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Log every HTTP request with client address, method, path and status.
Generation requests are already logged in detail; this adds the rest,
such as model list and health polls.
'';
};
};
logging = {
@@ -110,6 +138,15 @@ in
description = "Enable request logging. NOTE: Only use this for debugging!";
};
log_timestamps = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Prefix console log lines with the time of day.
The log files under logs/ always carry full timestamps.
'';
};
log_chat_completion_requests = lib.mkOption {
type = lib.types.bool;
default = false;
@@ -275,7 +312,7 @@ in
default = [ ];
description = ''
List of VRAM sizes to split between GPUs, in GB.
Used with tensor parallelism.
Used both with and without tensor parallelism.
'';
example = [
16
@@ -289,10 +326,49 @@ in
description = ''
Number of mixture-of-expert layers to offload to CPU inference.
Only affects MoE models. Set a large value such as 999 to offload all layers.
Mutually exclusive with cpu_moe_split_experts.
'';
example = 999;
};
cpu_moe_split_experts = lib.mkOption {
type = lib.types.ints.unsigned;
default = 0;
description = ''
Number of routed experts per MoE layer to offload to CPU inference.
Unlike cpu_moe_offload_layers, this splits every MoE layer instead of
offloading whole layers: the coldest experts are kept in system RAM and
computed on the CPU, overlapping each layer's own GPU compute, with dynamic
placement keeping hot experts in VRAM.
Mutually exclusive with cpu_moe_offload_layers; not supported with tensor
parallelism.
'';
example = 4;
};
cpu_moe_threads = lib.mkOption {
type = lib.types.nullOr lib.types.ints.unsigned;
default = null;
description = ''
Worker thread count for CPU MoE inference.
Applies to both cpu_moe_offload_layers and cpu_moe_split_experts. When null,
defers to the EXL3_MOE_CPU_THREADS environment variable, then half the CPU core
count.
'';
example = 8;
};
ngram_ram = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Load a model's n-gram embedding table fully into system RAM.
Only affects PLE models with n-gram embeddings (e.g. Qwen3.8-Flash-Next).
By default the table is streamed from disk during inference; loading it into
RAM avoids per-token disk reads at the cost of tens of GB of system memory.
'';
};
rope_scale = lib.mkOption {
type = lib.types.nullOr lib.types.number;
default = 1.0;
@@ -381,6 +457,16 @@ in
description = "Enables vision support if the model supports it.";
};
vision_offload = lib.mkOption {
type = lib.types.bool;
default = false;
description = ''
Keep the vision model's weights in system RAM instead of VRAM.
Weights are stored in pinned host memory and streamed to the GPU during
inference, trading vision speed for VRAM. Only applies when vision is enabled.
'';
};
template_vars_default = lib.mkOption {
type = lib.types.attrsOf lib.types.anything;
default = { };
@@ -455,6 +541,33 @@ in
'';
};
reasoning_budget_tokens = lib.mkOption {
type = lib.types.nullOr lib.types.int;
default = null;
description = ''
Default reasoning token budget.
When a request's reasoning content exceeds the budget, the server forces the
end of the reasoning phase by injecting reasoning_budget_message followed by
the model's end-of-reasoning tokens. 0 ends reasoning as soon as it starts;
null or a negative value disables the budget. Overridable per request via
reasoning_budget_tokens (aliases: reasoning_budget, thinking_budget,
thinking_token_budget) or reasoning.max_tokens.
Requires a reasoning format: reasoning tags, Harmony or Muse Glimmer.
'';
example = 1024;
};
reasoning_budget_message = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Text injected before the end-of-reasoning tokens when the reasoning budget is
exhausted. When null, only the end-of-reasoning tokens are forced.
Overridable per request via reasoning_budget_message.
'';
example = "Time to answer.";
};
tool_format = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
@@ -475,6 +588,17 @@ in
true. When active, supersedes the reasoning and tool format settings.
'';
};
muse_glimmer = lib.mkOption {
type = lib.types.nullOr lib.types.bool;
default = null;
description = ''
Parse responses in the Muse Glimmer message format.
Auto-detected from the model's special tokens when null; set to true or false
to override. Setting tool_format to "muse_glimmer" is equivalent to setting
this to true. When active, supersedes the reasoning and tool format settings.
'';
};
};
draft_model = {
@@ -595,10 +719,13 @@ in
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Select a sampler override preset, found in the sampler-overrides folder.
Select a sampler override preset, found in the sampler_overrides folder of the
package.
This overrides default fallbacks for sampler values that are passed to the API.
NOTE: "safe_defaults" is noob friendly and provides fallbacks for frontends that
don't send sampling parameters. Leave this null for any advanced usage.
NOTE: "safe_defaults" provides llama.cpp-style fallbacks (temperature 0.8,
top_k 40, top_p 0.95, min_p 0.05) for frontends that don't send sampling
parameters. Leaving this null means no fallbacks at all.
A preset that cannot be found makes TabbyAPI exit at startup.
'';
example = "safe_defaults";
};
@@ -654,14 +781,28 @@ in
example = 4096;
};
sysmem_multimodal_cache = lib.mkOption {
type = lib.types.ints.unsigned;
default = 1024;
description = ''
Size of the image embedding cache in system memory, in MB.
Encoded images are kept so repeated turns of a conversation don't re-run the
vision model. Images already in use by a request are never evicted; a context
whose images exceed the budget is cached only partially, with a warning.
Only applies when vision is enabled.
'';
example = 4096;
};
cuda_malloc_async = lib.mkOption {
type = lib.types.bool;
default = true;
default = false;
description = ''
Use the cudaMallocAsync backend in Torch.
Enabling this is generally preferable, but it may cause issues with certain
workloads. Try disabling it if you experience intermittent OoM errors. If false,
Torch will use the allocator defined by the system environment.
Use the cudaMallocAsync allocator backend in Torch.
When false, the allocator is left to the environment: unless
PYTORCH_CUDA_ALLOC_CONF is set, ExLlamaV3 enables expandable segments in
Torch's native allocator, which performs better than cudaMallocAsync.
Enable this to force the cudaMallocAsync backend instead.
'';
};
};
@@ -689,6 +830,14 @@ in
services.tabbyapi.settings.model.template_vars_force.enable_thinking = true;
'';
}
{
assertion =
!(cfg.settings.model.cpu_moe_offload_layers > 0 && cfg.settings.model.cpu_moe_split_experts > 0);
message = ''
services.tabbyapi.settings.model.cpu_moe_offload_layers and
services.tabbyapi.settings.model.cpu_moe_split_experts are mutually exclusive.
'';
}
];
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [
cfg.settings.network.port
@@ -706,6 +855,10 @@ in
TRITON_CACHE_DIR = "/tmp/triton";
};
preStart = ''
ln -sfn ${cfg.package}/share/tabbyapi/sampler_overrides sampler_overrides
'';
serviceConfig = {
ExecStart = "${lib.getExe cfg.package} --config=${configFile}";
Restart = "on-failure";

View File

@@ -0,0 +1,239 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.zerobyte;
in
{
meta.maintainers = with lib.maintainers; [ pbek ];
options.services.zerobyte = {
enable = lib.mkEnableOption "Zerobyte, backup automation for self-hosters built on top of restic";
package = lib.mkPackageOption pkgs "zerobyte" { };
user = lib.mkOption {
type = lib.types.str;
default = "zerobyte";
description = "User account under which Zerobyte runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "zerobyte";
description = "Group under which Zerobyte runs.";
};
appSecretFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/run/secrets/zerobyte-app-secret";
description = ''
Path to a file containing the application secret (32–256 characters),
used to encrypt sensitive data in the database. Generate one with
`openssl rand -hex 32`.
This should not be a path in the Nix store. The file is passed to the
service via systemd credentials.
'';
};
dataDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/zerobyte";
description = ''
Directory used to store the database, encryption keys, local
repositories, volume mounts and the restic cache.
Do not point this to a network share, this will cause permission
issues and strong performance degradation.
'';
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType = lib.types.attrsOf (
lib.types.oneOf [
lib.types.bool
lib.types.int
lib.types.str
]
);
};
default = { };
example = {
BASE_URL = "https://zerobyte.example.com";
LOG_LEVEL = "debug";
GOMAXPROCS = 2;
TRUST_PROXY = true;
};
description = ''
Zerobyte configuration passed as environment variables. See
<https://github.com/nicotsx/zerobyte#configuration> for the available
settings.
`BASE_URL` is required. It is highly discouraged to expose Zerobyte
directly to the internet; bind `HOST` to localhost and use a secure
tunnel or an authenticating reverse proxy instead.
Do not put secrets here; use
[](#opt-services.zerobyte.environmentFile) instead.
'';
};
environmentFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
example = "/run/secrets/zerobyte.env";
description = ''
Environment file loaded by systemd, which may be used to pass secrets
such as `APP_SECRET` to Zerobyte without putting them into the Nix
store.
'';
};
provisioningFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Path to a JSON file with operator-managed repositories and volumes to
sync at startup. See
<https://zerobyte.app/docs/guides/provisioning> for the format.
This may contain secrets, so it should not be a path in the Nix store.
The file is passed to the service via systemd credentials.
'';
};
extraPackages = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = with pkgs; [
cifs-utils
davfs2
fuse3
nfs-utils
openssh
sshfs
util-linux
];
defaultText = lib.literalExpression "with pkgs; [ cifs-utils davfs2 fuse3 nfs-utils openssh sshfs util-linux ]";
description = ''
Extra packages added to the `PATH` of the Zerobyte service. The
default contains the tools needed to mount NFS, SMB, WebDAV and SFTP
volumes. Add `shoutrrr` here if you want notifications to be
delivered.
'';
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to open the firewall for the Zerobyte web interface.";
};
};
config = lib.mkIf cfg.enable {
services.zerobyte.settings = {
NODE_ENV = lib.mkDefault "production";
# The generated web server binds to all interfaces and port 3000 when
# these are unset. Bind to 127.0.0.1 and use port 4096 like upstream:
# https://github.com/nicotsx/zerobyte/blob/main/Dockerfile
HOST = lib.mkDefault "127.0.0.1";
PORT = lib.mkDefault 4096;
RESTIC_HOSTNAME = lib.mkDefault config.networking.hostName;
ZEROBYTE_DATABASE_URL = "${cfg.dataDir}/data/zerobyte.db";
RESTIC_PASS_FILE = "${cfg.dataDir}/data/restic.pass";
ZEROBYTE_REPOSITORIES_DIR = "${cfg.dataDir}/repositories";
ZEROBYTE_VOLUMES_DIR = "${cfg.dataDir}/volumes";
RESTIC_CACHE_DIR = "${cfg.dataDir}/restic/cache";
RCLONE_CONFIG_DIR = "${cfg.dataDir}/rclone";
ENABLE_LOCAL_AGENT = lib.mkDefault true;
}
// lib.optionalAttrs (cfg.provisioningFile != null) {
PROVISIONING_PATH = "%d/provisioning.json";
}
// lib.optionalAttrs (cfg.appSecretFile != null) {
APP_SECRET_FILE = "%d/app-secret";
};
assertions = [
{
assertion = cfg.settings ? BASE_URL;
message = "services.zerobyte.settings.BASE_URL must be set.";
}
{
assertion = cfg.appSecretFile != null || cfg.environmentFile != null || cfg.settings ? APP_SECRET;
message = ''
services.zerobyte: A secret is required to encrypt sensitive data in
the database. Set `services.zerobyte.appSecretFile` or provide
`APP_SECRET` via `services.zerobyte.environmentFile`.
'';
}
{
assertion = !(cfg.settings ? APP_SECRET);
message = ''
services.zerobyte.settings.APP_SECRET would expose the secret in the
Nix store. Use `services.zerobyte.appSecretFile` or
`services.zerobyte.environmentFile` instead.
'';
}
];
systemd.services.zerobyte = {
description = "Zerobyte backup automation";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
environment = lib.mapAttrs (
_: value: if lib.isBool value then lib.boolToString value else toString value
) cfg.settings;
path = [ cfg.package ] ++ cfg.extraPackages;
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.group;
ExecStart = lib.getExe cfg.package;
Restart = "on-failure";
StateDirectory = lib.mkIf (lib.hasPrefix "/var/lib/" cfg.dataDir) (
lib.removePrefix "/var/lib/" cfg.dataDir
);
EnvironmentFile = lib.mkIf (cfg.environmentFile != null) cfg.environmentFile;
LoadCredential =
lib.optional (cfg.appSecretFile != null) "app-secret:${cfg.appSecretFile}"
++ lib.optional (cfg.provisioningFile != null) "provisioning.json:${cfg.provisioningFile}";
# The local agent and the volume mount backends need these to perform
# NFS, SMB, WebDAV and SFTP mounts.
AmbientCapabilities = [ "CAP_SYS_ADMIN" ];
CapabilityBoundingSet = [ "CAP_SYS_ADMIN" ];
NoNewPrivileges = true;
PrivateTmp = true;
ProtectHome = lib.mkDefault false;
ProtectSystem = lib.mkDefault "full";
RestartSec = "10s";
UMask = "0077";
};
};
users.users = lib.mkIf (cfg.user == "zerobyte") {
zerobyte = {
isSystemUser = true;
group = cfg.group;
home = cfg.dataDir;
};
};
users.groups = lib.mkIf (cfg.group == "zerobyte") { zerobyte = { }; };
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ (lib.toInt (toString cfg.settings.PORT)) ];
};
};
}

View File

@@ -7,6 +7,8 @@
let
cfg = config.boot.initrd.nix-store-veritysetup;
json = pkgs.formats.json { };
in
{
meta.maintainers = with lib.maintainers; [ nikstur ];
@@ -27,6 +29,15 @@ in
contents = {
"/etc/systemd/system-generators/nix-store-veritysetup-generator".source =
"${lib.getExe pkgs.nix-store-veritysetup-generator}";
"/etc/systemd/generator-environment.json".source =
json.generate "systemd-generator-environment.json"
{
SYSTEMD_VERITYSETUP_PATH = "${config.boot.initrd.systemd.package}/lib/systemd/systemd-veritysetup";
};
"/etc/systemd/system-environment-generators/env-generator".source =
"${config.system.nixos-init.package}/bin/env-generator";
};
storePaths = [

View File

@@ -63,6 +63,7 @@ in
nixStoreMountOpts = mkOption {
type = types.listOf types.nonEmptyStr;
default = [
"x-initrd.mount"
"ro"
"nodev"
"nosuid"

View File

@@ -772,7 +772,10 @@ in
];
services.initrd-nixos-activation = lib.mkIf (!config.system.nixos-init.enable) {
after = [ "initrd-switch-root.target" ];
after = [
"initrd-find-nixos-closure.service"
"initrd-switch-root.target"
];
requiredBy = [ "initrd-switch-root.service" ];
before = [ "initrd-switch-root.service" ];
unitConfig.DefaultDependencies = false;

View File

@@ -15,11 +15,12 @@ let
mkIf
optionals
mkDefault
nameValuePair
listToAttrs
filterAttrs
mapAttrsToList
foldl'
getExe
escape
versionOlder
;
inInitrd = config.boot.initrd.supportedFilesystems.btrfs or false;
@@ -149,66 +150,116 @@ in
)
);
# TODO: Did not manage to do it via the usual btrfs-scrub@.timer/.service
# template units due to problems enabling the parameterized units,
# so settled with many units and templating via nix for now.
# https://github.com/NixOS/nixpkgs/pull/32496#discussion_r156527544
systemd.timers =
let
scrubTimer =
fs:
let
fs' = utils.escapeSystemdPath fs;
in
nameValuePair "btrfs-scrub-${fs'}" {
description = "regular btrfs scrub timer on ${fs}";
systemd.services."btrfs-scrub@" = {
description = "btrfs scrub on %f";
documentation = [ "man:btrfs-scrub(8)" ];
# scrub prevents suspend2ram or proper shutdown on linux < 6.19
conflicts = optionals (versionOlder config.boot.kernelPackages.kernel.version "6.19") [
"shutdown.target"
"sleep.target"
];
before = optionals (versionOlder config.boot.kernelPackages.kernel.version "6.19") [
"shutdown.target"
"sleep.target"
];
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = cfgScrub.interval;
AccuracySec = "1d";
Persistent = true;
};
};
in
listToAttrs (map scrubTimer cfgScrub.fileSystems);
# prevent problems with MemoryDenyWriteExecute
environment.PYTHON_JIT = "0";
systemd.services =
let
scrubService =
fs:
let
fs' = utils.escapeSystemdPath fs;
in
nameValuePair "btrfs-scrub-${fs'}" {
description = "btrfs scrub on ${fs}";
documentation = [ "man:btrfs-scrub(8)" ];
# scrub prevents suspend2ram or proper shutdown on linux < 6.19
conflicts = lib.optionals (lib.versionOlder config.boot.kernelPackages.kernel.version "6.19") [
"shutdown.target"
"sleep.target"
];
before = lib.optionals (lib.versionOlder config.boot.kernelPackages.kernel.version "6.19") [
"shutdown.target"
"sleep.target"
];
unitConfig.RequiresMountsFor = "%f";
serviceConfig = {
# simple and not oneshot, otherwise ExecStop is not used
Type = "simple";
Nice = 19;
IOSchedulingClass = "idle";
ExecStart = "${pkgs.btrfs-progs}/bin/btrfs scrub start -B ${
lib.optionalString (cfgScrub.limit != null) "--limit ${cfgScrub.limit}"
} ${fs}";
# if the service is stopped before scrub end, cancel it
ExecStop = pkgs.writeShellScript "btrfs-scrub-maybe-cancel" ''
(${pkgs.btrfs-progs}/bin/btrfs scrub status ${fs} | ${pkgs.gnugrep}/bin/grep finished) || ${pkgs.btrfs-progs}/bin/btrfs scrub cancel ${fs}
'';
};
};
in
listToAttrs (map scrubService cfgScrub.fileSystems);
serviceConfig =
let
btrfsCmd = getExe pkgs.btrfs-progs;
btrfsCancelCmd = pkgs.writers.writePython3 "btrfs-scrub-maybe-cancel" { } ''
import subprocess
import sys
btrfs = "${escape [ "\"" "\\" ] btrfsCmd}"
result = subprocess.run(
[btrfs, "scrub", "cancel"] + sys.argv[1:],
stderr=subprocess.PIPE,
check=False,
shell=False
)
# ignore errors if there was no running scrub to cancel
if result.returncode == 2:
sys.exit(0)
sys.stderr.buffer.write(result.stderr)
sys.exit(result.returncode)
'';
additionalScrubArgs = optionals (cfgScrub.limit != null) [
"--limit"
cfgScrub.limit
];
in
{
# simple and not oneshot, otherwise ExecStop is not used
Type = "simple";
Nice = 19;
CPUSchedulingPolicy = "idle";
IOSchedulingClass = "idle";
ExecStart = "${
utils.escapeSystemdExecArgs (
[
btrfsCmd
"scrub"
"start"
"-B"
]
++ additionalScrubArgs
)
} %f";
# if the service is stopped before scrub end, cancel it
ExecStop = "${utils.escapeSystemdExecArg btrfsCancelCmd} %f";
# hardening
# required for starting/cancelling the scrub operation
CapabilityBoundingSet = [
"CAP_SYS_ADMIN"
"CAP_DAC_READ_SEARCH"
];
NoNewPrivileges = true;
# no ProtectSystem/ProtectHome since the path to be scrubbed can refer to a device,
# which in turn might be mounted there and mounting it read-only prevents scrubbing
StateDirectory = "btrfs"; # contains progress information
PrivateNetwork = true;
ProtectHostname = true;
ProtectClock = true;
ProtectKernelModules = true;
ProtectKernelLogs = true;
ProtectControlGroups = true;
RestrictAddressFamilies = [ "AF_UNIX" ]; # used internally for communication
LockPersonality = true;
MemoryDenyWriteExecute = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
PrivateMounts = true;
SystemCallFilter = [
"@system-service"
"~@mount"
];
SystemCallArchitectures = "native";
# no ProtectKernelTunables since /sys/fs/btrfs access is required
# no User= since written files have to be accessible by scrub commands run manually
};
};
systemd.timers."btrfs-scrub@" = {
description = "Regular btrfs scrub on %f";
documentation = [ "man:btrfs-scrub(8)" ];
timerConfig = {
OnCalendar = cfgScrub.interval;
AccuracySec = "1d";
Persistent = true;
};
};
systemd.targets.timers.wants = map (
fs: "btrfs-scrub@${utils.escapeSystemdPath fs}.timer"
) cfgScrub.fileSystems;
})
];
}

View File

@@ -7,6 +7,8 @@
let
serialDevice = if pkgs.stdenv.hostPlatform.isx86 then "ttyS0" else "ttyAMA0";
efiArch = pkgs.stdenv.hostPlatform.efiArch;
in
{
meta = {
@@ -14,6 +16,7 @@ in
};
imports = [
../image/repart.nix
./lxc-instance-common.nix
../profiles/qemu-guest.nix
@@ -28,6 +31,60 @@ in
copyChannel = config.system.installer.channel.enable;
};
system.build.repartImage = config.image.repart.image.overrideAttrs (previousAttrs: {
nativeBuildInputs = previousAttrs.nativeBuildInputs ++ [ pkgs.qemu-utils ];
postBuild = ''
qemu-img convert -f raw -O qcow2 -c ${config.image.baseName}.raw ${config.image.baseName}.qcow2
rm ${config.image.baseName}.raw
'';
# expose a hydra build product so lxc-ci can download it
postInstall = ''
mkdir $out/nix-support
echo "file qcow2-image $out/${config.image.baseName}.qcow2" > $out/nix-support/hydra-build-products
'';
});
image.repart = {
name = "nixos";
version = null;
sectorSize = 512;
compression.enable = false;
mkfsOptions.ext4 = [
"-i"
"8192"
];
partitions = {
esp = {
contents = {
"/EFI/BOOT/BOOT${lib.toUpper efiArch}.EFI".source =
"${config.systemd.package}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi";
"/EFI/Linux/${config.system.boot.loader.ukiFile}".source =
"${config.system.build.uki}/${config.system.boot.loader.ukiFile}";
};
repartConfig = {
Type = "esp";
Format = "vfat";
Label = "ESP";
# support 10 kernels, assuming 50MB on x86 and 100MB on aarch64
SizeMinBytes = if pkgs.stdenv.hostPlatform.isx86 then "512M" else "1G";
};
};
root = {
storePaths = [ config.system.build.toplevel ];
repartConfig = {
Type = "root";
Format = "ext4";
Label = "nixos";
Minimize = "guess";
PaddingMinBytes = "512M";
};
};
};
};
fileSystems = {
"/" = {
device = "/dev/disk/by-label/nixos";

View File

@@ -437,7 +437,7 @@ rec {
versionModule
./maintainers/scripts/incus/incus-virtual-machine-image.nix
];
}).config.system.build.qemuImage
}).config.system.build.repartImage
)
);

View File

@@ -93,26 +93,47 @@ in
};
};
testScript = ''
${(import ./utils.nix).pythonUtils}
testScript =
{ nodes, ... }:
let
orderRenewScript = nodes.client.systemd.services."acme-order-renew-${domain}".script;
in
''
${(import ./utils.nix).pythonUtils}
cert = "${domain}"
import shlex
dnsserver.start()
acme.start()
cert = "${domain}"
wait_for_running(dnsserver)
dnsserver.wait_for_open_port(53)
wait_for_running(acme)
acme.wait_for_open_port(443)
with subtest("Disable DNS propagation checks"):
order_renew_script = ${builtins.toJSON orderRenewScript}
lego_run_commands = [
line
for line in order_renew_script.splitlines()
if "lego run " in line
]
assert len(lego_run_commands) == 2, lego_run_commands
for command in lego_run_commands:
args = shlex.split(command)
assert "--dns.propagation.disable-ans" in args, args
assert "--dns.propagation.disable-rns" in args, args
assert "--dns.propagation.wait" not in args, args
with subtest("Boot and acquire a new cert"):
client.start()
wait_for_running(client)
dnsserver.start()
acme.start()
check_issuer(client, cert, "pebble")
check_domain(client, cert, cert, fail=True)
check_domain(client, cert, f"toodeep.nesting.{cert}", fail=True)
check_domain(client, cert, f"whatever.{cert}")
'';
wait_for_running(dnsserver)
dnsserver.wait_for_open_port(53)
wait_for_running(acme)
acme.wait_for_open_port(443)
with subtest("Boot and acquire a new cert"):
client.start()
wait_for_running(client)
check_issuer(client, cert, "pebble")
check_domain(client, cert, cert, fail=True)
check_domain(client, cert, f"toodeep.nesting.{cert}", fail=True)
check_domain(client, cert, f"whatever.{cert}")
'';
}

View File

@@ -6,6 +6,31 @@
let
domain = "example.test";
ip = "192.168.1.2";
migrationCertNames = [
"migration-2.${domain}"
"migration-3.${domain}"
];
migrationCerts = lib.genAttrs migrationCertNames (_: {
listenHTTP = ":80";
});
migrationTestLego = pkgs.writeShellScriptBin "lego" ''
if [[ ''${1-} == migrate ]]; then
printf '1\n' >> /run/acme/test-migration-attempts
if ! mkdir /run/acme/test-migration-active; then
touch /run/acme/test-migration-collision
exit 99
fi
trap 'rm -rf /run/acme/test-migration-active' EXIT
sleep 2
fi
${lib.getExe pkgs.lego} "$@"
'';
migrationServicePaths =
lib.genAttrs (map (name: "acme-order-renew-${name}") migrationCertNames)
(_: {
path = lib.mkBefore [ migrationTestLego ];
});
in
{
name = "http01-builtin";
@@ -38,6 +63,12 @@ in
listenHTTP = ":80";
};
# Delay account migration and fail a second concurrent invocation so
# the shared-account migration race is deterministic.
systemd.services."acme-order-renew-${config.networking.fqdn}".path = lib.mkBefore [
migrationTestLego
];
systemd.targets."renew-triggered" = {
wantedBy = [ "acme-order-renew-${config.networking.fqdn}.service" ];
after = [ "acme-order-renew-${config.networking.fqdn}.service" ];
@@ -81,6 +112,18 @@ in
preservation.configuration = { };
migration_serial.configuration = {
security.acme.maxConcurrentRenewals = 1;
security.acme.certs = migrationCerts;
systemd.services = migrationServicePaths;
};
migration_parallel.configuration = {
security.acme.maxConcurrentRenewals = 10;
security.acme.certs = migrationCerts;
systemd.services = migrationServicePaths;
};
add_cert_and_domain.configuration = {
security.acme.certs = {
"${config.networking.fqdn}" = {
@@ -96,6 +139,10 @@ in
security.acme.maxConcurrentRenewals = 10;
};
remove_domain.configuration = {
security.acme.certs."${config.networking.fqdn}".extraDomainNames = lib.mkForce [ ip ];
};
concurrency.configuration = {
# As above, relying on port binding behaviour to assert that concurrency limit
# prevents > 1 service running at a time.
@@ -170,9 +217,12 @@ in
domain = "${domain}"
ip = "${ip}"
jq = "${lib.getExe pkgs.jq}"
cert = "${certName}"
cert2 = "builtin-2." + domain
cert3 = "builtin-3." + domain
migration_certs = [cert] + ${builtins.toJSON migrationCertNames}
migration_units = [f"acme-order-renew-{name}.service" for name in migration_certs]
legacy_account_dir = "/var/lib/acme/.lego/accounts/1ccf607d9aa280e9af00"
acme.start()
@@ -187,6 +237,93 @@ in
check_domain(builtin, cert, cert)
check_ip(builtin, cert, ip)
with subtest("Handles v4 account and certificate state"):
switch_to(builtin, "migration_serial")
for migration_cert in migration_certs:
check_issuer(builtin, migration_cert, "pebble")
check_domain(builtin, migration_cert, migration_cert)
switch_to(builtin, "migration_parallel")
builtin.succeed(f"systemctl start {' '.join(migration_units)}")
builtin.succeed(
f"""
set -euo pipefail
account_file=$(find /var/lib/acme/.lego/accounts -type f -name account.json -print -quit)
test -n "$account_file"
account_dir=$(dirname "$account_file")
account_id=$(basename "$account_dir")
key_path="$account_dir/$account_id.key"
test -f "$key_path"
mkdir "$account_dir/keys"
mv "$key_path" "$account_dir/keys/$account_id.key"
{jq} '{{
email: .email,
registration: {{
body: (.registration | del(.accountURL)),
uri: .registration.accountURL
}}
}}' "$account_dir/account.json" > "$account_dir/account.json.tmp"
mv "$account_dir/account.json.tmp" "$account_dir/account.json"
{jq} -e '
.registration.body.status == "valid"
and (.registration.uri | startswith("https://acme.test/"))
' "$account_dir/account.json" > /dev/stderr
cert_resource=$(find "/var/lib/acme/.lego/{cert}" -type f -name '{cert}.json' -print -quit)
test -n "$cert_resource"
{jq} '{{
domain: .id,
certUrl: .certUrl,
certStableUrl: .certStableUrl
}}' "$cert_resource" > "$cert_resource.tmp"
mv "$cert_resource.tmp" "$cert_resource"
chown -R acme:acme /var/lib/acme/.lego/accounts "/var/lib/acme/.lego/{cert}"
chmod -R u=rwX,g=,o= /var/lib/acme/.lego/accounts
chmod -R u=rwX,g=rX,o= "/var/lib/acme/.lego/{cert}"
rm -rf /run/acme/test-migration-*
"""
)
builtin.succeed(f"systemctl start {' '.join(migration_units)}")
builtin.succeed('test "$(wc -l < /run/acme/test-migration-attempts)" -eq 1')
builtin.succeed("test ! -e /run/acme/test-migration-active")
builtin.succeed("test ! -e /run/acme/test-migration-collision")
builtin.succeed(
f"""
set -euo pipefail
account_file=$(find /var/lib/acme/.lego/accounts -type f -name account.json -print -quit)
test -n "$account_file"
account_dir=$(dirname "$account_file")
account_id=$(basename "$account_dir")
test -f "$account_dir/$account_id.key"
test ! -e "$account_dir/keys"
{jq} -e '
.origin == "migration"
and .keyType == "EC256"
and .server == "https://acme.test/dir"
and .registration.status == "valid"
and (.registration.accountURL | startswith("https://acme.test/"))
' "$account_dir/account.json" > /dev/stderr
"""
)
builtin.succeed(f"systemctl start acme-order-renew-{cert}.service")
check_issuer(builtin, cert, "pebble")
check_domain(builtin, cert, cert)
check_ip(builtin, cert, ip)
for migration_cert in migration_certs[1:]:
check_issuer(builtin, migration_cert, "pebble")
check_domain(builtin, migration_cert, migration_cert)
with subtest("Validate permissions"):
check_permissions(builtin, cert, "acme")
@@ -298,6 +435,16 @@ in
check_permissions(builtin, cert, "acme")
check_permissions(builtin, cert2, "acme")
with subtest("Remove an existing cert domain"):
builtin.succeed("systemctl stop renew-triggered.target")
switch_to(builtin, "remove_domain")
builtin.wait_for_unit("renew-triggered.target")
check_issuer(builtin, cert, "pebble")
check_domain(builtin, cert, f"builtin-alt.{domain}", fail=True)
check_ip(builtin, cert, ip)
check_permissions(builtin, cert, "acme")
with subtest("Check account hashing compatibility with pre-24.05 settings"):
builtin.succeed("systemctl stop renew-triggered.target")
switch_to(builtin, "legacy_account_hash"

View File

@@ -15,7 +15,8 @@
# Example callTest that just extracts the derivation from the test:
# callTest = t: t.test;
let
inherit (pkgs.lib)
inherit (pkgs) lib;
inherit (lib)
isAttrs
isFunction
mapAttrs
@@ -98,13 +99,25 @@ let
featureFlags.minimalModules = { };
};
evalMinimalConfig = module: nixosLib.evalModules { modules = [ module ]; };
/**
On platforms where NixOS does not run natively, nixosTests is still available
as a convenience for running VM tests. This makes them easier to use, and it
sidesteps the question of nested virtualisation.
`runTest` already includes similar logic as part of its [public] interface.
[public]: https://nixos.org/manual/nixos/stable/#sec-call-nixos-test-outside-nixos
*/
inherit (pkgs) pkgsLinux;
evalSystem =
module:
import ../lib/eval-config.nix {
system = null;
modules = [
../modules/misc/nixpkgs/read-only.nix
{ nixpkgs.pkgs = pkgs; }
{ nixpkgs.pkgs = pkgsLinux; }
module
];
};
@@ -223,8 +236,7 @@ in
accountsservice = runTest ./accountsservice.nix;
acl = pkgs.callPackage ./acl.nix { };
acme = import ./acme/default.nix {
inherit runTest;
inherit (pkgs) lib;
inherit runTest lib;
};
acme-dns = runTest ./acme-dns.nix;
activation = pkgs.callPackage ../modules/system/activation/test.nix { };
@@ -289,6 +301,7 @@ in
audiobookshelf = runTest ./audiobookshelf.nix;
audit = runTest ./audit.nix;
audit-testsuite = runTest ./audit-testsuite.nix;
aurral = runTest ./aurral.nix;
auth-mysql = runTest ./auth-mysql.nix;
authelia = runTest ./authelia.nix;
auto-cpufreq = runTest ./auto-cpufreq.nix;
@@ -362,6 +375,7 @@ in
btrbk-doas = runTest ./btrbk-doas.nix;
btrbk-no-timer = runTest ./btrbk-no-timer.nix;
btrbk-section-order = runTest ./btrbk-section-order.nix;
btrfs-autoscrub = runTest ./btrfs-autoscrub.nix;
budgie = runTest ./budgie.nix;
buildbot = runTest ./buildbot.nix;
buildkite-agents = runTest ./buildkite-agents.nix;
@@ -425,12 +439,13 @@ in
inherit runTest;
package = pkgs.clickhouse-lts;
};
cliproxyapi = runTest ./cliproxyapi.nix;
cloud-init = runTest ./cloud-init.nix;
cloud-init-hostname = runTest ./cloud-init-hostname.nix;
cloudcompare = import ./cloudcompare.nix { inherit pkgs runTest; };
cloudlog = runTest ./cloudlog.nix;
cntr = import ./cntr.nix {
inherit (pkgs) lib;
inherit lib;
runTest = runTestOn [
"aarch64-linux"
"x86_64-linux"
@@ -609,18 +624,15 @@ in
ergochat = runTest ./ergochat.nix;
ersatztv = runTest ./ersatztv.nix;
espanso = import ./espanso.nix {
inherit (pkgs) lib;
inherit runTest;
inherit lib runTest;
};
esphome = runTest ./esphome.nix;
etc = pkgs.callPackage ../modules/system/etc/test.nix { inherit evalMinimalConfig; };
etc = pkgsLinux.callPackage ../modules/system/etc/test.nix { inherit evalMinimalConfig; };
etcd = import ./etcd/default.nix { inherit pkgs runTest; };
etebase-server = runTest ./etebase-server.nix;
etesync-dav = runTest ./etesync-dav.nix;
evcc = runTest ./evcc.nix;
extra-initrd = import ./extra-initrd.nix {
inherit runTest pkgs;
};
extra-initrd = import ./extra-initrd.nix { inherit runTest; };
facter = runTest ./facter;
fail2ban = runTest ./fail2ban.nix;
fakeroute = runTest ./fakeroute.nix;
@@ -681,6 +693,7 @@ in
firewalld = runTest ./firewalld.nix;
firezone = runTest ./firezone/firezone.nix;
fish = runTest ./fish.nix;
flame = runTest ./flame.nix;
flannel = runTestOn [ "x86_64-linux" ] ./flannel.nix;
flap-alerted = runTest ./flap-alerted.nix;
flaresolverr = runTest ./flaresolverr.nix;
@@ -693,13 +706,12 @@ in
fontconfig-default-fonts = runTest ./fontconfig-default-fonts.nix;
forgejo = import ./forgejo.nix {
inherit runTest;
forgejoPackage = pkgs.forgejo;
forgejoPackage = pkgsLinux.forgejo;
};
forgejo-lts = import ./forgejo.nix {
inherit runTest;
forgejoPackage = pkgs.forgejo-lts;
forgejoPackage = pkgsLinux.forgejo-lts;
};
freenet = runTest ./freenet.nix;
freescout = import ./freescout {
inherit runTest;
};
@@ -724,11 +736,11 @@ in
gancio = runTest ./gancio.nix;
garage_1 = import ./garage {
inherit runTest;
package = pkgs.garage_1;
package = pkgsLinux.garage_1;
};
garage_2 = import ./garage {
inherit runTest;
package = pkgs.garage_2;
package = pkgsLinux.garage_2;
};
gatus = runTest ./gatus.nix;
gemstash = import ./gemstash.nix { inherit pkgs runTest; };
@@ -742,8 +754,9 @@ in
git-pages-modular = runTest ./git-pages.nix;
gitdaemon = runTest ./gitdaemon.nix;
gitea = import ./gitea.nix {
inherit pkgs runTest;
inherit (pkgs) lib;
inherit (pkgsLinux) gitea;
inherit runTest;
inherit lib;
};
gitea-actions-runner = runTest ./gitea-actions-runner.nix;
github-runner = runTest ./github-runner.nix;
@@ -780,7 +793,9 @@ in
gotify-server = runTest ./gotify-server.nix;
gotosocial = runTest ./web-apps/gotosocial.nix;
goupile = runTest ./web-apps/goupile;
grafana = handleTest ./grafana { };
grafana = import ./grafana {
inherit runTest;
};
grafana-to-ntfy = runTest ./grafana-to-ntfy.nix;
graphite = runTest ./graphite.nix;
grav = runTest ./web-apps/grav.nix;
@@ -800,8 +815,7 @@ in
guix = handleTest ./guix { };
gvisor = runTest ./gvisor.nix;
h2o = import ./web-servers/h2o {
inherit runTest;
inherit (pkgs) lib;
inherit lib runTest;
};
hadoop = import ./hadoop {
inherit handleTestOn;
@@ -847,6 +861,7 @@ in
hibernate-systemd-stage-1 = handleTestOn [ "x86_64-linux" ] ./hibernate.nix {
systemdStage1 = true;
};
hickory-dns = runTest ./hickory-dns.nix;
hister = runTest ./hister.nix;
hitch = runTest ./hitch;
hledger-web = runTest ./hledger-web.nix;
@@ -866,6 +881,7 @@ in
hound = runTest ./hound.nix;
hub = runTest ./git/hub.nix;
hydra = runTest ./hydra;
hyphanet = runTest ./hyphanet.nix;
i18n = runTest ./i18n.nix;
i2pd = runTest ./i2pd.nix;
i3wm = runTest ./i3wm.nix;
@@ -911,6 +927,7 @@ in
inventree = runTest ./inventree.nix;
invidious = runTest ./invidious.nix;
invoiceplane = runTest ./invoiceplane.nix;
iocaine = runTest ./iocaine.nix;
iodine = runTest ./iodine.nix;
iosched = runTest ./iosched.nix;
ipget = runTest ./ipget.nix;
@@ -927,18 +944,22 @@ in
jibri = runTest ./jibri.nix;
jirafeau = runTest ./jirafeau.nix;
jitsi-meet = runTest ./jitsi-meet.nix;
jool = import ./jool.nix { inherit pkgs runTest; };
jool = import ./jool.nix {
pkgs = pkgsLinux;
inherit runTest;
};
jotta-cli = runTest ./jotta-cli.nix;
k3s = import ./rancher {
inherit pkgs;
inherit (pkgs) lib;
inherit lib pkgs;
runTest = runTestOn [
"aarch64-linux"
"x86_64-linux"
];
rancherDistro = "k3s";
};
kafka = handleTest ./kafka { };
kafka = import ./kafka {
inherit runTest pkgs;
};
kaidan = runTest ./kaidan;
kanboard = runTest ./web-apps/kanboard.nix;
kanidm = runTest ./kanidm.nix;
@@ -953,7 +974,9 @@ in
keepalived = discoverTests (import ./keepalived.nix);
keepassxc = runTest ./keepassxc.nix;
kener = runTest ./kener.nix;
kerberos = handleTest ./kerberos/default.nix { };
kerberos = import ./kerberos/default.nix {
inherit pkgs runTest;
};
kernel-generic = handleTest ./kernel-generic { };
kernel-latest-ath-user-regd = runTest ./kernel-latest-ath-user-regd.nix;
kernel-rust = handleTest ./kernel-rust.nix { };
@@ -983,8 +1006,7 @@ in
kthxbye = runTest ./kthxbye.nix;
kubernetes = handleTestOn [ "x86_64-linux" ] ./kubernetes { };
kubo = import ./kubo {
inherit runTest;
inherit (pkgs) lib;
inherit lib runTest;
};
kvrocks = runTest ./kvrocks.nix;
labgrid = runTest ./labgrid.nix;
@@ -1093,7 +1115,7 @@ in
matrix-tuwunel = runTest ./matrix/tuwunel.nix;
matter-server = runTest ./matter-server.nix;
matterjs-server = runTest ./matterjs-server.nix;
mattermost = handleTest ./mattermost { };
mattermost = runTest ./mattermost;
mautrix-discord = runTest ./matrix/mautrix-discord.nix;
mautrix-meta-postgres = runTest ./matrix/mautrix-meta-postgres.nix;
mautrix-meta-sqlite = runTest ./matrix/mautrix-meta-sqlite.nix;
@@ -1101,8 +1123,7 @@ in
mediamtx = runTest ./mediamtx.nix;
mediatomb = runTest ./mediatomb.nix;
mediawiki = import ./mediawiki.nix {
inherit (pkgs) lib;
inherit runTest;
inherit lib runTest;
};
meilisearch = runTest ./meilisearch.nix;
memcached = runTest ./memcached.nix;
@@ -1125,7 +1146,7 @@ in
mobilizon = runTest ./mobilizon.nix;
mod_perl = runTest ./mod_perl.nix;
modular-service-etc = runTest ./modular-service-etc/test.nix;
modularService = pkgs.callPackage ../modules/system/service/systemd/test.nix {
modularService = pkgsLinux.callPackage ../modules/system/service/systemd/test.nix {
inherit evalSystem;
};
moduleStateRevisions = pkgs.callPackage ./moduleStateRevisions.nix { };
@@ -1156,8 +1177,7 @@ in
morph-browser = discoverTests (import ./morph-browser.nix);
mosquitto = runTest ./mosquitto.nix;
movim = import ./web-apps/movim {
inherit runTest;
inherit (pkgs) lib;
inherit lib runTest;
};
mpd = runTest ./mpd.nix;
mpv = runTest ./mpv.nix;
@@ -1258,7 +1278,7 @@ in
nginx-status-page = runTest ./nginx-status-page.nix;
nginx-tmpdir = runTest ./nginx-tmpdir.nix;
nginx-unix-socket = runTest ./nginx-unix-socket.nix;
nginx-variants = import ./nginx-variants.nix { inherit pkgs runTest; };
nginx-variants = import ./nginx-variants.nix { inherit runTest; };
nifi = runTestOn [ "x86_64-linux" ] ./web-apps/nifi.nix;
nimdow = runTest ./nimdow.nix;
nipap = runTest ./web-apps/nipap.nix;
@@ -1362,6 +1382,7 @@ in
ollama-cuda = runTestOn [ "x86_64-linux" "aarch64-linux" ] ./ollama-cuda.nix;
ollama-rocm = runTestOn [ "x86_64-linux" "aarch64-linux" ] ./ollama-rocm.nix;
ollama-vulkan = runTestOn [ "x86_64-linux" "aarch64-linux" ] ./ollama-vulkan.nix;
omada = runTestOn [ "x86_64-linux" ] ./omada.nix;
ombi = runTest ./ombi.nix;
omnom = runTest ./omnom;
oncall = runTest ./web-apps/oncall.nix;
@@ -1428,8 +1449,7 @@ in
patroni = handleTestOn [ "x86_64-linux" ] ./patroni.nix { };
pcsclite = runTest ./pcsclite.nix;
pdfding = import ./web-apps/pdfding {
inherit (pkgs) lib;
inherit runTest;
inherit lib runTest;
};
pdns-recursor = runTest ./pdns-recursor.nix;
pdudaemon = runTest ./pdudaemon.nix;
@@ -1497,7 +1517,10 @@ in
{ };
postfix-tlspol = runTest ./postfix-tlspol.nix;
postgres-websockets = runTest ./postgres-websockets.nix;
postgresql = import ./postgresql { inherit runTest pkgs; };
postgresql = import ./postgresql {
inherit runTest;
pkgs = pkgsLinux;
};
postgrest = runTest ./postgrest.nix;
power-profiles-daemon = runTest ./power-profiles-daemon.nix;
powerdns = runTest ./powerdns.nix;
@@ -1535,8 +1558,7 @@ in
privoxy = runTest ./privoxy.nix;
prometheus = import ./prometheus { inherit runTest; };
prometheus-exporters = import ./prometheus-exporters.nix {
inherit runTest;
inherit (pkgs) lib;
inherit lib runTest;
};
prosody = runTest ./xmpp/prosody.nix;
prosody-mysql = handleTest ./xmpp/prosody-mysql.nix { };
@@ -1576,14 +1598,16 @@ in
rathole = runTest ./rathole.nix;
rauc = runTest ./rauc.nix;
reaction = import ./reaction {
inherit (pkgs) lib;
inherit runTest;
inherit lib runTest;
};
readarr = runTest ./readarr.nix;
readeck = runTest ./readeck.nix;
realm = runTest ./realm.nix;
rebuilderd = runTest ./rebuilderd.nix;
redis = handleTest ./redis.nix { };
redis = runTest {
imports = [ ./redis.nix ];
_module.args.package = pkgs.redis;
};
redlib = runTest ./redlib.nix;
redmine = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./redmine.nix { };
refind = runTest ./refind.nix;
@@ -1598,8 +1622,7 @@ in
retroarch = runTest ./retroarch.nix;
ringboard = runTest ./ringboard.nix;
rke2 = import ./rancher {
inherit pkgs;
inherit (pkgs) lib;
inherit lib pkgs;
runTest = runTestOn [
"aarch64-linux"
"x86_64-linux"
@@ -1727,8 +1750,7 @@ in
sunshine = runTest ./sunshine.nix;
suricata = runTest ./suricata.nix;
suwayomi-server = import ./suwayomi-server.nix {
inherit runTest;
inherit (pkgs) lib;
inherit lib runTest;
};
svnserve = runTest ./svnserve.nix;
swap-file-btrfs = runTest ./swap-file-btrfs.nix;
@@ -1769,19 +1791,18 @@ in
system-services-compliance = recurseIntoAttrs (
import ./system-services-compliance.nix {
inherit
pkgs
evalSystem
runTest
callTest
;
pkgs = pkgsLinux;
}
);
systemd = runTest ./systemd.nix;
systemd-analyze = runTest ./systemd-analyze.nix;
systemd-binfmt = handleTestOn [ "x86_64-linux" ] ./systemd-binfmt.nix { };
systemd-boot = import ./systemd-boot.nix {
inherit runTest runTestOn;
inherit (pkgs) lib;
inherit lib runTest runTestOn;
};
systemd-bpf = runTest ./systemd-bpf.nix;
systemd-capsules = runTest ./systemd-capsules.nix;
@@ -1965,9 +1986,13 @@ in
utmp = runTest ./utmp.nix;
uwsgi = runTest ./uwsgi.nix;
v2ray = runTest ./v2ray.nix;
valkey = runTest {
imports = [ ./redis.nix ];
_module.args.package = pkgs.valkey;
};
varnish80 = runTest {
imports = [ ./varnish.nix ];
_module.args.package = pkgs.varnish80;
_module.args.package = pkgsLinux.varnish80;
};
vault = runTest ./vault.nix;
vault-agent = runTest ./vault-agent.nix;
@@ -1985,7 +2010,7 @@ in
vikunja = runTest ./vikunja.nix;
vinyl-cache_9 = runTest {
imports = [ ./vinyl-cache.nix ];
_module.args.package = pkgs.vinyl-cache_9;
_module.args.package = pkgsLinux.vinyl-cache_9;
};
virtualbox = handleTestOn [ "x86_64-linux" ] ./virtualbox.nix { };
vm-variant = handleTest ./vm-variant.nix { };
@@ -2010,16 +2035,15 @@ in
whoogle-search = runTest ./whoogle-search.nix;
wiki-js = runTest ./wiki-js.nix;
windmill = import ./windmill {
inherit pkgs runTest;
inherit (pkgs) lib;
inherit runTest;
};
wine = import ./wine.nix {
inherit pkgs runTest;
inherit (pkgs) lib;
};
wireguard = import ./wireguard {
inherit pkgs runTest;
inherit (pkgs) lib;
pkgs = pkgsLinux;
inherit runTest lib;
};
without-nix = runTest ./without-nix.nix;
wmderland = runTest ./wmderland.nix;
@@ -2027,7 +2051,7 @@ in
workout-tracker = runTest ./workout-tracker.nix;
wpa_supplicant = import ./wpa_supplicant.nix { inherit pkgs runTest; };
wrappers = runTest ./wrappers.nix;
writefreely = import ./web-apps/writefreely.nix { inherit pkgs runTest; };
writefreely = import ./web-apps/writefreely.nix { inherit runTest; };
wstunnel = runTest ./wstunnel.nix;
xandikos = runTest ./xandikos.nix;
xautolock = runTest ./xautolock.nix;
@@ -2045,14 +2069,14 @@ in
yarr = runTest ./yarr.nix;
yb = pkgs.callPackage ./yb.nix { inherit (pkgs.yb.passthru) ybPivHarnessTests testFixtures; };
ydotool = import ./ydotool.nix {
inherit (pkgs) lib;
inherit runTest;
inherit lib runTest;
};
yggdrasil = runTest ./yggdrasil.nix;
your_spotify = runTest ./your_spotify.nix;
zammad = runTest ./zammad.nix;
zapret2 = runTest ./zapret2.nix;
zenohd = runTest ./zenohd.nix;
zerobyte = runTest ./zerobyte.nix;
zeronet-conservancy = runTest ./zeronet-conservancy.nix;
zfs = import ./zfs.nix { inherit system pkgs runTest; };
zigbee2mqtt = runTest ./zigbee2mqtt.nix;

View File

@@ -27,6 +27,8 @@
};
image.repart = {
enable = true;
verityStore = {
enable = true;
# by default the module works with systemd-boot, for simplicity this test directly boots the UKI

View File

@@ -46,6 +46,8 @@ in
};
image.repart = {
enable = true;
name = "appliance-gpt-image";
# OVMF does not work with the default repart sector size of 4096
sectorSize = 512;

29
nixos/tests/aurral.nix Normal file
View File

@@ -0,0 +1,29 @@
{ lib, ... }:
{
name = "aurral";
meta = with lib.maintainers; {
maintainers = [ hougo ];
};
nodes = {
machine =
{ ... }:
{
services.aurral = {
enable = true;
environment = {
DOWNLOAD_FOLDER = "/var/lib/aurral-downloads";
};
};
};
};
testScript = ''
start_all()
machine.wait_for_unit("aurral.service")
machine.wait_for_open_port(3001)
machine.succeed('curl --fail http://localhost:3001/api/health')
'';
}

View File

@@ -0,0 +1,69 @@
{ ... }:
{
name = "btrfs-autoscrub";
nodes.machine =
{ ... }:
{
virtualisation.emptyDiskImages = [ 128 ];
# test sandbox permissiveness and command line escaping
virtualisation.fileSystems."/home/test/btrfs autoscrub test" = {
fsType = "btrfs";
device = "/dev/vdb";
autoFormat = true;
options = [ "X-mount.mkdir" ];
};
services.btrfs.autoScrub = {
enable = true;
# test that setting the limit works
limit = "1G";
};
};
testScript = ''
def run_scrub(fs):
machine.start_job(f"'btrfs-scrub@{fs}.service'")
machine.wait_until_fails(f"systemctl --quiet is-active 'btrfs-scrub@{fs}.service'")
machine.fail(f"systemctl is-failed 'btrfs-scrub@{fs}.service'")
invocation_id = machine.succeed(
f"systemctl show --value -p InvocationID 'btrfs-scrub@{fs}.service'"
)
output = machine.succeed(
f"journalctl --no-pager _SYSTEMD_INVOCATION_ID={invocation_id}"
)
t.assertNotRegex(output, "(?i)warning:|error:")
start_all()
machine.wait_for_unit("multi-user.target")
fs = "/home/test/btrfs autoscrub test"
escaped = r"home-test-btrfs\x20autoscrub\x20test"
with subtest("Verify that the configured timers and file systems are active"):
machine.require_unit_state(f"{escaped}.mount", "active")
machine.require_unit_state(f"btrfs-scrub@{escaped}.timer", "active")
# disable timers (and possible triggered services) to prevent them
# from interfering with the tests
machine.stop_job(f"'btrfs-scrub@{escaped}.timer'")
machine.stop_job(f"'btrfs-scrub@{escaped}.service'")
with subtest("Verify that scrubbing works"):
run_scrub(escaped)
result = machine.succeed(f"btrfs scrub status '{fs}'")
t.assertRegex(result, r"Status:\s*finished")
with subtest("Verify that scrubbing causes filesystems to be mounted"):
machine.stop_job(f"'{escaped}.mount'")
run_scrub(escaped)
machine.require_unit_state(f"{escaped}.mount", "active")
with subtest("Verify that the service can scrub private mountpoints"):
machine.succeed(f"chmod 000 '{fs}'")
machine.succeed("chmod 000 /home/test")
run_scrub(escaped)
with subtest("Verify that the service can scrub device files directly"):
run_scrub("dev-vdb")
'';
}

View File

@@ -1,7 +1,6 @@
{
pkgs,
runTest,
...
}:
let

View File

@@ -1,4 +1,4 @@
{ runTest, pkgs, ... }:
{ runTest, pkgs }:
let
authKey = pkgs.writeText "auth-key" "1234ABCD";

View File

@@ -0,0 +1,41 @@
{ lib, ... }:
{
name = "cliproxyapi";
meta.maintainers = [ lib.maintainers.anish ];
containers.machine =
{ pkgs, ... }:
{
services.cliproxyapi = {
enable = true;
settings = {
host = "127.0.0.1";
port = 8317;
api-keys = [ { _secret = "/etc/cliproxyapi-api-key"; } ];
};
};
environment.etc."cliproxyapi-api-key".text = "test-key";
environment.systemPackages = [ pkgs.curl ];
};
testScript = ''
machine.wait_for_unit("cliproxyapi.service")
machine.wait_for_open_port(8317)
# The API key secret must be substituted into config.yaml.
machine.succeed("grep -q test-key /var/lib/cliproxyapi/config.yaml")
# Requests without a valid API key are rejected.
status = machine.succeed(
"curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8317/v1/models"
).strip()
assert status == "401", f"expected 401 for unauthenticated /v1/models, got {status}"
# Requests carrying the configured API key are accepted.
machine.succeed(
"curl -sf -H 'Authorization: Bearer test-key' http://127.0.0.1:8317/v1/models"
)
'';
}

View File

@@ -1,7 +1,6 @@
{
pkgs,
runTest,
...
}:
let

View File

@@ -1,7 +1,4 @@
{
runTest,
...
}:
{ runTest }:
let
common =
{ config, pkgs, ... }:

68
nixos/tests/flame.nix Normal file
View File

@@ -0,0 +1,68 @@
{ lib, ... }:
{
name = "flame";
meta.maintainers = with lib.maintainers; [ DerGrumpf ];
nodes.machine = {
services.flame = {
enable = true;
passwordFile = "/etc/flame-password";
apps = [
{
name = "Test App";
url = "http://example.com";
}
];
categories = [
{
name = "Test Category";
bookmarks = [
{
name = "Nixpkgs";
url = "https://github.com/NixOS/nixpkgs";
}
];
}
];
settings = {
customTitle = "Test Flame";
customUnknownKey = "test-value";
};
customCSS = ''
body { background: #123456; }
'';
};
systemd.tmpfiles.rules = [
"f /etc/flame-password 0400 root root - testpassword"
];
};
testScript = ''
machine.wait_for_unit("flame.service")
machine.wait_for_open_port(5005)
machine.succeed("curl -f http://localhost:5005/")
machine.wait_for_unit("flame-seed.service")
machine.succeed("curl -f http://localhost:5005/api/apps | grep -q 'Test App'")
machine.succeed("curl -f http://localhost:5005/api/categories | grep -q 'Test Category'")
machine.succeed("curl -f http://localhost:5005/api/categories | grep -q Nixpkgs")
machine.succeed("curl -f http://localhost:5005/api/config | grep -q 'Test Flame'")
machine.succeed("curl -f http://localhost:5005/flame.css | grep -q '#123456'")
# Restart resilience
machine.succeed("systemctl restart flame.service")
machine.wait_for_unit("flame.service")
machine.wait_for_open_port(5005)
machine.succeed("curl -f http://localhost:5005/api/apps | grep -q 'Test App'")
# Freeform settings pass-through (unknown key, not explicitly declared)
machine.succeed("curl -f http://localhost:5005/api/config | grep -q customUnknownKey")
'';
}

View File

@@ -1,5 +1,5 @@
{
pkgs,
gitea,
lib,
runTest,
...
@@ -28,10 +28,18 @@ let
];
makeGiteaTest =
type:
lib.nameValuePair type (runTest {
name = "${pkgs.gitea.pname}-${type}";
runTest {
imports = [
module
];
name = "${gitea.pname}-${type}";
nodes.server.services.gitea.settings.database = { inherit type; };
};
module =
{ config, ... }:
{
meta = {
inherit (pkgs.gitea.meta) maintainers;
inherit (gitea.meta) maintainers;
};
nodes = {
@@ -42,7 +50,6 @@ let
services = {
gitea = {
enable = true;
database = { inherit type; };
package = pkgs.gitea;
metricsTokenFile = (pkgs.writeText "metrics_secret" "fakesecret").outPath;
settings = {
@@ -70,7 +77,7 @@ let
testScript =
let
inherit (import ./ssh-keys.nix pkgs) snakeOilPrivateKey snakeOilPublicKey;
inherit (import ./ssh-keys.nix config.node.pkgs) snakeOilPrivateKey snakeOilPublicKey;
in
/* python */ ''
GIT_SSH_COMMAND = "ssh -i $HOME/.ssh/privk -o StrictHostKeyChecking=no"
@@ -98,7 +105,7 @@ let
server.succeed(
"su -l gitea -c 'gpg --homedir /var/lib/gitea/data/home/.gnupg "
+ "--import ${toString (pkgs.writeText "gitea.key" signingPrivateKey)}'"
+ "--import ${toString (config.node.pkgs.writeText "gitea.key" signingPrivateKey)}'"
)
assert "BEGIN PGP PUBLIC KEY BLOCK" in server.succeed("curl http://localhost:3000/api/v1/signing-key.gpg")
@@ -150,7 +157,7 @@ let
+ 'http://localhost:3000/metrics '
+ '| grep gitea_accesses')
'';
});
};
in
lib.listToAttrs (map makeGiteaTest supportedDbTypes)
lib.genAttrs supportedDbTypes makeGiteaTest

View File

@@ -1,156 +1,154 @@
import ../make-test-python.nix (
{ lib, pkgs, ... }:
{ lib, pkgs, ... }:
let
inherit (lib) mkMerge;
let
inherit (lib) mkMerge;
baseGrafanaConf = {
services.grafana = {
baseGrafanaConf = {
services.grafana = {
enable = true;
settings = {
analytics.reporting_enabled = false;
server = {
http_addr = "::1";
domain = "localhost";
};
security = {
admin_user = "testadmin";
admin_password = "snakeoilpwd";
secret_key = "11111111111111111111";
};
};
};
};
extraNodeConfs = {
sqlite = { };
socket =
{ config, ... }:
{
services.grafana.settings.server = {
protocol = "socket";
socket = "/run/grafana/sock";
socket_gid = config.users.groups.nginx.gid;
};
users.users.grafana.extraGroups = [ "nginx" ];
services.nginx = {
enable = true;
recommendedProxySettings = true;
virtualHosts."_".locations."/".proxyPass = "http://unix:/run/grafana/sock";
};
};
declarativePlugins = {
services.grafana.declarativePlugins = [ pkgs.grafanaPlugins.grafana-clock-panel ];
};
postgresql = {
services.grafana.settings.database = {
host = "[::1]:5432";
user = "grafana";
};
services.postgresql = {
enable = true;
settings = {
analytics.reporting_enabled = false;
server = {
http_addr = "::1";
domain = "localhost";
};
security = {
admin_user = "testadmin";
admin_password = "snakeoilpwd";
secret_key = "11111111111111111111";
};
};
ensureDatabases = [ "grafana" ];
ensureUsers = [
{
name = "grafana";
ensureDBOwnership = true;
}
];
};
systemd.services.grafana.after = [ "postgresql.target" ];
};
extraNodeConfs = {
sqlite = { };
socket =
{ config, ... }:
{
services.grafana.settings.server = {
protocol = "socket";
socket = "/run/grafana/sock";
socket_gid = config.users.groups.nginx.gid;
};
users.users.grafana.extraGroups = [ "nginx" ];
services.nginx = {
enable = true;
recommendedProxySettings = true;
virtualHosts."_".locations."/".proxyPass = "http://unix:/run/grafana/sock";
};
};
declarativePlugins = {
services.grafana.declarativePlugins = [ pkgs.grafanaPlugins.grafana-clock-panel ];
};
postgresql = {
services.grafana.settings.database = {
host = "[::1]:5432";
user = "grafana";
};
services.postgresql = {
enable = true;
ensureDatabases = [ "grafana" ];
ensureUsers = [
{
name = "grafana";
ensureDBOwnership = true;
}
];
};
systemd.services.grafana.after = [ "postgresql.target" ];
};
mysql = {
services.grafana.settings.database.user = "grafana";
services.mysql = {
enable = true;
ensureDatabases = [ "grafana" ];
ensureUsers = [
{
name = "grafana";
ensurePermissions."grafana.*" = "ALL PRIVILEGES";
}
];
package = pkgs.mariadb;
};
systemd.services.grafana.after = [ "mysql.service" ];
mysql = {
services.grafana.settings.database.user = "grafana";
services.mysql = {
enable = true;
ensureDatabases = [ "grafana" ];
ensureUsers = [
{
name = "grafana";
ensurePermissions."grafana.*" = "ALL PRIVILEGES";
}
];
package = pkgs.mariadb;
};
systemd.services.grafana.after = [ "mysql.service" ];
};
};
containers = builtins.mapAttrs (
_: val:
mkMerge [
val
baseGrafanaConf
]
) extraNodeConfs;
in
{
name = "grafana-basic";
containers = builtins.mapAttrs (
_: val:
mkMerge [
val
baseGrafanaConf
]
) extraNodeConfs;
in
{
name = "grafana-basic";
meta.maintainers = [ ];
meta.maintainers = [ ];
inherit containers;
inherit containers;
testScript = ''
start_all()
testScript = ''
start_all()
with subtest("Declarative plugins installed"):
declarativePlugins.wait_for_unit("grafana.service")
declarativePlugins.wait_for_open_port(3000, addr="::1")
declarativePlugins.succeed(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/plugins | grep grafana-clock-panel"
)
declarativePlugins.shutdown()
with subtest("Declarative plugins installed"):
declarativePlugins.wait_for_unit("grafana.service")
declarativePlugins.wait_for_open_port(3000, addr="::1")
declarativePlugins.succeed(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/plugins | grep grafana-clock-panel"
)
declarativePlugins.shutdown()
with subtest("Successful API query as admin user with sqlite db"):
sqlite.wait_for_unit("grafana.service")
sqlite.wait_for_open_port(3000)
print(sqlite.succeed(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/org/users -i"
))
sqlite.succeed(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/org/users | grep admin\@localhost"
)
sqlite.shutdown()
with subtest("Successful API query as admin user with sqlite db"):
sqlite.wait_for_unit("grafana.service")
sqlite.wait_for_open_port(3000)
print(sqlite.succeed(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/org/users -i"
))
sqlite.succeed(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/org/users | grep admin\@localhost"
)
sqlite.shutdown()
with subtest("Successful API query as admin user with sqlite db listening on socket"):
socket.wait_for_unit("grafana.service")
socket.wait_for_open_port(80)
print(socket.succeed(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]/api/org/users -i"
))
socket.succeed(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]/api/org/users | grep admin\@localhost"
)
socket.shutdown()
with subtest("Successful API query as admin user with sqlite db listening on socket"):
socket.wait_for_unit("grafana.service")
socket.wait_for_open_port(80)
print(socket.succeed(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]/api/org/users -i"
))
socket.succeed(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]/api/org/users | grep admin\@localhost"
)
socket.shutdown()
with subtest("Successful API query as admin user with postgresql db"):
postgresql.wait_for_unit("grafana.service")
postgresql.wait_for_unit("postgresql.target")
postgresql.wait_for_open_port(3000)
postgresql.wait_for_open_port(5432)
postgresql.succeed(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/org/users | grep admin\@localhost"
)
postgresql.shutdown()
with subtest("Successful API query as admin user with postgresql db"):
postgresql.wait_for_unit("grafana.service")
postgresql.wait_for_unit("postgresql.target")
postgresql.wait_for_open_port(3000)
postgresql.wait_for_open_port(5432)
postgresql.succeed(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/org/users | grep admin\@localhost"
)
postgresql.shutdown()
with subtest("Successful API query as admin user with mysql db"):
mysql.wait_for_unit("grafana.service")
mysql.wait_for_unit("mysql.service")
mysql.wait_for_open_port(3000)
mysql.wait_for_open_port(3306)
mysql.succeed(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/org/users | grep admin\@localhost"
)
mysql.shutdown()
'';
}
)
with subtest("Successful API query as admin user with mysql db"):
mysql.wait_for_unit("grafana.service")
mysql.wait_for_unit("mysql.service")
mysql.wait_for_open_port(3000)
mysql.wait_for_open_port(3306)
mysql.succeed(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/org/users | grep admin\@localhost"
)
mysql.shutdown()
'';
}

View File

@@ -1,10 +1,8 @@
{
system ? builtins.currentSystem,
config ? { },
pkgs ? import ../../.. { inherit system config; },
runTest,
}:
{
basic = import ./basic.nix { inherit system pkgs; };
provision = import ./provision { inherit system pkgs; };
basic = runTest ./basic.nix;
provision = runTest ./provision;
}

View File

@@ -1,249 +1,247 @@
import ../../make-test-python.nix (
{ lib, pkgs, ... }:
{ lib, pkgs, ... }:
let
inherit (lib) mkMerge;
let
inherit (lib) mkMerge;
baseGrafanaConf = {
services.grafana = {
enable = true;
provision.enable = true;
settings = {
analytics.reporting_enabled = false;
baseGrafanaConf = {
services.grafana = {
enable = true;
provision.enable = true;
settings = {
analytics.reporting_enabled = false;
server = {
http_addr = "::1";
domain = "localhost";
};
server = {
http_addr = "::1";
domain = "localhost";
};
security = {
admin_user = "testadmin";
admin_password = "$__file{${pkgs.writeText "pwd" "snakeoilpwd"}}";
secret_key = "11111111111111111111";
};
security = {
admin_user = "testadmin";
admin_password = "$__file{${pkgs.writeText "pwd" "snakeoilpwd"}}";
secret_key = "11111111111111111111";
};
};
systemd.tmpfiles.rules =
let
dashboard = pkgs.writeText "test.json" (builtins.readFile ./test_dashboard.json);
in
[
"d /var/lib/grafana/dashboards 0700 grafana grafana -"
"C+ /var/lib/grafana/dashboards/test.json - - - - ${dashboard}"
];
};
extraNodeConfs = {
provisionNix = {
services.grafana.provision = {
datasources.settings = {
apiVersion = 1;
datasources = [
{
name = "Test Datasource";
type = "testdata";
access = "proxy";
uid = "test_datasource";
}
];
};
systemd.tmpfiles.rules =
let
dashboard = pkgs.writeText "test.json" (builtins.readFile ./test_dashboard.json);
in
[
"d /var/lib/grafana/dashboards 0700 grafana grafana -"
"C+ /var/lib/grafana/dashboards/test.json - - - - ${dashboard}"
];
};
dashboards.settings = {
apiVersion = 1;
providers = [
{
name = "default";
options.path = "/var/lib/grafana/dashboards";
}
];
};
extraNodeConfs = {
provisionNix = {
services.grafana.provision = {
datasources.settings = {
apiVersion = 1;
datasources = [
{
name = "Test Datasource";
type = "testdata";
access = "proxy";
uid = "test_datasource";
}
];
};
alerting = {
rules.settings = {
groups = [
{
name = "test_rule_group";
folder = "test_folder";
interval = "60s";
rules = [
{
uid = "test_rule";
title = "Test Rule";
condition = "A";
data = [
{
refId = "A";
datasourceUid = "-100";
model = {
conditions = [
{
evaluator = {
params = [ 3 ];
type = "git";
};
operator.type = "and";
query.params = [ "A" ];
reducer.type = "last";
type = "query";
}
];
datasource = {
type = "__expr__";
uid = "-100";
};
expression = "1==0";
intervalMs = 1000;
maxDataPoints = 43200;
refId = "A";
type = "math";
dashboards.settings = {
apiVersion = 1;
providers = [
{
name = "default";
options.path = "/var/lib/grafana/dashboards";
}
];
};
alerting = {
rules.settings = {
groups = [
{
name = "test_rule_group";
folder = "test_folder";
interval = "60s";
rules = [
{
uid = "test_rule";
title = "Test Rule";
condition = "A";
data = [
{
refId = "A";
datasourceUid = "-100";
model = {
conditions = [
{
evaluator = {
params = [ 3 ];
type = "git";
};
operator.type = "and";
query.params = [ "A" ];
reducer.type = "last";
type = "query";
}
];
datasource = {
type = "__expr__";
uid = "-100";
};
}
];
for = "60s";
}
];
}
];
};
expression = "1==0";
intervalMs = 1000;
maxDataPoints = 43200;
refId = "A";
type = "math";
};
}
];
for = "60s";
}
];
}
];
};
contactPoints.settings = {
contactPoints = [
{
name = "Test Contact Point";
receivers = [
{
uid = "test_contact_point";
type = "prometheus-alertmanager";
settings.url = "http://localhost:9000";
}
];
}
];
};
contactPoints.settings = {
contactPoints = [
{
name = "Test Contact Point";
receivers = [
{
uid = "test_contact_point";
type = "prometheus-alertmanager";
settings.url = "http://localhost:9000";
}
];
}
];
};
policies.settings = {
policies = [
{
receiver = "Test Contact Point";
}
];
};
policies.settings = {
policies = [
{
receiver = "Test Contact Point";
}
];
};
templates.settings = {
templates = [
{
name = "Test Template";
template = "Test message";
}
];
};
templates.settings = {
templates = [
{
name = "Test Template";
template = "Test message";
}
];
};
muteTimings.settings = {
muteTimes = [
{
name = "Test Mute Timing";
}
];
};
muteTimings.settings = {
muteTimes = [
{
name = "Test Mute Timing";
}
];
};
};
};
provisionYaml = {
services.grafana.provision = {
datasources.path = ./datasources.yaml;
dashboards.path = ./dashboards.yaml;
alerting = {
rules.path = ./rules.yaml;
contactPoints.path = ./contact-points.yaml;
policies.path = ./policies.yaml;
templates.path = ./templates.yaml;
muteTimings.path = ./mute-timings.yaml;
};
};
};
provisionYamlDirs =
let
mkdir = p: pkgs.writeTextDir (baseNameOf p) (builtins.readFile p);
in
{
services.grafana.provision = {
datasources.path = mkdir ./datasources.yaml;
dashboards.path = mkdir ./dashboards.yaml;
alerting = {
rules.path = mkdir ./rules.yaml;
contactPoints.path = mkdir ./contact-points.yaml;
policies.path = mkdir ./policies.yaml;
templates.path = mkdir ./templates.yaml;
muteTimings.path = mkdir ./mute-timings.yaml;
};
};
};
};
containers = builtins.mapAttrs (
_: val:
mkMerge [
val
baseGrafanaConf
]
) extraNodeConfs;
in
{
name = "grafana-provision";
provisionYaml = {
services.grafana.provision = {
datasources.path = ./datasources.yaml;
dashboards.path = ./dashboards.yaml;
alerting = {
rules.path = ./rules.yaml;
contactPoints.path = ./contact-points.yaml;
policies.path = ./policies.yaml;
templates.path = ./templates.yaml;
muteTimings.path = ./mute-timings.yaml;
};
};
};
meta.maintainers = [ ];
provisionYamlDirs =
let
mkdir = p: pkgs.writeTextDir (baseNameOf p) (builtins.readFile p);
in
{
services.grafana.provision = {
datasources.path = mkdir ./datasources.yaml;
dashboards.path = mkdir ./dashboards.yaml;
alerting = {
rules.path = mkdir ./rules.yaml;
contactPoints.path = mkdir ./contact-points.yaml;
policies.path = mkdir ./policies.yaml;
templates.path = mkdir ./templates.yaml;
muteTimings.path = mkdir ./mute-timings.yaml;
};
};
};
};
inherit containers;
containers = builtins.mapAttrs (
_: val:
mkMerge [
val
baseGrafanaConf
]
) extraNodeConfs;
in
{
name = "grafana-provision";
testScript = ''
start_all()
meta.maintainers = [ ];
nodeNix = ("Nix (new format)", provisionNix)
nodeYaml = ("Nix (YAML)", provisionYaml)
nodeYamlDir = ("Nix (YAML in dirs)", provisionYamlDirs)
inherit containers;
for description, machine in [nodeNix, nodeYaml, nodeYamlDir]:
with subtest(f"Should start provision node: {description}"):
machine.wait_for_unit("grafana.service")
machine.wait_for_open_port(3000, addr="::1")
testScript = ''
start_all()
with subtest(f"Successful datasource provision with {description}"):
machine.wait_until_succeeds(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/datasources/uid/test_datasource | grep Test\ Datasource"
)
nodeNix = ("Nix (new format)", provisionNix)
nodeYaml = ("Nix (YAML)", provisionYaml)
nodeYamlDir = ("Nix (YAML in dirs)", provisionYamlDirs)
with subtest(f"Successful dashboard provision with {description}"):
machine.wait_until_succeeds(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/dashboards/uid/test_dashboard | grep Test\ Dashboard"
)
for description, machine in [nodeNix, nodeYaml, nodeYamlDir]:
with subtest(f"Should start provision node: {description}"):
machine.wait_for_unit("grafana.service")
machine.wait_for_open_port(3000, addr="::1")
with subtest(f"Successful rule provision with {description}"):
machine.wait_until_succeeds(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/v1/provisioning/alert-rules/test_rule | grep Test\ Rule"
)
with subtest(f"Successful datasource provision with {description}"):
machine.wait_until_succeeds(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/datasources/uid/test_datasource | grep Test\ Datasource"
)
with subtest(f"Successful contact point provision with {description}"):
machine.wait_until_succeeds(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/v1/provisioning/contact-points | grep Test\ Contact\ Point"
)
with subtest(f"Successful dashboard provision with {description}"):
machine.wait_until_succeeds(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/dashboards/uid/test_dashboard | grep Test\ Dashboard"
)
with subtest(f"Successful policy provision with {description}"):
machine.wait_until_succeeds(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/v1/provisioning/policies | grep Test\ Contact\ Point"
)
with subtest(f"Successful rule provision with {description}"):
machine.wait_until_succeeds(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/v1/provisioning/alert-rules/test_rule | grep Test\ Rule"
)
with subtest(f"Successful template provision with {description}"):
machine.wait_until_succeeds(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/v1/provisioning/templates | grep Test\ Template"
)
with subtest(f"Successful contact point provision with {description}"):
machine.wait_until_succeeds(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/v1/provisioning/contact-points | grep Test\ Contact\ Point"
)
with subtest("Successful mute timings provision with {description}"):
machine.wait_until_succeeds(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/v1/provisioning/mute-timings | grep Test\ Mute\ Timing"
)
'';
}
)
with subtest(f"Successful policy provision with {description}"):
machine.wait_until_succeeds(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/v1/provisioning/policies | grep Test\ Contact\ Point"
)
with subtest(f"Successful template provision with {description}"):
machine.wait_until_succeeds(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/v1/provisioning/templates | grep Test\ Template"
)
with subtest("Successful mute timings provision with {description}"):
machine.wait_until_succeeds(
"curl -sSfN -u testadmin:snakeoilpwd http://[::1]:3000/api/v1/provisioning/mute-timings | grep Test\ Mute\ Timing"
)
'';
}

View File

@@ -32,21 +32,17 @@
machine.succeed("curl -sSf http://localhost/")
machine.succeed(
"curl -c cookies -sSf -X POST http://localhost/login -d 'username=admin&password=admin'"
)
cookie = machine.succeed(
"grep -v '^#' cookies | awk '{ print $7 }' | sed -e '/^$/d' | perl -pe 'chomp'"
"curl --cookie-jar cookies.txt -sSf -X POST http://localhost/login -d 'username=admin&password=admin'"
)
machine.succeed(
f"curl -sSf -X POST http://localhost/api/objects/tasks -b 'grocy_session={cookie}' "
"curl -sSf -X POST http://localhost/api/objects/tasks --cookie cookies.txt "
+ '-d \'{"assigned_to_user_id":1,"name":"Test Task","due_date":"1970-01-01"}\'''
+ " --header 'Content-Type: application/json'"
)
task_name = machine.succeed(
f"curl -sSf http://localhost/api/tasks -b 'grocy_session={cookie}' --header 'Accept: application/json' | jq '.[].name' | xargs echo | perl -pe 'chomp'"
"curl -sSf http://localhost/api/tasks --cookie cookies.txt --header 'Accept: application/json' | jq '.[].name' | xargs echo | perl -pe 'chomp'"
)
assert task_name == "Test Task"
@@ -62,7 +58,7 @@
)
machine.succeed(
f"curl -sSf -X 'PUT' -b 'grocy_session={cookie}' "
"curl -sSf -X 'PUT' --cookie cookies.txt "
+ f" 'http://localhost/api/files/equipmentmanuals/{file_name_base64_urlencode}' "
+ " --header 'Accept: */*' "
+ " --header 'Content-Type: application/octet-stream' "
@@ -70,7 +66,7 @@
)
machine.succeed(
f"curl -sSf -X 'GET' -b 'grocy_session={cookie}' "
"curl -sSf -X 'GET' --cookie cookies.txt "
+ f" 'http://localhost/api/files/equipmentmanuals/{file_name_base64_urlencode}' "
+ " --header 'Accept: application/octet-stream' "
+ f" | cmp /tmp/{file_name}"

View File

@@ -1,4 +1,4 @@
{ handleTestOn, package, ... }:
{ handleTestOn, package }:
{
all = handleTestOn [ "x86_64-linux" "aarch64-linux" ] ./hadoop.nix {

View File

@@ -0,0 +1,41 @@
{ pkgs, ... }:
{
name = "hickory-dns";
meta.maintainers = with pkgs.lib.maintainers; [ adamcstephens ];
containers.machine = {
environment.systemPackages = [ pkgs.doggo ];
services.hickory-dns = {
enable = true;
settings.zones = [
{
zone = "example.test";
file = pkgs.writeText "example.test.zone" ''
$ORIGIN example.test.
$TTL 3600
@ IN SOA ns.example.test. hostmaster.example.test. (1 3600 600 86400 3600)
@ IN NS ns.example.test.
ns IN A 127.0.0.1
www IN A 192.0.2.1
'';
}
];
};
};
testScript = ''
import json
machine.start()
machine.wait_for_unit("hickory-dns.service")
machine.wait_for_open_port(53)
response = json.loads(machine.succeed("doggo @127.0.0.1 www.example.test. A --json"))
answers = response["responses"][0]["answers"]
assert [(answer["name"], answer["type"], answer["address"]) for answer in answers] == [
("www.example.test.", "A", "192.0.2.1")
], response
'';
}

View File

@@ -1,21 +1,21 @@
{ lib, ... }:
{
name = "freenet";
name = "hyphanet";
meta = {
maintainers = with lib.maintainers; [ nagy ];
};
nodes = {
machine = {
services.freenet.enable = true;
services.hyphanet.enable = true;
};
};
testScript = ''
machine.wait_for_unit("freenet.service")
machine.wait_for_unit("hyphanet.service")
machine.wait_for_open_port(8888)
machine.wait_until_succeeds("curl -sfL http://localhost:8888/ | grep Freenet")
machine.succeed("systemctl stop freenet")
machine.succeed("systemctl stop hyphanet")
'';
}

61
nixos/tests/iocaine.nix Normal file
View File

@@ -0,0 +1,61 @@
{ lib, ... }:
{
name = "iocaine";
meta.maintainers = with lib.maintainers; [ poz ];
nodes = {
iocaine_default = {
services.iocaine = {
enable = true;
};
};
reverse_proxy_integration = {
services.iocaine = {
enable = true;
settings.server.main = {
bind = "/run/iocaine/iocaine.socket";
unix-socket-access = "group";
mode = "http";
use = {
handler-from = "default";
};
};
settings.handler.default = { };
};
services.caddy = {
enable = true;
globalConfig = ''
http_port 8080
https_port 8081
'';
};
services.nginx.enable = true;
};
};
testScript = ''
start_all()
iocaine_default.wait_for_unit("iocaine.service")
iocaine_default.fail("curl -s --show-error --fail http://127.0.0.1:42069/random-path/yes/")
iocaine_default.fail("curl -s --show-error --fail http://127.0.0.1:42069/ -A 'Googlebot'")
iocaine_default.succeed("curl -s --show-error --fail http://127.0.0.1:42069/a/path/very/deep/into/the/forest/ -A 'Perplexity'")
iocaine_default.fail("curl -s --show-error --fail http://127.0.0.1:42042/metrics")
reverse_proxy_integration.wait_for_unit("iocaine.service")
reverse_proxy_integration.wait_for_unit("caddy.service")
reverse_proxy_integration.wait_for_unit("nginx.service")
reverse_proxy_integration.stop_job("nginx")
reverse_proxy_integration.stop_job("caddy")
reverse_proxy_integration.stop_job("iocaine")
reverse_proxy_integration.start_job("nginx")
reverse_proxy_integration.succeed("systemctl is-active iocaine.service")
reverse_proxy_integration.stop_job("nginx")
reverse_proxy_integration.stop_job("iocaine")
reverse_proxy_integration.start_job("caddy")
reverse_proxy_integration.succeed("systemctl is-active iocaine.service")
'';
}

View File

@@ -127,14 +127,14 @@
def api_get(path):
return f"curl --fail 'http://localhost:8096{path}' -H 'X-Emby-Authorization:{auth_header}'"
return f"curl --fail 'http://localhost:8096{path}' -H 'Authorization:{auth_header}'"
def api_post(path, json_file=None):
if json_file:
return f"curl --fail -X post 'http://localhost:8096{path}' -d '@{json_file}' -H Content-Type:application/json -H 'X-Emby-Authorization:{auth_header}'"
return f"curl --fail -X post 'http://localhost:8096{path}' -d '@{json_file}' -H Content-Type:application/json -H 'Authorization:{auth_header}'"
else:
return f"curl --fail -X post 'http://localhost:8096{path}' -H 'X-Emby-Authorization:{auth_header}'"
return f"curl --fail -X post 'http://localhost:8096{path}' -H 'Authorization:{auth_header}'"
# Test dashboard-based configuration verification
with subtest("Dashboard configuration verification"):
@@ -149,7 +149,7 @@
token = auth_result["AccessToken"]
def api_get_with_token(path):
return f"curl --fail 'http://localhost:8096{path}' -H 'X-Emby-Authorization:MediaBrowser Client=\"Test\", DeviceId=\"test\", Token={token}'"
return f"curl --fail 'http://localhost:8096{path}' -H 'Authorization:MediaBrowser Client=\"Test\", DeviceId=\"test\", Token={token}'"
# Get encoding config and verify key settings
config = json.loads(machineWithTranscoding.succeed(api_get_with_token("/System/Configuration/encoding")))
@@ -279,7 +279,7 @@
machine.succeed(
"ffmpeg"
+ f" -headers 'X-Emby-Authorization:{auth_header}'"
+ f" -headers 'Authorization:{auth_header}'"
+ f" -i http://localhost:8096/Videos/{video}/master.m3u8?mediaSourceId={media_source_id}"
+ " /tmp/test.mkv"
)

View File

@@ -1,12 +1,17 @@
{ pkgs, ... }:
{
pkgs,
runTest,
lib,
...
}:
with pkgs.lib;
with lib;
let
makeKafkaTest =
name:
{ kafkaPackage }:
(import ../make-test-python.nix {
(runTest {
inherit name;
nodes = {

View File

@@ -1,199 +1,197 @@
import ../make-test-python.nix (
{ lib, pkgs, ... }:
{ lib, pkgs, ... }:
let
inherit (lib) mkMerge;
let
inherit (lib) mkMerge;
# Generate with `kafka-storage.sh random-uuid`
clusterId = "ii5pZE5LRkSeWrnyBhMOYQ";
# Generate with `kafka-storage.sh random-uuid`
clusterId = "ii5pZE5LRkSeWrnyBhMOYQ";
kafkaConfig = {
networking.firewall.allowedTCPPorts = [
9092
9093
];
kafkaConfig = {
networking.firewall.allowedTCPPorts = [
9092
9093
];
virtualisation.diskSize = 1024;
virtualisation.memorySize = 1024 * 2;
virtualisation.diskSize = 1024;
virtualisation.memorySize = 1024 * 2;
environment.systemPackages = [ pkgs.apacheKafka ];
environment.systemPackages = [ pkgs.apacheKafka ];
services.apache-kafka = {
enable = true;
services.apache-kafka = {
enable = true;
clusterId = "${clusterId}";
clusterId = "${clusterId}";
formatLogDirs = true;
formatLogDirs = true;
settings = {
listeners = [
"PLAINTEXT://:9092"
"CONTROLLER://:9093"
];
"listener.security.protocol.map" = [
"PLAINTEXT:PLAINTEXT"
"CONTROLLER:PLAINTEXT"
];
"controller.quorum.voters" = lib.imap1 (i: name: "${toString i}@${name}:9093") (
builtins.attrNames kafkaNodes
);
"controller.listener.names" = [ "CONTROLLER" ];
settings = {
listeners = [
"PLAINTEXT://:9092"
"CONTROLLER://:9093"
];
"listener.security.protocol.map" = [
"PLAINTEXT:PLAINTEXT"
"CONTROLLER:PLAINTEXT"
];
"controller.quorum.voters" = lib.imap1 (i: name: "${toString i}@${name}:9093") (
builtins.attrNames kafkaNodes
);
"controller.listener.names" = [ "CONTROLLER" ];
"process.roles" = [
"broker"
"controller"
];
"process.roles" = [
"broker"
"controller"
];
"log.dirs" = [ "/var/lib/apache-kafka" ];
"num.partitions" = 6;
"offsets.topic.replication.factor" = 2;
"transaction.state.log.replication.factor" = 2;
"transaction.state.log.min.isr" = 2;
};
};
systemd.services.apache-kafka = {
after = [ "network-online.target" ];
requires = [ "network-online.target" ];
serviceConfig.StateDirectory = "apache-kafka";
"log.dirs" = [ "/var/lib/apache-kafka" ];
"num.partitions" = 6;
"offsets.topic.replication.factor" = 2;
"transaction.state.log.replication.factor" = 2;
"transaction.state.log.min.isr" = 2;
};
};
extraKafkaConfig = {
kafka1 = {
services.apache-kafka.settings = {
"node.id" = 1;
"broker.rack" = 1;
};
};
systemd.services.apache-kafka = {
after = [ "network-online.target" ];
requires = [ "network-online.target" ];
serviceConfig.StateDirectory = "apache-kafka";
};
};
kafka2 = {
services.apache-kafka.settings = {
"node.id" = 2;
"broker.rack" = 2;
};
};
kafka3 = {
services.apache-kafka.settings = {
"node.id" = 3;
"broker.rack" = 3;
};
};
kafka4 = {
services.apache-kafka.settings = {
"node.id" = 4;
"broker.rack" = 3;
};
extraKafkaConfig = {
kafka1 = {
services.apache-kafka.settings = {
"node.id" = 1;
"broker.rack" = 1;
};
};
kafkaNodes = builtins.mapAttrs (
_: val:
mkMerge [
val
kafkaConfig
]
) extraKafkaConfig;
in
{
name = "kafka-cluster";
meta = with pkgs.lib.maintainers; {
maintainers = [ jpds ];
kafka2 = {
services.apache-kafka.settings = {
"node.id" = 2;
"broker.rack" = 2;
};
};
nodes = {
inherit (kafkaNodes)
kafka1
kafka2
kafka3
kafka4
;
client =
{ config, ... }:
{
environment.systemPackages = [ pkgs.apacheKafka ];
virtualisation.diskSize = 1024;
};
kafka3 = {
services.apache-kafka.settings = {
"node.id" = 3;
"broker.rack" = 3;
};
};
testScript = ''
import json
kafka4 = {
services.apache-kafka.settings = {
"node.id" = 4;
"broker.rack" = 3;
};
};
};
for machine in kafka1, kafka2, kafka3, kafka4:
machine.wait_for_unit("apache-kafka")
kafkaNodes = builtins.mapAttrs (
_: val:
mkMerge [
val
kafkaConfig
]
) extraKafkaConfig;
in
{
name = "kafka-cluster";
meta = with pkgs.lib.maintainers; {
maintainers = [ jpds ];
};
for machine in kafka1, kafka2, kafka3, kafka4:
machine.wait_for_open_port(9092)
machine.wait_for_open_port(9093)
nodes = {
inherit (kafkaNodes)
kafka1
kafka2
kafka3
kafka4
;
machine.wait_until_succeeds(
"journalctl -o cat -u apache-kafka.service | grep 'Transition from STARTING to STARTED'"
)
client =
{ config, ... }:
{
environment.systemPackages = [ pkgs.apacheKafka ];
virtualisation.diskSize = 1024;
};
};
machine.wait_until_succeeds(
"journalctl -o cat -u apache-kafka.service | grep 'Kafka Server started'"
)
testScript = ''
import json
machine.wait_until_succeeds(
"journalctl -o cat -u apache-kafka.service | grep 'BrokerLifecycleManager' | grep 'Incarnation [[:graph:]]\+ of broker [[:digit:]] in cluster ${clusterId}'"
)
for machine in kafka1, kafka2, kafka3, kafka4:
machine.wait_for_unit("apache-kafka")
current_voters_json = kafka1.wait_until_succeeds(
"kafka-metadata-quorum.sh --bootstrap-server kafka1:9092,kafka2:9092,kafka3:9092 describe --status | grep CurrentVoters"
).replace("CurrentVoters:", "")
for machine in kafka1, kafka2, kafka3, kafka4:
machine.wait_for_open_port(9092)
machine.wait_for_open_port(9093)
voters = json.loads(current_voters_json)
assert len(voters) == 4
kafka1.wait_until_succeeds(
"kafka-topics.sh --bootstrap-server kafka1:9092 --create --topic test-123 --replication-factor 2"
machine.wait_until_succeeds(
"journalctl -o cat -u apache-kafka.service | grep 'Transition from STARTING to STARTED'"
)
for machine in kafka1, kafka2, kafka3, kafka4:
machine.wait_until_succeeds(
"journalctl -o cat -u apache-kafka.service | grep -E 'Created log for partition test-123-[[:digit:]] in /var/lib/apache-kafka/test-123-[[:digit:]] with properties'"
)
kafka1.wait_until_succeeds(
"kafka-topics.sh --bootstrap-server=kafka1:9092 --describe --topic test-123 | "
+ "grep 'PartitionCount: 6'"
machine.wait_until_succeeds(
"journalctl -o cat -u apache-kafka.service | grep 'Kafka Server started'"
)
# Should never see a replica on both 3 and 4 as they're in the same rack
kafka1.fail(
"kafka-topics.sh --bootstrap-server=kafka1:9092 --describe --topic test-123 | "
+ "grep -E 'Replicas: (3,4|4,3)'"
machine.wait_until_succeeds(
"journalctl -o cat -u apache-kafka.service | grep 'BrokerLifecycleManager' | grep 'Incarnation [[:graph:]]\+ of broker [[:digit:]] in cluster ${clusterId}'"
)
client.succeed(
"echo 'test 2' | "
+ "kafka-console-producer.sh "
+ "--bootstrap-server kafka1:9092 "
+ "--topic test-123"
)
assert "test 2" in client.succeed(
"kafka-console-consumer.sh "
+ "--bootstrap-server kafka2:9092 --topic test-123 "
+ "--group readtest "
+ "--from-beginning --max-messages 1"
current_voters_json = kafka1.wait_until_succeeds(
"kafka-metadata-quorum.sh --bootstrap-server kafka1:9092,kafka2:9092,kafka3:9092 describe --status | grep CurrentVoters"
).replace("CurrentVoters:", "")
voters = json.loads(current_voters_json)
assert len(voters) == 4
kafka1.wait_until_succeeds(
"kafka-topics.sh --bootstrap-server kafka1:9092 --create --topic test-123 --replication-factor 2"
)
for machine in kafka1, kafka2, kafka3, kafka4:
machine.wait_until_succeeds(
"journalctl -o cat -u apache-kafka.service | grep -E 'Created log for partition test-123-[[:digit:]] in /var/lib/apache-kafka/test-123-[[:digit:]] with properties'"
)
client.succeed(
"echo 'test 3' | "
+ "kafka-console-producer.sh "
+ "--bootstrap-server kafka2:9092 "
+ "--topic test-123"
)
assert "test 3" in client.succeed(
"kafka-console-consumer.sh "
+ "--bootstrap-server kafka3:9092 --topic test-123 "
+ "--group readtest "
+ "--max-messages 1"
)
'';
}
)
kafka1.wait_until_succeeds(
"kafka-topics.sh --bootstrap-server=kafka1:9092 --describe --topic test-123 | "
+ "grep 'PartitionCount: 6'"
)
# Should never see a replica on both 3 and 4 as they're in the same rack
kafka1.fail(
"kafka-topics.sh --bootstrap-server=kafka1:9092 --describe --topic test-123 | "
+ "grep -E 'Replicas: (3,4|4,3)'"
)
client.succeed(
"echo 'test 2' | "
+ "kafka-console-producer.sh "
+ "--bootstrap-server kafka1:9092 "
+ "--topic test-123"
)
assert "test 2" in client.succeed(
"kafka-console-consumer.sh "
+ "--bootstrap-server kafka2:9092 --topic test-123 "
+ "--group readtest "
+ "--from-beginning --max-messages 1"
)
client.succeed(
"echo 'test 3' | "
+ "kafka-console-producer.sh "
+ "--bootstrap-server kafka2:9092 "
+ "--topic test-123"
)
assert "test 3" in client.succeed(
"kafka-console-consumer.sh "
+ "--bootstrap-server kafka3:9092 --topic test-123 "
+ "--group readtest "
+ "--max-messages 1"
)
'';
}

View File

@@ -1,11 +1,13 @@
{
system ? builtins.currentSystem,
config ? { },
pkgs ? import ../../.. { inherit system config; },
runTest,
pkgs,
}:
{
base = import ./base.nix { inherit system pkgs; };
cluster = import ./cluster.nix { inherit system pkgs; };
mirrormaker = import ./mirrormaker.nix { inherit system pkgs; };
base = import ./base.nix {
inherit pkgs runTest;
inherit (pkgs) lib;
};
cluster = runTest ./cluster.nix;
mirrormaker = runTest ./mirrormaker.nix;
}

View File

@@ -1,240 +1,238 @@
import ../make-test-python.nix (
{ lib, pkgs, ... }:
{ lib, pkgs, ... }:
let
inherit (lib) mkMerge;
let
inherit (lib) mkMerge;
# Generate with `kafka-storage.sh random-uuid`
clusterAId = "ihzlrasUQ9O3Yy0ZWYkd6w";
# Generate with `kafka-storage.sh random-uuid`
clusterAId = "ihzlrasUQ9O3Yy0ZWYkd6w";
clusterBId = "Bnu_zrzKRH6-7KcK7t3I5Q";
clusterBId = "Bnu_zrzKRH6-7KcK7t3I5Q";
kafkaConfig = {
networking.firewall.allowedTCPPorts = [
9092
9093
];
kafkaConfig = {
networking.firewall.allowedTCPPorts = [
9092
9093
];
virtualisation.diskSize = 1024;
virtualisation.memorySize = 1024 * 2;
virtualisation.diskSize = 1024;
virtualisation.memorySize = 1024 * 2;
environment.systemPackages = [ pkgs.apacheKafka ];
environment.systemPackages = [ pkgs.apacheKafka ];
services.apache-kafka = {
enable = true;
formatLogDirs = true;
settings = {
listeners = [
"PLAINTEXT://:9092"
"CONTROLLER://:9093"
];
"listener.security.protocol.map" = [
"PLAINTEXT:PLAINTEXT"
"CONTROLLER:PLAINTEXT"
];
"controller.listener.names" = [ "CONTROLLER" ];
"process.roles" = [
"broker"
"controller"
];
"log.dirs" = [ "/var/lib/apache-kafka" ];
"num.partitions" = 1;
"offsets.topic.replication.factor" = 1;
"transaction.state.log.replication.factor" = 1;
"transaction.state.log.min.isr" = 1;
};
};
systemd.services.apache-kafka = {
after = [ "network-online.target" ];
requires = [ "network-online.target" ];
serviceConfig.StateDirectory = "apache-kafka";
};
};
extraKafkaConfig = {
kafkaa1 = {
services.apache-kafka = {
enable = true;
formatLogDirs = true;
clusterId = "${clusterAId}";
settings = {
listeners = [
"PLAINTEXT://:9092"
"CONTROLLER://:9093"
];
"listener.security.protocol.map" = [
"PLAINTEXT:PLAINTEXT"
"CONTROLLER:PLAINTEXT"
];
"controller.listener.names" = [ "CONTROLLER" ];
"process.roles" = [
"broker"
"controller"
];
"log.dirs" = [ "/var/lib/apache-kafka" ];
"num.partitions" = 1;
"offsets.topic.replication.factor" = 1;
"transaction.state.log.replication.factor" = 1;
"transaction.state.log.min.isr" = 1;
"node.id" = 1;
"controller.quorum.voters" = [ "1@kafkaa1:9093" ];
};
};
systemd.services.apache-kafka = {
after = [ "network-online.target" ];
requires = [ "network-online.target" ];
serviceConfig.StateDirectory = "apache-kafka";
};
};
extraKafkaConfig = {
kafkaa1 = {
services.apache-kafka = {
clusterId = "${clusterAId}";
kafkab1 = {
services.apache-kafka = {
clusterId = "${clusterBId}";
settings = {
"node.id" = 1;
"controller.quorum.voters" = [ "1@kafkaa1:9093" ];
settings = {
"node.id" = 1;
"controller.quorum.voters" = [ "1@kafkab1:9093" ];
};
};
};
};
kafkaNodes = builtins.mapAttrs (
_: val:
mkMerge [
val
kafkaConfig
]
) extraKafkaConfig;
mirrorMakerProperties = pkgs.writeText "mm2.properties" ''
name = A->B
clusters = A, B
A.bootstrap.servers = kafkaa1:9092
B.bootstrap.servers = kafkab1:9092
A->B.enabled = true
A->B.topics = .*
B->A.enabled = false
B->A.topics = .*
replication.factor=1
replication.policy.class=org.apache.kafka.connect.mirror.IdentityReplicationPolicy
tasks.max = 2
refresh.topics.enabled = true
refresh.topics.interval.seconds = 5
sync.topic.configs.enabled = true
checkpoints.topic.replication.factor=1
heartbeats.topic.replication.factor=1
offset-syncs.topic.replication.factor=1
offset.storage.replication.factor=1
status.storage.replication.factor=1
config.storage.replication.factor=1
emit.checkpoints.enabled = true
emit.checkpoints.interval.seconds = 5
'';
in
{
name = "kafka-mirrormaker";
meta = with pkgs.lib.maintainers; {
maintainers = [ jpds ];
};
nodes = {
inherit (kafkaNodes) kafkaa1 kafkab1;
mirrormaker =
{ config, ... }:
{
virtualisation.diskSize = 1024;
virtualisation.memorySize = 1024 * 2;
# Define a mirrormaker systemd service
systemd.services.kafka-connect-mirror-maker = {
after = [ "network-online.target" ];
requires = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = ''
${pkgs.apacheKafka}/bin/connect-mirror-maker.sh ${mirrorMakerProperties}
'';
Restart = "on-failure";
RestartSec = "5s";
};
};
};
};
kafkab1 = {
services.apache-kafka = {
clusterId = "${clusterBId}";
testScript = ''
import json
settings = {
"node.id" = 1;
"controller.quorum.voters" = [ "1@kafkab1:9093" ];
};
};
};
};
for machine in kafkaa1, kafkab1:
machine.wait_for_unit("apache-kafka")
kafkaNodes = builtins.mapAttrs (
_: val:
mkMerge [
val
kafkaConfig
]
) extraKafkaConfig;
for machine in kafkaa1, kafkab1:
machine.wait_for_open_port(9092)
machine.wait_for_open_port(9093)
mirrorMakerProperties = pkgs.writeText "mm2.properties" ''
name = A->B
clusters = A, B
A.bootstrap.servers = kafkaa1:9092
B.bootstrap.servers = kafkab1:9092
A->B.enabled = true
A->B.topics = .*
B->A.enabled = false
B->A.topics = .*
replication.factor=1
replication.policy.class=org.apache.kafka.connect.mirror.IdentityReplicationPolicy
tasks.max = 2
refresh.topics.enabled = true
refresh.topics.interval.seconds = 5
sync.topic.configs.enabled = true
checkpoints.topic.replication.factor=1
heartbeats.topic.replication.factor=1
offset-syncs.topic.replication.factor=1
offset.storage.replication.factor=1
status.storage.replication.factor=1
config.storage.replication.factor=1
emit.checkpoints.enabled = true
emit.checkpoints.interval.seconds = 5
'';
in
{
name = "kafka-mirrormaker";
meta = with pkgs.lib.maintainers; {
maintainers = [ jpds ];
};
nodes = {
inherit (kafkaNodes) kafkaa1 kafkab1;
mirrormaker =
{ config, ... }:
{
virtualisation.diskSize = 1024;
virtualisation.memorySize = 1024 * 2;
# Define a mirrormaker systemd service
systemd.services.kafka-connect-mirror-maker = {
after = [ "network-online.target" ];
requires = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = ''
${pkgs.apacheKafka}/bin/connect-mirror-maker.sh ${mirrorMakerProperties}
'';
Restart = "on-failure";
RestartSec = "5s";
};
};
};
};
testScript = ''
import json
for machine in kafkaa1, kafkab1:
machine.wait_for_unit("apache-kafka")
for machine in kafkaa1, kafkab1:
machine.wait_for_open_port(9092)
machine.wait_for_open_port(9093)
machine.wait_until_succeeds(
"journalctl -o cat -u apache-kafka.service | grep 'Transition from STARTING to STARTED'"
)
machine.wait_until_succeeds(
"journalctl -o cat -u apache-kafka.service | grep 'Kafka Server started'"
)
for machine in kafkaa1, kafkab1:
current_voters_json = machine.wait_until_succeeds(
f"kafka-metadata-quorum.sh --bootstrap-server {machine.name}:9092 describe --status | grep CurrentVoters"
).replace("CurrentVoters:", "")
voters = json.loads(current_voters_json)
assert len(voters) == 1
mirrormaker.wait_for_unit("kafka-connect-mirror-maker")
mirrormaker.wait_until_succeeds(
"journalctl -o cat -u kafka-connect-mirror-maker.service | grep 'Kafka MirrorMaker initializing'"
)
mirrormaker.wait_until_succeeds(
"journalctl -o cat -u kafka-connect-mirror-maker.service | grep 'Targeting clusters \[A, B\]'"
)
mirrormaker.wait_until_succeeds(
"journalctl -o cat -u kafka-connect-mirror-maker.service | grep 'INFO \[Worker clientId=A->B, groupId=A-mm2\] Finished starting connectors and tasks'"
machine.wait_until_succeeds(
"journalctl -o cat -u apache-kafka.service | grep 'Transition from STARTING to STARTED'"
)
mirrormaker.wait_until_succeeds(
"""
journalctl -o cat -u kafka-connect-mirror-maker.service | grep 'INFO \[MirrorSourceConnector\|task-0\] \[Producer clientId=A->B\|A->B-0\|offset-syncs-source-producer\] Cluster ID: ${clusterAId}'
"""
machine.wait_until_succeeds(
"journalctl -o cat -u apache-kafka.service | grep 'Kafka Server started'"
)
kafkaa1.wait_until_succeeds(
"journalctl -o cat -u apache-kafka.service | grep 'Stabilized group B-mm2'"
for machine in kafkaa1, kafkab1:
current_voters_json = machine.wait_until_succeeds(
f"kafka-metadata-quorum.sh --bootstrap-server {machine.name}:9092 describe --status | grep CurrentVoters"
).replace("CurrentVoters:", "")
voters = json.loads(current_voters_json)
assert len(voters) == 1
mirrormaker.wait_for_unit("kafka-connect-mirror-maker")
mirrormaker.wait_until_succeeds(
"journalctl -o cat -u kafka-connect-mirror-maker.service | grep 'Kafka MirrorMaker initializing'"
)
mirrormaker.wait_until_succeeds(
"journalctl -o cat -u kafka-connect-mirror-maker.service | grep 'Targeting clusters \[A, B\]'"
)
mirrormaker.wait_until_succeeds(
"journalctl -o cat -u kafka-connect-mirror-maker.service | grep 'INFO \[Worker clientId=A->B, groupId=A-mm2\] Finished starting connectors and tasks'"
)
mirrormaker.wait_until_succeeds(
"""
journalctl -o cat -u kafka-connect-mirror-maker.service | grep 'INFO \[MirrorSourceConnector\|task-0\] \[Producer clientId=A->B\|A->B-0\|offset-syncs-source-producer\] Cluster ID: ${clusterAId}'
"""
)
kafkaa1.wait_until_succeeds(
"journalctl -o cat -u apache-kafka.service | grep 'Stabilized group B-mm2'"
)
kafkab1.wait_until_succeeds(
"journalctl -o cat -u apache-kafka.service | grep 'Stabilized group A-mm2'"
)
kafkaa1.wait_until_succeeds(
"kafka-topics.sh --bootstrap-server localhost:9092 --create --topic test-mm-1 --partitions 1 --replication-factor 1"
)
for machine in kafkaa1, kafkab1:
machine.succeed(
"kafka-topics.sh --bootstrap-server localhost:9092 --list | grep 'test-mm-1'"
)
kafkab1.wait_until_succeeds(
"journalctl -o cat -u apache-kafka.service | grep 'Stabilized group A-mm2'"
)
mirrormaker.wait_until_succeeds(
"journalctl -o cat -u kafka-connect-mirror-maker.service | grep 'replicating [[:digit:]]\+ topic-partitions A->B: \[test-mm-1-0\]'"
)
kafkaa1.wait_until_succeeds(
"kafka-topics.sh --bootstrap-server localhost:9092 --create --topic test-mm-1 --partitions 1 --replication-factor 1"
)
mirrormaker.wait_until_succeeds(
"journalctl -o cat -u kafka-connect-mirror-maker.service | grep 'Found [[:digit:]]\+ new topic-partitions on A'"
)
for machine in kafkaa1, kafkab1:
machine.succeed(
"kafka-topics.sh --bootstrap-server localhost:9092 --list | grep 'test-mm-1'"
)
kafkaa1.wait_until_succeeds(
"kafka-verifiable-producer.sh --bootstrap-server kafkaa1:9092 --throughput 10 --max-messages 100 --topic test-mm-1"
)
mirrormaker.wait_until_succeeds(
"journalctl -o cat -u kafka-connect-mirror-maker.service | grep 'replicating [[:digit:]]\+ topic-partitions A->B: \[test-mm-1-0\]'"
)
mirrormaker.wait_until_succeeds(
"journalctl -o cat -u kafka-connect-mirror-maker.service | grep 'Committing offsets for [[:digit:]]\+ acknowledged messages'"
)
mirrormaker.wait_until_succeeds(
"journalctl -o cat -u kafka-connect-mirror-maker.service | grep 'Found [[:digit:]]\+ new topic-partitions on A'"
)
kafkaa1.wait_until_succeeds(
"kafka-verifiable-producer.sh --bootstrap-server kafkaa1:9092 --throughput 10 --max-messages 100 --topic test-mm-1"
)
mirrormaker.wait_until_succeeds(
"journalctl -o cat -u kafka-connect-mirror-maker.service | grep 'Committing offsets for [[:digit:]]\+ acknowledged messages'"
)
kafkab1.wait_until_succeeds(
"kafka-verifiable-consumer.sh --bootstrap-server kafkab1:9092 --topic test-mm-1 --group-id testreplication --max-messages 100"
)
'';
}
)
kafkab1.wait_until_succeeds(
"kafka-verifiable-consumer.sh --bootstrap-server kafkab1:9092 --topic test-mm-1 --group-id testreplication --max-messages 100"
)
'';
}

View File

@@ -1,9 +1,9 @@
{
system ? builtins.currentSystem,
pkgs ? import ../../.. { inherit system; },
pkgs,
runTest,
}:
{
mit = import ./mit.nix { inherit system pkgs; };
heimdal = import ./heimdal.nix { inherit system pkgs; };
ldap = import ./ldap { inherit system pkgs; };
mit = runTest ./mit.nix;
heimdal = runTest ./heimdal.nix;
ldap = import ./ldap { inherit pkgs runTest; };
}

View File

@@ -1,259 +1,257 @@
import ../make-test-python.nix (
{ pkgs, ... }:
{
name = "kerberos_server-heimdal";
{ pkgs, ... }:
{
name = "kerberos_server-heimdal";
nodes = {
server =
{ config, pkgs, ... }:
{
imports = [ ../common/user-account.nix ];
nodes = {
server =
{ config, pkgs, ... }:
{
imports = [ ../common/user-account.nix ];
users.users.alice.extraGroups = [ "wheel" ];
users.users.alice.extraGroups = [ "wheel" ];
services.getty.autologinUser = "alice";
services.getty.autologinUser = "alice";
virtualisation.vlans = [ 1 ];
virtualisation.vlans = [ 1 ];
time.timeZone = "Etc/UTC";
time.timeZone = "Etc/UTC";
networking = {
domain = "foo.bar";
useDHCP = false;
firewall.enable = false;
hosts."10.0.0.1" = [ "server.foo.bar" ];
hosts."10.0.0.2" = [ "client.foo.bar" ];
};
networking = {
domain = "foo.bar";
useDHCP = false;
firewall.enable = false;
hosts."10.0.0.1" = [ "server.foo.bar" ];
hosts."10.0.0.2" = [ "client.foo.bar" ];
};
systemd.network.networks."01-eth1" = {
name = "eth1";
networkConfig.Address = "10.0.0.1/24";
};
systemd.network.networks."01-eth1" = {
name = "eth1";
networkConfig.Address = "10.0.0.1/24";
};
security.krb5 = {
enable = true;
package = pkgs.heimdal;
settings = {
libdefaults.default_realm = "FOO.BAR";
security.krb5 = {
enable = true;
package = pkgs.heimdal;
settings = {
libdefaults.default_realm = "FOO.BAR";
# Enable extra debug output
logging = {
admin_server = "SYSLOG:DEBUG:AUTH";
default = "SYSLOG:DEBUG:AUTH";
kdc = "SYSLOG:DEBUG:AUTH";
};
realms = {
"FOO.BAR" = {
admin_server = "server.foo.bar";
kpasswd_server = "server.foo.bar";
kdc = [ "server.foo.bar" ];
};
};
# Enable extra debug output
logging = {
admin_server = "SYSLOG:DEBUG:AUTH";
default = "SYSLOG:DEBUG:AUTH";
kdc = "SYSLOG:DEBUG:AUTH";
};
};
services.kerberos_server = {
enable = true;
settings.realms = {
realms = {
"FOO.BAR" = {
acl = [
{
principal = "kadmin/admin@FOO.BAR";
access = "all";
}
{
principal = "alice/admin@FOO.BAR";
access = [
"add"
"cpw"
"delete"
"get"
"list"
"modify"
];
}
];
admin_server = "server.foo.bar";
kpasswd_server = "server.foo.bar";
kdc = [ "server.foo.bar" ];
};
};
};
};
client =
{ config, pkgs, ... }:
{
imports = [ ../common/user-account.nix ];
users.users.alice.extraGroups = [ "wheel" ];
services.getty.autologinUser = "alice";
virtualisation.vlans = [ 1 ];
time.timeZone = "Etc/UTC";
networking = {
domain = "foo.bar";
useDHCP = false;
hosts."10.0.0.1" = [ "server.foo.bar" ];
hosts."10.0.0.2" = [ "client.foo.bar" ];
services.kerberos_server = {
enable = true;
settings.realms = {
"FOO.BAR" = {
acl = [
{
principal = "kadmin/admin@FOO.BAR";
access = "all";
}
{
principal = "alice/admin@FOO.BAR";
access = [
"add"
"cpw"
"delete"
"get"
"list"
"modify"
];
}
];
};
};
};
};
systemd.network.networks."01-eth1" = {
name = "eth1";
networkConfig.Address = "10.0.0.2/24";
};
client =
{ config, pkgs, ... }:
{
imports = [ ../common/user-account.nix ];
security.krb5 = {
enable = true;
package = pkgs.heimdal;
settings = {
libdefaults.default_realm = "FOO.BAR";
users.users.alice.extraGroups = [ "wheel" ];
logging = {
admin_server = "SYSLOG:DEBUG:AUTH";
default = "SYSLOG:DEBUG:AUTH";
kdc = "SYSLOG:DEBUG:AUTH";
};
services.getty.autologinUser = "alice";
realms = {
"FOO.BAR" = {
admin_server = "server.foo.bar";
kpasswd_server = "server.foo.bar";
kdc = [ "server.foo.bar" ];
};
virtualisation.vlans = [ 1 ];
time.timeZone = "Etc/UTC";
networking = {
domain = "foo.bar";
useDHCP = false;
hosts."10.0.0.1" = [ "server.foo.bar" ];
hosts."10.0.0.2" = [ "client.foo.bar" ];
};
systemd.network.networks."01-eth1" = {
name = "eth1";
networkConfig.Address = "10.0.0.2/24";
};
security.krb5 = {
enable = true;
package = pkgs.heimdal;
settings = {
libdefaults.default_realm = "FOO.BAR";
logging = {
admin_server = "SYSLOG:DEBUG:AUTH";
default = "SYSLOG:DEBUG:AUTH";
kdc = "SYSLOG:DEBUG:AUTH";
};
realms = {
"FOO.BAR" = {
admin_server = "server.foo.bar";
kpasswd_server = "server.foo.bar";
kdc = [ "server.foo.bar" ];
};
};
};
};
};
};
};
testScript =
{ nodes, ... }:
''
import string
import random
random.seed(0)
testScript =
{ nodes, ... }:
''
import string
import random
random.seed(0)
start_all()
start_all()
with subtest("Server: initialize realm"):
# for unit in ["kadmind.service", "kdc.socket", "kpasswdd.socket"]:
for unit in ["kadmind.service", "kdc.service", "kpasswdd.service"]:
server.wait_for_unit(unit)
with subtest("Server: initialize realm"):
# for unit in ["kadmind.service", "kdc.socket", "kpasswdd.socket"]:
for unit in ["kadmind.service", "kdc.service", "kpasswdd.service"]:
server.wait_for_unit(unit)
server.succeed("kadmin -l init --realm-max-ticket-life='8 day' --realm-max-renewable-life='10 day' FOO.BAR")
server.succeed("kadmin -l init --realm-max-ticket-life='8 day' --realm-max-renewable-life='10 day' FOO.BAR")
for unit in ["kadmind.service", "kdc.service", "kpasswdd.service"]:
server.systemctl(f"restart {unit}")
for unit in ["kadmind.service", "kdc.service", "kpasswdd.service"]:
server.systemctl(f"restart {unit}")
alice_krb_pw = "alice_hunter2"
alice_old_krb_pw = ""
alice_krb_admin_pw = "alice_admin_hunter2"
alice_krb_pw = "alice_hunter2"
alice_old_krb_pw = ""
alice_krb_admin_pw = "alice_admin_hunter2"
def random_password():
password_chars = string.ascii_letters + string.digits + string.punctuation.replace('"', "")
return "".join(random.choice(password_chars) for _ in range(16))
def random_password():
password_chars = string.ascii_letters + string.digits + string.punctuation.replace('"', "")
return "".join(random.choice(password_chars) for _ in range(16))
with subtest("Server: initialize user principals and keytabs"):
server.succeed(f'kadmin -l add --password="{alice_krb_admin_pw}" --use-defaults alice/admin')
server.succeed("kadmin -l ext_keytab --keytab=admin.keytab alice/admin")
with subtest("Server: initialize user principals and keytabs"):
server.succeed(f'kadmin -l add --password="{alice_krb_admin_pw}" --use-defaults alice/admin')
server.succeed("kadmin -l ext_keytab --keytab=admin.keytab alice/admin")
server.succeed(f'kadmin -p alice/admin -K admin.keytab add --password="{alice_krb_pw}" --use-defaults alice')
server.succeed("kadmin -l ext_keytab --keytab=alice.keytab alice")
server.succeed(f'kadmin -p alice/admin -K admin.keytab add --password="{alice_krb_pw}" --use-defaults alice')
server.succeed("kadmin -l ext_keytab --keytab=alice.keytab alice")
server.wait_for_unit("getty@tty1.service")
server.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
server.wait_for_unit("default.target")
server.wait_for_unit("getty@tty1.service")
server.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
server.wait_for_unit("default.target")
with subtest("Server: initialize host principal with keytab"):
server.send_chars("sudo ktutil get -p alice/admin host/server.foo.bar\n")
server.wait_until_tty_matches("1", "password for alice:")
server.send_chars("${nodes.server.users.users.alice.password}\n")
server.wait_until_tty_matches("1", "alice/admin@FOO.BAR's Password:")
server.send_chars(f'{alice_krb_admin_pw}\n')
server.wait_for_file("/etc/krb5.keytab")
with subtest("Server: initialize host principal with keytab"):
server.send_chars("sudo ktutil get -p alice/admin host/server.foo.bar\n")
server.wait_until_tty_matches("1", "password for alice:")
server.send_chars("${nodes.server.users.users.alice.password}\n")
server.wait_until_tty_matches("1", "alice/admin@FOO.BAR's Password:")
server.send_chars(f'{alice_krb_admin_pw}\n')
server.wait_for_file("/etc/krb5.keytab")
ktutil_list = server.succeed("sudo ktutil list")
if not "host/server.foo.bar" in ktutil_list:
exit(1)
ktutil_list = server.succeed("sudo ktutil list")
if not "host/server.foo.bar" in ktutil_list:
exit(1)
server.send_chars("clear\n")
server.send_chars("clear\n")
client.systemctl("start network-online.target")
client.wait_for_unit("network-online.target")
client.wait_for_unit("getty@tty1.service")
client.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
client.wait_for_unit("default.target")
client.systemctl("start network-online.target")
client.wait_for_unit("network-online.target")
client.wait_for_unit("getty@tty1.service")
client.wait_until_succeeds("pgrep -f 'agetty.*tty1'")
client.wait_for_unit("default.target")
with subtest("Client: initialize host principal with keytab"):
client.succeed(
f'echo "{alice_krb_admin_pw}" > pw.txt',
"kinit -p --password-file=pw.txt alice/admin",
)
with subtest("Client: initialize host principal with keytab"):
client.succeed(
f'echo "{alice_krb_admin_pw}" > pw.txt',
"kinit -p --password-file=pw.txt alice/admin",
)
client.send_chars("sudo ktutil get -p alice/admin host/client.foo.bar\n")
client.wait_until_tty_matches("1", "password for alice:")
client.send_chars("${nodes.client.users.users.alice.password}\n")
client.wait_until_tty_matches("1", "alice/admin@FOO.BAR's Password:")
client.send_chars(f"{alice_krb_admin_pw}\n")
client.wait_for_file("/etc/krb5.keytab")
client.send_chars("sudo ktutil get -p alice/admin host/client.foo.bar\n")
client.wait_until_tty_matches("1", "password for alice:")
client.send_chars("${nodes.client.users.users.alice.password}\n")
client.wait_until_tty_matches("1", "alice/admin@FOO.BAR's Password:")
client.send_chars(f"{alice_krb_admin_pw}\n")
client.wait_for_file("/etc/krb5.keytab")
ktutil_list = client.succeed("sudo ktutil list")
if not "host/client.foo.bar" in ktutil_list:
exit(1)
ktutil_list = client.succeed("sudo ktutil list")
if not "host/client.foo.bar" in ktutil_list:
exit(1)
client.send_chars("clear\n")
client.send_chars("clear\n")
with subtest("Client: kinit alice"):
client.succeed(
f"echo '{alice_krb_pw}' > pw.txt",
"kinit -p --password-file=pw.txt alice",
)
tickets = client.succeed("klist")
assert "Principal: alice@FOO.BAR" in tickets
client.send_chars("clear\n")
with subtest("Client: kinit alice"):
client.succeed(
f"echo '{alice_krb_pw}' > pw.txt",
"kinit -p --password-file=pw.txt alice",
)
tickets = client.succeed("klist")
assert "Principal: alice@FOO.BAR" in tickets
client.send_chars("clear\n")
with subtest("Client: kpasswd alice"):
alice_old_krb_pw = alice_krb_pw
alice_krb_pw = random_password()
client.send_chars("kpasswd\n")
client.wait_until_tty_matches("1", "alice@FOO.BAR's Password:")
client.send_chars(f"{alice_old_krb_pw}\n", 0.1)
client.wait_until_tty_matches("1", "New password:")
client.send_chars(f"{alice_krb_pw}\n", 0.1)
client.wait_until_tty_matches("1", "Verify password - New password:")
client.send_chars(f"{alice_krb_pw}\n", 0.1)
with subtest("Client: kpasswd alice"):
alice_old_krb_pw = alice_krb_pw
alice_krb_pw = random_password()
client.send_chars("kpasswd\n")
client.wait_until_tty_matches("1", "alice@FOO.BAR's Password:")
client.send_chars(f"{alice_old_krb_pw}\n", 0.1)
client.wait_until_tty_matches("1", "New password:")
client.send_chars(f"{alice_krb_pw}\n", 0.1)
client.wait_until_tty_matches("1", "Verify password - New password:")
client.send_chars(f"{alice_krb_pw}\n", 0.1)
client.wait_until_tty_matches("1", "Success : Password changed")
client.wait_until_tty_matches("1", "Success : Password changed")
client.send_chars("clear\n")
client.send_chars("clear\n")
with subtest("Server: kinit alice"):
server.succeed(
"echo 'alice_pw_2' > pw.txt"
"kinit -p --password-file=pw.txt alice",
)
tickets = client.succeed("klist")
assert "Principal: alice@FOO.BAR" in tickets
server.send_chars("clear\n")
with subtest("Server: kinit alice"):
server.succeed(
"echo 'alice_pw_2' > pw.txt"
"kinit -p --password-file=pw.txt alice",
)
tickets = client.succeed("klist")
assert "Principal: alice@FOO.BAR" in tickets
server.send_chars("clear\n")
with subtest("Server: kpasswd alice"):
alice_old_krb_pw = alice_krb_pw
alice_krb_pw = random_password()
server.send_chars("kpasswd\n")
server.wait_until_tty_matches("1", "alice@FOO.BAR's Password:")
server.send_chars(f"{alice_old_krb_pw}\n", 0.1)
server.wait_until_tty_matches("1", "New password:")
server.send_chars(f"{alice_krb_pw}\n", 0.1)
server.wait_until_tty_matches("1", "Verify password - New password:")
server.send_chars(f"{alice_krb_pw}\n", 0.1)
with subtest("Server: kpasswd alice"):
alice_old_krb_pw = alice_krb_pw
alice_krb_pw = random_password()
server.send_chars("kpasswd\n")
server.wait_until_tty_matches("1", "alice@FOO.BAR's Password:")
server.send_chars(f"{alice_old_krb_pw}\n", 0.1)
server.wait_until_tty_matches("1", "New password:")
server.send_chars(f"{alice_krb_pw}\n", 0.1)
server.wait_until_tty_matches("1", "Verify password - New password:")
server.send_chars(f"{alice_krb_pw}\n", 0.1)
server.wait_until_tty_matches("1", "Success : Password changed")
server.wait_until_tty_matches("1", "Success : Password changed")
server.send_chars("clear\n")
'';
server.send_chars("clear\n")
'';
meta.maintainers = pkgs.heimdal.meta.maintainers;
}
)
meta.maintainers = pkgs.heimdal.meta.maintainers;
}

View File

@@ -1,7 +1,7 @@
{
system ? builtins.currentSystem,
pkgs ? import ../../../.. { inherit system; },
pkgs,
runTest,
}:
{
mit = import ./mit.nix { inherit system pkgs; };
mit = runTest ./mit.nix;
}

View File

@@ -1,192 +1,190 @@
import ../../make-test-python.nix (
{ pkgs, ... }:
let
DITRoot = "dc=example,dc=com";
realm = "EXAMPLE.COM";
{ pkgs, ... }:
let
DITRoot = "dc=example,dc=com";
realm = "EXAMPLE.COM";
krb5Package = pkgs.krb5.override { withLdap = true; };
krb5Package = pkgs.krb5.override { withLdap = true; };
# Password used by Kerberos services to bind to their identities
krbSrvPwd = "kerberos_service_password";
# Stash file read by Kerberos daemons containing the service password
# DO NOT DO THIS IN PRODUCTION! The stash file is a fundamental secret!
krbPwdStash = pkgs.runCommand "krb-pwd-stash" { } ''
for srv in cn=kadmin,${DITRoot} cn=kdc,${DITRoot}
do
echo -e "${krbSrvPwd}\n${krbSrvPwd}" | \
${krb5Package}/bin/kdb5_ldap_util -r ${realm} stashsrvpw -f $out $srv 2>&1 > /dev/null
done
'';
# Password used by Kerberos services to bind to their identities
krbSrvPwd = "kerberos_service_password";
# Stash file read by Kerberos daemons containing the service password
# DO NOT DO THIS IN PRODUCTION! The stash file is a fundamental secret!
krbPwdStash = pkgs.runCommand "krb-pwd-stash" { } ''
for srv in cn=kadmin,${DITRoot} cn=kdc,${DITRoot}
do
echo -e "${krbSrvPwd}\n${krbSrvPwd}" | \
${krb5Package}/bin/kdb5_ldap_util -r ${realm} stashsrvpw -f $out $srv 2>&1 > /dev/null
done
'';
# The LDAP schema for Kerberos 5 objects is part of the source distribution of Kerberos 5
krbLdapSchema = pkgs.runCommand "krb-ldap-schema" { } ''
tar -Oxf ${krb5Package.src} \
${krb5Package.sourceRoot}/plugins/kdb/ldap/libkdb_ldap/kerberos.openldap.ldif > $out
'';
# The LDAP schema for Kerberos 5 objects is part of the source distribution of Kerberos 5
krbLdapSchema = pkgs.runCommand "krb-ldap-schema" { } ''
tar -Oxf ${krb5Package.src} \
${krb5Package.sourceRoot}/plugins/kdb/ldap/libkdb_ldap/kerberos.openldap.ldif > $out
'';
# Initial LDAP tree containing only the Kerberos services
ldapDIT = ''
dn: ${DITRoot}
objectClass: organization
objectClass: dcObject
dc: example
o: Example Company
# Initial LDAP tree containing only the Kerberos services
ldapDIT = ''
dn: ${DITRoot}
objectClass: organization
objectClass: dcObject
dc: example
o: Example Company
dn: cn=kdc,${DITRoot}
objectClass: krbKdcService
objectClass: simpleSecurityObject
cn: kdc
userPassword: ${krbSrvPwd}
dn: cn=kdc,${DITRoot}
objectClass: krbKdcService
objectClass: simpleSecurityObject
cn: kdc
userPassword: ${krbSrvPwd}
dn: cn=kadmin,${DITRoot}
objectClass: krbAdmService
objectClass: simpleSecurityObject
cn: kadmin
userPassword: ${krbSrvPwd}
'';
dn: cn=kadmin,${DITRoot}
objectClass: krbAdmService
objectClass: simpleSecurityObject
cn: kadmin
userPassword: ${krbSrvPwd}
'';
rootDnPwd = "ldap_root_password";
in
{
name = "kerberos_server-mit-ldap";
rootDnPwd = "ldap_root_password";
in
{
name = "kerberos_server-mit-ldap";
nodes.machine =
{ pkgs, ... }:
{
nodes.machine =
{ pkgs, ... }:
{
services.openldap = {
enable = true;
urlList = [
"ldapi:///"
"ldap://"
];
declarativeContents."${DITRoot}" = ldapDIT;
settings = {
children = {
"cn=schema".includes = [
"${pkgs.openldap}/etc/schema/core.ldif"
"${pkgs.openldap}/etc/schema/cosine.ldif"
"${pkgs.openldap}/etc/schema/inetorgperson.ldif"
"${pkgs.openldap}/etc/schema/nis.ldif"
"${krbLdapSchema}"
];
"olcDatabase={0}config" = {
attrs = {
objectClass = [ "olcDatabaseConfig" ];
olcDatabase = "{0}config";
};
};
"olcDatabase={1}mdb" = {
attrs = {
objectClass = [
"olcDatabaseConfig"
"olcMdbConfig"
];
olcDatabase = "{1}mdb";
olcDbDirectory = "/var/lib/openldap/db";
olcSuffix = DITRoot;
olcRootDN = "cn=root,${DITRoot}";
olcRootPW = rootDnPwd;
# A tiny but realistic ACL
olcAccess = [
''
to attrs=userPassword
by anonymous auth
by * none''
''
to dn.subtree="cn=${realm},cn=realms,${DITRoot}"
by dn.exact="cn=kdc,${DITRoot}" write
by dn.exact="cn=kadmin,${DITRoot}" write
by * none''
''
to *
by * read''
];
};
services.openldap = {
enable = true;
urlList = [
"ldapi:///"
"ldap://"
];
declarativeContents."${DITRoot}" = ldapDIT;
settings = {
children = {
"cn=schema".includes = [
"${pkgs.openldap}/etc/schema/core.ldif"
"${pkgs.openldap}/etc/schema/cosine.ldif"
"${pkgs.openldap}/etc/schema/inetorgperson.ldif"
"${pkgs.openldap}/etc/schema/nis.ldif"
"${krbLdapSchema}"
];
"olcDatabase={0}config" = {
attrs = {
objectClass = [ "olcDatabaseConfig" ];
olcDatabase = "{0}config";
};
};
};
};
services.kerberos_server = {
enable = true;
settings = {
libdefaults.default_realm = realm;
realms = {
"${realm}" = {
acl = [
{
principal = "admin";
access = "all";
}
"olcDatabase={1}mdb" = {
attrs = {
objectClass = [
"olcDatabaseConfig"
"olcMdbConfig"
];
olcDatabase = "{1}mdb";
olcDbDirectory = "/var/lib/openldap/db";
olcSuffix = DITRoot;
olcRootDN = "cn=root,${DITRoot}";
olcRootPW = rootDnPwd;
# A tiny but realistic ACL
olcAccess = [
''
to attrs=userPassword
by anonymous auth
by * none''
''
to dn.subtree="cn=${realm},cn=realms,${DITRoot}"
by dn.exact="cn=kdc,${DITRoot}" write
by dn.exact="cn=kadmin,${DITRoot}" write
by * none''
''
to *
by * read''
];
};
};
dbmodules = {
"${realm}" = {
db_library = "kldap";
ldap_kerberos_container_dn = "cn=realms,${DITRoot}";
ldap_kdc_dn = "cn=kdc,${DITRoot}";
ldap_kadmind_dn = "cn=kadmin,${DITRoot}";
ldap_service_password_file = toString krbPwdStash;
ldap_servers = "ldapi:///";
};
};
};
};
security.krb5 = {
enable = true;
package = krb5Package;
settings = {
libdefaults = {
default_realm = realm;
};
realms = {
"${realm}" = {
admin_server = "machine";
kdc = "machine";
};
};
};
};
users.extraUsers.alice = {
isNormalUser = true;
};
};
testScript = ''
machine.wait_for_unit("openldap.service")
services.kerberos_server = {
enable = true;
settings = {
libdefaults.default_realm = realm;
realms = {
"${realm}" = {
acl = [
{
principal = "admin";
access = "all";
}
];
};
};
dbmodules = {
"${realm}" = {
db_library = "kldap";
ldap_kerberos_container_dn = "cn=realms,${DITRoot}";
ldap_kdc_dn = "cn=kdc,${DITRoot}";
ldap_kadmind_dn = "cn=kadmin,${DITRoot}";
ldap_service_password_file = toString krbPwdStash;
ldap_servers = "ldapi:///";
};
};
};
};
with subtest("realm container initialization"):
machine.succeed(
# Passing a master key directly avoids the need for a separate master key stash file
"kdb5_ldap_util -D cn=root,${DITRoot} create -w ${rootDnPwd} -s -P master_key",
)
security.krb5 = {
enable = true;
package = krb5Package;
settings = {
libdefaults = {
default_realm = realm;
};
realms = {
"${realm}" = {
admin_server = "machine";
kdc = "machine";
};
};
};
};
# These units are bound to fail, as they are started before the directory service is ready
machine.execute("systemctl restart kadmind.service kdc.service")
users.extraUsers.alice = {
isNormalUser = true;
};
};
with subtest("service bind"):
for unit in ["kadmind", "kdc"]:
machine.wait_for_unit(f"{unit}.service")
testScript = ''
machine.wait_for_unit("openldap.service")
with subtest("administration principal initialization"):
machine.succeed("kadmin.local add_principal -pw admin_pw admin")
with subtest("realm container initialization"):
machine.succeed(
# Passing a master key directly avoids the need for a separate master key stash file
"kdb5_ldap_util -D cn=root,${DITRoot} create -w ${rootDnPwd} -s -P master_key",
)
with subtest("user principal creation and kinit"):
machine.succeed(
"kadmin -p admin -w admin_pw addprinc -pw alice_pw alice",
"echo alice_pw | sudo -u alice kinit",
)
# Make extra sure that the user principal actually exists in the directory
machine.succeed(
"ldapsearch -x -D cn=root,${DITRoot} -w ${rootDnPwd} \
-b ${DITRoot} 'krbPrincipalName=alice@${realm}' | grep 'numEntries: 1'"
)
'';
# These units are bound to fail, as they are started before the directory service is ready
machine.execute("systemctl restart kadmind.service kdc.service")
meta.maintainers = [ pkgs.lib.maintainers.nessdoor ];
}
)
with subtest("service bind"):
for unit in ["kadmind", "kdc"]:
machine.wait_for_unit(f"{unit}.service")
with subtest("administration principal initialization"):
machine.succeed("kadmin.local add_principal -pw admin_pw admin")
with subtest("user principal creation and kinit"):
machine.succeed(
"kadmin -p admin -w admin_pw addprinc -pw alice_pw alice",
"echo alice_pw | sudo -u alice kinit",
)
# Make extra sure that the user principal actually exists in the directory
machine.succeed(
"ldapsearch -x -D cn=root,${DITRoot} -w ${rootDnPwd} \
-b ${DITRoot} 'krbPrincipalName=alice@${realm}' | grep 'numEntries: 1'"
)
'';
meta.maintainers = [ pkgs.lib.maintainers.nessdoor ];
}

View File

@@ -1,66 +1,64 @@
import ../make-test-python.nix (
{ pkgs, ... }:
{
name = "kerberos_server-mit";
{ pkgs, ... }:
{
name = "kerberos_server-mit";
nodes.machine =
{
config,
libs,
pkgs,
...
}:
{
services.kerberos_server = {
enable = true;
settings.realms = {
"FOO.BAR".acl = [
{
principal = "admin";
access = [
"add"
"cpw"
];
}
];
};
};
security.krb5 = {
enable = true;
package = pkgs.krb5;
settings = {
libdefaults = {
default_realm = "FOO.BAR";
};
realms = {
"FOO.BAR" = {
admin_server = "machine";
kdc = "machine";
};
};
};
};
users.extraUsers.alice = {
isNormalUser = true;
nodes.machine =
{
config,
libs,
pkgs,
...
}:
{
services.kerberos_server = {
enable = true;
settings.realms = {
"FOO.BAR".acl = [
{
principal = "admin";
access = [
"add"
"cpw"
];
}
];
};
};
security.krb5 = {
enable = true;
package = pkgs.krb5;
settings = {
libdefaults = {
default_realm = "FOO.BAR";
};
realms = {
"FOO.BAR" = {
admin_server = "machine";
kdc = "machine";
};
};
};
};
users.extraUsers.alice = {
isNormalUser = true;
};
};
testScript = ''
machine.succeed(
"kdb5_util create -s -r FOO.BAR -P master_key",
"systemctl restart kadmind.service kdc.service",
)
testScript = ''
machine.succeed(
"kdb5_util create -s -r FOO.BAR -P master_key",
"systemctl restart kadmind.service kdc.service",
)
for unit in ["kadmind", "kdc"]:
machine.wait_for_unit(f"{unit}.service")
for unit in ["kadmind", "kdc"]:
machine.wait_for_unit(f"{unit}.service")
machine.succeed(
"kadmin.local add_principal -pw admin_pw admin",
"kadmin -p admin -w admin_pw addprinc -pw alice_pw alice",
"echo alice_pw | sudo -u alice kinit",
)
'';
machine.succeed(
"kadmin.local add_principal -pw admin_pw admin",
"kadmin -p admin -w admin_pw addprinc -pw alice_pw alice",
"echo alice_pw | sudo -u alice kinit",
)
'';
meta.maintainers = [ pkgs.lib.maintainers.dblsaiko ];
}
)
meta.maintainers = [ pkgs.lib.maintainers.dblsaiko ];
}

View File

@@ -44,6 +44,11 @@
};
testScript = ''
def with_unit_path(node, cmd):
"""Run cmd with the PATH systemd hands to komodo-periphery.service."""
unit_path = "$(systemctl show -p Environment --value komodo-periphery | grep -o 'PATH=[^ ]*' | cut -d= -f2)"
node.succeed(f"export PATH={unit_path}; {cmd}")
start_all()
with subtest("Inbound periphery starts and serves /version"):
@@ -59,9 +64,15 @@
periphery.succeed("test -d /var/lib/komodo-periphery/keys")
periphery.succeed("test -d /var/lib/komodo-periphery/ssl")
with subtest("Service PATH provides docker, docker compose and git"):
with_unit_path(periphery, "command -v docker && command -v git && docker compose version")
with subtest("Outbound periphery stays active despite unreachable core"):
peripheryOutbound.wait_for_unit("komodo-periphery.service")
peripheryOutbound.sleep(15)
peripheryOutbound.succeed("systemctl is-active komodo-periphery")
with subtest("Terminal-enabled periphery sees system-wide tools on PATH"):
with_unit_path(peripheryOutbound, "command -v bash")
'';
}

View File

@@ -1,6 +1,5 @@
{
runTest,
...
}:
{
additionalFiles = runTest ./additional-files.nix;

File diff suppressed because it is too large Load Diff

View File

@@ -401,17 +401,6 @@ let
driver = "nl80211";
pkcs11 = {
enable = true;
package = pkgs.libp11.overrideAttrs {
# TODO: Remove this override once a libp11 release includes the fix for
# https://github.com/OpenSC/libp11/issues/672
version = "0.4.21-unstable-2026-08-19";
src = pkgs.fetchFromGitHub {
owner = "OpenSC";
repo = "libp11";
rev = "e72a2014eb078c7b784e2a9be3e7abd3dce8fd5a";
hash = "sha256-V9ZRPUJp2FkK+Zb/qYC13SDE7+oyJ/hlnO/XEN2zDD8=";
};
};
};
};
};

View File

@@ -1,4 +1,4 @@
{ pkgs, runTest, ... }:
{ pkgs, runTest }:
let
hosts = ''
192.168.2.101 acme.test

Some files were not shown because too many files have changed in this diff Show More