Compare commits
No commits in common. "main" and "v0.7.0" have entirely different histories.
142 changed files with 1952 additions and 13133 deletions
15
.github/workflows/auto-merge.yaml
vendored
15
.github/workflows/auto-merge.yaml
vendored
|
|
@ -23,7 +23,7 @@ on:
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
automerge-rebase:
|
automerge:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: reitermarkus/automerge@v2
|
- uses: reitermarkus/automerge@v2
|
||||||
|
|
@ -35,16 +35,3 @@ jobs:
|
||||||
pull-request: ${{ github.event.inputs.pull-request }}
|
pull-request: ${{ github.event.inputs.pull-request }}
|
||||||
review: ${{ github.event.inputs.review }}
|
review: ${{ github.event.inputs.review }}
|
||||||
dry-run: false
|
dry-run: false
|
||||||
|
|
||||||
automerge-merge:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: reitermarkus/automerge@v2
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GH_TOKEN_FOR_UPDATES }}
|
|
||||||
merge-method: merge
|
|
||||||
do-not-merge-labels: never-merge
|
|
||||||
required-labels: automerge-merge
|
|
||||||
pull-request: ${{ github.event.inputs.pull-request }}
|
|
||||||
review: ${{ github.event.inputs.review }}
|
|
||||||
dry-run: false
|
|
||||||
|
|
|
||||||
8
.github/workflows/build.yaml
vendored
8
.github/workflows/build.yaml
vendored
|
|
@ -61,7 +61,7 @@ jobs:
|
||||||
keep-outputs = true
|
keep-outputs = true
|
||||||
keep-failed = true
|
keep-failed = true
|
||||||
- name: Setup Caching
|
- name: Setup Caching
|
||||||
uses: cachix/cachix-action@v17
|
uses: cachix/cachix-action@v16
|
||||||
with:
|
with:
|
||||||
name: selfhostblocks
|
name: selfhostblocks
|
||||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||||
|
|
@ -93,7 +93,7 @@ jobs:
|
||||||
keep-outputs = true
|
keep-outputs = true
|
||||||
keep-failed = true
|
keep-failed = true
|
||||||
- name: Setup Caching
|
- name: Setup Caching
|
||||||
uses: cachix/cachix-action@v17
|
uses: cachix/cachix-action@v16
|
||||||
with:
|
with:
|
||||||
name: selfhostblocks
|
name: selfhostblocks
|
||||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||||
|
|
@ -124,7 +124,7 @@ jobs:
|
||||||
keep-outputs = true
|
keep-outputs = true
|
||||||
keep-failed = true
|
keep-failed = true
|
||||||
- name: Setup Caching
|
- name: Setup Caching
|
||||||
uses: cachix/cachix-action@v17
|
uses: cachix/cachix-action@v16
|
||||||
with:
|
with:
|
||||||
name: selfhostblocks
|
name: selfhostblocks
|
||||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||||
|
|
@ -133,7 +133,7 @@ jobs:
|
||||||
echo "resultPath=$(nix eval .#checks.x86_64-linux.${{ matrix.check }} --raw)" >> $GITHUB_ENV
|
echo "resultPath=$(nix eval .#checks.x86_64-linux.${{ matrix.check }} --raw)" >> $GITHUB_ENV
|
||||||
nix build --print-build-logs --show-trace .#checks.x86_64-linux.${{ matrix.check }}
|
nix build --print-build-logs --show-trace .#checks.x86_64-linux.${{ matrix.check }}
|
||||||
- name: Upload Build Result
|
- name: Upload Build Result
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v5
|
||||||
if: always() && startsWith(matrix.check, 'vm_')
|
if: always() && startsWith(matrix.check, 'vm_')
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.check }}
|
name: ${{ matrix.check }}
|
||||||
|
|
|
||||||
2
.github/workflows/demo.yml
vendored
2
.github/workflows/demo.yml
vendored
|
|
@ -77,7 +77,7 @@ jobs:
|
||||||
keep-outputs = true
|
keep-outputs = true
|
||||||
keep-failed = true
|
keep-failed = true
|
||||||
|
|
||||||
- uses: cachix/cachix-action@v17
|
- uses: cachix/cachix-action@v16
|
||||||
with:
|
with:
|
||||||
name: selfhostblocks
|
name: selfhostblocks
|
||||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||||
|
|
|
||||||
2
.github/workflows/format.yaml
vendored
2
.github/workflows/format.yaml
vendored
|
|
@ -15,7 +15,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Setup Caching
|
- name: Setup Caching
|
||||||
uses: cachix/cachix-action@v17
|
uses: cachix/cachix-action@v16
|
||||||
with:
|
with:
|
||||||
name: selfhostblocks
|
name: selfhostblocks
|
||||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||||
|
|
|
||||||
34
.github/workflows/lock-update.yaml
vendored
34
.github/workflows/lock-update.yaml
vendored
|
|
@ -1,16 +1,9 @@
|
||||||
# See shb.skarabox.com/misc.html
|
|
||||||
name: Update Flake Lock
|
name: Update Flake Lock
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
|
||||||
- cron: '0 */3 * * *' # runs every 3 hours at :00
|
|
||||||
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
schedule:
|
||||||
bisect_future:
|
- cron: '0 0 * * *' # runs daily at 00:00
|
||||||
description: "Bisect in the future instead of the past"
|
|
||||||
required: false
|
|
||||||
default: "false"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lockfile:
|
lockfile:
|
||||||
|
|
@ -22,22 +15,9 @@ jobs:
|
||||||
uses: cachix/install-nix-action@v31
|
uses: cachix/install-nix-action@v31
|
||||||
with:
|
with:
|
||||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Cache nixpkgs mirror
|
|
||||||
uses: actions/cache@v5
|
|
||||||
with:
|
|
||||||
path: .cache/nixpkgs.git
|
|
||||||
key: nixpkgs-mirror-v1
|
|
||||||
- name: Update flake.lock
|
- name: Update flake.lock
|
||||||
env:
|
uses: DeterminateSystems/update-flake-lock@main
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN_FOR_UPDATES }}
|
with:
|
||||||
BISECT_FUTURE: ${{ inputs.bisect_future }}
|
token: ${{ secrets.GH_TOKEN_FOR_UPDATES }}
|
||||||
run: |
|
pr-labels: |
|
||||||
git config user.email "update-flake-lock-pr@selfhostblocks.com"
|
automerge
|
||||||
git config user.name "Update Flake Lock PR"
|
|
||||||
if [ "$BISECT_FUTURE" = false ]; then
|
|
||||||
BISECT_FUTURE=
|
|
||||||
elif [ -n "$BISECT_FUTURE" ]; then
|
|
||||||
BISECT_FUTURE=future
|
|
||||||
fi
|
|
||||||
nix run .#update-flake-lock-pr $BISECT_FUTURE
|
|
||||||
|
|
||||||
|
|
|
||||||
8
.github/workflows/pages.yml
vendored
8
.github/workflows/pages.yml
vendored
|
|
@ -37,7 +37,7 @@ jobs:
|
||||||
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
github_access_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Setup Caching
|
- name: Setup Caching
|
||||||
uses: cachix/cachix-action@v17
|
uses: cachix/cachix-action@v16
|
||||||
with:
|
with:
|
||||||
name: selfhostblocks
|
name: selfhostblocks
|
||||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||||
|
|
@ -59,13 +59,13 @@ jobs:
|
||||||
public
|
public
|
||||||
|
|
||||||
- name: Setup Pages
|
- name: Setup Pages
|
||||||
uses: actions/configure-pages@v6
|
uses: actions/configure-pages@v5
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v5
|
uses: actions/upload-pages-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: ./public
|
path: ./public
|
||||||
|
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
uses: actions/deploy-pages@v5
|
uses: actions/deploy-pages@v4
|
||||||
|
|
|
||||||
223
.github/workflows/update-flake-lock-pr.nix
vendored
223
.github/workflows/update-flake-lock-pr.nix
vendored
|
|
@ -1,223 +0,0 @@
|
||||||
{
|
|
||||||
gh,
|
|
||||||
writeShellApplication,
|
|
||||||
}:
|
|
||||||
writeShellApplication {
|
|
||||||
name = "update-flake-lock-pr";
|
|
||||||
runtimeInputs = [
|
|
||||||
gh
|
|
||||||
];
|
|
||||||
text = ''
|
|
||||||
branch=ci/nixpkgs-update
|
|
||||||
if [ "''${GITHUB_EVENT_NAME:-}" = "schedule" ]; then
|
|
||||||
branch=ci/nixpkgs-update-auto
|
|
||||||
fi
|
|
||||||
|
|
||||||
goto_future=no
|
|
||||||
if [ -n "''${1:-}" ]; then
|
|
||||||
goto_future=yes
|
|
||||||
fi
|
|
||||||
|
|
||||||
main () {
|
|
||||||
PR=$(get_pr)
|
|
||||||
|
|
||||||
if [ -z "$PR" ]; then
|
|
||||||
create_pr
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$(get_pr_check_status "$PR")" in
|
|
||||||
pending)
|
|
||||||
echo "Checks are running on PR $PR, nothing to do yet."
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
|
|
||||||
succeeded)
|
|
||||||
echo "Checks succeeded, nothing to do."
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
|
|
||||||
failing)
|
|
||||||
echo "Checks failed, updating PR to midpoint commit."
|
|
||||||
|
|
||||||
local CURRENT_COMMIT
|
|
||||||
local LAST_WORKING_COMMIT
|
|
||||||
local FAILING_COMMIT
|
|
||||||
local NEW_COMMIT
|
|
||||||
|
|
||||||
CURRENT_COMMIT="$(get_main_branch_commit)"
|
|
||||||
if [ "$goto_future" = "no" ]; then
|
|
||||||
LAST_WORKING_COMMIT="$CURRENT_COMMIT"
|
|
||||||
FAILING_COMMIT="$(get_pr_commit "$PR")"
|
|
||||||
else
|
|
||||||
LAST_WORKING_COMMIT="$(get_pr_commit "$PR")"
|
|
||||||
FAILING_COMMIT="$(get_latest_nixpkgs_commit)"
|
|
||||||
fi
|
|
||||||
NEW_COMMIT="$(midpoint_commit "$LAST_WORKING_COMMIT" "$FAILING_COMMIT")"
|
|
||||||
|
|
||||||
if [ "$NEW_COMMIT" = "$LAST_WORKING_COMMIT" ] || [ "$NEW_COMMIT" = "$FAILING_COMMIT" ]; then
|
|
||||||
NEW_COMMIT="$(get_latest_nixpkgs_commit)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$NEW_COMMIT" = "$FAILING_COMMIT" ]; then
|
|
||||||
echo "Latest nixpkgs commit is still same failing commit, will retry later."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
update_pr "$PR" "$CURRENT_COMMIT" "$NEW_COMMIT"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
get_main_branch_commit() {
|
|
||||||
git fetch origin main
|
|
||||||
|
|
||||||
git show origin/main:flake.lock \
|
|
||||||
| jq -r '.nodes.nixpkgs.locked.rev'
|
|
||||||
}
|
|
||||||
|
|
||||||
midpoint_commit() {
|
|
||||||
local good="$1"
|
|
||||||
local bad="$2"
|
|
||||||
|
|
||||||
mkdir -p .cache
|
|
||||||
if [ ! -d .cache/nixpkgs.git ]; then
|
|
||||||
git clone --mirror https://github.com/NixOS/nixpkgs.git .cache/nixpkgs.git
|
|
||||||
else
|
|
||||||
git --git-dir=.cache/nixpkgs.git fetch origin
|
|
||||||
fi
|
|
||||||
|
|
||||||
local commits=()
|
|
||||||
|
|
||||||
mapfile -t commits < <(
|
|
||||||
git --git-dir=.cache/nixpkgs.git \
|
|
||||||
rev-list \
|
|
||||||
--reverse \
|
|
||||||
--ancestry-path \
|
|
||||||
"''${good}..''${bad}"
|
|
||||||
)
|
|
||||||
|
|
||||||
local count="''${#commits[@]}"
|
|
||||||
|
|
||||||
if [ "$count" -eq 0 ]; then
|
|
||||||
echo "$bad"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
local midpoint_index=$((count / 2))
|
|
||||||
|
|
||||||
echo "''${commits[$midpoint_index]}"
|
|
||||||
}
|
|
||||||
|
|
||||||
get_latest_nixpkgs_commit () {
|
|
||||||
git ls-remote https://github.com/NixOS/nixpkgs nixos-unstable | cut -f1
|
|
||||||
}
|
|
||||||
|
|
||||||
get_pr () {
|
|
||||||
gh pr list \
|
|
||||||
--head "$branch" \
|
|
||||||
--state open \
|
|
||||||
--json number \
|
|
||||||
--jq '.[0].number // empty'
|
|
||||||
}
|
|
||||||
|
|
||||||
create_pr() {
|
|
||||||
local test_commit
|
|
||||||
|
|
||||||
test_commit="$(get_latest_nixpkgs_commit)"
|
|
||||||
echo "Creating PR on nixpkgs' latest commit $test_commit"
|
|
||||||
|
|
||||||
git checkout -B "$branch"
|
|
||||||
|
|
||||||
nix flake lock \
|
|
||||||
--override-input nixpkgs "github:NixOS/nixpkgs/$test_commit"
|
|
||||||
|
|
||||||
git add flake.lock
|
|
||||||
|
|
||||||
git commit -m "update nixpkgs to $test_commit"
|
|
||||||
|
|
||||||
git push -u origin "$branch" --force
|
|
||||||
|
|
||||||
current_commit="$(get_main_branch_commit)"
|
|
||||||
gh pr create \
|
|
||||||
--title "update nixpkgs to $test_commit" \
|
|
||||||
--body "$(printf "%s\n\n%s" "Automated nixpkgs update. Latest tries:" " - https://github.com/NixOS/nixpkgs/compare/$current_commit...$test_commit")" \
|
|
||||||
--label automerge-merge
|
|
||||||
}
|
|
||||||
|
|
||||||
append_pr_body() {
|
|
||||||
local pr="$1"
|
|
||||||
local text="$2"
|
|
||||||
|
|
||||||
local current_body
|
|
||||||
|
|
||||||
current_body="$(gh pr view "$pr" --json body --jq .body)"
|
|
||||||
|
|
||||||
gh pr edit "$pr" \
|
|
||||||
--body "$(printf '%s%b' "$current_body" "$text")"
|
|
||||||
}
|
|
||||||
|
|
||||||
update_pr() {
|
|
||||||
local pr="$1"
|
|
||||||
local current_commit="$2"
|
|
||||||
local test_commit="$3"
|
|
||||||
|
|
||||||
echo "Updating PR to nixpkgs' commit $test_commit"
|
|
||||||
|
|
||||||
git checkout "$branch"
|
|
||||||
|
|
||||||
nix flake lock \
|
|
||||||
--override-input nixpkgs "github:NixOS/nixpkgs/$test_commit"
|
|
||||||
|
|
||||||
git add flake.lock
|
|
||||||
|
|
||||||
git commit -m "update nixpkgs to $test_commit"
|
|
||||||
|
|
||||||
git push --force-with-lease origin "$branch"
|
|
||||||
|
|
||||||
local future_text
|
|
||||||
if [ "$goto_future" = "yes" ]; then
|
|
||||||
future_text="bisected in the future"
|
|
||||||
else
|
|
||||||
future_text="bisected in the past"
|
|
||||||
fi
|
|
||||||
|
|
||||||
gh pr edit "$pr" \
|
|
||||||
--title "update nixpkgs to $test_commit"
|
|
||||||
|
|
||||||
append_pr_body "$pr" \
|
|
||||||
" ❌\n - https://github.com/NixOS/nixpkgs/compare/$current_commit...$test_commit ($future_text)"
|
|
||||||
}
|
|
||||||
|
|
||||||
get_pr_check_status() {
|
|
||||||
local pr="$1"
|
|
||||||
|
|
||||||
local status
|
|
||||||
status="$(gh pr checks "$pr" --json state --jq '.[].state' || true)"
|
|
||||||
|
|
||||||
if echo "$status" | grep -qE 'PENDING|QUEUED|IN_PROGRESS'; then
|
|
||||||
echo "pending"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if echo "$status" | grep -qE 'FAILURE|TIMED_OUT|CANCELLED'; then
|
|
||||||
echo "failing"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "success"
|
|
||||||
}
|
|
||||||
|
|
||||||
get_pr_commit() {
|
|
||||||
local pr="$1"
|
|
||||||
|
|
||||||
git fetch origin "$branch"
|
|
||||||
|
|
||||||
git show "origin/$branch:flake.lock" \
|
|
||||||
| jq -r '.nodes.nixpkgs.locked.rev'
|
|
||||||
}
|
|
||||||
|
|
||||||
main
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
116
CHANGELOG.md
116
CHANGELOG.md
|
|
@ -16,122 +16,6 @@ Template:
|
||||||
|
|
||||||
# Upcoming Release
|
# Upcoming Release
|
||||||
|
|
||||||
# v0.9.0
|
|
||||||
|
|
||||||
Commits: https://github.com/ibizaman/selfhostblocks/compare/v0.8.0...v0.9.0
|
|
||||||
|
|
||||||
## Breaking Changes
|
|
||||||
|
|
||||||
- Updated simple-nixos-mailserver. Update all options following this pattern:
|
|
||||||
|
|
||||||
```diff
|
|
||||||
- mailserver.mailboxes.<name>.specialUse = "value"
|
|
||||||
+ mailserver.mailboxes.<name>.special_use = "\\value"
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that simple-nixos-mailserver wanted to upgrade the location of the storage path
|
|
||||||
to include the ldap UID instead of the email but I kept the email address.
|
|
||||||
This means there is no migration to do.
|
|
||||||
- Update nixpkgs [from 6201e2 to abd1ea](https://github.com/ibizaman/selfhostblocks/compare/6201e203d09599479a3b3450ed24fa81537ebc4e...abd1ea17fdbedd48cbca770847b39e3a7a09ab5b).
|
|
||||||
|
|
||||||
## New Features
|
|
||||||
|
|
||||||
- Add `shb.zfs.snapshotBeforeActivation` option to take a ZFS snapshot of given datasets before activation.
|
|
||||||
See [the manual](https://shb.skarabox.com/blocks-zfs.html) for more details.
|
|
||||||
- Add [sanoid block](https://shb.skarabox.com/blocks-sanoid.html)
|
|
||||||
- Add option to take snapshot before activation with ZFS block.
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
- Fix oidc_login build for Nextcloud
|
|
||||||
- Fix mailserver build
|
|
||||||
|
|
||||||
## Other Changes
|
|
||||||
|
|
||||||
- Harden lldap configuration
|
|
||||||
- Convert ZFS block to system and [add documentation](https://shb.skarabox.com/blocks-zfs.html)
|
|
||||||
- Switch monitoring log fetching from deprecated promtail to fluent-bit
|
|
||||||
- Add test to catch drift for Let's Encrypt DNS provider
|
|
||||||
|
|
||||||
# [BROKEN] v0.8.0
|
|
||||||
|
|
||||||
## Breaking Changes
|
|
||||||
|
|
||||||
- Bump of Nextcloud version to 32 and 33 because of nixpkgs bump. All provided apps are verified compatible with Nextcloud 33 thanks to new tests.
|
|
||||||
|
|
||||||
## New Features
|
|
||||||
|
|
||||||
- Added Immich Public Proxy service
|
|
||||||
- Add homepage service with dashboard contract implemented by all services
|
|
||||||
- Add scrutiny service.
|
|
||||||
- ZFS module now supports setting permissions
|
|
||||||
- Add landing page for mailserver and dashboard contract integration
|
|
||||||
|
|
||||||
## Bug Fixes
|
|
||||||
|
|
||||||
- Use configurable dataDir in arr stack
|
|
||||||
- Forgejo ensures ldap is setup when sso is configured
|
|
||||||
- Add nixpkgs patches on aarch64-linux too
|
|
||||||
- Self-signed certs are now idempotent
|
|
||||||
- Prometheus scrapes metrics at 15s interval instead of 1m
|
|
||||||
|
|
||||||
## Other Changes
|
|
||||||
|
|
||||||
- Arr stack declares ldap groups, declare ApiKeys and bypasses auth for readarr when sso is enabled
|
|
||||||
- Forgejo declares ldap group
|
|
||||||
|
|
||||||
# v0.7.3
|
|
||||||
|
|
||||||
## New Features
|
|
||||||
|
|
||||||
- Add [mailserver module](https://shb.skarabox.com/services-mailserver.html) integrating with [Simple NixOS Mailserver](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver) and allowing full backup of an email provider.
|
|
||||||
- Bump nixpkgs from https://github.com/NixOS/nixpkgs/commit/5e2a59a5b1a82f89f2c7e598302a9cacebb72a67 to https://github.com/NixOS/nixpkgs/commit/bfc1b8a4574108ceef22f02bafcf6611380c100d. [Full diff](https://github.com/nixos/nixpkgs/compare/5e2a59a5b1a82f89f2c7e598302a9cacebb72a67...bfc1b8a4574108ceef22f02bafcf6611380c100d).
|
|
||||||
On top of minor changes, the most notable one was:
|
|
||||||
- Updated Jellyfin LDAP and SSO plugins and configuration. @Codys-Wright
|
|
||||||
|
|
||||||
## Bug Fixes
|
|
||||||
|
|
||||||
- Fix Restic and Authelia modules referencing systemd services without the `.service` suffix and leading to
|
|
||||||
|
|
||||||
# v0.7.2
|
|
||||||
|
|
||||||
## New Features
|
|
||||||
|
|
||||||
- Forgejo uses secrets contract for smtp password.
|
|
||||||
- Add [Firefly-iii](https://shb.skarabox.com/services-firefly-iii.html) service.
|
|
||||||
- Jellyfin can [install plugins declaratively](https://shb.skarabox.com/services-jellyfin.html#services-jellyfin-options-shb.jellyfin.plugins).
|
|
||||||
(Support is quite crude and WIP).
|
|
||||||
- Jellyfin configures LDAP and SSO fully declaratively, including installing necessary plugins.
|
|
||||||
- Nextcloud 32 is fully supported thanks to tests for version 31 and 32.
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
- Revert Authelia to continue using dots in systemd service names.
|
|
||||||
This caused issue with nginx name resolution.
|
|
||||||
|
|
||||||
## Other Changes
|
|
||||||
|
|
||||||
- Authelia uses non deprecated `smtp.address` option.
|
|
||||||
- Add documentation for Nginx block
|
|
||||||
- Now a user which is only member of the admin LDAP group of a service can login.
|
|
||||||
Before, some services required a user to be member of both the user and admin LDAP group.
|
|
||||||
This is ensured by regression tests going forward.
|
|
||||||
|
|
||||||
# v0.7.1
|
|
||||||
|
|
||||||
## New Features
|
|
||||||
|
|
||||||
- Add a Grafana dashboard showing SSL certificate renewal jobs
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
- Fix let's encrypt certificate renewal jobs by removing duplicated domain name.
|
|
||||||
Also adds an assertion to catch these kinds of errors.
|
|
||||||
|
|
||||||
## Other Changes
|
|
||||||
|
|
||||||
- Reduce number of late SSL renewal alert by merging all metrics corresponding to one CN.
|
|
||||||
|
|
||||||
# v0.7.0
|
# v0.7.0
|
||||||
|
|
||||||
## Breaking Changes
|
## Breaking Changes
|
||||||
|
|
|
||||||
18
README.md
18
README.md
|
|
@ -153,7 +153,7 @@ SelfHostBlocks provides building blocks that take care of common self-hosting ne
|
||||||
- Backup for all services.
|
- Backup for all services.
|
||||||
- Automatic creation of ZFS datasets per service.
|
- Automatic creation of ZFS datasets per service.
|
||||||
- LDAP and SSO integration for most services.
|
- LDAP and SSO integration for most services.
|
||||||
- Monitoring with Grafana and Prometheus stack with provided dashboards and integration with Scrutiny.
|
- Monitoring with Grafana and Prometheus stack with provided dashboards.
|
||||||
- Automatic reverse proxy and certificate management for HTTPS.
|
- Automatic reverse proxy and certificate management for HTTPS.
|
||||||
- VPN and proxy tunneling services.
|
- VPN and proxy tunneling services.
|
||||||
|
|
||||||
|
|
@ -174,8 +174,6 @@ Also, the stack fits together nicely thanks to [contracts](#contracts).
|
||||||
- Nextcloud
|
- Nextcloud
|
||||||
- Audiobookshelf
|
- Audiobookshelf
|
||||||
- Deluge + *arr stack
|
- Deluge + *arr stack
|
||||||
- Simple NixOS Mailserver
|
|
||||||
- Firefly-iii
|
|
||||||
- Forgejo
|
- Forgejo
|
||||||
- Grocy
|
- Grocy
|
||||||
- Hledger
|
- Hledger
|
||||||
|
|
@ -208,7 +206,7 @@ which altogether provides a solid foundation for self-hosting services:
|
||||||
- BorgBackup
|
- BorgBackup
|
||||||
- Davfs
|
- Davfs
|
||||||
- LDAP
|
- LDAP
|
||||||
- Monitoring (Grafana - Prometheus - Loki stack + Scrutiny)
|
- Monitoring (Grafana - Prometheus - Loki stack)
|
||||||
- Nginx
|
- Nginx
|
||||||
- PostgreSQL
|
- PostgreSQL
|
||||||
- Restic
|
- Restic
|
||||||
|
|
@ -250,14 +248,14 @@ shb.nextcloud = {
|
||||||
host = "127.0.0.1";
|
host = "127.0.0.1";
|
||||||
port = config.shb.lldap.ldapPort;
|
port = config.shb.lldap.ldapPort;
|
||||||
dcdomain = config.shb.lldap.dcdomain;
|
dcdomain = config.shb.lldap.dcdomain;
|
||||||
adminPassword.result = config.shb.sops.secret."nextcloud/ldap/admin_password".result;
|
adminPassword.result = config.shb.sops.secrets."nextcloud/ldap/admin_password".result;
|
||||||
};
|
};
|
||||||
apps.sso = {
|
apps.sso = {
|
||||||
enable = true;
|
enable = true;
|
||||||
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
||||||
|
|
||||||
secret.result = config.shb.sops.secret."nextcloud/sso/secret".result;
|
secret.result = config.shb.sops.secrets."nextcloud/sso/secret".result;
|
||||||
secretForAuthelia.result = config.shb.sops.secret."nextcloud/sso/secretForAuthelia".result;
|
secretForAuthelia.result = config.shb.sops.secrets."nextcloud/sso/secretForAuthelia".result;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
@ -275,15 +273,15 @@ shb.forgejo = {
|
||||||
host = "127.0.0.1";
|
host = "127.0.0.1";
|
||||||
port = config.shb.lldap.ldapPort;
|
port = config.shb.lldap.ldapPort;
|
||||||
dcdomain = config.shb.lldap.dcdomain;
|
dcdomain = config.shb.lldap.dcdomain;
|
||||||
adminPassword.result = config.shb.sops.secret."nextcloud/ldap/admin_password".result;
|
adminPassword.result = config.shb.sops.secrets."nextcloud/ldap/admin_password".result;
|
||||||
};
|
};
|
||||||
|
|
||||||
sso = {
|
sso = {
|
||||||
enable = true;
|
enable = true;
|
||||||
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
||||||
|
|
||||||
secret.result = config.shb.sops.secret."forgejo/sso/secret".result;
|
secret.result = config.shb.sops.secrets."forgejo/sso/secret".result;
|
||||||
secretForAuthelia.result = config.shb.sops.secret."forgejo/sso/secretForAuthelia".result;
|
secretForAuthelia.result = config.shb.sops.secrets."forgejo/sso/secretForAuthelia".result;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
|
||||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.9.0
|
0.7.0
|
||||||
|
|
@ -24,10 +24,7 @@
|
||||||
# This module makes the assertions happy and the build succeed.
|
# This module makes the assertions happy and the build succeed.
|
||||||
# This is of course wrong and will not work on any real system.
|
# This is of course wrong and will not work on any real system.
|
||||||
filesystemModule = {
|
filesystemModule = {
|
||||||
fileSystems."/" = {
|
fileSystems."/".device = "/dev/null";
|
||||||
device = "/dev/null";
|
|
||||||
fsType = "none";
|
|
||||||
};
|
|
||||||
boot.loader.grub.devices = [ "/dev/null" ];
|
boot.loader.grub.devices = [ "/dev/null" ];
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
|
|
@ -184,40 +181,6 @@
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
# Test with:
|
|
||||||
# nix build .#nixosConfigurations.contractsDirect.config.system.build.toplevel
|
|
||||||
contractsDirect =
|
|
||||||
let
|
|
||||||
nixosSystem' = import "${selfhostblocks.inputs.nixpkgs}/nixos/lib/eval-config.nix";
|
|
||||||
in
|
|
||||||
nixosSystem' {
|
|
||||||
inherit system;
|
|
||||||
modules = [
|
|
||||||
filesystemModule
|
|
||||||
(import "${selfhostblocks}/lib/module.nix")
|
|
||||||
(
|
|
||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
shb,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
options.myOption = lib.mkOption {
|
|
||||||
# Using provided nixosSystem directly.
|
|
||||||
# SHB's lib is available under `shb` thanks to the overlay.
|
|
||||||
type = shb.secretFileType;
|
|
||||||
};
|
|
||||||
config = {
|
|
||||||
myOption.source = "/a/path";
|
|
||||||
# Use the option.
|
|
||||||
environment.etc.myOption.text = config.myOption.source;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -187,9 +187,8 @@ This section corresponds to the `sso` section of the [Nextcloud
|
||||||
manual](services-nextcloud.html#services-nextcloudserver-usage-oidc).
|
manual](services-nextcloud.html#services-nextcloudserver-usage-oidc).
|
||||||
::::
|
::::
|
||||||
|
|
||||||
At this point, it is assumed you already deployed the `sso` demo. This time, we cannot simply edit local
|
At this point, it is assumed you already deployed the `sso` demo. There is no host to add to
|
||||||
`/etc/hosts`, because Nextcloud SSO addon must be able to connect to Authelia by domain name
|
`/etc/hosts` here. Instead, there is a `dnsmasq` server running in the VM and you must create a
|
||||||
(`auth.example.com`). Instead, there is a `dnsmasq` server running in the VM and you must create a
|
|
||||||
SOCKS proxy to connect to it like so:
|
SOCKS proxy to connect to it like so:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
description = "Nextcloud example for Self Host Blocks";
|
description = "Home Assistant example for Self Host Blocks";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
selfhostblocks.url = "github:ibizaman/selfhostblocks";
|
selfhostblocks.url = "github:ibizaman/selfhostblocks";
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,8 @@ Blocks help you self-host apps or services. They implement a specific function l
|
||||||
access through a subdomain. Each block is designed to be usable on its own and to fit nicely with
|
access through a subdomain. Each block is designed to be usable on its own and to fit nicely with
|
||||||
others.
|
others.
|
||||||
|
|
||||||
All blocks are implemented under the blocks folder [in the repository](@REPO@/modules/blocks).
|
Not all blocks are documented yet.
|
||||||
|
You can find all available blocks [in the repository](@REPO@/modules/blocks).
|
||||||
All services in SHB document how to setup the various blocks provided here.
|
|
||||||
For custom services or those not provided by SHB,
|
|
||||||
the [Expose a service Recipe](recipes-exposeService.html) explains how to use the blocks here.
|
|
||||||
|
|
||||||
## Authentication {#blocks-category-authentication}
|
## Authentication {#blocks-category-authentication}
|
||||||
|
|
||||||
|
|
@ -31,10 +28,6 @@ modules/blocks/borgbackup/docs/default.md
|
||||||
modules/blocks/restic/docs/default.md
|
modules/blocks/restic/docs/default.md
|
||||||
```
|
```
|
||||||
|
|
||||||
```{=include=} chapters html:into-file=//blocks-sanoid.html
|
|
||||||
modules/blocks/sanoid/docs/default.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## Database {#blocks-category-database}
|
## Database {#blocks-category-database}
|
||||||
|
|
||||||
```{=include=} chapters html:into-file=//blocks-postgresql.html
|
```{=include=} chapters html:into-file=//blocks-postgresql.html
|
||||||
|
|
@ -47,22 +40,12 @@ modules/blocks/postgresql/docs/default.md
|
||||||
modules/blocks/sops/docs/default.md
|
modules/blocks/sops/docs/default.md
|
||||||
```
|
```
|
||||||
|
|
||||||
## Filesystem {#blocks-category-filesystem}
|
|
||||||
|
|
||||||
```{=include=} chapters html:into-file=//blocks-zfs.html
|
|
||||||
modules/blocks/zfs/docs/default.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## Network {#blocks-category-network}
|
## Network {#blocks-category-network}
|
||||||
|
|
||||||
```{=include=} chapters html:into-file=//blocks-ssl.html
|
```{=include=} chapters html:into-file=//blocks-ssl.html
|
||||||
modules/blocks/ssl/docs/default.md
|
modules/blocks/ssl/docs/default.md
|
||||||
```
|
```
|
||||||
|
|
||||||
```{=include=} chapters html:into-file=//blocks-nginx.html
|
|
||||||
modules/blocks/nginx/docs/default.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## Introspection {#blocks-category-introspection}
|
## Introspection {#blocks-category-introspection}
|
||||||
|
|
||||||
```{=include=} chapters html:into-file=//blocks-monitoring.html
|
```{=include=} chapters html:into-file=//blocks-monitoring.html
|
||||||
|
|
|
||||||
|
|
@ -37,21 +37,14 @@ Provided contracts are:
|
||||||
- [SSL generator contract](contracts-ssl.html) to generate SSL certificates.
|
- [SSL generator contract](contracts-ssl.html) to generate SSL certificates.
|
||||||
Two providers are implemented: self-signed and Let's Encrypt.
|
Two providers are implemented: self-signed and Let's Encrypt.
|
||||||
- [Backup contract][] to backup directories.
|
- [Backup contract][] to backup directories.
|
||||||
Two providers are implemented: [BorgBackup][] and [Restic][].
|
One provider is implemented: [Restic][].
|
||||||
- [Database Backup contract](contracts-databasebackup.html) to backup database dumps.
|
- [Database Backup contract](contracts-databasebackup.html) to backup database dumps.
|
||||||
Two providers are implemented: [BorgBackup][] and [Restic][].
|
One provider is implemented: [Restic][].
|
||||||
- [Dataset Backup contract](contracts-datasetbackup.html) to backup ZFS datasets.
|
- [Secret contract](contracts-secret.html) to provide secrets that are deployed outside of the Nix store.
|
||||||
One provider is implemented: [Sanoid][]
|
|
||||||
- [Contract for Secrets](contracts-secret.html) to provide secrets that are deployed outside of the Nix store.
|
|
||||||
One provider is implemented: [SOPS][].
|
One provider is implemented: [SOPS][].
|
||||||
- [Dashboard contract](contracts-dashboard.html) to show services in a nice user-facing dashboard.
|
|
||||||
One provider is implemented: [Homepage][].
|
|
||||||
|
|
||||||
[backup contract]: contracts-backup.html
|
[backup contract]: contracts-backup.html
|
||||||
[borgbackup]: blocks-borgbackup.html
|
|
||||||
[homepage]: services-homepage.html
|
|
||||||
[restic]: blocks-restic.html
|
[restic]: blocks-restic.html
|
||||||
[sanoid]: blocks-sanoid.html
|
|
||||||
[sops]: blocks-sops.html
|
[sops]: blocks-sops.html
|
||||||
|
|
||||||
```{=include=} chapters html:into-file=//contracts-ssl.html
|
```{=include=} chapters html:into-file=//contracts-ssl.html
|
||||||
|
|
@ -66,18 +59,10 @@ modules/contracts/backup/docs/default.md
|
||||||
modules/contracts/databasebackup/docs/default.md
|
modules/contracts/databasebackup/docs/default.md
|
||||||
```
|
```
|
||||||
|
|
||||||
```{=include=} chapters html:into-file=//contracts-datasetbackup.html
|
|
||||||
modules/contracts/datasetbackup/docs/default.md
|
|
||||||
```
|
|
||||||
|
|
||||||
```{=include=} chapters html:into-file=//contracts-secret.html
|
```{=include=} chapters html:into-file=//contracts-secret.html
|
||||||
modules/contracts/secret/docs/default.md
|
modules/contracts/secret/docs/default.md
|
||||||
```
|
```
|
||||||
|
|
||||||
```{=include=} chapters html:into-file=//contracts-dashboard.html
|
|
||||||
modules/contracts/dashboard/docs/default.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## Problem Statement {#contracts-why}
|
## Problem Statement {#contracts-why}
|
||||||
|
|
||||||
Currently in nixpkgs, every module accessing a shared resource
|
Currently in nixpkgs, every module accessing a shared resource
|
||||||
|
|
|
||||||
|
|
@ -53,66 +53,20 @@ $ nix build .#checks.${system}.modules
|
||||||
$ nix build .#checks.${system}.vm_postgresql_peerAuth
|
$ nix build .#checks.${system}.vm_postgresql_peerAuth
|
||||||
```
|
```
|
||||||
|
|
||||||
### Playwright Tests {#contributing-playwright-tests}
|
Run one VM test interactively:
|
||||||
|
|
||||||
If the test includes playwright tests, you can see the playwright trace with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ nix run .#playwright -- show-trace $(nix eval .#checks.x86_64-linux.vm_grocy_basic --raw)/trace/0.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
If the test fails, you won't have the output directory available.
|
|
||||||
Instead, run the test with `--keep-failed` and at the end you will see a line saying
|
|
||||||
|
|
||||||
```
|
|
||||||
Keeping build directory '/nix/var/nix/builds/nix-31776-4270051001/build'
|
|
||||||
```
|
|
||||||
|
|
||||||
Copy the path and run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo cp -r /nix/var/nix/builds/nix-31776-4270051001/build/shared-xchg/trace trace && sudo chown -R $USER: trace
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, open that file with playwright:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nix run .#playwright -- show-trace trace/0.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debug Tests {#contributing-debug-tests}
|
|
||||||
|
|
||||||
Run the test in driver interactive mode:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ nix run .#checks.${system}.vm_postgresql_peerAuth.driverInteractive
|
$ nix run .#checks.${system}.vm_postgresql_peerAuth.driverInteractive
|
||||||
```
|
```
|
||||||
|
|
||||||
When you get to the shell, start the server and/or client with one of the following commands:
|
When you get to the shell, run either `start_all()` or `test_script()`. The former just starts all
|
||||||
|
the VMs and service, then you can introspect. The latter also starts the VMs if they are not yet and
|
||||||
|
then will run the test script.
|
||||||
|
|
||||||
|
If the test includes playwright tests, you can see the playwright trace with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
server.start()
|
$ nix run .#playwright -- show-trace $(nix eval .#checks.x86_64-linux.vm_grocy_basic --raw)/trace/0.zip
|
||||||
client.start()
|
|
||||||
start_all()
|
|
||||||
```
|
|
||||||
|
|
||||||
To run the test from the shell, use `test_script()`.
|
|
||||||
Note that if the test script ends in error,
|
|
||||||
the shell will exit and you will need to restart the VMs.
|
|
||||||
|
|
||||||
After the shell started, you will see lines like so:
|
|
||||||
|
|
||||||
```
|
|
||||||
SSH backdoor enabled, the machines can be accessed like this:
|
|
||||||
Note: this requires systemd-ssh-proxy(1) to be enabled (default on NixOS 25.05 and newer).
|
|
||||||
client: ssh -o User=root vsock/3
|
|
||||||
server: ssh -o User=root vsock/4
|
|
||||||
```
|
|
||||||
|
|
||||||
With the following command, you can directly access the server's nginx instance with your browser at `http://localhost:8000`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ssh-keygen -R vsock/4; ssh -o User=root -L 8000:localhost:80 vsock/4
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Upload test results to CI {#contributing-upload}
|
## Upload test results to CI {#contributing-upload}
|
||||||
|
|
@ -126,18 +80,6 @@ After running the `nix-fast-build` command from the previous section, run:
|
||||||
$ find . -type l -name "result-vm_*" | xargs readlink | nix run nixpkgs#cachix -- push selfhostblocks
|
$ find . -type l -name "result-vm_*" | xargs readlink | nix run nixpkgs#cachix -- push selfhostblocks
|
||||||
```
|
```
|
||||||
|
|
||||||
## Upload package to CI {#contributing-upload-package}
|
|
||||||
|
|
||||||
In the rare case where a package must be built but cannot in CI,
|
|
||||||
for example because of not enough memory,
|
|
||||||
you can push the package directly to the cache with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nix build .#checks.x86_64-linux.vm_karakeep_backup.nodes.server.services.karakeep.package
|
|
||||||
readlink result | nix run nixpkgs#cachix -- push selfhostblocks
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deploy using colmena {#contributing-deploy-colmena}
|
## Deploy using colmena {#contributing-deploy-colmena}
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ let
|
||||||
|
|
||||||
outputPath = "share/doc/selfhostblocks";
|
outputPath = "share/doc/selfhostblocks";
|
||||||
|
|
||||||
manpage-urls = pkgs.writeText "manpage-urls.json" "{}";
|
manpage-urls = pkgs.writeText "manpage-urls.json" ''{}'';
|
||||||
in
|
in
|
||||||
stdenv.mkDerivation {
|
stdenv.mkDerivation {
|
||||||
name = "self-host-blocks-manual";
|
name = "self-host-blocks-manual";
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,6 @@ blocks.md
|
||||||
recipes.md
|
recipes.md
|
||||||
```
|
```
|
||||||
|
|
||||||
```{=include=} chapters html:into-file=//misc.html
|
|
||||||
misc.md
|
|
||||||
```
|
|
||||||
|
|
||||||
```{=include=} chapters html:into-file=//demos.html
|
```{=include=} chapters html:into-file=//demos.html
|
||||||
demos.md
|
demos.md
|
||||||
```
|
```
|
||||||
|
|
|
||||||
39
docs/misc.md
39
docs/misc.md
|
|
@ -1,39 +0,0 @@
|
||||||
<!-- Read these docs at https://shb.skarabox.com -->
|
|
||||||
# Miscellaneous {#misc}
|
|
||||||
|
|
||||||
Here goes meta-discussions around the repository and other topics not related directly to the usage of SHB.
|
|
||||||
|
|
||||||
## Lock file update {#misc-lock-file-update}
|
|
||||||
|
|
||||||
*SHB has an unusual strategy to keep up with its flake inputs. This section explains why.*
|
|
||||||
|
|
||||||
SHB only depends on `nixpkgs` as far as flake inputs goes.
|
|
||||||
There are others but they only matter for tasks around taking care of the repository itself.
|
|
||||||
So only the `nixpkgs` input is important for downstream consumers of SHB.
|
|
||||||
|
|
||||||
To keep up to date with nixpkgs unstable,
|
|
||||||
initially a job was updating the nixpkgs input by simply updating the lock file and creating a PR.
|
|
||||||
Now, this job failed most of the time because the tip of unstable often breaks modules and packages, resulting in failing SHB tests.
|
|
||||||
To fix this, we usually needed to choose a commit around 2 weeks prior to the latest unstable commit.
|
|
||||||
This usually did not lead to successful tests but the reason was then not
|
|
||||||
because of failing packages but because nixpkgs modules evolved and SHB needs to adapt to that.
|
|
||||||
The net effect was SHB was lagging too far behind unstable and updating it was becoming annoying.
|
|
||||||
|
|
||||||
The new strategy now is as follows. On every job run:
|
|
||||||
|
|
||||||
- If there is no PR, update nixpkgs input to latest unstable commit and create a PR which auto-merges when tests are successful.
|
|
||||||
- If there is a PR:
|
|
||||||
- If the checks succeeded, do nothing because the PR will auto-merge.
|
|
||||||
- If the checks are pending, do nothing yet.
|
|
||||||
- If the checks failed, bisect and update nixpkgs input to between the commit on main and the commit in the PR.
|
|
||||||
|
|
||||||
The effect here is as long as the tests are failing, we'll try a commit further in the past from nixpkgs unstable
|
|
||||||
and closer to the tip of main branch.
|
|
||||||
|
|
||||||
This could be made smarter by distinguishing between types of failures.
|
|
||||||
We will see if this is needed.
|
|
||||||
|
|
||||||
One can also run the bisect manually with `nix run .#update-flake-lock-pr`.
|
|
||||||
This will create a PR on a different branch than the one used for the automated workflow.
|
|
||||||
It accepts also one argument `future` which instead of trying a commit in between the tip of SHB main and the PR's failing one,
|
|
||||||
it tries a commit between the PR's failing one and the nixpkgs unstable latest commit.
|
|
||||||
|
|
@ -221,14 +221,14 @@ shb.nextcloud = {
|
||||||
host = "127.0.0.1";
|
host = "127.0.0.1";
|
||||||
port = config.shb.lldap.ldapPort;
|
port = config.shb.lldap.ldapPort;
|
||||||
dcdomain = config.shb.lldap.dcdomain;
|
dcdomain = config.shb.lldap.dcdomain;
|
||||||
adminPassword.result = config.shb.sops.secret."nextcloud/ldap/admin_password".result;
|
adminPassword.result = config.shb.sops.secrets."nextcloud/ldap/admin_password".result;
|
||||||
};
|
};
|
||||||
apps.sso = {
|
apps.sso = {
|
||||||
enable = true;
|
enable = true;
|
||||||
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
||||||
|
|
||||||
secret.result = config.shb.sops.secret."nextcloud/sso/secret".result;
|
secret.result = config.shb.sops.secrets."nextcloud/sso/secret".result;
|
||||||
secretForAuthelia.result = config.shb.sops.secret."nextcloud/sso/secretForAuthelia".result;
|
secretForAuthelia.result = config.shb.sops.secrets."nextcloud/sso/secretForAuthelia".result;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
@ -246,15 +246,15 @@ shb.forgejo = {
|
||||||
host = "127.0.0.1";
|
host = "127.0.0.1";
|
||||||
port = config.shb.lldap.ldapPort;
|
port = config.shb.lldap.ldapPort;
|
||||||
dcdomain = config.shb.lldap.dcdomain;
|
dcdomain = config.shb.lldap.dcdomain;
|
||||||
adminPassword.result = config.shb.sops.secret."nextcloud/ldap/admin_password".result;
|
adminPassword.result = config.shb.sops.secrets."nextcloud/ldap/admin_password".result;
|
||||||
};
|
};
|
||||||
|
|
||||||
sso = {
|
sso = {
|
||||||
enable = true;
|
enable = true;
|
||||||
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
||||||
|
|
||||||
secret.result = config.shb.sops.secret."forgejo/sso/secret".result;
|
secret.result = config.shb.sops.secrets."forgejo/sso/secret".result;
|
||||||
secretForAuthelia.result = config.shb.sops.secret."forgejo/sso/secretForAuthelia".result;
|
secretForAuthelia.result = config.shb.sops.secrets."forgejo/sso/secretForAuthelia".result;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<!-- Read these docs at https://shb.skarabox.com -->
|
<!-- Read these docs at https://shb.skarabox.com -->
|
||||||
# Self-Host a DNS server {#recipes-dnsServer}
|
# Self-Host a DNS server making {#recipes-dnsServer}
|
||||||
|
|
||||||
This recipe will show how to setup [dnsmasq][] as a local DNS server
|
This recipe will show how to setup [dnsmasq][] as a local DNS server
|
||||||
that forwards all queries to your own domain `example.com` to a local IP - your server running SelfHostBlocks for example.
|
that forwards all queries to your own domain `example.com` to a local IP - your server running SelfHostBlocks for example.
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ let
|
||||||
fqdn = "${subdomain}.${domain}";
|
fqdn = "${subdomain}.${domain}";
|
||||||
listenPort = 9000;
|
listenPort = 9000;
|
||||||
dataDir = "/var/lib/awesome";
|
dataDir = "/var/lib/awesome";
|
||||||
ldapGroup = "awesome_user";
|
|
||||||
in
|
in
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -76,11 +75,11 @@ shb.nginx.vhosts = [
|
||||||
{
|
{
|
||||||
inherit subdomain domain;
|
inherit subdomain domain;
|
||||||
ssl = config.shb.certs.certs.letsencrypt.${domain};
|
ssl = config.shb.certs.certs.letsencrypt.${domain};
|
||||||
upstream = "http://127.0.0.1:${toString listenPort}";
|
upstream = "http://127.0.0.1:${toString config.services.calibre-web.listen.port}";
|
||||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
||||||
autheliaRules = [{
|
autheliaRules = [{
|
||||||
policy = "one_factor";
|
policy = "one_factor";
|
||||||
subject = [ "group:${ldapGroup}" ];
|
subject = [ "group:${config.shb.lldap.ensureGroups.calibre_user.name}" ];
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
@ -121,36 +120,10 @@ shb.restic.instances.awesome = {
|
||||||
request.sourceDirectories = [ dataDir ];
|
request.sourceDirectories = [ dataDir ];
|
||||||
settings.enable = true;
|
settings.enable = true;
|
||||||
settings.passphrase.result = config.shb.sops.secret.awesome.result;
|
settings.passphrase.result = config.shb.sops.secret.awesome.result;
|
||||||
settings.repository.path = config.services.awesome.dataDir;
|
settings.repository.path = "/srv/backup/awesome";
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.sops.secret."awesome" = {
|
shb.sops.secret."awesome" = {
|
||||||
request = config.shb.restic.instances.awesome.settings.passphrase.request;
|
request = config.shb.restic.instances.awesome.settings.passphrase.request;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
## Impermanence {#recipes-exposeService-impermanence}
|
|
||||||
|
|
||||||
To save the data folder in an impermanence setup, add:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.zfs.datasets."safe/awesome".path = config.services.awesome.dataDir;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Application Dashboard {#recipes-exposeService-applicationdashboard}
|
|
||||||
|
|
||||||
For example using the [Homepage](services-homepage.html) service:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.homepage.servicesGroups.MyServices.services.Awesome = {
|
|
||||||
sortOrder = 1;
|
|
||||||
dashboard.request = {
|
|
||||||
externalUrl = "https://${fqdn}";
|
|
||||||
internalUrl = "http://127.0.0.1:${toString listenPort}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
|
||||||
1680
docs/redirects.json
1680
docs/redirects.json
File diff suppressed because it is too large
Load diff
|
|
@ -461,13 +461,6 @@ nix flake check
|
||||||
nix build .#manualHtml
|
nix build .#manualHtml
|
||||||
```
|
```
|
||||||
|
|
||||||
To continuously rebuild the documentation of file change, run the following command.
|
|
||||||
To exit, you'll need to do Ctrl-C twice in a row.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nix run .#manualHtml-watch
|
|
||||||
```
|
|
||||||
|
|
||||||
### Iterative Development Approach {#iterative-development-approach}
|
### Iterative Development Approach {#iterative-development-approach}
|
||||||
|
|
||||||
1. **Start with basic functionality** - get core service working
|
1. **Start with basic functionality** - get core service working
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<!-- Read these docs at https://shb.skarabox.com -->
|
<!-- Read these docs at https://shb.skarabox.com -->
|
||||||
# Services {#services}
|
# Services {#services}
|
||||||
|
|
||||||
Services are usually web applications that SHB help you self-host some of your data.
|
Services are usually web applications that SHB help you self-host.
|
||||||
Configuration of those is purposely made more opinionated than the upstream nixpkgs modules
|
Configuration of those is purposely made more opinionated than the upstream nixpkgs modules
|
||||||
in exchange for an uniformized configuration experience.
|
in exchange for requiring less options to define.
|
||||||
That is possible thanks to the extensive use of blocks provided by SHB.
|
That is possible thanks to the extensive use of blocks provided by SHB.
|
||||||
|
|
||||||
::: {.note}
|
::: {.note}
|
||||||
|
|
@ -13,20 +13,17 @@ Not all services are yet documented. You can find all available services [in the
|
||||||
The following table summarizes for each documented service what features it provides. More
|
The following table summarizes for each documented service what features it provides. More
|
||||||
information is provided in the respective manual sections.
|
information is provided in the respective manual sections.
|
||||||
|
|
||||||
| Service | Backup | Reverse Proxy | SSO | LDAP | Monitoring | Profiling |
|
| Service | Backup | Reverse Proxy | SSO | LDAP | Monitoring | Profiling |
|
||||||
|-----------------------------|--------|---------------|-----|-------|------------|-----------|
|
|----------------------|--------|---------------|-----|-------|------------|-----------|
|
||||||
| [*Arr][] | Y (1) | Y | Y | Y (4) | Y (2) | N |
|
| [*Arr][] | Y (1) | Y | Y | Y (4) | Y (2) | N |
|
||||||
| [Firefly-iii][] | Y (1) | Y | Y | Y | Y (2) | N |
|
| [Forgejo][] | Y (1) | Y | Y | Y | Y (2) | N |
|
||||||
| [Forgejo][] | Y (1) | Y | Y | Y | Y (2) | N |
|
| [Home-Assistant][] | Y (1) | Y | N | Y | Y (2) | N |
|
||||||
| [Home-Assistant][] | Y (1) | Y | N | Y | Y (2) | N |
|
| [Jellyfin][] | Y (1) | Y | Y | Y | Y (2) | N |
|
||||||
| [Homepage][] | Y (1) | Y | N | Y | Y (2) | N |
|
| [Karakeep][] | Y (1) | Y | Y | Y | Y (2) | N |
|
||||||
| [Jellyfin][] | Y (1) | Y | Y | Y | Y (2) | N |
|
| [Nextcloud Server][] | Y (1) | Y | Y | Y | Y (2) | P (3) |
|
||||||
| [Karakeep][] | Y (1) | Y | Y | Y | Y (2) | N |
|
| [Open WebUI][] | Y (1) | Y | Y | Y | Y (2) | N |
|
||||||
| [Nextcloud Server][] | Y (1) | Y | Y | Y | Y (2) | P (3) |
|
| [Pinchflat][] | Y | Y | Y | Y (4) | Y (5) | N |
|
||||||
| [Open WebUI][] | Y (1) | Y | Y | Y | Y (2) | N |
|
| [Vaultwarden][] | Y (1) | Y | Y | Y | Y (2) | N |
|
||||||
| [Pinchflat][] | Y | Y | Y | Y (4) | Y (5) | N |
|
|
||||||
| [Simple NixOS Mailserver][] | Y | Y | N | Y | Y | N |
|
|
||||||
| [Vaultwarden][] | Y (1) | Y | Y | Y | Y (2) | N |
|
|
||||||
|
|
||||||
Legend: **N**: no but WIP; **P**: partial; **Y**: yes
|
Legend: **N**: no but WIP; **P**: partial; **Y**: yes
|
||||||
|
|
||||||
|
|
@ -38,36 +35,21 @@ Legend: **N**: no but WIP; **P**: partial; **Y**: yes
|
||||||
4. Uses LDAP indirectly through forward auth.
|
4. Uses LDAP indirectly through forward auth.
|
||||||
|
|
||||||
[*Arr]: services-arr.html
|
[*Arr]: services-arr.html
|
||||||
[Firefly-iii]: services-firefly-iii.html
|
|
||||||
[Forgejo]: services-forgejo.html
|
[Forgejo]: services-forgejo.html
|
||||||
[Home-Assistant]: services-home-assistant.html
|
[Home-Assistant]: services-home-assistant.html
|
||||||
[Homepage]: services-homepage.html
|
|
||||||
[Jellyfin]: services-jellyfin.html
|
[Jellyfin]: services-jellyfin.html
|
||||||
[Karakeep]: services-karakeep.html
|
[Karakeep]: services-karakeep.html
|
||||||
[Nextcloud Server]: services-nextcloud.html
|
[Nextcloud Server]: services-nextcloud.html
|
||||||
[Open WebUI]: services-open-webui.html
|
[Open WebUI]: services-open-webui.html
|
||||||
[Pinchflat]: services-pinchflat.html
|
[Pinchflat]: services-pinchflat.html
|
||||||
[Simple NixOS Mailserver]: services-mailserver.html
|
|
||||||
[Vaultwarden]: services-vaultwarden.html
|
[Vaultwarden]: services-vaultwarden.html
|
||||||
|
|
||||||
## Dashboard {#services-category-dashboard}
|
|
||||||
|
|
||||||
```{=include=} chapters html:into-file=//services-homepage.html
|
|
||||||
modules/services/homepage/docs/default.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## Documents {#services-category-documents}
|
## Documents {#services-category-documents}
|
||||||
|
|
||||||
```{=include=} chapters html:into-file=//services-nextcloud.html
|
```{=include=} chapters html:into-file=//services-nextcloud.html
|
||||||
modules/services/nextcloud-server/docs/default.md
|
modules/services/nextcloud-server/docs/default.md
|
||||||
```
|
```
|
||||||
|
|
||||||
## Emails {#services-category-emails}
|
|
||||||
|
|
||||||
```{=include=} chapters html:into-file=//services-mailserver.html
|
|
||||||
modules/services/mailserver/docs/default.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## Passwords {#services-category-passwords}
|
## Passwords {#services-category-passwords}
|
||||||
|
|
||||||
```{=include=} chapters html:into-file=//services-vaultwarden.html
|
```{=include=} chapters html:into-file=//services-vaultwarden.html
|
||||||
|
|
@ -109,9 +91,3 @@ modules/services/jellyfin/docs/default.md
|
||||||
```{=include=} chapters html:into-file=//services-pinchflat.html
|
```{=include=} chapters html:into-file=//services-pinchflat.html
|
||||||
modules/services/pinchflat/docs/default.md
|
modules/services/pinchflat/docs/default.md
|
||||||
```
|
```
|
||||||
|
|
||||||
## Finance {#services-category-finance}
|
|
||||||
|
|
||||||
```{=include=} chapters html:into-file=//services-firefly-iii.html
|
|
||||||
modules/services/firefly-iii/docs/default.md
|
|
||||||
```
|
|
||||||
|
|
|
||||||
|
|
@ -634,8 +634,8 @@ One way to setup secrets management using `sops-nix`:
|
||||||
Setting the default this way makes all sops instances use that same file.
|
Setting the default this way makes all sops instances use that same file.
|
||||||
7. Reference the secrets in nix:
|
7. Reference the secrets in nix:
|
||||||
```nix
|
```nix
|
||||||
shb.sops.secret."nextcloud/adminpass".request = config.shb.nextcloud.adminPass.request;
|
shb.sops.secrets."nextcloud/adminpass".request = config.shb.nextcloud.adminPass.request;
|
||||||
shb.nextcloud.adminPass.result = config.shb.sops.secret."nextcloud/adminpass".result;
|
shb.nextcloud.adminPass.result = config.shb.sops.secrets."nextcloud/adminpass".result;
|
||||||
```
|
```
|
||||||
The above snippet uses the [secrets contract](./contracts-secret.html) and
|
The above snippet uses the [secrets contract](./contracts-secret.html) and
|
||||||
[sops block](./blocks-sops.html) to ease the configuration.
|
[sops block](./blocks-sops.html) to ease the configuration.
|
||||||
|
|
|
||||||
12
flake.lock
12
flake.lock
|
|
@ -20,11 +20,11 @@
|
||||||
},
|
},
|
||||||
"nix-flake-tests": {
|
"nix-flake-tests": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1775571237,
|
"lastModified": 1677844186,
|
||||||
"narHash": "sha256-1f5Uvgcy3gx/eyRp4rqfDRBjcZc9uiHoIlfcFIL7GvI=",
|
"narHash": "sha256-ErJZ/Gs1rxh561CJeWP5bohA2IcTq1rDneu1WT6CVII=",
|
||||||
"owner": "antifuchs",
|
"owner": "antifuchs",
|
||||||
"repo": "nix-flake-tests",
|
"repo": "nix-flake-tests",
|
||||||
"rev": "86b789cff8aecbd7c1bed7cd421dd837c3f62201",
|
"rev": "bbd9216bd0f6495bb961a8eb8392b7ef55c67afb",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -35,11 +35,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1781074563,
|
"lastModified": 1760878510,
|
||||||
"narHash": "sha256-md8WlXOlfnIeHeOScMTTHFyf2d6iaTwPl2apR5EQ3P4=",
|
"narHash": "sha256-K5Osef2qexezUfs0alLvZ7nQFTGS9DL2oTVsIXsqLgs=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "9ae611a455b90cf061d8f332b977e387bda8e1ca",
|
"rev": "5e2a59a5b1a82f89f2c7e598302a9cacebb72a67",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
355
flake.nix
355
flake.nix
|
|
@ -20,66 +20,52 @@
|
||||||
nmdsrc,
|
nmdsrc,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
|
||||||
shbPatches =
|
|
||||||
system:
|
|
||||||
nixpkgs.legacyPackages.${system}.lib.optionals
|
|
||||||
(system == "x86_64-linux" || system == "aarch64-linux")
|
|
||||||
[
|
|
||||||
# Get rid of lldap patches when https://github.com/NixOS/nixpkgs/pull/425923 is merged.
|
|
||||||
./patches/lldap.patch
|
|
||||||
./patches/0001-nixos-borgbackup-add-option-to-override-state-direct.patch
|
|
||||||
|
|
||||||
# Leaving commented out as an example.
|
|
||||||
# (originPkgs.fetchpatch {
|
|
||||||
# url = "https://github.com/NixOS/nixpkgs/pull/317107.patch";
|
|
||||||
# hash = "sha256-hoLrqV7XtR1hP/m0rV9hjYUBtrSjay0qcPUYlKKuVWk=";
|
|
||||||
# })
|
|
||||||
];
|
|
||||||
|
|
||||||
patchNixpkgs =
|
|
||||||
{
|
|
||||||
nixpkgs,
|
|
||||||
patches,
|
|
||||||
system,
|
|
||||||
}:
|
|
||||||
nixpkgs.legacyPackages.${system}.applyPatches {
|
|
||||||
name = "nixpkgs-patched";
|
|
||||||
src = nixpkgs;
|
|
||||||
inherit patches;
|
|
||||||
};
|
|
||||||
patchedNixpkgs =
|
|
||||||
system:
|
|
||||||
let
|
|
||||||
patched = patchNixpkgs {
|
|
||||||
nixpkgs = inputs.nixpkgs;
|
|
||||||
patches = shbPatches system;
|
|
||||||
inherit system;
|
|
||||||
};
|
|
||||||
in
|
|
||||||
patched
|
|
||||||
// {
|
|
||||||
nixosSystem = args: import "${patched}/nixos/lib/eval-config.nix" args;
|
|
||||||
};
|
|
||||||
pkgs' =
|
|
||||||
system:
|
|
||||||
import (patchedNixpkgs system) {
|
|
||||||
inherit system;
|
|
||||||
config.allowUnfree = true;
|
|
||||||
};
|
|
||||||
in
|
|
||||||
flake-utils.lib.eachDefaultSystem (
|
flake-utils.lib.eachDefaultSystem (
|
||||||
system:
|
system:
|
||||||
let
|
let
|
||||||
pkgs = pkgs' system;
|
originPkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
shbPatches = originPkgs.lib.optionals (system == "x86_64-linux") [
|
||||||
|
# Get rid of lldap patches when https://github.com/NixOS/nixpkgs/pull/425923 is merged.
|
||||||
|
./patches/lldap.patch
|
||||||
|
./patches/0001-nixos-borgbackup-add-option-to-override-state-direct.patch
|
||||||
|
|
||||||
|
# Leaving commented out as an example.
|
||||||
|
# (originPkgs.fetchpatch {
|
||||||
|
# url = "https://github.com/NixOS/nixpkgs/pull/317107.patch";
|
||||||
|
# hash = "sha256-hoLrqV7XtR1hP/m0rV9hjYUBtrSjay0qcPUYlKKuVWk=";
|
||||||
|
# })
|
||||||
|
];
|
||||||
|
patchNixpkgs =
|
||||||
|
{
|
||||||
|
nixpkgs,
|
||||||
|
patches,
|
||||||
|
system,
|
||||||
|
}:
|
||||||
|
nixpkgs.legacyPackages.${system}.applyPatches {
|
||||||
|
name = "nixpkgs-patched";
|
||||||
|
src = nixpkgs;
|
||||||
|
inherit patches;
|
||||||
|
};
|
||||||
|
patchedNixpkgs =
|
||||||
|
let
|
||||||
|
patched = patchNixpkgs {
|
||||||
|
nixpkgs = inputs.nixpkgs;
|
||||||
|
patches = shbPatches;
|
||||||
|
inherit system;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
patched
|
||||||
|
// {
|
||||||
|
nixosSystem = args: import "${patched}/nixos/lib/eval-config.nix" args;
|
||||||
|
};
|
||||||
|
pkgs = import patchedNixpkgs {
|
||||||
|
inherit system;
|
||||||
|
config.allowUnfree = true;
|
||||||
|
};
|
||||||
|
|
||||||
# The contract dummies are used to show options for contracts.
|
# The contract dummies are used to show options for contracts.
|
||||||
contractDummyModules = [
|
contractDummyModules = [
|
||||||
modules/contracts/backup/dummyModule.nix
|
modules/contracts/backup/dummyModule.nix
|
||||||
modules/contracts/dashboard/dummyModule.nix
|
|
||||||
modules/contracts/databasebackup/dummyModule.nix
|
|
||||||
modules/contracts/datasetbackup/dummyModule.nix
|
|
||||||
modules/contracts/secret/dummyModule.nix
|
|
||||||
modules/contracts/ssl/dummyModule.nix
|
modules/contracts/ssl/dummyModule.nix
|
||||||
];
|
];
|
||||||
in
|
in
|
||||||
|
|
@ -104,13 +90,6 @@
|
||||||
"blocks/authelia" = ./modules/blocks/authelia.nix;
|
"blocks/authelia" = ./modules/blocks/authelia.nix;
|
||||||
"blocks/borgbackup" = ./modules/blocks/borgbackup.nix;
|
"blocks/borgbackup" = ./modules/blocks/borgbackup.nix;
|
||||||
"blocks/lldap" = ./modules/blocks/lldap.nix;
|
"blocks/lldap" = ./modules/blocks/lldap.nix;
|
||||||
"blocks/mitmdump" = ./modules/blocks/mitmdump.nix;
|
|
||||||
"blocks/monitoring" = ./modules/blocks/monitoring.nix;
|
|
||||||
"blocks/nginx" = ./modules/blocks/nginx.nix;
|
|
||||||
"blocks/postgresql" = ./modules/blocks/postgresql.nix;
|
|
||||||
"blocks/restic" = ./modules/blocks/restic.nix;
|
|
||||||
"blocks/sanoid" = ./modules/blocks/sanoid.nix;
|
|
||||||
"blocks/sops" = ./modules/blocks/sops.nix;
|
|
||||||
"blocks/ssl" = {
|
"blocks/ssl" = {
|
||||||
module = ./modules/blocks/ssl.nix;
|
module = ./modules/blocks/ssl.nix;
|
||||||
optionRoot = [
|
optionRoot = [
|
||||||
|
|
@ -118,19 +97,19 @@
|
||||||
"certs"
|
"certs"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
"blocks/zfs" = ./modules/blocks/zfs.nix;
|
"blocks/mitmdump" = ./modules/blocks/mitmdump.nix;
|
||||||
|
"blocks/monitoring" = ./modules/blocks/monitoring.nix;
|
||||||
|
"blocks/postgresql" = ./modules/blocks/postgresql.nix;
|
||||||
|
"blocks/restic" = ./modules/blocks/restic.nix;
|
||||||
|
"blocks/sops" = ./modules/blocks/sops.nix;
|
||||||
"services/arr" = ./modules/services/arr.nix;
|
"services/arr" = ./modules/services/arr.nix;
|
||||||
"services/firefly-iii" = ./modules/services/firefly-iii.nix;
|
|
||||||
"services/forgejo" = [
|
"services/forgejo" = [
|
||||||
./modules/services/forgejo.nix
|
./modules/services/forgejo.nix
|
||||||
(pkgs.path + "/nixos/modules/services/misc/forgejo.nix")
|
(pkgs.path + "/nixos/modules/services/misc/forgejo.nix")
|
||||||
];
|
];
|
||||||
"services/home-assistant" = ./modules/services/home-assistant.nix;
|
"services/home-assistant" = ./modules/services/home-assistant.nix;
|
||||||
"services/homepage" = ./modules/services/homepage.nix;
|
|
||||||
"services/jellyfin" = ./modules/services/jellyfin.nix;
|
"services/jellyfin" = ./modules/services/jellyfin.nix;
|
||||||
"services/karakeep" = ./modules/services/karakeep.nix;
|
"services/karakeep" = ./modules/services/karakeep.nix;
|
||||||
"services/mailserver" = ./modules/services/mailserver.nix;
|
|
||||||
"services/nextcloud-server" = {
|
"services/nextcloud-server" = {
|
||||||
module = ./modules/services/nextcloud-server.nix;
|
module = ./modules/services/nextcloud-server.nix;
|
||||||
optionRoot = [
|
optionRoot = [
|
||||||
|
|
@ -141,7 +120,6 @@
|
||||||
"services/open-webui" = ./modules/services/open-webui.nix;
|
"services/open-webui" = ./modules/services/open-webui.nix;
|
||||||
"services/pinchflat" = ./modules/services/pinchflat.nix;
|
"services/pinchflat" = ./modules/services/pinchflat.nix;
|
||||||
"services/vaultwarden" = ./modules/services/vaultwarden.nix;
|
"services/vaultwarden" = ./modules/services/vaultwarden.nix;
|
||||||
|
|
||||||
"contracts/backup" = {
|
"contracts/backup" = {
|
||||||
module = ./modules/contracts/backup/dummyModule.nix;
|
module = ./modules/contracts/backup/dummyModule.nix;
|
||||||
optionRoot = [
|
optionRoot = [
|
||||||
|
|
@ -150,14 +128,6 @@
|
||||||
"backup"
|
"backup"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
"contracts/dashboard" = {
|
|
||||||
module = ./modules/contracts/dashboard/dummyModule.nix;
|
|
||||||
optionRoot = [
|
|
||||||
"shb"
|
|
||||||
"contracts"
|
|
||||||
"dashboard"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
"contracts/databasebackup" = {
|
"contracts/databasebackup" = {
|
||||||
module = ./modules/contracts/databasebackup/dummyModule.nix;
|
module = ./modules/contracts/databasebackup/dummyModule.nix;
|
||||||
optionRoot = [
|
optionRoot = [
|
||||||
|
|
@ -166,14 +136,6 @@
|
||||||
"databasebackup"
|
"databasebackup"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
"contracts/datasetbackup" = {
|
|
||||||
module = ./modules/contracts/datasetbackup/dummyModule.nix;
|
|
||||||
optionRoot = [
|
|
||||||
"shb"
|
|
||||||
"contracts"
|
|
||||||
"datasetbackup"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
"contracts/secret" = {
|
"contracts/secret" = {
|
||||||
module = ./modules/contracts/secret/dummyModule.nix;
|
module = ./modules/contracts/secret/dummyModule.nix;
|
||||||
optionRoot = [
|
optionRoot = [
|
||||||
|
|
@ -243,32 +205,108 @@
|
||||||
'';
|
'';
|
||||||
});
|
});
|
||||||
|
|
||||||
packages.manualHtml-watch = pkgs.writeShellApplication {
|
|
||||||
name = "manualHtml-watch";
|
|
||||||
runtimeInputs = [
|
|
||||||
pkgs.findutils
|
|
||||||
pkgs.entr
|
|
||||||
];
|
|
||||||
text = ''
|
|
||||||
while sleep 1; do
|
|
||||||
find . -name "*.nix" -o -name "*.md" \
|
|
||||||
| entr -d sh -c '(nix run --offline .#update-redirects && nix build --offline .#manualHtml)' || :
|
|
||||||
done
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
packages.update-flake-lock-pr = pkgs.callPackage ./.github/workflows/update-flake-lock-pr.nix { };
|
|
||||||
|
|
||||||
lib = (pkgs.callPackage ./lib { }) // {
|
lib = (pkgs.callPackage ./lib { }) // {
|
||||||
test = pkgs.callPackage ./test/common.nix { };
|
test = pkgs.callPackage ./test/common.nix { };
|
||||||
contracts = pkgs.callPackage ./modules/contracts {
|
contracts = pkgs.callPackage ./modules/contracts {
|
||||||
shb = self.lib.${system};
|
shb = self.lib.${system};
|
||||||
};
|
};
|
||||||
patches = shbPatches system;
|
patches = shbPatches;
|
||||||
inherit patchNixpkgs;
|
inherit patchNixpkgs patchedNixpkgs;
|
||||||
patchedNixpkgs = patchedNixpkgs system;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
checks =
|
||||||
|
let
|
||||||
|
inherit (pkgs.lib)
|
||||||
|
foldl
|
||||||
|
foldlAttrs
|
||||||
|
mergeAttrs
|
||||||
|
optionalAttrs
|
||||||
|
;
|
||||||
|
|
||||||
|
importFiles =
|
||||||
|
files:
|
||||||
|
map (
|
||||||
|
m:
|
||||||
|
pkgs.callPackage m {
|
||||||
|
shb = self.lib.${system};
|
||||||
|
}
|
||||||
|
) files;
|
||||||
|
|
||||||
|
mergeTests = foldl mergeAttrs { };
|
||||||
|
|
||||||
|
flattenAttrs =
|
||||||
|
root: attrset:
|
||||||
|
foldlAttrs (
|
||||||
|
acc: name: value:
|
||||||
|
acc
|
||||||
|
// {
|
||||||
|
"${root}_${name}" = value;
|
||||||
|
}
|
||||||
|
) { } attrset;
|
||||||
|
|
||||||
|
vm_test =
|
||||||
|
name: path:
|
||||||
|
flattenAttrs "vm_${name}" (
|
||||||
|
removeAttrs
|
||||||
|
(pkgs.callPackage path {
|
||||||
|
shb = self.lib.${system};
|
||||||
|
})
|
||||||
|
[
|
||||||
|
"override"
|
||||||
|
"overrideDerivation"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
in
|
||||||
|
(optionalAttrs (system == "x86_64-linux") (
|
||||||
|
{
|
||||||
|
modules = self.lib.${system}.check {
|
||||||
|
inherit pkgs;
|
||||||
|
tests = mergeTests (importFiles [
|
||||||
|
./test/modules/davfs.nix
|
||||||
|
# TODO: Make this not use IFD
|
||||||
|
./test/modules/lib.nix
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
# TODO: Make this not use IFD
|
||||||
|
lib = nix-flake-tests.lib.check {
|
||||||
|
inherit pkgs;
|
||||||
|
tests = pkgs.callPackage ./test/modules/lib.nix {
|
||||||
|
shb = self.lib.${system};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// (vm_test "arr" ./test/services/arr.nix)
|
||||||
|
// (vm_test "audiobookshelf" ./test/services/audiobookshelf.nix)
|
||||||
|
// (vm_test "deluge" ./test/services/deluge.nix)
|
||||||
|
// (vm_test "forgejo" ./test/services/forgejo.nix)
|
||||||
|
// (vm_test "grocy" ./test/services/grocy.nix)
|
||||||
|
// (vm_test "hledger" ./test/services/hledger.nix)
|
||||||
|
// (vm_test "immich" ./test/services/immich.nix)
|
||||||
|
// (vm_test "homeassistant" ./test/services/home-assistant.nix)
|
||||||
|
// (vm_test "jellyfin" ./test/services/jellyfin.nix)
|
||||||
|
// (vm_test "karakeep" ./test/services/karakeep.nix)
|
||||||
|
// (vm_test "nextcloud" ./test/services/nextcloud.nix)
|
||||||
|
// (vm_test "open-webui" ./test/services/open-webui.nix)
|
||||||
|
// (vm_test "paperless" ./test/services/paperless.nix)
|
||||||
|
// (vm_test "pinchflat" ./test/services/pinchflat.nix)
|
||||||
|
// (vm_test "vaultwarden" ./test/services/vaultwarden.nix)
|
||||||
|
|
||||||
|
// (vm_test "authelia" ./test/blocks/authelia.nix)
|
||||||
|
// (vm_test "borgbackup" ./test/blocks/borgbackup.nix)
|
||||||
|
// (vm_test "lldap" ./test/blocks/lldap.nix)
|
||||||
|
// (vm_test "lib" ./test/blocks/lib.nix)
|
||||||
|
// (vm_test "mitmdump" ./test/blocks/mitmdump.nix)
|
||||||
|
// (vm_test "monitoring" ./test/blocks/monitoring.nix)
|
||||||
|
// (vm_test "postgresql" ./test/blocks/postgresql.nix)
|
||||||
|
// (vm_test "restic" ./test/blocks/restic.nix)
|
||||||
|
// (vm_test "ssl" ./test/blocks/ssl.nix)
|
||||||
|
|
||||||
|
// (vm_test "contracts-backup" ./test/contracts/backup.nix)
|
||||||
|
// (vm_test "contracts-databasebackup" ./test/contracts/databasebackup.nix)
|
||||||
|
// (vm_test "contracts-secret" ./test/contracts/secret.nix)
|
||||||
|
));
|
||||||
|
|
||||||
# To see the traces, run:
|
# To see the traces, run:
|
||||||
# nix run .#playwright -- show-trace $(nix eval .#checks.x86_64-linux.vm_grocy_basic --raw)/trace/0.zip
|
# nix run .#playwright -- show-trace $(nix eval .#checks.x86_64-linux.vm_grocy_basic --raw)/trace/0.zip
|
||||||
packages.playwright = pkgs.callPackage (
|
packages.playwright = pkgs.callPackage (
|
||||||
|
|
@ -324,111 +362,6 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
// flake-utils.lib.eachSystem [ "x86_64-linux" ] (
|
|
||||||
system:
|
|
||||||
let
|
|
||||||
pkgs = pkgs' system;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
checks =
|
|
||||||
let
|
|
||||||
inherit (pkgs.lib)
|
|
||||||
foldl
|
|
||||||
foldlAttrs
|
|
||||||
mergeAttrs
|
|
||||||
;
|
|
||||||
|
|
||||||
importFiles =
|
|
||||||
files:
|
|
||||||
map (
|
|
||||||
m:
|
|
||||||
pkgs.callPackage m {
|
|
||||||
shb = self.lib.${system};
|
|
||||||
}
|
|
||||||
) files;
|
|
||||||
|
|
||||||
mergeTests = foldl mergeAttrs { };
|
|
||||||
|
|
||||||
flattenAttrs =
|
|
||||||
root: attrset:
|
|
||||||
foldlAttrs (
|
|
||||||
acc: name: value:
|
|
||||||
acc
|
|
||||||
// {
|
|
||||||
"${root}_${name}" = value;
|
|
||||||
}
|
|
||||||
) { } attrset;
|
|
||||||
|
|
||||||
vm_test =
|
|
||||||
name: path:
|
|
||||||
flattenAttrs "vm_${name}" (
|
|
||||||
removeAttrs
|
|
||||||
(pkgs.callPackage path {
|
|
||||||
shb = self.lib.${system};
|
|
||||||
})
|
|
||||||
[
|
|
||||||
"override"
|
|
||||||
"overrideDerivation"
|
|
||||||
]
|
|
||||||
);
|
|
||||||
in
|
|
||||||
(
|
|
||||||
{
|
|
||||||
modules = self.lib.${system}.check {
|
|
||||||
inherit pkgs;
|
|
||||||
tests = mergeTests (importFiles [
|
|
||||||
./test/modules/davfs.nix
|
|
||||||
# TODO: Make this not use IFD
|
|
||||||
./test/modules/lib.nix
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
# TODO: Make this not use IFD
|
|
||||||
lib = nix-flake-tests.lib.check {
|
|
||||||
inherit pkgs;
|
|
||||||
tests = pkgs.callPackage ./test/modules/lib.nix {
|
|
||||||
shb = self.lib.${system};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// (vm_test "arr" ./test/services/arr.nix)
|
|
||||||
// (vm_test "audiobookshelf" ./test/services/audiobookshelf.nix)
|
|
||||||
// (vm_test "deluge" ./test/services/deluge.nix)
|
|
||||||
// (vm_test "firefly-iii" ./test/services/firefly-iii.nix)
|
|
||||||
// (vm_test "forgejo" ./test/services/forgejo.nix)
|
|
||||||
// (vm_test "grocy" ./test/services/grocy.nix)
|
|
||||||
// (vm_test "hledger" ./test/services/hledger.nix)
|
|
||||||
// (vm_test "immich" ./test/services/immich.nix)
|
|
||||||
// (vm_test "homeassistant" ./test/services/home-assistant.nix)
|
|
||||||
// (vm_test "homepage" ./test/services/homepage.nix)
|
|
||||||
// (vm_test "jellyfin" ./test/services/jellyfin.nix)
|
|
||||||
// (vm_test "karakeep" ./test/services/karakeep.nix)
|
|
||||||
// (vm_test "mailserver" ./test/services/mailserver.nix)
|
|
||||||
// (vm_test "nextcloud" ./test/services/nextcloud.nix)
|
|
||||||
// (vm_test "open-webui" ./test/services/open-webui.nix)
|
|
||||||
// (vm_test "paperless" ./test/services/paperless.nix)
|
|
||||||
// (vm_test "pinchflat" ./test/services/pinchflat.nix)
|
|
||||||
// (vm_test "vaultwarden" ./test/services/vaultwarden.nix)
|
|
||||||
|
|
||||||
// (vm_test "authelia" ./test/blocks/authelia.nix)
|
|
||||||
// (vm_test "borgbackup" ./test/blocks/borgbackup.nix)
|
|
||||||
// (vm_test "lib" ./test/blocks/lib.nix)
|
|
||||||
// (vm_test "lldap" ./test/blocks/lldap.nix)
|
|
||||||
// (vm_test "mitmdump" ./test/blocks/mitmdump.nix)
|
|
||||||
// (vm_test "monitoring" ./test/blocks/monitoring.nix)
|
|
||||||
// (vm_test "postgresql" ./test/blocks/postgresql.nix)
|
|
||||||
// (vm_test "restic" ./test/blocks/restic.nix)
|
|
||||||
// (vm_test "ssl" ./test/blocks/ssl.nix)
|
|
||||||
// (vm_test "zfs" ./test/blocks/zfs.nix)
|
|
||||||
|
|
||||||
// (vm_test "contracts-backup" ./test/contracts/backup.nix)
|
|
||||||
// (vm_test "contracts-databasebackup" ./test/contracts/databasebackup.nix)
|
|
||||||
// (vm_test "contracts-datasetbackup" ./test/contracts/datasetbackup.nix)
|
|
||||||
// (vm_test "contracts-secret" ./test/contracts/secret.nix)
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// {
|
// {
|
||||||
herculesCI.ciSystems = [ "x86_64-linux" ];
|
herculesCI.ciSystems = [ "x86_64-linux" ];
|
||||||
|
|
||||||
|
|
@ -445,7 +378,6 @@
|
||||||
self.nixosModules.nginx
|
self.nixosModules.nginx
|
||||||
self.nixosModules.postgresql
|
self.nixosModules.postgresql
|
||||||
self.nixosModules.restic
|
self.nixosModules.restic
|
||||||
self.nixosModules.sanoid
|
|
||||||
self.nixosModules.ssl
|
self.nixosModules.ssl
|
||||||
self.nixosModules.tinyproxy
|
self.nixosModules.tinyproxy
|
||||||
self.nixosModules.vpn
|
self.nixosModules.vpn
|
||||||
|
|
@ -455,16 +387,13 @@
|
||||||
self.nixosModules.arr
|
self.nixosModules.arr
|
||||||
self.nixosModules.audiobookshelf
|
self.nixosModules.audiobookshelf
|
||||||
self.nixosModules.deluge
|
self.nixosModules.deluge
|
||||||
self.nixosModules.firefly-iii
|
|
||||||
self.nixosModules.forgejo
|
self.nixosModules.forgejo
|
||||||
self.nixosModules.grocy
|
self.nixosModules.grocy
|
||||||
self.nixosModules.hledger
|
self.nixosModules.hledger
|
||||||
self.nixosModules.immich
|
self.nixosModules.immich
|
||||||
self.nixosModules.home-assistant
|
self.nixosModules.home-assistant
|
||||||
self.nixosModules.homepage
|
|
||||||
self.nixosModules.jellyfin
|
self.nixosModules.jellyfin
|
||||||
self.nixosModules.karakeep
|
self.nixosModules.karakeep
|
||||||
self.nixosModules.mailserver
|
|
||||||
self.nixosModules.nextcloud-server
|
self.nixosModules.nextcloud-server
|
||||||
self.nixosModules.open-webui
|
self.nixosModules.open-webui
|
||||||
self.nixosModules.pinchflat
|
self.nixosModules.pinchflat
|
||||||
|
|
@ -485,9 +414,8 @@
|
||||||
nixosModules.nginx = modules/blocks/nginx.nix;
|
nixosModules.nginx = modules/blocks/nginx.nix;
|
||||||
nixosModules.postgresql = modules/blocks/postgresql.nix;
|
nixosModules.postgresql = modules/blocks/postgresql.nix;
|
||||||
nixosModules.restic = modules/blocks/restic.nix;
|
nixosModules.restic = modules/blocks/restic.nix;
|
||||||
nixosModules.sanoid = modules/blocks/sanoid.nix;
|
|
||||||
nixosModules.sops = modules/blocks/sops.nix;
|
|
||||||
nixosModules.ssl = modules/blocks/ssl.nix;
|
nixosModules.ssl = modules/blocks/ssl.nix;
|
||||||
|
nixosModules.sops = modules/blocks/sops.nix;
|
||||||
nixosModules.tinyproxy = modules/blocks/tinyproxy.nix;
|
nixosModules.tinyproxy = modules/blocks/tinyproxy.nix;
|
||||||
nixosModules.vpn = modules/blocks/vpn.nix;
|
nixosModules.vpn = modules/blocks/vpn.nix;
|
||||||
nixosModules.zfs = modules/blocks/zfs.nix;
|
nixosModules.zfs = modules/blocks/zfs.nix;
|
||||||
|
|
@ -495,16 +423,13 @@
|
||||||
nixosModules.arr = modules/services/arr.nix;
|
nixosModules.arr = modules/services/arr.nix;
|
||||||
nixosModules.audiobookshelf = modules/services/audiobookshelf.nix;
|
nixosModules.audiobookshelf = modules/services/audiobookshelf.nix;
|
||||||
nixosModules.deluge = modules/services/deluge.nix;
|
nixosModules.deluge = modules/services/deluge.nix;
|
||||||
nixosModules.firefly-iii = modules/services/firefly-iii.nix;
|
|
||||||
nixosModules.forgejo = modules/services/forgejo.nix;
|
nixosModules.forgejo = modules/services/forgejo.nix;
|
||||||
nixosModules.grocy = modules/services/grocy.nix;
|
nixosModules.grocy = modules/services/grocy.nix;
|
||||||
nixosModules.hledger = modules/services/hledger.nix;
|
nixosModules.hledger = modules/services/hledger.nix;
|
||||||
nixosModules.immich = modules/services/immich.nix;
|
nixosModules.immich = modules/services/immich.nix;
|
||||||
nixosModules.home-assistant = modules/services/home-assistant.nix;
|
nixosModules.home-assistant = modules/services/home-assistant.nix;
|
||||||
nixosModules.homepage = modules/services/homepage.nix;
|
|
||||||
nixosModules.jellyfin = modules/services/jellyfin.nix;
|
nixosModules.jellyfin = modules/services/jellyfin.nix;
|
||||||
nixosModules.karakeep = modules/services/karakeep.nix;
|
nixosModules.karakeep = modules/services/karakeep.nix;
|
||||||
nixosModules.mailserver = modules/services/mailserver.nix;
|
|
||||||
nixosModules.nextcloud-server = modules/services/nextcloud-server.nix;
|
nixosModules.nextcloud-server = modules/services/nextcloud-server.nix;
|
||||||
nixosModules.open-webui = modules/services/open-webui.nix;
|
nixosModules.open-webui = modules/services/open-webui.nix;
|
||||||
nixosModules.paperless = modules/services/paperless.nix;
|
nixosModules.paperless = modules/services/paperless.nix;
|
||||||
|
|
|
||||||
758
lib/default.nix
758
lib/default.nix
|
|
@ -1,460 +1,386 @@
|
||||||
{ pkgs, lib }:
|
{ pkgs, lib }:
|
||||||
let
|
let
|
||||||
inherit (builtins) isAttrs hasAttr;
|
inherit (builtins) isAttrs hasAttr;
|
||||||
inherit (lib)
|
inherit (lib) any concatMapStringsSep concatStringsSep;
|
||||||
any
|
in
|
||||||
concatMapStringsSep
|
rec {
|
||||||
concatStringsSep
|
# Replace secrets in a file.
|
||||||
escapeShellArg
|
# - userConfig is an attrset that will produce a config file.
|
||||||
;
|
# - resultPath is the location the config file should have on the filesystem.
|
||||||
shb = rec {
|
# - generator is a function taking two arguments name and value and returning path in the nix
|
||||||
# Replace secrets in a file.
|
# nix store where the
|
||||||
# - userConfig is an attrset that will produce a config file.
|
replaceSecrets =
|
||||||
# - resultPath is the location the config file should have on the filesystem.
|
{
|
||||||
# - generator is a function taking two arguments name and value and returning path in the nix
|
userConfig,
|
||||||
# nix store where the
|
resultPath,
|
||||||
replaceSecrets =
|
generator,
|
||||||
{
|
user ? null,
|
||||||
userConfig,
|
permissions ? "u=r,g=r,o=",
|
||||||
resultPath,
|
}:
|
||||||
generator,
|
let
|
||||||
user ? null,
|
configWithTemplates = withReplacements userConfig;
|
||||||
permissions ? "u=r,g=r,o=",
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
configWithTemplates = withReplacements userConfig;
|
|
||||||
|
|
||||||
nonSecretConfigFile = generator "template" configWithTemplates;
|
nonSecretConfigFile = generator "template" configWithTemplates;
|
||||||
|
|
||||||
replacements = getReplacements userConfig;
|
replacements = getReplacements userConfig;
|
||||||
in
|
in
|
||||||
replaceSecretsScript {
|
replaceSecretsScript {
|
||||||
file = nonSecretConfigFile;
|
file = nonSecretConfigFile;
|
||||||
inherit resultPath replacements;
|
inherit resultPath replacements;
|
||||||
inherit user permissions;
|
inherit user permissions;
|
||||||
};
|
|
||||||
|
|
||||||
replaceSecretsFormatAdapter = format: format.generate;
|
|
||||||
replaceSecretsGeneratorAdapter =
|
|
||||||
generator: name: value:
|
|
||||||
pkgs.writeText "generator " (generator value);
|
|
||||||
toEnvVar = replaceSecretsGeneratorAdapter (
|
|
||||||
v: (lib.generators.toINIWithGlobalSection { } { globalSection = v; })
|
|
||||||
);
|
|
||||||
|
|
||||||
template =
|
|
||||||
file: newPath: replacements:
|
|
||||||
replaceSecretsScript {
|
|
||||||
inherit file replacements;
|
|
||||||
resultPath = newPath;
|
|
||||||
};
|
|
||||||
|
|
||||||
genReplacement =
|
|
||||||
secret:
|
|
||||||
let
|
|
||||||
t =
|
|
||||||
{
|
|
||||||
transform ? null,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
if isNull transform then x: x else transform;
|
|
||||||
in
|
|
||||||
lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})");
|
|
||||||
|
|
||||||
replaceSecretsScript =
|
|
||||||
{
|
|
||||||
file,
|
|
||||||
resultPath,
|
|
||||||
replacements,
|
|
||||||
user ? null,
|
|
||||||
permissions ? "u=r,g=r,o=",
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
templatePath = resultPath + ".template";
|
|
||||||
|
|
||||||
# We check that the files containing the secrets have the
|
|
||||||
# correct permissions for us to read them in this separate
|
|
||||||
# step. Otherwise, the $(cat ...) commands inside the sed
|
|
||||||
# replacements could fail but not fail individually but
|
|
||||||
# not fail the whole script.
|
|
||||||
checkPermissions = concatMapStringsSep "\n" (
|
|
||||||
pattern: "cat ${pattern.source} > /dev/null"
|
|
||||||
) replacements;
|
|
||||||
|
|
||||||
generatedReplacements = map genReplacement replacements;
|
|
||||||
|
|
||||||
replaceScript = pkgs.writers.writePython3 "replace-secret" { } ''
|
|
||||||
import argparse
|
|
||||||
import pathlib
|
|
||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--file", required=True, type=pathlib.Path)
|
|
||||||
parser.add_argument("--marker", required=True)
|
|
||||||
parser.add_argument("--replacement", required=True)
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
args = parse_args()
|
|
||||||
content = args.file.read_text()
|
|
||||||
args.file.write_text(content.replace(args.marker, args.replacement))
|
|
||||||
'';
|
|
||||||
|
|
||||||
replaceCommands = concatMapStringsSep "\n" (pattern: ''
|
|
||||||
${replaceScript} \
|
|
||||||
--file ${escapeShellArg resultPath} \
|
|
||||||
--marker ${escapeShellArg pattern.name} \
|
|
||||||
--replacement "${pattern.value}"
|
|
||||||
'') generatedReplacements;
|
|
||||||
|
|
||||||
replaceCmd =
|
|
||||||
if replacements == [ ] then
|
|
||||||
"cat ${escapeShellArg templatePath} > ${escapeShellArg resultPath}"
|
|
||||||
else
|
|
||||||
''
|
|
||||||
cat ${escapeShellArg templatePath} > ${escapeShellArg resultPath}
|
|
||||||
${replaceCommands}
|
|
||||||
'';
|
|
||||||
in
|
|
||||||
''
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
${checkPermissions}
|
|
||||||
|
|
||||||
mkdir -p $(dirname ${templatePath})
|
|
||||||
ln -fs ${file} ${templatePath}
|
|
||||||
rm -f ${resultPath}
|
|
||||||
touch ${resultPath}
|
|
||||||
''
|
|
||||||
+ (lib.optionalString (user != null) ''
|
|
||||||
chown ${user} ${resultPath}
|
|
||||||
'')
|
|
||||||
+ ''
|
|
||||||
${replaceCmd}
|
|
||||||
chmod ${permissions} ${resultPath}
|
|
||||||
'';
|
|
||||||
|
|
||||||
secretFileType = lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
source = lib.mkOption {
|
|
||||||
type = lib.types.path;
|
|
||||||
description = "File containing the value.";
|
|
||||||
};
|
|
||||||
|
|
||||||
transform = lib.mkOption {
|
|
||||||
type = lib.types.raw;
|
|
||||||
description = "An optional function to transform the secret.";
|
|
||||||
default = null;
|
|
||||||
example = lib.literalExpression ''
|
|
||||||
v: "prefix-$${v}-suffix"
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
secretName =
|
replaceSecretsFormatAdapter = format: format.generate;
|
||||||
names: "%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) names)}%";
|
replaceSecretsGeneratorAdapter =
|
||||||
|
generator: name: value:
|
||||||
|
pkgs.writeText "generator " (generator value);
|
||||||
|
toEnvVar = replaceSecretsGeneratorAdapter (
|
||||||
|
v: (lib.generators.toINIWithGlobalSection { } { globalSection = v; })
|
||||||
|
);
|
||||||
|
|
||||||
withReplacements =
|
template =
|
||||||
attrs:
|
file: newPath: replacements:
|
||||||
let
|
replaceSecretsScript {
|
||||||
valueOrReplacement =
|
inherit file replacements;
|
||||||
name: value: if !(builtins.isAttrs value && value ? "source") then value else secretName name;
|
resultPath = newPath;
|
||||||
in
|
};
|
||||||
mapAttrsRecursiveCond (v: !v ? "source") valueOrReplacement attrs;
|
|
||||||
|
|
||||||
getReplacements =
|
genReplacement =
|
||||||
attrs:
|
secret:
|
||||||
let
|
let
|
||||||
addNameField =
|
t =
|
||||||
name: value:
|
{
|
||||||
if !(builtins.isAttrs value && value ? "source") then value else value // { name = name; };
|
transform ? null,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
if isNull transform then x: x else transform;
|
||||||
|
in
|
||||||
|
lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})");
|
||||||
|
|
||||||
secretsWithName = mapAttrsRecursiveCond (v: !v ? "source") addNameField attrs;
|
replaceSecretsScript =
|
||||||
in
|
{
|
||||||
collect (v: builtins.isAttrs v && v ? "source") secretsWithName;
|
file,
|
||||||
|
resultPath,
|
||||||
|
replacements,
|
||||||
|
user ? null,
|
||||||
|
permissions ? "u=r,g=r,o=",
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
templatePath = resultPath + ".template";
|
||||||
|
|
||||||
# Inspired lib.attrsets.mapAttrsRecursiveCond but also recurses on lists.
|
# We check that the files containing the secrets have the
|
||||||
mapAttrsRecursiveCond =
|
# correct permissions for us to read them in this separate
|
||||||
# A function, given the attribute set the recursion is currently at, determine if to recurse deeper into that attribute set.
|
# step. Otherwise, the $(cat ...) commands inside the sed
|
||||||
cond:
|
# replacements could fail but not fail individually but
|
||||||
# A function, given a list of attribute names and a value, returns a new value.
|
# not fail the whole script.
|
||||||
f:
|
checkPermissions = concatMapStringsSep "\n" (
|
||||||
# Attribute set or list to recursively map over.
|
pattern: "cat ${pattern.source} > /dev/null"
|
||||||
set:
|
) replacements;
|
||||||
let
|
|
||||||
recurse =
|
|
||||||
path: val:
|
|
||||||
if builtins.isAttrs val && cond val then
|
|
||||||
lib.attrsets.mapAttrs (n: v: recurse (path ++ [ n ]) v) val
|
|
||||||
else if builtins.isList val && cond val then
|
|
||||||
lib.lists.imap0 (i: v: recurse (path ++ [ (builtins.toString i) ]) v) val
|
|
||||||
else
|
|
||||||
f path val;
|
|
||||||
in
|
|
||||||
recurse [ ] set;
|
|
||||||
|
|
||||||
# Like lib.attrsets.collect but also recurses on lists.
|
sedPatterns = concatMapStringsSep " " (pattern: "-e \"s|${pattern.name}|${pattern.value}|\"") (
|
||||||
collect =
|
map genReplacement replacements
|
||||||
# Given an attribute's value, determine if recursion should stop.
|
);
|
||||||
pred:
|
|
||||||
# The attribute set to recursively collect.
|
|
||||||
attrs:
|
|
||||||
if pred attrs then
|
|
||||||
[ attrs ]
|
|
||||||
else if builtins.isAttrs attrs then
|
|
||||||
lib.lists.concatMap (collect pred) (lib.attrsets.attrValues attrs)
|
|
||||||
else if builtins.isList attrs then
|
|
||||||
lib.lists.concatMap (collect pred) attrs
|
|
||||||
else
|
|
||||||
[ ];
|
|
||||||
|
|
||||||
indent =
|
sedCmd = if replacements == [ ] then "cat" else "${pkgs.gnused}/bin/sed ${sedPatterns}";
|
||||||
i: str:
|
in
|
||||||
lib.concatMapStringsSep "\n" (x: (lib.strings.replicate i " ") + x) (lib.splitString "\n" str);
|
''
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
# Generator for XML
|
${checkPermissions}
|
||||||
formatXML =
|
|
||||||
{
|
|
||||||
enclosingRoot ? null,
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
type =
|
|
||||||
with lib.types;
|
|
||||||
let
|
|
||||||
valueType =
|
|
||||||
nullOr (oneOf [
|
|
||||||
bool
|
|
||||||
int
|
|
||||||
float
|
|
||||||
str
|
|
||||||
path
|
|
||||||
(attrsOf valueType)
|
|
||||||
(listOf valueType)
|
|
||||||
])
|
|
||||||
// {
|
|
||||||
description = "XML value";
|
|
||||||
};
|
|
||||||
in
|
|
||||||
valueType;
|
|
||||||
|
|
||||||
generate =
|
mkdir -p $(dirname ${templatePath})
|
||||||
name: value:
|
ln -fs ${file} ${templatePath}
|
||||||
pkgs.callPackage (
|
rm -f ${resultPath}
|
||||||
{ runCommand, python3 }:
|
touch ${resultPath}
|
||||||
runCommand "config"
|
''
|
||||||
{
|
+ (lib.optionalString (user != null) ''
|
||||||
value = builtins.toJSON (if enclosingRoot == null then value else { ${enclosingRoot} = value; });
|
chown ${user} ${resultPath}
|
||||||
passAsFile = [ "value" ];
|
'')
|
||||||
}
|
+ ''
|
||||||
(
|
${sedCmd} ${templatePath} > ${resultPath}
|
||||||
pkgs.writers.writePython3 "dict2xml"
|
chmod ${permissions} ${resultPath}
|
||||||
{
|
'';
|
||||||
libraries = with python3.pkgs; [
|
|
||||||
python
|
|
||||||
dict2xml
|
|
||||||
];
|
|
||||||
}
|
|
||||||
''
|
|
||||||
import os
|
|
||||||
import json
|
|
||||||
from dict2xml import dict2xml
|
|
||||||
|
|
||||||
with open(os.environ["valuePath"]) as f:
|
|
||||||
content = json.loads(f.read())
|
|
||||||
if content is None:
|
|
||||||
print("Could not parse env var valuePath as json")
|
|
||||||
os.exit(2)
|
|
||||||
with open(os.environ["out"], "w") as out:
|
|
||||||
out.write(dict2xml(content))
|
|
||||||
''
|
|
||||||
)
|
|
||||||
) { };
|
|
||||||
|
|
||||||
|
secretFileType = lib.types.submodule {
|
||||||
|
options = {
|
||||||
|
source = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
description = "File containing the value.";
|
||||||
};
|
};
|
||||||
|
|
||||||
parseXML =
|
transform = lib.mkOption {
|
||||||
xml:
|
type = lib.types.raw;
|
||||||
let
|
description = "An optional function to transform the secret.";
|
||||||
xmlToJsonFile = pkgs.callPackage (
|
default = null;
|
||||||
|
example = lib.literalExpression ''
|
||||||
|
v: "prefix-$${v}-suffix"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
secretName =
|
||||||
|
names: "%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) names)}%";
|
||||||
|
|
||||||
|
withReplacements =
|
||||||
|
attrs:
|
||||||
|
let
|
||||||
|
valueOrReplacement =
|
||||||
|
name: value: if !(builtins.isAttrs value && value ? "source") then value else secretName name;
|
||||||
|
in
|
||||||
|
mapAttrsRecursiveCond (v: !v ? "source") valueOrReplacement attrs;
|
||||||
|
|
||||||
|
getReplacements =
|
||||||
|
attrs:
|
||||||
|
let
|
||||||
|
addNameField =
|
||||||
|
name: value:
|
||||||
|
if !(builtins.isAttrs value && value ? "source") then value else value // { name = name; };
|
||||||
|
|
||||||
|
secretsWithName = mapAttrsRecursiveCond (v: !v ? "source") addNameField attrs;
|
||||||
|
in
|
||||||
|
collect (v: builtins.isAttrs v && v ? "source") secretsWithName;
|
||||||
|
|
||||||
|
# Inspired lib.attrsets.mapAttrsRecursiveCond but also recurses on lists.
|
||||||
|
mapAttrsRecursiveCond =
|
||||||
|
# A function, given the attribute set the recursion is currently at, determine if to recurse deeper into that attribute set.
|
||||||
|
cond:
|
||||||
|
# A function, given a list of attribute names and a value, returns a new value.
|
||||||
|
f:
|
||||||
|
# Attribute set or list to recursively map over.
|
||||||
|
set:
|
||||||
|
let
|
||||||
|
recurse =
|
||||||
|
path: val:
|
||||||
|
if builtins.isAttrs val && cond val then
|
||||||
|
lib.attrsets.mapAttrs (n: v: recurse (path ++ [ n ]) v) val
|
||||||
|
else if builtins.isList val && cond val then
|
||||||
|
lib.lists.imap0 (i: v: recurse (path ++ [ (builtins.toString i) ]) v) val
|
||||||
|
else
|
||||||
|
f path val;
|
||||||
|
in
|
||||||
|
recurse [ ] set;
|
||||||
|
|
||||||
|
# Like lib.attrsets.collect but also recurses on lists.
|
||||||
|
collect =
|
||||||
|
# Given an attribute's value, determine if recursion should stop.
|
||||||
|
pred:
|
||||||
|
# The attribute set to recursively collect.
|
||||||
|
attrs:
|
||||||
|
if pred attrs then
|
||||||
|
[ attrs ]
|
||||||
|
else if builtins.isAttrs attrs then
|
||||||
|
lib.lists.concatMap (collect pred) (lib.attrsets.attrValues attrs)
|
||||||
|
else if builtins.isList attrs then
|
||||||
|
lib.lists.concatMap (collect pred) attrs
|
||||||
|
else
|
||||||
|
[ ];
|
||||||
|
|
||||||
|
indent =
|
||||||
|
i: str:
|
||||||
|
lib.concatMapStringsSep "\n" (x: (lib.strings.replicate i " ") + x) (lib.splitString "\n" str);
|
||||||
|
|
||||||
|
# Generator for XML
|
||||||
|
formatXML =
|
||||||
|
{
|
||||||
|
enclosingRoot ? null,
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
type =
|
||||||
|
with lib.types;
|
||||||
|
let
|
||||||
|
valueType =
|
||||||
|
nullOr (oneOf [
|
||||||
|
bool
|
||||||
|
int
|
||||||
|
float
|
||||||
|
str
|
||||||
|
path
|
||||||
|
(attrsOf valueType)
|
||||||
|
(listOf valueType)
|
||||||
|
])
|
||||||
|
// {
|
||||||
|
description = "XML value";
|
||||||
|
};
|
||||||
|
in
|
||||||
|
valueType;
|
||||||
|
|
||||||
|
generate =
|
||||||
|
name: value:
|
||||||
|
pkgs.callPackage (
|
||||||
{ runCommand, python3 }:
|
{ runCommand, python3 }:
|
||||||
runCommand "config"
|
runCommand "config"
|
||||||
{
|
{
|
||||||
inherit xml;
|
value = builtins.toJSON (if enclosingRoot == null then value else { ${enclosingRoot} = value; });
|
||||||
passAsFile = [ "xml" ];
|
passAsFile = [ "value" ];
|
||||||
}
|
}
|
||||||
(
|
(
|
||||||
pkgs.writers.writePython3 "xml2json"
|
pkgs.writers.writePython3 "dict2xml"
|
||||||
{
|
{
|
||||||
libraries = with python3.pkgs; [ python ];
|
libraries = with python3.pkgs; [
|
||||||
|
python
|
||||||
|
dict2xml
|
||||||
|
];
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
from collections import ChainMap
|
from dict2xml import dict2xml
|
||||||
from xml.etree import ElementTree
|
|
||||||
|
|
||||||
|
|
||||||
def xml_to_dict_recursive(root):
|
|
||||||
all_descendants = list(root)
|
|
||||||
if len(all_descendants) == 0:
|
|
||||||
return {root.tag: root.text}
|
|
||||||
else:
|
|
||||||
merged_dict = ChainMap(*map(xml_to_dict_recursive, all_descendants))
|
|
||||||
return {root.tag: dict(merged_dict)}
|
|
||||||
|
|
||||||
|
|
||||||
with open(os.environ["xmlPath"]) as f:
|
|
||||||
root = ElementTree.XML(f.read())
|
|
||||||
xml = xml_to_dict_recursive(root)
|
|
||||||
j = json.dumps(xml)
|
|
||||||
|
|
||||||
|
with open(os.environ["valuePath"]) as f:
|
||||||
|
content = json.loads(f.read())
|
||||||
|
if content is None:
|
||||||
|
print("Could not parse env var valuePath as json")
|
||||||
|
os.exit(2)
|
||||||
with open(os.environ["out"], "w") as out:
|
with open(os.environ["out"], "w") as out:
|
||||||
out.write(j)
|
out.write(dict2xml(content))
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
) { };
|
) { };
|
||||||
in
|
|
||||||
builtins.fromJSON (builtins.readFile xmlToJsonFile);
|
|
||||||
|
|
||||||
renameAttrName =
|
};
|
||||||
attrset: from: to:
|
|
||||||
(lib.attrsets.filterAttrs (name: v: name == from) attrset)
|
|
||||||
// {
|
|
||||||
${to} = attrset.${from};
|
|
||||||
};
|
|
||||||
|
|
||||||
# Taken from https://github.com/antifuchs/nix-flake-tests/blob/main/default.nix
|
parseXML =
|
||||||
# with a nicer diff display function.
|
xml:
|
||||||
check =
|
let
|
||||||
{ pkgs, tests }:
|
xmlToJsonFile = pkgs.callPackage (
|
||||||
let
|
{ runCommand, python3 }:
|
||||||
formatValue =
|
runCommand "config"
|
||||||
val:
|
|
||||||
if (builtins.isList val || builtins.isAttrs val) then
|
|
||||||
builtins.toJSON val
|
|
||||||
else
|
|
||||||
builtins.toString val;
|
|
||||||
|
|
||||||
resultToString =
|
|
||||||
{
|
{
|
||||||
name,
|
inherit xml;
|
||||||
expected,
|
passAsFile = [ "xml" ];
|
||||||
result,
|
}
|
||||||
}:
|
(
|
||||||
builtins.readFile (
|
pkgs.writers.writePython3 "xml2json"
|
||||||
pkgs.runCommand "nix-flake-tests-error"
|
|
||||||
{
|
{
|
||||||
expected = formatValue expected;
|
libraries = with python3.pkgs; [ python ];
|
||||||
result = formatValue result;
|
|
||||||
passAsFile = [
|
|
||||||
"expected"
|
|
||||||
"result"
|
|
||||||
];
|
|
||||||
|
|
||||||
nativeBuildInputs = [
|
|
||||||
(pkgs.python3.withPackages (ps: [ ps.deepdiff ] ++ ps.deepdiff.optional-dependencies.cli))
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
echo "${name} failed (- expected, + result)" > $out
|
import os
|
||||||
cp ''${expectedPath} ''${expectedPath}.json
|
import json
|
||||||
cp ''${resultPath} ''${resultPath}.json
|
from collections import ChainMap
|
||||||
deep diff ''${expectedPath}.json ''${resultPath}.json >> $out
|
from xml.etree import ElementTree
|
||||||
|
|
||||||
|
|
||||||
|
def xml_to_dict_recursive(root):
|
||||||
|
all_descendants = list(root)
|
||||||
|
if len(all_descendants) == 0:
|
||||||
|
return {root.tag: root.text}
|
||||||
|
else:
|
||||||
|
merged_dict = ChainMap(*map(xml_to_dict_recursive, all_descendants))
|
||||||
|
return {root.tag: dict(merged_dict)}
|
||||||
|
|
||||||
|
|
||||||
|
with open(os.environ["xmlPath"]) as f:
|
||||||
|
root = ElementTree.XML(f.read())
|
||||||
|
xml = xml_to_dict_recursive(root)
|
||||||
|
j = json.dumps(xml)
|
||||||
|
|
||||||
|
with open(os.environ["out"], "w") as out:
|
||||||
|
out.write(j)
|
||||||
''
|
''
|
||||||
);
|
)
|
||||||
|
|
||||||
results = pkgs.lib.runTests tests;
|
|
||||||
in
|
|
||||||
if results != [ ] then
|
|
||||||
builtins.throw (concatStringsSep "\n" (map resultToString (lib.traceValSeq results)))
|
|
||||||
else
|
|
||||||
pkgs.runCommand "nix-flake-tests-success" { } "echo > $out";
|
|
||||||
|
|
||||||
genConfigOutOfBandSystemd =
|
|
||||||
{
|
|
||||||
config,
|
|
||||||
configLocation,
|
|
||||||
generator,
|
|
||||||
user ? null,
|
|
||||||
permissions ? "u=r,g=r,o=",
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
loadCredentials = getLoadCredentials "source" config;
|
|
||||||
preStart = lib.mkBefore (replaceSecrets {
|
|
||||||
userConfig = updateToLoadCredentials "source" "$CREDENTIALS_DIRECTORY" config;
|
|
||||||
resultPath = configLocation;
|
|
||||||
inherit generator;
|
|
||||||
inherit user permissions;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
updateToLoadCredentials =
|
|
||||||
sourceField: rootDir: attrs:
|
|
||||||
let
|
|
||||||
hasPlaceholderField = v: isAttrs v && hasAttr sourceField v;
|
|
||||||
|
|
||||||
valueOrLoadCredential =
|
|
||||||
path: value:
|
|
||||||
if !(hasPlaceholderField value) then
|
|
||||||
value
|
|
||||||
else
|
|
||||||
value // { ${sourceField} = rootDir + "/" + concatStringsSep "_" path; };
|
|
||||||
in
|
|
||||||
mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) valueOrLoadCredential attrs;
|
|
||||||
|
|
||||||
getLoadCredentials =
|
|
||||||
sourceField: attrs:
|
|
||||||
let
|
|
||||||
hasPlaceholderField = v: isAttrs v && hasAttr sourceField v;
|
|
||||||
|
|
||||||
addPathField =
|
|
||||||
path: value: if !(hasPlaceholderField value) then value else value // { inherit path; };
|
|
||||||
|
|
||||||
secretsWithPath = mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) addPathField attrs;
|
|
||||||
|
|
||||||
allSecrets = collect (v: hasPlaceholderField v) secretsWithPath;
|
|
||||||
|
|
||||||
genLoadCredentials = secret: "${concatStringsSep "_" secret.path}:${secret.${sourceField}}";
|
|
||||||
in
|
|
||||||
map genLoadCredentials allSecrets;
|
|
||||||
|
|
||||||
anyNotNull = any (x: x != null);
|
|
||||||
|
|
||||||
mkJellyfinPlugin =
|
|
||||||
{
|
|
||||||
pname,
|
|
||||||
version,
|
|
||||||
hash,
|
|
||||||
url,
|
|
||||||
}:
|
|
||||||
pkgs.callPackage (
|
|
||||||
{ stdenv, fetchzip }:
|
|
||||||
stdenv.mkDerivation (finalAttrs: {
|
|
||||||
inherit pname version;
|
|
||||||
|
|
||||||
src = fetchzip {
|
|
||||||
inherit url hash;
|
|
||||||
stripRoot = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
dontBuild = true;
|
|
||||||
|
|
||||||
installPhase = ''
|
|
||||||
mkdir $out
|
|
||||||
cp -r . $out
|
|
||||||
'';
|
|
||||||
})
|
|
||||||
) { };
|
) { };
|
||||||
|
in
|
||||||
|
builtins.fromJSON (builtins.readFile xmlToJsonFile);
|
||||||
|
|
||||||
update =
|
renameAttrName =
|
||||||
attr: fn: attrset:
|
attrset: from: to:
|
||||||
attrset // { ${attr} = fn attrset.${attr}; };
|
(lib.attrsets.filterAttrs (name: v: name == from) attrset)
|
||||||
|
// {
|
||||||
|
${to} = attrset.${from};
|
||||||
|
};
|
||||||
|
|
||||||
};
|
# Taken from https://github.com/antifuchs/nix-flake-tests/blob/main/default.nix
|
||||||
in
|
# with a nicer diff display function.
|
||||||
shb
|
check =
|
||||||
// {
|
{ pkgs, tests }:
|
||||||
homepage = pkgs.callPackage ./homepage.nix { inherit shb; };
|
let
|
||||||
|
formatValue =
|
||||||
|
val:
|
||||||
|
if (builtins.isList val || builtins.isAttrs val) then
|
||||||
|
builtins.toJSON val
|
||||||
|
else
|
||||||
|
builtins.toString val;
|
||||||
|
|
||||||
|
resultToString =
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
expected,
|
||||||
|
result,
|
||||||
|
}:
|
||||||
|
builtins.readFile (
|
||||||
|
pkgs.runCommand "nix-flake-tests-error"
|
||||||
|
{
|
||||||
|
expected = formatValue expected;
|
||||||
|
result = formatValue result;
|
||||||
|
passAsFile = [
|
||||||
|
"expected"
|
||||||
|
"result"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
echo "${name} failed (- expected, + result)" > $out
|
||||||
|
cp ''${expectedPath} ''${expectedPath}.json
|
||||||
|
cp ''${resultPath} ''${resultPath}.json
|
||||||
|
${pkgs.deepdiff}/bin/deep diff ''${expectedPath}.json ''${resultPath}.json >> $out
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
|
results = pkgs.lib.runTests tests;
|
||||||
|
in
|
||||||
|
if results != [ ] then
|
||||||
|
builtins.throw (concatStringsSep "\n" (map resultToString (lib.traceValSeq results)))
|
||||||
|
else
|
||||||
|
pkgs.runCommand "nix-flake-tests-success" { } "echo > $out";
|
||||||
|
|
||||||
|
genConfigOutOfBandSystemd =
|
||||||
|
{
|
||||||
|
config,
|
||||||
|
configLocation,
|
||||||
|
generator,
|
||||||
|
user ? null,
|
||||||
|
permissions ? "u=r,g=r,o=",
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
loadCredentials = getLoadCredentials "source" config;
|
||||||
|
preStart = lib.mkBefore (replaceSecrets {
|
||||||
|
userConfig = updateToLoadCredentials "source" "$CREDENTIALS_DIRECTORY" config;
|
||||||
|
resultPath = configLocation;
|
||||||
|
inherit generator;
|
||||||
|
inherit user permissions;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
updateToLoadCredentials =
|
||||||
|
sourceField: rootDir: attrs:
|
||||||
|
let
|
||||||
|
hasPlaceholderField = v: isAttrs v && hasAttr sourceField v;
|
||||||
|
|
||||||
|
valueOrLoadCredential =
|
||||||
|
path: value:
|
||||||
|
if !(hasPlaceholderField value) then
|
||||||
|
value
|
||||||
|
else
|
||||||
|
value // { ${sourceField} = rootDir + "/" + concatStringsSep "_" path; };
|
||||||
|
in
|
||||||
|
mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) valueOrLoadCredential attrs;
|
||||||
|
|
||||||
|
getLoadCredentials =
|
||||||
|
sourceField: attrs:
|
||||||
|
let
|
||||||
|
hasPlaceholderField = v: isAttrs v && hasAttr sourceField v;
|
||||||
|
|
||||||
|
addPathField =
|
||||||
|
path: value: if !(hasPlaceholderField value) then value else value // { inherit path; };
|
||||||
|
|
||||||
|
secretsWithPath = mapAttrsRecursiveCond (v: !(hasPlaceholderField v)) addPathField attrs;
|
||||||
|
|
||||||
|
allSecrets = collect (v: hasPlaceholderField v) secretsWithPath;
|
||||||
|
|
||||||
|
genLoadCredentials = secret: "${concatStringsSep "_" secret.path}:${secret.${sourceField}}";
|
||||||
|
in
|
||||||
|
map genLoadCredentials allSecrets;
|
||||||
|
|
||||||
|
anyNotNull = any (x: x != null);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,92 +0,0 @@
|
||||||
{ lib, shb }:
|
|
||||||
let
|
|
||||||
sort =
|
|
||||||
attr: vs:
|
|
||||||
map (v: { ${v.name} = v.${attr}; }) (
|
|
||||||
lib.sortOn (v: v.sortOrder) (lib.mapAttrsToList (n: v: v // { name = n; }) vs)
|
|
||||||
);
|
|
||||||
|
|
||||||
slufigy = builtins.replaceStrings [ "-" ] [ "_" ];
|
|
||||||
|
|
||||||
mkService =
|
|
||||||
groupName: serviceName:
|
|
||||||
{
|
|
||||||
request,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
apiKey: settings:
|
|
||||||
lib.recursiveUpdate (
|
|
||||||
{
|
|
||||||
href = request.externalUrl;
|
|
||||||
siteMonitor = if (request.internalUrl == null) then null else request.internalUrl;
|
|
||||||
icon = "sh-${lib.toLower serviceName}";
|
|
||||||
}
|
|
||||||
// lib.optionalAttrs (apiKey != null) {
|
|
||||||
widget = {
|
|
||||||
# Duplicating because widgets call the api key various names
|
|
||||||
# and duplicating is a hacky but easy solution.
|
|
||||||
key = "{{HOMEPAGE_FILE_${slufigy groupName}_${slufigy serviceName}}}";
|
|
||||||
password = "{{HOMEPAGE_FILE_${slufigy groupName}_${slufigy serviceName}}}";
|
|
||||||
type = lib.toLower serviceName;
|
|
||||||
url = if (request.internalUrl != null) then request.internalUrl else request.externalUrl;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
) settings;
|
|
||||||
|
|
||||||
asServiceGroup =
|
|
||||||
cfg:
|
|
||||||
sort "services" (
|
|
||||||
lib.mapAttrs (
|
|
||||||
groupName: groupCfg:
|
|
||||||
shb.update "services" (
|
|
||||||
services:
|
|
||||||
sort "dashboard" (
|
|
||||||
lib.mapAttrs (
|
|
||||||
serviceName: serviceCfg:
|
|
||||||
shb.update "dashboard" (
|
|
||||||
dashboard:
|
|
||||||
(mkService groupName serviceName) dashboard serviceCfg.apiKey (serviceCfg.settings or { })
|
|
||||||
) serviceCfg
|
|
||||||
) services
|
|
||||||
)
|
|
||||||
) groupCfg
|
|
||||||
) cfg
|
|
||||||
);
|
|
||||||
|
|
||||||
allKeys =
|
|
||||||
cfg:
|
|
||||||
let
|
|
||||||
flat = lib.flatten (
|
|
||||||
lib.mapAttrsToList (
|
|
||||||
groupName: groupCfg:
|
|
||||||
lib.mapAttrsToList (
|
|
||||||
serviceName: serviceCfg:
|
|
||||||
lib.optionalAttrs (serviceCfg.apiKey != null) {
|
|
||||||
inherit serviceName groupName;
|
|
||||||
inherit (serviceCfg.apiKey.result) path;
|
|
||||||
}
|
|
||||||
) groupCfg.services
|
|
||||||
) cfg
|
|
||||||
);
|
|
||||||
|
|
||||||
flatWithApiKey = builtins.filter (v: v != { }) flat;
|
|
||||||
in
|
|
||||||
builtins.listToAttrs (
|
|
||||||
map (
|
|
||||||
{
|
|
||||||
groupName,
|
|
||||||
serviceName,
|
|
||||||
path,
|
|
||||||
}:
|
|
||||||
lib.nameValuePair "${slufigy groupName}_${slufigy serviceName}" path
|
|
||||||
) flatWithApiKey
|
|
||||||
);
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit
|
|
||||||
allKeys
|
|
||||||
asServiceGroup
|
|
||||||
mkService
|
|
||||||
sort
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
@ -89,7 +89,7 @@ in
|
||||||
options = shb.contracts.secret.mkRequester {
|
options = shb.contracts.secret.mkRequester {
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
owner = cfg.autheliaUser;
|
owner = cfg.autheliaUser;
|
||||||
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}.service" ];
|
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -99,7 +99,7 @@ in
|
||||||
options = shb.contracts.secret.mkRequester {
|
options = shb.contracts.secret.mkRequester {
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
owner = cfg.autheliaUser;
|
owner = cfg.autheliaUser;
|
||||||
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}.service" ];
|
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -109,27 +109,27 @@ in
|
||||||
options = shb.contracts.secret.mkRequester {
|
options = shb.contracts.secret.mkRequester {
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
owner = cfg.autheliaUser;
|
owner = cfg.autheliaUser;
|
||||||
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}.service" ];
|
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
storageEncryptionKey = lib.mkOption {
|
storageEncryptionKey = lib.mkOption {
|
||||||
description = "Storage encryption key. Must be >= 20 characters.";
|
description = "Storage encryption key.";
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = shb.contracts.secret.mkRequester {
|
options = shb.contracts.secret.mkRequester {
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
owner = cfg.autheliaUser;
|
owner = cfg.autheliaUser;
|
||||||
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}.service" ];
|
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
identityProvidersOIDCHMACSecret = lib.mkOption {
|
identityProvidersOIDCHMACSecret = lib.mkOption {
|
||||||
description = "Identity provider OIDC HMAC secret. Must be >= 40 characters.";
|
description = "Identity provider OIDC HMAC secret.";
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = shb.contracts.secret.mkRequester {
|
options = shb.contracts.secret.mkRequester {
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
owner = cfg.autheliaUser;
|
owner = cfg.autheliaUser;
|
||||||
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}.service" ];
|
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -143,7 +143,7 @@ in
|
||||||
options = shb.contracts.secret.mkRequester {
|
options = shb.contracts.secret.mkRequester {
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
owner = cfg.autheliaUser;
|
owner = cfg.autheliaUser;
|
||||||
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}.service" ];
|
restartUnits = [ "authelia-${opt.subdomain}.${opt.domain}" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -295,15 +295,6 @@ in
|
||||||
description = "SMTP name from which the emails originate.";
|
description = "SMTP name from which the emails originate.";
|
||||||
default = "Authelia";
|
default = "Authelia";
|
||||||
};
|
};
|
||||||
scheme = lib.mkOption {
|
|
||||||
description = "The protocl must be smtp, submission, or submissions. The only difference between these schemes are the default ports and submissions requires a TLS transport per SMTP Ports Security Measures, whereas submission and smtp use a standard TCP transport and typically enforce StartTLS.";
|
|
||||||
type = lib.types.enum [
|
|
||||||
"smtp"
|
|
||||||
"submission"
|
|
||||||
"submissions"
|
|
||||||
];
|
|
||||||
default = "smtp";
|
|
||||||
};
|
|
||||||
host = lib.mkOption {
|
host = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "SMTP host to send the emails to.";
|
description = "SMTP host to send the emails to.";
|
||||||
|
|
@ -323,7 +314,7 @@ in
|
||||||
options = shb.contracts.secret.mkRequester {
|
options = shb.contracts.secret.mkRequester {
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
owner = cfg.autheliaUser;
|
owner = cfg.autheliaUser;
|
||||||
restartUnits = [ "authelia-${fqdn}.service" ];
|
restartUnits = [ "authelia-${fqdn}" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -390,20 +381,6 @@ in
|
||||||
to see exactly what Authelia receives and sends back.
|
to see exactly what Authelia receives and sends back.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
dashboard = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Dashboard contract consumer
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.dashboard.mkRequester {
|
|
||||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
|
||||||
externalUrlText = "https://\${config.shb.authelia.subdomain}.\${config.shb.authelia.domain}";
|
|
||||||
internalUrl = "http://127.0.0.1:${toString listenPort}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
|
|
@ -434,19 +411,20 @@ in
|
||||||
secrets = {
|
secrets = {
|
||||||
jwtSecretFile = cfg.secrets.jwtSecret.result.path;
|
jwtSecretFile = cfg.secrets.jwtSecret.result.path;
|
||||||
storageEncryptionKeyFile = cfg.secrets.storageEncryptionKey.result.path;
|
storageEncryptionKeyFile = cfg.secrets.storageEncryptionKey.result.path;
|
||||||
sessionSecretFile = cfg.secrets.sessionSecret.result.path;
|
|
||||||
oidcIssuerPrivateKeyFile = cfg.secrets.identityProvidersOIDCIssuerPrivateKey.result.path;
|
|
||||||
oidcHmacSecretFile = cfg.secrets.identityProvidersOIDCHMACSecret.result.path;
|
|
||||||
};
|
};
|
||||||
# See https://www.authelia.com/configuration/methods/secrets/
|
# See https://www.authelia.com/configuration/methods/secrets/
|
||||||
environmentVariables = {
|
environmentVariables = {
|
||||||
AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE = toString cfg.secrets.ldapAdminPassword.result.path;
|
AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE = toString cfg.secrets.ldapAdminPassword.result.path;
|
||||||
|
AUTHELIA_SESSION_SECRET_FILE = toString cfg.secrets.sessionSecret.result.path;
|
||||||
# Not needed since we use peer auth.
|
# Not needed since we use peer auth.
|
||||||
# AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE = "/run/secrets/authelia/postgres_password";
|
# AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE = "/run/secrets/authelia/postgres_password";
|
||||||
|
AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE = toString cfg.secrets.storageEncryptionKey.result.path;
|
||||||
|
AUTHELIA_IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE = toString cfg.secrets.identityProvidersOIDCHMACSecret.result.path;
|
||||||
|
AUTHELIA_IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE = toString cfg.secrets.identityProvidersOIDCIssuerPrivateKey.result.path;
|
||||||
|
|
||||||
AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE = lib.mkIf (!(builtins.isString cfg.smtp)) (
|
AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE = lib.mkIf (!(builtins.isString cfg.smtp)) (
|
||||||
toString cfg.smtp.password.result.path
|
toString cfg.smtp.password.result.path
|
||||||
);
|
);
|
||||||
X_AUTHELIA_CONFIG_FILTERS = "template";
|
|
||||||
};
|
};
|
||||||
settings = {
|
settings = {
|
||||||
server.address = "tcp://127.0.0.1:${toString listenPort}";
|
server.address = "tcp://127.0.0.1:${toString listenPort}";
|
||||||
|
|
@ -508,7 +486,8 @@ in
|
||||||
filename = cfg.smtp;
|
filename = cfg.smtp;
|
||||||
};
|
};
|
||||||
smtp = lib.mkIf (!(builtins.isString cfg.smtp)) {
|
smtp = lib.mkIf (!(builtins.isString cfg.smtp)) {
|
||||||
address = "${cfg.smtp.scheme}://${cfg.smtp.host}:${toString cfg.smtp.port}";
|
host = cfg.smtp.host;
|
||||||
|
port = cfg.smtp.port;
|
||||||
username = cfg.smtp.username;
|
username = cfg.smtp.username;
|
||||||
sender = "${cfg.smtp.from_name} <${cfg.smtp.from_address}>";
|
sender = "${cfg.smtp.from_name} <${cfg.smtp.from_address}>";
|
||||||
subject = "[Authelia] {title}";
|
subject = "[Authelia] {title}";
|
||||||
|
|
@ -645,7 +624,6 @@ in
|
||||||
shb.mitmdump.instances."authelia-${fqdn}" = lib.mkIf cfg.debug {
|
shb.mitmdump.instances."authelia-${fqdn}" = lib.mkIf cfg.debug {
|
||||||
listenPort = 9091;
|
listenPort = 9091;
|
||||||
upstreamPort = 9090;
|
upstreamPort = 9090;
|
||||||
timeout = 30;
|
|
||||||
after = [ "authelia-${fqdn}.service" ];
|
after = [ "authelia-${fqdn}.service" ];
|
||||||
enabledAddons = [ config.shb.mitmdump.addons.logger ];
|
enabledAddons = [ config.shb.mitmdump.addons.logger ];
|
||||||
extraArgs = [
|
extraArgs = [
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,10 @@ This block sets up an [Authelia][] service for Single-Sign On integration.
|
||||||
|
|
||||||
Compared to the upstream nixpkgs module, this module is tightly integrated
|
Compared to the upstream nixpkgs module, this module is tightly integrated
|
||||||
with SHB which allows easy configuration of SSO with [OIDC integration](#blocks-authelia-shb-oidc)
|
with SHB which allows easy configuration of SSO with [OIDC integration](#blocks-authelia-shb-oidc)
|
||||||
|
or with [forward auth integration](#blocks-authelia-shb-forward-auth)
|
||||||
as well as some extensive [troubleshooting](#blocks-authelia-troubleshooting) features.
|
as well as some extensive [troubleshooting](#blocks-authelia-troubleshooting) features.
|
||||||
|
|
||||||
Note that forward authentication is configured with the [nginx block](blocks-nginx.html#blocks-nginx-usage-shbforwardauth).
|
## Global Setup {#blocks-authelia-global-setup}
|
||||||
|
|
||||||
## Features {#services-authelia-features}
|
|
||||||
|
|
||||||
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-authelia-usage-applicationdashboard)
|
|
||||||
|
|
||||||
## Usage {#services-authelia-usage}
|
|
||||||
|
|
||||||
### Initial Configuration {#blocks-authelia-usage-configuration}
|
|
||||||
|
|
||||||
Authelia cannot work without SSL and LDAP.
|
Authelia cannot work without SSL and LDAP.
|
||||||
So setting up the Authelia block requires to setup the [SSL block][] first
|
So setting up the Authelia block requires to setup the [SSL block][] first
|
||||||
|
|
@ -50,31 +43,31 @@ shb.authelia = {
|
||||||
port = 587;
|
port = 587;
|
||||||
username = "postmaster@mg.example.com";
|
username = "postmaster@mg.example.com";
|
||||||
from_address = "authelia@example.com";
|
from_address = "authelia@example.com";
|
||||||
password.result = config.shb.sops.secret."authelia/smtp_password".result;
|
password.result = config.shb.sops.secrets."authelia/smtp_password".result;
|
||||||
};
|
};
|
||||||
|
|
||||||
secrets = {
|
secrets = {
|
||||||
jwtSecret.result = config.shb.sops.secret."authelia/jwt_secret".result;
|
jwtSecret.result = config.shb.sops.secrets."authelia/jwt_secret".result;
|
||||||
ldapAdminPassword.result = config.shb.sops.secret."authelia/ldap_admin_password".result;
|
ldapAdminPassword.result = config.shb.sops.secrets."authelia/ldap_admin_password".result;
|
||||||
sessionSecret.result = config.shb.sops.secret."authelia/session_secret".result;
|
sessionSecret.result = config.shb.sops.secrets."authelia/session_secret".result;
|
||||||
storageEncryptionKey.result = config.shb.sops.secret."authelia/storage_encryption_key".result;
|
storageEncryptionKey.result = config.shb.sops.secrets."authelia/storage_encryption_key".result;
|
||||||
identityProvidersOIDCHMACSecret.result = config.shb.sops.secret."authelia/hmac_secret".result;
|
identityProvidersOIDCHMACSecret.result = config.shb.sops.secrets."authelia/hmac_secret".result;
|
||||||
identityProvidersOIDCIssuerPrivateKey.result = config.shb.sops.secret."authelia/private_key".result;
|
identityProvidersOIDCIssuerPrivateKey.result = config.shb.sops.secrets."authelia/private_key".result;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.certs.certs.letsencrypt."example.com".extraDomains = [ "auth.example.com" ];
|
shb.certs.certs.letsencrypt."example.com".extraDomains = [ "auth.example.com" ];
|
||||||
|
|
||||||
shb.sops.secret."authelia/jwt_secret".request = config.shb.authelia.secrets.jwtSecret.request;
|
shb.sops.secrets."authelia/jwt_secret".request = config.shb.authelia.secrets.jwtSecret.request;
|
||||||
shb.sops.secret."authelia/ldap_admin_password" = {
|
shb.sops.secrets."authelia/ldap_admin_password" = {
|
||||||
request = config.shb.authelia.secrets.ldapAdminPassword.request;
|
request = config.shb.authelia.secrets.ldapAdminPassword.request;
|
||||||
settings.key = "lldap/user_password";
|
settings.key = "lldap/user_password";
|
||||||
};
|
};
|
||||||
shb.sops.secret."authelia/session_secret".request = config.shb.authelia.secrets.sessionSecret.request;
|
shb.sops.secrets."authelia/session_secret".request = config.shb.authelia.secrets.sessionSecret.request;
|
||||||
shb.sops.secret."authelia/storage_encryption_key".request = config.shb.authelia.secrets.storageEncryptionKey.request;
|
shb.sops.secrets."authelia/storage_encryption_key".request = config.shb.authelia.secrets.storageEncryptionKey.request;
|
||||||
shb.sops.secret."authelia/hmac_secret".request = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request;
|
shb.sops.secrets."authelia/hmac_secret".request = config.shb.authelia.secrets.identityProvidersOIDCHMACSecret.request;
|
||||||
shb.sops.secret."authelia/private_key".request = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request;
|
shb.sops.secrets."authelia/private_key".request = config.shb.authelia.secrets.identityProvidersOIDCIssuerPrivateKey.request;
|
||||||
shb.sops.secret."authelia/smtp_password".request = config.shb.authelia.smtp.password.request;
|
shb.sops.secrets."authelia/smtp_password".request = config.shb.authelia.smtp.password.request;
|
||||||
```
|
```
|
||||||
|
|
||||||
This assumes secrets are setup with SOPS
|
This assumes secrets are setup with SOPS
|
||||||
|
|
@ -86,22 +79,6 @@ Crucially, the `shb.authelia.secrets.ldapAdminPasswordFile` must be the same
|
||||||
as the `shb.lldap.ldapUserPassword` defined for the [LLDAP block][].
|
as the `shb.lldap.ldapUserPassword` defined for the [LLDAP block][].
|
||||||
This is done using Sops' `key` option.
|
This is done using Sops' `key` option.
|
||||||
|
|
||||||
### Application Dashboard {#services-authelia-usage-applicationdashboard}
|
|
||||||
|
|
||||||
Integration with the [dashboard contract](contracts-dashboard.html) is provided
|
|
||||||
by the [dashboard option](#blocks-authelia-options-shb.authelia.dashboard).
|
|
||||||
|
|
||||||
For example using the [Homepage](services-homepage.html) service:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.homepage.servicesGroups.Admin.services.Authelia = {
|
|
||||||
sortOrder = 2;
|
|
||||||
dashboard.request = config.shb.authelia.dashboard.request;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## SHB OIDC integration {#blocks-authelia-shb-oidc}
|
## SHB OIDC integration {#blocks-authelia-shb-oidc}
|
||||||
|
|
||||||
For services [provided by SelfHostBlocks][services] that handle [OIDC integration][OIDC],
|
For services [provided by SelfHostBlocks][services] that handle [OIDC integration][OIDC],
|
||||||
|
|
@ -117,8 +94,8 @@ shb.<service>.sso = {
|
||||||
enable = true;
|
enable = true;
|
||||||
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
||||||
|
|
||||||
secret.result = config.shb.sops.secret."<service>/sso/secret".result;
|
secret.result = config.shb.sops.secrets."<service>/sso/secret".result;
|
||||||
secretForAuthelia.result = config.shb.sops.secret."<service>/sso/secretForAuthelia".result;
|
secretForAuthelia.result = config.shb.sops.secrets."<service>/sso/secretForAuthelia".result;
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.sops.secret."<service>/sso/secret".request = config.shb.<service>.sso.secret.request;
|
shb.sops.secret."<service>/sso/secret".request = config.shb.<service>.sso.secret.request;
|
||||||
|
|
@ -144,7 +121,7 @@ the necessary configuration is:
|
||||||
shb.authelia.oidcClients = [
|
shb.authelia.oidcClients = [
|
||||||
{
|
{
|
||||||
client_id = "<service>";
|
client_id = "<service>";
|
||||||
client_secret.source = config.shb.sops.secret."<service>/sso/secretForAuthelia".response.path;
|
client_secret.source = shb.sops.secret."<service>/sso/secretForAuthelia".response.path;
|
||||||
scopes = [ "openid" "email" "profile" ];
|
scopes = [ "openid" "email" "profile" ];
|
||||||
redirect_uris = [
|
redirect_uris = [
|
||||||
"<provided by service documentation>"
|
"<provided by service documentation>"
|
||||||
|
|
@ -189,7 +166,7 @@ services.open-webui.environment = {
|
||||||
shb.authelia.oidcClients = [
|
shb.authelia.oidcClients = [
|
||||||
{
|
{
|
||||||
client_id = "open-webui";
|
client_id = "open-webui";
|
||||||
client_secret.source = config.shb.sops.secret."open-webui/sso/secretForAuthelia".response.path;
|
client_secret.source = shb.sops.secret."open-webui/sso/secretForAuthelia".response.path;
|
||||||
scopes = [ "openid" "email" "profile" ];
|
scopes = [ "openid" "email" "profile" ];
|
||||||
redirect_uris = [
|
redirect_uris = [
|
||||||
"<provided by service documentation>"
|
"<provided by service documentation>"
|
||||||
|
|
@ -214,9 +191,40 @@ Inspiration can be taken from SelfHostBlocks' source code.
|
||||||
To access the UI, we will need to create an `open-webui_user` and
|
To access the UI, we will need to create an `open-webui_user` and
|
||||||
`open-webui_admin` LDAP group and assign our user to it.
|
`open-webui_admin` LDAP group and assign our user to it.
|
||||||
|
|
||||||
|
## SHB Forward Auth {#blocks-authelia-shb-forward-auth}
|
||||||
|
|
||||||
|
For services provided by SelfHostBlocks that do not handle [OIDC integration][OIDC],
|
||||||
|
this block can provide [forward authentication][] which still allows the service to be protected by Authelia.
|
||||||
|
|
||||||
|
The user could still be required to authenticate to the service itself,
|
||||||
|
although some services can automatically users authorized by Authelia.
|
||||||
|
|
||||||
|
[forward authentication]: https://doc.traefik.io/traefik/middlewares/http/forwardauth/
|
||||||
|
|
||||||
|
Integrating with this block is done with the following code:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
shb.<services>.authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
||||||
|
```
|
||||||
|
|
||||||
## Forward Auth {#blocks-authelia-forward-auth}
|
## Forward Auth {#blocks-authelia-forward-auth}
|
||||||
|
|
||||||
Forward authentication is provided by the [nginx block](blocks-nginx.html#blocks-nginx-usage-ssl).
|
To integrate a service that does not handle OIDC integration
|
||||||
|
and which is not provided by SelfHostBlocks with this Authelia block,
|
||||||
|
the necessary configuration is:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
shb.nginx.vhosts = [
|
||||||
|
{
|
||||||
|
subdomain = "<service>";
|
||||||
|
domain = "example.com";
|
||||||
|
ssl = config.shb.certs.certs.letsencrypt."example.com";
|
||||||
|
upstream = "http://127.0.0.1:${toString config.services.<service>.port}/";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
This configuration assumes usage of the [SSL block][].
|
||||||
|
|
||||||
## Troubleshooting {#blocks-authelia-troubleshooting}
|
## Troubleshooting {#blocks-authelia-troubleshooting}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ let
|
||||||
OnCalendar = "daily";
|
OnCalendar = "daily";
|
||||||
Persistent = true;
|
Persistent = true;
|
||||||
};
|
};
|
||||||
description = "When to run the backup. See {manpage}`systemd.timer(5)` for details.";
|
description = ''When to run the backup. See {manpage}`systemd.timer(5)` for details.'';
|
||||||
example = {
|
example = {
|
||||||
OnCalendar = "00:05";
|
OnCalendar = "00:05";
|
||||||
RandomizedDelaySec = "5h";
|
RandomizedDelaySec = "5h";
|
||||||
|
|
@ -158,14 +158,9 @@ in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
../../lib/module.nix
|
../../lib/module.nix
|
||||||
../blocks/monitoring.nix
|
|
||||||
];
|
];
|
||||||
|
|
||||||
options.shb.borgbackup = {
|
options.shb.borgbackup = {
|
||||||
enableDashboard = lib.mkEnableOption "the Backups SHB dashboard" // {
|
|
||||||
default = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
instances = mkOption {
|
instances = mkOption {
|
||||||
description = "Files to backup following the [backup contract](./shb.contracts-backup.html).";
|
description = "Files to backup following the [backup contract](./shb.contracts-backup.html).";
|
||||||
default = { };
|
default = { };
|
||||||
|
|
@ -386,13 +381,8 @@ in
|
||||||
${serviceName} = mkMerge [
|
${serviceName} = mkMerge [
|
||||||
{
|
{
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
# Purposely not a oneshot systemd service otherwise
|
# Makes the systemd service wait for the backup to be done before changing state to inactive.
|
||||||
# the service waits on the completion the backup before deactivating.
|
Type = "oneshot";
|
||||||
# This seems like a nice property at first but there is one annoying
|
|
||||||
# edge case when deploying. If a backup job somehow is started when
|
|
||||||
# the deploy happens, the deploy will wait on the service to finish
|
|
||||||
# before considering the deploy done. And worse, it will consider the
|
|
||||||
# deploy as failed if the backup fails, which is not what you want.
|
|
||||||
Nice = lib.mkForce cfg.performance.niceness;
|
Nice = lib.mkForce cfg.performance.niceness;
|
||||||
IOSchedulingClass = lib.mkForce cfg.performance.ioSchedulingClass;
|
IOSchedulingClass = lib.mkForce cfg.performance.ioSchedulingClass;
|
||||||
IOSchedulingPriority = lib.mkForce cfg.performance.ioPriority;
|
IOSchedulingPriority = lib.mkForce cfg.performance.ioPriority;
|
||||||
|
|
@ -419,7 +409,6 @@ in
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
script = script.preStart;
|
script = script.preStart;
|
||||||
# Makes the systemd service wait for the backup to be done before changing state to inactive.
|
|
||||||
serviceConfig.Type = "oneshot";
|
serviceConfig.Type = "oneshot";
|
||||||
serviceConfig.LoadCredential = script.loadCredentials;
|
serviceConfig.LoadCredential = script.loadCredentials;
|
||||||
}
|
}
|
||||||
|
|
@ -457,16 +446,39 @@ in
|
||||||
let
|
let
|
||||||
mkBorgBackupBinary =
|
mkBorgBackupBinary =
|
||||||
name: instance:
|
name: instance:
|
||||||
shb.contracts.backup.mkRestoreScript {
|
pkgs.writeShellApplication {
|
||||||
name = fullName name instance.settings.repository;
|
name = fullName name instance.settings.repository;
|
||||||
user = instance.request.user;
|
text = ''
|
||||||
sudoPreserveEnvs = [
|
usage() {
|
||||||
"BORG_REPO"
|
echo "$0 restore latest"
|
||||||
"BORG_PASSCOMMAND"
|
}
|
||||||
];
|
|
||||||
secretsFile = "/run/secrets_borgbackup_env/${fullName name instance.settings.repository}";
|
if ! [ "$1" = "restore" ]; then
|
||||||
restoreCmd = ''(cd / && ${pkgs.borgbackup}/bin/borg extract \"$BORG_REPO::$snapshot\")'';
|
usage
|
||||||
listCmd = ''if [ -e \"$BORG_REPO/data\" ]; then borg list --short \"$BORG_REPO\"; fi'';
|
exit 1
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
|
||||||
|
if ! [ "$1" = "latest" ]; then
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
|
||||||
|
sudocmd() {
|
||||||
|
sudo --preserve-env=BORG_REPO,BORG_PASSCOMMAND -u ${instance.request.user} "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source <(sudocmd cat "/run/secrets_borgbackup_env/${fullName name instance.settings.repository}")
|
||||||
|
set +a
|
||||||
|
|
||||||
|
archive="$(sudocmd borg list --short "$BORG_REPO" | tail -n 1)"
|
||||||
|
echo "Will restore archive $archive"
|
||||||
|
|
||||||
|
(cd / && sudocmd ${pkgs.borgbackup}/bin/borg extract "$BORG_REPO"::"$archive")
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
flatten (mapAttrsToList mkBorgBackupBinary cfg.instances);
|
flatten (mapAttrsToList mkBorgBackupBinary cfg.instances);
|
||||||
|
|
@ -476,26 +488,43 @@ in
|
||||||
let
|
let
|
||||||
mkBorgBackupBinary =
|
mkBorgBackupBinary =
|
||||||
name: instance:
|
name: instance:
|
||||||
shb.contracts.backup.mkRestoreScript {
|
pkgs.writeShellApplication {
|
||||||
name = fullName name instance.settings.repository;
|
name = fullName name instance.settings.repository;
|
||||||
user = instance.request.user;
|
text = ''
|
||||||
sudoPreserveEnvs = [
|
usage() {
|
||||||
"BORG_REPO"
|
echo "$0 restore latest"
|
||||||
"BORG_PASSCOMMAND"
|
}
|
||||||
];
|
|
||||||
secretsFile = "/run/secrets_borgbackup_env/${fullName name instance.settings.repository}";
|
if ! [ "$1" = "restore" ]; then
|
||||||
restoreCmd = ''${pkgs.borgbackup}/bin/borg extract \"$BORG_REPO::$snapshot\" --stdout | ${instance.request.restoreCmd}'';
|
usage
|
||||||
listCmd = ''if [ -e \"$BORG_REPO/data\" ]; then borg list --short \"$BORG_REPO\"; fi'';
|
exit 1
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
|
||||||
|
if ! [ "$1" = "latest" ]; then
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
|
||||||
|
sudocmd() {
|
||||||
|
sudo --preserve-env=BORG_REPO,BORG_PASSCOMMAND -u ${instance.request.user} "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source <(sudocmd cat "/run/secrets_borgbackup_env/${fullName name instance.settings.repository}")
|
||||||
|
set +a
|
||||||
|
|
||||||
|
archive="$(sudocmd borg list --short "$BORG_REPO" | tail -n 1)"
|
||||||
|
echo "Will restore archive $archive"
|
||||||
|
|
||||||
|
sudocmd sh -c "${pkgs.borgbackup}/bin/borg extract $BORG_REPO::$archive --stdout | ${instance.request.restoreCmd}"
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
flatten (mapAttrsToList mkBorgBackupBinary cfg.databases);
|
flatten (mapAttrsToList mkBorgBackupBinary cfg.databases);
|
||||||
}
|
}
|
||||||
|
|
||||||
(lib.mkIf (cfg.enableDashboard && (cfg.instances != { } || cfg.databases != { })) {
|
|
||||||
shb.monitoring.dashboards = [
|
|
||||||
./backup/dashboard/Backups.json
|
|
||||||
];
|
|
||||||
})
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
Defined in [`/modules/blocks/borgbackup.nix`](@REPO@/modules/blocks/borgbackup.nix).
|
Defined in [`/modules/blocks/borgbackup.nix`](@REPO@/modules/blocks/borgbackup.nix).
|
||||||
|
|
||||||
This block sets up a backup job using [BorgBackup][].
|
This block sets up a backup job using [BorgBackup][].
|
||||||
It is heavily based on the nixpkgs BorgBackup module.
|
|
||||||
|
|
||||||
[borgbackup]: https://www.borgbackup.org/
|
[borgbackup]: https://www.borgbackup.org/
|
||||||
|
|
||||||
|
|
@ -51,7 +50,7 @@ shb.borgbackup.instances."myservice" = {
|
||||||
settings = {
|
settings = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
passphrase.result = config.shb.sops.secret."passphrase".result;
|
passphrase.result = shb.sops.secret."passphrase".result;
|
||||||
|
|
||||||
repository = {
|
repository = {
|
||||||
path = "/srv/backups/myservice";
|
path = "/srv/backups/myservice";
|
||||||
|
|
@ -72,7 +71,7 @@ shb.borgbackup.instances."myservice" = {
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.sops.secret."passphrase".request =
|
shb.sops.secret."passphrase".request =
|
||||||
config.shb.borgbackup.instances."myservice".settings.passphrase.request;
|
shb.borgbackup.instances."myservice".settings.passphrase.request;
|
||||||
```
|
```
|
||||||
|
|
||||||
### One folder backed up with contract {#blocks-borgbackup-usage-provider-contract}
|
### One folder backed up with contract {#blocks-borgbackup-usage-provider-contract}
|
||||||
|
|
@ -88,7 +87,7 @@ shb.borgbackup.instances."myservice" = {
|
||||||
settings = {
|
settings = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
passphrase.result = config.shb.sops.secret."passphrase".result;
|
passphrase.result = shb.sops.secret."passphrase".result;
|
||||||
|
|
||||||
repository = {
|
repository = {
|
||||||
path = "/srv/backups/myservice";
|
path = "/srv/backups/myservice";
|
||||||
|
|
@ -109,7 +108,7 @@ shb.borgbackup.instances."myservice" = {
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.sops.secret."passphrase".request =
|
shb.sops.secret."passphrase".request =
|
||||||
config.shb.borgbackup.instances."myservice".settings.passphrase.request;
|
shb.borgbackup.instances."myservice".settings.passphrase.request;
|
||||||
```
|
```
|
||||||
|
|
||||||
### One folder backed up to S3 {#blocks-borgbackup-usage-provider-remote}
|
### One folder backed up to S3 {#blocks-borgbackup-usage-provider-remote}
|
||||||
|
|
@ -225,41 +224,10 @@ shb.test.backup.instances.all = backupcfg repos ["/var/lib/myfolder1" "/var/lib/
|
||||||
|
|
||||||
## Monitoring {#blocks-borgbackup-monitoring}
|
## Monitoring {#blocks-borgbackup-monitoring}
|
||||||
|
|
||||||
A generic dashboard for all backup solutions is provided.
|
[WIP](https://github.com/ibizaman/selfhostblocks/issues/151)
|
||||||
See [Backups Dashboard and Alert](blocks-monitoring.html#blocks-monitoring-backup) section in the monitoring chapter.
|
|
||||||
|
|
||||||
## Maintenance {#blocks-borgbackup-maintenance}
|
## Maintenance {#blocks-borgbackup-maintenance}
|
||||||
|
|
||||||
### Manual Backup {#blocks-borgbackup-maintenance-manuql}
|
|
||||||
|
|
||||||
To launch a backup manually, just run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
systemctl start <systemd-service-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
You can easily discover the systemd service name you need by either listing the units:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
systemctl list-units 'borgbackup*'
|
|
||||||
```
|
|
||||||
|
|
||||||
Or by autocompleting the unit name with `<TAB>`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
systemctl start borgbackup<TAB><TAB>
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that the systemd services are of `Type=simple` which means the systemd service
|
|
||||||
will not wait for the backup completion to terminate.
|
|
||||||
If you want instead to wait for the backup to complete, use the `--wait` flag:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
systemctl start --wait <systemd-service-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Restore {#blocks-borgbackup-maintenance-restore}
|
|
||||||
|
|
||||||
One command-line helper is provided per backup instance and repository pair to automatically supply the needed secrets.
|
One command-line helper is provided per backup instance and repository pair to automatically supply the needed secrets.
|
||||||
|
|
||||||
The restore script has all the secrets needed to access the repo,
|
The restore script has all the secrets needed to access the repo,
|
||||||
|
|
|
||||||
|
|
@ -75,30 +75,6 @@ in
|
||||||
config = {
|
config = {
|
||||||
services.davfs2.enable = builtins.length cfg.mounts > 0;
|
services.davfs2.enable = builtins.length cfg.mounts > 0;
|
||||||
|
|
||||||
systemd.services = lib.optionalAttrs (builtins.length cfg.mounts > 0) {
|
|
||||||
davfs2-password-generation = {
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
serviceConfig.Type = "oneshot";
|
|
||||||
script = ''
|
|
||||||
mkdir -p /etc/davfs2
|
|
||||||
[ -f /etc/davfs2/secrets ] && mv /etc/davfs2/secrets /etc/davfs2/secrets.prev
|
|
||||||
touch /etc/davfs2/secrets
|
|
||||||
chown root: /etc/davfs2
|
|
||||||
chown root: /etc/davfs2/secrets
|
|
||||||
chmod 700 /etc/davfs2
|
|
||||||
chmod 600 /etc/davfs2/secrets
|
|
||||||
''
|
|
||||||
+ (
|
|
||||||
let
|
|
||||||
mkPasswordCmd = cfg': ''
|
|
||||||
printf "%s %s %s\n" "${cfg'.mountPoint}" "${cfg'.username}" "$(cat ${cfg'.passwordFile})" >> /etc/davfs2/secrets
|
|
||||||
'';
|
|
||||||
in
|
|
||||||
lib.concatStringsSep "\n" (map mkPasswordCmd cfg.mounts)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.mounts =
|
systemd.mounts =
|
||||||
let
|
let
|
||||||
mkMountCfg = c: {
|
mkMountCfg = c: {
|
||||||
|
|
@ -106,7 +82,6 @@ in
|
||||||
description = "Webdav mount point";
|
description = "Webdav mount point";
|
||||||
after = [ "network-online.target" ];
|
after = [ "network-online.target" ];
|
||||||
wants = [ "network-online.target" ];
|
wants = [ "network-online.target" ];
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
|
|
||||||
what = c.remoteUrl;
|
what = c.remoteUrl;
|
||||||
where = c.mountPoint;
|
where = c.mountPoint;
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
ldapUserPassword = lib.mkOption {
|
ldapUserPassword = lib.mkOption {
|
||||||
description = "LDAP admin user secret. Must be >= 8 characters.";
|
description = "LDAP admin user secret.";
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
options = shb.contracts.secret.mkRequester {
|
options = shb.contracts.secret.mkRequester {
|
||||||
mode = "0440";
|
mode = "0440";
|
||||||
|
|
@ -331,21 +331,7 @@ in
|
||||||
enforceGroups = mkOption {
|
enforceGroups = mkOption {
|
||||||
description = "Remove groups not set declaratively.";
|
description = "Remove groups not set declaratively.";
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = false;
|
default = true;
|
||||||
};
|
|
||||||
|
|
||||||
dashboard = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Dashboard contract consumer
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.dashboard.mkRequester {
|
|
||||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
|
||||||
externalUrlText = "https://\${config.shb.lldap.subdomain}.\${config.shb.lldap.domain}";
|
|
||||||
internalUrl = "http://127.0.0.1:${toString cfg.webUIListenPort}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -418,41 +404,6 @@ in
|
||||||
) cfg.ensureUsers;
|
) cfg.ensureUsers;
|
||||||
};
|
};
|
||||||
|
|
||||||
# Harden lldap following https://github.com/NixOS/nixpkgs/pull/487933
|
|
||||||
systemd.services.lldap.serviceConfig = {
|
|
||||||
RemoveIPC = true;
|
|
||||||
RestrictNamespaces = true;
|
|
||||||
RestrictRealtime = true;
|
|
||||||
RestrictSUIDSGID = true;
|
|
||||||
RestrictAddressFamilies = [
|
|
||||||
"AF_UNIX"
|
|
||||||
"AF_INET"
|
|
||||||
"AF_INET6"
|
|
||||||
];
|
|
||||||
SystemCallFilter = [
|
|
||||||
"@system-service"
|
|
||||||
"~@privileged"
|
|
||||||
"~@resources"
|
|
||||||
];
|
|
||||||
SystemCallArchitectures = "native";
|
|
||||||
CapabilityBoundingSet = "";
|
|
||||||
LockPersonality = true;
|
|
||||||
NoNewPrivileges = true;
|
|
||||||
PrivateTmp = true;
|
|
||||||
PrivateDevices = true;
|
|
||||||
ProtectClock = true;
|
|
||||||
ProtectControlGroups = true;
|
|
||||||
ProtectHome = true;
|
|
||||||
ProtectHostname = true;
|
|
||||||
ProtectKernelLogs = true;
|
|
||||||
ProtectKernelModules = true;
|
|
||||||
ProtectKernelTunables = true;
|
|
||||||
ProtectSystem = "strict";
|
|
||||||
ProtectProc = "invisible";
|
|
||||||
ProcSubset = "pid";
|
|
||||||
MemoryDenyWriteExecute = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
shb.mitmdump.instances."lldap-web" = lib.mkIf cfg.debug {
|
shb.mitmdump.instances."lldap-web" = lib.mkIf cfg.debug {
|
||||||
listenPort = config.shb.lldap.webUIListenPort;
|
listenPort = config.shb.lldap.webUIListenPort;
|
||||||
upstreamPort = config.shb.lldap.webUIListenPort + 1;
|
upstreamPort = config.shb.lldap.webUIListenPort + 1;
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,7 @@ across services.
|
||||||
|
|
||||||
[LLDAP]: https://github.com/lldap/lldap
|
[LLDAP]: https://github.com/lldap/lldap
|
||||||
|
|
||||||
## Features {#blocks-lldap-features}
|
## Global Setup {#blocks-lldap-global-setup}
|
||||||
|
|
||||||
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#blocks-lldap-usage-applicationdashboard)
|
|
||||||
|
|
||||||
## Usage {#blocks-lldap-usage}
|
|
||||||
|
|
||||||
### Initial Configuration {#blocks-lldap-usage-configuration}
|
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
shb.lldap = {
|
shb.lldap = {
|
||||||
|
|
@ -35,7 +29,7 @@ shb.sops.secret."lldap/user_password".request = config.shb.lldap.ldapUserPasswor
|
||||||
This assumes secrets are setup with SOPS
|
This assumes secrets are setup with SOPS
|
||||||
as mentioned in [the secrets setup section](usage.html#usage-secrets) of the manual.
|
as mentioned in [the secrets setup section](usage.html#usage-secrets) of the manual.
|
||||||
|
|
||||||
### SSL {#blocks-lldap-usage-ssl}
|
## SSL {#blocks-lldap-ssl}
|
||||||
|
|
||||||
Using SSL is an important security practice, like always.
|
Using SSL is an important security practice, like always.
|
||||||
Using the [SSL block][], the configuration to add to the one above is:
|
Using the [SSL block][], the configuration to add to the one above is:
|
||||||
|
|
@ -50,7 +44,7 @@ shb.certs.certs.letsencrypt.${domain}.extraDomains = [
|
||||||
shb.lldap.ssl = config.shb.certs.certs.letsencrypt.${config.shb.lldap.domain};
|
shb.lldap.ssl = config.shb.certs.certs.letsencrypt.${config.shb.lldap.domain};
|
||||||
```
|
```
|
||||||
|
|
||||||
### Restrict Access By IP {#blocks-lldap-usage-restrict-access-by-ip}
|
## Restrict Access By IP {#blocks-lldap-restrict-access-by-ip}
|
||||||
|
|
||||||
For added security, you can restrict access to the LLDAP UI
|
For added security, you can restrict access to the LLDAP UI
|
||||||
by adding the following line:
|
by adding the following line:
|
||||||
|
|
@ -59,22 +53,6 @@ by adding the following line:
|
||||||
shb.lldap.restrictAccessIPRange = "192.168.50.0/24";
|
shb.lldap.restrictAccessIPRange = "192.168.50.0/24";
|
||||||
```
|
```
|
||||||
|
|
||||||
### Application Dashboard {#blocks-lldap-usage-applicationdashboard}
|
|
||||||
|
|
||||||
Integration with the [dashboard contract](contracts-dashboard.html) is provided
|
|
||||||
by the [dashboard option](#blocks-lldap-options-shb.lldap.dashboard).
|
|
||||||
|
|
||||||
For example using the [Homepage](services-homepage.html) service:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.homepage.servicesGroups.Admin.services.LLDAP = {
|
|
||||||
sortOrder = 2;
|
|
||||||
dashboard.request = config.shb.lldap.dashboard.request;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Manage Groups {#blocks-lldap-manage-groups}
|
## Manage Groups {#blocks-lldap-manage-groups}
|
||||||
|
|
||||||
The following snippet will create group named "family" if it does not exist yet.
|
The following snippet will create group named "family" if it does not exist yet.
|
||||||
|
|
@ -83,7 +61,7 @@ Also, all other groups will be deleted and only the "family" group will remain.
|
||||||
Note that the `lldap_admin`, `lldap_password_manager` and `lldap_strict_readonly` groups, which are internal to LLDAP, will always exist.
|
Note that the `lldap_admin`, `lldap_password_manager` and `lldap_strict_readonly` groups, which are internal to LLDAP, will always exist.
|
||||||
|
|
||||||
If you want existing groups not declared in the `shb.lldap.ensureGroups` to be deleted,
|
If you want existing groups not declared in the `shb.lldap.ensureGroups` to be deleted,
|
||||||
set [`shb.lldap.enforceGroups`](#blocks-lldap-options-shb.lldap.enforceGroups) to `true`.
|
set [`shb.lldap.enforceGroups`](#blocks-lldap-options-shb.lldap.enforceGroups) to `false`.
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
{
|
{
|
||||||
|
|
@ -164,7 +142,7 @@ set [`shb.lldap.enforceUserMemberships`](#blocks-lldap-options-shb.lldap.enforce
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.sops.secret."dad".request =
|
shb.sops.secret."dad".request =
|
||||||
config.shb.lldap.ensureUsers.dad.password.request;
|
shb.lldap.ensureUsers.dad.password.request;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ let
|
||||||
p = pkgs.python3Packages;
|
p = pkgs.python3Packages;
|
||||||
in
|
in
|
||||||
[
|
[
|
||||||
p.systemd-python
|
p.systemd
|
||||||
p.mitmproxy
|
p.mitmproxy
|
||||||
];
|
];
|
||||||
flakeIgnore = [ "E501" ];
|
flakeIgnore = [ "E501" ];
|
||||||
|
|
@ -48,7 +48,7 @@ let
|
||||||
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
||||||
|
|
||||||
|
|
||||||
def wait_for_port(host, port, timeout):
|
def wait_for_port(host, port, timeout=10):
|
||||||
deadline = time.time() + timeout
|
deadline = time.time() + timeout
|
||||||
while time.time() < deadline:
|
while time.time() < deadline:
|
||||||
try:
|
try:
|
||||||
|
|
@ -68,7 +68,6 @@ let
|
||||||
parser.add_argument("--listen_port", required=True, help="Port mitmdump will listen on")
|
parser.add_argument("--listen_port", required=True, help="Port mitmdump will listen on")
|
||||||
parser.add_argument("--upstream_host", default="http://127.0.0.1", help="Host mitmdump will connect to for upstream. Example: http://127.0.0.1 or https://otherhost")
|
parser.add_argument("--upstream_host", default="http://127.0.0.1", help="Host mitmdump will connect to for upstream. Example: http://127.0.0.1 or https://otherhost")
|
||||||
parser.add_argument("--upstream_port", required=True, help="Port mitmdump will connect to for upstream")
|
parser.add_argument("--upstream_port", required=True, help="Port mitmdump will connect to for upstream")
|
||||||
parser.add_argument("--timeout", required=False, type=int, default=10, help="Timeout to wait for port availability")
|
|
||||||
args, rest = parser.parse_known_args()
|
args, rest = parser.parse_known_args()
|
||||||
|
|
||||||
MITMDUMP_BIN = os.environ.get("MITMDUMP_BIN")
|
MITMDUMP_BIN = os.environ.get("MITMDUMP_BIN")
|
||||||
|
|
@ -76,7 +75,7 @@ let
|
||||||
raise Exception("MITMDUMP_BIN env var must be set to the path of the mitmdump binary")
|
raise Exception("MITMDUMP_BIN env var must be set to the path of the mitmdump binary")
|
||||||
|
|
||||||
logging.info(f"Waiting for upstream address '{args.upstream_host}:{args.upstream_port}' to be up.")
|
logging.info(f"Waiting for upstream address '{args.upstream_host}:{args.upstream_port}' to be up.")
|
||||||
wait_for_port(args.upstream_host, args.upstream_port, timeout=args.timeout)
|
wait_for_port(args.upstream_host, args.upstream_port, timeout=10)
|
||||||
logging.info(f"Upstream address '{args.upstream_host}:{args.upstream_port}' is up.")
|
logging.info(f"Upstream address '{args.upstream_host}:{args.upstream_port}' is up.")
|
||||||
|
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
|
|
@ -91,11 +90,10 @@ let
|
||||||
)
|
)
|
||||||
|
|
||||||
logging.info(f"Waiting for mitmdump instance to start on port {args.listen_port}.")
|
logging.info(f"Waiting for mitmdump instance to start on port {args.listen_port}.")
|
||||||
if wait_for_port("127.0.0.1", args.listen_port, timeout=args.timeout):
|
if wait_for_port("127.0.0.1", args.listen_port, timeout=10):
|
||||||
logging.info(f"Mitmdump is started on port {args.listen_port}.")
|
logging.info(f"Mitmdump is started on port {args.listen_port}.")
|
||||||
notify("READY=1")
|
notify("READY=1")
|
||||||
else:
|
else:
|
||||||
print(f"Mitmdump instance did not start before the timeout of {args.timeout} seconds, consider increasing the timeout")
|
|
||||||
proc.terminate()
|
proc.terminate()
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
|
@ -224,14 +222,6 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
timeout = mkOption {
|
|
||||||
type = types.int;
|
|
||||||
default = 30;
|
|
||||||
description = ''
|
|
||||||
Time to wait for upstream to start and mitmdump to start.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
after = mkOption {
|
after = mkOption {
|
||||||
type = listOf str;
|
type = listOf str;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
|
|
@ -249,7 +239,7 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Addons to enable on this mitmdump instance.
|
Addons to enable on this mitmdump instance.
|
||||||
'';
|
'';
|
||||||
example = lib.literalExpression "[ config.shb.mitmdump.addons.logger ]";
|
example = lib.literalExpression ''[ config.shb.mitmdump.addons.logger ]'';
|
||||||
};
|
};
|
||||||
|
|
||||||
extraArgs = mkOption {
|
extraArgs = mkOption {
|
||||||
|
|
@ -292,7 +282,7 @@ in
|
||||||
addons = lib.concatMapStringsSep " " (addon: "-s ${addon}") cfg'.enabledAddons;
|
addons = lib.concatMapStringsSep " " (addon: "-s ${addon}") cfg'.enabledAddons;
|
||||||
extraArgs = lib.concatStringsSep " " cfg'.extraArgs;
|
extraArgs = lib.concatStringsSep " " cfg'.extraArgs;
|
||||||
in
|
in
|
||||||
"${lib.getExe mitmdumpScript} --listen_host ${cfg'.listenHost} --listen_port ${toString cfg'.listenPort} --upstream_host ${cfg'.upstreamHost} --upstream_port ${toString cfg'.upstreamPort} --timeout ${toString cfg'.timeout} ${addons} ${extraArgs}";
|
"${lib.getExe mitmdumpScript} --listen_host ${cfg'.listenHost} --listen_port ${toString cfg'.listenPort} --upstream_host ${cfg'.upstreamHost} --upstream_port ${toString cfg'.upstreamPort} ${addons} ${extraArgs}";
|
||||||
};
|
};
|
||||||
requires = cfg'.after;
|
requires = cfg'.after;
|
||||||
after = cfg'.after;
|
after = cfg'.after;
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,8 @@ let
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
../../lib/module.nix
|
|
||||||
../blocks/authelia.nix
|
../blocks/authelia.nix
|
||||||
../blocks/lldap.nix
|
../blocks/lldap.nix
|
||||||
../blocks/nginx.nix
|
|
||||||
];
|
];
|
||||||
|
|
||||||
options.shb.monitoring = {
|
options.shb.monitoring = {
|
||||||
|
|
@ -98,10 +96,10 @@ in
|
||||||
default = 1;
|
default = 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
dashboards = lib.mkOption {
|
provisionDashboards = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.path;
|
type = lib.types.bool;
|
||||||
description = "Dashboards to provision under 'Self Host Blocks' folder.";
|
description = "Provision Self Host Blocks dashboards under 'Self Host Blocks' folder.";
|
||||||
default = [ ];
|
default = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
contactPoints = lib.mkOption {
|
contactPoints = lib.mkOption {
|
||||||
|
|
@ -110,35 +108,6 @@ in
|
||||||
default = [ ];
|
default = [ ];
|
||||||
};
|
};
|
||||||
|
|
||||||
scrutiny = {
|
|
||||||
enable = lib.mkEnableOption "scrutiny service" // {
|
|
||||||
default = true;
|
|
||||||
};
|
|
||||||
subdomain = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
description = ''
|
|
||||||
If a string, this will be the subdomain under which the scrutiny web interface will be servced.
|
|
||||||
|
|
||||||
If null, the web interface will not be served and only the prometheus metrics will be accessible.
|
|
||||||
'';
|
|
||||||
default = "scrutiny";
|
|
||||||
};
|
|
||||||
dashboard = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Dashboard contract consumer
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.dashboard.mkRequester {
|
|
||||||
externalUrl = "https://${cfg.scrutiny.subdomain}.${cfg.domain}";
|
|
||||||
externalUrlText = "https://\${config.shb.monitoring.scrutiny.subdomain}.\${config.shb.monitoring.domain}";
|
|
||||||
internalUrl = "http://127.0.0.1:${toString config.services.scrutiny.settings.web.listen.port}";
|
|
||||||
internalUrlText = "https://127.0.0.1.\${config.services.scrutiny.settings.web.listen.port}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
adminPassword = lib.mkOption {
|
adminPassword = lib.mkOption {
|
||||||
description = "Initial admin password.";
|
description = "Initial admin password.";
|
||||||
type = lib.types.submodule {
|
type = lib.types.submodule {
|
||||||
|
|
@ -278,38 +247,13 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
dashboard = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Dashboard contract consumer
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.dashboard.mkRequester {
|
|
||||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
|
||||||
externalUrlText = "https://\${config.shb.monitoring.subdomain}.\${config.shb.monitoring.domain}";
|
|
||||||
internalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
|
||||||
internalUrlText = "https://\${config.shb.monitoring.subdomain}.\${config.shb.monitoring.domain}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
impermanence = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Paths to save when using impermanence setup.
|
|
||||||
'';
|
|
||||||
type = lib.types.attrsOf lib.types.str;
|
|
||||||
default = {
|
|
||||||
fluent-bit = "/var/fluent-bit";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkMerge [
|
config = lib.mkMerge [
|
||||||
(lib.mkIf cfg.enable {
|
(lib.mkIf cfg.enable {
|
||||||
assertions = [
|
assertions = [
|
||||||
{
|
{
|
||||||
assertion = builtins.length cfg.contactPoints > 0;
|
assertion = (!(isNull cfg.smtp)) -> builtins.length cfg.contactPoints > 0;
|
||||||
message = "Must have at least one contact point for alerting";
|
message = "Must have at least one contact point for alerting";
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
@ -358,25 +302,14 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
})
|
|
||||||
|
|
||||||
(lib.mkIf cfg.enable {
|
|
||||||
shb.monitoring.dashboards = [
|
|
||||||
./monitoring/dashboards/Errors.json
|
|
||||||
./monitoring/dashboards/Performance.json
|
|
||||||
./monitoring/dashboards/Scraping_Jobs.json
|
|
||||||
];
|
|
||||||
|
|
||||||
services.grafana.provision = {
|
services.grafana.provision = {
|
||||||
dashboards.settings = lib.mkIf (cfg.dashboards != [ ]) {
|
dashboards.settings = lib.mkIf cfg.provisionDashboards {
|
||||||
apiVersion = 1;
|
apiVersion = 1;
|
||||||
providers = [
|
providers = [
|
||||||
{
|
{
|
||||||
folder = "Self Host Blocks";
|
folder = "Self Host Blocks";
|
||||||
options.path = pkgs.symlinkJoin {
|
options.path = ./monitoring/dashboards;
|
||||||
name = "dashboards";
|
|
||||||
paths = map (p: pkgs.runCommand "dashboard" { } "mkdir $out; cp ${p} $out") cfg.dashboards;
|
|
||||||
};
|
|
||||||
allowUiUpdates = true;
|
allowUiUpdates = true;
|
||||||
disableDeletion = true;
|
disableDeletion = true;
|
||||||
}
|
}
|
||||||
|
|
@ -467,15 +400,10 @@ in
|
||||||
# any updates.
|
# any updates.
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
})
|
|
||||||
|
|
||||||
(lib.mkIf cfg.enable {
|
|
||||||
services.prometheus = {
|
services.prometheus = {
|
||||||
enable = true;
|
enable = true;
|
||||||
port = cfg.prometheusPort;
|
port = cfg.prometheusPort;
|
||||||
globalConfig = {
|
|
||||||
scrape_interval = "15s";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
services.loki = {
|
services.loki = {
|
||||||
|
|
@ -585,62 +513,41 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# I decided to switch to fluent-bit because it can be tested locally https://docs.fluentbit.io/manual/local-testing/logging-pipeline
|
services.promtail = {
|
||||||
services.fluent-bit = {
|
|
||||||
enable = true;
|
enable = true;
|
||||||
settings = {
|
configuration = {
|
||||||
service = {
|
server = {
|
||||||
flush = 1;
|
http_listen_port = 9080;
|
||||||
log_level = "info";
|
grpc_listen_port = 0;
|
||||||
http_server = "true";
|
|
||||||
http_listen = "127.0.0.1";
|
|
||||||
http_port = 9080;
|
|
||||||
grace = 30;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pipeline = {
|
positions.filename = "/tmp/positions.yaml";
|
||||||
inputs = [
|
|
||||||
{
|
|
||||||
name = "systemd";
|
|
||||||
|
|
||||||
# The asterisk appends the _SYSTEMD_UNIT to the prefix.
|
client.url = "http://localhost:${toString config.services.loki.configuration.server.http_listen_port}/api/prom/push";
|
||||||
tag = "systemd.*";
|
|
||||||
|
|
||||||
# Read logs from this systemd journal directory.
|
scrape_configs = [
|
||||||
|
{
|
||||||
|
job_name = "systemd";
|
||||||
|
journal = {
|
||||||
|
json = false;
|
||||||
|
max_age = "12h";
|
||||||
path = "/var/log/journal";
|
path = "/var/log/journal";
|
||||||
|
# matches = "_TRANSPORT=kernel";
|
||||||
# Database file to keep track of the journald cursor.
|
labels = {
|
||||||
db = "/var/fluent-bit/systemd.db";
|
|
||||||
|
|
||||||
# Start reading new entries. Skip entries already stored in journald.
|
|
||||||
read_from_tail = true;
|
|
||||||
|
|
||||||
# Max entries to lookback on start.
|
|
||||||
max_entries = 10000;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
outputs = [
|
|
||||||
{
|
|
||||||
name = "loki";
|
|
||||||
|
|
||||||
match = "systemd.*";
|
|
||||||
|
|
||||||
host = "localhost";
|
|
||||||
port = config.services.loki.configuration.server.http_listen_port;
|
|
||||||
|
|
||||||
labels = lib.concatMapAttrsStringSep ", " (n: v: "${n}=${v}") {
|
|
||||||
job = "systemd-journal";
|
|
||||||
domain = cfg.domain;
|
domain = cfg.domain;
|
||||||
hostname = config.networking.hostName;
|
hostname = config.networking.hostName;
|
||||||
|
job = "systemd-journal";
|
||||||
};
|
};
|
||||||
|
};
|
||||||
label_keys = "$unit";
|
relabel_configs = [
|
||||||
}
|
{
|
||||||
];
|
source_labels = [ "__journal__systemd_unit" ];
|
||||||
};
|
target_label = "unit";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
];
|
||||||
};
|
};
|
||||||
graceLimit = "1m";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
services.nginx = {
|
services.nginx = {
|
||||||
|
|
@ -660,9 +567,7 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
})
|
|
||||||
|
|
||||||
(lib.mkIf cfg.enable {
|
|
||||||
services.prometheus.scrapeConfigs = [
|
services.prometheus.scrapeConfigs = [
|
||||||
{
|
{
|
||||||
job_name = "node";
|
job_name = "node";
|
||||||
|
|
@ -914,77 +819,5 @@ in
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
|
|
||||||
(lib.mkIf (cfg.enable && cfg.scrutiny.enable) {
|
|
||||||
services.scrutiny = {
|
|
||||||
enable = true;
|
|
||||||
|
|
||||||
openFirewall = false;
|
|
||||||
|
|
||||||
# This src includes Prometheus metrics exporter.
|
|
||||||
package = pkgs.scrutiny.overrideAttrs ({
|
|
||||||
src = pkgs.fetchFromGitHub {
|
|
||||||
owner = "ibizaman";
|
|
||||||
repo = "scrutiny";
|
|
||||||
rev = "74faf06f77df83f29e7e1806cd88b2fafc0bbb82";
|
|
||||||
hash = "sha256-r0AVWL+E046xHxitwMPfRNTOpjuOk+W6tB41YgmLTPg=";
|
|
||||||
};
|
|
||||||
|
|
||||||
vendorHash = "sha256-kAlnlWnBMFCdgdak5L5hRquRtyLi5MTmDa/kxwqPs4E=";
|
|
||||||
});
|
|
||||||
|
|
||||||
settings = {
|
|
||||||
web = {
|
|
||||||
metrics.enabled = true; # Enables Prometheus exporter
|
|
||||||
listenHost = "127.0.0.1";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
collector = {
|
|
||||||
enable = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
services.prometheus.scrapeConfigs = [
|
|
||||||
{
|
|
||||||
job_name = "scrutiny";
|
|
||||||
metrics_path = "/api/metrics";
|
|
||||||
static_configs = [
|
|
||||||
{
|
|
||||||
targets = [ "127.0.0.1:${toString config.services.scrutiny.settings.web.listen.port}" ];
|
|
||||||
labels = commonLabels;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
shb.monitoring.dashboards = [
|
|
||||||
./monitoring/dashboards/Health.json
|
|
||||||
];
|
|
||||||
|
|
||||||
shb.nginx.vhosts = lib.mkIf (cfg.scrutiny.subdomain != null) [
|
|
||||||
(
|
|
||||||
{
|
|
||||||
inherit (cfg) domain ssl;
|
|
||||||
subdomain = cfg.scrutiny.subdomain;
|
|
||||||
|
|
||||||
upstream = "http://127.0.0.1:${toString config.services.scrutiny.settings.web.listen.port}";
|
|
||||||
autheliaRules = lib.optionals (cfg.sso.enable) [
|
|
||||||
{
|
|
||||||
domain = "${cfg.subdomain}.${cfg.domain}";
|
|
||||||
policy = cfg.sso.authorization_policy;
|
|
||||||
subject = [
|
|
||||||
"group:${cfg.ldap.userGroup}"
|
|
||||||
"group:${cfg.ldap.adminGroup}"
|
|
||||||
];
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
// lib.optionalAttrs cfg.sso.enable {
|
|
||||||
inherit (cfg.sso) authEndpoint;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
];
|
|
||||||
})
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,872 +0,0 @@
|
||||||
{
|
|
||||||
"annotations": {
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"builtIn": 1,
|
|
||||||
"datasource": {
|
|
||||||
"type": "grafana",
|
|
||||||
"uid": "-- Grafana --"
|
|
||||||
},
|
|
||||||
"enable": true,
|
|
||||||
"hide": true,
|
|
||||||
"iconColor": "rgba(0, 211, 255, 1)",
|
|
||||||
"name": "Annotations & Alerts",
|
|
||||||
"type": "dashboard"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"editable": true,
|
|
||||||
"fiscalYearStartMonth": 0,
|
|
||||||
"graphTooltip": 0,
|
|
||||||
"links": [],
|
|
||||||
"panels": [
|
|
||||||
{
|
|
||||||
"collapsed": false,
|
|
||||||
"gridPos": {
|
|
||||||
"h": 1,
|
|
||||||
"w": 24,
|
|
||||||
"x": 0,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"id": 4,
|
|
||||||
"panels": [],
|
|
||||||
"repeat": "hostname",
|
|
||||||
"title": "${hostname}",
|
|
||||||
"type": "row"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "thresholds"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"align": "auto",
|
|
||||||
"cellOptions": {
|
|
||||||
"type": "auto"
|
|
||||||
},
|
|
||||||
"footer": {
|
|
||||||
"reducers": []
|
|
||||||
},
|
|
||||||
"inspect": false
|
|
||||||
},
|
|
||||||
"mappings": [],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "green",
|
|
||||||
"value": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": 80
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 4,
|
|
||||||
"w": 12,
|
|
||||||
"x": 0,
|
|
||||||
"y": 1
|
|
||||||
},
|
|
||||||
"id": 3,
|
|
||||||
"maxDataPoints": 400,
|
|
||||||
"options": {
|
|
||||||
"cellHeight": "sm",
|
|
||||||
"showHeader": true
|
|
||||||
},
|
|
||||||
"pluginVersion": "12.4.0",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"disableTextWrap": false,
|
|
||||||
"editorMode": "builder",
|
|
||||||
"expr": "node_os_info",
|
|
||||||
"fullMetaSearch": false,
|
|
||||||
"includeNullMetadata": true,
|
|
||||||
"legendFormat": "__auto",
|
|
||||||
"range": true,
|
|
||||||
"refId": "A",
|
|
||||||
"useBackend": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "OS Versions",
|
|
||||||
"transformations": [
|
|
||||||
{
|
|
||||||
"id": "labelsToFields",
|
|
||||||
"options": {
|
|
||||||
"keepLabels": [
|
|
||||||
"build_id",
|
|
||||||
"domain",
|
|
||||||
"hostname",
|
|
||||||
"id",
|
|
||||||
"instance",
|
|
||||||
"job",
|
|
||||||
"name",
|
|
||||||
"pretty_name",
|
|
||||||
"version",
|
|
||||||
"version_codename",
|
|
||||||
"version_id"
|
|
||||||
],
|
|
||||||
"mode": "columns"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "groupBy",
|
|
||||||
"options": {
|
|
||||||
"fields": {
|
|
||||||
"Time": {
|
|
||||||
"aggregations": [
|
|
||||||
"firstNotNull"
|
|
||||||
],
|
|
||||||
"operation": "aggregate"
|
|
||||||
},
|
|
||||||
"pretty_name": {
|
|
||||||
"aggregations": [],
|
|
||||||
"operation": "groupby"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"type": "table"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "palette-classic"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"axisBorderShow": false,
|
|
||||||
"axisCenteredZero": false,
|
|
||||||
"axisColorMode": "text",
|
|
||||||
"axisLabel": "",
|
|
||||||
"axisPlacement": "auto",
|
|
||||||
"barAlignment": 0,
|
|
||||||
"barWidthFactor": 0.6,
|
|
||||||
"drawStyle": "line",
|
|
||||||
"fillOpacity": 0,
|
|
||||||
"gradientMode": "none",
|
|
||||||
"hideFrom": {
|
|
||||||
"legend": false,
|
|
||||||
"tooltip": false,
|
|
||||||
"viz": false
|
|
||||||
},
|
|
||||||
"insertNulls": false,
|
|
||||||
"lineInterpolation": "linear",
|
|
||||||
"lineWidth": 1,
|
|
||||||
"pointSize": 5,
|
|
||||||
"scaleDistribution": {
|
|
||||||
"type": "linear"
|
|
||||||
},
|
|
||||||
"showPoints": "auto",
|
|
||||||
"showValues": false,
|
|
||||||
"spanNulls": false,
|
|
||||||
"stacking": {
|
|
||||||
"group": "A",
|
|
||||||
"mode": "none"
|
|
||||||
},
|
|
||||||
"thresholdsStyle": {
|
|
||||||
"mode": "off"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mappings": [],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "green",
|
|
||||||
"value": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": 80
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 12,
|
|
||||||
"x": 12,
|
|
||||||
"y": 1
|
|
||||||
},
|
|
||||||
"id": 1,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"calcs": [
|
|
||||||
"lastNotNull",
|
|
||||||
"max"
|
|
||||||
],
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "right",
|
|
||||||
"showLegend": true
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"hideZeros": false,
|
|
||||||
"mode": "single",
|
|
||||||
"sort": "none"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pluginVersion": "12.4.0",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"expr": "node_hwmon_temp_celsius{hostname=~\"$hostname\"}",
|
|
||||||
"instant": false,
|
|
||||||
"legendFormat": "{{chip}} - {{sensor}}",
|
|
||||||
"range": true,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Temperature",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "thresholds"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"axisPlacement": "auto",
|
|
||||||
"fillOpacity": 70,
|
|
||||||
"hideFrom": {
|
|
||||||
"legend": false,
|
|
||||||
"tooltip": false,
|
|
||||||
"viz": false
|
|
||||||
},
|
|
||||||
"lineWidth": 0
|
|
||||||
},
|
|
||||||
"mappings": [],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "green",
|
|
||||||
"value": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 4,
|
|
||||||
"w": 24,
|
|
||||||
"x": 0,
|
|
||||||
"y": 9
|
|
||||||
},
|
|
||||||
"id": 5,
|
|
||||||
"interval": "1m",
|
|
||||||
"maxDataPoints": 200,
|
|
||||||
"options": {
|
|
||||||
"colWidth": 1,
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "list",
|
|
||||||
"placement": "bottom",
|
|
||||||
"showLegend": false
|
|
||||||
},
|
|
||||||
"rowHeight": 0.8,
|
|
||||||
"showValue": "never",
|
|
||||||
"tooltip": {
|
|
||||||
"hideZeros": false,
|
|
||||||
"mode": "none",
|
|
||||||
"sort": "none"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pluginVersion": "12.4.0",
|
|
||||||
"repeat": "hostname",
|
|
||||||
"repeatDirection": "h",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"editorMode": "code",
|
|
||||||
"expr": "node_zfs_zpool_state{hostname=~\"$hostname\", state=\"online\"} > 0",
|
|
||||||
"legendFormat": "{{zpool}} - {{state}}",
|
|
||||||
"range": true,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "ZFS Pools",
|
|
||||||
"type": "status-history"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "palette-classic"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"axisBorderShow": false,
|
|
||||||
"axisCenteredZero": false,
|
|
||||||
"axisColorMode": "text",
|
|
||||||
"axisLabel": "",
|
|
||||||
"axisPlacement": "auto",
|
|
||||||
"barAlignment": 0,
|
|
||||||
"barWidthFactor": 0.6,
|
|
||||||
"drawStyle": "line",
|
|
||||||
"fillOpacity": 0,
|
|
||||||
"gradientMode": "none",
|
|
||||||
"hideFrom": {
|
|
||||||
"legend": false,
|
|
||||||
"tooltip": false,
|
|
||||||
"viz": false
|
|
||||||
},
|
|
||||||
"insertNulls": false,
|
|
||||||
"lineInterpolation": "linear",
|
|
||||||
"lineWidth": 1,
|
|
||||||
"pointSize": 5,
|
|
||||||
"scaleDistribution": {
|
|
||||||
"type": "linear"
|
|
||||||
},
|
|
||||||
"showPoints": "auto",
|
|
||||||
"showValues": false,
|
|
||||||
"spanNulls": false,
|
|
||||||
"stacking": {
|
|
||||||
"group": "A",
|
|
||||||
"mode": "none"
|
|
||||||
},
|
|
||||||
"thresholdsStyle": {
|
|
||||||
"mode": "line+area"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mappings": [],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "transparent",
|
|
||||||
"value": 604808
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"unit": "s"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 12,
|
|
||||||
"x": 0,
|
|
||||||
"y": 13
|
|
||||||
},
|
|
||||||
"id": 2,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"calcs": [
|
|
||||||
"lastNotNull"
|
|
||||||
],
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "bottom",
|
|
||||||
"showLegend": true
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"hideZeros": false,
|
|
||||||
"mode": "single",
|
|
||||||
"sort": "none"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pluginVersion": "12.4.0",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"expr": "ssl_certificate_expiry_seconds",
|
|
||||||
"legendFormat": "{{exported_hostname}}: {{subject}} {{path}}",
|
|
||||||
"range": true,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Certificate Remaining Validity",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "palette-classic"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"axisBorderShow": false,
|
|
||||||
"axisCenteredZero": false,
|
|
||||||
"axisColorMode": "text",
|
|
||||||
"axisLabel": "",
|
|
||||||
"axisPlacement": "auto",
|
|
||||||
"axisSoftMin": 0,
|
|
||||||
"barAlignment": 0,
|
|
||||||
"barWidthFactor": 0.6,
|
|
||||||
"drawStyle": "line",
|
|
||||||
"fillOpacity": 0,
|
|
||||||
"gradientMode": "none",
|
|
||||||
"hideFrom": {
|
|
||||||
"legend": false,
|
|
||||||
"tooltip": false,
|
|
||||||
"viz": false
|
|
||||||
},
|
|
||||||
"insertNulls": false,
|
|
||||||
"lineInterpolation": "linear",
|
|
||||||
"lineWidth": 1,
|
|
||||||
"pointSize": 5,
|
|
||||||
"scaleDistribution": {
|
|
||||||
"type": "linear"
|
|
||||||
},
|
|
||||||
"showPoints": "auto",
|
|
||||||
"showValues": false,
|
|
||||||
"spanNulls": false,
|
|
||||||
"stacking": {
|
|
||||||
"group": "A",
|
|
||||||
"mode": "none"
|
|
||||||
},
|
|
||||||
"thresholdsStyle": {
|
|
||||||
"mode": "off"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mappings": [],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "green",
|
|
||||||
"value": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": 80
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"unit": "years"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 12,
|
|
||||||
"x": 12,
|
|
||||||
"y": 13
|
|
||||||
},
|
|
||||||
"id": 7,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"calcs": [
|
|
||||||
"lastNotNull"
|
|
||||||
],
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "right",
|
|
||||||
"showLegend": true
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"hideZeros": false,
|
|
||||||
"mode": "single",
|
|
||||||
"sort": "none"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pluginVersion": "12.4.0",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"editorMode": "builder",
|
|
||||||
"expr": "scrutiny_smart_power_on_hours{hostname=~\"$hostname\"} / (24 * 365)",
|
|
||||||
"legendFormat": "{{device_name}}",
|
|
||||||
"range": true,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Operating Years",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "continuous-YlRd"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"axisBorderShow": false,
|
|
||||||
"axisCenteredZero": false,
|
|
||||||
"axisColorMode": "text",
|
|
||||||
"axisLabel": "",
|
|
||||||
"axisPlacement": "auto",
|
|
||||||
"axisSoftMax": 100,
|
|
||||||
"axisSoftMin": 0,
|
|
||||||
"barAlignment": 0,
|
|
||||||
"barWidthFactor": 0.6,
|
|
||||||
"drawStyle": "line",
|
|
||||||
"fillOpacity": 0,
|
|
||||||
"gradientMode": "none",
|
|
||||||
"hideFrom": {
|
|
||||||
"legend": false,
|
|
||||||
"tooltip": false,
|
|
||||||
"viz": false
|
|
||||||
},
|
|
||||||
"insertNulls": false,
|
|
||||||
"lineInterpolation": "linear",
|
|
||||||
"lineStyle": {
|
|
||||||
"fill": "solid"
|
|
||||||
},
|
|
||||||
"lineWidth": 1,
|
|
||||||
"pointSize": 5,
|
|
||||||
"scaleDistribution": {
|
|
||||||
"type": "linear"
|
|
||||||
},
|
|
||||||
"showPoints": "auto",
|
|
||||||
"showValues": false,
|
|
||||||
"spanNulls": false,
|
|
||||||
"stacking": {
|
|
||||||
"group": "A",
|
|
||||||
"mode": "none"
|
|
||||||
},
|
|
||||||
"thresholdsStyle": {
|
|
||||||
"mode": "off"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mappings": [],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "green",
|
|
||||||
"value": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": 80
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"unit": "percent"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 12,
|
|
||||||
"x": 0,
|
|
||||||
"y": 21
|
|
||||||
},
|
|
||||||
"id": 6,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"calcs": [
|
|
||||||
"lastNotNull",
|
|
||||||
"max"
|
|
||||||
],
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "bottom",
|
|
||||||
"showLegend": true,
|
|
||||||
"width": 400
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"hideZeros": false,
|
|
||||||
"mode": "single",
|
|
||||||
"sort": "none"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pluginVersion": "12.4.0",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"disableTextWrap": false,
|
|
||||||
"editorMode": "builder",
|
|
||||||
"expr": "sum by(hostname, domain, mountpoint, device) (node_filesystem_free_bytes{hostname=~\"$hostname\"})",
|
|
||||||
"fullMetaSearch": false,
|
|
||||||
"hide": true,
|
|
||||||
"includeNullMetadata": false,
|
|
||||||
"legendFormat": "__auto",
|
|
||||||
"range": true,
|
|
||||||
"refId": "A",
|
|
||||||
"useBackend": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
|
||||||
},
|
|
||||||
"editorMode": "builder",
|
|
||||||
"expr": "sum by(hostname, domain, mountpoint, device) (node_filesystem_size_bytes{hostname=~\"$hostname\"})",
|
|
||||||
"hide": true,
|
|
||||||
"instant": false,
|
|
||||||
"legendFormat": "__auto",
|
|
||||||
"range": true,
|
|
||||||
"refId": "B"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"name": "Expression",
|
|
||||||
"type": "__expr__",
|
|
||||||
"uid": "__expr__"
|
|
||||||
},
|
|
||||||
"expression": "(1 - $A / $B) * 100",
|
|
||||||
"refId": "Disk Full",
|
|
||||||
"type": "math"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Filesystem Disk Usage",
|
|
||||||
"transformations": [
|
|
||||||
{
|
|
||||||
"id": "joinByField",
|
|
||||||
"options": {
|
|
||||||
"byField": "Time",
|
|
||||||
"mode": "outer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "palette-classic"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"axisBorderShow": false,
|
|
||||||
"axisCenteredZero": false,
|
|
||||||
"axisColorMode": "text",
|
|
||||||
"axisLabel": "",
|
|
||||||
"axisPlacement": "auto",
|
|
||||||
"axisSoftMin": 0,
|
|
||||||
"barAlignment": 0,
|
|
||||||
"barWidthFactor": 0.6,
|
|
||||||
"drawStyle": "line",
|
|
||||||
"fillOpacity": 0,
|
|
||||||
"gradientMode": "none",
|
|
||||||
"hideFrom": {
|
|
||||||
"legend": false,
|
|
||||||
"tooltip": false,
|
|
||||||
"viz": false
|
|
||||||
},
|
|
||||||
"insertNulls": false,
|
|
||||||
"lineInterpolation": "linear",
|
|
||||||
"lineWidth": 1,
|
|
||||||
"pointSize": 5,
|
|
||||||
"scaleDistribution": {
|
|
||||||
"type": "linear"
|
|
||||||
},
|
|
||||||
"showPoints": "auto",
|
|
||||||
"showValues": false,
|
|
||||||
"spanNulls": false,
|
|
||||||
"stacking": {
|
|
||||||
"group": "A",
|
|
||||||
"mode": "none"
|
|
||||||
},
|
|
||||||
"thresholdsStyle": {
|
|
||||||
"mode": "off"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mappings": [],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "green",
|
|
||||||
"value": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": 80
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 12,
|
|
||||||
"x": 12,
|
|
||||||
"y": 21
|
|
||||||
},
|
|
||||||
"id": 9,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"calcs": [
|
|
||||||
"lastNotNull"
|
|
||||||
],
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "right",
|
|
||||||
"showLegend": true
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"hideZeros": false,
|
|
||||||
"mode": "single",
|
|
||||||
"sort": "none"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pluginVersion": "12.4.0",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"editorMode": "builder",
|
|
||||||
"expr": "scrutiny_smart_power_on_hours{hostname=~\"$hostname\"}",
|
|
||||||
"legendFormat": "{{device_name}}",
|
|
||||||
"range": true,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Operating Years",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "thresholds"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"align": "auto",
|
|
||||||
"cellOptions": {
|
|
||||||
"type": "auto"
|
|
||||||
},
|
|
||||||
"footer": {
|
|
||||||
"reducers": []
|
|
||||||
},
|
|
||||||
"inspect": false
|
|
||||||
},
|
|
||||||
"mappings": [],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "green",
|
|
||||||
"value": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": 80
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 12,
|
|
||||||
"x": 12,
|
|
||||||
"y": 29
|
|
||||||
},
|
|
||||||
"id": 8,
|
|
||||||
"options": {
|
|
||||||
"cellHeight": "sm",
|
|
||||||
"showHeader": true
|
|
||||||
},
|
|
||||||
"pluginVersion": "12.4.0",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"editorMode": "builder",
|
|
||||||
"exemplar": false,
|
|
||||||
"expr": "scrutiny_device_info{hostname=~\"$hostname\"}",
|
|
||||||
"format": "table",
|
|
||||||
"instant": true,
|
|
||||||
"legendFormat": "{{device_name}}",
|
|
||||||
"range": false,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Disk Info",
|
|
||||||
"transformations": [
|
|
||||||
{
|
|
||||||
"id": "organize",
|
|
||||||
"options": {
|
|
||||||
"excludeByName": {
|
|
||||||
"Time": true,
|
|
||||||
"Value": true,
|
|
||||||
"__name__": true,
|
|
||||||
"domain": true,
|
|
||||||
"hostname": true,
|
|
||||||
"instance": true,
|
|
||||||
"job": true,
|
|
||||||
"wwn": false
|
|
||||||
},
|
|
||||||
"includeByName": {},
|
|
||||||
"indexByName": {},
|
|
||||||
"renameByName": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"type": "table"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"preload": false,
|
|
||||||
"schemaVersion": 42,
|
|
||||||
"tags": [],
|
|
||||||
"templating": {
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"current": {
|
|
||||||
"text": "baryum",
|
|
||||||
"value": "baryum"
|
|
||||||
},
|
|
||||||
"definition": "label_values(up,hostname)",
|
|
||||||
"name": "hostname",
|
|
||||||
"options": [],
|
|
||||||
"query": {
|
|
||||||
"qryType": 1,
|
|
||||||
"query": "label_values(up,hostname)",
|
|
||||||
"refId": "PrometheusVariableQueryEditor-VariableQuery"
|
|
||||||
},
|
|
||||||
"refresh": 1,
|
|
||||||
"regex": "",
|
|
||||||
"regexApplyTo": "value",
|
|
||||||
"type": "query"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"time": {
|
|
||||||
"from": "now-30m",
|
|
||||||
"to": "now"
|
|
||||||
},
|
|
||||||
"timepicker": {},
|
|
||||||
"timezone": "browser",
|
|
||||||
"title": "Node Health",
|
|
||||||
"uid": "edhuvl28vpjwge",
|
|
||||||
"version": 25,
|
|
||||||
"weekStart": ""
|
|
||||||
}
|
|
||||||
143
modules/blocks/monitoring/dashboards/SSL.json
Normal file
143
modules/blocks/monitoring/dashboards/SSL.json
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana",
|
||||||
|
"uid": "-- Grafana --"
|
||||||
|
},
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"fiscalYearStartMonth": 0,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": 16,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"barWidthFactor": 0.6,
|
||||||
|
"drawStyle": "line",
|
||||||
|
"fillOpacity": 0,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"insertNulls": false,
|
||||||
|
"lineInterpolation": "linear",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"showPoints": "auto",
|
||||||
|
"spanNulls": false,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "line+area"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"min": 0,
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"color": "transparent",
|
||||||
|
"value": 604808
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"unit": "s"
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 12,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 3,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": [
|
||||||
|
"lastNotNull"
|
||||||
|
],
|
||||||
|
"displayMode": "table",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true,
|
||||||
|
"sortBy": "Last *",
|
||||||
|
"sortDesc": false
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pluginVersion": "11.4.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "prometheus",
|
||||||
|
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"expr": "ssl_certificate_expiry_seconds",
|
||||||
|
"legendFormat": "{{exported_hostname}}: {{subject}} {{path}}",
|
||||||
|
"range": true,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Certificate Remaining Validity",
|
||||||
|
"type": "timeseries"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"preload": false,
|
||||||
|
"schemaVersion": 40,
|
||||||
|
"tags": [],
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-6h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {},
|
||||||
|
"timezone": "browser",
|
||||||
|
"title": "SSL Certificates",
|
||||||
|
"uid": "ae818js0bvw8wb",
|
||||||
|
"version": 8,
|
||||||
|
"weekStart": ""
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 349 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 499 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 255 KiB |
|
|
@ -17,13 +17,9 @@ This block sets up the monitoring stack for Self Host Blocks. It is composed of:
|
||||||
- Registration is enabled through SSO.
|
- Registration is enabled through SSO.
|
||||||
- Access through [subdomain](#blocks-monitoring-options-shb.monitoring.subdomain) using reverse proxy.
|
- Access through [subdomain](#blocks-monitoring-options-shb.monitoring.subdomain) using reverse proxy.
|
||||||
- Access through [HTTPS](#blocks-monitoring-options-shb.monitoring.ssl) using reverse proxy.
|
- Access through [HTTPS](#blocks-monitoring-options-shb.monitoring.ssl) using reverse proxy.
|
||||||
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#blocks-monitoring-usage-applicationdashboard)
|
|
||||||
- Out of the box integration with [Scrutiny](https://github.com/AnalogJ/scrutiny) service for Hard Drives monitoring. [Manual](#blocks-monitoring-usage-scrutiny)
|
|
||||||
|
|
||||||
## Usage {#blocks-monitoring-usage}
|
## Usage {#blocks-monitoring-usage}
|
||||||
|
|
||||||
### Initial Configuration {#blocks-monitoring-usage-configuration}
|
|
||||||
|
|
||||||
The following snippet assumes a few blocks have been setup already:
|
The following snippet assumes a few blocks have been setup already:
|
||||||
|
|
||||||
- the [secrets block](usage.html#usage-secrets) with SOPS,
|
- the [secrets block](usage.html#usage-secrets) with SOPS,
|
||||||
|
|
@ -38,16 +34,16 @@ The following snippet assumes a few blocks have been setup already:
|
||||||
subdomain = "grafana";
|
subdomain = "grafana";
|
||||||
inherit domain;
|
inherit domain;
|
||||||
contactPoints = [ "me@example.com" ];
|
contactPoints = [ "me@example.com" ];
|
||||||
adminPassword.result = config.shb.sops.secret."monitoring/admin_password".result;
|
adminPassword.result = config.sops.secrets."monitoring/admin_password".result;
|
||||||
secretKey.result = config.shb.sops.secret."monitoring/secret_key".result;
|
secretKey.result = config.sops.secrets."monitoring/secret_key".result;
|
||||||
|
|
||||||
sso = {
|
sso = {
|
||||||
enable = true;
|
enable = true;
|
||||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
||||||
|
|
||||||
sharedSecret.result = config.shb.sops.secret."monitoring/oidcSecret".result;
|
sharedSecret.result = config.shb.sops.secret.oidcSecret.result;
|
||||||
sharedSecretForAuthelia.result = config.shb.sops.secret."monitoring/oidcAutheliaSecret".result;
|
sharedSecretForAuthelia.result = config.shb.sops.secret.oidcAutheliaSecret.result;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.sops.secret."monitoring/admin_password".request = config.shb.monitoring.adminPassword.request;
|
shb.sops.secret."monitoring/admin_password".request = config.shb.monitoring.adminPassword.request;
|
||||||
|
|
@ -71,7 +67,7 @@ LDAP groups are created automatically.
|
||||||
|
|
||||||
### SMTP {#blocks-monitoring-usage-smtp}
|
### SMTP {#blocks-monitoring-usage-smtp}
|
||||||
|
|
||||||
I recommend adding an SMTP server configuration so you receive alerts by email:
|
I recommend adding a STMP server configuration so you receive alerts by email:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
shb.monitoring.smtp = {
|
shb.monitoring.smtp = {
|
||||||
|
|
@ -114,42 +110,6 @@ You might for example want to update the metrics retention time with:
|
||||||
services.prometheus.retentionTime = "60d";
|
services.prometheus.retentionTime = "60d";
|
||||||
```
|
```
|
||||||
|
|
||||||
### Application Dashboard {#blocks-monitoring-usage-applicationdashboard}
|
|
||||||
|
|
||||||
Integration with the [dashboard contract](contracts-dashboard.html) is provided
|
|
||||||
by the [dashboard option](#blocks-monitoring-options-shb.monitoring.dashboard).
|
|
||||||
|
|
||||||
For example using the [Homepage](services-homepage.html) service:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.homepage.servicesGroups.Admin.services.Grafana = {
|
|
||||||
sortOrder = 10;
|
|
||||||
dashboard.request = config.shb.monitoring.dashboard.request;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
There is also an integration for the scrutiny service, see next section.
|
|
||||||
|
|
||||||
### Scrutiny {#blocks-monitoring-usage-scrutiny}
|
|
||||||
|
|
||||||
Integration with the [Scrutiny](https://github.com/AnalogJ/scrutiny) service is enabled by default and setup automatically.
|
|
||||||
|
|
||||||
The web interface will be served under the [scrutiny.subdomain](#blocks-monitoring-options-shb.monitoring.scrutiny.subdomain) option.
|
|
||||||
If you don't want the web interface, set the option to `null`.
|
|
||||||
|
|
||||||
For integration with the [dashboard contract](contracts-dashboard.html):
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.homepage.servicesGroups.Admin.services.Scrutiny = {
|
|
||||||
sortOrder = 11;
|
|
||||||
dashboard.request = config.shb.monitoring.scrutiny.dashboard.request;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Provisioning {#blocks-monitoring-provisioning}
|
## Provisioning {#blocks-monitoring-provisioning}
|
||||||
|
|
||||||
Self Host Blocks will create automatically the following resources:
|
Self Host Blocks will create automatically the following resources:
|
||||||
|
|
@ -202,7 +162,7 @@ This dashboard is used to monitor a [deluge](./services-deluge.html) instance.
|
||||||
|
|
||||||
## Backups Dashboard and Alert {#blocks-monitoring-backup}
|
## Backups Dashboard and Alert {#blocks-monitoring-backup}
|
||||||
|
|
||||||
This dashboard shows Restic and BorgBackup backup jobs, or any job with "backup" in the systemd service name.
|
This dashboard show Restic and BorgBackup backup jobs, or any job with "backup" in the systemd service name.
|
||||||
|
|
||||||
### Dashboard {#blocks-monitoring-backup-dashboard}
|
### Dashboard {#blocks-monitoring-backup-dashboard}
|
||||||
|
|
||||||
|
|
@ -245,55 +205,6 @@ and the total requests to that service exceeds 1%.
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## SSL Certificates Dashboard and Alert {#blocks-monitoring-ssl}
|
|
||||||
|
|
||||||
This dashboard shows Let's Encrypt renewal and setup jobs,
|
|
||||||
or any job starting with "acme-" in the systemd service name.
|
|
||||||
|
|
||||||
### Dashboard {#blocks-monitoring-ssl-dashboard}
|
|
||||||
|
|
||||||
Variables:
|
|
||||||
|
|
||||||
- The "Job" variable allows to focus on one or more certificate. "All" is the default.
|
|
||||||
|
|
||||||
Graphs:
|
|
||||||
|
|
||||||
- "Certificate Remaining Validity": Shows in how long will certificates expire.
|
|
||||||
It shows all files under `/var/lib/acme`.
|
|
||||||
An annotation will show up when the "Certificate Did Not Renew" alert fired or resolved.
|
|
||||||
- "Schedule": Shows when a job will run.
|
|
||||||
The unit is "Datetime from Now" meaning it shows when a job ran or will run relative to the current time.
|
|
||||||
- "Jobs in the Past Week": Shows stats on all renewal jobs that ran in the past.
|
|
||||||
It is sorted by the "Failed" column in descending order.
|
|
||||||
This way, one can directly see when a job has failures.
|
|
||||||
Note, the stats is not accurate because detecting jobs taking taking less than 15 seconds
|
|
||||||
is not supported well.
|
|
||||||
- "Job Runs": Shows when a renewal job ran.
|
|
||||||
Normally, jobs running for less than 15 seconds will not show up in the graph.
|
|
||||||
We crafted a query that still shows them but the length is 100 seconds, even if the job
|
|
||||||
took less time to run.
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
### Alerts {#blocks-monitoring-ssl-alerts}
|
|
||||||
|
|
||||||
- The "Certificate Did Not Renew" alert will fire if a backup job did not run at all in the last 24 hours
|
|
||||||
or if all runs were failures in the last 24 hours.
|
|
||||||
It will show up as annotations in the "Schedule" panel of the dashboard.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Impermanence {#blocks-monitoring-impermanence}
|
|
||||||
|
|
||||||
To save the fluent-bit folder in an impermanence setup, add:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.zfs.datasets."safe/monitoring-fluent-bit".path = config.shb.jellyfin.impermanence.fluent-bit;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Options Reference {#blocks-monitoring-options}
|
## Options Reference {#blocks-monitoring-options}
|
||||||
|
|
||||||
```{=include=} options
|
```{=include=} options
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,7 @@
|
||||||
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "min by(subject) (ssl_certificate_expiry_seconds)",
|
"expr": "ssl_certificate_expiry_seconds",
|
||||||
"interval": "",
|
"interval": "",
|
||||||
"intervalMs": 15000,
|
"intervalMs": 15000,
|
||||||
"legendFormat": "{{exported_hostname}}: {{subject}} {{path}}",
|
"legendFormat": "{{exported_hostname}}: {{subject}} {{path}}",
|
||||||
|
|
@ -275,7 +275,7 @@
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"exemplar": false,
|
"exemplar": false,
|
||||||
"expr": "(\n # Timer triggered at least once in the last 24h\n label_replace((\n time()\n -\n systemd_timer_last_trigger_seconds{name=~\".*backup.*.timer\"}\n ) < 24*60*60, \"name\", \"$1.service\", \"name\", \"(.*).timer\")\n AND on(name)\n # At least one failure in the last 24h\n (\n max_over_time(systemd_unit_state{name=~\".*backup.*.service\", state=\"failed\"}[24h]) > 0\n )\n AND on(name)\n # No successes in the last 24h\n (\n max_over_time(systemd_unit_state{name=~\".*backup.*.service\", state=\"inactive\"}[24h]) == 0\n )\n)",
|
"expr": "(\n # No run in the last 24 hours\n systemd_timer_last_trigger_seconds{name=~\".*backup.*.timer\"}\n ==\n systemd_timer_last_trigger_seconds{name=~\".*backup.*.timer\"} offset 24h\n)\nOR\n(\n # All runs in last 24 hours failed\n label_replace((\n time()\n -\n systemd_timer_last_trigger_seconds{name=~\".*backup.*.timer\"}\n ) < 24*60*60, \"name\", \"$1.service\", \"name\", \"(.*).timer\")\n AND on(name)\n (\n max_over_time(systemd_unit_state{name=~\".*backup.*.service\", state=\"failed\"}[24h]) > 0\n )\n)",
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"interval": "",
|
"interval": "",
|
||||||
"intervalMs": 15000,
|
"intervalMs": 15000,
|
||||||
|
|
@ -371,12 +371,12 @@
|
||||||
],
|
],
|
||||||
"dashboardUid": "f05500d0-15ed-4719-b68d-fb898ca13cc8",
|
"dashboardUid": "f05500d0-15ed-4719-b68d-fb898ca13cc8",
|
||||||
"panelId": 15,
|
"panelId": 15,
|
||||||
"noDataState": "OK",
|
"noDataState": "NoData",
|
||||||
"execErrState": "Error",
|
"execErrState": "Error",
|
||||||
"annotations": {
|
"annotations": {
|
||||||
"__dashboardUid__": "f05500d0-15ed-4719-b68d-fb898ca13cc8",
|
"__dashboardUid__": "f05500d0-15ed-4719-b68d-fb898ca13cc8",
|
||||||
"__panelId__": "15",
|
"__panelId__": "15",
|
||||||
"summary": "A backup did not run in the last 24 hours."
|
"summary": "A backup for service {{name}} did not run in the last 24 hours."
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"role": "sysadmin"
|
"role": "sysadmin"
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,8 @@ let
|
||||||
};
|
};
|
||||||
|
|
||||||
upstream = lib.mkOption {
|
upstream = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Upstream url to be protected.";
|
description = "Upstream url to be protected.";
|
||||||
default = null;
|
|
||||||
example = "http://127.0.0.1:1234";
|
example = "http://127.0.0.1:1234";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -49,40 +48,10 @@ let
|
||||||
default = [ ];
|
default = [ ];
|
||||||
description = "Authelia rule configuration";
|
description = "Authelia rule configuration";
|
||||||
example = lib.literalExpression ''
|
example = lib.literalExpression ''
|
||||||
[
|
[{
|
||||||
# Protect /admin endpoint with 2FA
|
policy = "two_factor";
|
||||||
# and only allow access to admin users.
|
subject = ["group:service_user"];
|
||||||
{
|
}]'';
|
||||||
domain = "myapp.example.com";
|
|
||||||
policy = "two_factor";
|
|
||||||
subject = [ "group:service_admin" ];
|
|
||||||
resources = [
|
|
||||||
"^/admin"
|
|
||||||
];
|
|
||||||
}
|
|
||||||
# Leave /api endpoint open - assumes an API key is used to protect it.
|
|
||||||
{
|
|
||||||
domain = "myapp.example.com";
|
|
||||||
policy = "bypass";
|
|
||||||
resources = [
|
|
||||||
"^/api"
|
|
||||||
];
|
|
||||||
},
|
|
||||||
# Protect rest of app with 1FA
|
|
||||||
# and allow access to normal and admin users.
|
|
||||||
{
|
|
||||||
domain = "myapp.example.com";
|
|
||||||
policy = "one_factor";
|
|
||||||
subject = ["group:service_user"];
|
|
||||||
},
|
|
||||||
]
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
phpForwardAuth = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
default = false;
|
|
||||||
description = "Authelia rule configuration";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
extraConfig = lib.mkOption {
|
extraConfig = lib.mkOption {
|
||||||
|
|
@ -164,58 +133,52 @@ in
|
||||||
sslCertificateKey = lib.mkIf (!(isNull c.ssl)) c.ssl.paths.key;
|
sslCertificateKey = lib.mkIf (!(isNull c.ssl)) c.ssl.paths.key;
|
||||||
|
|
||||||
# Taken from https://github.com/authelia/authelia/issues/178
|
# Taken from https://github.com/authelia/authelia/issues/178
|
||||||
locations."/".extraConfig =
|
locations."/".extraConfig = ''
|
||||||
lib.optionalString (c.upstream != null) ''
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
add_header X-Content-Type-Options nosniff;
|
||||||
add_header X-Content-Type-Options nosniff;
|
add_header X-Frame-Options "SAMEORIGIN";
|
||||||
add_header X-Frame-Options "SAMEORIGIN";
|
add_header X-XSS-Protection "1; mode=block";
|
||||||
add_header X-XSS-Protection "1; mode=block";
|
add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive";
|
||||||
add_header X-Robots-Tag "noindex, nofollow, nosnippet, noarchive";
|
add_header X-Download-Options noopen;
|
||||||
add_header X-Download-Options noopen;
|
add_header X-Permitted-Cross-Domain-Policies none;
|
||||||
add_header X-Permitted-Cross-Domain-Policies none;
|
|
||||||
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
proxy_cache_bypass $http_upgrade;
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
|
||||||
proxy_pass ${c.upstream};
|
proxy_pass ${c.upstream};
|
||||||
''
|
''
|
||||||
+ c.extraConfig
|
+ c.extraConfig
|
||||||
+ lib.optionalString (c.authEndpoint != null) ''
|
+ lib.optionalString (c.authEndpoint != null) ''
|
||||||
auth_request /authelia;
|
auth_request /authelia;
|
||||||
auth_request_set $user $upstream_http_remote_user;
|
auth_request_set $user $upstream_http_remote_user;
|
||||||
auth_request_set $groups $upstream_http_remote_groups;
|
auth_request_set $groups $upstream_http_remote_groups;
|
||||||
proxy_set_header X-Forwarded-User $user;
|
proxy_set_header X-Forwarded-User $user;
|
||||||
proxy_set_header X-Forwarded-Groups $groups;
|
proxy_set_header X-Forwarded-Groups $groups;
|
||||||
# TODO: Are those needed?
|
# TODO: Are those needed?
|
||||||
# auth_request_set $name $upstream_http_remote_name;
|
# auth_request_set $name $upstream_http_remote_name;
|
||||||
# auth_request_set $email $upstream_http_remote_email;
|
# auth_request_set $email $upstream_http_remote_email;
|
||||||
# proxy_set_header Remote-Name $name;
|
# proxy_set_header Remote-Name $name;
|
||||||
# proxy_set_header Remote-Email $email;
|
# proxy_set_header Remote-Email $email;
|
||||||
# TODO: Would be nice to have this working, I think.
|
# TODO: Would be nice to have this working, I think.
|
||||||
# set $new_cookie $http_cookie;
|
# set $new_cookie $http_cookie;
|
||||||
# if ($http_cookie ~ "(.*)(?:^|;)\s*example\.com\.session\.id=[^;]+(.*)") {
|
# if ($http_cookie ~ "(.*)(?:^|;)\s*example\.com\.session\.id=[^;]+(.*)") {
|
||||||
# set $new_cookie $1$2;
|
# set $new_cookie $1$2;
|
||||||
# }
|
# }
|
||||||
# proxy_set_header Cookie $new_cookie;
|
# proxy_set_header Cookie $new_cookie;
|
||||||
|
|
||||||
auth_request_set $redirect $scheme://$http_host$request_uri;
|
auth_request_set $redirect $scheme://$http_host$request_uri;
|
||||||
error_page 401 =302 ${c.authEndpoint}?rd=$redirect;
|
error_page 401 =302 ${c.authEndpoint}?rd=$redirect;
|
||||||
error_page 403 = ${c.authEndpoint}/error/403;
|
error_page 403 = ${c.authEndpoint}/error/403;
|
||||||
'';
|
|
||||||
|
|
||||||
locations."~ \\.php$".extraConfig = lib.mkIf (c.phpForwardAuth) ''
|
|
||||||
fastcgi_param HTTP_X_FORWARDED_USER $user;
|
|
||||||
fastcgi_param HTTP_X_FORWARDED_GROUPS $groups;
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
# Virtual endpoint created by nginx to forward auth requests.
|
# Virtual endpoint created by nginx to forward auth requests.
|
||||||
locations."/authelia".extraConfig = lib.mkIf (c.authEndpoint != null) ''
|
locations."/authelia".extraConfig = lib.mkIf (!(isNull c.authEndpoint)) ''
|
||||||
internal;
|
internal;
|
||||||
proxy_pass ${c.authEndpoint}/api/verify;
|
proxy_pass ${c.authEndpoint}/api/verify;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,211 +0,0 @@
|
||||||
# Nginx Block {#blocks-nginx}
|
|
||||||
|
|
||||||
Defined in [`/modules/blocks/nginx.nix`](@REPO@/modules/blocks/nginx.nix).
|
|
||||||
|
|
||||||
This block sets up a [Nginx](https://nginx.org/) instance.
|
|
||||||
|
|
||||||
It complements the upstream nixpkgs with some authentication and debugging improvements as shows in the Usage section.
|
|
||||||
|
|
||||||
## Usage {#blocks-nginx-usage}
|
|
||||||
|
|
||||||
### Access Logging {#blocks-nginx-usage-accesslog}
|
|
||||||
|
|
||||||
JSON access logging is enabled with the [`shb.nginx.accessLog`](#blocks-nginx-options-shb.nginx.accessLog) option:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.nginx.accessLog = true;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Looking at the systemd logs (`journalctl -fu nginx`) will show for example:
|
|
||||||
|
|
||||||
```json
|
|
||||||
nginx[969]: server nginx:
|
|
||||||
{
|
|
||||||
"remote_addr":"192.168.1.1",
|
|
||||||
"remote_user":"-",
|
|
||||||
"time_local":"29/Dec/2025:14:22:41 +0000",
|
|
||||||
"request":"POST /api/firstfactor HTTP/2.0",
|
|
||||||
"request_length":"264",
|
|
||||||
"server_name":"auth_example_com",
|
|
||||||
"status":"200",
|
|
||||||
"bytes_sent":"855",
|
|
||||||
"body_bytes_sent":"60",
|
|
||||||
"referrer":"-",
|
|
||||||
"user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0",
|
|
||||||
"gzip_ration":"-",
|
|
||||||
"post":"{\x22username\x22:\x22charlie\x22,\x22password\x22:\x22CharliePassword\x22,\x22keepMeLoggedIn\x22:false,\x22targetURL\x22:\x22https://f.example.com/\x22,\x22requestMethod\x22:null}",
|
|
||||||
"upstream_addr":"127.0.0.1:9091",
|
|
||||||
"upstream_status":"200",
|
|
||||||
"request_time":"0.873",
|
|
||||||
"upstream_response_time":"0.873",
|
|
||||||
"upstream_connect_time":"0.001",
|
|
||||||
"upstream_header_time":"0.872"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This _will_ log the body of POST queries so it should only be enabled for debug logging.
|
|
||||||
|
|
||||||
### Debug Logging {#blocks-nginx-usage-debuglog}
|
|
||||||
|
|
||||||
Debug logging is enabled with the [`shb.nginx.debugLog`](#blocks-nginx-options-shb.nginx.debugLog) option:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.nginx.debugLog = true;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
If enabled, it sets:
|
|
||||||
|
|
||||||
```
|
|
||||||
error_log stderr warn;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Virtual Host Upstream Proxy {#blocks-nginx-usage-upstream}
|
|
||||||
|
|
||||||
Easy upstream proxy setup is done with the [`shb.nginx.vhosts.*.upstream`](#blocks-nginx-options-shb.nginx.vhosts._.upstream) option:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.nginx.vhosts = [
|
|
||||||
{
|
|
||||||
domain = "example.com";
|
|
||||||
subdomain = "mysubdomain";
|
|
||||||
upstream = "http://127.0.0.1:9090";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This will set also a few headers.
|
|
||||||
Some are shown here and others please see in the [nginx](@REPO@/modules/blocks/nginx.nix) module:
|
|
||||||
|
|
||||||
- `Host` = `$host`;
|
|
||||||
- `X-Real-IP` = `$remote_addr`;
|
|
||||||
- `X-Forwarded-For` = `$proxy_add_x_forwarded_for`;
|
|
||||||
- `X-Forwarded-Proto` = `$scheme`;
|
|
||||||
|
|
||||||
### Virtual Host SSL Generator Contract Integration {#blocks-nginx-usage-ssl}
|
|
||||||
|
|
||||||
This module integrates with the [SSL Generator Contract](./contracts-ssl.html)
|
|
||||||
to setup HTTPs with the [`shb.nginx.vhosts.*.ssl`](#blocks-nginx-options-shb.nginx.vhosts._.ssl) option:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.nginx.vhosts = [
|
|
||||||
{
|
|
||||||
domain = "example.com";
|
|
||||||
subdomain = "mysubdomain";
|
|
||||||
ssl = config.shb.certs.certs.letsencrypt.${domain};;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
shb.certs.certs.letsencrypt.${domain} = {
|
|
||||||
inherit domain;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Virtual Host SHB Forward Authentication {#blocks-nginx-usage-shbforwardauth}
|
|
||||||
|
|
||||||
For services provided by SelfHostBlocks that do not handle [OIDC integration][OIDC],
|
|
||||||
this block can provide [forward authentication][] which still allows the service
|
|
||||||
to still be protected by an SSO server.
|
|
||||||
|
|
||||||
[OIDC]: blocks-authelia.html#blocks-authelia-shb-oidc
|
|
||||||
|
|
||||||
The user could still be required to authenticate to the service itself,
|
|
||||||
although some services can automatically users authorized by Authelia.
|
|
||||||
|
|
||||||
[forward authentication]: https://doc.traefik.io/traefik/middlewares/http/forwardauth/
|
|
||||||
|
|
||||||
Integrating with this block is done with the following code:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
shb.<services>.authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
|
||||||
```
|
|
||||||
|
|
||||||
### Virtual Host Forward Authentication {#blocks-nginx-usage-forwardauth}
|
|
||||||
|
|
||||||
Forward authentication is when Nginx talks with the SSO service directly
|
|
||||||
and the user is authenticated before reaching the upstream application.
|
|
||||||
|
|
||||||
The SSO service responds with the username, group and more information about the user.
|
|
||||||
This is then forwarded to the upstream application by Nginx.
|
|
||||||
|
|
||||||
Note that _every_ request is authenticated this way with the SSO server
|
|
||||||
so it involves more hops than a direct [OIDC integration](blocks-authelia.html#blocks-authelia-shb-oidc).
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.nginx.vhosts = [
|
|
||||||
{
|
|
||||||
domain = "example.com";
|
|
||||||
subdomain = "mysubdomain";
|
|
||||||
authEndpoint = "authelia.example.com";
|
|
||||||
autheliaRules = [
|
|
||||||
[
|
|
||||||
# Protect /admin endpoint with 2FA
|
|
||||||
# and only allow access to admin users.
|
|
||||||
{
|
|
||||||
domain = "myapp.example.com";
|
|
||||||
policy = "two_factor";
|
|
||||||
subject = [ "group:service_admin" ];
|
|
||||||
resources = [
|
|
||||||
"^/admin"
|
|
||||||
];
|
|
||||||
}
|
|
||||||
# Leave /api endpoint open - assumes an API key is used to protect it.
|
|
||||||
{
|
|
||||||
domain = "myapp.example.com";
|
|
||||||
policy = "bypass";
|
|
||||||
resources = [
|
|
||||||
"^/api"
|
|
||||||
];
|
|
||||||
},
|
|
||||||
# Protect rest of app with 1FA
|
|
||||||
# and allow access to normal and admin users.
|
|
||||||
{
|
|
||||||
domain = "myapp.example.com";
|
|
||||||
policy = "one_factor";
|
|
||||||
subject = ["group:service_user"];
|
|
||||||
},
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
If PHP is used with fastCGI,
|
|
||||||
extra headers must be added by enabling the [`shb.nginx.vhosts.*.phpForwardAuth`](#blocks-nginx-options-shb.nginx.vhosts._.phpForwardAuth) option.
|
|
||||||
|
|
||||||
### Virtual Host Extra Config {#blocks-nginx-usage-extraconfig}
|
|
||||||
|
|
||||||
To add extra configuration to a virtual host,
|
|
||||||
use the [`shb.nginx.vhosts.*.extraConfig`](#blocks-nginx-options-shb.nginx.vhosts._.extraConfig) option.
|
|
||||||
This can be used to add headers, for example:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.nginx.vhosts = [
|
|
||||||
{
|
|
||||||
domain = "example.com";
|
|
||||||
subdomain = "mysubdomain";
|
|
||||||
extraConfig = ''
|
|
||||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Options Reference {#blocks-nginx-options}
|
|
||||||
|
|
||||||
```{=include=} options
|
|
||||||
id-prefix: blocks-nginx-options-
|
|
||||||
list-id: selfhostblocks-block-nginx-options
|
|
||||||
source: @OPTIONS_JSON@
|
|
||||||
```
|
|
||||||
|
|
@ -73,7 +73,7 @@ in
|
||||||
backupName = "postgres.sql";
|
backupName = "postgres.sql";
|
||||||
|
|
||||||
backupCmd = ''
|
backupCmd = ''
|
||||||
${pkgs.postgresql}/bin/pg_dumpall --clean --if-exists | ${pkgs.gzip}/bin/gzip --rsyncable
|
${pkgs.postgresql}/bin/pg_dumpall | ${pkgs.gzip}/bin/gzip --rsyncable
|
||||||
'';
|
'';
|
||||||
|
|
||||||
restoreCmd = ''
|
restoreCmd = ''
|
||||||
|
|
@ -162,7 +162,7 @@ in
|
||||||
{ username, passwordFile, ... }:
|
{ username, passwordFile, ... }:
|
||||||
''
|
''
|
||||||
password := trim(both from replace(pg_read_file('${passwordFile}'), E'\n', '''));
|
password := trim(both from replace(pg_read_file('${passwordFile}'), E'\n', '''));
|
||||||
EXECUTE format('ALTER ROLE "${username}" WITH PASSWORD '''%s''';', password);
|
EXECUTE format('ALTER ROLE ${username} WITH PASSWORD '''%s''';', password);
|
||||||
'';
|
'';
|
||||||
cfgsWithPasswords = builtins.filter (cfg: cfg.passwordFile != null) ensureCfgs;
|
cfgsWithPasswords = builtins.filter (cfg: cfg.passwordFile != null) ensureCfgs;
|
||||||
in
|
in
|
||||||
|
|
|
||||||
|
|
@ -6,46 +6,17 @@ This block sets up a [PostgreSQL][] database.
|
||||||
|
|
||||||
[postgresql]: https://www.postgresql.org/
|
[postgresql]: https://www.postgresql.org/
|
||||||
|
|
||||||
Compared to the upstream nixpkgs module, this module also sets up:
|
## Tests {#blocks-postgresql-tests}
|
||||||
|
|
||||||
- Enabling TCP/IP login and also accepting password authentication from localhost with [`shb.postgresql.enableTCPIP`](#blocks-postgresql-options-shb.postgresql.enableTCPIP).
|
Specific integration tests are defined in [`/test/blocks/postgresql.nix`](@REPO@/test/blocks/postgresql.nix).
|
||||||
- Enhance the `ensure*` upstream option by setting up a database's password from a password file with [`shb.postgresql.ensures`](#blocks-postgresql-options-shb.postgresql.ensures).
|
|
||||||
- Debug logging with `auto_explain` and `pg_stat_statements` with [`shb.postgresql.debug`](#blocks-postgresql-options-shb.postgresql.debug).
|
|
||||||
|
|
||||||
## Usage {#blocks-postgresql-usage}
|
## Database Backup Requester Contracts {#blocks-postgresql-contract-databasebackup}
|
||||||
|
|
||||||
### Ensure User and Database {#blocks-postgresql-ensures}
|
|
||||||
|
|
||||||
Ensure a database and user exists:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
shb.postgresql.ensures = [
|
|
||||||
{
|
|
||||||
username = "firefly-iii";
|
|
||||||
database = "firefly-iii";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
Also set up the database password from a file path:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
shb.postgresql.ensures = [
|
|
||||||
{
|
|
||||||
username = "firefly-iii";
|
|
||||||
database = "firefly-iii";
|
|
||||||
passwordFile = "/run/secrets/firefly-iii_db_password";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Backup Requester Contracts {#blocks-postgresql-contract-databasebackup}
|
|
||||||
|
|
||||||
This block can be backed up using the [database backup](contracts-databasebackup.html) contract.
|
This block can be backed up using the [database backup](contracts-databasebackup.html) contract.
|
||||||
|
|
||||||
Contract integration tests are defined in [`/test/contracts/databasebackup.nix`](@REPO@/test/contracts/databasebackup.nix).
|
Contract integration tests are defined in [`/test/contracts/databasebackup.nix`](@REPO@/test/contracts/databasebackup.nix).
|
||||||
|
|
||||||
#### Backing up All Databases {#blocks-postgresql-contract-databasebackup-all}
|
### Backing up All Databases {#blocks-postgresql-contract-databasebackup-all}
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
{
|
{
|
||||||
|
|
@ -59,10 +30,6 @@ Contract integration tests are defined in [`/test/contracts/databasebackup.nix`]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tests {#blocks-postgresql-tests}
|
|
||||||
|
|
||||||
Specific integration tests are defined in [`/test/blocks/postgresql.nix`](@REPO@/test/blocks/postgresql.nix).
|
|
||||||
|
|
||||||
## Options Reference {#blocks-postgresql-options}
|
## Options Reference {#blocks-postgresql-options}
|
||||||
|
|
||||||
```{=include=} options
|
```{=include=} options
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ let
|
||||||
mode = "0400";
|
mode = "0400";
|
||||||
owner = config.request.user;
|
owner = config.request.user;
|
||||||
ownerText = "[shb.restic.${prefix}.<name>.request.user](#blocks-restic-options-shb.restic.${prefix}._name_.request.user)";
|
ownerText = "[shb.restic.${prefix}.<name>.request.user](#blocks-restic-options-shb.restic.${prefix}._name_.request.user)";
|
||||||
restartUnits = [ "${fullName name config.settings.repository}.service" ];
|
restartUnits = [ (fullName name config.settings.repository) ];
|
||||||
restartUnitsText = "[ [shb.restic.${prefix}.<name>.settings.repository](#blocks-restic-options-shb.restic.${prefix}._name_.settings.repository) ]";
|
restartUnitsText = "[ [shb.restic.${prefix}.<name>.settings.repository](#blocks-restic-options-shb.restic.${prefix}._name_.settings.repository) ]";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -102,7 +102,7 @@ let
|
||||||
OnCalendar = "daily";
|
OnCalendar = "daily";
|
||||||
Persistent = true;
|
Persistent = true;
|
||||||
};
|
};
|
||||||
description = "When to run the backup. See {manpage}`systemd.timer(5)` for details.";
|
description = ''When to run the backup. See {manpage}`systemd.timer(5)` for details.'';
|
||||||
example = {
|
example = {
|
||||||
OnCalendar = "00:05";
|
OnCalendar = "00:05";
|
||||||
RandomizedDelaySec = "5h";
|
RandomizedDelaySec = "5h";
|
||||||
|
|
@ -149,14 +149,9 @@ in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
../../lib/module.nix
|
../../lib/module.nix
|
||||||
../blocks/monitoring.nix
|
|
||||||
];
|
];
|
||||||
|
|
||||||
options.shb.restic = {
|
options.shb.restic = {
|
||||||
enableDashboard = lib.mkEnableOption "the Backups SHB dashboard" // {
|
|
||||||
default = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
instances = mkOption {
|
instances = mkOption {
|
||||||
description = "Files to backup following the [backup contract](./shb.contracts-backup.html).";
|
description = "Files to backup following the [backup contract](./shb.contracts-backup.html).";
|
||||||
default = { };
|
default = { };
|
||||||
|
|
@ -413,13 +408,7 @@ in
|
||||||
nameValuePair "${fullName name instance.settings.repository}_restore_gen" {
|
nameValuePair "${fullName name instance.settings.repository}_restore_gen" {
|
||||||
enable = true;
|
enable = true;
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
# Purposely not a oneshot systemd service otherwise
|
serviceConfig.Type = "oneshot";
|
||||||
# the service waits on the completion the backup before deactivating.
|
|
||||||
# This seems like a nice property at first but there is one annoying
|
|
||||||
# edge case when deploying. If a backup job somehow is started when
|
|
||||||
# the deploy happens, the deploy will wait on the service to finish
|
|
||||||
# before considering the deploy done. And worse, it will consider the
|
|
||||||
# deploy as failed if the backup fails, which is not what you want.
|
|
||||||
script = (
|
script = (
|
||||||
shb.replaceSecrets {
|
shb.replaceSecrets {
|
||||||
userConfig = instance.settings.repository.secrets // {
|
userConfig = instance.settings.repository.secrets // {
|
||||||
|
|
@ -440,16 +429,38 @@ in
|
||||||
let
|
let
|
||||||
mkResticBinary =
|
mkResticBinary =
|
||||||
name: instance:
|
name: instance:
|
||||||
shb.contracts.backup.mkRestoreScript {
|
pkgs.writeShellApplication {
|
||||||
name = fullName name instance.settings.repository;
|
name = fullName name instance.settings.repository;
|
||||||
user = instance.request.user;
|
text = ''
|
||||||
sudoPreserveEnvs = [
|
usage() {
|
||||||
"RESTIC_REPOSITORY"
|
echo "$0 restore latest"
|
||||||
"RESTIC_PASSWORD_FILE"
|
}
|
||||||
];
|
|
||||||
secretsFile = "/run/secrets_restic_env/${fullName name instance.settings.repository}";
|
if ! [ "$1" = "restore" ]; then
|
||||||
restoreCmd = ''${pkgs.restic}/bin/restic restore \"$snapshot\" --target /'';
|
usage
|
||||||
listCmd = ''if [ -e \"$RESTIC_REPOSITORY/index\" ]; then ${pkgs.restic}/bin/restic snapshots --json | ${pkgs.jq}/bin/jq '.[].id'; fi'';
|
exit 1
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
|
||||||
|
if ! [ "$1" = "latest" ]; then
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
|
||||||
|
sudocmd() {
|
||||||
|
sudo --preserve-env=RESTIC_REPOSITORY,RESTIC_PASSWORD_FILE -u ${instance.request.user} "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source <(sudocmd cat "/run/secrets_restic_env/${fullName name instance.settings.repository}")
|
||||||
|
set +a
|
||||||
|
|
||||||
|
echo "Will restore archive 'latest'"
|
||||||
|
|
||||||
|
sudocmd ${pkgs.restic}/bin/restic restore latest --target /
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
flatten (mapAttrsToList mkResticBinary cfg.instances);
|
flatten (mapAttrsToList mkResticBinary cfg.instances);
|
||||||
|
|
@ -459,26 +470,42 @@ in
|
||||||
let
|
let
|
||||||
mkResticBinary =
|
mkResticBinary =
|
||||||
name: instance:
|
name: instance:
|
||||||
shb.contracts.backup.mkRestoreScript {
|
pkgs.writeShellApplication {
|
||||||
name = fullName name instance.settings.repository;
|
name = fullName name instance.settings.repository;
|
||||||
user = instance.request.user;
|
text = ''
|
||||||
sudoPreserveEnvs = [
|
usage() {
|
||||||
"RESTIC_REPOSITORY"
|
echo "$0 restore latest"
|
||||||
"RESTIC_PASSWORD_FILE"
|
}
|
||||||
];
|
|
||||||
secretsFile = "/run/secrets_restic_env/${fullName name instance.settings.repository}";
|
if ! [ "$1" = "restore" ]; then
|
||||||
restoreCmd = ''${pkgs.restic}/bin/restic dump \"$snapshot\" ${instance.request.backupName} | ${instance.request.restoreCmd}'';
|
usage
|
||||||
listCmd = ''if [ -e \"$RESTIC_REPOSITORY/index\" ]; then ${pkgs.restic}/bin/restic snapshots --json | ${pkgs.jq}/bin/jq '.[].id'; fi'';
|
exit 1
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
|
||||||
|
if ! [ "$1" = "latest" ]; then
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
|
||||||
|
sudocmd() {
|
||||||
|
sudo --preserve-env=RESTIC_REPOSITORY,RESTIC_PASSWORD_FILE -u ${instance.request.user} "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source <(sudocmd cat "/run/secrets_restic_env/${fullName name instance.settings.repository}")
|
||||||
|
set +a
|
||||||
|
|
||||||
|
echo "Will restore archive 'latest'"
|
||||||
|
|
||||||
|
sudocmd sh -c "${pkgs.restic}/bin/restic dump latest ${instance.request.backupName} | ${instance.request.restoreCmd}"
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
flatten (mapAttrsToList mkResticBinary cfg.databases);
|
flatten (mapAttrsToList mkResticBinary cfg.databases);
|
||||||
}
|
}
|
||||||
|
|
||||||
(lib.mkIf (cfg.enableDashboard && (cfg.instances != { } || cfg.databases != { })) {
|
|
||||||
shb.monitoring.dashboards = [
|
|
||||||
./backup/dashboard/Backups.json
|
|
||||||
];
|
|
||||||
})
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
Defined in [`/modules/blocks/restic.nix`](@REPO@/modules/blocks/restic.nix).
|
Defined in [`/modules/blocks/restic.nix`](@REPO@/modules/blocks/restic.nix).
|
||||||
|
|
||||||
This block sets up a backup job using [Restic][].
|
This block sets up a backup job using [Restic][].
|
||||||
It is heavily based on the nixpkgs Restic module.
|
|
||||||
|
|
||||||
[restic]: https://restic.net/
|
[restic]: https://restic.net/
|
||||||
|
|
||||||
|
|
@ -51,7 +50,7 @@ shb.restic.instances."myservice" = {
|
||||||
settings = {
|
settings = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
passphrase.result = config.shb.sops.secret."passphrase".result;
|
passphrase.result = shb.sops.secret."passphrase".result;
|
||||||
|
|
||||||
repository = {
|
repository = {
|
||||||
path = "/srv/backups/myservice";
|
path = "/srv/backups/myservice";
|
||||||
|
|
@ -72,7 +71,7 @@ shb.restic.instances."myservice" = {
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.sops.secret."passphrase".request =
|
shb.sops.secret."passphrase".request =
|
||||||
config.shb.restic.instances."myservice".settings.passphrase.request;
|
shb.restic.instances."myservice".settings.passphrase.request;
|
||||||
```
|
```
|
||||||
|
|
||||||
### One folder backed up with contract {#blocks-restic-usage-provider-contract}
|
### One folder backed up with contract {#blocks-restic-usage-provider-contract}
|
||||||
|
|
@ -83,12 +82,12 @@ the snippet above becomes:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
shb.restic.instances."myservice" = {
|
shb.restic.instances."myservice" = {
|
||||||
request = config.myservice.backup.request;
|
request = config.myservice.backup;
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
||||||
passphrase.result = config.shb.sops.secret."passphrase".result;
|
passphrase.result = shb.sops.secret."passphrase".result;
|
||||||
|
|
||||||
repository = {
|
repository = {
|
||||||
path = "/srv/backups/myservice";
|
path = "/srv/backups/myservice";
|
||||||
|
|
@ -109,7 +108,7 @@ shb.restic.instances."myservice" = {
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.sops.secret."passphrase".request =
|
shb.sops.secret."passphrase".request =
|
||||||
config.shb.restic.instances."myservice".settings.passphrase.request;
|
shb.restic.instances."myservice".settings.passphrase.request;
|
||||||
```
|
```
|
||||||
|
|
||||||
### One folder backed up to S3 {#blocks-restic-usage-provider-remote}
|
### One folder backed up to S3 {#blocks-restic-usage-provider-remote}
|
||||||
|
|
@ -225,49 +224,15 @@ shb.test.backup.instances.all = backupcfg repos ["/var/lib/myfolder1" "/var/lib/
|
||||||
|
|
||||||
## Monitoring {#blocks-restic-monitoring}
|
## Monitoring {#blocks-restic-monitoring}
|
||||||
|
|
||||||
A generic dashboard for all backup solutions is provided.
|
[WIP](https://github.com/ibizaman/selfhostblocks/issues/151)
|
||||||
See [Backups Dashboard and Alert](blocks-monitoring.html#blocks-monitoring-backup) section in the monitoring chapter.
|
|
||||||
|
|
||||||
## Maintenance {#blocks-restic-maintenance}
|
## Maintenance {#blocks-restic-maintenance}
|
||||||
|
|
||||||
### Manual Backup {#blocks-restic-maintenance-manuql}
|
One command-line helper is provided per backup instance and repository pair to automatically supply the needed secrets.
|
||||||
|
|
||||||
To launch a backup manually, just run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
systemctl start <systemd-service-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
You can easily discover the systemd service name you need by either listing the units:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
systemctl list-units 'restic*'
|
|
||||||
```
|
|
||||||
|
|
||||||
Or by autocompleting the unit name with `<TAB>`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
systemctl start restic<TAB><TAB>
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that the systemd services are of `Type=simple` which means the systemd service
|
|
||||||
will not wait for the backup completion to terminate.
|
|
||||||
If you want instead to wait for the backup to complete, use the `--wait` flag:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
systemctl start --wait <systemd-service-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Restore {#blocks-restic-maintenance-restore}
|
|
||||||
|
|
||||||
One command-line helper is provided per backup instance and repository pair which allows to:
|
|
||||||
|
|
||||||
- list snapshots: `<script> snapshots`
|
|
||||||
- to restore a snapshot: `<script> restore <snapshot>`
|
|
||||||
|
|
||||||
The restore script has all the secrets needed to access the repo,
|
The restore script has all the secrets needed to access the repo,
|
||||||
it will run `sudo` automatically
|
it will run `sudo` automatically
|
||||||
and the user running it needs to have correct permissions for privilege escalation.
|
and the user running it needs to have correct permissions for privilege escalation
|
||||||
|
|
||||||
In the [multiple directories example](#blocks-restic-usage-multiple) above, the following 6 helpers are provided in the `$PATH`:
|
In the [multiple directories example](#blocks-restic-usage-multiple) above, the following 6 helpers are provided in the `$PATH`:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
shb,
|
|
||||||
utils,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
cfg = config.shb.sanoid;
|
|
||||||
|
|
||||||
restoreScriptName = name: "sanoid-${utils.escapeSystemdPath name}-restore";
|
|
||||||
backupScriptBase = "sanoid";
|
|
||||||
|
|
||||||
restoreScript =
|
|
||||||
name:
|
|
||||||
pkgs.writers.writePython3Bin (restoreScriptName name)
|
|
||||||
{
|
|
||||||
flakeIgnore = [ "E501" ];
|
|
||||||
}
|
|
||||||
''
|
|
||||||
import argparse
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
dataset = "${name}"
|
|
||||||
|
|
||||||
|
|
||||||
class ZFSError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def run_command(cmd: list[str]) -> str:
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
cmd,
|
|
||||||
check=True,
|
|
||||||
text=True,
|
|
||||||
capture_output=True,
|
|
||||||
)
|
|
||||||
return result.stdout.strip()
|
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
raise ZFSError(
|
|
||||||
f"Command failed: {' '.join(cmd)}\n"
|
|
||||||
f"Exit code: {e.returncode}\n"
|
|
||||||
f"stderr: {e.stderr.strip()}"
|
|
||||||
) from e
|
|
||||||
|
|
||||||
|
|
||||||
def list_snapshots() -> None:
|
|
||||||
"""List all ZFS snapshots."""
|
|
||||||
output = run_command(["zfs", "list", "-H", "-t", "snapshot", dataset])
|
|
||||||
if not output:
|
|
||||||
return []
|
|
||||||
return output.splitlines()
|
|
||||||
|
|
||||||
|
|
||||||
def restore_snapshot(snapshot: str) -> None:
|
|
||||||
"""Rollback to a given snapshot."""
|
|
||||||
if not snapshot:
|
|
||||||
raise ValueError("Snapshot name must not be empty")
|
|
||||||
|
|
||||||
print(f"Rolling back to snapshot: {snapshot}")
|
|
||||||
run_command(["zfs", "rollback", "-r", snapshot])
|
|
||||||
print("Rollback completed successfully.")
|
|
||||||
|
|
||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
|
||||||
parser = argparse.ArgumentParser(description=f"Restore script for {dataset}")
|
|
||||||
|
|
||||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
||||||
|
|
||||||
subparsers.add_parser(
|
|
||||||
"snapshots",
|
|
||||||
help="List all ZFS snapshots",
|
|
||||||
)
|
|
||||||
|
|
||||||
restore_parser = subparsers.add_parser(
|
|
||||||
"restore",
|
|
||||||
help="Rollback to a specific snapshot",
|
|
||||||
)
|
|
||||||
restore_parser.add_argument(
|
|
||||||
"snapshot",
|
|
||||||
help="Snapshot name (e.g. pool/dataset@snapname)",
|
|
||||||
)
|
|
||||||
|
|
||||||
return parser
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = build_parser()
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
try:
|
|
||||||
if args.command == "snapshots":
|
|
||||||
snapshots = list_snapshots()
|
|
||||||
for s in snapshots:
|
|
||||||
print(s)
|
|
||||||
elif args.command == "restore":
|
|
||||||
restore_snapshot(args.snapshot)
|
|
||||||
else:
|
|
||||||
parser.print_help()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
except ZFSError as e:
|
|
||||||
print(f"ERROR: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Unexpected error: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
'';
|
|
||||||
in
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
../../lib/module.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
options.shb.sanoid.backup = lib.mkOption {
|
|
||||||
description = "Sanoid prodiver for file backup contract";
|
|
||||||
default = { };
|
|
||||||
type = lib.types.attrsOf (
|
|
||||||
lib.types.submodule (
|
|
||||||
{ name, ... }:
|
|
||||||
{
|
|
||||||
options = shb.contracts.backup.mkProvider {
|
|
||||||
resultCfg = {
|
|
||||||
restoreScript = restoreScriptName name;
|
|
||||||
restoreScriptText = restoreScriptName "<name>";
|
|
||||||
|
|
||||||
backupService = "${backupScriptBase}.service";
|
|
||||||
backupServiceText = "${backupScriptBase}.service";
|
|
||||||
};
|
|
||||||
|
|
||||||
settings = lib.mkOption {
|
|
||||||
description = "Options passed to the `services.sanoid.datasets.<name>` option.";
|
|
||||||
default = { };
|
|
||||||
type = lib.types.attrsOf lib.types.anything;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf (cfg.backup != { }) {
|
|
||||||
services.sanoid.enable = true;
|
|
||||||
|
|
||||||
services.sanoid.datasets =
|
|
||||||
let
|
|
||||||
mkDataset = name: cfg': {
|
|
||||||
inherit name;
|
|
||||||
value = cfg'.settings;
|
|
||||||
};
|
|
||||||
in
|
|
||||||
lib.mapAttrs' mkDataset cfg.backup;
|
|
||||||
|
|
||||||
environment.systemPackages = map restoreScript (lib.attrNames cfg.backup);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
# Sanoid Block {#blocks-sanoid}
|
|
||||||
|
|
||||||
Defined in [`/modules/blocks/sanoid.nix`](@REPO@/modules/blocks/sanoid.nix):
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
inputs.selfhostblocks.nixosModules.sanoid
|
|
||||||
];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Provider Contracts {#blocks-sanoid-contract-provider}
|
|
||||||
|
|
||||||
This block provides the following contracts:
|
|
||||||
|
|
||||||
- [dataset backup contract](contracts-datasetbackup.html) under the [`shb.sanoid.backup`][backup] option.
|
|
||||||
It is tested with the [generic contract tests][backup contract tests].
|
|
||||||
|
|
||||||
[backup]: #blocks-sanoid-options-shb.sanoid.backup
|
|
||||||
[backup contract tests]: @REPO@/test/contracts/backup.nix
|
|
||||||
|
|
||||||
## Usage {#blocks-sanoid-usage}
|
|
||||||
|
|
||||||
Sanoid uses templates to know when snapshots should be kept or pruned.
|
|
||||||
|
|
||||||
### Default Template {#blocks-sanoid-usage-default-template}
|
|
||||||
|
|
||||||
Backup a dataset using the default Sanoid template:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.zfs.pools.root.datasets.home = {
|
|
||||||
path = "/home";
|
|
||||||
};
|
|
||||||
|
|
||||||
shb.sanoid.backup."root/home" = {
|
|
||||||
request = shb.zfs.pools.root.datasets.home.datasetBackup.request;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This uses the dataset backup contract which is exposed through the ZFS module's [`shb.zfs.pools.<name>.datasets.<name>.datasetBackup`](blocks-zfs.html#blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.datasetbackup) option.
|
|
||||||
|
|
||||||
### Custom Template {#blocks-sanoid-usage-custom-template}
|
|
||||||
|
|
||||||
Create a custom template and use it:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.zfs.pools.root.datasets.home = {
|
|
||||||
path = "/home";
|
|
||||||
};
|
|
||||||
|
|
||||||
services.sanoid.templates."yearly" = {
|
|
||||||
hourly = 10;
|
|
||||||
daily = 3;
|
|
||||||
monthly = 3;
|
|
||||||
yearly = 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
shb.sanoid.backup."root/home" = {
|
|
||||||
request = shb.zfs.pools.root.datasets.home.datasetBackup.request;
|
|
||||||
template = "yearly";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Note we use the upstream `services.sanoid.templates` option to define the templates.
|
|
||||||
|
|
||||||
### Without contract {#blocks-sanoid-usage-without-contract}
|
|
||||||
|
|
||||||
To backup a dataset that does not provide the dataset backup contract,
|
|
||||||
we can just set the request manually:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.zfs.pools.root.datasets.home = {
|
|
||||||
path = "/home";
|
|
||||||
};
|
|
||||||
|
|
||||||
shb.sanoid.backup."root/home" = {
|
|
||||||
request.dataset = "root/home";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Note the attr name under the `shb.sanoid.backup` option does not
|
|
||||||
set the dataset name.
|
|
||||||
|
|
||||||
## Options Reference {#blocks-sanoid-options}
|
|
||||||
|
|
||||||
```{=include=} options
|
|
||||||
id-prefix: blocks-sanoid-options-
|
|
||||||
list-id: selfhostblocks-blocks-sanoid-options
|
|
||||||
source: @OPTIONS_JSON@
|
|
||||||
```
|
|
||||||
|
|
@ -13,7 +13,7 @@ to adapt it to the [secret contract](./contracts-secret.html).
|
||||||
|
|
||||||
This block provides the following contracts:
|
This block provides the following contracts:
|
||||||
|
|
||||||
- [secret contract][] under the [`shb.sops.secret`][secret] option.
|
- [secret contract][] under the [`shb.sops.secrets`][secret] option.
|
||||||
It is not yet tested with [contract tests][secret contract tests] but it is used extensively on several machines.
|
It is not yet tested with [contract tests][secret contract tests] but it is used extensively on several machines.
|
||||||
|
|
||||||
[secret]: #blocks-sops-options-shb.sops.secret
|
[secret]: #blocks-sops-options-shb.sops.secret
|
||||||
|
|
@ -21,7 +21,7 @@ This block provides the following contracts:
|
||||||
[secret contract tests]: @REPO@/test/contracts/secret.nix
|
[secret contract tests]: @REPO@/test/contracts/secret.nix
|
||||||
|
|
||||||
As requested by the contract, when asking for a secret with the `shb.sops` module,
|
As requested by the contract, when asking for a secret with the `shb.sops` module,
|
||||||
the path where the secret will be located can be found under the [`shb.sops.secret.<name>.result`][result] option.
|
the path where the secret will be located can be found under the [`shb.sops.secrets.<name>.result`][result] option.
|
||||||
|
|
||||||
[result]: #blocks-sops-options-shb.sops.secret._name_.result
|
[result]: #blocks-sops-options-shb.sops.secret._name_.result
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
../../lib/module.nix
|
../../lib/module.nix
|
||||||
./monitoring.nix
|
|
||||||
];
|
];
|
||||||
|
|
||||||
options.shb.certs = {
|
options.shb.certs = {
|
||||||
|
|
@ -32,9 +31,6 @@ in
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
default = "shb-ca-bundle.service";
|
default = "shb-ca-bundle.service";
|
||||||
};
|
};
|
||||||
enableDashboard = lib.mkEnableOption "the SSL SHB dashboard" // {
|
|
||||||
default = true;
|
|
||||||
};
|
|
||||||
cas.selfsigned = lib.mkOption {
|
cas.selfsigned = lib.mkOption {
|
||||||
description = "Generate a self-signed Certificate Authority.";
|
description = "Generate a self-signed Certificate Authority.";
|
||||||
default = { };
|
default = { };
|
||||||
|
|
@ -342,40 +338,6 @@ in
|
||||||
serviceName = lib.strings.removeSuffix ".service";
|
serviceName = lib.strings.removeSuffix ".service";
|
||||||
in
|
in
|
||||||
lib.mkMerge [
|
lib.mkMerge [
|
||||||
# Generic assertions
|
|
||||||
{
|
|
||||||
assertions = lib.flatten (
|
|
||||||
lib.mapAttrsToList (
|
|
||||||
_name: certCfg:
|
|
||||||
(
|
|
||||||
let
|
|
||||||
domainInExtraDomains =
|
|
||||||
(lib.lists.findFirstIndex (x: x == certCfg.domain) null certCfg.extraDomains) != null;
|
|
||||||
firstDuplicateDomain =
|
|
||||||
(
|
|
||||||
l:
|
|
||||||
let
|
|
||||||
sorted = lib.sort (x: y: x < y) l;
|
|
||||||
maybeDupe = lib.lists.removePrefix (lib.lists.commonPrefix (lib.uniqueStrings sorted) sorted) sorted;
|
|
||||||
in
|
|
||||||
if maybeDupe == [ ] then null else lib.head maybeDupe
|
|
||||||
)
|
|
||||||
certCfg.extraDomains;
|
|
||||||
in
|
|
||||||
[
|
|
||||||
{
|
|
||||||
assertion = !domainInExtraDomains;
|
|
||||||
message = "Error in SHB option for domain ${certCfg.domain}: do not repeat the domain name in the `extraDomain` option.";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion = firstDuplicateDomain == null;
|
|
||||||
message = "Error in SHB option for domain ${certCfg.domain}: `extraDomain` option cannot have duplicates, first offender is: ${firstDuplicateDomain} in the following list: ${lib.concatStringsSep " " certCfg.extraDomains}.";
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
) (cfg.certs.selfsigned // cfg.certs.letsencrypt)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
# Config for self-signed CA.
|
# Config for self-signed CA.
|
||||||
{
|
{
|
||||||
systemd.services = lib.mapAttrs' (
|
systemd.services = lib.mapAttrs' (
|
||||||
|
|
@ -399,25 +361,21 @@ in
|
||||||
crl_signing_key
|
crl_signing_key
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
keyfile="${caCfg.paths.key}"
|
mkdir -p "$(dirname -- "${caCfg.paths.key}")"
|
||||||
keydir="$(dirname -- "$keyfile")"
|
${pkgs.gnutls}/bin/certtool \
|
||||||
mkdir -p "$keydir"
|
--generate-privkey \
|
||||||
[ -f "$keyfile" ] || ${pkgs.gnutls}/bin/certtool \
|
--key-type rsa \
|
||||||
--generate-privkey \
|
--sec-param High \
|
||||||
--key-type rsa \
|
--outfile ${caCfg.paths.key}
|
||||||
--sec-param High \
|
chmod 666 ${caCfg.paths.key}
|
||||||
--outfile "$keyfile"
|
|
||||||
chmod 666 "$keyfile"
|
|
||||||
|
|
||||||
certfile="${caCfg.paths.cert}"
|
mkdir -p "$(dirname -- "${caCfg.paths.cert}")"
|
||||||
certdir="$(dirname -- "$certfile")"
|
${pkgs.gnutls}/bin/certtool \
|
||||||
mkdir -p "$certdir"
|
--generate-self-signed \
|
||||||
[ -f "$certfile" ] || ${pkgs.gnutls}/bin/certtool \
|
--load-privkey ${caCfg.paths.key} \
|
||||||
--generate-self-signed \
|
--template ca.template \
|
||||||
--load-privkey "$keyfile" \
|
--outfile ${caCfg.paths.cert}
|
||||||
--template ca.template \
|
chmod 666 ${caCfg.paths.cert}
|
||||||
--outfile "$certfile"
|
|
||||||
chmod 666 "$certfile"
|
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
) cfg.cas.selfsigned;
|
) cfg.cas.selfsigned;
|
||||||
|
|
@ -431,8 +389,6 @@ in
|
||||||
script = ''
|
script = ''
|
||||||
mkdir -p /etc/ssl/certs
|
mkdir -p /etc/ssl/certs
|
||||||
|
|
||||||
# This file is automatically idempotent since the source files used are idempotent.
|
|
||||||
|
|
||||||
rm -f /etc/ssl/certs/ca-bundle.crt
|
rm -f /etc/ssl/certs/ca-bundle.crt
|
||||||
rm -f /etc/ssl/certs/ca-certificates.crt
|
rm -f /etc/ssl/certs/ca-certificates.crt
|
||||||
|
|
||||||
|
|
@ -462,8 +418,8 @@ in
|
||||||
let
|
let
|
||||||
extraDnsNames = lib.strings.concatStringsSep "\n" (map (n: "dns_name = ${n}") certCfg.extraDomains);
|
extraDnsNames = lib.strings.concatStringsSep "\n" (map (n: "dns_name = ${n}") certCfg.extraDomains);
|
||||||
chmod = cert: ''
|
chmod = cert: ''
|
||||||
chown root:${certCfg.group} "${cert}"
|
chown root:${certCfg.group} ${cert}
|
||||||
chmod 640 "${cert}"
|
chmod 640 ${cert}
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
''
|
''
|
||||||
|
|
@ -480,27 +436,23 @@ in
|
||||||
signing_key
|
signing_key
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
keyfile="${certCfg.paths.key}"
|
mkdir -p "$(dirname -- "${certCfg.paths.key}")"
|
||||||
keydir="$(dirname -- "$keyfile")"
|
${pkgs.gnutls}/bin/certtool \
|
||||||
mkdir -p "$keydir"
|
--generate-privkey \
|
||||||
[ -f "$keyfile" ] || ${pkgs.gnutls}/bin/certtool \
|
--key-type rsa \
|
||||||
--generate-privkey \
|
--sec-param High \
|
||||||
--key-type rsa \
|
--outfile ${certCfg.paths.key}
|
||||||
--sec-param High \
|
${chmod certCfg.paths.key}
|
||||||
--outfile "$keyfile"
|
|
||||||
${chmod "$keyfile"}
|
|
||||||
|
|
||||||
certfile="${certCfg.paths.cert}"
|
mkdir -p "$(dirname -- "${certCfg.paths.cert}")"
|
||||||
certdir="$(dirname -- "$certfile")"
|
${pkgs.gnutls}/bin/certtool \
|
||||||
mkdir -p "$certdir"
|
--generate-certificate \
|
||||||
[ -f "$certfile" ] || ${pkgs.gnutls}/bin/certtool \
|
--load-privkey ${certCfg.paths.key} \
|
||||||
--generate-certificate \
|
--load-ca-privkey ${certCfg.ca.paths.key} \
|
||||||
--load-privkey "$keyfile" \
|
--load-ca-certificate ${certCfg.ca.paths.cert} \
|
||||||
--load-ca-privkey "${certCfg.ca.paths.key}" \
|
--template server.template \
|
||||||
--load-ca-certificate "${certCfg.ca.paths.cert}" \
|
--outfile ${certCfg.paths.cert}
|
||||||
--template server.template \
|
${chmod certCfg.paths.cert}
|
||||||
--outfile "$certfile"
|
|
||||||
${chmod "$certfile"}
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
postStart = lib.optionalString (certCfg.reloadServices != [ ]) ''
|
postStart = lib.optionalString (certCfg.reloadServices != [ ]) ''
|
||||||
|
|
@ -543,7 +495,7 @@ in
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"${name}" = {
|
"${name}" = {
|
||||||
extraDomainNames = certCfg.extraDomains;
|
extraDomainNames = [ certCfg.domain ] ++ certCfg.extraDomains;
|
||||||
email = certCfg.adminEmail;
|
email = certCfg.adminEmail;
|
||||||
enableDebugLogs = certCfg.debug;
|
enableDebugLogs = certCfg.debug;
|
||||||
server = lib.mkIf certCfg.stagingServer "https://acme-staging-v02.api.letsencrypt.org/directory";
|
server = lib.mkIf certCfg.stagingServer "https://acme-staging-v02.api.letsencrypt.org/directory";
|
||||||
|
|
@ -551,7 +503,7 @@ in
|
||||||
// lib.optionalAttrs (certCfg.dnsProvider != null) {
|
// lib.optionalAttrs (certCfg.dnsProvider != null) {
|
||||||
inherit (certCfg) dnsProvider dnsResolver;
|
inherit (certCfg) dnsProvider dnsResolver;
|
||||||
inherit (certCfg) group reloadServices;
|
inherit (certCfg) group reloadServices;
|
||||||
environmentFile = certCfg.credentialsFile;
|
credentialsFile = certCfg.credentialsFile;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -676,10 +628,5 @@ in
|
||||||
in
|
in
|
||||||
optionals (cfg.certs.letsencrypt != { }) (flatten (mapAttrsToList scrapeCfg cfg.certs.letsencrypt));
|
optionals (cfg.certs.letsencrypt != { }) (flatten (mapAttrsToList scrapeCfg cfg.certs.letsencrypt));
|
||||||
}
|
}
|
||||||
(lib.mkIf (cfg.enableDashboard && (cfg.certs.selfsigned != { } || cfg.certs.letsencrypt != { })) {
|
|
||||||
shb.monitoring.dashboards = [
|
|
||||||
./ssl/dashboard/SSL.json
|
|
||||||
];
|
|
||||||
})
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,727 +0,0 @@
|
||||||
{
|
|
||||||
"annotations": {
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"builtIn": 1,
|
|
||||||
"datasource": {
|
|
||||||
"type": "grafana",
|
|
||||||
"uid": "-- Grafana --"
|
|
||||||
},
|
|
||||||
"enable": true,
|
|
||||||
"hide": true,
|
|
||||||
"iconColor": "rgba(0, 211, 255, 1)",
|
|
||||||
"name": "Annotations & Alerts",
|
|
||||||
"type": "dashboard"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"editable": true,
|
|
||||||
"fiscalYearStartMonth": 0,
|
|
||||||
"graphTooltip": 0,
|
|
||||||
"id": 16,
|
|
||||||
"links": [],
|
|
||||||
"panels": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "palette-classic"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"axisBorderShow": false,
|
|
||||||
"axisCenteredZero": false,
|
|
||||||
"axisColorMode": "text",
|
|
||||||
"axisLabel": "",
|
|
||||||
"axisPlacement": "auto",
|
|
||||||
"axisSoftMin": 0,
|
|
||||||
"barAlignment": 0,
|
|
||||||
"barWidthFactor": 0.6,
|
|
||||||
"drawStyle": "line",
|
|
||||||
"fillOpacity": 0,
|
|
||||||
"gradientMode": "none",
|
|
||||||
"hideFrom": {
|
|
||||||
"legend": false,
|
|
||||||
"tooltip": false,
|
|
||||||
"viz": false
|
|
||||||
},
|
|
||||||
"insertNulls": false,
|
|
||||||
"lineInterpolation": "linear",
|
|
||||||
"lineWidth": 1,
|
|
||||||
"pointSize": 5,
|
|
||||||
"scaleDistribution": {
|
|
||||||
"type": "linear"
|
|
||||||
},
|
|
||||||
"showPoints": "auto",
|
|
||||||
"showValues": false,
|
|
||||||
"spanNulls": false,
|
|
||||||
"stacking": {
|
|
||||||
"group": "A",
|
|
||||||
"mode": "none"
|
|
||||||
},
|
|
||||||
"thresholdsStyle": {
|
|
||||||
"mode": "line+area"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mappings": [],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "transparent",
|
|
||||||
"value": 604808
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"unit": "s"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 12,
|
|
||||||
"x": 0,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"id": 3,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"calcs": [
|
|
||||||
"lastNotNull"
|
|
||||||
],
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "bottom",
|
|
||||||
"showLegend": true,
|
|
||||||
"sortBy": "Last *",
|
|
||||||
"sortDesc": false
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"hideZeros": false,
|
|
||||||
"mode": "single",
|
|
||||||
"sort": "none"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pluginVersion": "12.2.0",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"expr": "min by(exported_hostname, subject, path) (ssl_certificate_expiry_seconds{subject=~\"CN=$job\"})",
|
|
||||||
"legendFormat": "{{exported_hostname}}: {{subject}} {{path}}",
|
|
||||||
"range": true,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Certificate Remaining Validity",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "palette-classic"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"axisBorderShow": false,
|
|
||||||
"axisCenteredZero": false,
|
|
||||||
"axisColorMode": "text",
|
|
||||||
"axisLabel": "",
|
|
||||||
"axisPlacement": "auto",
|
|
||||||
"barAlignment": 0,
|
|
||||||
"barWidthFactor": 0.6,
|
|
||||||
"drawStyle": "line",
|
|
||||||
"fillOpacity": 0,
|
|
||||||
"gradientMode": "none",
|
|
||||||
"hideFrom": {
|
|
||||||
"legend": false,
|
|
||||||
"tooltip": false,
|
|
||||||
"viz": false
|
|
||||||
},
|
|
||||||
"insertNulls": false,
|
|
||||||
"lineInterpolation": "linear",
|
|
||||||
"lineStyle": {
|
|
||||||
"fill": "solid"
|
|
||||||
},
|
|
||||||
"lineWidth": 1,
|
|
||||||
"pointSize": 5,
|
|
||||||
"scaleDistribution": {
|
|
||||||
"type": "linear"
|
|
||||||
},
|
|
||||||
"showPoints": "never",
|
|
||||||
"showValues": false,
|
|
||||||
"spanNulls": false,
|
|
||||||
"stacking": {
|
|
||||||
"group": "A",
|
|
||||||
"mode": "none"
|
|
||||||
},
|
|
||||||
"thresholdsStyle": {
|
|
||||||
"mode": "off"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fieldMinMax": false,
|
|
||||||
"mappings": [],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"unit": "dateTimeFromNow"
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 12,
|
|
||||||
"x": 12,
|
|
||||||
"y": 0
|
|
||||||
},
|
|
||||||
"id": 5,
|
|
||||||
"options": {
|
|
||||||
"legend": {
|
|
||||||
"calcs": [
|
|
||||||
"lastNotNull"
|
|
||||||
],
|
|
||||||
"displayMode": "table",
|
|
||||||
"placement": "right",
|
|
||||||
"showLegend": true,
|
|
||||||
"sortBy": "Last *",
|
|
||||||
"sortDesc": true,
|
|
||||||
"width": 300
|
|
||||||
},
|
|
||||||
"tooltip": {
|
|
||||||
"hideZeros": false,
|
|
||||||
"mode": "single",
|
|
||||||
"sort": "none"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pluginVersion": "12.2.0",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"editorMode": "code",
|
|
||||||
"exemplar": false,
|
|
||||||
"expr": "systemd_timer_next_trigger_seconds{name=~\"acme-renew-$job.timer\"} * 1000",
|
|
||||||
"format": "time_series",
|
|
||||||
"instant": false,
|
|
||||||
"legendFormat": "{{name}}",
|
|
||||||
"range": true,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Schedule",
|
|
||||||
"type": "timeseries"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"fixedColor": "green",
|
|
||||||
"mode": "fixed"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"align": "auto",
|
|
||||||
"cellOptions": {
|
|
||||||
"type": "auto"
|
|
||||||
},
|
|
||||||
"footer": {
|
|
||||||
"reducers": []
|
|
||||||
},
|
|
||||||
"inspect": false
|
|
||||||
},
|
|
||||||
"decimals": 0,
|
|
||||||
"mappings": [],
|
|
||||||
"noValue": "0",
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "green",
|
|
||||||
"value": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "green",
|
|
||||||
"value": 80
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"matcher": {
|
|
||||||
"id": "byName",
|
|
||||||
"options": "% failed"
|
|
||||||
},
|
|
||||||
"properties": [
|
|
||||||
{
|
|
||||||
"id": "unit",
|
|
||||||
"value": "percentunit"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "custom.cellOptions",
|
|
||||||
"value": {
|
|
||||||
"mode": "gradient",
|
|
||||||
"type": "color-background"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "color",
|
|
||||||
"value": {
|
|
||||||
"mode": "continuous-GrYlRd"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "max",
|
|
||||||
"value": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matcher": {
|
|
||||||
"id": "byName",
|
|
||||||
"options": "total"
|
|
||||||
},
|
|
||||||
"properties": [
|
|
||||||
{
|
|
||||||
"id": "custom.cellOptions",
|
|
||||||
"value": {
|
|
||||||
"mode": "gradient",
|
|
||||||
"type": "color-background"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "color",
|
|
||||||
"value": {
|
|
||||||
"mode": "thresholds"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "thresholds",
|
|
||||||
"value": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "red",
|
|
||||||
"value": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "transparent",
|
|
||||||
"value": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matcher": {
|
|
||||||
"id": "byType",
|
|
||||||
"options": "string"
|
|
||||||
},
|
|
||||||
"properties": [
|
|
||||||
{
|
|
||||||
"id": "custom.minWidth",
|
|
||||||
"value": 150
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matcher": {
|
|
||||||
"id": "byType",
|
|
||||||
"options": "number"
|
|
||||||
},
|
|
||||||
"properties": [
|
|
||||||
{
|
|
||||||
"id": "custom.width",
|
|
||||||
"value": 100
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 12,
|
|
||||||
"x": 0,
|
|
||||||
"y": 8
|
|
||||||
},
|
|
||||||
"id": 4,
|
|
||||||
"options": {
|
|
||||||
"cellHeight": "sm",
|
|
||||||
"enablePagination": true,
|
|
||||||
"frozenColumns": {},
|
|
||||||
"showHeader": true,
|
|
||||||
"sortBy": []
|
|
||||||
},
|
|
||||||
"pluginVersion": "12.2.0",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"exemplar": false,
|
|
||||||
"expr": "increase(systemd_unit_state{name=~\"acme-(order-renew-)?[[job]].service\", state=\"activating\"}[7d])",
|
|
||||||
"instant": true,
|
|
||||||
"legendFormat": "__auto",
|
|
||||||
"range": false,
|
|
||||||
"refId": "A"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"exemplar": false,
|
|
||||||
"expr": "increase(systemd_unit_state{name=~\"acme-(order-renew-)?[[job]].service\", state=\"failed\"}[7d])",
|
|
||||||
"hide": false,
|
|
||||||
"instant": true,
|
|
||||||
"legendFormat": "__auto",
|
|
||||||
"range": false,
|
|
||||||
"refId": "B"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Jobs in the Past Week",
|
|
||||||
"transformations": [
|
|
||||||
{
|
|
||||||
"id": "labelsToFields",
|
|
||||||
"options": {
|
|
||||||
"mode": "columns"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "merge",
|
|
||||||
"options": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "groupingToMatrix",
|
|
||||||
"options": {
|
|
||||||
"columnField": "state",
|
|
||||||
"rowField": "name",
|
|
||||||
"valueField": "Value"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "calculateField",
|
|
||||||
"options": {
|
|
||||||
"alias": "total",
|
|
||||||
"binary": {
|
|
||||||
"left": {
|
|
||||||
"matcher": {
|
|
||||||
"id": "byName",
|
|
||||||
"options": "activating"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"operator": "+",
|
|
||||||
"right": {
|
|
||||||
"matcher": {
|
|
||||||
"id": "byName",
|
|
||||||
"options": "failed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mode": "binary",
|
|
||||||
"reduce": {
|
|
||||||
"include": [
|
|
||||||
"activating",
|
|
||||||
"failed"
|
|
||||||
],
|
|
||||||
"reducer": "sum"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "calculateField",
|
|
||||||
"options": {
|
|
||||||
"alias": "% failed",
|
|
||||||
"binary": {
|
|
||||||
"left": {
|
|
||||||
"matcher": {
|
|
||||||
"id": "byName",
|
|
||||||
"options": "failed"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"operator": "/",
|
|
||||||
"right": {
|
|
||||||
"matcher": {
|
|
||||||
"id": "byName",
|
|
||||||
"options": "total"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mode": "binary",
|
|
||||||
"reduce": {
|
|
||||||
"reducer": "sum"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "sortBy",
|
|
||||||
"options": {
|
|
||||||
"fields": {},
|
|
||||||
"sort": [
|
|
||||||
{
|
|
||||||
"desc": true,
|
|
||||||
"field": "total"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "sortBy",
|
|
||||||
"options": {
|
|
||||||
"fields": {},
|
|
||||||
"sort": [
|
|
||||||
{
|
|
||||||
"desc": true,
|
|
||||||
"field": "failed"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "organize",
|
|
||||||
"options": {
|
|
||||||
"excludeByName": {},
|
|
||||||
"includeByName": {},
|
|
||||||
"indexByName": {},
|
|
||||||
"renameByName": {
|
|
||||||
"activating": "success",
|
|
||||||
"name\\state": "Job"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"type": "table"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "",
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 2,
|
|
||||||
"w": 12,
|
|
||||||
"x": 12,
|
|
||||||
"y": 8
|
|
||||||
},
|
|
||||||
"id": 7,
|
|
||||||
"options": {
|
|
||||||
"code": {
|
|
||||||
"language": "plaintext",
|
|
||||||
"showLineNumbers": false,
|
|
||||||
"showMiniMap": false
|
|
||||||
},
|
|
||||||
"content": "If the log panel is empty, it may be because the amount of lines is too high. Try filtering a few jobs first.",
|
|
||||||
"mode": "markdown"
|
|
||||||
},
|
|
||||||
"pluginVersion": "12.2.0",
|
|
||||||
"title": "",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"default": false,
|
|
||||||
"type": "loki",
|
|
||||||
"uid": "cd6cc53e-840c-484d-85f7-96fede324006"
|
|
||||||
},
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 14,
|
|
||||||
"w": 12,
|
|
||||||
"x": 12,
|
|
||||||
"y": 10
|
|
||||||
},
|
|
||||||
"id": 8,
|
|
||||||
"options": {
|
|
||||||
"dedupStrategy": "none",
|
|
||||||
"enableInfiniteScrolling": false,
|
|
||||||
"enableLogDetails": false,
|
|
||||||
"prettifyLogMessage": false,
|
|
||||||
"showCommonLabels": false,
|
|
||||||
"showLabels": true,
|
|
||||||
"showTime": true,
|
|
||||||
"sortOrder": "Descending",
|
|
||||||
"wrapLogMessage": true
|
|
||||||
},
|
|
||||||
"pluginVersion": "12.2.0",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "loki",
|
|
||||||
"uid": "cd6cc53e-840c-484d-85f7-96fede324006"
|
|
||||||
},
|
|
||||||
"direction": "backward",
|
|
||||||
"editorMode": "code",
|
|
||||||
"expr": "{unit=~\"acme-(order-renew-)?($job).service\"}",
|
|
||||||
"queryType": "range",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Logs - $job",
|
|
||||||
"type": "logs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
|
||||||
},
|
|
||||||
"description": "The job duration is not accurate. Jobs taking less than 15s to run will sometimes appear as taking 100s.",
|
|
||||||
"fieldConfig": {
|
|
||||||
"defaults": {
|
|
||||||
"color": {
|
|
||||||
"mode": "fixed"
|
|
||||||
},
|
|
||||||
"custom": {
|
|
||||||
"axisPlacement": "auto",
|
|
||||||
"fillOpacity": 70,
|
|
||||||
"hideFrom": {
|
|
||||||
"legend": false,
|
|
||||||
"tooltip": false,
|
|
||||||
"viz": false
|
|
||||||
},
|
|
||||||
"insertNulls": false,
|
|
||||||
"lineWidth": 0,
|
|
||||||
"spanNulls": false
|
|
||||||
},
|
|
||||||
"mappings": [
|
|
||||||
{
|
|
||||||
"options": {
|
|
||||||
"1": {
|
|
||||||
"color": "green",
|
|
||||||
"index": 0,
|
|
||||||
"text": "Running"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "value"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"thresholds": {
|
|
||||||
"mode": "absolute",
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"color": "green",
|
|
||||||
"value": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"color": "#EAB839",
|
|
||||||
"value": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"overrides": []
|
|
||||||
},
|
|
||||||
"gridPos": {
|
|
||||||
"h": 8,
|
|
||||||
"w": 12,
|
|
||||||
"x": 0,
|
|
||||||
"y": 16
|
|
||||||
},
|
|
||||||
"id": 6,
|
|
||||||
"options": {
|
|
||||||
"alignValue": "left",
|
|
||||||
"legend": {
|
|
||||||
"displayMode": "list",
|
|
||||||
"placement": "bottom",
|
|
||||||
"showLegend": false
|
|
||||||
},
|
|
||||||
"mergeValues": true,
|
|
||||||
"rowHeight": 0.9,
|
|
||||||
"showValue": "never",
|
|
||||||
"tooltip": {
|
|
||||||
"hideZeros": false,
|
|
||||||
"mode": "single",
|
|
||||||
"sort": "none"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"pluginVersion": "12.2.0",
|
|
||||||
"targets": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "prometheus",
|
|
||||||
"uid": "df80f9f5-97d7-4112-91d8-72f523a02b09"
|
|
||||||
},
|
|
||||||
"editorMode": "code",
|
|
||||||
"exemplar": false,
|
|
||||||
"expr": "max(label_replace(systemd_unit_state{name=~\"acme-(order-renew-)?$job.service\", state=\"activating\"}, \"name\", \"$1\", \"name\", \"acme-order-renew-(.*).service\") > 0 or on(name) label_replace(clamp(systemd_timer_last_trigger_seconds{name=~\"acme-renew-$job.timer\"} - (systemd_timer_last_trigger_seconds{name=~\"acme-renew-$job.timer\"} offset 100s) > 0, 0, 1), \"name\", \"$1\", \"name\", \"acme-renew-(.*).timer\")) by (name)",
|
|
||||||
"format": "time_series",
|
|
||||||
"hide": false,
|
|
||||||
"instant": false,
|
|
||||||
"key": "Q-e1d5c07a-8dcc-4f34-aa5c-cdebcbdda322-0",
|
|
||||||
"legendFormat": "{{name}}",
|
|
||||||
"range": true,
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"title": "Job Runs",
|
|
||||||
"type": "state-timeline"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"preload": false,
|
|
||||||
"schemaVersion": 42,
|
|
||||||
"tags": [],
|
|
||||||
"templating": {
|
|
||||||
"list": [
|
|
||||||
{
|
|
||||||
"current": {
|
|
||||||
"text": [
|
|
||||||
"All"
|
|
||||||
],
|
|
||||||
"value": [
|
|
||||||
"$__all"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"definition": "label_values(systemd_unit_state{name=~\"acme-renew-.*.timer\"},name)",
|
|
||||||
"includeAll": true,
|
|
||||||
"label": "Job",
|
|
||||||
"multi": true,
|
|
||||||
"name": "job",
|
|
||||||
"options": [],
|
|
||||||
"query": {
|
|
||||||
"qryType": 1,
|
|
||||||
"query": "label_values(systemd_unit_state{name=~\"acme-renew-.*.timer\"},name)",
|
|
||||||
"refId": "PrometheusVariableQueryEditor-VariableQuery"
|
|
||||||
},
|
|
||||||
"refresh": 1,
|
|
||||||
"regex": "/acme-renew-(?<value>.*).timer/",
|
|
||||||
"type": "query"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"time": {
|
|
||||||
"from": "now-6h",
|
|
||||||
"to": "now"
|
|
||||||
},
|
|
||||||
"timepicker": {},
|
|
||||||
"timezone": "browser",
|
|
||||||
"title": "SSL Certificates",
|
|
||||||
"uid": "ae818js0bvw8wb",
|
|
||||||
"version": 25
|
|
||||||
}
|
|
||||||
|
|
@ -62,7 +62,6 @@ We can ask Let's Encrypt to generate a certificate with:
|
||||||
shb.certs.certs.letsencrypt."example.com" = {
|
shb.certs.certs.letsencrypt."example.com" = {
|
||||||
domain = "example.com";
|
domain = "example.com";
|
||||||
group = "nginx";
|
group = "nginx";
|
||||||
reloadServices = [ "nginx.service" ];
|
|
||||||
dnsProvider = "linode";
|
dnsProvider = "linode";
|
||||||
adminEmail = "admin@example.com";
|
adminEmail = "admin@example.com";
|
||||||
credentialsFile = /path/to/secret/file;
|
credentialsFile = /path/to/secret/file;
|
||||||
|
|
@ -80,13 +79,6 @@ The credential file's content would be a key-value pair:
|
||||||
LINODE_TOKEN=XYZ...
|
LINODE_TOKEN=XYZ...
|
||||||
```
|
```
|
||||||
|
|
||||||
If you use one subdomain per service,
|
|
||||||
asking for certificates for a subdomain is done with:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
shb.certs.certs.letsencrypt."example.com".extraDomains = [ "nextcloud.${domain}" ];
|
|
||||||
```
|
|
||||||
|
|
||||||
For other providers, see the [official instruction](https://go-acme.github.io/lego/dns/).
|
For other providers, see the [official instruction](https://go-acme.github.io/lego/dns/).
|
||||||
|
|
||||||
## Usage {#block-ssl-usage}
|
## Usage {#block-ssl-usage}
|
||||||
|
|
@ -118,35 +110,11 @@ config.shb.certs.systemdService
|
||||||
See also the [SSL certificate generator usage](contracts-ssl.html#ssl-contract-usage) for a more detailed usage
|
See also the [SSL certificate generator usage](contracts-ssl.html#ssl-contract-usage) for a more detailed usage
|
||||||
example.
|
example.
|
||||||
|
|
||||||
## Monitoring {#blocks-ssl-monitoring}
|
|
||||||
|
|
||||||
A dashboard for SSL certificates is provided.
|
|
||||||
See [SSL Certificates Dashboard and Alert](blocks-monitoring.html#blocks-monitoring-ssl) section in the monitoring chapter.
|
|
||||||
|
|
||||||
## Debug {#block-ssl-debug}
|
## Debug {#block-ssl-debug}
|
||||||
|
|
||||||
Each CA and Cert is generated by a systemd service whose name can be seen in the `systemdService`
|
Each CA and Cert is generated by a systemd service whose name can be seen in the `systemdService`
|
||||||
option. You can then see the latest errors messages using `journalctl`.
|
option. You can then see the latest errors messages using `journalctl`.
|
||||||
|
|
||||||
### Let's Encrypt debug {#blocks-ssl-debug-lets-encrypt}
|
|
||||||
|
|
||||||
Since the SHB SSL block uses the [`security.acme`][] module under the hood,
|
|
||||||
knowing how that one works can become required if something goes wrong.
|
|
||||||
|
|
||||||
For each domain and subdomain, noted as `fqdn` hereunder,
|
|
||||||
the following systemd timers and services are created:
|
|
||||||
|
|
||||||
- `acme-renew-${fqdn}.timer` triggers the `acme-order-renew-${fqdn}.service` service every day.
|
|
||||||
- `acme-${fqdn}.service` (re)generate the initial self-signed certificate,
|
|
||||||
only if the following job never succeeded at least once yet.
|
|
||||||
- `acme-order-renew-${fqdn}.service` asks for a new certificate
|
|
||||||
only if the certificate will expire in the next 30 days.
|
|
||||||
Has logic to only renew if the list of domains has not changed.
|
|
||||||
|
|
||||||
Also, a global service named `acme-setup.service` is created
|
|
||||||
|
|
||||||
[`security.acme`]: https://nixos.org/manual/nixos/stable/#module-security-acme
|
|
||||||
|
|
||||||
## Tests {#block-ssl-tests}
|
## Tests {#block-ssl-tests}
|
||||||
|
|
||||||
The self-signed implementation is tested in [`/tests/vm/ssl.nix`](@REPO@/tests/vm/ssl.nix).
|
The self-signed implementation is tested in [`/tests/vm/ssl.nix`](@REPO@/tests/vm/ssl.nix).
|
||||||
|
|
|
||||||
|
|
@ -2,258 +2,83 @@
|
||||||
config,
|
config,
|
||||||
pkgs,
|
pkgs,
|
||||||
lib,
|
lib,
|
||||||
shb,
|
|
||||||
utils,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.shb.zfs;
|
cfg = config.shb.zfs;
|
||||||
|
|
||||||
datasets =
|
|
||||||
if cfg.snapshotBeforeActivation.datasets != null then
|
|
||||||
cfg.snapshotBeforeActivation.datasets
|
|
||||||
else
|
|
||||||
config.boot.zfs.extraPools;
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
|
||||||
../../lib/module.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
options.shb.zfs = {
|
options.shb.zfs = {
|
||||||
pools = lib.mkOption {
|
defaultPoolName = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = "ZFS pool name datasets should be created on if no pool name is given in the dataset.";
|
||||||
|
};
|
||||||
|
|
||||||
|
datasets = lib.mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Attrset of ZFS pools under which datasets will be created.
|
ZFS Datasets.
|
||||||
|
|
||||||
The ZFS pools are not managed by this module, they should already exist.
|
Each entry in the attrset will be created and mounted in the given path.
|
||||||
|
The attrset name is the dataset name.
|
||||||
|
|
||||||
Each pool named here will be added to the [`boot.zfs.extraPools`](https://search.nixos.org/options?channel=unstable&include_modular_service_options=0&include_nixos_options=1&query=boot.zfs.extrapools&show=option:boot.zfs.extraPools) option.
|
This block implements the following contracts:
|
||||||
|
- mount
|
||||||
'';
|
'';
|
||||||
default = { };
|
default = { };
|
||||||
|
example = lib.literalExpression ''
|
||||||
|
shb.zfs."safe/postgresql".path = "/var/lib/postgresql";
|
||||||
|
'';
|
||||||
type = lib.types.attrsOf (
|
type = lib.types.attrsOf (
|
||||||
lib.types.submodule {
|
lib.types.submodule {
|
||||||
options = {
|
options = {
|
||||||
datasets = lib.mkOption {
|
enable = lib.mkEnableOption "shb.zfs.datasets";
|
||||||
description = ''
|
|
||||||
ZFS Datasets.
|
|
||||||
|
|
||||||
Each entry in the attrset will be created and mounted in the given path.
|
poolName = lib.mkOption {
|
||||||
The attrset name is the dataset name.
|
type = lib.types.nullOr lib.types.str;
|
||||||
|
default = null;
|
||||||
|
description = "ZFS pool name this dataset should be created on. Overrides the defaultPoolName.";
|
||||||
|
};
|
||||||
|
|
||||||
This block implements the following contracts:
|
path = lib.mkOption {
|
||||||
- mount
|
type = lib.types.str;
|
||||||
'';
|
description = "Path this dataset should be mounted on.";
|
||||||
default = { };
|
|
||||||
example = lib.literalExpression ''
|
|
||||||
shb.zfs."safe/postgresql".path = "/var/lib/postgresql";
|
|
||||||
'';
|
|
||||||
type = lib.types.attrsOf (
|
|
||||||
lib.types.submodule (
|
|
||||||
{ config, name, ... }:
|
|
||||||
{
|
|
||||||
options = {
|
|
||||||
enable = lib.mkEnableOption "shb.zfs.datasets";
|
|
||||||
|
|
||||||
path = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "Path this dataset should be mounted on. If the string 'none' is given, the dataset will not be mounted.";
|
|
||||||
};
|
|
||||||
|
|
||||||
mode = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
description = "If non null, unix mode to apply to the dataset root folder.";
|
|
||||||
default = null;
|
|
||||||
example = "ug=rwx,g+s";
|
|
||||||
};
|
|
||||||
|
|
||||||
owner = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
description = "If non null, unix user to apply to the dataset root folder.";
|
|
||||||
default = null;
|
|
||||||
example = "syncthing";
|
|
||||||
};
|
|
||||||
|
|
||||||
group = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
description = "If non null, unix group to apply to the dataset root folder.";
|
|
||||||
default = null;
|
|
||||||
example = "syncthing";
|
|
||||||
};
|
|
||||||
|
|
||||||
defaultACLs = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.str;
|
|
||||||
description = ''
|
|
||||||
If non null, default ACL to set on the dataset root folder.
|
|
||||||
|
|
||||||
Executes "setfacl -d -m $acl $path"
|
|
||||||
'';
|
|
||||||
default = null;
|
|
||||||
example = "g:syncthing:rwX";
|
|
||||||
};
|
|
||||||
|
|
||||||
after = lib.mkOption {
|
|
||||||
type = lib.types.listOf lib.types.str;
|
|
||||||
description = ''
|
|
||||||
Order creating this dataset after the mentioned ones.
|
|
||||||
This only works with datasets managed by this module.
|
|
||||||
|
|
||||||
Use the name of the dataset without the pool name.
|
|
||||||
'';
|
|
||||||
default = [ ];
|
|
||||||
example = lib.literalExpression ''
|
|
||||||
[
|
|
||||||
"backup"
|
|
||||||
]
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
backup = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Backup contract consumer configuration.
|
|
||||||
|
|
||||||
This contract will backup the files inside the dataset.
|
|
||||||
'';
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.backup.mkRequester {
|
|
||||||
user = if config.owner == null then "root" else config.owner;
|
|
||||||
sourceDirectories = [
|
|
||||||
config.path
|
|
||||||
];
|
|
||||||
sourceDirectoriesText = ''
|
|
||||||
[
|
|
||||||
shb.zfs.pools.<name>.datasets.<name>.path
|
|
||||||
]
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
datasetbackup = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
ZFS dataset backup contract configuration.
|
|
||||||
|
|
||||||
This contract will take snaphots of the dataset.
|
|
||||||
'';
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.datasetbackup.mkRequester {
|
|
||||||
dataset = name;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
snapshotBeforeActivation = {
|
|
||||||
enable = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
description = "Take a snapshot of all datasets before activation";
|
|
||||||
default = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
template = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "Bash command to generate the snapshot name. $1 is the path to the new generation.";
|
|
||||||
default = ''pre-$(date --utc '+%y%m%dT%H%M%S')-$(basename "$1")'';
|
|
||||||
};
|
|
||||||
|
|
||||||
datasets = lib.mkOption {
|
|
||||||
type = lib.types.nullOr (lib.types.listOf lib.types.str);
|
|
||||||
description = ''
|
|
||||||
Defines all datasets to take a snapshot of.
|
|
||||||
|
|
||||||
If set to `null`, the default, take the list of datasets from `config.boot.zfs.extraPools`.
|
|
||||||
|
|
||||||
The snapshots are not recursive unless specificed in the `recursive` option.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
recursive = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
description = "If true, take snapshots recurisevly on the given datasets.";
|
|
||||||
default = false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
# The implementation is greatly inspired by https://discourse.nixos.org/t/configure-zfs-filesystems-after-install/48633/3
|
|
||||||
config = {
|
config = {
|
||||||
boot.zfs.extraPools = lib.uniqueStrings (builtins.attrNames cfg.pools);
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion =
|
||||||
|
lib.any (x: x.poolName == null) (lib.mapAttrsToList (n: v: v) cfg.datasets)
|
||||||
|
-> cfg.defaultPoolName != null;
|
||||||
|
message = "Cannot have both datasets.poolName and defaultPoolName set to null";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
systemd.services =
|
system.activationScripts = lib.mapAttrs' (
|
||||||
|
name: cfg':
|
||||||
let
|
let
|
||||||
mkPool =
|
dataset = (if cfg'.poolName != null then cfg'.poolName else cfg.defaultPoolName) + "/" + name;
|
||||||
poolName: poolCfg: lib.listToAttrs (lib.mapAttrsToList (mkDataset poolName) poolCfg.datasets);
|
|
||||||
|
|
||||||
mkDataset =
|
|
||||||
poolName: name: cfg':
|
|
||||||
let
|
|
||||||
dataset = poolName + "/" + name;
|
|
||||||
in
|
|
||||||
lib.attrsets.nameValuePair "zfs-create-${utils.escapeSystemdPath dataset}" {
|
|
||||||
# oneshot is used to make the systemd service wait on completion of the script.
|
|
||||||
serviceConfig.Type = "oneshot";
|
|
||||||
unitConfig.DefaultDependencies = false;
|
|
||||||
requiredBy = [ "local-fs.target" ];
|
|
||||||
before = [ "local-fs.target" ];
|
|
||||||
after = [
|
|
||||||
"zfs-import-${poolName}.service"
|
|
||||||
"zfs-mount.service"
|
|
||||||
]
|
|
||||||
++ map (n: "zfs-create-${poolName}-${n}.service") cfg'.after;
|
|
||||||
|
|
||||||
unitConfig.ConditionPathIsMountPoint = lib.mkIf (cfg'.path != "none") "!${cfg'.path}";
|
|
||||||
|
|
||||||
script = ''
|
|
||||||
${pkgs.zfs}/bin/zfs list ${dataset} > /dev/null 2>&1 \
|
|
||||||
|| ${pkgs.zfs}/bin/zfs create \
|
|
||||||
-o mountpoint=none \
|
|
||||||
${dataset} || :
|
|
||||||
|
|
||||||
[ "$(${pkgs.zfs}/bin/zfs get -H mountpoint -o value ${dataset})" = ${cfg'.path} ] \
|
|
||||||
|| ${pkgs.zfs}/bin/zfs set \
|
|
||||||
mountpoint="${cfg'.path}" \
|
|
||||||
${dataset}
|
|
||||||
|
|
||||||
''
|
|
||||||
+ lib.optionalString (cfg'.path != "none" && cfg'.mode != null) ''
|
|
||||||
chmod "${cfg'.mode}" "${cfg'.path}"
|
|
||||||
''
|
|
||||||
+ lib.optionalString (cfg'.path != "none" && cfg'.owner != null) ''
|
|
||||||
chown "${cfg'.owner}" "${cfg'.path}"
|
|
||||||
''
|
|
||||||
+ lib.optionalString (cfg'.path != "none" && cfg'.group != null) ''
|
|
||||||
chown :"${cfg'.group}" "${cfg'.path}"
|
|
||||||
''
|
|
||||||
+ lib.optionalString (cfg'.path != "none" && cfg'.defaultACLs != null) ''
|
|
||||||
${pkgs.acl}/bin/setfacl -d -m "${cfg'.defaultACLs}" "${cfg'.path}"
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
mergeAttrs = lib.foldl lib.mergeAttrs { };
|
|
||||||
in
|
in
|
||||||
mergeAttrs (lib.mapAttrsToList mkPool cfg.pools);
|
lib.attrsets.nameValuePair "zfsCreate-${name}" {
|
||||||
|
text = ''
|
||||||
|
${pkgs.zfs}/bin/zfs list ${dataset} > /dev/null 2>&1 \
|
||||||
|
|| ${pkgs.zfs}/bin/zfs create \
|
||||||
|
-o mountpoint=none \
|
||||||
|
${dataset} || :
|
||||||
|
|
||||||
system.preSwitchChecks."shb-zfs-snapshot" = lib.mkIf cfg.snapshotBeforeActivation.enable (
|
[ "$(${pkgs.zfs}/bin/zfs get -H mountpoint -o value ${dataset})" = ${cfg'.path} ] \
|
||||||
''
|
|| ${pkgs.zfs}/bin/zfs set \
|
||||||
name="${cfg.snapshotBeforeActivation.template}"
|
mountpoint=${cfg'.path} \
|
||||||
''
|
${dataset}
|
||||||
+ (
|
'';
|
||||||
let
|
}
|
||||||
recursiveFlag = lib.optionalString cfg.snapshotBeforeActivation.recursive "-r";
|
) cfg.datasets;
|
||||||
in
|
|
||||||
lib.concatMapStringsSep "\n" (ds: ''
|
|
||||||
echo "Taking ZFS snapshot of ${ds}@$name"
|
|
||||||
zfs snapshot ${recursiveFlag} ${ds}@"$name"
|
|
||||||
'') (lib.uniqueStrings datasets)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,138 +0,0 @@
|
||||||
# ZFS Block {#blocks-zfs}
|
|
||||||
|
|
||||||
Defined in [`/modules/blocks/zfs.nix`](@REPO@/modules/blocks/zfs.nix):
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
inputs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
inputs.selfhostblocks.nixosModules.zfs
|
|
||||||
];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This block creates ZFS datasets, optionally mounts them and sets permissions on the mount point.
|
|
||||||
It also enables taking snapshots on activation.
|
|
||||||
|
|
||||||
## Features {#blocks-zfs-features}
|
|
||||||
|
|
||||||
- Creates ZFS dataset which is [optionally mounted](#blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.path).
|
|
||||||
- Sets permissions, [owner](#blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.owner), [group](#blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.group) and [ACL](#blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.defaultACLs) on the mount point.
|
|
||||||
- Backup of the files in the dataset [`shb.zfs.<name>.backup`][backup] through the [backup contract](./contracts-backup.html).
|
|
||||||
- Backup of the dataset itself [`shb.zfs.<name>.datasetbackup`][datasetbackup] through the [dataset backup contract](./contracts-datasetbackup.html).
|
|
||||||
- Take a snapshot [before activating or deploying][activationsnapshot] allowing a safe rollback.
|
|
||||||
|
|
||||||
[backup]: #blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.backup
|
|
||||||
[datasetbackup]: #blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.datasetbackup
|
|
||||||
[activationsnapshot]: #blocks-zfs-options-shb.zfs.snapshotBeforeActivation.enable
|
|
||||||
|
|
||||||
## Usage {#blocks-zfs-usage}
|
|
||||||
|
|
||||||
Create a dataset at `root/safe/users` mounted on `/var/lib/nixos`:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
shb.zfs.pools.root.datasets."safe/users".path = "/var/lib/nixos";
|
|
||||||
```
|
|
||||||
|
|
||||||
Create a dataset at `backup/syncoid` but do not mount it:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
shb.zfs.pools.backup.datasets."syncoid".path = "none";
|
|
||||||
```
|
|
||||||
|
|
||||||
Create a dataset at `root/syncthing` and set custom permissions and ACL.
|
|
||||||
Permission and ACL are only enforced for the mount point.
|
|
||||||
|
|
||||||
```nix
|
|
||||||
shb.zfs.pools.root.datasets."syncthing" = {
|
|
||||||
path = "/srv/syncthing";
|
|
||||||
|
|
||||||
mode = "ug=rwx,g+s,o=";
|
|
||||||
owner = "syncthing";
|
|
||||||
group = "syncthing";
|
|
||||||
defaultACLs = "g:syncthing:rwX";
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backup dataset {#blocks-zfs-usage-backup-dataset}
|
|
||||||
|
|
||||||
To backup the dataset directly, use the [dataset backup contract](contracts-datasetbackup.html).
|
|
||||||
For example, with the sanoid module as the dataset backup contract provider:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.zfs.pools.root.datasets.home = {
|
|
||||||
path = "/home";
|
|
||||||
};
|
|
||||||
|
|
||||||
shb.sanoid.backup."root/home" = {
|
|
||||||
request = shb.zfs.pools.root.datasets.home.datasetBackup.request;
|
|
||||||
template = "yearly";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [Sanoid block](blocks-sanoid.html) for more examples.
|
|
||||||
|
|
||||||
### Backup files {#blocks-zfs-usage-backup-files}
|
|
||||||
|
|
||||||
To backup the files in the dataset, use the [file backup contract](contracts-backup.html).
|
|
||||||
For example, with the restic module as the file backup contract provider:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.zfs.pools.root.datasets.home = {
|
|
||||||
path = "/home";
|
|
||||||
};
|
|
||||||
|
|
||||||
shb.restic.instances."myservice" = {
|
|
||||||
request = shb.zfs.pools.root.datasets.home.backup.request;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [Restic block](blocks-restic.html) for more examples.
|
|
||||||
|
|
||||||
### Snapshot before activation {#blocks-zfs-usage-snapshot-before-activation}
|
|
||||||
|
|
||||||
To take a snapshot of all datasets before activating a new configuration:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.zfs.snapshotBeforeActivation.enable = true;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This does not take a recursive snapshot by default, to do it recursively, do:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.zfs.snapshotBeforeActivation = {
|
|
||||||
enable = true;
|
|
||||||
recursive = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
We did not specify datasets manually, which means the datasets list comes from `boot.zfs.extraPools`.
|
|
||||||
To specify the datasets manually, you can use:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.zfs.snapshotBeforeActivation = {
|
|
||||||
enable = true;
|
|
||||||
datasets = [ "root" "root/var" ];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Options Reference {#blocks-zfs-options}
|
|
||||||
|
|
||||||
```{=include=} options
|
|
||||||
id-prefix: blocks-zfs-options-
|
|
||||||
list-id: selfhostblocks-block-zfs-options
|
|
||||||
source: @OPTIONS_JSON@
|
|
||||||
```
|
|
||||||
|
|
@ -1,9 +1,4 @@
|
||||||
{
|
{ lib, shb, ... }:
|
||||||
lib,
|
|
||||||
shb,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
let
|
||||||
inherit (lib)
|
inherit (lib)
|
||||||
concatStringsSep
|
concatStringsSep
|
||||||
|
|
@ -163,7 +158,7 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Result part of the backup contract.
|
Result part of the backup contract.
|
||||||
|
|
||||||
Options set by the provider module that indicates the name of the backup and restore scripts.
|
Options set by the provider module that indicates the name of the backup and restor scripts.
|
||||||
'';
|
'';
|
||||||
default = {
|
default = {
|
||||||
inherit restoreScript backupService;
|
inherit restoreScript backupService;
|
||||||
|
|
@ -192,14 +187,12 @@ in
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} snapshots
|
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} snapshots
|
||||||
<snapshot 1> <metadata>
|
|
||||||
<snapshot 2> <metadata>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
And restore the database with:
|
And restore the database with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} restore <snapshot 1>
|
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} restore latest
|
||||||
```
|
```
|
||||||
'';
|
'';
|
||||||
type = str;
|
type = str;
|
||||||
|
|
@ -229,56 +222,4 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
passthru.mkRestoreScript =
|
|
||||||
{
|
|
||||||
name,
|
|
||||||
user,
|
|
||||||
sudoPreserveEnvs ? [ ],
|
|
||||||
secretsFile,
|
|
||||||
restoreCmd,
|
|
||||||
listCmd,
|
|
||||||
}:
|
|
||||||
pkgs.writeShellApplication {
|
|
||||||
inherit name;
|
|
||||||
|
|
||||||
text = ''
|
|
||||||
usage() {
|
|
||||||
echo "$0 snapshots"
|
|
||||||
echo "$0 restore <snapshot>"
|
|
||||||
}
|
|
||||||
|
|
||||||
sudocmd() {
|
|
||||||
sudo --preserve-env=${lib.concatStringsSep "," sudoPreserveEnvs} -u ${user} "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
loadenv() {
|
|
||||||
set -a
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
source <(sudocmd cat "${secretsFile}")
|
|
||||||
set +a
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ "$1" = "restore" ]; then
|
|
||||||
shift
|
|
||||||
snapshot="$1"
|
|
||||||
|
|
||||||
loadenv
|
|
||||||
|
|
||||||
echo "Will restore snapshot '$snapshot'"
|
|
||||||
|
|
||||||
sudocmd sh -c "${restoreCmd}"
|
|
||||||
elif [ "$1" = "snapshots" ]; then
|
|
||||||
shift
|
|
||||||
|
|
||||||
loadenv
|
|
||||||
|
|
||||||
sudocmd sh -c "${listCmd}"
|
|
||||||
else
|
|
||||||
usage
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,22 +20,25 @@ source: @OPTIONS_JSON@
|
||||||
## Usage {#contract-backup-usage}
|
## Usage {#contract-backup-usage}
|
||||||
|
|
||||||
A service that can be backed up will provide a `backup` option.
|
A service that can be backed up will provide a `backup` option.
|
||||||
|
Such a service is a `requester` providing a `request` for a module `provider` of this contract.
|
||||||
|
|
||||||
Here is an example module defining such a `backup` option,
|
What this option defines is, from the user perspective - that is _you_ - an implementation detail
|
||||||
which defines what directories to backup (`sourceDirectories`)
|
but it will at least define what directories to backup,
|
||||||
and the user to backup with (`user`).
|
the user to backup with
|
||||||
|
and possibly hooks to run before or after the backup job runs.
|
||||||
|
|
||||||
|
Here is an example module defining such a `backup` option:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
{
|
{
|
||||||
options = {
|
options = {
|
||||||
myservice.backup = mkOption {
|
myservice.backup = mkOption {
|
||||||
type = lib.types.submodule {
|
type = contracts.backup.request;
|
||||||
options = shb.contracts.backup.mkRequester {
|
default = {
|
||||||
user = "nextcloud";
|
user = "myservice";
|
||||||
sourceDirectories = [
|
sourceDirectories = [
|
||||||
"/var/lib/nextcloud"
|
"/var/lib/myservice"
|
||||||
];
|
];
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -45,13 +48,13 @@ and the user to backup with (`user`).
|
||||||
Now, on the other side we have a service that uses this `backup` option and actually backs up files.
|
Now, on the other side we have a service that uses this `backup` option and actually backs up files.
|
||||||
This service is a `provider` of this contract and will provide a `result` option.
|
This service is a `provider` of this contract and will provide a `result` option.
|
||||||
|
|
||||||
Let's assume such a module is available under the `backupService` option
|
Let's assume such a module is available under the `backupservice` option
|
||||||
and that one can create multiple backup instances under `backupService.instances`.
|
and that one can create multiple backup instances under `backupservice.instances`.
|
||||||
Then, to actually backup the `myservice` service, one would write:
|
Then, to actually backup the `myservice` service, one would write:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
backupService.instances.myservice = {
|
backupservice.instances.myservice = {
|
||||||
request = myservice.backup.request;
|
request = myservice.backup;
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
@ -60,17 +63,17 @@ backupService.instances.myservice = {
|
||||||
path = "/srv/backup/myservice";
|
path = "/srv/backup/myservice";
|
||||||
};
|
};
|
||||||
|
|
||||||
# ... Other options specific to backupService like scheduling.
|
# ... Other options specific to backupservice like scheduling.
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
It is advised to backup files to different location, to improve redundancy.
|
It is advised to backup files to different location, to improve redundancy.
|
||||||
Thanks to using contracts, this can be made easily either with the same `backupService`:
|
Thanks to using contracts, this can be made easily either with the same `backupservice`:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
backupService.instances.myservice_2 = {
|
backupservice.instances.myservice_2 = {
|
||||||
request = myservice.backup.request;
|
request = myservice.backup;
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
@ -82,13 +85,12 @@ backupService.instances.myservice_2 = {
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Or with another module `backupService_2`!
|
Or with another module `backupservice_2`!
|
||||||
|
|
||||||
## Providers of the Backup Contract {#contract-backup-providers}
|
## Providers of the Backup Contract {#contract-backup-providers}
|
||||||
|
|
||||||
- [Restic block](blocks-restic.html).
|
- [Restic block](blocks-restic.html).
|
||||||
- [Borgbackup block](blocks-borgbackup.html).
|
- [Borgbackup block](blocks-borgbackup.html) [WIP].
|
||||||
- [ZFS block](blocks-zfs.html).
|
|
||||||
|
|
||||||
## Requester Blocks and Services {#contract-backup-requesters}
|
## Requester Blocks and Services {#contract-backup-requesters}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ in
|
||||||
"/opt/files/A"
|
"/opt/files/A"
|
||||||
"/opt/files/B"
|
"/opt/files/B"
|
||||||
],
|
],
|
||||||
settings ? { ... }: { }, # { filesRoot, config } -> attrset
|
settings, # { repository, config } -> attrset
|
||||||
extraConfig ? null, # { filesRoot, username, config } -> attrset
|
extraConfig ? null, # { username, config } -> attrset
|
||||||
}:
|
}:
|
||||||
shb.test.runNixOSTest {
|
shb.test.runNixOSTest {
|
||||||
inherit name;
|
inherit name;
|
||||||
|
|
@ -40,7 +40,7 @@ shb.test.runNixOSTest {
|
||||||
};
|
};
|
||||||
settings = settings {
|
settings = settings {
|
||||||
inherit config;
|
inherit config;
|
||||||
filesRoot = "/opt/files";
|
repository = "/opt/repos/${name}";
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
(mkIf (username != "root") {
|
(mkIf (username != "root") {
|
||||||
|
|
@ -52,7 +52,6 @@ shb.test.runNixOSTest {
|
||||||
})
|
})
|
||||||
(optionalAttrs (extraConfig != null) (extraConfig {
|
(optionalAttrs (extraConfig != null) (extraConfig {
|
||||||
inherit username config;
|
inherit username config;
|
||||||
filesRoot = "/opt/files";
|
|
||||||
}))
|
}))
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
@ -66,9 +65,7 @@ shb.test.runNixOSTest {
|
||||||
provider = (getAttrFromPath providerRoot nodes.machine).result;
|
provider = (getAttrFromPath providerRoot nodes.machine).result;
|
||||||
in
|
in
|
||||||
''
|
''
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from dictdiffer import diff
|
from dictdiffer import diff
|
||||||
import re
|
|
||||||
|
|
||||||
username = "${username}"
|
username = "${username}"
|
||||||
sourceDirectories = [ ${concatMapStringsSep ", " (x: ''"${x}"'') sourceDirectories} ]
|
sourceDirectories = [ ${concatMapStringsSep ", " (x: ''"${x}"'') sourceDirectories} ]
|
||||||
|
|
@ -106,25 +103,9 @@ shb.test.runNixOSTest {
|
||||||
f'{path}/fileB': 'repo_fileB_1',
|
f'{path}/fileB': 'repo_fileB_1',
|
||||||
})
|
})
|
||||||
|
|
||||||
with subtest("Initial snapshot"):
|
|
||||||
out = machine.succeed("${provider.restoreScript} snapshots").splitlines()
|
|
||||||
if len(out) != 0:
|
|
||||||
raise Exception(f"Unexpected snapshots:\n{out}")
|
|
||||||
|
|
||||||
with subtest("First backup in repo"):
|
with subtest("First backup in repo"):
|
||||||
print(machine.succeed("systemctl cat ${provider.backupService}"))
|
print(machine.succeed("systemctl cat ${provider.backupService}"))
|
||||||
machine.succeed("systemctl start --wait ${provider.backupService}")
|
machine.succeed("systemctl start ${provider.backupService}")
|
||||||
|
|
||||||
with subtest("One snapshot"):
|
|
||||||
out = machine.succeed("${provider.restoreScript} snapshots").splitlines()
|
|
||||||
print(f"Found snapshots:\n{out}")
|
|
||||||
if len(out) != 1:
|
|
||||||
raise Exception(f"Unexpected snapshots:\n{out}")
|
|
||||||
|
|
||||||
# To accomodate for snapshot orchestrators which keep only a given amount
|
|
||||||
# of snapshots per unit of time, we set the time to now + 2h.
|
|
||||||
new_date = (datetime.now() + timedelta(hours=2)).strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
machine.succeed(f"timedatectl set-time '{new_date}'")
|
|
||||||
|
|
||||||
with subtest("New content"):
|
with subtest("New content"):
|
||||||
for path in sourceDirectories:
|
for path in sourceDirectories:
|
||||||
|
|
@ -138,37 +119,14 @@ shb.test.runNixOSTest {
|
||||||
f'{path}/fileB': 'repo_fileB_2',
|
f'{path}/fileB': 'repo_fileB_2',
|
||||||
})
|
})
|
||||||
|
|
||||||
with subtest("Second backup in repo"):
|
|
||||||
machine.succeed("systemctl start --wait ${provider.backupService}")
|
|
||||||
|
|
||||||
with subtest("two snapshots"):
|
|
||||||
out = machine.succeed("${provider.restoreScript} snapshots").splitlines()
|
|
||||||
print(f"Found snapshots:\n{out}")
|
|
||||||
if len(out) != 2:
|
|
||||||
raise Exception(f"Unexpected snapshots:\n{out}")
|
|
||||||
|
|
||||||
firstSnapshot = re.split("[ \t+]", out[0], maxsplit=1)[0]
|
|
||||||
secondSnapshot = re.split("[ \t+]", out[1], maxsplit=1)[0]
|
|
||||||
print(f"First snapshot {firstSnapshot}")
|
|
||||||
print(f"Second snapshot {secondSnapshot}")
|
|
||||||
|
|
||||||
with subtest("Delete content"):
|
with subtest("Delete content"):
|
||||||
for path in sourceDirectories:
|
for path in sourceDirectories:
|
||||||
machine.succeed(f"""rm -r {path}/*""")
|
machine.succeed(f"""rm -r {path}/*""")
|
||||||
|
|
||||||
assert_files(path, {})
|
assert_files(path, {})
|
||||||
|
|
||||||
with subtest("Restore second backup"):
|
with subtest("Restore initial content from repo"):
|
||||||
machine.succeed(f"${provider.restoreScript} restore {secondSnapshot}")
|
machine.succeed("""${provider.restoreScript} restore latest""")
|
||||||
|
|
||||||
for path in sourceDirectories:
|
|
||||||
assert_files(path, {
|
|
||||||
f'{path}/fileA': 'repo_fileA_2',
|
|
||||||
f'{path}/fileB': 'repo_fileB_2',
|
|
||||||
})
|
|
||||||
|
|
||||||
with subtest("Restore first backup"):
|
|
||||||
machine.succeed(f"${provider.restoreScript} restore {firstSnapshot}")
|
|
||||||
|
|
||||||
for path in sourceDirectories:
|
for path in sourceDirectories:
|
||||||
assert_files(path, {
|
assert_files(path, {
|
||||||
|
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
{ lib, ... }:
|
|
||||||
let
|
|
||||||
inherit (lib) mkOption;
|
|
||||||
inherit (lib.types)
|
|
||||||
nullOr
|
|
||||||
submodule
|
|
||||||
str
|
|
||||||
;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
mkRequest =
|
|
||||||
{
|
|
||||||
serviceName ? "",
|
|
||||||
externalUrl ? "",
|
|
||||||
externalUrlText ? null,
|
|
||||||
internalUrl ? null,
|
|
||||||
internalUrlText ? null,
|
|
||||||
apiKey ? null,
|
|
||||||
}:
|
|
||||||
mkOption {
|
|
||||||
description = ''
|
|
||||||
Request part of the dashboard contract.
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = submodule {
|
|
||||||
options = {
|
|
||||||
externalUrl =
|
|
||||||
mkOption {
|
|
||||||
description = ''
|
|
||||||
URL at which the service can be accessed.
|
|
||||||
|
|
||||||
This URL should go through the reverse proxy.
|
|
||||||
'';
|
|
||||||
type = str;
|
|
||||||
default = externalUrl;
|
|
||||||
example = "https://jellyfin.example.com";
|
|
||||||
}
|
|
||||||
// (lib.optionalAttrs (externalUrlText != null) {
|
|
||||||
defaultText = externalUrlText;
|
|
||||||
});
|
|
||||||
|
|
||||||
internalUrl =
|
|
||||||
mkOption {
|
|
||||||
description = ''
|
|
||||||
URL at which the service can be accessed directly.
|
|
||||||
|
|
||||||
This URL should bypass the reverse proxy.
|
|
||||||
It can be used for example to ping the service
|
|
||||||
and making sure it is up and running correctly.
|
|
||||||
'';
|
|
||||||
type = nullOr str;
|
|
||||||
default = internalUrl;
|
|
||||||
example = "http://127.0.0.1:8081";
|
|
||||||
}
|
|
||||||
// (lib.optionalAttrs (internalUrlText != null) {
|
|
||||||
defaultText = internalUrlText;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
mkResult =
|
|
||||||
{
|
|
||||||
}:
|
|
||||||
mkOption {
|
|
||||||
description = ''
|
|
||||||
Result part of the dashboard contract.
|
|
||||||
|
|
||||||
No option is provided here.
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = submodule {
|
|
||||||
options = {
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
# Dashboard Contract {#contract-dashboard}
|
|
||||||
|
|
||||||
This NixOS contract is used for user-facing services
|
|
||||||
that want to be displayed on a dashboard.
|
|
||||||
|
|
||||||
It is a contract between a service that can be accessed through an URL
|
|
||||||
and a service that wants to show a list of those services.
|
|
||||||
|
|
||||||
## Providers {#contract-dashboard-providers}
|
|
||||||
|
|
||||||
The providers of this contract in SHB are:
|
|
||||||
|
|
||||||
<!-- Somehow generate this list -->
|
|
||||||
|
|
||||||
- The homepage service under its [shb.homepage.servicesGroups.<name>.services.<name>.dashboard](#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.dashboard) option.
|
|
||||||
|
|
||||||
## Usage {#contracts-dashboard-usage}
|
|
||||||
|
|
||||||
A service that can be shown on a dashboard will provide a `dashboard` option.
|
|
||||||
|
|
||||||
Here is an example module defining such a requester option for this dashboard contract:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
options = {
|
|
||||||
myservice.dashboard = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Dashboard contract consumer
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.dashboard.mkRequester {
|
|
||||||
externalUrl = "https://${config.myservice.subdomain}.${config.myservice.domain}";
|
|
||||||
internalUrl = "http://127.0.0.1:${config.myservice.port}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, plug both consumer and provider together in the `config`:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
config = {
|
|
||||||
<provider-module> = {
|
|
||||||
dashboard.request = config.myservice.dashboard.request;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
And that's it for the contract part.
|
|
||||||
For more specific details on each provider, go to their respective manual pages.
|
|
||||||
|
|
||||||
## Contract Reference {#contract-dashboard-options}
|
|
||||||
|
|
||||||
These are all the options that are expected to exist for this contract to be respected.
|
|
||||||
|
|
||||||
```{=include=} options
|
|
||||||
id-prefix: contracts-dashboard-options-
|
|
||||||
list-id: selfhostblocks-options
|
|
||||||
source: @OPTIONS_JSON@
|
|
||||||
```
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
{ lib, shb, ... }:
|
|
||||||
let
|
|
||||||
inherit (lib) mkOption;
|
|
||||||
inherit (lib.types) submodule;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
../../../lib/module.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
options.shb.contracts.dashboard = mkOption {
|
|
||||||
description = ''
|
|
||||||
Contract for user-facing services that want to
|
|
||||||
be displayed on a dashboard.
|
|
||||||
|
|
||||||
The requester communicates to the provider
|
|
||||||
how to access the service
|
|
||||||
through the `request` options.
|
|
||||||
|
|
||||||
The provider reads from the `request` options
|
|
||||||
and configures what is necessary on its side
|
|
||||||
to show the service and check its availability.
|
|
||||||
It does not communicate back to the requester.
|
|
||||||
'';
|
|
||||||
|
|
||||||
type = submodule {
|
|
||||||
options = shb.contracts.dashboard.contract;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -24,7 +24,7 @@ in
|
||||||
}:
|
}:
|
||||||
mkOption {
|
mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Request part of the database backup contract.
|
Request part of the backup contract.
|
||||||
|
|
||||||
Options set by the requester module
|
Options set by the requester module
|
||||||
enforcing how to backup files.
|
enforcing how to backup files.
|
||||||
|
|
@ -122,9 +122,9 @@ in
|
||||||
}:
|
}:
|
||||||
mkOption {
|
mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
Result part of the database backup contract.
|
Result part of the backup contract.
|
||||||
|
|
||||||
Options set by the provider module that indicates the name of the backup and restore scripts.
|
Options set by the provider module that indicates the name of the backup and restor scripts.
|
||||||
'';
|
'';
|
||||||
default = {
|
default = {
|
||||||
inherit restoreScript backupService;
|
inherit restoreScript backupService;
|
||||||
|
|
|
||||||
|
|
@ -20,21 +20,21 @@ source: @OPTIONS_JSON@
|
||||||
## Usage {#contract-databasebackup-usage}
|
## Usage {#contract-databasebackup-usage}
|
||||||
|
|
||||||
A database that can be backed up will provide a `databasebackup` option.
|
A database that can be backed up will provide a `databasebackup` option.
|
||||||
|
Such a service is a `requester` providing a `request` for a module `provider` of this contract.
|
||||||
|
|
||||||
What this option defines is, from the user perspective - that is _you_ - an implementation detail
|
What this option defines is, from the user perspective - that is _you_ - an implementation detail
|
||||||
but it will at least define how to create a database dump,
|
but it will at least define how to create a database dump,
|
||||||
the user to backup with
|
the user to backup with
|
||||||
and how to restore from a database dump.
|
and how to restore from a database dump.
|
||||||
|
|
||||||
Here is an example module defining such a `databasebackup` option,
|
Here is an example module defining such a `databasebackup` option:
|
||||||
which defines the user to backup with (`user`)
|
|
||||||
and how to backup (`backupCmd`) and restore (`restoreCmd`) the database:
|
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
{
|
{
|
||||||
options = {
|
options = {
|
||||||
myservice.databasebackup = mkOption {
|
myservice.databasebackup = mkOption {
|
||||||
type = shb.contracts.databasebackup.mkRequester = {
|
type = contracts.databasebackup.request;
|
||||||
|
default = {
|
||||||
user = "myservice";
|
user = "myservice";
|
||||||
backupCmd = ''
|
backupCmd = ''
|
||||||
${pkgs.postgresql}/bin/pg_dumpall | ${pkgs.gzip}/bin/gzip --rsyncable
|
${pkgs.postgresql}/bin/pg_dumpall | ${pkgs.gzip}/bin/gzip --rsyncable
|
||||||
|
|
@ -48,16 +48,16 @@ and how to backup (`backupCmd`) and restore (`restoreCmd`) the database:
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Now, on the other side we have a service that uses this `databasebackup` option and actually backs up files.
|
Now, on the other side we have a service that uses this `backup` option and actually backs up files.
|
||||||
This service is a `provider` of this contract and will provide a `result` option.
|
This service is a `provider` of this contract and will provide a `result` option.
|
||||||
|
|
||||||
Let's assume such a module is available under the `databaseBackupService` option
|
Let's assume such a module is available under the `databasebackupservice` option
|
||||||
and that one can create multiple backup instances under `databaseBackupService.instances`.
|
and that one can create multiple backup instances under `databasebackupservice.instances`.
|
||||||
Then, to actually backup the `myservice` service, one would write:
|
Then, to actually backup the `myservice` service, one would write:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
databaseBackupService.instances.myservice = {
|
databasebackupservice.instances.myservice = {
|
||||||
request = myservice.databasebackup.request;
|
request = myservice.databasebackup;
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
@ -72,11 +72,11 @@ databaseBackupService.instances.myservice = {
|
||||||
```
|
```
|
||||||
|
|
||||||
It is advised to backup files to different location, to improve redundancy.
|
It is advised to backup files to different location, to improve redundancy.
|
||||||
Thanks to using contracts, this can be made easily either with the same `databaseBackupService`:
|
Thanks to using contracts, this can be made easily either with the same `databasebackupservice`:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
databaseBackupService.instances.myservice_2 = {
|
databasebackupservice.instances.myservice_2 = {
|
||||||
request = myservice.databasebackup.request;
|
request = myservice.backup;
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
@ -88,12 +88,12 @@ databaseBackupService.instances.myservice_2 = {
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Or with another module `databaseBackupService_2`!
|
Or with another module `databasebackupservice_2`!
|
||||||
|
|
||||||
## Providers of the Database Backup Contract {#contract-databasebackup-providers}
|
## Providers of the Database Backup Contract {#contract-databasebackup-providers}
|
||||||
|
|
||||||
- [Restic block](blocks-restic.html).
|
- [Restic block](blocks-restic.html).
|
||||||
- [Borgbackup block](blocks-borgbackup.html).
|
- [Borgbackup block](blocks-borgbackup.html) [WIP].
|
||||||
|
|
||||||
## Requester Blocks and Services {#contract-databasebackup-requesters}
|
## Requester Blocks and Services {#contract-databasebackup-requesters}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,18 +51,16 @@ shb.test.runNixOSTest {
|
||||||
testScript =
|
testScript =
|
||||||
{ nodes, ... }:
|
{ nodes, ... }:
|
||||||
let
|
let
|
||||||
provider = (getAttrFromPath providerRoot nodes.machine).result;
|
provider = getAttrFromPath providerRoot nodes.machine;
|
||||||
in
|
in
|
||||||
''
|
''
|
||||||
import csv
|
import csv
|
||||||
import re
|
|
||||||
|
|
||||||
start_all()
|
start_all()
|
||||||
machine.wait_for_unit("postgresql.service")
|
machine.wait_for_unit("postgresql.service")
|
||||||
machine.wait_for_unit("postgresql-setup.service")
|
|
||||||
machine.wait_for_open_port(5432)
|
machine.wait_for_open_port(5432)
|
||||||
|
|
||||||
def peer_cmd(cmd, db="${database}"):
|
def peer_cmd(cmd, db="me"):
|
||||||
return "sudo -u ${database} psql -U ${database} {db} --csv --command \"{cmd}\"".format(cmd=cmd, db=db)
|
return "sudo -u ${database} psql -U ${database} {db} --csv --command \"{cmd}\"".format(cmd=cmd, db=db)
|
||||||
|
|
||||||
def query(query):
|
def query(query):
|
||||||
|
|
@ -75,68 +73,30 @@ shb.test.runNixOSTest {
|
||||||
if len(diff) > 0:
|
if len(diff) > 0:
|
||||||
raise Exception(i, diff)
|
raise Exception(i, diff)
|
||||||
|
|
||||||
|
table = [{'name': 'car', 'count': '1'}, {'name': 'lollipop', 'count': '2'}]
|
||||||
|
|
||||||
with subtest("create fixture"):
|
with subtest("create fixture"):
|
||||||
machine.succeed(peer_cmd("CREATE TABLE test (name text, count int)"))
|
machine.succeed(peer_cmd("CREATE TABLE test (name text, count int)"))
|
||||||
machine.succeed(peer_cmd("INSERT INTO test VALUES ('car', 1)"))
|
machine.succeed(peer_cmd("INSERT INTO test VALUES ('car', 1), ('lollipop', 2)"))
|
||||||
|
|
||||||
res = query("SELECT * FROM test")
|
res = query("SELECT * FROM test")
|
||||||
table = [{'name': 'car', 'count': '1'}]
|
|
||||||
cmp_tables(res, table)
|
|
||||||
|
|
||||||
with subtest("Initial snapshots"):
|
|
||||||
out = machine.succeed("${provider.restoreScript} snapshots").splitlines()
|
|
||||||
print(f"Found snapshots:\n{out}")
|
|
||||||
if len(out) != 0:
|
|
||||||
raise Exception(f"Unexpected snapshots:\n{out}")
|
|
||||||
|
|
||||||
with subtest("backup"):
|
|
||||||
machine.succeed("systemctl start --wait ${provider.backupService}")
|
|
||||||
|
|
||||||
with subtest("One snapshot"):
|
|
||||||
out = machine.succeed("${provider.restoreScript} snapshots").splitlines()
|
|
||||||
print(f"Found snapshots:\n{out}")
|
|
||||||
if len(out) != 1:
|
|
||||||
raise Exception(f"Unexpected snapshots:\n{out}")
|
|
||||||
|
|
||||||
with subtest("New content"):
|
|
||||||
machine.succeed(peer_cmd("INSERT INTO test VALUES ('lollipop', 2)"))
|
|
||||||
|
|
||||||
res = query("SELECT * FROM test")
|
|
||||||
table = [{'name': 'car', 'count': '1'}, {'name': 'lollipop', 'count': '2'}]
|
|
||||||
cmp_tables(res, table)
|
cmp_tables(res, table)
|
||||||
|
|
||||||
with subtest("backup"):
|
with subtest("backup"):
|
||||||
machine.succeed("systemctl start --wait ${provider.backupService}")
|
print(machine.succeed("systemctl cat ${provider.result.backupService}"))
|
||||||
|
print(machine.succeed("ls -l /run/hardcodedsecrets/hardcodedsecret_passphrase"))
|
||||||
with subtest("Two snapshots"):
|
machine.succeed("systemctl start ${provider.result.backupService}")
|
||||||
out = machine.succeed("${provider.restoreScript} snapshots").splitlines()
|
|
||||||
print(f"Found snapshots:\n{out}")
|
|
||||||
if len(out) != 2:
|
|
||||||
raise Exception(f"Unexpected snapshots:\n{out}")
|
|
||||||
|
|
||||||
firstSnapshot = re.split("[ \t+]", out[0], maxsplit=1)[0]
|
|
||||||
secondSnapshot = re.split("[ \t+]", out[1], maxsplit=1)[0]
|
|
||||||
print(f"First snapshot {firstSnapshot}")
|
|
||||||
print(f"Second snapshot {secondSnapshot}")
|
|
||||||
|
|
||||||
with subtest("drop database"):
|
with subtest("drop database"):
|
||||||
machine.succeed(peer_cmd("DROP DATABASE ${database}", db="postgres"))
|
machine.succeed(peer_cmd("DROP DATABASE ${database}", db="postgres"))
|
||||||
machine.fail(peer_cmd("SELECT * FROM test"))
|
machine.fail(peer_cmd("SELECT * FROM test"))
|
||||||
|
|
||||||
with subtest("restore second snapshot"):
|
with subtest("restore"):
|
||||||
print(machine.succeed("readlink -f $(type ${provider.restoreScript})"))
|
print(machine.succeed("readlink -f $(type ${provider.result.restoreScript})"))
|
||||||
machine.succeed(f"${provider.restoreScript} restore {secondSnapshot}")
|
machine.succeed("${provider.result.restoreScript} restore latest ")
|
||||||
|
|
||||||
|
with subtest("check restoration"):
|
||||||
res = query("SELECT * FROM test")
|
res = query("SELECT * FROM test")
|
||||||
table = [{'name': 'car', 'count': '1'}, {'name': 'lollipop', 'count': '2'}]
|
|
||||||
cmp_tables(res, table)
|
|
||||||
|
|
||||||
with subtest("restore first snapshot"):
|
|
||||||
print(machine.succeed("readlink -f $(type ${provider.restoreScript})"))
|
|
||||||
machine.succeed(f"${provider.restoreScript} restore {firstSnapshot}")
|
|
||||||
|
|
||||||
res = query("SELECT * FROM test")
|
|
||||||
table = [{'name': 'car', 'count': '1'}]
|
|
||||||
cmp_tables(res, table)
|
cmp_tables(res, table)
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
{ lib, ... }:
|
|
||||||
let
|
|
||||||
inherit (lib)
|
|
||||||
literalMD
|
|
||||||
mkOption
|
|
||||||
optionalAttrs
|
|
||||||
;
|
|
||||||
inherit (lib.types)
|
|
||||||
submodule
|
|
||||||
str
|
|
||||||
;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
mkRequest =
|
|
||||||
{
|
|
||||||
dataset ? "",
|
|
||||||
datasetText ? null,
|
|
||||||
}:
|
|
||||||
mkOption {
|
|
||||||
description = ''
|
|
||||||
Request part of the backup contract.
|
|
||||||
|
|
||||||
Options set by the requester module
|
|
||||||
enforcing how to backup files.
|
|
||||||
'';
|
|
||||||
|
|
||||||
default = { };
|
|
||||||
|
|
||||||
type = submodule {
|
|
||||||
options = {
|
|
||||||
dataset =
|
|
||||||
mkOption {
|
|
||||||
description = ''
|
|
||||||
Dataset to backup, including the pool name.
|
|
||||||
'';
|
|
||||||
type = str;
|
|
||||||
example = "root/home";
|
|
||||||
default = dataset;
|
|
||||||
}
|
|
||||||
// optionalAttrs (datasetText != null) {
|
|
||||||
defaultText = literalMD datasetText;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
mkResult =
|
|
||||||
{
|
|
||||||
restoreScript ? "restore",
|
|
||||||
restoreScriptText ? null,
|
|
||||||
backupService ? "backup.service",
|
|
||||||
backupServiceText ? null,
|
|
||||||
}:
|
|
||||||
mkOption {
|
|
||||||
description = ''
|
|
||||||
Result part of the backup contract.
|
|
||||||
|
|
||||||
Options set by the provider module that indicates the name of the backup and restore scripts.
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
|
|
||||||
type = submodule {
|
|
||||||
options = {
|
|
||||||
restoreScript =
|
|
||||||
mkOption {
|
|
||||||
description = ''
|
|
||||||
Name of script that can restore the database.
|
|
||||||
One can then list snapshots with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} snapshots
|
|
||||||
<snapshot 1> <metadata>
|
|
||||||
<snapshot 2> <metadata>
|
|
||||||
```
|
|
||||||
|
|
||||||
And restore the database with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ ${if restoreScriptText != null then restoreScriptText else restoreScript} restore <snapshot 1>
|
|
||||||
```
|
|
||||||
|
|
||||||
It is not garanteed to be able to restore back to a snapshot in the future.
|
|
||||||
With the above example, it may not be possible to restore `<snapshot 2>`
|
|
||||||
after having restored `<snapshot 1>`.
|
|
||||||
'';
|
|
||||||
type = str;
|
|
||||||
default = restoreScript;
|
|
||||||
}
|
|
||||||
// optionalAttrs (restoreScriptText != null) {
|
|
||||||
defaultText = literalMD restoreScriptText;
|
|
||||||
};
|
|
||||||
|
|
||||||
backupService =
|
|
||||||
mkOption {
|
|
||||||
description = ''
|
|
||||||
Name of service backing up the database.
|
|
||||||
|
|
||||||
This script can be ran manually to backup the database:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ systemctl start ${if backupServiceText != null then backupServiceText else backupService}
|
|
||||||
```
|
|
||||||
'';
|
|
||||||
type = str;
|
|
||||||
default = backupService;
|
|
||||||
}
|
|
||||||
// optionalAttrs (backupServiceText != null) {
|
|
||||||
defaultText = literalMD backupServiceText;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
# Dataset Backup Contract {#contract-dataset-backup}
|
|
||||||
|
|
||||||
This NixOS contract represents a backup job
|
|
||||||
that will backup one ZFS dataset.
|
|
||||||
|
|
||||||
It is a contract between a service that manages ZFS datasets
|
|
||||||
and a service that backs up ZFS datasets.
|
|
||||||
|
|
||||||
## Contract Reference {#contract-datatsetbackup-options}
|
|
||||||
|
|
||||||
These are all the options that are expected to exist for this contract to be respected.
|
|
||||||
|
|
||||||
```{=include=} options
|
|
||||||
id-prefix: contracts-datasetbackup-options-
|
|
||||||
list-id: selfhostblocks-options
|
|
||||||
source: @OPTIONS_JSON@
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage {#contract-datasetbackup-usage}
|
|
||||||
|
|
||||||
A service that manages ZFS datasets that can be backed up will provide a `datasetbackup` option.
|
|
||||||
|
|
||||||
Here is an example module defining such a `backup` option,
|
|
||||||
which defines what directories to backup (`sourceDirectories`)
|
|
||||||
and the user to backup with (`user`).
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
options = {
|
|
||||||
myservice.datasetbackup = mkOption {
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.datasetbackup.mkRequester {
|
|
||||||
dataset = "root/test";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, on the other side we have a service that uses this `datasetbackup` option and actually backs up the dataset.
|
|
||||||
This service is a `provider` of this contract and will provide a `result` option.
|
|
||||||
|
|
||||||
Let's assume such a module is available under the `backupService` option
|
|
||||||
and that one can create multiple backup instances under `backupService.instances`.
|
|
||||||
Then, to actually backup the `myservice` service, one would write:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
backupService.instances.myservice = {
|
|
||||||
request = myservice.datasetbackup.request;
|
|
||||||
|
|
||||||
settings = {
|
|
||||||
enable = true;
|
|
||||||
|
|
||||||
targetDataset = "backup/myservice";
|
|
||||||
|
|
||||||
# ... Other options specific to backupService like scheduling.
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
It is advised to backup files to different location, to improve redundancy.
|
|
||||||
Thanks to using contracts, this can be made easily either with the same `backupService`:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
backupService.instances.myservice_2 = {
|
|
||||||
request = myservice.datasetbackup.request;
|
|
||||||
|
|
||||||
settings = {
|
|
||||||
enable = true;
|
|
||||||
|
|
||||||
targetDataset = "backup2/myservice";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
Or with another module `backupService_2`!
|
|
||||||
|
|
||||||
## Providers of the Backup Contract {#contract-datasetbackup-providers}
|
|
||||||
|
|
||||||
- [Sanoid block](blocks-sanoid.html).
|
|
||||||
|
|
||||||
## Requester Blocks and Services {#contract-datasetbackup-requesters}
|
|
||||||
|
|
||||||
- [ZFS](blocks-zfs.html#blocks-zfs-backup).
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
{ lib, shb, ... }:
|
|
||||||
let
|
|
||||||
inherit (lib) mkOption;
|
|
||||||
inherit (lib.types) submodule;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
../../../lib/module.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
options.shb.contracts.datasetbackup = mkOption {
|
|
||||||
description = ''
|
|
||||||
Contract for backing up ZFS datasets.
|
|
||||||
|
|
||||||
The requester communicates to the provider
|
|
||||||
the dataset to backup
|
|
||||||
through the `request` options.
|
|
||||||
|
|
||||||
The provider reads from the `request` options
|
|
||||||
and backs up the requested dataset.
|
|
||||||
It communicates to the requester what script is used
|
|
||||||
to backup and restore the files
|
|
||||||
through the `result` options.
|
|
||||||
'';
|
|
||||||
|
|
||||||
type = submodule {
|
|
||||||
options = shb.contracts.datasetbackup.contract;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
{
|
|
||||||
pkgs,
|
|
||||||
lib,
|
|
||||||
shb,
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
inherit (lib)
|
|
||||||
concatMapStringsSep
|
|
||||||
getAttrFromPath
|
|
||||||
setAttrByPath
|
|
||||||
;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
name,
|
|
||||||
providerRoot,
|
|
||||||
modules ? [ ],
|
|
||||||
dataset ? "root/test",
|
|
||||||
sourceDirectories ? [
|
|
||||||
"/opt/files/A"
|
|
||||||
"/opt/files/B"
|
|
||||||
],
|
|
||||||
settings ? { ... }: { }, # { filesRoot, config } -> attrset
|
|
||||||
extraConfig ? null, # { filesRoot, config } -> attrset
|
|
||||||
}:
|
|
||||||
shb.test.runNixOSTest {
|
|
||||||
inherit name;
|
|
||||||
|
|
||||||
nodes.machine =
|
|
||||||
{ config, ... }:
|
|
||||||
{
|
|
||||||
imports = [ shb.test.baseImports ] ++ modules;
|
|
||||||
|
|
||||||
config = lib.mkMerge [
|
|
||||||
(setAttrByPath providerRoot {
|
|
||||||
request = {
|
|
||||||
inherit dataset;
|
|
||||||
};
|
|
||||||
settings = settings {
|
|
||||||
inherit config;
|
|
||||||
filesRoot = "/opt/files";
|
|
||||||
};
|
|
||||||
})
|
|
||||||
(lib.optionalAttrs (extraConfig != null) (extraConfig {
|
|
||||||
inherit config;
|
|
||||||
filesRoot = "/opt/files";
|
|
||||||
}))
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
extraPythonPackages = p: [ p.dictdiffer ];
|
|
||||||
skipTypeCheck = true;
|
|
||||||
|
|
||||||
testScript =
|
|
||||||
{ nodes, ... }:
|
|
||||||
let
|
|
||||||
provider = (getAttrFromPath providerRoot nodes.machine).result;
|
|
||||||
in
|
|
||||||
''
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from dictdiffer import diff
|
|
||||||
import re
|
|
||||||
|
|
||||||
sourceDirectories = [ ${concatMapStringsSep ", " (x: ''"${x}"'') sourceDirectories} ]
|
|
||||||
|
|
||||||
def list_files(dir):
|
|
||||||
files_and_content = {}
|
|
||||||
|
|
||||||
files = machine.succeed(f"""find {dir} -type f""").split("\n")[:-1]
|
|
||||||
|
|
||||||
for f in files:
|
|
||||||
content = machine.succeed(f"""cat {f}""").strip()
|
|
||||||
files_and_content[f] = content
|
|
||||||
|
|
||||||
return files_and_content
|
|
||||||
|
|
||||||
def assert_files(dir, files):
|
|
||||||
result = list(diff(list_files(dir), files))
|
|
||||||
if len(result) > 0:
|
|
||||||
raise Exception("Unexpected files:", result)
|
|
||||||
|
|
||||||
with subtest("Create initial content"):
|
|
||||||
for path in sourceDirectories:
|
|
||||||
machine.succeed(f"""
|
|
||||||
mkdir -p {path}
|
|
||||||
echo repo_fileA_1 > {path}/fileA
|
|
||||||
echo repo_fileB_1 > {path}/fileB
|
|
||||||
""")
|
|
||||||
|
|
||||||
for path in sourceDirectories:
|
|
||||||
assert_files(path, {
|
|
||||||
f'{path}/fileA': 'repo_fileA_1',
|
|
||||||
f'{path}/fileB': 'repo_fileB_1',
|
|
||||||
})
|
|
||||||
|
|
||||||
with subtest("Initial snapshot"):
|
|
||||||
out = machine.succeed("${provider.restoreScript} snapshots").splitlines()
|
|
||||||
if len(out) != 0:
|
|
||||||
raise Exception(f"Unexpected snapshots:\n{out}")
|
|
||||||
|
|
||||||
with subtest("First backup in repo"):
|
|
||||||
machine.succeed("systemctl start --wait ${provider.backupService}")
|
|
||||||
|
|
||||||
with subtest("One snapshot"):
|
|
||||||
out = machine.succeed("${provider.restoreScript} snapshots").splitlines()
|
|
||||||
print(f"Found snapshots:\n{out}")
|
|
||||||
if len(out) != 1:
|
|
||||||
raise Exception(f"Unexpected snapshots:\n{out}")
|
|
||||||
|
|
||||||
# To accomodate for snapshot orchestrators which keep only a given amount
|
|
||||||
# of snapshots per unit of time, we set the time to now + 2h.
|
|
||||||
new_date = (datetime.now() + timedelta(hours=2)).strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
machine.succeed(f"timedatectl set-time '{new_date}'")
|
|
||||||
|
|
||||||
with subtest("New content"):
|
|
||||||
for path in sourceDirectories:
|
|
||||||
machine.succeed(f"""
|
|
||||||
echo repo_fileA_2 > {path}/fileA
|
|
||||||
echo repo_fileB_2 > {path}/fileB
|
|
||||||
""")
|
|
||||||
|
|
||||||
assert_files(path, {
|
|
||||||
f'{path}/fileA': 'repo_fileA_2',
|
|
||||||
f'{path}/fileB': 'repo_fileB_2',
|
|
||||||
})
|
|
||||||
|
|
||||||
with subtest("Second backup in repo"):
|
|
||||||
machine.succeed("systemctl start --wait ${provider.backupService}")
|
|
||||||
|
|
||||||
with subtest("two snapshots"):
|
|
||||||
out = machine.succeed("${provider.restoreScript} snapshots").splitlines()
|
|
||||||
print(f"Found snapshots:\n{out}")
|
|
||||||
if len(out) != 2:
|
|
||||||
raise Exception(f"Unexpected snapshots:\n{out}")
|
|
||||||
|
|
||||||
firstSnapshot = re.split("[ \t+]", out[0], maxsplit=1)[0]
|
|
||||||
secondSnapshot = re.split("[ \t+]", out[1], maxsplit=1)[0]
|
|
||||||
print(f"First snapshot {firstSnapshot}")
|
|
||||||
print(f"Second snapshot {secondSnapshot}")
|
|
||||||
|
|
||||||
with subtest("Delete content"):
|
|
||||||
for path in sourceDirectories:
|
|
||||||
machine.succeed(f"""rm -r {path}/*""")
|
|
||||||
|
|
||||||
assert_files(path, {})
|
|
||||||
|
|
||||||
with subtest("Restore second backup"):
|
|
||||||
machine.succeed(f"${provider.restoreScript} restore {secondSnapshot}")
|
|
||||||
|
|
||||||
for path in sourceDirectories:
|
|
||||||
assert_files(path, {
|
|
||||||
f'{path}/fileA': 'repo_fileA_2',
|
|
||||||
f'{path}/fileB': 'repo_fileB_2',
|
|
||||||
})
|
|
||||||
|
|
||||||
with subtest("Restore first backup"):
|
|
||||||
machine.succeed(f"${provider.restoreScript} restore {firstSnapshot}")
|
|
||||||
|
|
||||||
for path in sourceDirectories:
|
|
||||||
assert_files(path, {
|
|
||||||
f'{path}/fileA': 'repo_fileA_1',
|
|
||||||
f'{path}/fileB': 'repo_fileB_1',
|
|
||||||
})
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
|
|
@ -48,31 +48,21 @@ let
|
||||||
importContract =
|
importContract =
|
||||||
module:
|
module:
|
||||||
let
|
let
|
||||||
importedModule = pkgs.callPackage module {
|
importedModule = pkgs.callPackage module { inherit shb; };
|
||||||
shb = shb // {
|
|
||||||
inherit contracts;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in
|
in
|
||||||
(mkContractFunctions {
|
mkContractFunctions {
|
||||||
inherit (importedModule) mkRequest mkResult;
|
inherit (importedModule) mkRequest mkResult;
|
||||||
})
|
|
||||||
// (importedModule.passthru or { });
|
|
||||||
|
|
||||||
contracts = {
|
|
||||||
databasebackup = importContract ./databasebackup.nix;
|
|
||||||
datasetbackup = importContract ./datasetbackup.nix;
|
|
||||||
dashboard = importContract ./dashboard.nix;
|
|
||||||
backup = importContract ./backup.nix;
|
|
||||||
mount = pkgs.callPackage ./mount.nix { };
|
|
||||||
secret = importContract ./secret.nix;
|
|
||||||
ssl = pkgs.callPackage ./ssl.nix { };
|
|
||||||
test = {
|
|
||||||
secret = pkgs.callPackage ./secret/test.nix { inherit shb; };
|
|
||||||
databasebackup = pkgs.callPackage ./databasebackup/test.nix { inherit shb; };
|
|
||||||
datasetbackup = pkgs.callPackage ./datasetbackup/test.nix { inherit shb; };
|
|
||||||
backup = pkgs.callPackage ./backup/test.nix { inherit shb; };
|
|
||||||
};
|
};
|
||||||
};
|
|
||||||
in
|
in
|
||||||
contracts
|
{
|
||||||
|
databasebackup = importContract ./databasebackup.nix;
|
||||||
|
backup = importContract ./backup.nix;
|
||||||
|
mount = pkgs.callPackage ./mount.nix { };
|
||||||
|
secret = importContract ./secret.nix;
|
||||||
|
ssl = pkgs.callPackage ./ssl.nix { };
|
||||||
|
test = {
|
||||||
|
secret = pkgs.callPackage ./secret/test.nix { inherit shb; };
|
||||||
|
databasebackup = pkgs.callPackage ./databasebackup/test.nix { inherit shb; };
|
||||||
|
backup = pkgs.callPackage ./backup/test.nix { inherit shb; };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,12 +42,12 @@ Now, with this contract, a layer on top of `sops` is added which is found under
|
||||||
The configuration then becomes:
|
The configuration then becomes:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
shb.sops.secret."ldap/user_password" = {
|
shb.sops.secrets."ldap/user_password" = {
|
||||||
request = config.shb.lldap.userPassword.request;
|
request = config.shb.lldap.userPassword.request;
|
||||||
settings.sopsFile = ./secrets.yaml;
|
settings.sopsFile = ./secrets.yaml;
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.lldap.userPassword.result = config.shb.sops.secret."ldap/user_password".result;
|
shb.lldap.userPassword.result = config.shb.sops.secrets."ldap/user_password".result;
|
||||||
```
|
```
|
||||||
|
|
||||||
The issue is now gone as the responsibility falls
|
The issue is now gone as the responsibility falls
|
||||||
|
|
@ -63,9 +63,9 @@ sops.defaultSopsFile = ./secrets.yaml;
|
||||||
Then the snippet above is even more simplified:
|
Then the snippet above is even more simplified:
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
shb.sops.secret."ldap/user_password".request = config.shb.lldap.userPassword.request;
|
shb.sops.secrets."ldap/user_password".request = config.shb.lldap.userPassword.request;
|
||||||
|
|
||||||
shb.lldap.userPassword.result = config.shb.sops.secret."ldap/user_password".result;
|
shb.lldap.userPassword.result = config.shb.sops.secrets."ldap/user_password".result;
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contract Reference {#contract-secret-options}
|
## Contract Reference {#contract-secret-options}
|
||||||
|
|
|
||||||
|
|
@ -144,10 +144,6 @@ let
|
||||||
description = "Log level.";
|
description = "Log level.";
|
||||||
default = "info";
|
default = "info";
|
||||||
};
|
};
|
||||||
ApiKey = lib.mkOption {
|
|
||||||
type = shb.secretFileType;
|
|
||||||
description = "Path to api key secret file.";
|
|
||||||
};
|
|
||||||
Port = lib.mkOption {
|
Port = lib.mkOption {
|
||||||
type = lib.types.port;
|
type = lib.types.port;
|
||||||
description = "Port on which bazarr listens to incoming requests.";
|
description = "Port on which bazarr listens to incoming requests.";
|
||||||
|
|
@ -176,10 +172,6 @@ let
|
||||||
description = "Log level.";
|
description = "Log level.";
|
||||||
default = "info";
|
default = "info";
|
||||||
};
|
};
|
||||||
ApiKey = lib.mkOption {
|
|
||||||
type = shb.secretFileType;
|
|
||||||
description = "Path to api key secret file.";
|
|
||||||
};
|
|
||||||
Port = lib.mkOption {
|
Port = lib.mkOption {
|
||||||
type = lib.types.port;
|
type = lib.types.port;
|
||||||
description = "Port on which readarr listens to incoming requests.";
|
description = "Port on which readarr listens to incoming requests.";
|
||||||
|
|
@ -207,10 +199,6 @@ let
|
||||||
description = "Log level.";
|
description = "Log level.";
|
||||||
default = "info";
|
default = "info";
|
||||||
};
|
};
|
||||||
ApiKey = lib.mkOption {
|
|
||||||
type = shb.secretFileType;
|
|
||||||
description = "Path to api key secret file.";
|
|
||||||
};
|
|
||||||
Port = lib.mkOption {
|
Port = lib.mkOption {
|
||||||
type = lib.types.port;
|
type = lib.types.port;
|
||||||
description = "Port on which lidarr listens to incoming requests.";
|
description = "Port on which lidarr listens to incoming requests.";
|
||||||
|
|
@ -317,7 +305,7 @@ let
|
||||||
{
|
{
|
||||||
domain = "${c.subdomain}.${c.domain}";
|
domain = "${c.subdomain}.${c.domain}";
|
||||||
policy = "two_factor";
|
policy = "two_factor";
|
||||||
subject = [ "group:${c.ldapUserGroup}" ];
|
subject = [ "group:arr_user" ];
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
@ -356,16 +344,6 @@ let
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
ldapUserGroup = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
LDAP group a user must belong to be able to login.
|
|
||||||
|
|
||||||
Note that all users are admins too.
|
|
||||||
'';
|
|
||||||
type = lib.types.str;
|
|
||||||
default = "arr_user";
|
|
||||||
};
|
|
||||||
|
|
||||||
authEndpoint = lib.mkOption {
|
authEndpoint = lib.mkOption {
|
||||||
type = lib.types.nullOr lib.types.str;
|
type = lib.types.nullOr lib.types.str;
|
||||||
default = null;
|
default = null;
|
||||||
|
|
@ -392,20 +370,6 @@ let
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
dashboard = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Dashboard contract consumer
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.dashboard.mkRequester {
|
|
||||||
externalUrl = "https://${cfg.${name}.subdomain}.${cfg.${name}.domain}";
|
|
||||||
externalUrlText = "https://\${config.shb.arr.${name}.subdomain}.\${config.shb.arr.${name}.domain}";
|
|
||||||
internalUrl = "http://127.0.0.1:${toString cfg.${name}.settings.Port}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
// (c.moreOptions or { });
|
// (c.moreOptions or { });
|
||||||
};
|
};
|
||||||
|
|
@ -416,7 +380,6 @@ in
|
||||||
imports = [
|
imports = [
|
||||||
../../lib/module.nix
|
../../lib/module.nix
|
||||||
../blocks/nginx.nix
|
../blocks/nginx.nix
|
||||||
../blocks/lldap.nix
|
|
||||||
];
|
];
|
||||||
|
|
||||||
options.shb.arr = lib.listToAttrs (lib.mapAttrsToList appOption apps);
|
options.shb.arr = lib.listToAttrs (lib.mapAttrsToList appOption apps);
|
||||||
|
|
@ -432,7 +395,7 @@ in
|
||||||
|
|
||||||
services.radarr = {
|
services.radarr = {
|
||||||
enable = true;
|
enable = true;
|
||||||
dataDir = cfg'.dataDir;
|
dataDir = "/var/lib/radarr";
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.radarr.preStart = shb.replaceSecrets {
|
systemd.services.radarr.preStart = shb.replaceSecrets {
|
||||||
|
|
@ -442,15 +405,11 @@ in
|
||||||
AuthenticationRequired = "DisabledForLocalAddresses";
|
AuthenticationRequired = "DisabledForLocalAddresses";
|
||||||
AuthenticationMethod = "External";
|
AuthenticationMethod = "External";
|
||||||
});
|
});
|
||||||
resultPath = "${cfg'.dataDir}/config.xml";
|
resultPath = "${config.services.radarr.dataDir}/config.xml";
|
||||||
generator = shb.replaceSecretsFormatAdapter apps.radarr.settingsFormat;
|
generator = shb.replaceSecretsFormatAdapter apps.radarr.settingsFormat;
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.nginx.vhosts = [ (vhosts { } cfg') ];
|
shb.nginx.vhosts = [ (vhosts { } cfg') ];
|
||||||
|
|
||||||
shb.lldap.ensureGroups = {
|
|
||||||
${cfg'.ldapUserGroup} = { };
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
@ -460,15 +419,11 @@ in
|
||||||
isSSOEnabled = !(isNull cfg'.authEndpoint);
|
isSSOEnabled = !(isNull cfg'.authEndpoint);
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
systemd.tmpfiles.rules = [
|
|
||||||
"d ${cfg'.dataDir} 0700 ${config.services.sonarr.user} ${config.services.sonarr.user}"
|
|
||||||
];
|
|
||||||
|
|
||||||
services.nginx.enable = true;
|
services.nginx.enable = true;
|
||||||
|
|
||||||
services.sonarr = {
|
services.sonarr = {
|
||||||
enable = true;
|
enable = true;
|
||||||
dataDir = cfg'.dataDir;
|
dataDir = "/var/lib/sonarr";
|
||||||
};
|
};
|
||||||
users.users.sonarr = {
|
users.users.sonarr = {
|
||||||
extraGroups = [ "media" ];
|
extraGroups = [ "media" ];
|
||||||
|
|
@ -481,15 +436,11 @@ in
|
||||||
AuthenticationRequired = "DisabledForLocalAddresses";
|
AuthenticationRequired = "DisabledForLocalAddresses";
|
||||||
AuthenticationMethod = "External";
|
AuthenticationMethod = "External";
|
||||||
});
|
});
|
||||||
resultPath = "${cfg'.dataDir}/config.xml";
|
resultPath = "${config.services.sonarr.dataDir}/config.xml";
|
||||||
generator = apps.sonarr.settingsFormat.generate;
|
generator = apps.sonarr.settingsFormat.generate;
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.nginx.vhosts = [ (vhosts { } cfg') ];
|
shb.nginx.vhosts = [ (vhosts { } cfg') ];
|
||||||
|
|
||||||
shb.lldap.ensureGroups = {
|
|
||||||
${cfg'.ldapUserGroup} = { };
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
@ -501,64 +452,45 @@ in
|
||||||
{
|
{
|
||||||
services.bazarr = {
|
services.bazarr = {
|
||||||
enable = true;
|
enable = true;
|
||||||
dataDir = cfg'.dataDir;
|
|
||||||
listenPort = cfg'.settings.Port;
|
listenPort = cfg'.settings.Port;
|
||||||
};
|
};
|
||||||
users.users.bazarr = {
|
users.users.bazarr = {
|
||||||
extraGroups = [ "media" ];
|
extraGroups = [ "media" ];
|
||||||
};
|
};
|
||||||
# This is actually not working. Bazarr uses a config file in dataDir/config/config.yaml
|
systemd.services.bazarr.preStart = shb.replaceSecrets {
|
||||||
# which includes all configuration so we must somehow merge our declarative config with it.
|
|
||||||
# It's doable but will take some time. Help is welcomed.
|
|
||||||
#
|
|
||||||
# systemd.services.bazarr.preStart = shb.replaceSecrets {
|
|
||||||
# userConfig =
|
|
||||||
# cfg'.settings
|
|
||||||
# // (lib.optionalAttrs isSSOEnabled {
|
|
||||||
# AuthenticationRequired = "DisabledForLocalAddresses";
|
|
||||||
# AuthenticationMethod = "External";
|
|
||||||
# });
|
|
||||||
# resultPath = "${cfg'.dataDir}/config.xml";
|
|
||||||
# generator = apps.bazarr.settingsFormat.generate;
|
|
||||||
# };
|
|
||||||
|
|
||||||
shb.nginx.vhosts = [ (vhosts { } cfg') ];
|
|
||||||
|
|
||||||
shb.lldap.ensureGroups = {
|
|
||||||
${cfg'.ldapUserGroup} = { };
|
|
||||||
};
|
|
||||||
}
|
|
||||||
))
|
|
||||||
|
|
||||||
(lib.mkIf cfg.readarr.enable (
|
|
||||||
let
|
|
||||||
cfg' = cfg.readarr;
|
|
||||||
isSSOEnabled = !(isNull cfg'.authEndpoint);
|
|
||||||
in
|
|
||||||
{
|
|
||||||
services.readarr = {
|
|
||||||
enable = true;
|
|
||||||
dataDir = cfg'.dataDir;
|
|
||||||
};
|
|
||||||
users.users.readarr = {
|
|
||||||
extraGroups = [ "media" ];
|
|
||||||
};
|
|
||||||
systemd.services.readarr.preStart = shb.replaceSecrets {
|
|
||||||
userConfig =
|
userConfig =
|
||||||
cfg'.settings
|
cfg'.settings
|
||||||
// (lib.optionalAttrs isSSOEnabled {
|
// (lib.optionalAttrs isSSOEnabled {
|
||||||
AuthenticationRequired = "DisabledForLocalAddresses";
|
AuthenticationRequired = "DisabledForLocalAddresses";
|
||||||
AuthenticationMethod = "External";
|
AuthenticationMethod = "External";
|
||||||
});
|
});
|
||||||
resultPath = "${cfg'.dataDir}/config.xml";
|
resultPath = "/var/lib/bazarr/config.xml";
|
||||||
|
generator = apps.bazarr.settingsFormat.generate;
|
||||||
|
};
|
||||||
|
|
||||||
|
shb.nginx.vhosts = [ (vhosts { } cfg') ];
|
||||||
|
}
|
||||||
|
))
|
||||||
|
|
||||||
|
(lib.mkIf cfg.readarr.enable (
|
||||||
|
let
|
||||||
|
cfg' = cfg.readarr;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
services.readarr = {
|
||||||
|
enable = true;
|
||||||
|
dataDir = "/var/lib/readarr";
|
||||||
|
};
|
||||||
|
users.users.readarr = {
|
||||||
|
extraGroups = [ "media" ];
|
||||||
|
};
|
||||||
|
systemd.services.readarr.preStart = shb.replaceSecrets {
|
||||||
|
userConfig = cfg'.settings;
|
||||||
|
resultPath = "${config.services.readarr.dataDir}/config.xml";
|
||||||
generator = apps.readarr.settingsFormat.generate;
|
generator = apps.readarr.settingsFormat.generate;
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.nginx.vhosts = [ (vhosts { } cfg') ];
|
shb.nginx.vhosts = [ (vhosts { } cfg') ];
|
||||||
|
|
||||||
shb.lldap.ensureGroups = {
|
|
||||||
${cfg'.ldapUserGroup} = { };
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
@ -570,7 +502,7 @@ in
|
||||||
{
|
{
|
||||||
services.lidarr = {
|
services.lidarr = {
|
||||||
enable = true;
|
enable = true;
|
||||||
dataDir = cfg'.dataDir;
|
dataDir = "/var/lib/lidarr";
|
||||||
};
|
};
|
||||||
users.users.lidarr = {
|
users.users.lidarr = {
|
||||||
extraGroups = [ "media" ];
|
extraGroups = [ "media" ];
|
||||||
|
|
@ -582,15 +514,11 @@ in
|
||||||
AuthenticationRequired = "DisabledForLocalAddresses";
|
AuthenticationRequired = "DisabledForLocalAddresses";
|
||||||
AuthenticationMethod = "External";
|
AuthenticationMethod = "External";
|
||||||
});
|
});
|
||||||
resultPath = "${cfg'.dataDir}/config.xml";
|
resultPath = "${config.services.lidarr.dataDir}/config.xml";
|
||||||
generator = apps.lidarr.settingsFormat.generate;
|
generator = apps.lidarr.settingsFormat.generate;
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.nginx.vhosts = [ (vhosts { } cfg') ];
|
shb.nginx.vhosts = [ (vhosts { } cfg') ];
|
||||||
|
|
||||||
shb.lldap.ensureGroups = {
|
|
||||||
${cfg'.ldapUserGroup} = { };
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
@ -601,7 +529,7 @@ in
|
||||||
{
|
{
|
||||||
services.jackett = {
|
services.jackett = {
|
||||||
enable = true;
|
enable = true;
|
||||||
dataDir = cfg'.dataDir;
|
dataDir = "/var/lib/jackett";
|
||||||
};
|
};
|
||||||
# TODO: avoid implicitly relying on the media group
|
# TODO: avoid implicitly relying on the media group
|
||||||
users.users.jackett = {
|
users.users.jackett = {
|
||||||
|
|
@ -609,7 +537,7 @@ in
|
||||||
};
|
};
|
||||||
systemd.services.jackett.preStart = shb.replaceSecrets {
|
systemd.services.jackett.preStart = shb.replaceSecrets {
|
||||||
userConfig = shb.renameAttrName cfg'.settings "ApiKey" "APIKey";
|
userConfig = shb.renameAttrName cfg'.settings "ApiKey" "APIKey";
|
||||||
resultPath = "${cfg'.dataDir}/ServerConfig.json";
|
resultPath = "${config.services.jackett.dataDir}/ServerConfig.json";
|
||||||
generator = apps.jackett.settingsFormat.generate;
|
generator = apps.jackett.settingsFormat.generate;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -618,10 +546,6 @@ in
|
||||||
extraBypassResources = [ "^/dl.*" ];
|
extraBypassResources = [ "^/dl.*" ];
|
||||||
} cfg')
|
} cfg')
|
||||||
];
|
];
|
||||||
|
|
||||||
shb.lldap.ensureGroups = {
|
|
||||||
${cfg'.ldapUserGroup} = { };
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -3,242 +3,12 @@
|
||||||
Defined in [`/modules/services/arr.nix`](@REPO@/modules/services/arr.nix).
|
Defined in [`/modules/services/arr.nix`](@REPO@/modules/services/arr.nix).
|
||||||
|
|
||||||
This NixOS module sets up multiple [Servarr](https://wiki.servarr.com/) services.
|
This NixOS module sets up multiple [Servarr](https://wiki.servarr.com/) services.
|
||||||
## Features {#services-arr-features}
|
|
||||||
|
|
||||||
Compared to the stock module from nixpkgs,
|
Compared to the stock module from nixpkgs,
|
||||||
this one sets up, in a fully declarative manner
|
this one sets up, in a fully declarative manner
|
||||||
LDAP and SSO integration as well as the API key.
|
LDAP and SSO integration as well as the API key.
|
||||||
|
|
||||||
## Usage {#services-arr-usage}
|
This manual page is under construction.
|
||||||
|
|
||||||
### Initial Configuration {#services-arr-usage-configuration}
|
|
||||||
|
|
||||||
The following snippet assumes a few blocks have been setup already:
|
|
||||||
|
|
||||||
- the [secrets block](usage.html#usage-secrets) with SOPS,
|
|
||||||
- the [`shb.ssl` block](blocks-ssl.html#usage),
|
|
||||||
- the [`shb.lldap` block](blocks-lldap.html#blocks-lldap-global-setup).
|
|
||||||
- the [`shb.authelia` block](blocks-authelia.html#blocks-sso-global-setup).
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.certs.certs.letsencrypt.${domain}.extraDomains = [
|
|
||||||
"moviesdl.${domain}"
|
|
||||||
"seriesdl.${domain}"
|
|
||||||
"subtitlesdl.${domain}"
|
|
||||||
"booksdl.${domain}"
|
|
||||||
"musicdl.${domain}"
|
|
||||||
"indexer.${domain}"
|
|
||||||
];
|
|
||||||
|
|
||||||
shb.arr = {
|
|
||||||
radarr = {
|
|
||||||
inherit domain;
|
|
||||||
enable = true;
|
|
||||||
ssl = config.shb.certs.certs.letsencrypt.${domain};
|
|
||||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
|
||||||
};
|
|
||||||
sonarr = {
|
|
||||||
inherit domain;
|
|
||||||
enable = true;
|
|
||||||
ssl = config.shb.certs.certs.letsencrypt."${domain}";
|
|
||||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
|
||||||
};
|
|
||||||
bazarr = {
|
|
||||||
inherit domain;
|
|
||||||
enable = true;
|
|
||||||
ssl = config.shb.certs.certs.letsencrypt."${domain}";
|
|
||||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
|
||||||
};
|
|
||||||
readarr = {
|
|
||||||
inherit domain;
|
|
||||||
enable = true;
|
|
||||||
ssl = config.shb.certs.certs.letsencrypt."${domain}";
|
|
||||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
|
||||||
};
|
|
||||||
lidarr = {
|
|
||||||
inherit domain;
|
|
||||||
enable = true;
|
|
||||||
ssl = config.shb.certs.certs.letsencrypt."${domain}";
|
|
||||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
|
||||||
};
|
|
||||||
jackett = {
|
|
||||||
inherit domain;
|
|
||||||
enable = true;
|
|
||||||
ssl = config.shb.certs.certs.letsencrypt."${domain}";
|
|
||||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The user and admin LDAP groups are created automatically.
|
|
||||||
|
|
||||||
### API Keys {#services-arr-usage-apikeys}
|
|
||||||
|
|
||||||
The API keys for each arr service can be created declaratively.
|
|
||||||
|
|
||||||
First, generate one secret for each service with `nix run nixpkgs#openssl -- rand -hex 64`
|
|
||||||
and store it in your secrets file (for example the SOPS file).
|
|
||||||
|
|
||||||
Then, add the API key to each service:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.arr = {
|
|
||||||
radarr = {
|
|
||||||
settings = {
|
|
||||||
ApiKey.source = config.shb.sops.secret."radarr/apikey".result.path;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
sonarr = {
|
|
||||||
settings = {
|
|
||||||
ApiKey.source = config.shb.sops.secret."sonarr/apikey".result.path;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
bazarr = {
|
|
||||||
settings = {
|
|
||||||
ApiKey.source = config.shb.sops.secret."bazarr/apikey".result.path;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
readarr = {
|
|
||||||
settings = {
|
|
||||||
ApiKey.source = config.shb.sops.secret."readarr/apikey".result.path;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
lidarr = {
|
|
||||||
settings = {
|
|
||||||
ApiKey.source = config.shb.sops.secret."lidarr/apikey".result.path;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
jackett = {
|
|
||||||
settings = {
|
|
||||||
ApiKey.source = config.shb.sops.secret."jackett/apikey".result.path;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
shb.sops.secret."radarr/apikey".request = {
|
|
||||||
mode = "0440";
|
|
||||||
owner = "radarr";
|
|
||||||
group = "radarr";
|
|
||||||
restartUnits = [ "radarr.service" ];
|
|
||||||
};
|
|
||||||
shb.sops.secret."sonarr/apikey".request = {
|
|
||||||
mode = "0440";
|
|
||||||
owner = "sonarr";
|
|
||||||
group = "sonarr";
|
|
||||||
restartUnits = [ "sonarr.service" ];
|
|
||||||
};
|
|
||||||
shb.sops.secret."bazarr/apikey".request = {
|
|
||||||
mode = "0440";
|
|
||||||
owner = "bazarr";
|
|
||||||
group = "bazarr";
|
|
||||||
restartUnits = [ "bazarr.service" ];
|
|
||||||
};
|
|
||||||
shb.sops.secret."readarr/apikey".request = {
|
|
||||||
mode = "0440";
|
|
||||||
owner = "readarr";
|
|
||||||
group = "readarr";
|
|
||||||
restartUnits = [ "readarr.service" ];
|
|
||||||
};
|
|
||||||
shb.sops.secret."lidarr/apikey".request = {
|
|
||||||
mode = "0440";
|
|
||||||
owner = "lidarr";
|
|
||||||
group = "lidarr";
|
|
||||||
restartUnits = [ "lidarr.service" ];
|
|
||||||
};
|
|
||||||
shb.sops.secret."jackett/apikey".request = {
|
|
||||||
mode = "0440";
|
|
||||||
owner = "jackett";
|
|
||||||
group = "jackett";
|
|
||||||
restartUnits = [ "jackett.service" ];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Application Dashboard {#services-arr-usage-applicationdashboard}
|
|
||||||
|
|
||||||
Integration with the [dashboard contract](contracts-dashboard.html) is provided
|
|
||||||
by the various dashboard options.
|
|
||||||
|
|
||||||
For example using the [Homepage](services-homepage.html) service:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.homepage.servicesGroups.Media.services.Radarr = {
|
|
||||||
sortOrder = 10;
|
|
||||||
dashboard.request = config.shb.arr.radarr.dashboard.request;
|
|
||||||
apiKey.result = config.shb.sops.secret."radarr/homepageApiKey".result;
|
|
||||||
};
|
|
||||||
shb.sops.secret."radarr/homepageApiKey" = {
|
|
||||||
settings.key = "radarr/apikey";
|
|
||||||
request = config.shb.homepage.servicesGroups.Media.services.Radarr.apiKey.request;
|
|
||||||
};
|
|
||||||
shb.homepage.servicesGroups.Media.services.Sonarr = {
|
|
||||||
sortOrder = 11;
|
|
||||||
dashboard.request = config.shb.arr.sonarr.dashboard.request;
|
|
||||||
apiKey.result = config.shb.sops.secret."sonarr/homepageApiKey".result;
|
|
||||||
};
|
|
||||||
shb.sops.secret."sonarr/homepageApiKey" = {
|
|
||||||
settings.key = "sonarr/apikey";
|
|
||||||
request = config.shb.homepage.servicesGroups.Media.services.Sonarr.apiKey.request;
|
|
||||||
};
|
|
||||||
shb.homepage.servicesGroups.Media.services.Bazarr = {
|
|
||||||
sortOrder = 12;
|
|
||||||
dashboard.request = config.shb.arr.bazarr.dashboard.request;
|
|
||||||
apiKey.result = config.shb.sops.secret."bazarr/homepageApiKey".result;
|
|
||||||
};
|
|
||||||
shb.sops.secret."bazarr/homepageApiKey" = {
|
|
||||||
settings.key = "bazarr/apikey";
|
|
||||||
request = config.shb.homepage.servicesGroups.Media.services.Bazarr.apiKey.request;
|
|
||||||
};
|
|
||||||
shb.homepage.servicesGroups.Media.services.Readarr = {
|
|
||||||
sortOrder = 13;
|
|
||||||
dashboard.request = config.shb.arr.readarr.dashboard.request;
|
|
||||||
apiKey.result = config.shb.sops.secret."readarr/homepageApiKey".result;
|
|
||||||
};
|
|
||||||
shb.sops.secret."readarr/homepageApiKey" = {
|
|
||||||
settings.key = "readarr/apikey";
|
|
||||||
request = config.shb.homepage.servicesGroups.Media.services.Readarr.apiKey.request;
|
|
||||||
};
|
|
||||||
shb.homepage.servicesGroups.Media.services.Lidarr = {
|
|
||||||
sortOrder = 14;
|
|
||||||
dashboard.request = config.shb.arr.lidarr.dashboard.request;
|
|
||||||
apiKey.result = config.shb.sops.secret."lidarr/homepageApiKey".result;
|
|
||||||
};
|
|
||||||
shb.sops.secret."lidarr/homepageApiKey" = {
|
|
||||||
settings.key = "lidarr/apikey";
|
|
||||||
request = config.shb.homepage.servicesGroups.Media.services.Lidarr.apiKey.request;
|
|
||||||
};
|
|
||||||
shb.homepage.servicesGroups.Media.services.Jackett = {
|
|
||||||
sortOrder = 15;
|
|
||||||
dashboard.request = config.shb.arr.jackett.dashboard.request;
|
|
||||||
apiKey.result = config.shb.sops.secret."jackett/homepageApiKey".result;
|
|
||||||
};
|
|
||||||
shb.sops.secret."jackett/homepageApiKey" = {
|
|
||||||
settings.key = "jackett/apikey";
|
|
||||||
request = config.shb.homepage.servicesGroups.Media.services.Jackett.apiKey.request;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This example reuses the API keys generated declaratively from the previous section.
|
|
||||||
|
|
||||||
### Jackett Proxy {#services-arr-usage-jackett-proxy}
|
|
||||||
|
|
||||||
The Jackett service can be made to use a proxy with:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.arr.jackett = {
|
|
||||||
settings = {
|
|
||||||
ProxyType = "0";
|
|
||||||
ProxyUrl = "127.0.0.1:1234";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## Options Reference {#services-arr-options}
|
## Options Reference {#services-arr-options}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -152,20 +152,6 @@ in
|
||||||
default = false;
|
default = false;
|
||||||
example = true;
|
example = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
dashboard = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Dashboard contract consumer
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.dashboard.mkRequester {
|
|
||||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
|
||||||
externalUrlText = "https://\${config.shb.audiobookshelf.subdomain}.\${config.shb.audiobookshelf.domain}";
|
|
||||||
internalUrl = "http://127.0.0.1:${toString cfg.webPort}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable (
|
config = lib.mkIf cfg.enable (
|
||||||
|
|
|
||||||
|
|
@ -30,15 +30,10 @@ in
|
||||||
imports = [
|
imports = [
|
||||||
../../lib/module.nix
|
../../lib/module.nix
|
||||||
../blocks/nginx.nix
|
../blocks/nginx.nix
|
||||||
../blocks/monitoring.nix
|
|
||||||
];
|
];
|
||||||
|
|
||||||
options.shb.deluge = {
|
options.shb.deluge = {
|
||||||
enable = lib.mkEnableOption "the SHB Deluge service";
|
enable = lib.mkEnableOption "selfhostblocks.deluge";
|
||||||
|
|
||||||
enableDashboard = lib.mkEnableOption "the Torrents SHB monitoring dashboard" // {
|
|
||||||
default = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
subdomain = lib.mkOption {
|
subdomain = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
|
|
@ -301,20 +296,6 @@ in
|
||||||
default = null;
|
default = null;
|
||||||
example = "info";
|
example = "info";
|
||||||
};
|
};
|
||||||
|
|
||||||
dashboard = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Dashboard contract consumer
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.dashboard.mkRequester {
|
|
||||||
externalUrl = "https://${fqdn}";
|
|
||||||
externalUrlText = "https://\${config.shb.deluge.subdomain}.\${config.shb.deluge.domain}";
|
|
||||||
internalUrl = "http://127.0.0.1:${toString cfg.webPort}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable (
|
config = lib.mkIf cfg.enable (
|
||||||
|
|
@ -471,12 +452,6 @@ in
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
|
|
||||||
(lib.mkIf (cfg.enable && cfg.enableDashboard) {
|
|
||||||
shb.monitoring.dashboards = [
|
|
||||||
./deluge/dashboard/Torrents.json
|
|
||||||
];
|
|
||||||
})
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,457 +0,0 @@
|
||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
shb,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
|
|
||||||
let
|
|
||||||
cfg = config.shb.firefly-iii;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
../blocks/nginx.nix
|
|
||||||
../blocks/lldap.nix
|
|
||||||
|
|
||||||
../../lib/module.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
options.shb.firefly-iii = {
|
|
||||||
enable = lib.mkEnableOption "SHB's firefly-iii module";
|
|
||||||
|
|
||||||
subdomain = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = ''
|
|
||||||
Subdomain under which firefly-iii will be served.
|
|
||||||
|
|
||||||
```
|
|
||||||
<subdomain>.<domain>
|
|
||||||
```
|
|
||||||
'';
|
|
||||||
example = "firefly-iii";
|
|
||||||
};
|
|
||||||
|
|
||||||
domain = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Domain under which firefly-iii is served.
|
|
||||||
|
|
||||||
```
|
|
||||||
<subdomain>.<domain>[:<port>]
|
|
||||||
```
|
|
||||||
'';
|
|
||||||
type = lib.types.str;
|
|
||||||
example = "domain.com";
|
|
||||||
};
|
|
||||||
|
|
||||||
ssl = lib.mkOption {
|
|
||||||
description = "Path to SSL files";
|
|
||||||
type = lib.types.nullOr shb.contracts.ssl.certs;
|
|
||||||
default = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
siteOwnerEmail = lib.mkOption {
|
|
||||||
description = "Email of the site owner.";
|
|
||||||
type = lib.types.str;
|
|
||||||
example = "mail@example.com";
|
|
||||||
};
|
|
||||||
|
|
||||||
impermanence = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Path to save when using impermanence setup.
|
|
||||||
'';
|
|
||||||
type = lib.types.str;
|
|
||||||
default = config.services.firefly-iii.dataDir;
|
|
||||||
defaultText = "services.firefly-iii.dataDir";
|
|
||||||
};
|
|
||||||
|
|
||||||
backup = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Backup configuration.
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.backup.mkRequester {
|
|
||||||
user = config.services.firefly-iii.user;
|
|
||||||
userText = "services.firefly-iii.user";
|
|
||||||
sourceDirectories = [
|
|
||||||
config.services.firefly-iii.dataDir
|
|
||||||
];
|
|
||||||
sourceDirectoriesText = ''
|
|
||||||
[
|
|
||||||
config.services.firefly-iii.dataDir
|
|
||||||
]
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
appKey = lib.mkOption {
|
|
||||||
description = "Encryption key used for sessions. Must be 32 characters long exactly.";
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.secret.mkRequester {
|
|
||||||
mode = "0400";
|
|
||||||
owner = config.services.firefly-iii.user;
|
|
||||||
ownerText = "services.firefly-iii.user";
|
|
||||||
restartUnits = [ "firefly-iii-setup.service" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
dbPassword = lib.mkOption {
|
|
||||||
description = "DB password.";
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.secret.mkRequester {
|
|
||||||
mode = "0440";
|
|
||||||
owner = config.services.firefly-iii.user;
|
|
||||||
ownerText = "services.firefly-iii.user";
|
|
||||||
group = "postgres";
|
|
||||||
restartUnits = [
|
|
||||||
"postgresql.service"
|
|
||||||
"firefly-iii-setup.service"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
ldap = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
LDAP Integration
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
userGroup = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "Group users must belong to to be able to login to Firefly-iii.";
|
|
||||||
default = "firefly-iii_user";
|
|
||||||
};
|
|
||||||
adminGroup = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "Group users must belong to to be able to import data user the Firefly-iii data importer.";
|
|
||||||
default = "firefly-iii_admin";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
sso = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
SSO Integration
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
enable = lib.mkEnableOption "SSO integration.";
|
|
||||||
|
|
||||||
authEndpoint = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "OIDC endpoint for SSO.";
|
|
||||||
example = "https://authelia.example.com";
|
|
||||||
};
|
|
||||||
|
|
||||||
port = lib.mkOption {
|
|
||||||
description = "If given, adds a port to the endpoint.";
|
|
||||||
type = lib.types.nullOr lib.types.port;
|
|
||||||
default = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
provider = lib.mkOption {
|
|
||||||
type = lib.types.enum [ "Authelia" ];
|
|
||||||
description = "OIDC provider name, used for display.";
|
|
||||||
default = "Authelia";
|
|
||||||
};
|
|
||||||
|
|
||||||
clientID = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "Client ID for the OIDC endpoint.";
|
|
||||||
default = "firefly-iii";
|
|
||||||
};
|
|
||||||
|
|
||||||
authorization_policy = lib.mkOption {
|
|
||||||
type = lib.types.enum [
|
|
||||||
"one_factor"
|
|
||||||
"two_factor"
|
|
||||||
];
|
|
||||||
description = "Require one factor (password) or two factor (device) authentication.";
|
|
||||||
default = "one_factor";
|
|
||||||
};
|
|
||||||
|
|
||||||
adminGroup = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "Group admins must belong to to be able to login to Firefly-iii.";
|
|
||||||
default = "firefly-iii_admin";
|
|
||||||
};
|
|
||||||
|
|
||||||
secret = lib.mkOption {
|
|
||||||
description = "OIDC shared secret.";
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.secret.mkRequester {
|
|
||||||
mode = "0400";
|
|
||||||
owner = "firefly-iii";
|
|
||||||
restartUnits = [ "firefly-iii-setup.service" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
secretForAuthelia = lib.mkOption {
|
|
||||||
description = "OIDC shared secret. Content must be the same as `secretFile` option.";
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.secret.mkRequester {
|
|
||||||
mode = "0400";
|
|
||||||
owner = "authelia";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
smtp = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
If set, send notifications through smtp.
|
|
||||||
|
|
||||||
https://docs.firefly-iii.org/how-to/firefly-iii/advanced/notifications/
|
|
||||||
'';
|
|
||||||
default = null;
|
|
||||||
type = lib.types.nullOr (
|
|
||||||
lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
from_address = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "SMTP address from which the emails originate.";
|
|
||||||
example = "authelia@mydomain.com";
|
|
||||||
};
|
|
||||||
host = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "SMTP host to send the emails to.";
|
|
||||||
};
|
|
||||||
port = lib.mkOption {
|
|
||||||
type = lib.types.port;
|
|
||||||
description = "SMTP port to send the emails to.";
|
|
||||||
default = 25;
|
|
||||||
};
|
|
||||||
username = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "Username to connect to the SMTP host.";
|
|
||||||
};
|
|
||||||
password = lib.mkOption {
|
|
||||||
description = "File containing the password to connect to the SMTP host.";
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.secret.mkRequester {
|
|
||||||
mode = "0400";
|
|
||||||
owner = config.services.firefly-iii.user;
|
|
||||||
ownerText = "services.firefly-iii.user";
|
|
||||||
restartUnits = [ "firefly-iii-setup.service" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
debug = lib.mkOption {
|
|
||||||
type = lib.types.bool;
|
|
||||||
description = "Enable more verbose logging.";
|
|
||||||
default = false;
|
|
||||||
example = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
importer = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Configuration for Firefly-iii data importer.
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = {
|
|
||||||
enable = lib.mkEnableOption "Firefly-iii Data Importer." // {
|
|
||||||
default = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
subdomain = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = ''
|
|
||||||
Subdomain under which the firefly-iii data importer will be served.
|
|
||||||
'';
|
|
||||||
default = "${cfg.subdomain}-importer";
|
|
||||||
defaultText = lib.literalExpression "\${shb.firefly-iii.subdomain}-importer";
|
|
||||||
};
|
|
||||||
|
|
||||||
firefly-iii-accessToken = lib.mkOption {
|
|
||||||
type = lib.types.nullOr (
|
|
||||||
lib.types.submodule {
|
|
||||||
options = shb.contracts.secret.mkRequester {
|
|
||||||
mode = "0400";
|
|
||||||
owner = config.services.firefly-iii-data-importer.user;
|
|
||||||
ownerText = "services.firefly-iii-data-importer.user";
|
|
||||||
restartUnits = [ "firefly-iii-data-importer-setup.service" ];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
description = ''
|
|
||||||
Create a Personal Access Token then set then token in this option.
|
|
||||||
'';
|
|
||||||
default = null;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
dashboard = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Dashboard contract consumer
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.dashboard.mkRequester {
|
|
||||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
|
||||||
externalUrlText = "https://\${config.shb.firefly-iii.subdomain}.\${config.shb.firefly-iii.domain}";
|
|
||||||
# This works thanks to the Personal Access Token.
|
|
||||||
internalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
|
||||||
internalUrlText = "https://\${config.shb.firefly-iii.subdomain}.\${config.shb.firefly-iii.domain}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable (
|
|
||||||
lib.mkMerge [
|
|
||||||
{
|
|
||||||
services.firefly-iii = {
|
|
||||||
enable = true;
|
|
||||||
group = "nginx";
|
|
||||||
|
|
||||||
virtualHost = "${cfg.subdomain}.${cfg.domain}";
|
|
||||||
|
|
||||||
# https://github.com/firefly-iii/firefly-iii/blob/main/.env.example
|
|
||||||
settings = {
|
|
||||||
APP_ENV = "production";
|
|
||||||
APP_URL = "https://${cfg.subdomain}.${cfg.domain}";
|
|
||||||
|
|
||||||
APP_DEBUG = cfg.debug;
|
|
||||||
APP_LOG_LEVEL = if cfg.debug then "debug" else "notice";
|
|
||||||
LOG_CHANNEL = "stdout";
|
|
||||||
|
|
||||||
APP_KEY_FILE = cfg.appKey.result.path;
|
|
||||||
SITE_OWNER = cfg.siteOwnerEmail;
|
|
||||||
DB_CONNECTION = "pgsql";
|
|
||||||
DB_HOST = "localhost";
|
|
||||||
DB_PORT = config.services.postgresql.settings.port;
|
|
||||||
DB_DATABASE = "firefly-iii";
|
|
||||||
DB_USERNAME = "firefly-iii";
|
|
||||||
DB_PASSWORD_FILE = cfg.dbPassword.result.path;
|
|
||||||
|
|
||||||
# MAP_DEFAULT_LAT = "51.983333";
|
|
||||||
# MAP_DEFAULT_LONG = "5.916667";
|
|
||||||
# MAP_DEFAULT_ZOOM = "6";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
shb.postgresql.enableTCPIP = true;
|
|
||||||
shb.postgresql.ensures = [
|
|
||||||
{
|
|
||||||
username = "firefly-iii";
|
|
||||||
database = "firefly-iii";
|
|
||||||
passwordFile = cfg.dbPassword.result.path;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
# This should be using a contract instead of setting the option directly.
|
|
||||||
shb.lldap = lib.mkIf config.shb.lldap.enable {
|
|
||||||
ensureGroups = {
|
|
||||||
${cfg.ldap.userGroup} = { };
|
|
||||||
${cfg.ldap.adminGroup} = { };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# We enable the firefly-iii nginx integration and merge it with SHB's nginx configuration.
|
|
||||||
services.firefly-iii.enableNginx = true;
|
|
||||||
shb.nginx.vhosts = [
|
|
||||||
{
|
|
||||||
inherit (cfg) subdomain domain ssl;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
(lib.mkIf cfg.importer.enable {
|
|
||||||
services.firefly-iii-data-importer = {
|
|
||||||
enable = true;
|
|
||||||
|
|
||||||
virtualHost = "${cfg.importer.subdomain}.${cfg.domain}";
|
|
||||||
|
|
||||||
settings = {
|
|
||||||
FIREFLY_III_URL = "https://${config.services.firefly-iii.virtualHost}";
|
|
||||||
}
|
|
||||||
// lib.optionalAttrs (cfg.importer.firefly-iii-accessToken != null) {
|
|
||||||
FIREFLY_III_ACCESS_TOKEN_FILE = cfg.importer.firefly-iii-accessToken.result.path;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# We enable the firefly-iii-data-importer nginx integration and merge it with SHB's nginx configuration.
|
|
||||||
services.firefly-iii-data-importer.enableNginx = true;
|
|
||||||
shb.nginx.vhosts = [
|
|
||||||
{
|
|
||||||
inherit (cfg) domain ssl;
|
|
||||||
subdomain = cfg.importer.subdomain;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
})
|
|
||||||
(lib.mkIf (cfg.smtp != null) {
|
|
||||||
services.firefly-iii.settings = {
|
|
||||||
MAIL_MAILER = "smtp";
|
|
||||||
MAIL_HOST = cfg.smtp.host;
|
|
||||||
MAIL_PORT = cfg.smtp.port;
|
|
||||||
MAIL_FROM = cfg.smtp.from_address;
|
|
||||||
MAIL_USERNAME = cfg.smtp.username;
|
|
||||||
MAIL_PASSWORD_FILE = cfg.smtp.password.result.path;
|
|
||||||
MAIL_ENCRYPTION = "tls";
|
|
||||||
};
|
|
||||||
})
|
|
||||||
(lib.mkIf cfg.sso.enable {
|
|
||||||
services.firefly-iii.settings = {
|
|
||||||
AUTHENTICATION_GUARD = "remote_user_guard";
|
|
||||||
AUTHENTICATION_GUARD_HEADER = "HTTP_X_FORWARDED_USER";
|
|
||||||
};
|
|
||||||
|
|
||||||
shb.nginx.vhosts = [
|
|
||||||
{
|
|
||||||
inherit (cfg) subdomain domain ssl;
|
|
||||||
inherit (cfg.sso) authEndpoint;
|
|
||||||
|
|
||||||
phpForwardAuth = true;
|
|
||||||
autheliaRules = [
|
|
||||||
{
|
|
||||||
domain = "${cfg.subdomain}.${cfg.domain}";
|
|
||||||
policy = "bypass";
|
|
||||||
resources = [ "^/api" ];
|
|
||||||
}
|
|
||||||
{
|
|
||||||
domain = "${cfg.subdomain}.${cfg.domain}";
|
|
||||||
policy = cfg.sso.authorization_policy;
|
|
||||||
subject = [
|
|
||||||
"group:${cfg.ldap.userGroup}"
|
|
||||||
"group:${cfg.ldap.adminGroup}"
|
|
||||||
];
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
];
|
|
||||||
})
|
|
||||||
(lib.mkIf (cfg.sso.enable && cfg.importer.enable) {
|
|
||||||
shb.nginx.vhosts = [
|
|
||||||
{
|
|
||||||
inherit (cfg.importer) subdomain;
|
|
||||||
inherit (cfg) domain ssl;
|
|
||||||
inherit (cfg.sso) authEndpoint;
|
|
||||||
|
|
||||||
autheliaRules = [
|
|
||||||
{
|
|
||||||
domain = "${cfg.importer.subdomain}.${cfg.domain}";
|
|
||||||
policy = cfg.sso.authorization_policy;
|
|
||||||
subject = [ "group:${cfg.ldap.adminGroup}" ];
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
];
|
|
||||||
})
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,193 +0,0 @@
|
||||||
# Firefly-iii Service {#services-firefly-iii}
|
|
||||||
|
|
||||||
Defined in [`/modules/services/firefly-iii.nix`](@REPO@/modules/services/firefly-iii.nix).
|
|
||||||
|
|
||||||
This NixOS module is a service that sets up a [Firefly-iii](https://www.firefly-iii.org/) instance.
|
|
||||||
|
|
||||||
Compared to the stock module from nixpkgs,
|
|
||||||
this one sets up, in a fully declarative manner,
|
|
||||||
LDAP and SSO integration
|
|
||||||
and has a nicer option for secrets.
|
|
||||||
It also sets up the Firefly-iii data importer service
|
|
||||||
and nearly automatically links it to the Firefly-iii instance using a Personal Account Token.
|
|
||||||
Instructions on how to do so is given in the next section.
|
|
||||||
|
|
||||||
## Features {#services-firefly-iii-features}
|
|
||||||
|
|
||||||
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-firefly-iii-usage-applicationdashboard)
|
|
||||||
|
|
||||||
## Usage {#services-firefly-iii-usage}
|
|
||||||
|
|
||||||
### Initial Configuration {#services-firefly-iii-usage-configuration}
|
|
||||||
|
|
||||||
The following snippet assumes a few blocks have been setup already:
|
|
||||||
|
|
||||||
- the [secrets block](usage.html#usage-secrets) with SOPS,
|
|
||||||
- the [`shb.ssl` block](blocks-ssl.html#usage),
|
|
||||||
- the [`shb.lldap` block](blocks-lldap.html#blocks-lldap-global-setup).
|
|
||||||
- the [`shb.authelia` block](blocks-authelia.html#blocks-sso-global-setup).
|
|
||||||
|
|
||||||
```nix
|
|
||||||
shb.firefly-iii = {
|
|
||||||
enable = true;
|
|
||||||
debug = false;
|
|
||||||
|
|
||||||
appKey.result = config.shb.sops.secret."firefly-iii/appKey".result;
|
|
||||||
dbPassword.result = config.shb.sops.secret."firefly-iii/dbPassword".result;
|
|
||||||
|
|
||||||
domain = "example.com";
|
|
||||||
subdomain = "firefly-iii";
|
|
||||||
siteOwnerEmail = "mail@example.com";
|
|
||||||
ssl = config.shb.certs.certs.letsencrypt.${domain};
|
|
||||||
|
|
||||||
smtp = {
|
|
||||||
host = "smtp.eu.mailgun.org";
|
|
||||||
port = 587;
|
|
||||||
username = "postmaster@mg.example.com";
|
|
||||||
from_address = "firefly-iii@example.com";
|
|
||||||
password.result = config.shb.sops.secret."firefly-iii/smtpPassword".result;
|
|
||||||
};
|
|
||||||
|
|
||||||
sso = {
|
|
||||||
enable = true;
|
|
||||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
|
||||||
};
|
|
||||||
|
|
||||||
importer = {
|
|
||||||
# See note hereunder.
|
|
||||||
# firefly-iii-accessToken.result = config.shb.sops.secret."firefly-iii/importerAccessToken".result;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
shb.sops.secret."firefly-iii/appKey".request = config.shb.firefly-iii.appKey.request;
|
|
||||||
shb.sops.secret."firefly-iii/dbPassword".request = config.shb.firefly-iii.dbPassword.request;
|
|
||||||
shb.sops.secret."firefly-iii/smtpPassword".request = config.shb.firefly-iii.smtp.password.request;
|
|
||||||
# See not hereunder.
|
|
||||||
# shb.sops.secret."firefly-iii/importerAccessToken".request = config.shb.firefly-iii.importer.firefly-iii-accessToken.request;
|
|
||||||
```
|
|
||||||
|
|
||||||
Secrets can be randomly generated with `nix run nixpkgs#openssl -- rand -hex 64`.
|
|
||||||
Note that for `appKey`, the secret length must be exactly 32 characters.
|
|
||||||
|
|
||||||
The [user](#services-firefly-iii-options-shb.firefly-iii.ldap.userGroup)
|
|
||||||
and [admin](#services-firefly-iii-options-shb.firefly-iii.ldap.adminGroup)
|
|
||||||
LDAP groups are created automatically.
|
|
||||||
Only admin users have access to the Firefly-iii data importer.
|
|
||||||
On the Firefly-iii web UI, the first user to login will be the admin.
|
|
||||||
We cannot yet create multiple admins in the Firefly-iii web UI.
|
|
||||||
|
|
||||||
On first start, leave the `shb.firefly-iii.importer.firefly-iii-accessToken` option empty.
|
|
||||||
To fill it out and connect the data importer to the Firefly-iii instance,
|
|
||||||
you must first create a personal access token then fill that option and redeploy.
|
|
||||||
|
|
||||||
### Backup {#services-firefly-iii-usage-backup}
|
|
||||||
|
|
||||||
Backing up Firefly-iii using the [Restic block](blocks-restic.html) is done like so:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
shb.restic.instances."firefly-iii" = {
|
|
||||||
request = config.shb.firefly-iii.backup;
|
|
||||||
settings = {
|
|
||||||
enable = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
The name `"firefly-iii"` in the `instances` can be anything.
|
|
||||||
The `config.shb.firefly-iii.backup` option provides what directories to backup.
|
|
||||||
You can define any number of Restic instances to backup Firefly-iii multiple times.
|
|
||||||
|
|
||||||
You will then need to configure more options like the `repository`,
|
|
||||||
as explained in the [restic](blocks-restic.html) documentation.
|
|
||||||
|
|
||||||
### Certificates {#services-firefly-iii-certs}
|
|
||||||
|
|
||||||
For Let's Encrypt certificates, add:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.certs.certs.letsencrypt.${domain}.extraDomains = [
|
|
||||||
"${config.shb.firefly-iii.subdomain}.${config.shb.firefly-iii.domain}"
|
|
||||||
"${config.shb.firefly-iii.importer.subdomain}.${config.shb.firefly-iii.domain}"
|
|
||||||
];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Impermanence {#services-firefly-iii-impermanence}
|
|
||||||
|
|
||||||
To save the data folder in an impermanence setup, add:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.zfs.datasets."safe/firefly-iii".path = config.shb.firefly-iii.impermanence;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Declarative LDAP {#services-firefly-iii-declarative-ldap}
|
|
||||||
|
|
||||||
To add a user `USERNAME` to the user and admin groups for Firefly-iii, add:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
shb.lldap.ensureUsers.USERNAME.groups = [
|
|
||||||
config.shb.firefly-iii.ldap.userGroup
|
|
||||||
config.shb.firefly-iii.ldap.adminGroup
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
### Application Dashboard {#services-firefly-iii-usage-applicationdashboard}
|
|
||||||
|
|
||||||
Integration with the [dashboard contract](contracts-dashboard.html) is provided
|
|
||||||
by the [dashboard option](#services-firefly-iii-options-shb.firefly-iii.dashboard).
|
|
||||||
|
|
||||||
For example using the [Homepage](services-homepage.html) service:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.homepage.servicesGroups.Finance.services.Firefly-iii = {
|
|
||||||
sortOrder = 1;
|
|
||||||
dashboard.request = config.shb.firefly-iii.dashboard.request;
|
|
||||||
settings.widget.type = "firefly";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The widget type needs to be set manually otherwise it is not displayed correctly.
|
|
||||||
|
|
||||||
An API key can be set to show extra info:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.homepage.servicesGroups.Finance.services.Firefly-iii = {
|
|
||||||
apiKey.result = config.shb.sops.secret."firefly-iii/homepageApiKey".result;
|
|
||||||
};
|
|
||||||
|
|
||||||
shb.sops.secret."firefly-iii/homepageApiKey".request =
|
|
||||||
config.shb.homepage.servicesGroups.Finance.services.Firefly-iii.apiKey.request;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Database Inspection {#services-firefly-iii-database-inspection}
|
|
||||||
|
|
||||||
Access the database with:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
sudo -u firefly-iii psql
|
|
||||||
```
|
|
||||||
|
|
||||||
Dump the database with:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
sudo -u firefly-iii pg_dump --data-only --inserts firefly-iii > dump
|
|
||||||
```
|
|
||||||
|
|
||||||
## Mobile Apps {#services-firefly-iii-mobile}
|
|
||||||
|
|
||||||
This module was tested with the [Abacus iOS](https://github.com/victorbalssa/abacus) mobile app
|
|
||||||
using a Personal Account Token.
|
|
||||||
|
|
||||||
## Options Reference {#services-firefly-iii-options}
|
|
||||||
|
|
||||||
```{=include=} options
|
|
||||||
id-prefix: services-firefly-iii-options-
|
|
||||||
list-id: selfhostblocks-service-firefly-iii-options
|
|
||||||
source: @OPTIONS_JSON@
|
|
||||||
```
|
|
||||||
|
|
@ -44,18 +44,16 @@ in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
../blocks/nginx.nix
|
../blocks/nginx.nix
|
||||||
../blocks/lldap.nix
|
|
||||||
|
|
||||||
(lib.mkRemovedOptionModule [ "shb" "forgejo" "adminPassword" ] ''
|
(lib.mkRemovedOptionModule [ "shb" "forgejo" "adminPassword" ] ''
|
||||||
Instead, define an admin user in shb.forgejo.users and give it the same password, like so:
|
Instead, define an admin user in shb.forgejo.users and give it the same password, like so:
|
||||||
|
shb.forgejo.users = {
|
||||||
shb.forgejo.users = {
|
"forgejoadmin" = {
|
||||||
"forgejoadmin" = {
|
isAdmin = true;
|
||||||
isAdmin = true;
|
email = "forgejoadmin@example.com";
|
||||||
email = "forgejoadmin@example.com";
|
password.result = <path/to/password>;
|
||||||
password.result = <path/to/password>;
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
'')
|
'')
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -235,7 +233,6 @@ in
|
||||||
|
|
||||||
users = mkOption {
|
users = mkOption {
|
||||||
description = "Users managed declaratively.";
|
description = "Users managed declaratively.";
|
||||||
default = { };
|
|
||||||
type = attrsOf (submodule {
|
type = attrsOf (submodule {
|
||||||
options = {
|
options = {
|
||||||
isAdmin = mkOption {
|
isAdmin = mkOption {
|
||||||
|
|
@ -248,7 +245,7 @@ in
|
||||||
description = ''
|
description = ''
|
||||||
Email of user.
|
Email of user.
|
||||||
|
|
||||||
This is only set when the user is created, changing this later on will have no effect.
|
This is only set when the user is created, changing this later on will have no effect.
|
||||||
'';
|
'';
|
||||||
type = str;
|
type = str;
|
||||||
};
|
};
|
||||||
|
|
@ -334,7 +331,7 @@ in
|
||||||
options = shb.contracts.backup.mkRequester {
|
options = shb.contracts.backup.mkRequester {
|
||||||
user = options.services.forgejo.user.value;
|
user = options.services.forgejo.user.value;
|
||||||
sourceDirectories = [
|
sourceDirectories = [
|
||||||
config.services.forgejo.dump.backupDir
|
options.services.forgejo.dump.backupDir.value
|
||||||
]
|
]
|
||||||
++ optionals (cfg.repositoryRoot != null) [
|
++ optionals (cfg.repositoryRoot != null) [
|
||||||
cfg.repositoryRoot
|
cfg.repositoryRoot
|
||||||
|
|
@ -388,16 +385,9 @@ in
|
||||||
type = str;
|
type = str;
|
||||||
description = "Username to connect to the SMTP host.";
|
description = "Username to connect to the SMTP host.";
|
||||||
};
|
};
|
||||||
password = mkOption {
|
passwordFile = mkOption {
|
||||||
|
type = str;
|
||||||
description = "File containing the password to connect to the SMTP host.";
|
description = "File containing the password to connect to the SMTP host.";
|
||||||
type = submodule {
|
|
||||||
options = shb.contracts.secret.mkRequester {
|
|
||||||
mode = "0440";
|
|
||||||
owner = "forgejo";
|
|
||||||
group = "forgejo";
|
|
||||||
restartUnits = [ "forgejo.service" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -408,21 +398,6 @@ in
|
||||||
type = bool;
|
type = bool;
|
||||||
default = false;
|
default = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
dashboard = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Dashboard contract consumer
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.dashboard.mkRequester {
|
|
||||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
|
||||||
externalUrlText = "https://\${config.shb.forgejo.subdomain}.\${config.shb.forgejo.domain}";
|
|
||||||
internalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
|
||||||
internalUrlText = "https://\${config.shb.forgejo.subdomain}.\${config.shb.forgejo.domain}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkMerge [
|
config = mkMerge [
|
||||||
|
|
@ -486,12 +461,6 @@ in
|
||||||
(mkIf (cfg.enable && cfg.ldap.enable != false) {
|
(mkIf (cfg.enable && cfg.ldap.enable != false) {
|
||||||
systemd.services.forgejo.wants = cfg.ldap.waitForSystemdServices;
|
systemd.services.forgejo.wants = cfg.ldap.waitForSystemdServices;
|
||||||
systemd.services.forgejo.after = cfg.ldap.waitForSystemdServices;
|
systemd.services.forgejo.after = cfg.ldap.waitForSystemdServices;
|
||||||
|
|
||||||
shb.lldap.ensureGroups = {
|
|
||||||
${cfg.ldap.adminGroup} = { };
|
|
||||||
${cfg.ldap.userGroup} = { };
|
|
||||||
};
|
|
||||||
|
|
||||||
# The delimiter in the `cut` command is a TAB!
|
# The delimiter in the `cut` command is a TAB!
|
||||||
systemd.services.forgejo.preStart =
|
systemd.services.forgejo.preStart =
|
||||||
let
|
let
|
||||||
|
|
@ -517,7 +486,7 @@ in
|
||||||
--bind-password $(tr -d '\n' < ${cfg.ldap.adminPassword.result.path}) \
|
--bind-password $(tr -d '\n' < ${cfg.ldap.adminPassword.result.path}) \
|
||||||
--security-protocol Unencrypted \
|
--security-protocol Unencrypted \
|
||||||
--user-search-base ou=people,${cfg.ldap.dcdomain} \
|
--user-search-base ou=people,${cfg.ldap.dcdomain} \
|
||||||
--user-filter '(&(|(memberof=cn=${cfg.ldap.userGroup},ou=groups,${cfg.ldap.dcdomain})(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain}))(|(uid=%[1]s)(mail=%[1]s)))' \
|
--user-filter '(&(memberof=cn=${cfg.ldap.userGroup},ou=groups,${cfg.ldap.dcdomain})(|(uid=%[1]s)(mail=%[1]s)))' \
|
||||||
--admin-filter '(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain})' \
|
--admin-filter '(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain})' \
|
||||||
--username-attribute uid \
|
--username-attribute uid \
|
||||||
--firstname-attribute givenName \
|
--firstname-attribute givenName \
|
||||||
|
|
@ -536,7 +505,7 @@ in
|
||||||
--bind-password $(tr -d '\n' < ${cfg.ldap.adminPassword.result.path}) \
|
--bind-password $(tr -d '\n' < ${cfg.ldap.adminPassword.result.path}) \
|
||||||
--security-protocol Unencrypted \
|
--security-protocol Unencrypted \
|
||||||
--user-search-base ou=people,${cfg.ldap.dcdomain} \
|
--user-search-base ou=people,${cfg.ldap.dcdomain} \
|
||||||
--user-filter '(&(|(memberof=cn=${cfg.ldap.userGroup},ou=groups,${cfg.ldap.dcdomain})(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain}))(|(uid=%[1]s)(mail=%[1]s)))' \
|
--user-filter '(&(memberof=cn=${cfg.ldap.userGroup},ou=groups,${cfg.ldap.dcdomain})(|(uid=%[1]s)(mail=%[1]s)))' \
|
||||||
--admin-filter '(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain})' \
|
--admin-filter '(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain})' \
|
||||||
--username-attribute uid \
|
--username-attribute uid \
|
||||||
--firstname-attribute givenName \
|
--firstname-attribute givenName \
|
||||||
|
|
@ -553,13 +522,6 @@ in
|
||||||
# For Forgejo config: https://forgejo.org/docs/latest/admin/config-cheat-sheet
|
# For Forgejo config: https://forgejo.org/docs/latest/admin/config-cheat-sheet
|
||||||
# For cli info: https://docs.gitea.com/usage/command-line
|
# For cli info: https://docs.gitea.com/usage/command-line
|
||||||
(mkIf (cfg.enable && cfg.sso.enable != false) {
|
(mkIf (cfg.enable && cfg.sso.enable != false) {
|
||||||
assertions = [
|
|
||||||
{
|
|
||||||
assertion = cfg.ldap.enable == true;
|
|
||||||
message = "'shb.forgejo.ldap.enable' must be set to true and ldap configured when 'shb.forgejo.sso.enable' is true. Otherwise you will never be able to register new accounts.";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
services.forgejo.settings = {
|
services.forgejo.settings = {
|
||||||
oauth2 = {
|
oauth2 = {
|
||||||
ENABLED = true;
|
ENABLED = true;
|
||||||
|
|
@ -655,7 +617,7 @@ in
|
||||||
SMTP_ADDR = "${cfg.smtp.host}:${toString cfg.smtp.port}";
|
SMTP_ADDR = "${cfg.smtp.host}:${toString cfg.smtp.port}";
|
||||||
FROM = cfg.smtp.from_address;
|
FROM = cfg.smtp.from_address;
|
||||||
USER = cfg.smtp.username;
|
USER = cfg.smtp.username;
|
||||||
PASSWD = cfg.smtp.password.result.path;
|
PASSWD = cfg.smtp.passwordFile;
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -667,7 +629,7 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
services.gitea-actions-runner = mkIf cfg.localActionRunner {
|
services.gitea-actions-runner = mkIf cfg.localActionRunner {
|
||||||
package = pkgs.forgejo-runner;
|
package = pkgs.forgejo-actions-runner;
|
||||||
instances.local = {
|
instances.local = {
|
||||||
enable = true;
|
enable = true;
|
||||||
name = "local";
|
name = "local";
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ LDAP and SSO integration as well as one local runner.
|
||||||
- Access through [subdomain](#services-forgejo-options-shb.forgejo.subdomain) using reverse proxy. [Manual](#services-forgejo-usage-configuration).
|
- Access through [subdomain](#services-forgejo-options-shb.forgejo.subdomain) using reverse proxy. [Manual](#services-forgejo-usage-configuration).
|
||||||
- Access through [HTTPS](#services-forgejo-options-shb.forgejo.ssl) using reverse proxy. [Manual](#services-forgejo-usage-configuration).
|
- Access through [HTTPS](#services-forgejo-options-shb.forgejo.ssl) using reverse proxy. [Manual](#services-forgejo-usage-configuration).
|
||||||
- [Backup](#services-forgejo-options-shb.forgejo.sso) through the [backup block](./blocks-backup.html). [Manual](#services-forgejo-usage-backup).
|
- [Backup](#services-forgejo-options-shb.forgejo.sso) through the [backup block](./blocks-backup.html). [Manual](#services-forgejo-usage-backup).
|
||||||
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-forgejo-usage-applicationdashboard)
|
|
||||||
|
|
||||||
## Usage {#services-forgejo-usage}
|
## Usage {#services-forgejo-usage}
|
||||||
|
|
||||||
|
|
@ -35,20 +34,20 @@ shb.forgejo = {
|
||||||
"theadmin" = {
|
"theadmin" = {
|
||||||
isAdmin = true;
|
isAdmin = true;
|
||||||
email = "theadmin@example.com";
|
email = "theadmin@example.com";
|
||||||
password.result = config.shb.sops.secret.forgejoAdminPassword.result;
|
password.result = config.shb.sops.secrets.forgejoAdminPassword.result;
|
||||||
};
|
};
|
||||||
"theuser" = {
|
"theuser" = {
|
||||||
email = "theuser@example.com";
|
email = "theuser@example.com";
|
||||||
password.result = config.shb.sops.secret.forgejoUserPassword.result;
|
password.result = config.shb.sops.secrets.forgejoUserPassword.result;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.sops.secret."forgejo/admin/password" = {
|
shb.sops.secrets."forgejo/admin/password" = {
|
||||||
request = config.shb.forgejo.users."theadmin".password.request;
|
request = config.shb.forgejo.users."theadmin".password.request;
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.sops.secret."forgejo/user/password" = {
|
shb.sops.secrets."forgejo/user/password" = {
|
||||||
request = config.shb.forgejo.users."theuser".password.request;
|
request = config.shb.forgejo.users."theuser".password.request;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
@ -111,10 +110,10 @@ shb.forgejo.ldap = {
|
||||||
host = "127.0.0.1";
|
host = "127.0.0.1";
|
||||||
port = config.shb.lldap.ldapPort;
|
port = config.shb.lldap.ldapPort;
|
||||||
dcdomain = config.shb.lldap.dcdomain;
|
dcdomain = config.shb.lldap.dcdomain;
|
||||||
adminPassword.result = config.shb.sops.secret."forgejo/ldap/adminPassword".result
|
adminPassword.result = config.shb.sops.secrets."forgejo/ldap/adminPassword".result
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.sops.secret."forgejo/ldap/adminPassword" = {
|
shb.sops.secrets."forgejo/ldap/adminPassword" = {
|
||||||
request = config.shb.forgejo.ldap.adminPassword.request;
|
request = config.shb.forgejo.ldap.adminPassword.request;
|
||||||
settings.key = "ldap/userPassword";
|
settings.key = "ldap/userPassword";
|
||||||
};
|
};
|
||||||
|
|
@ -164,28 +163,6 @@ must have the same content. The former is a file that must be owned by the `forg
|
||||||
the latter must be owned by the `authelia` user. I want to avoid needing to define the same secret
|
the latter must be owned by the `authelia` user. I want to avoid needing to define the same secret
|
||||||
twice with a future secrets SHB block.
|
twice with a future secrets SHB block.
|
||||||
|
|
||||||
### SMTP {#services-forgejo-usage-smtp}
|
|
||||||
|
|
||||||
To send e-mails, notifications, define the SMTP settings like so:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
services.forgejo = {
|
|
||||||
smtp = {
|
|
||||||
host = "smtp.mailgun.org";
|
|
||||||
port = 587;
|
|
||||||
username = "postmaster@mg.${domain}";
|
|
||||||
from_address = "authelia@${domain}";
|
|
||||||
password.result = config.shb.sops.secret."forgejo/smtpPassword".result;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
shb.sops.secret."forgejo/smtpPassword" = {
|
|
||||||
request = config.shb.forgejo.smtp.password.request;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backup {#services-forgejo-usage-backup}
|
### Backup {#services-forgejo-usage-backup}
|
||||||
|
|
||||||
Every hour, Forgejo takes a backup using the [built-in `dump` command](https://forgejo.org/docs/latest/admin/command-line/#dump).
|
Every hour, Forgejo takes a backup using the [built-in `dump` command](https://forgejo.org/docs/latest/admin/command-line/#dump).
|
||||||
|
|
@ -207,22 +184,6 @@ The name `"forgejo"` in the `instances` can be anything.
|
||||||
The `config.shb.forgejo.backup` option provides what directories to backup.
|
The `config.shb.forgejo.backup` option provides what directories to backup.
|
||||||
You can define any number of Restic instances to backup Forgejo multiple times.
|
You can define any number of Restic instances to backup Forgejo multiple times.
|
||||||
|
|
||||||
### Application Dashboard {#services-forgejo-usage-applicationdashboard}
|
|
||||||
|
|
||||||
Integration with the [dashboard contract](contracts-dashboard.html) is provided
|
|
||||||
by the [dashboard option](#services-forgejo-options-shb.forgejo.dashboard).
|
|
||||||
|
|
||||||
For example using the [Homepage](services-homepage.html) service:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.homepage.servicesGroups.Admin.services.Forgejo = {
|
|
||||||
sortOrder = 1;
|
|
||||||
dashboard.request = config.shb.forgejo.dashboard.request;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Extra Settings {#services-forgejo-usage-extra-settings}
|
### Extra Settings {#services-forgejo-usage-extra-settings}
|
||||||
|
|
||||||
Other Forgejo settings can be accessed through the nixpkgs [stock service][].
|
Other Forgejo settings can be accessed through the nixpkgs [stock service][].
|
||||||
|
|
|
||||||
|
|
@ -111,21 +111,6 @@ in
|
||||||
default = false;
|
default = false;
|
||||||
example = true;
|
example = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
dashboard = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Dashboard contract consumer
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.dashboard.mkRequester {
|
|
||||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
|
||||||
externalUrlText = "https://\${config.shb.grocy.subdomain}.\${config.shb.grocy.domain}";
|
|
||||||
internalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
|
||||||
internalUrlText = "https://\${config.shb.grocy.subdomain}.\${config.shb.grocy.domain}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable (
|
config = lib.mkIf cfg.enable (
|
||||||
|
|
|
||||||
|
|
@ -82,21 +82,6 @@ in
|
||||||
default = [ "--forecast" ];
|
default = [ "--forecast" ];
|
||||||
type = lib.types.listOf lib.types.str;
|
type = lib.types.listOf lib.types.str;
|
||||||
};
|
};
|
||||||
|
|
||||||
dashboard = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Dashboard contract consumer
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.dashboard.mkRequester {
|
|
||||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
|
||||||
externalUrlText = "https://\${config.shb.hledger.subdomain}.\${config.shb.hledger.domain}";
|
|
||||||
internalUrl = "http://127.0.0.1:${toString config.services.hledger-web.port}";
|
|
||||||
internalUrlText = "http://127.0.0.1:\${config.services.hledger-web.port}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,23 @@ let
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
|
|
||||||
|
ldap_auth_script_repo = pkgs.fetchFromGitHub {
|
||||||
|
owner = "lldap";
|
||||||
|
repo = "lldap";
|
||||||
|
rev = "7d1f5abc137821c500de99c94f7579761fc949d8";
|
||||||
|
sha256 = "sha256-8D+7ww70Ja6Qwdfa+7MpjAAHewtCWNf/tuTAExoUrg0=";
|
||||||
|
};
|
||||||
|
|
||||||
|
ldap_auth_script = pkgs.writeShellScriptBin "ldap_auth.sh" ''
|
||||||
|
export PATH=${pkgs.gnused}/bin:${pkgs.curl}/bin:${pkgs.jq}/bin
|
||||||
|
exec ${pkgs.bash}/bin/bash ${ldap_auth_script_repo}/example_configs/lldap-ha-auth.sh $@
|
||||||
|
'';
|
||||||
|
|
||||||
# Filter secrets from config. Secrets are those of the form { source = <path>; }
|
# Filter secrets from config. Secrets are those of the form { source = <path>; }
|
||||||
secrets = lib.attrsets.filterAttrs (k: v: builtins.isAttrs v) cfg.config;
|
secrets = lib.attrsets.filterAttrs (k: v: builtins.isAttrs v) cfg.config;
|
||||||
|
|
||||||
nonSecrets = (lib.attrsets.filterAttrs (k: v: !(builtins.isAttrs v)) cfg.config);
|
nonSecrets = (lib.attrsets.filterAttrs (k: v: !(builtins.isAttrs v)) cfg.config);
|
||||||
|
|
||||||
lldap_ha_auth = pkgs.callPackage ./home-assistant/lldap_ha_auth.nix { };
|
|
||||||
|
|
||||||
configWithSecretsIncludes = nonSecrets // (lib.attrsets.mapAttrs (k: v: "!secret ${k}") secrets);
|
configWithSecretsIncludes = nonSecrets // (lib.attrsets.mapAttrs (k: v: "!secret ${k}") secrets);
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|
@ -208,21 +218,6 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
dashboard = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Dashboard contract consumer
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.dashboard.mkRequester {
|
|
||||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
|
||||||
externalUrlText = "https://\${config.shb.home-assistant.subdomain}.\${config.shb.home-assistant.domain}";
|
|
||||||
internalUrl = "http://127.0.0.1:${toString config.services.home-assistant.config.http.server_port}";
|
|
||||||
internalUrlText = "http://127.0.0.1:\${config.services.home-assistant.config.http.server_port}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
|
|
@ -253,6 +248,7 @@ in
|
||||||
config = {
|
config = {
|
||||||
# Includes dependencies for a basic setup
|
# Includes dependencies for a basic setup
|
||||||
# https://www.home-assistant.io/integrations/default_config/
|
# https://www.home-assistant.io/integrations/default_config/
|
||||||
|
default_config = { };
|
||||||
http = {
|
http = {
|
||||||
use_x_forwarded_for = true;
|
use_x_forwarded_for = true;
|
||||||
server_host = "127.0.0.1";
|
server_host = "127.0.0.1";
|
||||||
|
|
@ -270,10 +266,9 @@ in
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
++ (lib.optionals cfg.ldap.enable [
|
++ (lib.optionals cfg.ldap.enable [
|
||||||
# https://www.home-assistant.io/docs/authentication/providers/#command-line
|
|
||||||
{
|
{
|
||||||
type = "command_line";
|
type = "command_line";
|
||||||
command = lldap_ha_auth + "/bin/lldap-ha-auth";
|
command = ldap_auth_script + "/bin/ldap_auth.sh";
|
||||||
args = [
|
args = [
|
||||||
"http://${cfg.ldap.host}:${toString cfg.ldap.port}"
|
"http://${cfg.ldap.host}:${toString cfg.ldap.port}"
|
||||||
cfg.ldap.userGroup
|
cfg.ldap.userGroup
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ LDAP and SSO integration.
|
||||||
- Access through [subdomain](#services-home-assistant-options-shb.home-assistant.subdomain) using reverse proxy. [Manual](#services-home-assistant-usage-configuration).
|
- Access through [subdomain](#services-home-assistant-options-shb.home-assistant.subdomain) using reverse proxy. [Manual](#services-home-assistant-usage-configuration).
|
||||||
- Access through [HTTPS](#services-home-assistant-options-shb.home-assistant.ssl) using reverse proxy. [Manual](#services-home-assistant-usage-configuration).
|
- Access through [HTTPS](#services-home-assistant-options-shb.home-assistant.ssl) using reverse proxy. [Manual](#services-home-assistant-usage-configuration).
|
||||||
- [Backup](#services-home-assistant-options-shb.home-assistant.backup) through the [backup block](./blocks-backup.html). [Manual](#services-home-assistant-usage-backup).
|
- [Backup](#services-home-assistant-options-shb.home-assistant.backup) through the [backup block](./blocks-backup.html). [Manual](#services-home-assistant-usage-backup).
|
||||||
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-home-assistant-usage-applicationdashboard)
|
|
||||||
|
|
||||||
- Not yet: declarative SSO.
|
- Not yet: declarative SSO.
|
||||||
|
|
||||||
|
|
@ -164,57 +163,6 @@ You can define any number of Restic instances to backup Home-Assistant multiple
|
||||||
You will then need to configure more options like the `repository`,
|
You will then need to configure more options like the `repository`,
|
||||||
as explained in the [restic](blocks-restic.html) documentation.
|
as explained in the [restic](blocks-restic.html) documentation.
|
||||||
|
|
||||||
### Application Dashboard {#services-home-assistant-usage-applicationdashboard}
|
|
||||||
|
|
||||||
Integration with the [dashboard contract](contracts-dashboard.html) is provided
|
|
||||||
by the [dashboard option](#services-home-assistant-options-shb.home-assistant.dashboard).
|
|
||||||
|
|
||||||
For example using the [Homepage](services-homepage.html) service:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.homepage.servicesGroups.Home.services.HomeAssistant = {
|
|
||||||
sortOrder = 1;
|
|
||||||
dashboard.request = config.shb.home-assistant.dashboard.request;
|
|
||||||
settings.icon = "si-homeassistant";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The icon needs to be set manually otherwise it is not displayed correctly.
|
|
||||||
|
|
||||||
An API key can be set to show extra info:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.homepage.servicesGroups.Home.services.HomeAssistant = {
|
|
||||||
apiKey.result = config.shb.sops.secret."home-assistant/homepageApiKey".result;
|
|
||||||
};
|
|
||||||
|
|
||||||
shb.sops.secret."home-assistant/homepageApiKey".request =
|
|
||||||
config.shb.homepage.servicesGroups.Home.services.HomeAssistant.apiKey.request;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Custom widgets can be set using Home Assistant templating:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.homepage.servicesGroups.Home.services.HomeAssistant = {
|
|
||||||
settings.widget.custom = [
|
|
||||||
{
|
|
||||||
template = "{{ states('sensor.power_consumption_power_consumption', with_unit=True, rounded=True) }}";
|
|
||||||
label = "energy now";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
state = "sensor.power_consumption_daily_power_consumption";
|
|
||||||
label = "energy today";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Extra Components {#services-home-assistant-usage-extra-components}
|
### Extra Components {#services-home-assistant-usage-extra-components}
|
||||||
|
|
||||||
Packaged components can be found in the documentation of the corresponding option
|
Packaged components can be found in the documentation of the corresponding option
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
{
|
|
||||||
lib,
|
|
||||||
pkgs,
|
|
||||||
stdenvNoCC,
|
|
||||||
}:
|
|
||||||
stdenvNoCC.mkDerivation {
|
|
||||||
name = "lldap-ha-auth";
|
|
||||||
|
|
||||||
src = pkgs.fetchFromGitHub {
|
|
||||||
owner = "lldap";
|
|
||||||
repo = "lldap";
|
|
||||||
rev = "7d1f5abc137821c500de99c94f7579761fc949d8";
|
|
||||||
sha256 = "sha256-8D+7ww70Ja6Qwdfa+7MpjAAHewtCWNf/tuTAExoUrg0=";
|
|
||||||
};
|
|
||||||
|
|
||||||
nativeBuildInputs = [
|
|
||||||
pkgs.makeWrapper
|
|
||||||
];
|
|
||||||
|
|
||||||
buildPhase = ''
|
|
||||||
mkdir -p $out/bin
|
|
||||||
|
|
||||||
cp example_configs/lldap-ha-auth.sh $out/bin/lldap-ha-auth
|
|
||||||
chmod a+x $out/bin/lldap-ha-auth
|
|
||||||
'';
|
|
||||||
|
|
||||||
installPhase = ''
|
|
||||||
wrapProgram $out/bin/lldap-ha-auth \
|
|
||||||
--prefix PATH : ${
|
|
||||||
lib.makeBinPath [
|
|
||||||
pkgs.gnused
|
|
||||||
pkgs.curl
|
|
||||||
pkgs.jq
|
|
||||||
]
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
|
|
@ -1,286 +0,0 @@
|
||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
shb,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
cfg = config.shb.homepage;
|
|
||||||
|
|
||||||
inherit (lib) types;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
../../lib/module.nix
|
|
||||||
|
|
||||||
../blocks/lldap.nix
|
|
||||||
../blocks/nginx.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
options.shb.homepage = {
|
|
||||||
enable = lib.mkEnableOption "the SHB homepage service";
|
|
||||||
|
|
||||||
subdomain = lib.mkOption {
|
|
||||||
type = types.str;
|
|
||||||
description = ''
|
|
||||||
Subdomain under which homepage will be served.
|
|
||||||
|
|
||||||
```
|
|
||||||
<subdomain>.<domain>
|
|
||||||
```
|
|
||||||
'';
|
|
||||||
example = "homepage";
|
|
||||||
};
|
|
||||||
|
|
||||||
domain = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Domain under which homepage is served.
|
|
||||||
|
|
||||||
```
|
|
||||||
<subdomain>.<domain>
|
|
||||||
```
|
|
||||||
'';
|
|
||||||
type = types.str;
|
|
||||||
example = "domain.com";
|
|
||||||
};
|
|
||||||
|
|
||||||
ssl = lib.mkOption {
|
|
||||||
description = "Path to SSL files";
|
|
||||||
type = types.nullOr shb.contracts.ssl.certs;
|
|
||||||
default = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
servicesGroups = lib.mkOption {
|
|
||||||
description = "Group of services that should be showed on the dashboard.";
|
|
||||||
default = { };
|
|
||||||
type = types.attrsOf (
|
|
||||||
types.submodule (
|
|
||||||
{ name, ... }:
|
|
||||||
{
|
|
||||||
options = {
|
|
||||||
name = lib.mkOption {
|
|
||||||
type = types.str;
|
|
||||||
description = "Display name of the group. Defaults to the attr name.";
|
|
||||||
default = name;
|
|
||||||
};
|
|
||||||
sortOrder = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Order in which groups will be shown.
|
|
||||||
|
|
||||||
The rules are:
|
|
||||||
|
|
||||||
- Lowest number is shown first.
|
|
||||||
- Two groups having the same number are shown in a consistent (same across multiple deploys) but undefined order.
|
|
||||||
- Default is null which means at the end.
|
|
||||||
'';
|
|
||||||
type = types.nullOr types.int;
|
|
||||||
default = null;
|
|
||||||
};
|
|
||||||
services = lib.mkOption {
|
|
||||||
description = "Services that should be showed in the group on the dashboard.";
|
|
||||||
default = { };
|
|
||||||
type = types.attrsOf (
|
|
||||||
types.submodule (
|
|
||||||
{ name, ... }:
|
|
||||||
{
|
|
||||||
options = {
|
|
||||||
name = lib.mkOption {
|
|
||||||
type = types.str;
|
|
||||||
description = "Display name of the service. Defaults to the attr name.";
|
|
||||||
default = name;
|
|
||||||
};
|
|
||||||
sortOrder = lib.mkOption {
|
|
||||||
type = types.nullOr types.int;
|
|
||||||
description = ''
|
|
||||||
Order in which groups will be shown.
|
|
||||||
|
|
||||||
The rules are:
|
|
||||||
|
|
||||||
- Lowest number is shown first.
|
|
||||||
- Two groups having the same number are shown in a consistent (same across multiple deploys) but undefined order.
|
|
||||||
- Default is null which means at the end.
|
|
||||||
'';
|
|
||||||
default = null;
|
|
||||||
};
|
|
||||||
dashboard = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Provider of the dashboard contract.
|
|
||||||
|
|
||||||
By default:
|
|
||||||
|
|
||||||
- The `serviceName` option comes from the attr name.
|
|
||||||
- The `icon` option comes from applying `toLower` on the attr name.
|
|
||||||
- The `siteMonitor` option is set only if `internalUrl` is set.
|
|
||||||
'';
|
|
||||||
type = types.submodule {
|
|
||||||
options = shb.contracts.dashboard.mkProvider {
|
|
||||||
resultCfg = { };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
apiKey = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
API key used to access the service.
|
|
||||||
|
|
||||||
This can be used to get data from the service.
|
|
||||||
'';
|
|
||||||
default = null;
|
|
||||||
type = types.nullOr (
|
|
||||||
lib.types.submodule {
|
|
||||||
options = shb.contracts.secret.mkRequester {
|
|
||||||
owner = "root";
|
|
||||||
restartUnits = [ "homepage-dashboard.service" ];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
settings = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Extra options to pass to the homepage service.
|
|
||||||
|
|
||||||
Check https://gethomepage.dev/configs/services/#icons
|
|
||||||
if the default icon is not correct.
|
|
||||||
|
|
||||||
And check https://gethomepage.dev/widgets
|
|
||||||
if the default widget type is not correct.
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = types.attrsOf types.anything;
|
|
||||||
example = lib.literalExpression ''
|
|
||||||
{
|
|
||||||
icon = "si-homeassistant";
|
|
||||||
widget.type = "firefly";
|
|
||||||
widget.custom = [
|
|
||||||
{
|
|
||||||
template = "{{ states('sensor.total_power', with_unit=True, rounded=True) }}";
|
|
||||||
label = "energy now";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
state = "sensor.total_power_today";
|
|
||||||
label = "energy today";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ldap = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Setup LDAP integration.
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = types.submodule {
|
|
||||||
options = {
|
|
||||||
userGroup = lib.mkOption {
|
|
||||||
type = types.str;
|
|
||||||
description = "Group users must belong to be able to login.";
|
|
||||||
default = "homepage_user";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
sso = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Setup SSO integration.
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = types.submodule {
|
|
||||||
options = {
|
|
||||||
enable = lib.mkEnableOption "SSO integration.";
|
|
||||||
|
|
||||||
authEndpoint = lib.mkOption {
|
|
||||||
type = lib.types.str;
|
|
||||||
description = "Endpoint to the SSO provider.";
|
|
||||||
example = "https://authelia.example.com";
|
|
||||||
};
|
|
||||||
|
|
||||||
authorization_policy = lib.mkOption {
|
|
||||||
type = types.enum [
|
|
||||||
"one_factor"
|
|
||||||
"two_factor"
|
|
||||||
];
|
|
||||||
description = "Require one factor (password) or two factor (device) authentication.";
|
|
||||||
default = "one_factor";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
services.homepage-dashboard = {
|
|
||||||
enable = true;
|
|
||||||
|
|
||||||
allowedHosts = "${cfg.subdomain}.${cfg.domain}";
|
|
||||||
|
|
||||||
settings = {
|
|
||||||
baseUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
|
||||||
startUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
|
||||||
disableUpdateCheck = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
bookmarks = [ ];
|
|
||||||
|
|
||||||
services = shb.homepage.asServiceGroup cfg.servicesGroups;
|
|
||||||
|
|
||||||
widgets = [ ];
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.homepage-dashboard.serviceConfig =
|
|
||||||
let
|
|
||||||
keys = shb.homepage.allKeys cfg.servicesGroups;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
# LoadCredential = [
|
|
||||||
# "Media_Jellyfin:/path"
|
|
||||||
# ];
|
|
||||||
LoadCredential = lib.mapAttrsToList (name: path: "${name}:${path}") keys;
|
|
||||||
# Environment = [
|
|
||||||
# "HOMEPAGE_FILE_Media_Jellyfin=%d/Media_Jellyfin"
|
|
||||||
# ];
|
|
||||||
Environment = lib.mapAttrsToList (name: path: "HOMEPAGE_FILE_${name}=%d/${name}") keys;
|
|
||||||
};
|
|
||||||
|
|
||||||
# This should be using a contract instead of setting the option directly.
|
|
||||||
shb.lldap = lib.mkIf config.shb.lldap.enable {
|
|
||||||
ensureGroups = {
|
|
||||||
${cfg.ldap.userGroup} = { };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
shb.nginx.vhosts = [
|
|
||||||
(
|
|
||||||
{
|
|
||||||
inherit (cfg) subdomain domain ssl;
|
|
||||||
|
|
||||||
upstream = "http://127.0.0.1:${toString config.services.homepage-dashboard.listenPort}/";
|
|
||||||
extraConfig = ''
|
|
||||||
proxy_read_timeout 300s;
|
|
||||||
proxy_send_timeout 300s;
|
|
||||||
'';
|
|
||||||
autheliaRules = lib.optionals (cfg.sso.enable) [
|
|
||||||
{
|
|
||||||
domain = "${cfg.subdomain}.${cfg.domain}";
|
|
||||||
policy = cfg.sso.authorization_policy;
|
|
||||||
subject = [ "group:${cfg.ldap.userGroup}" ];
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
// lib.optionalAttrs cfg.sso.enable {
|
|
||||||
inherit (cfg.sso) authEndpoint;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 128 KiB |
|
|
@ -1,193 +0,0 @@
|
||||||
# Homepage Service {#services-homepage}
|
|
||||||
|
|
||||||
Defined in [`/modules/services/homepage.nix`](@REPO@/modules/services/homepage.nix),
|
|
||||||
found in the `selfhostblocks.nixosModules.homepage` module.
|
|
||||||
See [the manual](usage.html#usage-flake) for how to import the module in your code.
|
|
||||||
|
|
||||||
This service sets up [Homepage Dashboard][] which provides
|
|
||||||
a highly customizable homepage Docker and service API integrations.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
[Homepage Dashboard]: https://github.com/gethomepage/homepage
|
|
||||||
|
|
||||||
## Features {#services-homepage-features}
|
|
||||||
|
|
||||||
- Declarative SSO login through forward authentication.
|
|
||||||
Only users of the [Homepage LDAP user group][] can access the web UI.
|
|
||||||
This is enforced using the [Authelia block][] which integrates with the LLDAP block.
|
|
||||||
- Access through [subdomain][] using the reverse proxy.
|
|
||||||
It is implemented with the [Nginx block][].
|
|
||||||
- Access through [HTTPS][] using the reverse proxy.
|
|
||||||
It is implemented with the [SSL block][].
|
|
||||||
- Integration with [secrets contract][] to set the API key for a widget.
|
|
||||||
|
|
||||||
[Homepage LDAP user group]: #services-homepage-options-shb.homepage.ldap.userGroup
|
|
||||||
[Authelia block]: blocks-authelia.html
|
|
||||||
[subdomain]: #services-open-webui-options-shb.open-webui.subdomain
|
|
||||||
[HTTPS]: #services-open-webui-options-shb.open-webui.ssl
|
|
||||||
[Nginx block]: blocks-nginx.html
|
|
||||||
[SSL block]: blocks-ssl.html
|
|
||||||
[secrets contract]: contracts-secret.html
|
|
||||||
|
|
||||||
::: {.note}
|
|
||||||
The service does not use state so no backup or impermanence integration is provided.
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Usage {#services-homepage-usage}
|
|
||||||
|
|
||||||
The following snippet assumes a few blocks have been setup already:
|
|
||||||
|
|
||||||
- the [secrets block](usage.html#usage-secrets) with SOPS,
|
|
||||||
- the [`shb.ssl` block](blocks-ssl.html#usage),
|
|
||||||
- the [`shb.lldap` block](blocks-lldap.html#blocks-lldap-global-setup).
|
|
||||||
- the [`shb.authelia` block](blocks-authelia.html#blocks-sso-global-setup).
|
|
||||||
|
|
||||||
::: {.note}
|
|
||||||
Part of the configuration is done through the `shb.homepage` option described here
|
|
||||||
and the rest is done through the upstream [`services.homepage-dashboard`][] option.
|
|
||||||
:::
|
|
||||||
|
|
||||||
[`services.homepage-dashboard`]: https://search.nixos.org/options?query=services.homepage-dashboard
|
|
||||||
|
|
||||||
### Main service configuration {#services-homepage-usage-main}
|
|
||||||
|
|
||||||
This part sets up the web UI and its integration with the other SHB services.
|
|
||||||
It also creates the various service groups which will hold each service.
|
|
||||||
The names are arbitrary and you can order them as you wish through the `sortOrder` option.
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.certs.certs.letsencrypt.${domain}.extraDomains = [
|
|
||||||
"${config.shb.homepage.subdomain}.${config.shb.homepage.domain}"
|
|
||||||
];
|
|
||||||
shb.homepage = {
|
|
||||||
enable = true;
|
|
||||||
subdomain = "home";
|
|
||||||
inherit domain;
|
|
||||||
ssl = config.shb.certs.certs.letsencrypt.${domain};
|
|
||||||
|
|
||||||
sso = {
|
|
||||||
enable = true;
|
|
||||||
authEndpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
|
||||||
};
|
|
||||||
|
|
||||||
servicesGroups = {
|
|
||||||
Home.sortOrder = 1;
|
|
||||||
Documents.sortOrder = 2;
|
|
||||||
Finance.sortOrder = 3;
|
|
||||||
Media.sortOrder = 4;
|
|
||||||
Admin.sortOrder = 5;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
services.homepage-dashboard = {
|
|
||||||
settings = {
|
|
||||||
statusStyle = "dot";
|
|
||||||
disableIndexing = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
widgets = [
|
|
||||||
{
|
|
||||||
datetime = {
|
|
||||||
locale = "fr";
|
|
||||||
format = {
|
|
||||||
dateStyle = "long";
|
|
||||||
timeStyle = "long";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The [Homepage LDAP user group][] is created automatically and users can be added declaratively to the group with:.
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.lldap.ensureUsers.${user}.groups = [
|
|
||||||
config.shb.homepage.ldap.userGroup
|
|
||||||
];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Display SHB service {#services-homepage-usage-service}
|
|
||||||
|
|
||||||
A service consumer of the dashboard contract provides a `dashboard` option that can be used like so:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.homepage.servicesGroups.Media.services.Jellyfin = {
|
|
||||||
sortOrder = 2;
|
|
||||||
dashboard.request = config.shb.jellyfin.dashboard.request;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
By default:
|
|
||||||
|
|
||||||
- The `serviceName` option comes from the attr name, here `Jellyfin`.
|
|
||||||
- The `icon` option comes from applying `toLower` on the attr name.
|
|
||||||
- The `siteMonitor` option is set only if `internalUrl` is set.
|
|
||||||
|
|
||||||
They can be overridden by setting them in the [settings](#services-homepage-options-shb.homepage.servicesGroups._name_.services._name_.settings option) (see option documentation for examples):
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.homepage.servicesGroups.Media.services.Jellyfin = {
|
|
||||||
sortOrder = 2;
|
|
||||||
dashboard.request = config.shb.<service>.dashboard.request;
|
|
||||||
settings = {
|
|
||||||
// custom options here.
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Secrets can be randomly generated with `nix run nixpkgs#openssl -- rand -hex 64`.
|
|
||||||
|
|
||||||
### Display custom service {#services-homepage-usage-custom}
|
|
||||||
|
|
||||||
To display a service that does not provide a `dashboard` option like in the previous section, set the values of the request manually:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.homepage.servicesGroups.Media.services.Jellyfin = {
|
|
||||||
sortOrder = 2;
|
|
||||||
dashboard.request = {
|
|
||||||
externalUrl = "https://jellyfin.example.com";
|
|
||||||
internalUrl = "http://127.0.0.1:8081";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Add API key for widget {#services-homepage-usage-widget}
|
|
||||||
|
|
||||||
For services [supporting a widget](https://gethomepage.dev/widgets/),
|
|
||||||
create an API key through the service's web UI if available
|
|
||||||
then store it securely (using SOPS for example) and provide it through the
|
|
||||||
`apiKey` option:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.homepage.servicesGroups.Media.services.Jellyfin = {
|
|
||||||
sortOrder = 1;
|
|
||||||
dashboard.request = config.shb.jellyfin.dashboard.request;
|
|
||||||
apiKey.result = config.shb.sops.secret."jellyfin/homepageApiKey".result;
|
|
||||||
};
|
|
||||||
shb.sops.secret."jellyfin/homepageApiKey".request =
|
|
||||||
config.shb.homepage.servicesGroups.Media.services.Jellyfin.apiKey.request;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Unfortunately creating API keys declaratively is rarely supported by upstream services.
|
|
||||||
|
|
||||||
## Options Reference {#services-homepage-options}
|
|
||||||
|
|
||||||
```{=include=} options
|
|
||||||
id-prefix: services-homepage-options-
|
|
||||||
list-id: selfhostblocks-service-homepage-options
|
|
||||||
source: @OPTIONS_JSON@
|
|
||||||
```
|
|
||||||
|
|
@ -144,22 +144,6 @@ in
|
||||||
default = 2283;
|
default = 2283;
|
||||||
};
|
};
|
||||||
|
|
||||||
publicProxyEnable = mkOption {
|
|
||||||
description = ''
|
|
||||||
Enable Immich Public Proxy service for sharing media publically.
|
|
||||||
'';
|
|
||||||
type = bool;
|
|
||||||
default = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
publicProxyPort = mkOption {
|
|
||||||
description = ''
|
|
||||||
Port under which Immich Public Proxy will listen.
|
|
||||||
'';
|
|
||||||
type = port;
|
|
||||||
default = 2284;
|
|
||||||
};
|
|
||||||
|
|
||||||
ssl = mkOption {
|
ssl = mkOption {
|
||||||
description = "Path to SSL files";
|
description = "Path to SSL files";
|
||||||
type = nullOr shb.contracts.ssl.certs;
|
type = nullOr shb.contracts.ssl.certs;
|
||||||
|
|
@ -465,20 +449,6 @@ in
|
||||||
default = false;
|
default = false;
|
||||||
example = true;
|
example = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
dashboard = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Dashboard contract consumer
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.dashboard.mkRequester {
|
|
||||||
externalUrl = "https://${fqdn}";
|
|
||||||
externalUrlText = "https://\${config.shb.immich.subdomain}.\${config.shb.immich.domain}";
|
|
||||||
internalUrl = "http://127.0.0.1:${toString cfg.port}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
config = mkIf cfg.enable {
|
||||||
|
|
@ -505,6 +475,12 @@ in
|
||||||
|
|
||||||
# Database configuration defaults to Unix socket /run/postgresql
|
# Database configuration defaults to Unix socket /run/postgresql
|
||||||
|
|
||||||
|
# Database configuration
|
||||||
|
database = {
|
||||||
|
# Disable pgvecto.rs, as it was deprecated before SHB integration
|
||||||
|
enableVectors = false;
|
||||||
|
};
|
||||||
|
|
||||||
# Machine learning configuration
|
# Machine learning configuration
|
||||||
machine-learning = mkIf cfg.machineLearning.enable {
|
machine-learning = mkIf cfg.machineLearning.enable {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
@ -526,12 +502,6 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
services.immich-public-proxy = mkIf (cfg.publicProxyEnable) {
|
|
||||||
enable = true;
|
|
||||||
port = cfg.publicProxyPort;
|
|
||||||
immichUrl = "https://${fqdn}";
|
|
||||||
};
|
|
||||||
|
|
||||||
# Create basic directories for Immich
|
# Create basic directories for Immich
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
"d /var/lib/immich 0700 immich immich"
|
"d /var/lib/immich 0700 immich immich"
|
||||||
|
|
@ -580,8 +550,6 @@ in
|
||||||
resources = [
|
resources = [
|
||||||
"^/api.*"
|
"^/api.*"
|
||||||
"^/.well-known/immich"
|
"^/.well-known/immich"
|
||||||
"^/share.*"
|
|
||||||
"^/_app/immutable/.*"
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
|
@ -604,15 +572,9 @@ in
|
||||||
];
|
];
|
||||||
|
|
||||||
# Allow large uploads from mobile app
|
# Allow large uploads from mobile app
|
||||||
services.nginx.virtualHosts."${fqdn}" = {
|
services.nginx.virtualHosts."${fqdn}".extraConfig = ''
|
||||||
extraConfig = ''
|
client_max_body_size 50G;
|
||||||
client_max_body_size 50G;
|
'';
|
||||||
'';
|
|
||||||
locations."^~ /share" = {
|
|
||||||
recommendedProxySettings = true;
|
|
||||||
proxyPass = "http://127.0.0.1:${toString cfg.publicProxyPort}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# Ensure services start in correct order
|
# Ensure services start in correct order
|
||||||
systemd.services.immich-server = {
|
systemd.services.immich-server = {
|
||||||
|
|
|
||||||
|
|
@ -13,38 +13,31 @@ let
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
|
|
||||||
jellyfin = pkgs.buildDotnetModule rec {
|
jellyfin-cli = pkgs.buildDotnetModule rec {
|
||||||
pname = "jellyfin";
|
pname = "jellyfin-cli";
|
||||||
version = "10.11.6";
|
version = "10.10.7";
|
||||||
|
|
||||||
src = pkgs.fetchFromGitHub {
|
src = pkgs.fetchFromGitHub {
|
||||||
owner = "ibizaman";
|
owner = "ibizaman";
|
||||||
repo = "jellyfin";
|
repo = "jellyfin";
|
||||||
rev = "c58ca41d9ee76d137be788cd6f2d089e288ad561";
|
rev = "0b1a5d929960f852dba90c1fc36f3a19dc094f8d";
|
||||||
hash = "sha256-gTHsz5qRT+9FjAqBb4hDBkHChYDU52snBWu6cQb10i4=";
|
hash = "sha256-H9V65+886EYMn/xDEgmxvoEOrbZaI1wSfmkN9vAzGhw=";
|
||||||
};
|
};
|
||||||
|
|
||||||
propagatedBuildInputs = [ pkgs.sqlite ];
|
propagatedBuildInputs = [ pkgs.sqlite ];
|
||||||
|
|
||||||
projectFile = "Jellyfin.Server/Jellyfin.Server.csproj";
|
projectFile = "Jellyfin.Cli/Jellyfin.Cli.csproj";
|
||||||
executables = [ "jellyfin" ];
|
executables = [ "jellyfin-cli" ];
|
||||||
nugetDeps = "${pkgs.path}/pkgs/by-name/je/jellyfin/nuget-deps.json";
|
nugetDeps = "${pkgs.path}/pkgs/by-name/je/jellyfin/nuget-deps.json";
|
||||||
runtimeDeps = [
|
runtimeDeps = [
|
||||||
pkgs.jellyfin-ffmpeg
|
pkgs.jellyfin-ffmpeg
|
||||||
pkgs.fontconfig
|
pkgs.fontconfig
|
||||||
pkgs.freetype
|
pkgs.freetype
|
||||||
];
|
];
|
||||||
dotnet-sdk = pkgs.dotnetCorePackages.sdk_9_0;
|
dotnet-sdk = pkgs.dotnetCorePackages.sdk_8_0;
|
||||||
dotnet-runtime = pkgs.dotnetCorePackages.aspnetcore_9_0;
|
dotnet-runtime = pkgs.dotnetCorePackages.aspnetcore_8_0;
|
||||||
dotnetBuildFlags = [ "--no-self-contained" ];
|
dotnetBuildFlags = [ "--no-self-contained" ];
|
||||||
|
|
||||||
makeWrapperArgs = [
|
|
||||||
"--append-flags"
|
|
||||||
"--ffmpeg=${pkgs.jellyfin-ffmpeg}/bin/ffmpeg"
|
|
||||||
"--append-flags"
|
|
||||||
"--webdir=${pkgs.jellyfin-web}/share/jellyfin-web"
|
|
||||||
];
|
|
||||||
|
|
||||||
passthru.tests = {
|
passthru.tests = {
|
||||||
smoke-test = pkgs.nixosTests.jellyfin;
|
smoke-test = pkgs.nixosTests.jellyfin;
|
||||||
};
|
};
|
||||||
|
|
@ -60,24 +53,10 @@ let
|
||||||
purcell
|
purcell
|
||||||
jojosch
|
jojosch
|
||||||
];
|
];
|
||||||
mainProgram = "jellyfin";
|
mainProgram = "jellyfin-cli";
|
||||||
platforms = dotnet-runtime.meta.platforms;
|
platforms = dotnet-runtime.meta.platforms;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
pluginName =
|
|
||||||
src:
|
|
||||||
let
|
|
||||||
meta = builtins.fromJSON (builtins.readFile "${src}/meta.json");
|
|
||||||
in
|
|
||||||
"${meta.name}_${meta.version}";
|
|
||||||
|
|
||||||
pluginNamePrefix =
|
|
||||||
src:
|
|
||||||
let
|
|
||||||
meta = builtins.fromJSON (builtins.readFile "${src}/meta.json");
|
|
||||||
in
|
|
||||||
"${meta.name}";
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.shb.jellyfin = {
|
options.shb.jellyfin = {
|
||||||
|
|
@ -140,30 +119,6 @@ in
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
plugins = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Install plugins declaratively.
|
|
||||||
|
|
||||||
The LDAP and SSO plugins will be added if their respective
|
|
||||||
shb.jellyfin.ldap.enable and shb.jellyfin.sso.enable options are set to true.
|
|
||||||
|
|
||||||
The interface for plugin creation is WIP.
|
|
||||||
Feel free to add yours following the examples from the LDAP and SSO plugins
|
|
||||||
but know that they may require some tweaks later on.
|
|
||||||
Notably, configuration is not yet handled by this option
|
|
||||||
so that will be added in the future.
|
|
||||||
|
|
||||||
Each plugin's meta.json must be writeable because Jellyfin appends some information
|
|
||||||
upon installing the plugin, like its active or disabled status.
|
|
||||||
SHB automatically enables the plugin
|
|
||||||
and deletes any plugin with the same prefix but other versions.
|
|
||||||
Note that SHB does not attempt to find which version is latest.
|
|
||||||
If twice the same plugin is added, the last one in the "plugins" list wins.
|
|
||||||
'';
|
|
||||||
default = [ ];
|
|
||||||
type = types.listOf types.package;
|
|
||||||
};
|
|
||||||
|
|
||||||
ldap = lib.mkOption {
|
ldap = lib.mkOption {
|
||||||
description = "LDAP configuration.";
|
description = "LDAP configuration.";
|
||||||
default = { };
|
default = { };
|
||||||
|
|
@ -171,17 +126,6 @@ in
|
||||||
options = {
|
options = {
|
||||||
enable = lib.mkEnableOption "LDAP";
|
enable = lib.mkEnableOption "LDAP";
|
||||||
|
|
||||||
plugin = lib.mkOption {
|
|
||||||
type = lib.types.package;
|
|
||||||
description = "Pluging used for LDAP authentication.";
|
|
||||||
default = shb.mkJellyfinPlugin (rec {
|
|
||||||
pname = "jellyfin-plugin-ldapauth";
|
|
||||||
version = "22";
|
|
||||||
url = "https://github.com/jellyfin/${pname}/releases/download/v${version}/ldap-authentication_${version}.0.0.0.zip";
|
|
||||||
hash = "sha256-m2oD9woEuoSRiV9OeifAxZN7XQULMKS0Yq4TF+LjjpI=";
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
host = lib.mkOption {
|
host = lib.mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
description = "Host serving the LDAP server.";
|
description = "Host serving the LDAP server.";
|
||||||
|
|
@ -234,17 +178,6 @@ in
|
||||||
options = {
|
options = {
|
||||||
enable = lib.mkEnableOption "SSO";
|
enable = lib.mkEnableOption "SSO";
|
||||||
|
|
||||||
plugin = lib.mkOption {
|
|
||||||
type = lib.types.package;
|
|
||||||
description = "Pluging used for SSO authentication.";
|
|
||||||
default = shb.mkJellyfinPlugin (rec {
|
|
||||||
pname = "jellyfin-plugin-sso";
|
|
||||||
version = "4.0.0.3";
|
|
||||||
url = "https://github.com/9p4/${pname}/releases/download/v${version}/sso-authentication_${version}.zip";
|
|
||||||
hash = "sha256-Jkuc+Ua7934iSutf/zTY1phTxaltUkfiujOkCi7BW8w=";
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
provider = lib.mkOption {
|
provider = lib.mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
description = "OIDC provider name";
|
description = "OIDC provider name";
|
||||||
|
|
@ -263,6 +196,18 @@ in
|
||||||
default = "jellyfin";
|
default = "jellyfin";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
adminUserGroup = lib.mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "OIDC admin group";
|
||||||
|
default = "jellyfin_admin";
|
||||||
|
};
|
||||||
|
|
||||||
|
userGroup = lib.mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "OIDC user group";
|
||||||
|
default = "jellyfin_user";
|
||||||
|
};
|
||||||
|
|
||||||
authorization_policy = lib.mkOption {
|
authorization_policy = lib.mkOption {
|
||||||
type = types.enum [
|
type = types.enum [
|
||||||
"one_factor"
|
"one_factor"
|
||||||
|
|
@ -311,23 +256,8 @@ in
|
||||||
];
|
];
|
||||||
sourceDirectoriesText = ''
|
sourceDirectoriesText = ''
|
||||||
[
|
[
|
||||||
"services.jellyfin.dataDir"
|
"services.jellyfin.dataDir"
|
||||||
]
|
]'';
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
dashboard = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Dashboard contract consumer
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = types.submodule {
|
|
||||||
options = shb.contracts.dashboard.mkRequester {
|
|
||||||
externalUrl = "https://${fqdn}";
|
|
||||||
externalUrlText = "https://\${config.shb.jellyfin.subdomain}.\${config.shb.jellyfin.domain}";
|
|
||||||
internalUrl = "http://127.0.0.1:${toString cfg.port}";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
@ -340,16 +270,6 @@ in
|
||||||
[ "shb" "jellyfin" "adminPassword" ]
|
[ "shb" "jellyfin" "adminPassword" ]
|
||||||
[ "shb" "jellyfin" "admin" "password" ]
|
[ "shb" "jellyfin" "admin" "password" ]
|
||||||
)
|
)
|
||||||
|
|
||||||
# (lib.mkRenamedOptionModule
|
|
||||||
# [ "shb" "jellyfin" "sso" "userGroup" ]
|
|
||||||
# [ "shb" "jellyfin" "ldap" "userGroup" ]
|
|
||||||
# )
|
|
||||||
|
|
||||||
# (lib.mkRenamedOptionModule
|
|
||||||
# [ "shb" "jellyfin" "sso" "adminUserGroup" ]
|
|
||||||
# [ "shb" "jellyfin" "ldap" "adminGroup" ]
|
|
||||||
# )
|
|
||||||
];
|
];
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
|
|
@ -361,7 +281,6 @@ in
|
||||||
];
|
];
|
||||||
|
|
||||||
services.jellyfin.enable = true;
|
services.jellyfin.enable = true;
|
||||||
services.jellyfin.package = jellyfin;
|
|
||||||
|
|
||||||
networking.firewall = {
|
networking.firewall = {
|
||||||
# from https://jellyfin.org/docs/general/networking/index.html, for auto-discovery
|
# from https://jellyfin.org/docs/general/networking/index.html, for auto-discovery
|
||||||
|
|
@ -552,10 +471,10 @@ in
|
||||||
<EnableAllFolders>true</EnableAllFolders>
|
<EnableAllFolders>true</EnableAllFolders>
|
||||||
<EnabledFolders />
|
<EnabledFolders />
|
||||||
<AdminRoles>
|
<AdminRoles>
|
||||||
<string>${cfg.ldap.adminGroup}</string>
|
<string>${cfg.sso.adminUserGroup}</string>
|
||||||
</AdminRoles>
|
</AdminRoles>
|
||||||
<Roles>
|
<Roles>
|
||||||
<string>${cfg.ldap.userGroup}</string>
|
<string>${cfg.sso.userGroup}</string>
|
||||||
</Roles>
|
</Roles>
|
||||||
<EnableFolderRoles>false</EnableFolderRoles>
|
<EnableFolderRoles>false</EnableFolderRoles>
|
||||||
<FolderRoleMappings />
|
<FolderRoleMappings />
|
||||||
|
|
@ -564,7 +483,6 @@ in
|
||||||
<string>groups</string>
|
<string>groups</string>
|
||||||
</OidScopes>
|
</OidScopes>
|
||||||
<CanonicalLinks />
|
<CanonicalLinks />
|
||||||
<DisablePushedAuthorization>true</DisablePushedAuthorization>
|
|
||||||
</PluginConfiguration>
|
</PluginConfiguration>
|
||||||
</value>
|
</value>
|
||||||
</item>
|
</item>
|
||||||
|
|
@ -678,7 +596,7 @@ in
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
+ lib.strings.optionalString cfg.ldap.enable (
|
+ lib.strings.optionalString cfg.ldap.enable (
|
||||||
(shb.replaceSecretsScript {
|
shb.replaceSecretsScript {
|
||||||
file = ldapConfig;
|
file = ldapConfig;
|
||||||
resultPath = "${config.services.jellyfin.dataDir}/plugins/configurations/LDAP-Auth.xml";
|
resultPath = "${config.services.jellyfin.dataDir}/plugins/configurations/LDAP-Auth.xml";
|
||||||
replacements = [
|
replacements = [
|
||||||
|
|
@ -687,7 +605,7 @@ in
|
||||||
source = cfg.ldap.adminPassword.result.path;
|
source = cfg.ldap.adminPassword.result.path;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
})
|
}
|
||||||
)
|
)
|
||||||
+ lib.strings.optionalString cfg.sso.enable (
|
+ lib.strings.optionalString cfg.sso.enable (
|
||||||
shb.replaceSecretsScript {
|
shb.replaceSecretsScript {
|
||||||
|
|
@ -708,52 +626,12 @@ in
|
||||||
replacements = [
|
replacements = [
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
)
|
|
||||||
+ (
|
|
||||||
let
|
|
||||||
pluginInstallScript = p: ''
|
|
||||||
pluginDir="${config.services.jellyfin.dataDir}/plugins/${pluginName p}"
|
|
||||||
mkdir -p "$pluginDir"
|
|
||||||
for f in "${p}"/*; do
|
|
||||||
ln -sf "$f" "$pluginDir"
|
|
||||||
done
|
|
||||||
|
|
||||||
rm "$pluginDir/meta.json"
|
|
||||||
${pkgs.jq}/bin/jq ". + {
|
|
||||||
status: \"Active\",
|
|
||||||
autoUpdate: false,
|
|
||||||
assemblies: []
|
|
||||||
}" "${p}/meta.json" > "$pluginDir/meta.json"
|
|
||||||
|
|
||||||
echo "Disabling other versions of plugin ${pluginName p}"
|
|
||||||
for p in "${config.services.jellyfin.dataDir}/plugins/${pluginNamePrefix p}"*; do
|
|
||||||
if [ "$p" = "$pluginDir" ]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
echo "Marking plugin $p as disabled"
|
|
||||||
${pkgs.jq}/bin/jq ". + {
|
|
||||||
status: \"Disabled\",
|
|
||||||
}" "$p/meta.json" > "$p/meta.json.new"
|
|
||||||
mv "$p/meta.json.new" "$p/meta.json"
|
|
||||||
done
|
|
||||||
'';
|
|
||||||
in
|
|
||||||
lib.concatMapStringsSep "\n" pluginInstallScript cfg.plugins
|
|
||||||
);
|
);
|
||||||
|
|
||||||
shb.jellyfin.plugins =
|
|
||||||
lib.optionals cfg.ldap.enable [ cfg.ldap.plugin ]
|
|
||||||
++ lib.optionals cfg.sso.enable [ cfg.sso.plugin ];
|
|
||||||
|
|
||||||
systemd.tmpfiles.rules = lib.optionals cfg.ldap.enable [
|
|
||||||
"d '${config.services.jellyfin.dataDir}/plugins' 0750 jellyfin jellyfin - -"
|
|
||||||
];
|
|
||||||
|
|
||||||
systemd.services.jellyfin.serviceConfig.ExecStartPost =
|
systemd.services.jellyfin.serviceConfig.ExecStartPost =
|
||||||
let
|
let
|
||||||
# We must always wait for the service to be fully initialized,
|
# We must always wait for the service to be fully initialized,
|
||||||
# even if we're planning on changing the config and restarting.
|
# even if we're planning on changing the config and restarting.
|
||||||
# And the service is not initialized until this URL returns a 200 and not a 503.
|
|
||||||
waitForCurl = pkgs.writeShellApplication {
|
waitForCurl = pkgs.writeShellApplication {
|
||||||
name = "waitForCurl";
|
name = "waitForCurl";
|
||||||
runtimeInputs = [ pkgs.curl ];
|
runtimeInputs = [ pkgs.curl ];
|
||||||
|
|
@ -794,14 +672,14 @@ in
|
||||||
#
|
#
|
||||||
# If the file does not exist, write the config, create the file then restart.
|
# If the file does not exist, write the config, create the file then restart.
|
||||||
# If the file exists, do nothing and remove the file, resetting the state for the next time.
|
# If the file exists, do nothing and remove the file, resetting the state for the next time.
|
||||||
restartedFile = "${config.services.jellyfin.dataDir}/shb-jellyfin-restarted";
|
restartedFile = "${config.services.jellyfin.dataDir}/.jellyfin-restarted";
|
||||||
|
|
||||||
writeConfig = pkgs.writeShellApplication {
|
writeConfig = pkgs.writeShellApplication {
|
||||||
name = "writeConfig";
|
name = "writeConfig";
|
||||||
runtimeInputs = [ pkgs.systemd ];
|
runtimeInputs = [ pkgs.systemd ];
|
||||||
text = ''
|
text = ''
|
||||||
if ! [ -f "${restartedFile}" ]; then
|
if ! [ -f "${restartedFile}" ]; then
|
||||||
${lib.getExe config.services.jellyfin.package} config \
|
${lib.getExe jellyfin-cli} wizard \
|
||||||
--datadir='${config.services.jellyfin.dataDir}' \
|
--datadir='${config.services.jellyfin.dataDir}' \
|
||||||
--configdir='${config.services.jellyfin.configDir}' \
|
--configdir='${config.services.jellyfin.configDir}' \
|
||||||
--cachedir='${config.services.jellyfin.cacheDir}' \
|
--cachedir='${config.services.jellyfin.cacheDir}' \
|
||||||
|
|
@ -823,7 +701,7 @@ in
|
||||||
rm "${restartedFile}"
|
rm "${restartedFile}"
|
||||||
else
|
else
|
||||||
echo "Restarting jellyfin.service"
|
echo "Restarting jellyfin.service"
|
||||||
echo "This file is used by SelfHostBlocks to know when to restart jellyfin" > "${restartedFile}"
|
touch "${restartedFile}"
|
||||||
systemctl reload-or-restart jellyfin.service
|
systemctl reload-or-restart jellyfin.service
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
|
|
@ -840,7 +718,7 @@ in
|
||||||
|
|
||||||
systemd.services.jellyfin.serviceConfig.TimeoutStartSec = 300;
|
systemd.services.jellyfin.serviceConfig.TimeoutStartSec = 300;
|
||||||
|
|
||||||
shb.authelia.oidcClients = lib.optionals cfg.sso.enable [
|
shb.authelia.oidcClients = lib.lists.optionals (!(isNull cfg.sso)) [
|
||||||
{
|
{
|
||||||
client_id = cfg.sso.clientID;
|
client_id = cfg.sso.clientID;
|
||||||
client_name = "Jellyfin";
|
client_name = "Jellyfin";
|
||||||
|
|
@ -854,15 +732,7 @@ in
|
||||||
require_pkce = true;
|
require_pkce = true;
|
||||||
pkce_challenge_method = "S256";
|
pkce_challenge_method = "S256";
|
||||||
userinfo_signed_response_alg = "none";
|
userinfo_signed_response_alg = "none";
|
||||||
# Jellyfin SSO plugin uses client_secret_post for token exchange
|
|
||||||
token_endpoint_auth_method = "client_secret_post";
|
token_endpoint_auth_method = "client_secret_post";
|
||||||
# Required OIDC scopes for Authelia to return group claims
|
|
||||||
scopes = [
|
|
||||||
"openid"
|
|
||||||
"profile"
|
|
||||||
"email"
|
|
||||||
"groups"
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,33 +5,25 @@ Defined in [`/modules/services/jellyfin.nix`](@REPO@/modules/services/jellyfin.n
|
||||||
This NixOS module is a service that sets up a [Jellyfin](https://jellyfin.org/) instance.
|
This NixOS module is a service that sets up a [Jellyfin](https://jellyfin.org/) instance.
|
||||||
|
|
||||||
Compared to the stock module from nixpkgs,
|
Compared to the stock module from nixpkgs,
|
||||||
this one sets up, in a fully declarative manner:
|
this one sets up, in a fully declarative manner
|
||||||
- the initial wizard with an admin user thanks to a custom Jellyfin CLI
|
the initial wizard with an admin user
|
||||||
and a custom restart logic to apply the changes from the CLI.
|
and LDAP and SSO integration.
|
||||||
- LDAP and SSO integration thanks to a custom declarative installation of plugins.
|
|
||||||
|
|
||||||
## Features {#services-jellyfin-features}
|
## Features {#services-jellyfin-features}
|
||||||
|
|
||||||
- Declarative creation of admin user.
|
- Declarative creation of admin user.
|
||||||
- Declarative selection of listening port.
|
- Declarative selection of listening port.
|
||||||
- Access through [subdomain](#services-jellyfin-options-shb.jellyfin.subdomain)
|
- Access through [subdomain](#services-jellyfin-options-shb.jellyfin.subdomain) using reverse proxy. [Manual](#services-jellyfin-usage-configuration).
|
||||||
and [HTTPS](#services-jellyfin-options-shb.jellyfin.ssl) using reverse proxy. [Manual](#services-jellyfin-usage).
|
- Access through [HTTPS](#services-jellyfin-options-shb.jellyfin.ssl) using reverse proxy. [Manual](#services-jellyfin-usage-https).
|
||||||
- Declarative plugin installation. [Manual](#services-jellyfin-options-shb.jellyfin.plugins).
|
- Declarative [LDAP](#services-jellyfin-options-shb.jellyfin.ldap) configuration. [Manual](#services-jellyfin-usage-ldap).
|
||||||
- Declarative [LDAP](#services-jellyfin-options-shb.jellyfin.ldap) configuration.
|
- Declarative [SSO](#services-jellyfin-options-shb.jellyfin.sso) configuration. [Manual](#services-jellyfin-usage-sso).
|
||||||
- Declarative [SSO](#services-jellyfin-options-shb.jellyfin.sso) configuration.
|
|
||||||
- [Backup](#services-jellyfin-options-shb.jellyfin.backup) through the [backup block](./blocks-backup.html). [Manual](#services-jellyfin-usage-backup).
|
- [Backup](#services-jellyfin-options-shb.jellyfin.backup) through the [backup block](./blocks-backup.html). [Manual](#services-jellyfin-usage-backup).
|
||||||
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-jellyfin-usage-applicationdashboard)
|
|
||||||
|
|
||||||
## Usage {#services-jellyfin-usage}
|
## Usage {#services-jellyfin-usage}
|
||||||
|
|
||||||
### Initial Configuration {#services-jellyfin-usage-configuration}
|
### Initial Configuration {#services-jellyfin-usage-configuration}
|
||||||
|
|
||||||
The following snippet assumes a few blocks have been setup already:
|
The following snippet enables Jellyfin and makes it available under the `jellyfin.example.com` endpoint.
|
||||||
|
|
||||||
- the [secrets block](usage.html#usage-secrets) with SOPS,
|
|
||||||
- the [`shb.ssl` block](blocks-ssl.html#usage),
|
|
||||||
- the [`shb.lldap` block](blocks-lldap.html#blocks-lldap-global-setup).
|
|
||||||
- the [`shb.authelia` block](blocks-authelia.html#blocks-sso-global-setup).
|
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
shb.jellyfin = {
|
shb.jellyfin = {
|
||||||
|
|
@ -41,60 +33,122 @@ shb.jellyfin = {
|
||||||
|
|
||||||
admin = {
|
admin = {
|
||||||
username = "admin";
|
username = "admin";
|
||||||
password.result = config.shb.sops.secret."jellyfin/adminPassword".result;
|
password.result = config.shb.sops.secret.jellyfinAdminPassword.result;
|
||||||
};
|
|
||||||
|
|
||||||
ldap = {
|
|
||||||
enable = true;
|
|
||||||
host = "127.0.0.1";
|
|
||||||
port = config.shb.lldap.ldapPort;
|
|
||||||
dcdomain = config.shb.lldap.dcdomain;
|
|
||||||
adminPassword.result = config.shb.sops.secret."jellyfin/ldap/adminPassword".result
|
|
||||||
};
|
|
||||||
|
|
||||||
sso = {
|
|
||||||
enable = true;
|
|
||||||
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
|
||||||
|
|
||||||
secretFile = config.shb.sops.secret."jellyfin/sso_secret".result;
|
|
||||||
secretFileForAuthelia = config.shb.sops.secret."jellyfin/authelia/sso_secret".result;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.sops.secret."jellyfin/adminPassword".request = config.shb.jellyfin.admin.password.request;
|
shb.sops.secret.jellyfinAdminPassword.request = config.shb.jellyfin.admin.password.request;
|
||||||
|
```
|
||||||
|
|
||||||
shb.sops.secret."jellyfin/ldap/adminPassword".request = config.shb.jellyfin.ldap.adminPassword.request;
|
This assumes secrets are setup with SOPS
|
||||||
|
as mentioned in [the secrets setup section](usage.html#usage-secrets) of the manual.
|
||||||
|
|
||||||
shb.sops.secret."jellyfin/sso_secret".request = config.shb.jellyfin.sso.sharedSecret.request;
|
### Jellyfin through HTTPS {#services-jellyfin-usage-https}
|
||||||
shb.sops.secret."jellyfin/authelia/sso_secret" = {
|
|
||||||
request = config.shb.jellyfin.sso.sharedSecretForAuthelia.request;
|
:::: {.note}
|
||||||
settings.key = "jellyfin/sso_secret";
|
We will build upon the [Initial Configuration](#services-jellyfin-usage-configuration) section,
|
||||||
|
so please follow that first.
|
||||||
|
::::
|
||||||
|
|
||||||
|
If the `shb.ssl` block is used (see [manual](blocks-ssl.html#usage) on how to set it up),
|
||||||
|
the instance will be reachable at `https://jellyfin.example.com`.
|
||||||
|
|
||||||
|
Here is an example with Let's Encrypt certificates, validated using the HTTP method.
|
||||||
|
First, set the global configuration for your domain:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
shb.certs.certs.letsencrypt."example.com" = {
|
||||||
|
domain = "example.com";
|
||||||
|
group = "nginx";
|
||||||
|
reloadServices = [ "nginx.service" ];
|
||||||
|
adminEmail = "myemail@mydomain.com";
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Secrets can be randomly generated with `nix run nixpkgs#openssl -- rand -hex 64`.
|
Then you can tell Jellyfin to use those certificates.
|
||||||
|
|
||||||
The [user](#services-jellyfin-options-shb.jellyfin.ldap.userGroup)
|
```nix
|
||||||
and [admin](#services-jellyfin-options-shb.jellyfin.ldap.adminGroup)
|
shb.certs.certs.letsencrypt."example.com".extraDomains = [ "jellyfin.example.com" ];
|
||||||
LDAP groups are created automatically.
|
|
||||||
|
shb.jellyfin = {
|
||||||
|
ssl = config.shb.certs.certs.letsencrypt."example.com";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### With LDAP Support {#services-jellyfin-usage-ldap}
|
||||||
|
|
||||||
|
:::: {.note}
|
||||||
|
We will build upon the [HTTPS](#services-jellyfin-usage-https) section,
|
||||||
|
so please follow that first.
|
||||||
|
::::
|
||||||
|
|
||||||
|
We will use the [LLDAP block][] provided by Self Host Blocks.
|
||||||
|
Assuming it [has been set already][LLDAP block setup], add the following configuration:
|
||||||
|
|
||||||
|
[LLDAP block]: blocks-lldap.html
|
||||||
|
[LLDAP block setup]: blocks-lldap.html#blocks-lldap-global-setup
|
||||||
|
|
||||||
|
```nix
|
||||||
|
shb.jellyfin.ldap
|
||||||
|
enable = true;
|
||||||
|
host = "127.0.0.1";
|
||||||
|
port = config.shb.lldap.ldapPort;
|
||||||
|
dcdomain = config.shb.lldap.dcdomain;
|
||||||
|
adminPassword.result = config.shb.sops.secrets."jellyfin/ldap/adminPassword".result
|
||||||
|
};
|
||||||
|
|
||||||
|
shb.sops.secrets."jellyfin/ldap/adminPassword" = {
|
||||||
|
request = config.shb.jellyfin.ldap.adminPassword.request;
|
||||||
|
settings.key = "ldap/userPassword";
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The `shb.jellyfin.ldap.adminPasswordFile` must be the same
|
||||||
|
as the `shb.lldap.ldapUserPasswordFile` which is achieved
|
||||||
|
with the `key` option.
|
||||||
|
The other secrets can be randomly generated with
|
||||||
|
`nix run nixpkgs#openssl -- rand -hex 64`.
|
||||||
|
|
||||||
|
And that's it.
|
||||||
|
Now, go to the LDAP server at `http://ldap.example.com`,
|
||||||
|
create the `jellyfin_user` and `jellyfin_admin` groups,
|
||||||
|
create a user and add it to one or both groups.
|
||||||
|
When that's done, go back to the Jellyfin server at
|
||||||
|
`http://jellyfin.example.com` and login with that user.
|
||||||
|
|
||||||
|
Work is in progress to make the creation of the LDAP user and group declarative too.
|
||||||
|
|
||||||
|
### With SSO Support {#services-jellyfin-usage-sso}
|
||||||
|
|
||||||
|
:::: {.note}
|
||||||
|
We will build upon the [LDAP](#services-jellyfin-usage-ldap) section,
|
||||||
|
so please follow that first.
|
||||||
|
::::
|
||||||
|
|
||||||
|
We will use the [SSO block][] provided by Self Host Blocks.
|
||||||
|
Assuming it [has been set already][SSO block setup], add the following configuration:
|
||||||
|
|
||||||
|
[SSO block]: blocks-sso.html
|
||||||
|
[SSO block setup]: blocks-sso.html#blocks-sso-global-setup
|
||||||
|
|
||||||
|
```nix
|
||||||
|
shb.jellyfin.sso = {
|
||||||
|
enable = true;
|
||||||
|
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
||||||
|
|
||||||
|
secretFile = <path/to/oidcJellyfinSharedSecret>;
|
||||||
|
secretFileForAuthelia = <path/to/oidcJellyfinSharedSecret>;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Passing the `ssl` option will auto-configure nginx to force SSL connections with the given
|
||||||
|
certificate.
|
||||||
|
|
||||||
The `shb.jellyfin.sso.secretFile` and `shb.jellyfin.sso.secretFileForAuthelia` options
|
The `shb.jellyfin.sso.secretFile` and `shb.jellyfin.sso.secretFileForAuthelia` options
|
||||||
must have the same content. The former is a file that must be owned by the `jellyfin` user while
|
must have the same content. The former is a file that must be owned by the `jellyfin` user while
|
||||||
the latter must be owned by the `authelia` user. I want to avoid needing to define the same secret
|
the latter must be owned by the `authelia` user. I want to avoid needing to define the same secret
|
||||||
twice with a future secrets SHB block.
|
twice with a future secrets SHB block.
|
||||||
|
|
||||||
### Certificates {#services-jellyfin-certs}
|
|
||||||
|
|
||||||
For Let's Encrypt certificates, add:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.certs.certs.letsencrypt.${domain}.extraDomains = [
|
|
||||||
"${config.shb.jellyfin.subdomain}.${config.shb.jellyfin.domain}"
|
|
||||||
];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backup {#services-jellyfin-usage-backup}
|
### Backup {#services-jellyfin-usage-backup}
|
||||||
|
|
||||||
Backing up Jellyfin using the [Restic block](blocks-restic.html) is done like so:
|
Backing up Jellyfin using the [Restic block](blocks-restic.html) is done like so:
|
||||||
|
|
@ -115,56 +169,6 @@ You can define any number of Restic instances to backup Jellyfin multiple times.
|
||||||
You will then need to configure more options like the `repository`,
|
You will then need to configure more options like the `repository`,
|
||||||
as explained in the [restic](blocks-restic.html) documentation.
|
as explained in the [restic](blocks-restic.html) documentation.
|
||||||
|
|
||||||
### Impermanence {#services-jellyfin-impermanence}
|
|
||||||
|
|
||||||
To save the data folder in an impermanence setup, add:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.zfs.datasets."safe/jellyfin".path = config.shb.jellyfin.impermanence;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Declarative LDAP {#services-jellyfin-declarative-ldap}
|
|
||||||
|
|
||||||
To add a user `USERNAME` to the user and admin groups for jellyfin, add:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
shb.lldap.ensureUsers.USERNAME.groups = [
|
|
||||||
config.shb.jellyfin.ldap.userGroup
|
|
||||||
config.shb.jellyfin.ldap.adminGroup
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
### Application Dashboard {#services-jellyfin-usage-applicationdashboard}
|
|
||||||
|
|
||||||
Integration with the [dashboard contract](contracts-dashboard.html) is provided
|
|
||||||
by the [dashboard option](#services-jellyfin-options-shb.jellyfin.dashboard).
|
|
||||||
|
|
||||||
For example using the [Homepage](services-homepage.html) service:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.homepage.servicesGroups.Media.services.Jellyfin = {
|
|
||||||
sortOrder = 1;
|
|
||||||
dashboard.request = config.shb.jellyfin.dashboard.request;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
An API key can be set to show extra info:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.homepage.servicesGroups.Media.services.Jellyfin = {
|
|
||||||
apiKey.result = config.shb.sops.secret."jellyfin/homepageApiKey".result;
|
|
||||||
};
|
|
||||||
|
|
||||||
shb.sops.secret."jellyfin/homepageApiKey".request =
|
|
||||||
config.shb.homepage.servicesGroups.Media.services.Jellyfin.apiKey.request;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Debug {#services-jellyfin-debug}
|
## Debug {#services-jellyfin-debug}
|
||||||
|
|
||||||
In case of an issue, check the logs for systemd service `jellyfin.service`.
|
In case of an issue, check the logs for systemd service `jellyfin.service`.
|
||||||
|
|
|
||||||
|
|
@ -175,20 +175,6 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
dashboard = lib.mkOption {
|
|
||||||
description = ''
|
|
||||||
Dashboard contract consumer
|
|
||||||
'';
|
|
||||||
default = { };
|
|
||||||
type = lib.types.submodule {
|
|
||||||
options = shb.contracts.dashboard.mkRequester {
|
|
||||||
externalUrl = "https://${cfg.subdomain}.${cfg.domain}";
|
|
||||||
externalUrlText = "https://\${config.shb.karakeep.subdomain}.\${config.shb.karakeep.domain}";
|
|
||||||
internalUrl = "http://127.0.0.1:${toString cfg.port}";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
config = (
|
config = (
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,9 @@ It integrates well with [Ollama][].
|
||||||
- Access through [subdomain](#services-karakeep-options-shb.karakeep.subdomain) using reverse proxy.
|
- Access through [subdomain](#services-karakeep-options-shb.karakeep.subdomain) using reverse proxy.
|
||||||
- Access through [HTTPS](#services-karakeep-options-shb.karakeep.ssl) using reverse proxy.
|
- Access through [HTTPS](#services-karakeep-options-shb.karakeep.ssl) using reverse proxy.
|
||||||
- [Backup](#services-karakeep-options-shb.karakeep.sso) through the [backup block](./blocks-backup.html).
|
- [Backup](#services-karakeep-options-shb.karakeep.sso) through the [backup block](./blocks-backup.html).
|
||||||
- Integration with the [dashboard contract](contracts-dashboard.html) for displaying user facing application in a dashboard. [Manual](#services-karakeep-usage-applicationdashboard)
|
|
||||||
|
|
||||||
## Usage {#services-karakeep-usage}
|
## Usage {#services-karakeep-usage}
|
||||||
|
|
||||||
### Initial Configuration {#services-karakeep-usage-configuration}
|
|
||||||
|
|
||||||
The following snippet assumes a few blocks have been setup already:
|
The following snippet assumes a few blocks have been setup already:
|
||||||
|
|
||||||
- the [secrets block](usage.html#usage-secrets) with SOPS,
|
- the [secrets block](usage.html#usage-secrets) with SOPS,
|
||||||
|
|
@ -69,35 +66,6 @@ The [user](#services-open-webui-options-shb.open-webui.ldap.userGroup)
|
||||||
and [admin](#services-open-webui-options-shb.open-webui.ldap.adminGroup)
|
and [admin](#services-open-webui-options-shb.open-webui.ldap.adminGroup)
|
||||||
LDAP groups are created automatically.
|
LDAP groups are created automatically.
|
||||||
|
|
||||||
### Application Dashboard {#services-karakeep-usage-applicationdashboard}
|
|
||||||
|
|
||||||
Integration with the [dashboard contract](contracts-dashboard.html) is provided
|
|
||||||
by the [dashboard option](#services-karakeep-options-shb.karakeep.dashboard).
|
|
||||||
|
|
||||||
For example using the [Homepage](services-homepage.html) service:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.homepage.servicesGroups.Documents.services.Karakeep = {
|
|
||||||
sortOrder = 3;
|
|
||||||
dashboard.request = config.shb.karakeep.dashboard.request;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
An API key can be set to show extra info:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{
|
|
||||||
shb.homepage.servicesGroups.Documents.services.Karakeep = {
|
|
||||||
apiKey.result = config.shb.sops.secret."karakeep/homepageApiKey".result;
|
|
||||||
};
|
|
||||||
|
|
||||||
shb.sops.secret."karakeep/homepageApiKey".request =
|
|
||||||
config.shb.homepage.servicesGroups.Documents.services.Karakeep.apiKey.request;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Integration with Ollama {#services-karakeep-ollama}
|
## Integration with Ollama {#services-karakeep-ollama}
|
||||||
|
|
||||||
Assuming ollama is enabled, it will be available on port `config.services.ollama.port`.
|
Assuming ollama is enabled, it will be available on port `config.services.ollama.port`.
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue