Compare commits
2 Commits
d6e33250f4
...
dce4fa6022
| Author | SHA1 | Date |
|---|---|---|
|
|
dce4fa6022 | |
|
|
fe661f9b24 |
|
|
@ -11,6 +11,10 @@ bootstrap/host/inventory/
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.zip
|
*.zip
|
||||||
infra/gitea/data/
|
infra/gitea/data/
|
||||||
|
infra/matrix-server/synapse-data/
|
||||||
|
infra/matrix-server/postgres-data/
|
||||||
|
infra/matrix-server/.env
|
||||||
|
infra/omniroute/.env
|
||||||
infra/youtube-backup/.env
|
infra/youtube-backup/.env
|
||||||
infra/youtube-backup/cookies.txt
|
infra/youtube-backup/cookies.txt
|
||||||
infra/youtube-backup/archive.txt
|
infra/youtube-backup/archive.txt
|
||||||
|
|
@ -21,6 +25,8 @@ infra/youtube-backup/logs/
|
||||||
*.decrypted.yaml
|
*.decrypted.yaml
|
||||||
*.plain.yaml
|
*.plain.yaml
|
||||||
*.secret.local.yaml
|
*.secret.local.yaml
|
||||||
|
*.secret.yaml.plain
|
||||||
|
*.secret.plain
|
||||||
.age-key.txt
|
.age-key.txt
|
||||||
sops-age.key
|
sops-age.key
|
||||||
.sops.yaml.tmp
|
.sops.yaml.tmp
|
||||||
|
|
@ -31,3 +37,7 @@ sops-age.key
|
||||||
# Ignore local Python bytecode from validation helpers
|
# Ignore local Python bytecode from validation helpers
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|
||||||
|
# Tofu state backups
|
||||||
|
*.tfstate.*.backup
|
||||||
|
*.tfstate.bak*
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Copy this file to .sops.yaml after replacing the age recipient with the
|
||||||
|
# public key generated on the Debian homelab server.
|
||||||
|
creation_rules:
|
||||||
|
- path_regex: '(^|/).*\.(secret|enc)\.(ya?ml|json)$'
|
||||||
|
encrypted_regex: '.*'
|
||||||
|
age: age1ywayyaqwksdzedhf3w8xlsf89lnzw89c5yxtwjp42pnc5cds4dds5j6yxm
|
||||||
|
|
@ -52,6 +52,14 @@ spec:
|
||||||
annotations:
|
annotations:
|
||||||
summary: "PVC {{ $labels.namespace }}/{{ $labels.persistentvolumeclaim }} is over 85% full"
|
summary: "PVC {{ $labels.namespace }}/{{ $labels.persistentvolumeclaim }} is over 85% full"
|
||||||
description: "PVC capacity is near exhaustion. Review app storage before it becomes an outage."
|
description: "PVC capacity is near exhaustion. Review app storage before it becomes an outage."
|
||||||
|
- alert: HomelabHostFilesystemNearlyFull
|
||||||
|
expr: 100 * (1 - node_filesystem_avail_bytes{fstype!~"tmpfs|overlay|squashfs",mountpoint=~"/|/data|/var/lib/docker"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay|squashfs",mountpoint=~"/|/data|/var/lib/docker"}) > 85
|
||||||
|
for: 15m
|
||||||
|
labels:
|
||||||
|
severity: warning
|
||||||
|
annotations:
|
||||||
|
summary: "Host filesystem {{ $labels.mountpoint }} on {{ $labels.instance }} is over 85% full"
|
||||||
|
description: "The {{ $labels.mountpoint }} mount on {{ $labels.instance }} is near exhaustion. Review disk usage before it becomes an outage."
|
||||||
- name: homelab.edge
|
- name: homelab.edge
|
||||||
rules:
|
rules:
|
||||||
- alert: HomelabTraefik5xxSpike
|
- alert: HomelabTraefik5xxSpike
|
||||||
|
|
|
||||||
|
|
@ -39,12 +39,22 @@ locals {
|
||||||
backend_host = var.backend_host
|
backend_host = var.backend_host
|
||||||
backend_port = tostring(var.backend_port)
|
backend_port = tostring(var.backend_port)
|
||||||
})
|
})
|
||||||
|
default_conf_matrix = var.matrix_enabled ? templatefile("${path.module}/templates/matrix-vhost.conf.tftpl", {
|
||||||
|
matrix_server_name = var.matrix_server_name
|
||||||
|
matrix_backend_host = var.matrix_backend_host
|
||||||
|
matrix_backend_port = tostring(var.matrix_backend_port)
|
||||||
|
matrix_cert_dir = var.matrix_cert_dir
|
||||||
|
element_server_name = var.element_server_name
|
||||||
|
element_backend_host = var.element_backend_host
|
||||||
|
element_backend_port = tostring(var.element_backend_port)
|
||||||
|
}) : ""
|
||||||
config_hash = sha256(join("\n---\n", [
|
config_hash = sha256(join("\n---\n", [
|
||||||
local.compose_file,
|
local.compose_file,
|
||||||
local.default_conf,
|
local.default_conf,
|
||||||
local.default_vcl,
|
local.default_vcl,
|
||||||
local.haproxy_cfg,
|
local.haproxy_cfg,
|
||||||
local.squid_conf,
|
local.squid_conf,
|
||||||
|
local.default_conf_matrix,
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,6 +119,11 @@ resource "null_resource" "edge_services" {
|
||||||
destination = "/tmp/homelab-edge/config_files/squid.conf"
|
destination = "/tmp/homelab-edge/config_files/squid.conf"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
provisioner "file" {
|
||||||
|
content = local.default_conf_matrix
|
||||||
|
destination = "/tmp/homelab-edge/config_files/matrix.conf"
|
||||||
|
}
|
||||||
|
|
||||||
provisioner "remote-exec" {
|
provisioner "remote-exec" {
|
||||||
inline = [
|
inline = [
|
||||||
<<EOT
|
<<EOT
|
||||||
|
|
@ -184,6 +199,7 @@ sudo cp /tmp/homelab-edge/config_files/default.conf "$install_dir/config_files/d
|
||||||
sudo cp /tmp/homelab-edge/config_files/default.vcl "$install_dir/config_files/default.vcl"
|
sudo cp /tmp/homelab-edge/config_files/default.vcl "$install_dir/config_files/default.vcl"
|
||||||
sudo cp /tmp/homelab-edge/config_files/haproxy.cfg "$install_dir/config_files/haproxy.cfg"
|
sudo cp /tmp/homelab-edge/config_files/haproxy.cfg "$install_dir/config_files/haproxy.cfg"
|
||||||
sudo cp /tmp/homelab-edge/config_files/squid.conf "$install_dir/config_files/squid.conf"
|
sudo cp /tmp/homelab-edge/config_files/squid.conf "$install_dir/config_files/squid.conf"
|
||||||
|
sudo cp /tmp/homelab-edge/config_files/matrix.conf "$install_dir/config_files/matrix.conf"
|
||||||
|
|
||||||
if [ ! -s "$install_dir/certs/current.crt" ] || [ ! -s "$install_dir/certs/current.key" ]; then
|
if [ ! -s "$install_dir/certs/current.crt" ] || [ ! -s "$install_dir/certs/current.key" ]; then
|
||||||
sudo openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
|
sudo openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ services:
|
||||||
- "443:443"
|
- "443:443"
|
||||||
volumes:
|
volumes:
|
||||||
- ./config_files/default.conf:/etc/nginx/conf.d/default.conf:ro
|
- ./config_files/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
- ./config_files/matrix.conf:/etc/nginx/conf.d/matrix.conf:ro
|
||||||
- ./certs:/etc/nginx/certs:ro
|
- ./certs:/etc/nginx/certs:ro
|
||||||
- ./certbot/www:/var/www/certbot:ro
|
- ./certbot/www:/var/www/certbot:ro
|
||||||
- nginx_cache:/var/cache/nginx
|
- nginx_cache:/var/cache/nginx
|
||||||
|
|
@ -37,6 +38,21 @@ services:
|
||||||
- squid_cache:/var/spool/squid
|
- squid_cache:/var/spool/squid
|
||||||
- squid_logs:/var/log/squid
|
- squid_logs:/var/log/squid
|
||||||
|
|
||||||
|
node-exporter:
|
||||||
|
image: prom/node-exporter:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
pid: host
|
||||||
|
ports:
|
||||||
|
- "0.0.0.0:9100:9100/tcp"
|
||||||
|
command:
|
||||||
|
- --path.procfs=/host/proc
|
||||||
|
- --path.sysfs=/host/sys
|
||||||
|
- --path.rootfs=/rootfs
|
||||||
|
volumes:
|
||||||
|
- /proc:/host/proc:ro
|
||||||
|
- /sys:/host/sys:ro
|
||||||
|
- /:/rootfs:ro
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
nginx_cache:
|
nginx_cache:
|
||||||
nginx_dynamic_cache:
|
nginx_dynamic_cache:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
#----------------------------------------------------------------------
|
||||||
|
# MATRIX + ELEMENT (isolated certs, does NOT touch shared current.crt)
|
||||||
|
# Rendered by bootstrap/edge terraform. Keep this file idempotent:
|
||||||
|
# it regenerates identically on every apply.
|
||||||
|
#----------------------------------------------------------------------
|
||||||
|
|
||||||
|
# --- ACME + HTTP redirect for matrix / element ---
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name ${matrix_server_name} ${element_server_name};
|
||||||
|
|
||||||
|
location ^~ /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
default_type "text/plain";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- matrix server_name — Synapse client-server API ---
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
http2 on;
|
||||||
|
server_name ${matrix_server_name};
|
||||||
|
|
||||||
|
ssl_certificate ${matrix_cert_dir}/matrix.crt;
|
||||||
|
ssl_certificate_key ${matrix_cert_dir}/matrix.key;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
ssl_session_cache shared:matrix_ssl:10m;
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://${matrix_backend_host}:${matrix_backend_port};
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
proxy_read_timeout 90s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /.well-known/matrix {
|
||||||
|
proxy_pass http://${matrix_backend_host}:${matrix_backend_port}/.well-known/matrix;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- element server_name — Element web UI ---
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
http2 on;
|
||||||
|
server_name ${element_server_name};
|
||||||
|
|
||||||
|
ssl_certificate ${matrix_cert_dir}/matrix.crt;
|
||||||
|
ssl_certificate_key ${matrix_cert_dir}/matrix.key;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||||
|
ssl_session_cache shared:matrix_ssl:10m;
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://${element_backend_host}:${element_backend_port};
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -103,6 +103,49 @@ variable "heimdall_backend_port" {
|
||||||
default = 8082
|
default = 8082
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Matrix (isolated vhost + its own cert; does NOT touch shared current.crt)
|
||||||
|
variable "matrix_enabled" {
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "matrix_server_name" {
|
||||||
|
type = string
|
||||||
|
default = "matrix.lab2025.duckdns.org"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Synapse client-server port on the LAN host. Override to use a Tailscale IP.
|
||||||
|
variable "matrix_backend_host" {
|
||||||
|
type = string
|
||||||
|
default = "192.168.100.73"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "matrix_backend_port" {
|
||||||
|
type = number
|
||||||
|
default = 8008
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "matrix_cert_dir" {
|
||||||
|
type = string
|
||||||
|
default = "/etc/nginx/certs"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Element (shares the matrix isolated cert)
|
||||||
|
variable "element_server_name" {
|
||||||
|
type = string
|
||||||
|
default = "element.lab2025.duckdns.org"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "element_backend_host" {
|
||||||
|
type = string
|
||||||
|
default = "192.168.100.73"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "element_backend_port" {
|
||||||
|
type = number
|
||||||
|
default = 8081
|
||||||
|
}
|
||||||
|
|
||||||
variable "haproxy_stats_user" {
|
variable "haproxy_stats_user" {
|
||||||
type = string
|
type = string
|
||||||
default = "admin"
|
default = "admin"
|
||||||
|
|
|
||||||
|
|
@ -1865,6 +1865,46 @@ resource "helm_release" "prometheus_stack" {
|
||||||
prometheusSpec = {
|
prometheusSpec = {
|
||||||
nodeSelector = local.prometheus_stack_node_selector
|
nodeSelector = local.prometheus_stack_node_selector
|
||||||
retention = var.observability.prometheus.retention
|
retention = var.observability.prometheus.retention
|
||||||
|
additionalScrapeConfigs = yamlencode([
|
||||||
|
{
|
||||||
|
job_name = "rpi-node"
|
||||||
|
static_configs = [
|
||||||
|
{
|
||||||
|
targets = ["192.168.100.89:9100"]
|
||||||
|
labels = {
|
||||||
|
host_group = "edge-dns-worker"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
metrics_path = "/metrics"
|
||||||
|
scheme = "http"
|
||||||
|
relabel_configs = [
|
||||||
|
{
|
||||||
|
source_labels = ["__address__"]
|
||||||
|
target_label = "instance"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
job_name = "oci-edge-node"
|
||||||
|
static_configs = [
|
||||||
|
{
|
||||||
|
targets = ["132.145.170.74:9100"]
|
||||||
|
labels = {
|
||||||
|
host_group = "public-edge"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
metrics_path = "/metrics"
|
||||||
|
scheme = "http"
|
||||||
|
relabel_configs = [
|
||||||
|
{
|
||||||
|
source_labels = ["__address__"]
|
||||||
|
target_label = "instance"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
])
|
||||||
resources = {
|
resources = {
|
||||||
requests = {
|
requests = {
|
||||||
cpu = "100m"
|
cpu = "100m"
|
||||||
|
|
|
||||||
28
homelab.yml
28
homelab.yml
|
|
@ -12,6 +12,8 @@ domain:
|
||||||
subdomains:
|
subdomains:
|
||||||
- demos.lab2025.duckdns.org
|
- demos.lab2025.duckdns.org
|
||||||
- heimdall.lab2025.duckdns.org
|
- heimdall.lab2025.duckdns.org
|
||||||
|
- matrix.lab2025.duckdns.org
|
||||||
|
- element.lab2025.duckdns.org
|
||||||
- grafana.lab2025.duckdns.org
|
- grafana.lab2025.duckdns.org
|
||||||
- prometheus.lab2025.duckdns.org
|
- prometheus.lab2025.duckdns.org
|
||||||
- alertmanager.lab2025.duckdns.org
|
- alertmanager.lab2025.duckdns.org
|
||||||
|
|
@ -116,6 +118,32 @@ services:
|
||||||
suwayomi_port: 4567
|
suwayomi_port: 4567
|
||||||
maintainerr_port: 6246
|
maintainerr_port: 6246
|
||||||
flaresolverr_port: 8191
|
flaresolverr_port: 8191
|
||||||
|
matrix:
|
||||||
|
host: debian
|
||||||
|
install_dir: /home/jv/matrix-server
|
||||||
|
synapse_port: 8008
|
||||||
|
element_port: 8081
|
||||||
|
server_name: matrix.lab2025.duckdns.org
|
||||||
|
synapse_image: matrixdotorg/synapse:latest
|
||||||
|
element_image: vectorim/element-web:latest
|
||||||
|
postgres_image: postgres:16-alpine
|
||||||
|
postgres_db: synapse
|
||||||
|
postgres_user: synapse
|
||||||
|
bot_user: hermes
|
||||||
|
allowed_users:
|
||||||
|
- "@jv:matrix.lab2025.duckdns.org"
|
||||||
|
omniroute:
|
||||||
|
host: debian
|
||||||
|
install_dir: /home/jv/homelab-omniroute
|
||||||
|
port: 20128
|
||||||
|
image: diegosouzapw/omniroute:latest
|
||||||
|
container_name: omniroute
|
||||||
|
hermes:
|
||||||
|
host: debian
|
||||||
|
home: /home/jv/.hermes
|
||||||
|
install_dir: /home/jv/.hermes/hermes-agent
|
||||||
|
uv_bin: /home/jv/.hermes/bin/uv
|
||||||
|
bot_user: hermes
|
||||||
traefik:
|
traefik:
|
||||||
host: kubernetes
|
host: kubernetes
|
||||||
load_balancer_ip: 192.168.100.240
|
load_balancer_ip: 192.168.100.240
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ FLARESOLVERR_PORT=8191
|
||||||
KAPOWARR_PORT=5656
|
KAPOWARR_PORT=5656
|
||||||
SUWAYOMI_PORT=4567
|
SUWAYOMI_PORT=4567
|
||||||
MAINTAINERR_PORT=6246
|
MAINTAINERR_PORT=6246
|
||||||
|
JELLYFIN_PORT=8096
|
||||||
|
JELLYSEERR_PORT=5055
|
||||||
|
|
||||||
# Optional bootstrap values. Leave blank until the apps have created API keys.
|
# Optional bootstrap values. Leave blank until the apps have created API keys.
|
||||||
PROWLARR_API_KEY=
|
PROWLARR_API_KEY=
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,26 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- arr
|
- arr
|
||||||
|
|
||||||
|
jellyfin:
|
||||||
|
<<: *linuxserver-common
|
||||||
|
image: ${JELLYFIN_IMAGE:-lscr.io/linuxserver/jellyfin:latest}
|
||||||
|
container_name: ${JELLYFIN_CONTAINER_NAME:-arr-jellyfin}
|
||||||
|
dns:
|
||||||
|
- ${ARR_DNS_PRIMARY:-1.1.1.1}
|
||||||
|
- ${ARR_DNS_SECONDARY:-8.8.8.8}
|
||||||
|
group_add:
|
||||||
|
- "44" # video group (for access to DRM render node on the host)
|
||||||
|
devices:
|
||||||
|
- "/dev/dri/renderD128:/dev/dri/renderD128"
|
||||||
|
- "/dev/dri/card0:/dev/dri/card0"
|
||||||
|
ports:
|
||||||
|
- "${JELLYFIN_PORT:-8096}:8096"
|
||||||
|
# /config keeps its state; /data/media carries the media root so libraries
|
||||||
|
# can point at movies/tv/comics/manga/youtube. See README/media layout.
|
||||||
|
volumes:
|
||||||
|
- ${ARR_ROOT:-/data/arr}/config/jellyfin:/config
|
||||||
|
- ${ARR_ROOT:-/data/arr}/media:/data/media
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
arr:
|
arr:
|
||||||
name: homelab-arr
|
name: homelab-arr
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Hermes agent secrets - SOPS-encrypted. Do not commit plaintext.
|
||||||
|
# Decrypted at deploy time by jeannie deploy_hermes.
|
||||||
|
MATRIX_BOT_PASSWORD: ENC[AES256_GCM,data:+21aq+VzaYZLizC2p8oOEf1614M=,iv:qKpVrpE50ByIOXW32RnYy7zwPIbtt3UybAn7Pb8bY5Y=,tag:utUx46AyxK+jk3c7Wdnygw==,type:str]
|
||||||
|
HERMES_OMNIROUTE_API_KEY: ""
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBySEFyREdGOS9HOGtkeVRF
|
||||||
|
M0w4aUcxK2F5NTdSR29CSDY2SHZVbWlXZ0hzCmZOV3VXa3J5eEJLY2pTa2RHTGhV
|
||||||
|
eWpXdXVmVWFiLzZpMmlyUmJIQ3hUdjAKLS0tIG8ybWVNTXNZTHM4dnJ4Y2Z5VXAr
|
||||||
|
TXVIVXVWenlBc0lEZ0VSWmdFQmpPSGsK2FtwcyBFqVzCphxGVV8xg6QZea3SC6wt
|
||||||
|
gX1rp3icFegLisy/m4cUJkfku9zTHoHjsqPjbl+xzs3iaReHSVQRlA==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1ywayyaqwksdzedhf3w8xlsf89lnzw89c5yxtwjp42pnc5cds4dds5j6yxm
|
||||||
|
encrypted_regex: .*
|
||||||
|
lastmodified: "2026-09-16T16:32:24Z"
|
||||||
|
mac: ENC[AES256_GCM,data:0wKCLSIIDh4+/IHY/UN2KeYEdHBn0ZhCVRcIa3TV4P8CtRbcItQbhj5e95q5FPWJRBASXIxj/WwOkyd5JFUmxH5XhaeKCHvxbgNMClPb8Ic5KSnNMRgSZtrCgKlACySFEDL5aD9mxqkd3f5bVd+JY04HGHkSJUWRfdGnNdwGfnw=,iv:BPyQLTty1qjSldlxeUEC/Uo80H1R8Vlfey9ie26JZ30=,tag:oYZ2TVwKUgf+oPl8nvqeLA==,type:str]
|
||||||
|
version: 3.13.3
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Matrix stack — non-secret defaults.
|
||||||
|
# Copy to a sibling .env (gitignored) or export these as env vars when running
|
||||||
|
# deploy_matrix. NEVER put real secrets in this committed example.
|
||||||
|
MATRIX_SERVER_NAME=matrix.lab2025.duckdns.org
|
||||||
|
MATRIX_SYNAPSE_PORT=8008
|
||||||
|
MATRIX_ELEMENT_PORT=8081
|
||||||
|
MATRIX_SYNAPSE_CONTAINER_NAME=matrix-synapse
|
||||||
|
MATRIX_POSTGRES_CONTAINER_NAME=matrix-postgres
|
||||||
|
MATRIX_ELEMENT_CONTAINER_NAME=matrix-element
|
||||||
|
MATRIX_POSTGRES_DB=synapse
|
||||||
|
MATRIX_POSTGRES_USER=synapse
|
||||||
|
|
||||||
|
# SECRETS (must be supplied at deploy time, not committed):
|
||||||
|
# MATRIX_POSTGRES_PASSWORD postgres/synapse db password
|
||||||
|
# MATRIX_REGISTRATION_SHARED_SECRET shared registration secret for register_new_matrix_user
|
||||||
|
# MATRIX_MACAROON_SECRET_KEY macaroon secret
|
||||||
|
# MATRIX_FORM_SECRET form secret
|
||||||
|
# MATRIX_BOT_PASSWORD hermes bot account password
|
||||||
|
# (The synapse signing key is generated by Synapse on first boot — not a secret you set.)
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Deploy/update the self-hosted Matrix stack (Synapse + Postgres + Element).
|
||||||
|
# Idempotent: renders homeserver.yaml from the committed template, ensures
|
||||||
|
# correct data-dir ownership, and starts containers.
|
||||||
|
#
|
||||||
|
# Secrets are read from an install-dir .env (written by jeannie deploy_matrix),
|
||||||
|
# NOT from the repo. The signing key is created by Synapse on first boot.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ENV_FILE="${MATRIX_ENV_FILE:-${SCRIPT_DIR}/.env}"
|
||||||
|
|
||||||
|
if [[ ! -f "${ENV_FILE}" ]]; then
|
||||||
|
echo "Missing secret env file ${ENV_FILE}. Run jeannie deploy-matrix first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "${ENV_FILE}"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
MATRIX_SERVER_NAME="${MATRIX_SERVER_NAME:-matrix.lab2025.duckdns.org}"
|
||||||
|
MATRIX_POSTGRES_DB="${MATRIX_POSTGRES_DB:-synapse}"
|
||||||
|
MATRIX_POSTGRES_USER="${MATRIX_POSTGRES_USER:-synapse}"
|
||||||
|
SYNAPSE_UID="${MATRIX_SYNAPSE_UID:-991}"
|
||||||
|
POSTGRES_UID="${MATRIX_POSTGRES_UID:-70}"
|
||||||
|
|
||||||
|
render_homeserver() {
|
||||||
|
local tpl="${SCRIPT_DIR}/homeserver.yaml.tpl"
|
||||||
|
local out="${SCRIPT_DIR}/synapse-data/homeserver.yaml"
|
||||||
|
for var in MATRIX_SERVER_NAME MATRIX_POSTGRES_DB MATRIX_POSTGRES_USER \
|
||||||
|
MATRIX_POSTGRES_PASSWORD MATRIX_REGISTRATION_SHARED_SECRET \
|
||||||
|
MATRIX_MACAROON_SECRET_KEY MATRIX_FORM_SECRET; do
|
||||||
|
if [[ -z "${!var:-}" ]]; then
|
||||||
|
echo "Missing required secret/env ${var}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
envsubst \
|
||||||
|
'${MATRIX_SERVER_NAME} ${MATRIX_POSTGRES_DB} ${MATRIX_POSTGRES_USER} ${MATRIX_POSTGRES_PASSWORD} ${MATRIX_REGISTRATION_SHARED_SECRET} ${MATRIX_MACAROON_SECRET_KEY} ${MATRIX_FORM_SECRET}' \
|
||||||
|
<"${tpl}" >"${out}"
|
||||||
|
cp "${SCRIPT_DIR}/log_config.yaml" "${SCRIPT_DIR}/synapse-data/${MATRIX_SERVER_NAME}.log.config"
|
||||||
|
}
|
||||||
|
|
||||||
|
sudo mkdir -p "${SCRIPT_DIR}/synapse-data" "${SCRIPT_DIR}/postgres-data"
|
||||||
|
|
||||||
|
# First boot: generate signing key + log config (the files a hand-written
|
||||||
|
# homeserver.yaml would not provide). Without this Synapse fails to boot on a
|
||||||
|
# missing signing key. Only touches the data dir (not postgres).
|
||||||
|
if [[ ! -f "${SCRIPT_DIR}/synapse-data/${MATRIX_SERVER_NAME}.signing.key" ]]; then
|
||||||
|
echo "First boot: generating Synapse signing key + log config..."
|
||||||
|
docker compose --env-file "${ENV_FILE}" -f "${SCRIPT_DIR}/docker-compose.yml" \
|
||||||
|
run --rm --no-deps synapse generate --config-path /data/homeserver.yaml
|
||||||
|
fi
|
||||||
|
|
||||||
|
render_homeserver
|
||||||
|
|
||||||
|
# Ownership foot-gun: wrong uid prevents boot (synapse 991, postgres 70).
|
||||||
|
sudo chown -R "${SYNAPSE_UID}:${SYNAPSE_UID}" "${SCRIPT_DIR}/synapse-data"
|
||||||
|
sudo chown -R "${POSTGRES_UID}:${POSTGRES_UID}" "${SCRIPT_DIR}/postgres-data"
|
||||||
|
|
||||||
|
docker compose --env-file "${ENV_FILE}" -f "${SCRIPT_DIR}/docker-compose.yml" up -d
|
||||||
|
|
||||||
|
echo "Matrix stack up:"
|
||||||
|
echo " Synapse http://127.0.0.1:${MATRIX_SYNAPSE_PORT:-8008}"
|
||||||
|
echo " Element http://127.0.0.1:${MATRIX_ELEMENT_PORT:-8081}"
|
||||||
|
echo " server_name ${MATRIX_SERVER_NAME}"
|
||||||
|
|
||||||
|
cat <<'EOF'
|
||||||
|
Next: register users (hermes bot, and any humans) via the shared-secret CLI:
|
||||||
|
docker exec -u 991 matrix-synapse register_new_matrix_user -c /data/homeserver.yaml -u NAME -p PASS [--no-admin]
|
||||||
|
EOF
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
services:
|
||||||
|
synapse:
|
||||||
|
image: matrixdotorg/synapse:latest
|
||||||
|
container_name: ${MATRIX_SYNAPSE_CONTAINER_NAME:-matrix-synapse}
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
environment:
|
||||||
|
SYNAPSE_SERVER_NAME: ${MATRIX_SERVER_NAME:-matrix.lab2025.duckdns.org}
|
||||||
|
SYNAPSE_REPORT_STATS: "no"
|
||||||
|
# federation disabled (user choice): client-only listener on 8008
|
||||||
|
ports:
|
||||||
|
- "${MATRIX_SYNAPSE_PORT:-8008}:8008"
|
||||||
|
volumes:
|
||||||
|
- ./synapse-data:/data
|
||||||
|
networks:
|
||||||
|
- matrix
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: ${MATRIX_POSTGRES_CONTAINER_NAME:-matrix-postgres}
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${MATRIX_POSTGRES_DB:-synapse}
|
||||||
|
POSTGRES_USER: ${MATRIX_POSTGRES_USER:-synapse}
|
||||||
|
POSTGRES_PASSWORD: ${MATRIX_POSTGRES_PASSWORD}
|
||||||
|
POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=C"
|
||||||
|
volumes:
|
||||||
|
- ./postgres-data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- matrix
|
||||||
|
|
||||||
|
element:
|
||||||
|
image: vectorim/element-web:latest
|
||||||
|
container_name: ${MATRIX_ELEMENT_CONTAINER_NAME:-matrix-element}
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- synapse
|
||||||
|
ports:
|
||||||
|
- "${MATRIX_ELEMENT_PORT:-8081}:80"
|
||||||
|
volumes:
|
||||||
|
# Must mount the FILE, not a directory (element-web reads /app/config.json)
|
||||||
|
- ./element-config/config.json:/app/config.json
|
||||||
|
networks:
|
||||||
|
- matrix
|
||||||
|
|
||||||
|
networks:
|
||||||
|
matrix:
|
||||||
|
driver: bridge
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"default_server_name": "matrix.lab2025.duckdns.org",
|
||||||
|
"default_server_config": {
|
||||||
|
"m.homeserver": {
|
||||||
|
"base_url": "https://matrix.lab2025.duckdns.org",
|
||||||
|
"server_name": "matrix.lab2025.duckdns.org"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"disable_custom_urls": false,
|
||||||
|
"disable_guests": true,
|
||||||
|
"disable_login_language_selector": false,
|
||||||
|
"disable_3pid_login": false,
|
||||||
|
"force_verification": false,
|
||||||
|
"brand": "Matrix",
|
||||||
|
"default_federate": false,
|
||||||
|
"show_labs_settings": true,
|
||||||
|
"default_theme": "dark",
|
||||||
|
"room_directory": {
|
||||||
|
"servers": ["matrix.lab2025.duckdns.org"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
# Configuration file for Synapse.
|
||||||
|
# This is generated by Jeannie from infra/matrix-server/homeserver.yaml.tpl.
|
||||||
|
# Secrets (registration_shared_secret, macaroon_secret_key, form_secret, postgres
|
||||||
|
# password, signing key) are injected by the deploy_matrix() bootstrap — never store
|
||||||
|
# them in this template.
|
||||||
|
|
||||||
|
server_name: "${MATRIX_SERVER_NAME}"
|
||||||
|
pid_file: /data/homeserver.pid
|
||||||
|
listeners:
|
||||||
|
- port: 8008
|
||||||
|
resources:
|
||||||
|
- compress: false
|
||||||
|
names:
|
||||||
|
- client
|
||||||
|
- federation
|
||||||
|
tls: false
|
||||||
|
type: http
|
||||||
|
x_forwarded: true
|
||||||
|
database:
|
||||||
|
name: psycopg2
|
||||||
|
args:
|
||||||
|
user: "${MATRIX_POSTGRES_USER}"
|
||||||
|
password: "${MATRIX_POSTGRES_PASSWORD}"
|
||||||
|
database: "${MATRIX_POSTGRES_DB}"
|
||||||
|
host: postgres
|
||||||
|
port: 5432
|
||||||
|
cp_min: 5
|
||||||
|
cp_max: 10
|
||||||
|
log_config: "/data/${MATRIX_SERVER_NAME}.log.config"
|
||||||
|
media_store_path: /data/media_store
|
||||||
|
registration_shared_secret: "${MATRIX_REGISTRATION_SHARED_SECRET}"
|
||||||
|
report_stats: false
|
||||||
|
macaroon_secret_key: "${MATRIX_MACAROON_SECRET_KEY}"
|
||||||
|
form_secret: "${MATRIX_FORM_SECRET}"
|
||||||
|
signing_key_path: "/data/${MATRIX_SERVER_NAME}.signing.key"
|
||||||
|
trusted_key_servers:
|
||||||
|
- server_name: "matrix.org"
|
||||||
|
|
||||||
|
# vim:ft=yaml
|
||||||
|
user_directory:
|
||||||
|
enabled: true
|
||||||
|
search_all_users: true
|
||||||
|
prefer_local_users: true
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
version: 1
|
||||||
|
|
||||||
|
formatters:
|
||||||
|
precise:
|
||||||
|
format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s'
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
console:
|
||||||
|
class: logging.StreamHandler
|
||||||
|
formatter: precise
|
||||||
|
|
||||||
|
loggers:
|
||||||
|
_placeholder:
|
||||||
|
level: "INFO"
|
||||||
|
|
||||||
|
synapse.storage.SQL:
|
||||||
|
# beware: increasing this to DEBUG will make synapse log sensitive
|
||||||
|
# information such as access tokens.
|
||||||
|
level: INFO
|
||||||
|
|
||||||
|
root:
|
||||||
|
level: INFO
|
||||||
|
handlers: [console]
|
||||||
|
|
||||||
|
disable_existing_loggers: false
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Matrix homeserver secrets - SOPS-encrypted. Do not commit plaintext.
|
||||||
|
# Decrypted at deploy time by jeannie deploy_matrix.
|
||||||
|
MATRIX_POSTGRES_PASSWORD: ENC[AES256_GCM,data:HYti676SrsKpr5UuYX7wrg==,iv:pggYfnjqZAGulrCNVBJ/vcICcsn9hic150hmRb1D5uE=,tag:rs4jSLwMZeoaUgjqCYIkTA==,type:str]
|
||||||
|
MATRIX_REGISTRATION_SHARED_SECRET: ENC[AES256_GCM,data:+OChmnoaVrWhLCQHPe3sSbfuEs2H6nJuPhFPelRM+hOPCADXYZPiUPxH3RJfLY9niCg=,iv:b2wVg4Xg/qTTT7ghyNJJNOfnafESE7EZAZoVUKFJxJc=,tag:ewdDXpFoeb2/+HlLzYJySw==,type:str]
|
||||||
|
MATRIX_MACAROON_SECRET_KEY: ENC[AES256_GCM,data:0IY3J7ijJhYiqd10A2D/xo0Utok0llFFjUOOqY57QjnCA8tgeJOH4lNuiPTScKPMnnI=,iv:PkqyW3RaQpA8+TQtKI6ibo11CJVUiod/OO2QLBa4Lrs=,tag:Z8j5xqlcysgkrmbmiMVNEA==,type:str]
|
||||||
|
MATRIX_FORM_SECRET: ENC[AES256_GCM,data:WfaToWF7NO5VgpeJcuONlhHK+GopNjPnVAjFLorrronMNUzTKIICr2xjrVBq3wvPuf4=,iv:Qgf7fdO9boSV136LL7/IxNcYz33gNP7YbIQohJFEAAI=,tag:FXtUqNj/JtsrWLx/ntypag==,type:str]
|
||||||
|
sops:
|
||||||
|
age:
|
||||||
|
- enc: |
|
||||||
|
-----BEGIN AGE ENCRYPTED FILE-----
|
||||||
|
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1THJOdkhOb21UeFk2NHdx
|
||||||
|
bUNSMTZUVDlOeUh1T1N4WnA5akpVVWVEVFRNCk02RG9kNENCVHJUSVVXNkE0ZUFh
|
||||||
|
ek8vU2NzRUwvMm1EMDNwdnc1YjhzZ2cKLS0tIFpmZU5Pb0FJRTl1ZktVVXA1RTAw
|
||||||
|
MjBHcmZQTFJqZUdHWVgxdURhQk5wRW8Ka0TXyBnr7Puq+dYVizMK+HIAEncAD2bl
|
||||||
|
5JGP5TU1Dp0I9njHJYmDAicx5iaMhfwQe4iL9/73FOy1vnfYPlv9gQ==
|
||||||
|
-----END AGE ENCRYPTED FILE-----
|
||||||
|
recipient: age1ywayyaqwksdzedhf3w8xlsf89lnzw89c5yxtwjp42pnc5cds4dds5j6yxm
|
||||||
|
encrypted_regex: .*
|
||||||
|
lastmodified: "2026-09-16T16:32:24Z"
|
||||||
|
mac: ENC[AES256_GCM,data:lyhzE48mseEWnf2+UoYy4lkBPFtvD3PuB4BR/LbTEkz4xrC0KYHqlVma5dEhLoOTs+k5rhFy3ko3lZoZ396kkrOMftd9zyYTK7Nk/ZxPRrg3gPqr/Dd4bTCGDstznNMgbsCgzklhB35IYWgP/PL5ccZiP/OnqWXoTMPRYy/EPC8=,iv:FVhAK3zGSROkI0iyhRo1+PMlXLbZQ+74A4pnlSR6Y80=,tag:x3/mNU6l8PqPBb0cRbPAuQ==,type:str]
|
||||||
|
version: 3.13.3
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# OmniRoute — non-secret defaults.
|
||||||
|
# Copy to a sibling .env or export when running deploy_omniroute.
|
||||||
|
# Secrets (JWT_SECRET, API_KEY_SECRET, STORAGE_ENCRYPTION_*) are auto-generated
|
||||||
|
# by OmniRoute into its data volume (server.env) on first boot — not stored here.
|
||||||
|
OMNIROUTE_IMAGE=diegosouzapw/omniroute:latest
|
||||||
|
OMNIROUTE_CONTAINER_NAME=omniroute
|
||||||
|
OMNIROUTE_PORT=20128
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Deploy/update the OmniRoute LLM-routing gateway that Hermes uses as its model
|
||||||
|
# provider (http://localhost:20128/v1). Idempotent. Secrets live in the data
|
||||||
|
# volume (server.env) generated by OmniRoute on first boot.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ENV_FILE="${OMNIROUTE_ENV_FILE:-${SCRIPT_DIR}/.env}"
|
||||||
|
|
||||||
|
if [[ ! -f "${ENV_FILE}" ]]; then
|
||||||
|
echo "Missing env file ${ENV_FILE}. Run jeannie deploy-omniroute first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "${ENV_FILE}"
|
||||||
|
set +a
|
||||||
|
|
||||||
|
sudo mkdir -p "${SCRIPT_DIR}"
|
||||||
|
docker compose --env-file "${ENV_FILE}" -f "${SCRIPT_DIR}/docker-compose.yml" up -d
|
||||||
|
|
||||||
|
echo "OmniRoute up at http://127.0.0.1:${OMNIROUTE_PORT:-20128}"
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
services:
|
||||||
|
omniroute:
|
||||||
|
image: ${OMNIROUTE_IMAGE:-diegosouzapw/omniroute:latest}
|
||||||
|
container_name: ${OMNIROUTE_CONTAINER_NAME:-omniroute}
|
||||||
|
restart: unless-stopped
|
||||||
|
stop_timeout: 40
|
||||||
|
ports:
|
||||||
|
- "${OMNIROUTE_PORT:-20128}:20128"
|
||||||
|
volumes:
|
||||||
|
- omniroute-data:/app/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
omniroute-data:
|
||||||
|
|
@ -55,6 +55,24 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- dns
|
- dns
|
||||||
|
|
||||||
|
node-exporter:
|
||||||
|
image: prom/node-exporter:latest
|
||||||
|
container_name: ${NODE_EXPORTER_CONTAINER_NAME:-homelab-node-exporter}
|
||||||
|
restart: unless-stopped
|
||||||
|
pid: host
|
||||||
|
ports:
|
||||||
|
- "${NODE_EXPORTER_BIND_IP:-192.168.100.89}:9100:9100/tcp"
|
||||||
|
command:
|
||||||
|
- --path.procfs=/host/proc
|
||||||
|
- --path.sysfs=/host/sys
|
||||||
|
- --path.rootfs=/rootfs
|
||||||
|
volumes:
|
||||||
|
- /proc:/host/proc:ro
|
||||||
|
- /sys:/host/sys:ro
|
||||||
|
- /:/rootfs:ro
|
||||||
|
networks:
|
||||||
|
- dns
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
dns:
|
dns:
|
||||||
name: ${RPI_SERVICES_NETWORK:-homelab-rpi-services}
|
name: ${RPI_SERVICES_NETWORK:-homelab-rpi-services}
|
||||||
|
|
|
||||||
336
jeannie
336
jeannie
|
|
@ -4117,6 +4117,233 @@ ENV_EOT
|
||||||
echo "Heimdall is reachable at http://127.0.0.1:${http_port}/ and ${public_url%/}/."
|
echo "Heimdall is reachable at http://127.0.0.1:${http_port}/ and ${public_url%/}/."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deploy_matrix() {
|
||||||
|
local mode="${LAB_MATRIX_DEPLOY:-true}"
|
||||||
|
local install_dir="${LAB_MATRIX_INSTALL_DIR:-/home/jv/matrix-server}"
|
||||||
|
local image=""
|
||||||
|
local container_name=""
|
||||||
|
local http_port=""
|
||||||
|
local ssh_port=""
|
||||||
|
local domain=""
|
||||||
|
local install_docker="false"
|
||||||
|
local source_dir="${REPO_ROOT}/infra/matrix-server"
|
||||||
|
local value_name
|
||||||
|
local value
|
||||||
|
|
||||||
|
require_debian_server "deploy-matrix"
|
||||||
|
|
||||||
|
if disabled_value "${mode}"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
for value_name in install_dir; do
|
||||||
|
value="${!value_name}"
|
||||||
|
if [[ "${value}" == *$'\n'* ]]; then
|
||||||
|
echo "${value_name} cannot contain a newline." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ! -s "${source_dir}/docker-compose.yml" || ! -s "${source_dir}/homeserver.yaml.tpl" || ! -s "${source_dir}/bootstrap.sh" ]]; then
|
||||||
|
echo "Missing Matrix source files in ${source_dir}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deploying Matrix stack on the Debian host at ${install_dir}..."
|
||||||
|
|
||||||
|
ensure_local_docker_compose "MATRIX" "${install_docker}"
|
||||||
|
repair_local_docker_iptables
|
||||||
|
|
||||||
|
sudo mkdir -p "${install_dir}"
|
||||||
|
sudo cp "${source_dir}/docker-compose.yml" "${install_dir}/docker-compose.yml"
|
||||||
|
sudo cp "${source_dir}/homeserver.yaml.tpl" "${install_dir}/homeserver.yaml.tpl"
|
||||||
|
sudo cp "${source_dir}/log_config.yaml" "${install_dir}/log_config.yaml"
|
||||||
|
sudo cp "${source_dir}/element-config/config.json" "${install_dir}/element-config/config.json" 2>/dev/null || {
|
||||||
|
sudo mkdir -p "${install_dir}/element-config"
|
||||||
|
sudo cp "${source_dir}/element-config/config.json" "${install_dir}/element-config/config.json"
|
||||||
|
}
|
||||||
|
sudo cp "${source_dir}/bootstrap.sh" "${install_dir}/bootstrap.sh"
|
||||||
|
sudo chmod +x "${install_dir}/bootstrap.sh"
|
||||||
|
|
||||||
|
# Secrets come from the SOPS-encrypted repo file, decrypted at deploy time.
|
||||||
|
# (Env vars override the file, for manual/ops overrides.)
|
||||||
|
if ! sops_load_secrets "${REPO_ROOT}/infra/matrix-server/matrix.secret.yaml"; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${MATRIX_POSTGRES_PASSWORD:-}" || -z "${MATRIX_REGISTRATION_SHARED_SECRET:-}" ]]; then
|
||||||
|
echo "MATRIX_POSTGRES_PASSWORD and MATRIX_REGISTRATION_SHARED_SECRET missing after decrypt." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local matrix_server_name="${LAB_MATRIX_SERVER_NAME:-matrix.lab2025.duckdns.org}"
|
||||||
|
local matrix_synapse_port="${LAB_MATRIX_SYNAPSE_PORT:-8008}"
|
||||||
|
local matrix_element_port="${LAB_MATRIX_ELEMENT_PORT:-8081}"
|
||||||
|
local postgres_db="${LAB_MATRIX_POSTGRES_DB:-synapse}"
|
||||||
|
local postgres_user="${LAB_MATRIX_POSTGRES_USER:-synapse}"
|
||||||
|
|
||||||
|
sudo tee "${install_dir}/.env" >/dev/null <<ENV_EOT
|
||||||
|
MATRIX_SERVER_NAME=${matrix_server_name}
|
||||||
|
MATRIX_SYNAPSE_PORT=${matrix_synapse_port}
|
||||||
|
MATRIX_ELEMENT_PORT=${matrix_element_port}
|
||||||
|
MATRIX_SYNAPSE_CONTAINER_NAME=${LAB_MATRIX_SYNAPSE_CONTAINER_NAME:-matrix-synapse}
|
||||||
|
MATRIX_POSTGRES_CONTAINER_NAME=${LAB_MATRIX_POSTGRES_CONTAINER_NAME:-matrix-postgres}
|
||||||
|
MATRIX_ELEMENT_CONTAINER_NAME=${LAB_MATRIX_ELEMENT_CONTAINER_NAME:-matrix-element}
|
||||||
|
MATRIX_POSTGRES_DB=${postgres_db}
|
||||||
|
MATRIX_POSTGRES_USER=${postgres_user}
|
||||||
|
MATRIX_POSTGRES_PASSWORD=${MATRIX_POSTGRES_PASSWORD}
|
||||||
|
MATRIX_REGISTRATION_SHARED_SECRET=${MATRIX_REGISTRATION_SHARED_SECRET}
|
||||||
|
MATRIX_MACAROON_SECRET_KEY=${MATRIX_MACAROON_SECRET_KEY:-${MATRIX_REGISTRATION_SHARED_SECRET}}
|
||||||
|
MATRIX_FORM_SECRET=${MATRIX_FORM_SECRET:-${MATRIX_REGISTRATION_SHARED_SECRET}}
|
||||||
|
ENV_EOT
|
||||||
|
|
||||||
|
# Pull + render + chown + up. bootstrap.sh is the single source of truth.
|
||||||
|
# sudo the docker compose commands; data dirs are chown'd inside bootstrap.
|
||||||
|
(
|
||||||
|
cd "${install_dir}"
|
||||||
|
sudo docker compose --env-file "${install_dir}/.env" -f "${install_dir}/docker-compose.yml" pull
|
||||||
|
sudo MATRIX_ENV_FILE="${install_dir}/.env" bash "${install_dir}/bootstrap.sh"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "Matrix stack is reachable at http://127.0.0.1:${matrix_synapse_port}/ and Element at http://127.0.0.1:${matrix_element_port}/."
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy_omniroute() {
|
||||||
|
local mode="${LAB_OMNIROUTE_DEPLOY:-true}"
|
||||||
|
local install_dir="${LAB_OMNIROUTE_INSTALL_DIR:-/home/jv/homelab-omniroute}"
|
||||||
|
local install_docker="false"
|
||||||
|
local source_dir="${REPO_ROOT}/infra/omniroute"
|
||||||
|
|
||||||
|
require_debian_server "deploy-omniroute"
|
||||||
|
|
||||||
|
if disabled_value "${mode}"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -s "${source_dir}/docker-compose.yml" || ! -s "${source_dir}/bootstrap.sh" ]]; then
|
||||||
|
echo "Missing OmniRoute source files in ${source_dir}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deploying OmniRoute LLM gateway on the Debian host at ${install_dir}..."
|
||||||
|
|
||||||
|
ensure_local_docker_compose "OMNIROUTE" "${install_docker}"
|
||||||
|
repair_local_docker_iptables
|
||||||
|
|
||||||
|
sudo mkdir -p "${install_dir}"
|
||||||
|
sudo cp "${source_dir}/docker-compose.yml" "${install_dir}/docker-compose.yml"
|
||||||
|
sudo cp "${source_dir}/bootstrap.sh" "${install_dir}/bootstrap.sh"
|
||||||
|
sudo chmod +x "${install_dir}/bootstrap.sh"
|
||||||
|
|
||||||
|
sudo tee "${install_dir}/.env" >/dev/null <<ENV_EOT
|
||||||
|
OMNIROUTE_IMAGE=${LAB_OMNIROUTE_IMAGE:-diegosouzapw/omniroute:latest}
|
||||||
|
OMNIROUTE_CONTAINER_NAME=${LAB_OMNIROUTE_CONTAINER_NAME:-omniroute}
|
||||||
|
OMNIROUTE_PORT=${LAB_OMNIROUTE_PORT:-20128}
|
||||||
|
ENV_EOT
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "${install_dir}"
|
||||||
|
sudo docker compose pull
|
||||||
|
sudo bash "${install_dir}/bootstrap.sh"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "OmniRoute is reachable at http://127.0.0.1:${LAB_OMNIROUTE_PORT:-20128}."
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy_hermes() {
|
||||||
|
local mode="${LAB_HERMES_DEPLOY:-true}"
|
||||||
|
local hermes_home="${LAB_HERMES_HOME:-/home/jv/.hermes}"
|
||||||
|
local install_dir="${LAB_HERMES_INSTALL_DIR:-/home/jv/.hermes/hermes-agent}"
|
||||||
|
local uv_bin="${LAB_HERMES_UV_BIN:-/home/jv/.hermes/bin/uv}"
|
||||||
|
local install_docker="${LAB_HERMES_INSTALL_DOCKER:-false}"
|
||||||
|
local value_name
|
||||||
|
local value
|
||||||
|
|
||||||
|
require_debian_server "deploy-hermes"
|
||||||
|
|
||||||
|
if disabled_value "${mode}"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
for value_name in hermes_home install_dir uv_bin; do
|
||||||
|
value="${!value_name}"
|
||||||
|
if [[ "${value}" == *$'\n'* ]]; then
|
||||||
|
echo "${value_name} cannot contain a newline." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Deploying Hermes agent on the Debian host at ${install_dir}..."
|
||||||
|
|
||||||
|
ensure_local_docker_compose "HERMES" "${install_docker}"
|
||||||
|
|
||||||
|
# Install uv if missing.
|
||||||
|
if [[ ! -x "${uv_bin}" ]]; then
|
||||||
|
mkdir -p "$(dirname "${uv_bin}")"
|
||||||
|
echo "Installing uv..."
|
||||||
|
curl -LsSf https://astral.sh/uv/install.sh | bash
|
||||||
|
# uv installs to ~/.local/bin; copy/alias into HERMES bin if that path differs.
|
||||||
|
if command -v uv >/dev/null 2>&1 && [[ ! -x "${uv_bin}" ]]; then
|
||||||
|
cp "$(command -v uv)" "${uv_bin}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install Hermes via its official installer (git method, managed venv layout).
|
||||||
|
if [[ ! -x "${hermes_home}/hermes-agent/venv/bin/python" ]]; then
|
||||||
|
echo "Installing Hermes agent..."
|
||||||
|
curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure the Matrix platform dependencies (E2EE) are present in the venv.
|
||||||
|
if [[ -x "${uv_bin}" && -f "${install_dir}/pyproject.toml" ]]; then
|
||||||
|
cd "${install_dir}"
|
||||||
|
"${uv_bin}" pip install 'mautrix[encryption]' -p venv/bin/python
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Secret env for the gateway (.env under HERMES_HOME, mode 600, never committed).
|
||||||
|
# Pull the hermes secrets from the SOPS-encrypted repo file; env vars override.
|
||||||
|
if ! sops_load_secrets "${REPO_ROOT}/infra/hermes.secret.yaml"; then
|
||||||
|
echo "WARNING: could not load hermes secrets; MATRIX_PASSWORD may be blank." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
local secrets_file="${hermes_home}/.env"
|
||||||
|
sudo install -d -m 0700 -o "$(id -u)" -g "$(id -g)" "${hermes_home}"
|
||||||
|
if [[ -f "${secrets_file}" ]]; then
|
||||||
|
sudo cp "${secrets_file}" "${secrets_file}.bak-pre-jeannie"
|
||||||
|
fi
|
||||||
|
sudo tee "${secrets_file}" >/dev/null <<ENV_EOT
|
||||||
|
OPENAI_BASE_URL=${HERMES_OPENAI_BASE_URL:-http://localhost:${LAB_OMNIROUTE_PORT:-20128}/v1}
|
||||||
|
HERMES_CUSTOM_LOCALHOST_${LAB_OMNIROUTE_PORT:-20128}_API_KEY=${HERMES_OMNIROUTE_API_KEY:-}
|
||||||
|
MATRIX_HOMESERVER=${LAB_MATRIX_SERVER_NAME:-matrix.lab2025.duckdns.org}
|
||||||
|
MATRIX_USER_ID=${HERMES_MATRIX_USER_ID:-@hermes:matrix.lab2025.duckdns.org}
|
||||||
|
MATRIX_PASSWORD=${MATRIX_BOT_PASSWORD:-}
|
||||||
|
MATRIX_ALLOWED_USERS=${HERMES_MATRIX_ALLOWED_USERS:-@jv:matrix.lab2025.duckdns.org}
|
||||||
|
MATRIX_E2EE_MODE=required
|
||||||
|
ENV_EOT
|
||||||
|
sudo chmod 600 "${secrets_file}"
|
||||||
|
|
||||||
|
# Create the Matrix bot account (idempotent) if the homeserver is up.
|
||||||
|
if command -v docker >/dev/null 2>&1 && docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^matrix-synapse$'; then
|
||||||
|
if [[ -n "${MATRIX_BOT_PASSWORD:-}" ]]; then
|
||||||
|
if ! docker exec matrix-synapse register_new_matrix_user -c /data/homeserver.yaml \
|
||||||
|
-u hermes -p "${MATRIX_BOT_PASSWORD}" --no-admin >/dev/null 2>&1; then
|
||||||
|
echo "Hermes bot account already exists (expected on re-run) or auto-registration skipped." >&2
|
||||||
|
else
|
||||||
|
echo "Hermes Matrix bot account ready."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "MATRIX_BOT_PASSWORD not set; skipping Hermes bot account registration (needed only on fresh install)." >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Install/start the gateway systemd user service (Hermes manages the unit).
|
||||||
|
"${hermes_home}/hermes-agent/venv/bin/python" "${hermes_home}/hermes-agent/hermes" gateway install
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now hermes-gateway 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "Hermes agent deployed. Run 'hermes' to configure the model, or check gateway logs at ${hermes_home}/logs/gateway.log."
|
||||||
|
}
|
||||||
|
|
||||||
deploy_rpi_services() {
|
deploy_rpi_services() {
|
||||||
local mode="${LAB_RPI_SERVICES_DEPLOY:-true}"
|
local mode="${LAB_RPI_SERVICES_DEPLOY:-true}"
|
||||||
local rpi_host="${LAB_RPI_HOST:-${LAB_RASPBERRY_HOST:-192.168.100.89}}"
|
local rpi_host="${LAB_RPI_HOST:-${LAB_RASPBERRY_HOST:-192.168.100.89}}"
|
||||||
|
|
@ -5609,6 +5836,22 @@ spec = {
|
||||||
"title": "Heimdall deploy",
|
"title": "Heimdall deploy",
|
||||||
"command": [jeannie, "__up-task", "heimdall-deploy"],
|
"command": [jeannie, "__up-task", "heimdall-deploy"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "matrix-deploy",
|
||||||
|
"title": "Matrix deploy",
|
||||||
|
"command": [jeannie, "__up-task", "matrix-deploy"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "omniroute-deploy",
|
||||||
|
"title": "OmniRoute deploy",
|
||||||
|
"command": [jeannie, "__up-task", "omniroute-deploy"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "hermes-deploy",
|
||||||
|
"title": "Hermes deploy",
|
||||||
|
"needs": ["omniroute-deploy", "matrix-deploy"],
|
||||||
|
"command": [jeannie, "__up-task", "hermes-deploy"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "rpi-services",
|
"id": "rpi-services",
|
||||||
"title": "RPi services",
|
"title": "RPi services",
|
||||||
|
|
@ -5656,6 +5899,15 @@ up_task() {
|
||||||
heimdall-deploy)
|
heimdall-deploy)
|
||||||
deploy_heimdall
|
deploy_heimdall
|
||||||
;;
|
;;
|
||||||
|
matrix-deploy)
|
||||||
|
deploy_matrix
|
||||||
|
;;
|
||||||
|
omniroute-deploy)
|
||||||
|
deploy_omniroute
|
||||||
|
;;
|
||||||
|
hermes-deploy)
|
||||||
|
deploy_hermes
|
||||||
|
;;
|
||||||
rpi-services)
|
rpi-services)
|
||||||
deploy_rpi_services
|
deploy_rpi_services
|
||||||
;;
|
;;
|
||||||
|
|
@ -7579,9 +7831,6 @@ ensure_sops_age_tools() {
|
||||||
if ! command -v age-keygen >/dev/null 2>&1; then
|
if ! command -v age-keygen >/dev/null 2>&1; then
|
||||||
missing_packages+=(age)
|
missing_packages+=(age)
|
||||||
fi
|
fi
|
||||||
if ! command -v sops >/dev/null 2>&1; then
|
|
||||||
missing_packages+=(sops)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ((${#missing_packages[@]} > 0)); then
|
if ((${#missing_packages[@]} > 0)); then
|
||||||
echo "Installing missing secret-management tools: ${missing_packages[*]}"
|
echo "Installing missing secret-management tools: ${missing_packages[*]}"
|
||||||
|
|
@ -7589,10 +7838,26 @@ ensure_sops_age_tools() {
|
||||||
sudo apt-get install -y --no-install-recommends "${missing_packages[@]}"
|
sudo apt-get install -y --no-install-recommends "${missing_packages[@]}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# age must be present; it's small and apt-provided on Debian.
|
||||||
if ! command -v age-keygen >/dev/null 2>&1; then
|
if ! command -v age-keygen >/dev/null 2>&1; then
|
||||||
echo "age-keygen is still unavailable after package installation." >&2
|
echo "age-keygen is still unavailable after package installation." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# sops: not packaged on Debian trixie — fall back to the GitHub release binary.
|
||||||
|
if ! command -v sops >/dev/null 2>&1; then
|
||||||
|
echo "sops not found in the apt repos; installing from GitHub release..."
|
||||||
|
local sops_ver sops_url
|
||||||
|
sops_ver="$(curl -fsSL https://api.github.com/repos/getsops/sops/releases/latest | grep -oE '"tag_name":\s*"v[^"]+"' | head -1 | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+')"
|
||||||
|
if [[ -z "${sops_ver}" ]]; then
|
||||||
|
echo "Could not determine the latest SOPS release version." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sops_url="https://github.com/getsops/sops/releases/download/${sops_ver}/sops-v${sops_ver#v}.linux.amd64"
|
||||||
|
curl -fsSL -o "/tmp/sops-${sops_ver}" "${sops_url}" && \
|
||||||
|
sudo install -m 0755 "/tmp/sops-${sops_ver}" /usr/local/bin/sops && \
|
||||||
|
rm -f "/tmp/sops-${sops_ver}"
|
||||||
|
fi
|
||||||
if ! command -v sops >/dev/null 2>&1; then
|
if ! command -v sops >/dev/null 2>&1; then
|
||||||
echo "sops is still unavailable after package installation." >&2
|
echo "sops is still unavailable after package installation." >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -7609,6 +7874,56 @@ sops_age_recipient() {
|
||||||
awk -F': ' '/^# public key:/ { print $2; exit }' "${key_file}"
|
awk -F': ' '/^# public key:/ { print $2; exit }' "${key_file}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Decrypt a SOPS-encrypted YAML of `KEY: value` pairs into the current env.
|
||||||
|
# Usage: sops_load_secrets <repo/path/to/file.secret.yaml>
|
||||||
|
# Requires the age key (sops_age_key_file) to decrypt; fails loudly if missing.
|
||||||
|
sops_load_secrets() {
|
||||||
|
local file="${1:?sops_load_secrets: missing file}"
|
||||||
|
local key_file
|
||||||
|
local out
|
||||||
|
|
||||||
|
if [[ ! -f "${file}" ]]; then
|
||||||
|
echo "Missing encrypted secret file: ${file} (run ./jeannie secrets-init then sops -e)." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! sops_available; then
|
||||||
|
echo "sops not installed. Run ./jeannie secrets-init first." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
key_file="$(sops_age_key_file)"
|
||||||
|
if [[ ! -s "${key_file}" ]]; then
|
||||||
|
echo "Missing SOPS age key at ${key_file}. Run ./jeannie secrets-init, or copy the age key to this machine." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
out="$(SOPS_AGE_KEY_FILE="${key_file}" sops --decrypt "${file}")" || {
|
||||||
|
echo "SOPS decryption failed for ${file}." >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Export every bare `KEY: value` line (ignore comments/blank).
|
||||||
|
local line key val syn="0"
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[[ "${line}" =~ ^[A-Za-z_][A-Za-z0-9_]*: ]] || continue
|
||||||
|
key="${line%%:*}"
|
||||||
|
val="${line#*: }"
|
||||||
|
# Strip surrounding double quotes, if sops kept them.
|
||||||
|
val="${val%\"}"; val="${val#\"}"
|
||||||
|
export "${key}=${val}"
|
||||||
|
syn="$((syn + 1))"
|
||||||
|
done <<<"${out}"
|
||||||
|
|
||||||
|
if ((syn == 0)); then
|
||||||
|
echo "No KEY: value pairs found in decrypted ${file}." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
sops_available() {
|
||||||
|
command -v sops >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
secrets_init() {
|
secrets_init() {
|
||||||
local key_file
|
local key_file
|
||||||
local key_dir
|
local key_dir
|
||||||
|
|
@ -8108,6 +8423,9 @@ Build And Bootstrap
|
||||||
up Deploy the full homelab pipeline.
|
up Deploy the full homelab pipeline.
|
||||||
deploy-gitea Deploy the Debian-hosted Gitea container.
|
deploy-gitea Deploy the Debian-hosted Gitea container.
|
||||||
deploy-heimdall Deploy the Debian-hosted Heimdall dashboard.
|
deploy-heimdall Deploy the Debian-hosted Heimdall dashboard.
|
||||||
|
deploy-matrix Deploy the Matrix homeserver (Synapse + Element + Postgres).
|
||||||
|
deploy-omniroute Deploy the OmniRoute LLM routing gateway.
|
||||||
|
deploy-hermes Deploy the Hermes agent (git-install + venv + gateway service).
|
||||||
bootstrap-gitea-repo Ensure the Gitea repo and SSH key wiring exist.
|
bootstrap-gitea-repo Ensure the Gitea repo and SSH key wiring exist.
|
||||||
rpi-services Deploy Pi-hole, Unbound, and Uptime Kuma on RPi4.
|
rpi-services Deploy Pi-hole, Unbound, and Uptime Kuma on RPi4.
|
||||||
ollama-setup Install/configure Ollama on the Debian host.
|
ollama-setup Install/configure Ollama on the Debian host.
|
||||||
|
|
@ -8395,6 +8713,18 @@ case "${1:-}" in
|
||||||
record_change_journal "deploy-heimdall" "$@"
|
record_change_journal "deploy-heimdall" "$@"
|
||||||
deploy_heimdall
|
deploy_heimdall
|
||||||
;;
|
;;
|
||||||
|
deploy-matrix)
|
||||||
|
record_change_journal "deploy-matrix" "$@"
|
||||||
|
deploy_matrix
|
||||||
|
;;
|
||||||
|
deploy-omniroute)
|
||||||
|
record_change_journal "deploy-omniroute" "$@"
|
||||||
|
deploy_omniroute
|
||||||
|
;;
|
||||||
|
deploy-hermes)
|
||||||
|
record_change_journal "deploy-hermes" "$@"
|
||||||
|
deploy_hermes
|
||||||
|
;;
|
||||||
rpi-services)
|
rpi-services)
|
||||||
record_change_journal "rpi-services" "$@"
|
record_change_journal "rpi-services" "$@"
|
||||||
deploy_rpi_services
|
deploy_rpi_services
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue