Compare commits

...
Author SHA1 Message Date
Richie c2b74c926a removed hostPlatform from portal-1
treefmt / nix fmt (pull_request) Successful in 6s
test ebook search / test-ebook-search (pull_request) Successful in 38s
pytest / pytest (pull_request) Successful in 38s
build_systems / build-jeeves (pull_request) Canceled after 10m16s
build_systems / build-portal-1 (pull_request) Canceled after 10m16s
build_systems / build-brain (pull_request) Canceled after 10m21s
build_systems / build-rhapsody-in-green (pull_request) Canceled after 10m22s
build_systems / build-bob (pull_request) Canceled after 10m26s
2026-09-09 07:39:02 -04:00
Richie 211f1efd5c testing x86_v3 2026-09-09 07:39:02 -04:00
Richie a59ccd293d feat(jeeves): enable x86-64 v2 and v3 system features
treefmt / nix fmt (pull_request) Successful in 6s
build_systems / build-portal-1 (pull_request) Successful in 31s
pytest / pytest (pull_request) Successful in 33s
test ebook search / test-ebook-search (pull_request) Successful in 36s
build_systems / build-brain (pull_request) Successful in 53s
build_systems / build-bob (pull_request) Successful in 53s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m5s
build_systems / build-jeeves (pull_request) Successful in 1m8s
treefmt / nix fmt (push) Successful in 5s
build_systems / build-jeeves (push) Successful in 10s
build_systems / build-portal-1 (push) Successful in 22s
pytest / pytest (push) Successful in 32s
test ebook search / test-ebook-search (push) Successful in 34s
build_systems / build-bob (push) Successful in 41s
build_systems / build-brain (push) Successful in 36s
build_systems / build-rhapsody-in-green (push) Successful in 51s
2026-09-08 19:59:14 -04:00
Richie 5514047eda feat(jeeves): share one system closure across builders
treefmt / nix fmt (pull_request) Successful in 5s
build_systems / build-portal-1 (pull_request) Successful in 32s
pytest / pytest (pull_request) Successful in 32s
test ebook search / test-ebook-search (pull_request) Successful in 36s
build_systems / build-brain (pull_request) Successful in 54s
build_systems / build-bob (pull_request) Successful in 55s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m6s
build_systems / build-jeeves (pull_request) Successful in 1m58s
treefmt / nix fmt (push) Successful in 5s
build_systems / build-portal-1 (push) Successful in 24s
pytest / pytest (push) Successful in 32s
build_systems / build-brain (push) Successful in 37s
test ebook search / test-ebook-search (push) Successful in 35s
build_systems / build-bob (push) Successful in 41s
build_systems / build-rhapsody-in-green (push) Successful in 50s
build_systems / build-jeeves (push) Successful in 53s
Reuse a single NixOS container build for all runners while preserving
unique runtime hostnames and existing Gitea registrations.
2026-09-08 19:37:18 -04:00
Richie 810f9a66f6 flake update 09-08-26
treefmt / nix fmt (pull_request) Successful in 5s
build_systems / build-portal-1 (pull_request) Successful in 30s
pytest / pytest (pull_request) Successful in 32s
test ebook search / test-ebook-search (pull_request) Successful in 37s
build_systems / build-brain (pull_request) Successful in 49s
build_systems / build-bob (pull_request) Successful in 55s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m6s
build_systems / build-jeeves (pull_request) Successful in 2m33s
treefmt / nix fmt (push) Successful in 5s
build_systems / build-portal-1 (push) Successful in 24s
pytest / pytest (push) Successful in 32s
build_systems / build-brain (push) Successful in 38s
test ebook search / test-ebook-search (push) Successful in 36s
build_systems / build-bob (push) Successful in 41s
build_systems / build-rhapsody-in-green (push) Successful in 52s
build_systems / build-jeeves (push) Successful in 2m9s
2026-09-08 19:26:26 -04:00
Richie 3aa66c48af removing zerotierone from jeeves
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 39s
build_systems / build-portal-1 (pull_request) Successful in 45s
test ebook search / test-ebook-search (pull_request) Successful in 46s
build_systems / build-brain (pull_request) Successful in 1m1s
build_systems / build-bob (pull_request) Successful in 1m1s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m10s
build_systems / build-jeeves (pull_request) Successful in 2m29s
treefmt / nix fmt (push) Successful in 5s
build_systems / build-portal-1 (push) Successful in 24s
pytest / pytest (push) Successful in 32s
build_systems / build-brain (push) Successful in 37s
build_systems / build-bob (push) Successful in 37s
test ebook search / test-ebook-search (push) Successful in 37s
build_systems / build-rhapsody-in-green (push) Successful in 50s
build_systems / build-jeeves (push) Successful in 2m2s
2026-09-08 18:12:20 -04:00
Richie 97d63eecda add portal-1 to build_system
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 35s
test ebook search / test-ebook-search (pull_request) Successful in 41s
build_systems / build-portal-1 (pull_request) Successful in 57s
build_systems / build-brain (pull_request) Successful in 57s
build_systems / build-bob (pull_request) Successful in 59s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m12s
build_systems / build-jeeves (pull_request) Successful in 2m29s
treefmt / nix fmt (push) Successful in 6s
build_systems / build-portal-1 (push) Successful in 26s
pytest / pytest (push) Successful in 34s
test ebook search / test-ebook-search (push) Successful in 35s
build_systems / build-brain (push) Successful in 39s
build_systems / build-bob (push) Successful in 39s
build_systems / build-rhapsody-in-green (push) Successful in 58s
build_systems / build-jeeves (push) Successful in 2m21s
2026-09-08 16:40:45 -04:00
Richie 1d0ced8fd4 removed pirate-weather from home_assistant
test ebook search / test-ebook-search (push) Successful in 45s
treefmt / nix fmt (pull_request) Successful in 6s
treefmt / nix fmt (push) Successful in 6s
pytest / pytest (pull_request) Successful in 35s
test ebook search / test-ebook-search (pull_request) Successful in 47s
pytest / pytest (push) Successful in 42s
build_systems / build-brain (pull_request) Successful in 59s
build_systems / build-bob (push) Successful in 58s
build_systems / build-bob (pull_request) Successful in 1m1s
build_systems / build-brain (push) Successful in 55s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m15s
build_systems / build-rhapsody-in-green (push) Successful in 1m10s
build_systems / build-jeeves (pull_request) Successful in 2m52s
build_systems / build-jeeves (push) Successful in 2m50s
2026-09-08 16:38:21 -04:00
Richie 3ebead7663 feat(van_weather): remove python van_weather and apscheduler
build_systems / build-rhapsody-in-green (push) Successful in 48s
build_systems / build-jeeves (push) Successful in 2m0s
pytest / pytest (pull_request) Successful in 44s
test ebook search / test-ebook-search (pull_request) Successful in 47s
build_systems / build-brain (pull_request) Successful in 1m0s
build_systems / build-jeeves (pull_request) Successful in 2m59s
treefmt / nix fmt (pull_request) Successful in 6s
build_systems / build-bob (pull_request) Successful in 1m1s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m35s
pytest / pytest (push) Successful in 32s
build_systems / build-bob (push) Successful in 35s
treefmt / nix fmt (push) Successful in 6s
build_systems / build-brain (push) Successful in 8s
test ebook search / test-ebook-search (push) Successful in 32s
2026-09-08 14:44:39 -04:00
Richie 2882960a8e feat(gitea): replace GITEA_TOKEN with JEEVES_BOT_TOKEN in workflows and script
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 32s
build_systems / build-brain (push) Successful in 36s
build_systems / build-rhapsody-in-green (push) Successful in 47s
build_systems / build-jeeves (push) Successful in 1m59s
test ebook search / test-ebook-search (push) Successful in 31s
build_systems / build-bob (push) Successful in 36s
test ebook search / test-ebook-search (pull_request) Successful in 35s
treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 30s
build_systems / build-brain (pull_request) Successful in 47s
build_systems / build-bob (pull_request) Successful in 49s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m0s
build_systems / build-jeeves (pull_request) Successful in 2m15s
2026-08-29 09:38:36 -04:00
Richie bdf67e96f0 feat(monitoring): add Portal-1 and Vultr dashboards
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 30s
test ebook search / test-ebook-search (pull_request) Successful in 34s
build_systems / build-brain (pull_request) Successful in 49s
build_systems / build-jeeves (pull_request) Successful in 2m14s
build_systems / build-bob (pull_request) Successful in 50s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m2s
build_systems / build-brain (push) Successful in 43s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 36s
test ebook search / test-ebook-search (push) Successful in 37s
build_systems / build-bob (push) Successful in 43s
build_systems / build-rhapsody-in-green (push) Successful in 55s
build_systems / build-jeeves (push) Successful in 2m7s
- scrape Portal-1 node metrics over Tailscale
- add a hardened Vultr billing and bandwidth exporter
- provision host health and infrastructure cost dashboards
2026-08-27 17:49:56 -04:00
Richie d95b8b99ce feat(jeeves): centralize firewall configuration
test ebook search / test-ebook-search (pull_request) Successful in 34s
build_systems / build-brain (pull_request) Successful in 48s
build_systems / build-bob (pull_request) Successful in 49s
build_systems / build-jeeves (pull_request) Successful in 2m11s
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 29s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m1s
build_systems / build-bob (push) Successful in 9s
pytest / pytest (push) Successful in 28s
test ebook search / test-ebook-search (push) Successful in 34s
build_systems / build-jeeves (push) Successful in 1m59s
treefmt / nix fmt (push) Successful in 6s
build_systems / build-brain (push) Successful in 33s
build_systems / build-rhapsody-in-green (push) Successful in 46s
- split networking into interface and firewall modules
- define named service ports in one location
- remove scattered service-level firewall rules
2026-08-27 16:59:44 -04:00
Richie 7e6908ef3d feat(networking): migrate host services from ZeroTier to Tailscale
test ebook search / test-ebook-search (pull_request) Successful in 35s
build_systems / build-bob (pull_request) Successful in 52s
build_systems / build-brain (pull_request) Successful in 52s
treefmt / nix fmt (pull_request) Successful in 5s
pytest / pytest (pull_request) Successful in 30s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m3s
build_systems / build-brain (push) Successful in 35s
build_systems / build-bob (push) Successful in 36s
build_systems / build-jeeves (pull_request) Successful in 2m20s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 29s
test ebook search / test-ebook-search (push) Successful in 34s
build_systems / build-rhapsody-in-green (push) Successful in 49s
build_systems / build-jeeves (push) Successful in 2m2s
- replace legacy IP addresses with Tailscale MagicDNS names
- route Prometheus scraping and exporter firewall access through Tailscale
- wait for Tailscale before starting Prometheus
- restrict service UI ports to the Tailscale interface
- update PostgreSQL and Transmission access rules
- remove Jeeves ZeroTier configuration and Docker host overrides
2026-08-27 12:25:27 -04:00
Richie dd51e89abf fix(haproxy): use IPv6 wildcard for dual-stack listeners
pytest / pytest (pull_request) Successful in 29s
test ebook search / test-ebook-search (pull_request) Successful in 34s
treefmt / nix fmt (pull_request) Successful in 5s
build_systems / build-rhapsody-in-green (pull_request) Successful in 59s
build_systems / build-bob (pull_request) Successful in 49s
build_systems / build-brain (pull_request) Successful in 49s
build_systems / build-jeeves (pull_request) Successful in 2m17s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 30s
build_systems / build-brain (push) Successful in 35s
test ebook search / test-ebook-search (push) Successful in 34s
build_systems / build-bob (push) Successful in 35s
build_systems / build-rhapsody-in-green (push) Successful in 47s
build_systems / build-jeeves (push) Successful in 2m1s
Bind HTTP, HTTPS, and Gitea SSH to [::] with v4v6 support.
2026-08-27 09:08:18 -04:00
48 changed files with 2304 additions and 583 deletions
+1
View File
@@ -18,6 +18,7 @@ jobs:
- "brain" - "brain"
- "jeeves" - "jeeves"
- "rhapsody-in-green" - "rhapsody-in-green"
- "portal-1"
continue-on-error: true continue-on-error: true
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -19,5 +19,5 @@ jobs:
python -m python.gitea_flake_lock merge python -m python.gitea_flake_lock merge
--repo "${{ github.repository }}" --repo "${{ github.repository }}"
env: env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} JEEVES_BOT_TOKEN: ${{ secrets.JEEVES_BOT_TOKEN }}
GITEA_URL: https://gitea.tmmworkshop.com GITEA_URL: https://gitea.tmmworkshop.com
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
run: nix flake update run: nix flake update
- name: Create or update flake.lock PR - name: Create or update flake.lock PR
env: env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} JEEVES_BOT_TOKEN: ${{ secrets.JEEVES_BOT_TOKEN }}
GITEA_URL: https://gitea.tmmworkshop.com GITEA_URL: https://gitea.tmmworkshop.com
run: >- run: >-
nix develop .#devShells.x86_64-linux.default -c nix develop .#devShells.x86_64-linux.default -c
-3
View File
@@ -10,7 +10,6 @@
"aiounifi", "aiounifi",
"alsa", "alsa",
"apiclient", "apiclient",
"apscheduler",
"archlinux", "archlinux",
"ashift", "ashift",
"asrouter", "asrouter",
@@ -337,8 +336,6 @@
"yubioath", "yubioath",
"yzhang", "yzhang",
"zeroconf", "zeroconf",
"zerotier",
"zerotierone",
"zoxide", "zoxide",
"zram", "zram",
"zstd" "zstd"
+1 -1
View File
@@ -5,7 +5,7 @@
... ...
}: }:
let let
monitoringInterface = "ztwfunumly"; monitoringInterface = "tailscale0";
nodeTextfileDir = "/var/lib/prometheus-node-exporter-textfile"; nodeTextfileDir = "/var/lib/prometheus-node-exporter-textfile";
mkProcessNameTemplate = mkProcessNameTemplate =
+9
View File
@@ -0,0 +1,9 @@
{
nixpkgs.hostPlatform = {
system = "x86_64-linux";
gcc = {
arch = "x86-64-v3";
tune = "generic";
};
};
}
Generated
+18 -18
View File
@@ -28,11 +28,11 @@
}, },
"locked": { "locked": {
"dir": "pkgs/firefox-addons", "dir": "pkgs/firefox-addons",
"lastModified": 1787025780, "lastModified": 1788840136,
"narHash": "sha256-NhyLP9G4DFOn/7aYr7K/D7hWrzEGr5EgUBV+lpdmJ24=", "narHash": "sha256-ej5jnQIfjbw4wwPzy8Y4ntG9F3asdF6YBkLqpYeGUxc=",
"owner": "rycee", "owner": "rycee",
"repo": "nur-expressions", "repo": "nur-expressions",
"rev": "5ad360b6d3cb0aa1b61f9cb27fef113ca9117c37", "rev": "555b23e68256c5abfb8a85a385231830d1d7ad1f",
"type": "gitlab" "type": "gitlab"
}, },
"original": { "original": {
@@ -49,11 +49,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1786999651, "lastModified": 1788651960,
"narHash": "sha256-MTGMFlLDTklsXhCp4r5GXB4VAVadPdalXLvUjd/K7h0=", "narHash": "sha256-v9wJd32eZ2bvhBzVOd7TIjLQd011P7nwOhjKtWlci5I=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "353742587cbaf079b3caee743115d037bc51fea6", "rev": "2c0350c759688177331b8f5242311fae8877bdb3",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -67,11 +67,11 @@
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
}, },
"locked": { "locked": {
"lastModified": 1786867632, "lastModified": 1788860136,
"narHash": "sha256-ez+ubZlA1RtdjCB18a6zJ9M4u8qoPDy08EcnsW5M3Xw=", "narHash": "sha256-MhPMOFV4pVkygWEbQ8t1De/uQ9cWF1u++tRe2L5tG48=",
"owner": "nixos", "owner": "nixos",
"repo": "nixos-hardware", "repo": "nixos-hardware",
"rev": "ff17823245ab9ff7bcae6acf950bd89cba82c38c", "rev": "62173785b9a18c78b4a15aca2623d02bceb9d077",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -96,11 +96,11 @@
}, },
"nixpkgs-master": { "nixpkgs-master": {
"locked": { "locked": {
"lastModified": 1787081018, "lastModified": 1788892992,
"narHash": "sha256-K0uwZBtZsbBigHAMQW7YWti3gPe6a5ct5bcOw5F+Q9Y=", "narHash": "sha256-cIMFh9gyU4/aLeB3JCcsWM3tTAvD9pAq9Smr1Wa8aIU=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "cacac5ac351a010599d9f9d106acfed25a8e4c77", "rev": "dff6994123e257ec9901c271bc2b52e64d7c8f05",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -128,11 +128,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1787001381, "lastModified": 1788752844,
"narHash": "sha256-Ue1Yo8gfHdD4TMtNewhA4tkSYeFqXThju0nCyJc3ALo=", "narHash": "sha256-VaWGJ6+cIYN2erfSecbRV+4ljI185Ty2wUrXyvQbgOw=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "ec2d622de0773551768cf98f3fc50cbcc003b9c5", "rev": "dc5d91f840324650bac8c379428c7037a416959a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -162,11 +162,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1786629091, "lastModified": 1788337237,
"narHash": "sha256-gkig4nPi1CWc4Z50GBsjE4ygSE7hMpl/TwID2an2Cck=", "narHash": "sha256-gkSH8VUtCo6hnysNmb9DbTuDepH2t5pv+QWjP75xKAk=",
"owner": "Mic92", "owner": "Mic92",
"repo": "sops-nix", "repo": "sops-nix",
"rev": "a8627b21b9107c5711c96b84f32a9a4b3d45295f", "rev": "fbf759290e0cb0a98dfc813a4eb7d53ad1dacb57",
"type": "github" "type": "github"
}, },
"original": { "original": {
-1
View File
@@ -20,7 +20,6 @@
ps: with ps; [ ps: with ps; [
alembic alembic
apprise apprise
apscheduler
fastapi fastapi
fastapi-cli fastapi-cli
httpx httpx
-2
View File
@@ -11,7 +11,6 @@ license = "MIT"
dependencies = [ dependencies = [
"alembic", "alembic",
"apprise", "apprise",
"apscheduler",
"beautifulsoup4", "beautifulsoup4",
"bm25s", "bm25s",
"ebooklib", "ebooklib",
@@ -20,7 +19,6 @@ dependencies = [
"httpx", "httpx",
"jinja2", "jinja2",
"pgvector", "pgvector",
"polars",
"psycopg[binary]", "psycopg[binary]",
"pydantic", "pydantic",
"pydantic-settings", "pydantic-settings",
+2 -2
View File
@@ -37,7 +37,7 @@ class RerankConfig(BaseSettings):
model_config = SettingsConfigDict(env_prefix="EBOOK_SEARCH_RERANK_", frozen=True, protected_namespaces=()) model_config = SettingsConfigDict(env_prefix="EBOOK_SEARCH_RERANK_", frozen=True, protected_namespaces=())
enabled: bool = True enabled: bool = True
base_url: str = "http://192.168.90.25:8001" base_url: str = "http://bob:8001"
model: str = "qwen3-reranker-06b" model: str = "qwen3-reranker-06b"
candidates: int = 24 candidates: int = 24
timeout_seconds: float = 30.0 timeout_seconds: float = 30.0
@@ -67,7 +67,7 @@ class EbookSearchConfig(BaseSettings):
) )
chat_model: str = "deepseek-v4-flash" chat_model: str = "deepseek-v4-flash"
answer_enabled: bool = True answer_enabled: bool = True
embedding_base_url: str = "http://192.168.90.25:8000/v1" embedding_base_url: str = "http://bob:8000/v1"
embedding_api_key: str = "not-needed" embedding_api_key: str = "not-needed"
embedding_model: str = "qwen3-embedding-0.6b" embedding_model: str = "qwen3-embedding-0.6b"
embedding_batch_size: int = 32 embedding_batch_size: int = 32
@@ -9,8 +9,6 @@ services:
restart: unless-stopped restart: unless-stopped
ports: ports:
- "${EBOOK_SEARCH_PORT:-8070}:8070" - "${EBOOK_SEARCH_PORT:-8070}:8070"
extra_hosts:
- "jeeves:192.168.90.40"
env_file: env_file:
- ../../../.env - ../../../.env
environment: environment:
+5 -5
View File
@@ -84,13 +84,13 @@ def push_branch(*, branch: str) -> None:
run_cmd(["git", "push", "origin", f"HEAD:{branch}", "--force"]) run_cmd(["git", "push", "origin", f"HEAD:{branch}", "--force"])
def _required_gitea_token() -> str: def _required_jeeves_bot_token() -> str:
"""Read the required Gitea token from the environment.""" """Read the required Gitea token from the environment."""
token = getenv("GITEA_TOKEN") token = getenv("JEEVES_BOT_TOKEN")
if token: if token:
return token return token
msg = "GITEA_TOKEN environment variable is required" msg = "JEEVES_BOT_TOKEN environment variable is required"
raise RuntimeError(msg) raise RuntimeError(msg)
@@ -111,7 +111,7 @@ def update(
owner, repo_name = split_repo_name(repo) owner, repo_name = split_repo_name(repo)
with GiteaClient( with GiteaClient(
base_url=getenv("GITEA_URL", DEFAULT_GITEA_URL), base_url=getenv("GITEA_URL", DEFAULT_GITEA_URL),
token=_required_gitea_token(), token=_required_jeeves_bot_token(),
) as client: ) as client:
pull_request = ensure_flake_lock_pull_request( pull_request = ensure_flake_lock_pull_request(
client, client,
@@ -134,7 +134,7 @@ def merge(
owner, repo_name = split_repo_name(repo) owner, repo_name = split_repo_name(repo)
with GiteaClient( with GiteaClient(
base_url=getenv("GITEA_URL", DEFAULT_GITEA_URL), base_url=getenv("GITEA_URL", DEFAULT_GITEA_URL),
token=_required_gitea_token(), token=_required_jeeves_bot_token(),
) as client: ) as client:
pull_request = find_flake_lock_pull_request(client, owner=owner, repo=repo_name) pull_request = find_flake_lock_pull_request(client, owner=owner, repo=repo_name)
if not pull_request: if not pull_request:
-1
View File
@@ -1 +0,0 @@
"""Van weather service - fetches weather with masked GPS location."""
-293
View File
@@ -1,293 +0,0 @@
"""Van weather service - fetches weather with masked GPS for privacy."""
import logging
from datetime import UTC, datetime
from typing import Annotated, Any
import httpx
import typer
from apscheduler.schedulers.blocking import BlockingScheduler
from tenacity import before_sleep_log, retry, stop_after_attempt, wait_fixed
from python.common import configure_logger
from python.van_weather.models import Config, DailyForecast, HourlyForecast, Weather
# Map Pirate Weather icons to Home Assistant conditions
CONDITION_MAP = {
"clear-day": "sunny",
"clear-night": "clear-night",
"rain": "rainy",
"snow": "snowy",
"sleet": "snowy-rainy",
"wind": "windy",
"fog": "fog",
"cloudy": "cloudy",
"partly-cloudy-day": "partlycloudy",
"partly-cloudy-night": "partlycloudy",
}
logger = logging.getLogger(__name__)
@retry(
stop=stop_after_attempt(3),
wait=wait_fixed(5),
before_sleep=before_sleep_log(logger, logging.WARNING),
reraise=True,
)
def get_ha_state(url: str, token: str, entity_id: str) -> float:
"""Get numeric state from Home Asasistant entity."""
response = httpx.get(
f"{url}/api/states/{entity_id}",
headers={"Authorization": f"Bearer {token}"},
timeout=30,
)
response.raise_for_status()
state = response.json()["state"]
if state in ("unavailable", "unknown"):
error = f"{entity_id} is {state}"
raise ValueError(error)
return float(state)
def parse_daily_forecast(data: dict[str, dict[str, Any]]) -> list[DailyForecast]:
"""Parse daily forecast from Pirate Weather API."""
daily = data.get("daily", {}).get("data", [])
daily_forecasts = []
for day in daily[:8]: # Up to 8 days
time_stamp = day.get("time")
if time_stamp:
date_time = datetime.fromtimestamp(time_stamp, tz=UTC).isoformat()
daily_forecasts.append(
DailyForecast(
date_time=date_time,
condition=CONDITION_MAP.get(day.get("icon", ""), "cloudy"),
temperature=day.get("temperatureHigh"),
templow=day.get("temperatureLow"),
precipitation_probability=day.get("precipProbability"),
moon_phase=day.get("moonPhase"),
wind_gust=day.get("windGust"),
cloud_cover=day.get("cloudCover"),
)
)
return daily_forecasts
def parse_hourly_forecast(data: dict[str, dict[str, Any]]) -> list[HourlyForecast]:
"""Parse hourly forecast from Pirate Weather API."""
hourly = data.get("hourly", {}).get("data", [])
hourly_forecasts = []
for hour in hourly[:48]: # Up to 48 hours
time_stamp = hour.get("time")
if time_stamp:
date_time = datetime.fromtimestamp(time_stamp, tz=UTC).isoformat()
hourly_forecasts.append(
HourlyForecast(
date_time=date_time,
condition=CONDITION_MAP.get(hour.get("icon", ""), "cloudy"),
temperature=hour.get("temperature"),
precipitation_probability=hour.get("precipProbability"),
)
)
return hourly_forecasts
@retry(
stop=stop_after_attempt(3),
wait=wait_fixed(5),
before_sleep=before_sleep_log(logger, logging.WARNING),
reraise=True,
)
def fetch_weather(api_key: str, lat: float, lon: float) -> Weather:
"""Fetch weather from Pirate Weather API."""
url = f"https://api.pirateweather.net/forecast/{api_key}/{lat},{lon}"
response = httpx.get(url, params={"units": "us"}, timeout=30)
response.raise_for_status()
data = response.json()
daily_forecasts = parse_daily_forecast(data)
hourly_forecasts = parse_hourly_forecast(data)
current = data.get("currently", {})
icon = current.get("icon", "")
return Weather(
temperature=current.get("temperature"),
feels_like=current.get("apparentTemperature"),
humidity=current.get("humidity"),
wind_speed=current.get("windSpeed"),
wind_bearing=current.get("windBearing"),
condition=CONDITION_MAP.get(icon, "cloudy"),
summary=current.get("summary"),
pressure=current.get("pressure"),
visibility=current.get("visibility"),
uv_index=current.get("uvIndex"),
ozone=current.get("ozone"),
nearest_storm_distance=current.get("nearestStormDistance"),
nearest_storm_bearing=current.get("nearestStormBearing"),
precip_probability=current.get("precipProbability"),
cloud_cover=current.get("cloudCover"),
daily_forecasts=daily_forecasts,
hourly_forecasts=hourly_forecasts,
)
@retry(
stop=stop_after_attempt(3),
wait=wait_fixed(5),
before_sleep=before_sleep_log(logger, logging.WARNING),
reraise=True,
)
def post_to_ha(url: str, token: str, weather: Weather) -> None:
"""Post weather data to Home Assistant as sensor entities."""
headers = {"Authorization": f"Bearer {token}"}
# Post current weather as individual sensors
sensors = {
"sensor.van_weather_condition": {
"state": weather.condition or "unknown",
"attributes": {"friendly_name": "Van Weather Condition"},
},
"sensor.van_weather_temperature": {
"state": weather.temperature,
"attributes": {"unit_of_measurement": "°F", "device_class": "temperature"},
},
"sensor.van_weather_apparent_temperature": {
"state": weather.feels_like,
"attributes": {"unit_of_measurement": "°F", "device_class": "temperature"},
},
"sensor.van_weather_humidity": {
"state": int((weather.humidity or 0) * 100),
"attributes": {"unit_of_measurement": "%", "device_class": "humidity"},
},
"sensor.van_weather_pressure": {
"state": weather.pressure,
"attributes": {"unit_of_measurement": "mbar", "device_class": "pressure"},
},
"sensor.van_weather_wind_speed": {
"state": weather.wind_speed,
"attributes": {"unit_of_measurement": "mph", "device_class": "wind_speed"},
},
"sensor.van_weather_wind_bearing": {
"state": weather.wind_bearing,
"attributes": {"unit_of_measurement": "°"},
},
"sensor.van_weather_visibility": {
"state": weather.visibility,
"attributes": {"unit_of_measurement": "mi"},
},
"sensor.van_weather_uv_index": {
"state": weather.uv_index,
"attributes": {"friendly_name": "Van Weather UV Index", "icon": "mdi:sun-wireless"},
},
"sensor.van_weather_ozone": {
"state": weather.ozone,
"attributes": {"unit_of_measurement": "DU", "icon": "mdi:earth"},
},
"sensor.van_weather_nearest_storm_distance": {
"state": weather.nearest_storm_distance,
"attributes": {"unit_of_measurement": "mi", "icon": "mdi:weather-lightning"},
},
"sensor.van_weather_nearest_storm_bearing": {
"state": weather.nearest_storm_bearing,
"attributes": {"unit_of_measurement": "°", "icon": "mdi:weather-lightning"},
},
"sensor.van_weather_precip_probability": {
"state": int((weather.precip_probability or 0) * 100),
"attributes": {"unit_of_measurement": "%", "icon": "mdi:weather-rainy"},
},
"sensor.van_weather_cloud_cover": {
"state": int((weather.cloud_cover or 0) * 100),
"attributes": {"unit_of_measurement": "%", "icon": "mdi:weather-cloudy"},
},
}
for entity_id, data in sensors.items():
if data["state"] is not None:
response = httpx.post(f"{url}/api/states/{entity_id}", headers=headers, json=data, timeout=30)
response.raise_for_status()
# Post daily forecast as JSON attribute sensor
daily_forecast = [
{
"datetime": daily_forecast.date_time.isoformat(),
"condition": daily_forecast.condition,
"temperature": daily_forecast.temperature,
"templow": daily_forecast.templow,
"precipitation_probability": int((daily_forecast.precipitation_probability or 0) * 100),
}
for daily_forecast in weather.daily_forecasts
]
response = httpx.post(
f"{url}/api/states/sensor.van_weather_forecast_daily",
headers=headers,
json={"state": len(daily_forecast), "attributes": {"forecast": daily_forecast}},
timeout=30,
)
response.raise_for_status()
# Post hourly forecast as JSON attribute sensor
hourly_forecast = [
{
"datetime": hourly_forecast.date_time.isoformat(),
"condition": hourly_forecast.condition,
"temperature": hourly_forecast.temperature,
"precipitation_probability": int((hourly_forecast.precipitation_probability or 0) * 100),
}
for hourly_forecast in weather.hourly_forecasts
]
response = httpx.post(
f"{url}/api/states/sensor.van_weather_forecast_hourly",
headers=headers,
json={"state": len(hourly_forecast), "attributes": {"forecast": hourly_forecast}},
timeout=30,
)
response.raise_for_status()
def update_weather(config: Config) -> None:
"""Fetch weather using last-known location, post to HA."""
lat = get_ha_state(config.ha_url, config.ha_token, config.lat_entity)
lon = get_ha_state(config.ha_url, config.ha_token, config.lon_entity)
masked_lat = round(lat, config.mask_decimals)
masked_lon = round(lon, config.mask_decimals)
logger.info(f"Masked location: {masked_lat}, {masked_lon}")
weather = fetch_weather(config.pirate_weather_api_key, masked_lat, masked_lon)
logger.info(f"Weather: {weather.temperature}°F, {weather.condition}")
post_to_ha(config.ha_url, config.ha_token, weather)
logger.info("Posted weather to HA")
def main(
ha_url: Annotated[str, typer.Option(envvar="HA_URL")],
ha_token: Annotated[str, typer.Option(envvar="HA_TOKEN")],
api_key: Annotated[str, typer.Option(envvar="PIRATE_WEATHER_API_KEY")],
interval: Annotated[int, typer.Option(help="Poll interval in seconds")] = 900,
log_level: Annotated[str, typer.Option()] = "INFO",
) -> None:
"""Fetch weather for van using masked GPS location."""
configure_logger(log_level)
config = Config(ha_url=ha_url, ha_token=ha_token, pirate_weather_api_key=api_key)
logger.info(f"Starting van weather service, polling every {interval}s")
scheduler = BlockingScheduler()
scheduler.add_job(
update_weather,
"interval",
seconds=interval,
args=[config],
next_run_time=datetime.now(UTC),
)
scheduler.start()
if __name__ == "__main__":
typer.run(main)
-72
View File
@@ -1,72 +0,0 @@
"""Models for van weather service."""
from __future__ import annotations
from datetime import datetime # noqa: TC003 This is required for pydantic
from pydantic import BaseModel, field_serializer
class Config(BaseModel):
"""Service configuration."""
ha_url: str
ha_token: str
pirate_weather_api_key: str
lat_entity: str = "sensor.van_last_known_latitude"
lon_entity: str = "sensor.van_last_known_longitude"
mask_decimals: int = 1 # ~11km accuracy
class DailyForecast(BaseModel):
"""Daily forecast entry."""
date_time: datetime
condition: str | None = None
temperature: float | None = None # High
templow: float | None = None # Low
precipitation_probability: float | None = None
moon_phase: float | None = None
wind_gust: float | None = None
cloud_cover: float | None = None
@field_serializer("date_time")
def serialize_date_time(self, date_time: datetime) -> str:
"""Serialize datetime to ISO format."""
return date_time.isoformat()
class HourlyForecast(BaseModel):
"""Hourly forecast entry."""
date_time: datetime
condition: str | None = None
temperature: float | None = None
precipitation_probability: float | None = None
@field_serializer("date_time")
def serialize_date_time(self, date_time: datetime) -> str:
"""Serialize datetime to ISO format."""
return date_time.isoformat()
class Weather(BaseModel):
"""Weather data from Pirate Weather."""
temperature: float | None = None
feels_like: float | None = None
humidity: float | None = None
wind_speed: float | None = None
wind_bearing: float | None = None
condition: str | None = None
summary: str | None = None
pressure: float | None = None
visibility: float | None = None
uv_index: float | None = None
ozone: float | None = None
nearest_storm_distance: float | None = None
nearest_storm_bearing: float | None = None
precip_probability: float | None = None
cloud_cover: float | None = None
daily_forecasts: list[DailyForecast] = []
hourly_forecasts: list[HourlyForecast] = []
+1 -1
View File
@@ -14,7 +14,7 @@
"${inputs.self}/common/optional/systemd-boot.nix" "${inputs.self}/common/optional/systemd-boot.nix"
"${inputs.self}/common/optional/tailscale.nix" "${inputs.self}/common/optional/tailscale.nix"
"${inputs.self}/common/optional/update.nix" "${inputs.self}/common/optional/update.nix"
"${inputs.self}/common/optional/yubikey.nix" "${inputs.self}/common/optional/x86-64-v3.nix"
"${inputs.self}/common/optional/zfs" "${inputs.self}/common/optional/zfs"
./hardware.nix ./hardware.nix
./syncthing.nix ./syncthing.nix
+1
View File
@@ -9,6 +9,7 @@
"${inputs.self}/common/optional/systemd-boot.nix" "${inputs.self}/common/optional/systemd-boot.nix"
"${inputs.self}/common/optional/tailscale.nix" "${inputs.self}/common/optional/tailscale.nix"
"${inputs.self}/common/optional/update.nix" "${inputs.self}/common/optional/update.nix"
"${inputs.self}/common/optional/x86-64-v3.nix"
"${inputs.self}/common/optional/zfs" "${inputs.self}/common/optional/zfs"
./docker ./docker
./hardware.nix ./hardware.nix
+1 -2
View File
@@ -79,14 +79,13 @@
extraComponents = [ "isal" ]; extraComponents = [ "isal" ];
customComponents = with pkgs.home-assistant-custom-components; [ customComponents = with pkgs.home-assistant-custom-components; [
garmin_connect garmin_connect
pirate-weather
]; ];
}; };
esphome = { esphome = {
enable = true; enable = true;
openFirewall = true; openFirewall = true;
address = "192.168.90.35"; address = "0.0.0.0";
}; };
}; };
} }
+2 -2
View File
@@ -19,7 +19,7 @@
local all richie trust local all richie trust
host all richie 127.0.0.1/32 trust host all richie 127.0.0.1/32 trust
host all richie ::1/128 trust host all richie ::1/128 trust
host all richie 192.168.90.1/24 trust host all richie 100.64.0.0/10 trust
host all richie 192.168.99.1/24 trust host all richie 192.168.99.1/24 trust
local vaninventory vaninventory trust local vaninventory vaninventory trust
@@ -28,7 +28,7 @@
local hass hass trust local hass hass trust
# ipv4 # ipv4
host hass hass 192.168.90.1/24 trust host hass hass 100.64.0.0/10 trust
host hass hass 127.0.0.1/32 trust host hass hass 127.0.0.1/32 trust
# ipv6 # ipv6
+1 -1
View File
@@ -3,7 +3,7 @@
services.syncthing = { services.syncthing = {
overrideFolders = false; overrideFolders = false;
guiAddress = "192.168.90.35:8384"; guiAddress = "0.0.0.0:8384";
settings = { settings = {
"dotfiles" = { "dotfiles" = {
path = "/home/richie/dotfiles"; path = "/home/richie/dotfiles";
+7 -10
View File
@@ -1,4 +1,4 @@
{ inputs, ... }: { inputs, lib, ... }:
let let
vars = import ./vars.nix; vars = import ./vars.nix;
in in
@@ -15,16 +15,21 @@ in
"${inputs.self}/common/optional/syncthing_base.nix" "${inputs.self}/common/optional/syncthing_base.nix"
"${inputs.self}/common/optional/tailscale.nix" "${inputs.self}/common/optional/tailscale.nix"
"${inputs.self}/common/optional/update.nix" "${inputs.self}/common/optional/update.nix"
"${inputs.self}/common/optional/x86-64-v3.nix"
"${inputs.self}/common/optional/zfs" "${inputs.self}/common/optional/zfs"
./monitoring ./monitoring
./docker ./docker
./services ./services
./hardware.nix ./hardware.nix
./networking.nix ./networking
./programs.nix ./programs.nix
./runners ./runners
./syncthing.nix ./syncthing.nix
]; ];
nix.settings.system-features = lib.mkAfter [
"gccarch-x86-64-v2"
"gccarch-x86-64-v3"
];
services = { services = {
openssh.ports = [ 629 ]; openssh.ports = [ 629 ];
@@ -35,14 +40,6 @@ in
path = ./snapshot_config.toml; path = ./snapshot_config.toml;
EnvironmentFile = "${vars.secrets}/services/snapshot_manager"; EnvironmentFile = "${vars.secrets}/services/snapshot_manager";
}; };
zerotierone = {
enable = true;
joinNetworks = [
"e4da7455b2ae64ca"
"a09acf02330d37b9"
];
};
}; };
users.groups = { users.groups = {
@@ -2,9 +2,6 @@ let
vars = import ../vars.nix; vars = import ../vars.nix;
in in
{ {
networking.firewall.allowedTCPPorts = [
8989
];
virtualisation.oci-containers.containers.signal_cli_rest_api = { virtualisation.oci-containers.containers.signal_cli_rest_api = {
image = "bbernhard/signal-cli-rest-api:0.199-dev"; image = "bbernhard/signal-cli-rest-api:0.199-dev";
ports = [ ports = [
+33
View File
@@ -0,0 +1,33 @@
# Monitoring
## Vultr API metrics
The `vultr-exporter` service reads its API token from:
```text
/zfs/storage/secrets/services/vultr-exporter
```
Create the file on Jeeves as root with the following contents:
```text
API_KEY=<Vultr API token>
```
The token needs read access to the Vultr Account and Billing APIs. Unrelated
resource collectors are disabled in the packaged exporter.
Restrict the file to root and ensure the public egress IP used by Jeeves is
allowed for the token in the Vultr API settings:
```console
sudo chown root:root /zfs/storage/secrets/services/vultr-exporter
sudo chmod 600 /zfs/storage/secrets/services/vultr-exporter
```
The exporter listens on `127.0.0.1:9188`; it is scraped by the local
`prometheus-main` service every five minutes and is not exposed through the
host firewall.
Portal-1 exposes its node exporter only through `tailscale0` on port `9100`.
Jeeves reaches it using the Portal-1 Tailscale hostname.
@@ -0,0 +1,798 @@
{
"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": false,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"links": [],
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "short",
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "red",
"value": null
},
{
"color": "green",
"value": 1
}
]
},
"mappings": [
{
"options": {
"0": {
"color": "red",
"index": 1,
"text": "Offline"
},
"1": {
"color": "green",
"index": 0,
"text": "Online"
}
},
"type": "value"
}
]
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 4,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "up{job=\"node\",instance=\"portal-1\"}",
"instant": true,
"legendFormat": "",
"range": false,
"refId": "A"
}
],
"title": "Node Exporter",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 70
},
{
"color": "red",
"value": 90
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 5,
"x": 4,
"y": 0
},
"id": 2,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "100 * (1 - avg(rate(node_cpu_seconds_total{job=\"node\",instance=\"portal-1\",mode=\"idle\"}[5m])))",
"instant": true,
"legendFormat": "",
"range": false,
"refId": "A"
}
],
"title": "CPU Used",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 70
},
{
"color": "red",
"value": 90
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 5,
"x": 9,
"y": 0
},
"id": 3,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "100 * (1 - (node_memory_MemAvailable_bytes{job=\"node\",instance=\"portal-1\"} / node_memory_MemTotal_bytes{job=\"node\",instance=\"portal-1\"}))",
"instant": true,
"legendFormat": "",
"range": false,
"refId": "A"
}
],
"title": "RAM Used",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 70
},
{
"color": "red",
"value": 90
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 5,
"x": 14,
"y": 0
},
"id": 4,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "100 * (1 - (node_filesystem_avail_bytes{job=\"node\",instance=\"portal-1\",mountpoint=\"/\",fstype!=\"\"} / node_filesystem_size_bytes{job=\"node\",instance=\"portal-1\",mountpoint=\"/\",fstype!=\"\"}))",
"instant": true,
"legendFormat": "",
"range": false,
"refId": "A"
}
],
"title": "Root Filesystem Used",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 5,
"w": 5,
"x": 19,
"y": 0
},
"id": 5,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto",
"wideLayout": true
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "node_load1{job=\"node\",instance=\"portal-1\"}",
"instant": true,
"legendFormat": "",
"range": false,
"refId": "A"
}
],
"title": "Load (1m)",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 70
},
{
"color": "red",
"value": 90
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 5
},
"id": 6,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "100 * (1 - avg(rate(node_cpu_seconds_total{job=\"node\",instance=\"portal-1\",mode=\"idle\"}[5m])))",
"instant": false,
"legendFormat": "CPU used",
"range": true,
"refId": "A"
}
],
"title": "CPU Usage",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 70
},
{
"color": "red",
"value": 90
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 5
},
"id": 7,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "100 * (1 - (node_memory_MemAvailable_bytes{job=\"node\",instance=\"portal-1\"} / node_memory_MemTotal_bytes{job=\"node\",instance=\"portal-1\"}))",
"instant": false,
"legendFormat": "RAM used",
"range": true,
"refId": "A"
}
],
"title": "RAM Usage",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 70
},
{
"color": "red",
"value": 90
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 13
},
"id": 8,
"options": {
"cellHeight": "sm",
"showHeader": true,
"sortBy": [
{
"desc": true,
"displayName": "Value"
}
]
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "100 * (1 - (node_filesystem_avail_bytes{job=\"node\",instance=\"portal-1\",fstype!~\"tmpfs|devtmpfs|overlay|squashfs\",mountpoint!~\"/run.*\"} / node_filesystem_size_bytes{job=\"node\",instance=\"portal-1\",fstype!~\"tmpfs|devtmpfs|overlay|squashfs\",mountpoint!~\"/run.*\"}))",
"format": "table",
"instant": true,
"legendFormat": "{{mountpoint}}",
"range": false,
"refId": "A"
}
],
"title": "Filesystem Usage",
"type": "table"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "Bps"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 13
},
"id": 9,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "sum(rate(node_disk_read_bytes_total{job=\"node\",instance=\"portal-1\",device!~\"loop.*|ram.*|fd.*\"}[5m]))",
"instant": false,
"legendFormat": "read",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "sum(rate(node_disk_written_bytes_total{job=\"node\",instance=\"portal-1\",device!~\"loop.*|ram.*|fd.*\"}[5m]))",
"instant": false,
"legendFormat": "write",
"range": true,
"refId": "B"
}
],
"title": "Disk Throughput",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "iops"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 21
},
"id": 10,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "sum(rate(node_disk_reads_completed_total{job=\"node\",instance=\"portal-1\",device!~\"loop.*|ram.*|fd.*\"}[5m]))",
"instant": false,
"legendFormat": "reads",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "sum(rate(node_disk_writes_completed_total{job=\"node\",instance=\"portal-1\",device!~\"loop.*|ram.*|fd.*\"}[5m]))",
"instant": false,
"legendFormat": "writes",
"range": true,
"refId": "B"
}
],
"title": "Disk Operations",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"unit": "Bps"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 21
},
"id": 11,
"options": {
"legend": {
"calcs": [
"lastNotNull"
],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "sum(rate(node_network_receive_bytes_total{job=\"node\",instance=\"portal-1\",device!=\"lo\"}[5m]))",
"instant": false,
"legendFormat": "received",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "prom-main"
},
"editorMode": "code",
"expr": "sum(rate(node_network_transmit_bytes_total{job=\"node\",instance=\"portal-1\",device!=\"lo\"}[5m]))",
"instant": false,
"legendFormat": "sent",
"range": true,
"refId": "B"
}
],
"title": "Network Traffic",
"type": "timeseries"
}
],
"refresh": "30s",
"schemaVersion": 39,
"tags": [
"monitoring",
"portal-1",
"vultr"
],
"templating": {
"list": []
},
"time": {
"from": "now-24h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "Portal-1 Host",
"uid": "portal-1-host",
"version": 1,
"weekStart": ""
}
File diff suppressed because it is too large Load Diff
+21 -10
View File
@@ -41,29 +41,36 @@ let
{ {
job_name = "node"; job_name = "node";
static_configs = [ static_configs = [
(mkTarget "jeeves" "192.168.90.40:9100") (mkTarget "jeeves" "jeeves.taile39c31.ts.net:9100")
(mkTarget "bob" "192.168.90.25:9100") (mkTarget "bob" "bob.taile39c31.ts.net:9100")
(mkTarget "portal-1" "portal-1.taile39c31.ts.net:9100")
]; ];
} }
{
job_name = "vultr";
scrape_interval = "5m";
scrape_timeout = "45s";
static_configs = [ (mkTarget "vultr" "127.0.0.1:9188") ];
}
{ {
job_name = "process_grouped"; job_name = "process_grouped";
static_configs = [ static_configs = [
(mkTarget "jeeves" "192.168.90.40:9256") (mkTarget "jeeves" "jeeves.taile39c31.ts.net:9256")
(mkTarget "bob" "192.168.90.25:9256") (mkTarget "bob" "bob.taile39c31.ts.net:9256")
]; ];
} }
{ {
job_name = "smartctl"; job_name = "smartctl";
static_configs = [ static_configs = [
(mkTarget "jeeves" "192.168.90.40:9633") (mkTarget "jeeves" "jeeves.taile39c31.ts.net:9633")
(mkTarget "bob" "192.168.90.25:9633") (mkTarget "bob" "bob.taile39c31.ts.net:9633")
]; ];
} }
{ {
job_name = "zfs"; job_name = "zfs";
static_configs = [ static_configs = [
(mkTarget "jeeves" "192.168.90.40:9134") (mkTarget "jeeves" "jeeves.taile39c31.ts.net:9134")
(mkTarget "bob" "192.168.90.25:9134") (mkTarget "bob" "bob.taile39c31.ts.net:9134")
]; ];
} }
]; ];
@@ -79,8 +86,8 @@ let
{ {
job_name = "process_pid"; job_name = "process_pid";
static_configs = [ static_configs = [
(mkTarget "jeeves" "192.168.90.40:9257") (mkTarget "jeeves" "jeeves.taile39c31.ts.net:9257")
(mkTarget "bob" "192.168.90.25:9257") (mkTarget "bob" "bob.taile39c31.ts.net:9257")
]; ];
} }
]; ];
@@ -97,7 +104,9 @@ let
after = [ after = [
"zfs-media-database-prometheus.mount" "zfs-media-database-prometheus.mount"
"network.target" "network.target"
"tailscaled-autoconnect.service"
]; ];
wants = [ "tailscaled-autoconnect.service" ];
requires = [ "zfs-media-database-prometheus.mount" ]; requires = [ "zfs-media-database-prometheus.mount" ];
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
unitConfig.RequiresMountsFor = [ dataDir ]; unitConfig.RequiresMountsFor = [ dataDir ];
@@ -151,6 +160,8 @@ let
}; };
in in
{ {
imports = [ ./vultr-exporter.nix ];
users = { users = {
groups.prometheus = { }; groups.prometheus = { };
users.prometheus = { users.prometheus = {
@@ -0,0 +1,96 @@
{
lib,
pkgs,
...
}:
let
vars = import ../vars.nix;
vultrExporter = pkgs.buildGoModule rec {
pname = "vultr-exporter";
version = "0-unstable-2025-10-14";
src = pkgs.fetchFromGitHub {
owner = "DazWilkin";
repo = "vultr-exporter";
rev = "d8b5d7107c2eda31566e6e895c8a67db32fdaf68";
hash = "sha256-3LpXh+mFMBRJfiY2ftELpM3AIE0LiZ9c6NFok0OeH5I=";
};
vendorHash = "sha256-qCHWmg6GxLIjqlf5UqRcuDRd1xqRFI3sl4ZduLajx+Y=";
subPackages = [ "cmd/server" ];
postPatch = ''
substituteInPlace cmd/server/main.go \
--replace-fail 'registry.MustRegister(collector.NewBlockStorageCollector(s, client, log))' "" \
--replace-fail 'registry.MustRegister(collector.NewKubernetesCollector(s, client, log))' "" \
--replace-fail 'registry.MustRegister(collector.NewLoadBalancerCollector(s, client, log))' "" \
--replace-fail 'registry.MustRegister(collector.NewReservedIPsCollector(s, client, log))' ""
'';
ldflags = [
"-s"
"-w"
"-X main.GitCommit=${src.rev}"
"-X main.OSVersion=NixOS"
];
postInstall = ''
mv $out/bin/server $out/bin/vultr-exporter
'';
meta = {
description = "Prometheus exporter for Vultr account, billing, and bandwidth metrics";
homepage = "https://github.com/DazWilkin/vultr-exporter";
license = lib.licenses.asl20;
mainProgram = "vultr-exporter";
};
};
in
{
systemd.services.vultr-exporter = {
description = "Vultr Prometheus exporter";
documentation = [ "https://github.com/DazWilkin/vultr-exporter" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${lib.getExe vultrExporter} --endpoint=127.0.0.1:9188 --path=/metrics";
EnvironmentFile = "${vars.secrets}/services/vultr-exporter";
Restart = "on-failure";
RestartSec = "30s";
CapabilityBoundingSet = [ "" ];
DeviceAllow = [ "" ];
DynamicUser = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
RemoveIPC = true;
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
];
UMask = "0077";
};
};
}
+16
View File
@@ -0,0 +1,16 @@
{
imports = [
./firewall.nix
./interfaces.nix
];
networking = {
hostName = "jeeves";
hostId = "0e15ce35";
useNetworkd = true;
# Without this, unconfigured NICs get a catch-all DHCP network and
# join the LAN uninvited (how the dead atlantic card ended up
# ARP-poisoning 192.168.99.14). Every interface must be configured
# explicitly below.
useDHCP = false;
};
}
+47
View File
@@ -0,0 +1,47 @@
let
ports = {
audiobookshelf = 8000;
ebook_searcher = 8070;
gems = 8002;
gitea_http = 6443;
gitea_ssh = 2223;
grafana = 3000;
jellyfin_http = 8096;
nix_binary_cache = 5000;
nornsight = 8001;
ollama = 11434;
open_webui = 8080;
postgresql = 5432;
share = 8091;
signal_cli = 8989;
syncthing = 8384;
};
in
{
networking.firewall = {
enable = true;
interfaces.br-nix-builder = {
allowedTCPPorts = [ ];
allowedUDPPorts = [ ];
};
interfaces.tailscale0.allowedTCPPorts = [
ports.audiobookshelf
ports.gems
ports.jellyfin_http
ports.nix_binary_cache
ports.nornsight
ports.share
];
allowedTCPPorts = [
ports.ebook_searcher
ports.gitea_http
ports.gitea_ssh
ports.grafana
ports.ollama
ports.open_webui
ports.postgresql
ports.signal_cli
ports.syncthing
];
};
}
@@ -8,36 +8,6 @@
"net.bridge.bridge-nf-call-iptables" = 0; "net.bridge.bridge-nf-call-iptables" = 0;
}; };
networking = {
hostName = "jeeves";
hostId = "0e15ce35";
firewall = {
enable = true;
interfaces.br-nix-builder = {
allowedTCPPorts = [ ];
allowedUDPPorts = [ ];
};
interfaces.tailscale0.allowedTCPPorts = [
5000 # Nix binary cache
8000 # Audiobookshelf
8001 # Norn Sight
8002 # Gems
8091 # Share
8096 # Jellyfin HTTP
];
allowedTCPPorts = [
6443 # Gitea HTTP
8070 # ebook searcher
];
};
useNetworkd = true;
# Without this, unconfigured NICs get a catch-all DHCP network and
# join the LAN uninvited (how the dead atlantic card ended up
# ARP-poisoning 192.168.99.14). Every interface must be configured
# explicitly below.
useDHCP = false;
};
systemd.network = { systemd.network = {
enable = true; enable = true;
wait-online = { wait-online = {
+118 -89
View File
@@ -1,8 +1,7 @@
{ {
config, config,
lib, lib,
outputs, pkgs,
utils,
... ...
}: }:
@@ -12,6 +11,121 @@ let
cfg = config.services.nix_builder; cfg = config.services.nix_builder;
runnerUsername = "gitea-runner"; runnerUsername = "gitea-runner";
runnerUserid = 601; runnerUserid = 601;
runnerLabels = [
"self-hosted:host"
"nixos:host"
];
containerConfig =
{
config,
pkgs,
lib,
...
}:
let
runnerConfigFile = (pkgs.formats.yaml { }).generate "gitea-runner.yaml" { };
registerRunner = pkgs.writeShellApplication {
name = "register-gitea-runner";
runtimeInputs = [
pkgs.coreutils
pkgs.hostname
config.services.gitea-actions-runner.package
];
text = builtins.readFile ./register_gitea_runner.sh;
};
in
{
networking = {
hostName = "nix-builder";
useDHCP = lib.mkDefault true;
interfaces.eth0.useDHCP = true;
# Ensure containers don't inherit the host's stub resolver (127.0.0.53) which was causing issues
useHostResolvConf = false;
};
nix.settings = {
system-features = lib.mkAfter [
"gccarch-x86-64-v2"
"gccarch-x86-64-v3"
];
trusted-substituters = [
"https://cache.nixos.org"
"https://cache.tmmworkshop.com"
"https://nix-community.cachix.org"
];
substituters = [
"https://cache.nixos.org/?priority=2&want-mass-query=true"
"https://cache.tmmworkshop.com/?priority=2&want-mass-query=true"
"https://nix-community.cachix.org/?priority=10&want-mass-query=true"
];
trusted-public-keys = [
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
"cache.tmmworkshop.com:jHffkpgbmEdstQPoihJPYW9TQe6jnQbWR2LqkNGV3iA="
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
];
experimental-features = [
"flakes"
"nix-command"
];
sandbox = true;
allowed-users = [ runnerUsername ];
trusted-users = [
"root"
runnerUsername
];
};
users = {
users.${runnerUsername} = {
isSystemUser = true;
group = runnerUsername;
uid = runnerUserid;
};
groups.${runnerUsername}.gid = runnerUserid;
};
services.gitea-actions-runner.instances.builder = {
enable = true;
name = "jeeves-nix-builder";
url = "http://192.168.99.14:6443/";
labels = runnerLabels;
tokenFile = "/run/secrets/gitea-runners/registration-token";
hostPackages = with pkgs; [
bash
coreutils
curl
gawk
gitMinimal
gnused
my_python
nix
nixfmt
nixos-rebuild
nodejs
treefmt
wget
];
};
systemd.services."gitea-runner-builder" = {
serviceConfig = {
DynamicUser = mkForce false;
User = mkForce runnerUsername;
Group = mkForce runnerUsername;
ExecStartPre = mkForce [
"${getExe registerRunner} builder http://192.168.99.14:6443/ ${runnerConfigFile} ${escapeShellArgs runnerLabels}"
];
};
};
system.stateVersion = "24.05";
};
sharedContainerPath =
(import "${pkgs.path}/nixos/lib/eval-config.nix" {
modules = [
{
boot.isNspawnContainer = true;
nixpkgs.pkgs = pkgs;
}
containerConfig
];
system = null;
}).config.system.build.toplevel;
in in
{ {
options.services.nix_builder = { options.services.nix_builder = {
@@ -48,9 +162,11 @@ in
containers = mapAttrs ( containers = mapAttrs (
name: containerCfg: name: containerCfg:
mkIf containerCfg.enable { mkIf containerCfg.enable {
path = sharedContainerPath;
autoStart = true; autoStart = true;
privateNetwork = true; privateNetwork = true;
hostBridge = cfg.bridgeName; hostBridge = cfg.bridgeName;
extraFlags = [ "--hostname=${name}" ];
bindMounts = { bindMounts = {
host-nix = { host-nix = {
mountPoint = "/host-nix/var/nix/daemon-socket"; mountPoint = "/host-nix/var/nix/daemon-socket";
@@ -68,93 +184,6 @@ in
isReadOnly = false; isReadOnly = false;
}; };
}; };
config =
{
config,
pkgs,
lib,
...
}:
{
networking = {
useDHCP = lib.mkDefault true;
interfaces.eth0.useDHCP = true;
# Ensure containers don't inherit the host's stub resolver (127.0.0.53) which was causing issues
useHostResolvConf = false;
};
nix.settings = {
trusted-substituters = [
"https://cache.nixos.org"
"https://cache.tmmworkshop.com"
"https://nix-community.cachix.org"
];
substituters = [
"https://cache.nixos.org/?priority=2&want-mass-query=true"
"https://cache.tmmworkshop.com/?priority=2&want-mass-query=true"
"https://nix-community.cachix.org/?priority=10&want-mass-query=true"
];
trusted-public-keys = [
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
"cache.tmmworkshop.com:jHffkpgbmEdstQPoihJPYW9TQe6jnQbWR2LqkNGV3iA="
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
];
experimental-features = [
"flakes"
"nix-command"
];
sandbox = true;
allowed-users = [ "gitea-runner" ];
trusted-users = [
"root"
"gitea-runner"
];
};
nixpkgs = {
overlays = builtins.attrValues outputs.overlays;
config.allowUnfree = true;
};
users = {
users.${runnerUsername} = {
isSystemUser = true;
group = runnerUsername;
uid = runnerUserid;
};
groups.${runnerUsername}.gid = runnerUserid;
};
services.gitea-actions-runner.instances.${name} = {
enable = true;
name = "jeeves-${name}";
url = "http://192.168.99.14:6443/";
labels = [
"self-hosted:host"
"nixos:host"
];
tokenFile = "/run/secrets/gitea-runners/registration-token";
hostPackages = with pkgs; [
bash
coreutils
curl
gawk
gitMinimal
gnused
my_python
nix
nixfmt
nixos-rebuild
nodejs
treefmt
wget
];
};
systemd.services."gitea-runner-${utils.escapeSystemdPath name}" = {
serviceConfig = {
DynamicUser = mkForce false;
User = mkForce runnerUsername;
Group = mkForce runnerUsername;
};
};
system.stateVersion = "24.05";
};
} }
) cfg.containers; ) cfg.containers;
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
legacy_instance_dir="$STATE_DIRECTORY/$(hostname)"
instance_dir="$STATE_DIRECTORY/$1"
runner_url="$2"
runner_config_file="$3"
shift 3
runner_labels="$(IFS=,; printf '%s' "$*")"
if [ ! -e "$instance_dir" ] && [ -d "$legacy_instance_dir" ]; then
mv "$legacy_instance_dir" "$instance_dir"
fi
mkdir -vp "$instance_dir"
cd "$instance_dir" || exit 1
token_hash_file="$instance_dir/.token-hash"
token_hash_current="$(printf '%s' "$TOKEN" | sha256sum | cut -d' ' -f1)"
token_hash_stored="$(cat "$token_hash_file" 2>/dev/null || echo "")"
labels_file="$instance_dir/.labels"
labels_wanted="$(printf '%s\n' "$@" | sort)"
labels_current="$(cat "$labels_file" 2>/dev/null || echo 0)"
if [ ! -e "$instance_dir/.runner" ] || [ "$labels_wanted" != "$labels_current" ] || [ "$token_hash_current" != "$token_hash_stored" ]; then
rm -v "$instance_dir/.runner" || true
gitea-runner register --no-interactive \
--instance "$runner_url" \
--token "$TOKEN" \
--name "jeeves-$(hostname)" \
--labels "$runner_labels" \
--config "$runner_config_file"
printf '%s' "$token_hash_current" > "$token_hash_file"
printf '%s\n' "$labels_wanted" > "$labels_file"
fi
-4
View File
@@ -2,10 +2,6 @@ let
vars = import ../vars.nix; vars = import ../vars.nix;
in in
{ {
networking.firewall.allowedTCPPorts = [
2223
];
services.gitea = { services.gitea = {
enable = true; enable = true;
appName = "TMM Workshop"; appName = "TMM Workshop";
+2 -4
View File
@@ -6,8 +6,6 @@ let
grafanaDataDir = "${vars.services}/grafana"; grafanaDataDir = "${vars.services}/grafana";
in in
{ {
networking.firewall.allowedTCPPorts = [ 3000 ];
services.grafana = { services.grafana = {
enable = true; enable = true;
dataDir = grafanaDataDir; dataDir = grafanaDataDir;
@@ -19,9 +17,9 @@ in
secret_key = "$__file{${vars.secrets}/services/grafana/secret_key}"; secret_key = "$__file{${vars.secrets}/services/grafana/secret_key}";
}; };
server = { server = {
http_addr = "192.168.90.40"; http_addr = "0.0.0.0";
http_port = 3000; http_port = 3000;
root_url = "http://192.168.90.40:3000/"; root_url = "http://jeeves:3000/";
}; };
}; };
provision = { provision = {
-1
View File
@@ -4,7 +4,6 @@ in
{ {
services.jellyfin = { services.jellyfin = {
enable = true; enable = true;
openFirewall = false;
dataDir = "${vars.services}/jellyfin"; dataDir = "${vars.services}/jellyfin";
cacheDir = "${vars.services}/jellyfin/cache"; cacheDir = "${vars.services}/jellyfin/cache";
}; };
-1
View File
@@ -38,7 +38,6 @@ in
"qwen3.5:35b" "qwen3.5:35b"
]; ];
modelsDir = vars.ollama; modelsDir = vars.ollama;
openFirewall = true;
}; };
systemd.services = { systemd.services = {
ollama.serviceConfig = { ollama.serviceConfig = {
-1
View File
@@ -7,6 +7,5 @@ in
package = pkgs.nix-serve-ng; package = pkgs.nix-serve-ng;
enable = true; enable = true;
secretKeyFile = "${vars.secrets}/services/nix-cache/cache-priv-key.pem"; secretKeyFile = "${vars.secrets}/services/nix-cache/cache-priv-key.pem";
openFirewall = false;
}; };
} }
-1
View File
@@ -2,7 +2,6 @@
services.open-webui = { services.open-webui = {
enable = true; enable = true;
host = "0.0.0.0"; host = "0.0.0.0";
openFirewall = true;
environment = { environment = {
ANONYMIZED_TELEMETRY = "False"; ANONYMIZED_TELEMETRY = "False";
DO_NOT_TRACK = "True"; DO_NOT_TRACK = "True";
+3 -5
View File
@@ -3,8 +3,6 @@ let
vars = import ../vars.nix; vars = import ../vars.nix;
in in
{ {
networking.firewall.allowedTCPPorts = [ 5432 ];
# Symlink pg_wal to a ZFS dataset on the special (metadata) vdev for fast WAL writes # Symlink pg_wal to a ZFS dataset on the special (metadata) vdev for fast WAL writes
# this is required for systemd sandboxing # this is required for systemd sandboxing
systemd.services.postgresql.serviceConfig.ReadWritePaths = [ "/zfs/media/database/postgres-wal" ]; systemd.services.postgresql.serviceConfig.ReadWritePaths = [ "/zfs/media/database/postgres-wal" ];
@@ -27,7 +25,7 @@ in
local all richie trust local all richie trust
host all richie 127.0.0.1/32 trust host all richie 127.0.0.1/32 trust
host all richie ::1/128 trust host all richie ::1/128 trust
host all richie 192.168.90.1/24 trust host all richie 100.64.0.0/10 trust
host all richie 192.168.99.1/24 trust host all richie 192.168.99.1/24 trust
host all richie 172.16.0.0/12 trust host all richie 172.16.0.0/12 trust
@@ -42,12 +40,12 @@ in
local postgres math trust local postgres math trust
host postgres math 127.0.0.1/32 trust host postgres math 127.0.0.1/32 trust
host postgres math ::1/128 trust host postgres math ::1/128 trust
host postgres math 192.168.90.1/24 trust host postgres math 100.64.0.0/10 trust
local data_science_dev math trust local data_science_dev math trust
host data_science_dev math 127.0.0.1/32 trust host data_science_dev math 127.0.0.1/32 trust
host data_science_dev math ::1/128 trust host data_science_dev math ::1/128 trust
host data_science_dev math 192.168.90.1/24 trust host data_science_dev math 100.64.0.0/10 trust
''; '';
identMap = '' identMap = ''
+2 -2
View File
@@ -21,11 +21,11 @@ in
message-level = 3; message-level = 3;
peer-port = 51413; peer-port = 51413;
rpc-bind-address = "0.0.0.0"; rpc-bind-address = "0.0.0.0";
rpc-host-whitelist = "127.0.0.1,192.168.90.40"; rpc-host-whitelist = "127.0.0.1,jeeves,jeeves.taile39c31.ts.net";
rpc-host-whitelist-enabled = true; rpc-host-whitelist-enabled = true;
rpc-port = 9091; rpc-port = 9091;
rpc-whitelist-enabled = true; rpc-whitelist-enabled = true;
rpc-whitelist = "127.0.0.1,192.168.90.49"; rpc-whitelist = "127.0.0.1,100.*.*.*";
seed-queue-enabled = false; seed-queue-enabled = false;
}; };
}; };
+1 -3
View File
@@ -2,11 +2,9 @@ let
vars = import ./vars.nix; vars = import ./vars.nix;
in in
{ {
networking.firewall.allowedTCPPorts = [ 8384 ];
services.syncthing = { services.syncthing = {
overrideFolders = false; overrideFolders = false;
guiAddress = "192.168.90.40:8384"; guiAddress = "0.0.0.0:8384";
settings = { settings = {
devices.davids-server.id = "7GXTDGR-AOXFW2O-K6J7NM3-XYZNRRW-AKHAFWM-GBOWUPQ-OA6JIWD-ER7RDQL"; # cspell:disable-line devices.davids-server.id = "7GXTDGR-AOXFW2O-K6J7NM3-XYZNRRW-AKHAFWM-GBOWUPQ-OA6JIWD-ER7RDQL"; # cspell:disable-line
folders = { folders = {
+2 -2
View File
@@ -10,12 +10,12 @@
"${inputs.self}/users/richie" "${inputs.self}/users/richie"
"${inputs.self}/common/global" "${inputs.self}/common/global"
"${inputs.self}/common/optional/tailscale.nix" "${inputs.self}/common/optional/tailscale.nix"
"${inputs.self}/common/optional/x86-64-v3.nix"
./disk-config.nix ./disk-config.nix
./haproxy ./haproxy
./monitoring.nix
]; ];
nixpkgs.hostPlatform = "x86_64-linux";
boot = { boot = {
# Avoid consuming the VM's limited memory for /tmp. # Avoid consuming the VM's limited memory for /tmp.
tmp.useTmpfs = false; tmp.useTmpfs = false;
+3 -3
View File
@@ -22,8 +22,8 @@ defaults
#Application Setup #Application Setup
frontend ContentSwitching frontend ContentSwitching
bind *:80 v4v6 bind [::]:80 v4v6
bind *:443 v4v6 ssl crt /var/lib/acme/audiobookshelf.tmmworkshop.com/full.pem crt /var/lib/acme/cache.tmmworkshop.com/full.pem crt /var/lib/acme/gems.tmmworkshop.com/full.pem crt /var/lib/acme/jellyfin.tmmworkshop.com/full.pem crt /var/lib/acme/share.tmmworkshop.com/full.pem crt /var/lib/acme/gitea.tmmworkshop.com/full.pem crt /var/lib/acme/www.norn-sight.com/full.pem bind [::]:443 v4v6 ssl crt /var/lib/acme/audiobookshelf.tmmworkshop.com/full.pem crt /var/lib/acme/cache.tmmworkshop.com/full.pem crt /var/lib/acme/gems.tmmworkshop.com/full.pem crt /var/lib/acme/jellyfin.tmmworkshop.com/full.pem crt /var/lib/acme/share.tmmworkshop.com/full.pem crt /var/lib/acme/gitea.tmmworkshop.com/full.pem crt /var/lib/acme/www.norn-sight.com/full.pem
mode http mode http
# ACME challenge routing (must be first) # ACME challenge routing (must be first)
@@ -113,7 +113,7 @@ frontend ContentSwitching
# Proxy Gitea's built-in SSH server over Tailscale to Jeeves. Port 22 remains # Proxy Gitea's built-in SSH server over Tailscale to Jeeves. Port 22 remains
# available to Endlessh on Portal. # available to Endlessh on Portal.
frontend GiteaSSH frontend GiteaSSH
bind *:2223 v4v6 bind [::]:2223 v4v6
mode tcp mode tcp
option tcplog option tcplog
default_backend gitea_ssh default_backend gitea_ssh
+15
View File
@@ -0,0 +1,15 @@
{ config, ... }:
{
networking.firewall.interfaces.tailscale0.allowedTCPPorts = [
config.services.prometheus.exporters.node.port
];
services.prometheus.exporters.node = {
enable = true;
enabledCollectors = [
"pressure"
"processes"
"systemd"
];
};
}
+1
View File
@@ -9,6 +9,7 @@
"${inputs.self}/common/optional/syncthing_base.nix" "${inputs.self}/common/optional/syncthing_base.nix"
"${inputs.self}/common/optional/systemd-boot.nix" "${inputs.self}/common/optional/systemd-boot.nix"
"${inputs.self}/common/optional/tailscale.nix" "${inputs.self}/common/optional/tailscale.nix"
"${inputs.self}/common/optional/x86-64-v3.nix"
"${inputs.self}/common/optional/yubikey.nix" "${inputs.self}/common/optional/yubikey.nix"
"${inputs.self}/common/optional/zfs" "${inputs.self}/common/optional/zfs"
./hardware.nix ./hardware.nix
+1 -1
View File
@@ -51,7 +51,7 @@ def test_config_defaults_enable_reranking(mocker: MockerFixture) -> None:
config = load_rerank_config() config = load_rerank_config()
assert config.enabled is True assert config.enabled is True
assert config.base_url == "http://192.168.90.25:8001" assert config.base_url == "http://bob:8001"
assert config.model == "qwen3-reranker-06b" assert config.model == "qwen3-reranker-06b"
assert config.candidates == 24 assert config.candidates == 24
assert config.timeout_seconds == 30 assert config.timeout_seconds == 30
+1 -1
View File
@@ -35,7 +35,7 @@
"sqltools.connections": [ "sqltools.connections": [
{ {
"previewLimit": 50, "previewLimit": 50,
"server": "192.168.90.40", "server": "jeeves.taile39c31.ts.net",
"port": 5432, "port": 5432,
"askForPassword": true, "askForPassword": true,
"driver": "PostgreSQL", "driver": "PostgreSQL",
+4 -4
View File
@@ -4,7 +4,7 @@
enableDefaultConfig = false; enableDefaultConfig = false;
settings = { settings = {
jeeves = { jeeves = {
HostName = "192.168.90.40"; HostName = "jeeves";
User = "richie"; User = "richie";
IdentityFile = "~/.ssh/id_ed25519"; IdentityFile = "~/.ssh/id_ed25519";
Port = 629; Port = 629;
@@ -18,7 +18,7 @@
Port = 2222; Port = 2222;
}; };
brain = { brain = {
HostName = "192.168.90.35"; HostName = "brain";
User = "richie"; User = "richie";
IdentityFile = "~/.ssh/id_ed25519"; IdentityFile = "~/.ssh/id_ed25519";
Port = 129; Port = 129;
@@ -31,14 +31,14 @@
Port = 2222; Port = 2222;
}; };
bob = { bob = {
HostName = "192.168.90.25"; HostName = "bob";
User = "richie"; User = "richie";
IdentityFile = "~/.ssh/id_ed25519"; IdentityFile = "~/.ssh/id_ed25519";
Port = 262; Port = 262;
DynamicForward = [ { port = 9050; } ]; DynamicForward = [ { port = 9050; } ];
}; };
rhapsody-in-green = { rhapsody-in-green = {
HostName = "192.168.90.221"; HostName = "rhapsody-in-green";
User = "richie"; User = "richie";
IdentityFile = "~/.ssh/id_ed25519"; IdentityFile = "~/.ssh/id_ed25519";
Port = 922; Port = 922;