Compare commits
190 Commits
319b515882
...
55fa2cbce4
| Author | SHA1 | Date | |
|---|---|---|---|
| 55fa2cbce4 | |||
| d5a526c5fa | |||
| efd258fc71 | |||
| 3852ebc0f1 | |||
| 88db462f8f | |||
| e44def25f8 | |||
| 7ae8bf9705 | |||
| 088fed6720 | |||
| 84e4dc0616 | |||
| 96a8d271a9 | |||
| 84aa870cda | |||
| 876ec19543 | |||
| ec1d33f1ca | |||
| 1de9d94138 | |||
| 571bf759a2 | |||
| 7525289a0c | |||
| c7b73555c4 | |||
| de727eee07 | |||
| 2122ce3e31 | |||
| f2d496c6c0 | |||
| 127d09755e | |||
| 9f5e61ebed | |||
| b1b39c4dcd | |||
| 65d8986279 | |||
| b9202b6829 | |||
| 1e8de60198 | |||
| 2906e3e5d9 | |||
| 7210c0784d | |||
| 46b6d471eb | |||
| 7e46ffc075 | |||
| d8f466e53e | |||
| ffdb4ed010 | |||
| 5af23034de | |||
| 72a83a1af9 | |||
| 42b3ac0139 | |||
| e53ca4dd91 | |||
| 134e39d9a4 | |||
| 12fd5229dc | |||
| 1963fadec1 | |||
| d23e2fe78c | |||
| e7d521f203 | |||
| 54e4a1ed93 | |||
| 9895695b36 | |||
| 2fc73097ba | |||
| 7b1cc7061a | |||
| f44370c41f | |||
| 3fbaa54f4f | |||
| ea60425d42 | |||
| a8cb8c0287 | |||
| f7f124ad71 | |||
| d062c10675 | |||
| 97b7b479bc | |||
| 0b44f2d1d4 | |||
| bcda1b396d | |||
| a15ee26ae2 | |||
| 1970b820e7 | |||
| e4f0eeca99 | |||
| 00e9c90746 | |||
| b1d84d646a | |||
| 7e4b2f8ba2 | |||
| a028fde4f7 | |||
| 703e1d4e3c | |||
| 16f8b5f30b | |||
| ebfeb78e87 | |||
| d5e1003de8 | |||
| a411694bda | |||
| 1df06f18f6 | |||
| 9bd7effdee | |||
| 991d6defc4 | |||
| 43b9265cdf | |||
| 9233ba60fc | |||
| ccca363fb4 | |||
| f22c19bc5d | |||
| 0e9b293e95 | |||
| 5a2cafb5db | |||
| 5ce1493b3b | |||
| c85c6b1bc3 | |||
| 64059a08f5 | |||
| 2073ffe944 | |||
| a99e1ba227 | |||
| 8d42f501e5 | |||
| 7358f9e618 | |||
| 831d1fe707 | |||
| 8c263b36b9 | |||
| bf31272339 | |||
| a34e58d319 | |||
| 6a60e4284a | |||
| 0f7d0b7bac | |||
| 665dfa2e52 | |||
| 5858a80c72 | |||
| d844e068ec | |||
| 77c3e260a3 | |||
| 2e6b9a47c8 | |||
| 48f9c6d715 | |||
| da82ebd469 | |||
| 37b93de3e7 | |||
| 89c0fbfd44 | |||
| cb402d0bb9 | |||
| 597556d1c0 | |||
| f886e2b873 | |||
| 94f0cd939d | |||
| bc757265cf | |||
| 4d3a4cd2b4 | |||
| ac7863802a | |||
| afb926439f | |||
| ebf5a8aef9 | |||
| dca749cc04 | |||
| 65b3e3fbb8 | |||
| 45ad2a2b06 | |||
| 396acb818a | |||
| aae55a14f8 | |||
| 8ac040a7d8 | |||
| 79a17412af | |||
| 1bdc0efdac | |||
| 8b6ddcd44d | |||
| ffbfee1ebd | |||
| 85aa07c0cc | |||
| e2e2916139 | |||
| 077654fa2d | |||
| 3c229baece | |||
| 48995cc6ed | |||
| c94959a687 | |||
| d992be1061 | |||
| 79d71f471f | |||
| 8f724e02be | |||
| d2ffd738ef | |||
| 16b2c15eda | |||
| 761fdd29b2 | |||
| 4567b1685c | |||
| 2182e98c05 | |||
| 503a95a8e8 | |||
| 9dfe6bb700 | |||
| 358da0ea00 | |||
| 3b50199e1d | |||
| 5b97966395 | |||
| 9a34ee3d2e | |||
| 53d3079bce | |||
| 259451e273 | |||
| 518d7bb160 | |||
| 632949c29c | |||
| 6a77f7749f | |||
| 16997fba10 | |||
| 3637a99bfb | |||
| 7e2baa343c | |||
| 02bde10852 | |||
| e224215406 | |||
| 03d43d097b | |||
| ca62df5508 | |||
| 2f68bc664a | |||
| 3878d39579 | |||
| 19ae80e5e0 | |||
| 46f02ee826 | |||
| e34744d144 | |||
| fdbd8ef048 | |||
| 535c3de0bf | |||
| 2be629a998 | |||
| 0b5aed217d | |||
| eb6aeae2d2 | |||
| 3276e4f196 | |||
| e31bf05cc1 | |||
| e0169b5bba | |||
| ba140fb638 | |||
| 10b34c353b | |||
| 26e15f7651 | |||
| 22683b0dc4 | |||
| 7468e62023 | |||
| 0d492eb622 | |||
| c8a91ebe4f | |||
| ee3b0f3f25 | |||
| ab02f4537e | |||
| f51c06efac | |||
| 773637273d | |||
| 8b1c083fe0 | |||
| 128fad192c | |||
| eac7aaa91b | |||
| 28903add8f | |||
| eea64c7eb1 | |||
| c7a184eace | |||
| ba233fd909 | |||
| 04cd5b0c62 | |||
| ec744e45bf | |||
| b16eda5894 | |||
| 1ba463001a | |||
| 2304c41ba8 | |||
| 7ca10afce7 | |||
| ead0c486a5 | |||
| 1de7fcc287 | |||
| 7efc4a4dfb | |||
| 19bfa0878c | |||
| fab2d944ff |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
AGENTS.md
|
||||
12
clusters/atlas/applications/kustomization.yaml
Normal file
12
clusters/atlas/applications/kustomization.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
# clusters/atlas/applications/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- ../../services/crypto
|
||||
- ../../services/gitea
|
||||
- ../../services/jellyfin
|
||||
- ../../services/jitsi
|
||||
- ../../services/monitoring
|
||||
- ../../services/pegasus
|
||||
- ../../services/vault
|
||||
- ../../services/zot
|
||||
@ -1,4 +1,4 @@
|
||||
# infrastructure/flux-system/kustomization-crypto.yaml
|
||||
# clusters/atlas/flux-system/applications/crypto/kustomization.yaml
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
@ -1,4 +1,4 @@
|
||||
# infrastructure/flux-system/kustomization-gitea.yaml
|
||||
# clusters/atlas/flux-system/applications/gitea/kustomization.yaml
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
@ -1,4 +1,4 @@
|
||||
# infrastructure/flux-system/kustomization-jellyfin.yaml
|
||||
# clusters/atlas/flux-system/applications/jellyfin/kustomization.yaml
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
@ -0,0 +1,19 @@
|
||||
# clusters/atlas/flux-system/applications/jitsi/kustomization.yaml
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: jitsi
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 10m
|
||||
path: ./services/jitsi
|
||||
targetNamespace: jitsi
|
||||
prune: true
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: flux-system
|
||||
namespace: flux-system
|
||||
dependsOn:
|
||||
- name: core
|
||||
wait: true
|
||||
timeout: 5m
|
||||
@ -0,0 +1,15 @@
|
||||
# clusters/atlas/flux-system/applications/keycloak/kustomization.yaml
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: keycloak
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 10m
|
||||
prune: true
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: flux-system
|
||||
path: ./services/keycloak
|
||||
targetNamespace: sso
|
||||
timeout: 2m
|
||||
17
clusters/atlas/flux-system/applications/kustomization.yaml
Normal file
17
clusters/atlas/flux-system/applications/kustomization.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
# clusters/atlas/flux-system/applications/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- zot/kustomization.yaml
|
||||
- gitea/kustomization.yaml
|
||||
- vault/kustomization.yaml
|
||||
- jitsi/kustomization.yaml
|
||||
- crypto/kustomization.yaml
|
||||
- monerod/kustomization.yaml
|
||||
- pegasus/kustomization.yaml
|
||||
- pegasus/image-automation.yaml
|
||||
- jellyfin/kustomization.yaml
|
||||
- xmr-miner/kustomization.yaml
|
||||
- sui-metrics/kustomization.yaml
|
||||
- keycloak/kustomization.yaml
|
||||
- oauth2-proxy/kustomization.yaml
|
||||
@ -1,4 +1,4 @@
|
||||
# infrastructure/flux-system/kustomization-monerod.yaml
|
||||
# clusters/atlas/flux-system/applications/monerod/kustomization.yaml
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
@ -0,0 +1,15 @@
|
||||
# clusters/atlas/flux-system/applications/oauth2-proxy/kustomization.yaml
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: oauth2-proxy
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 10m
|
||||
prune: true
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: flux-system
|
||||
path: ./services/oauth2-proxy
|
||||
targetNamespace: sso
|
||||
timeout: 2m
|
||||
@ -0,0 +1,20 @@
|
||||
# clusters/atlas/flux-system/applications/pegasus/image-automation.yaml
|
||||
apiVersion: image.toolkit.fluxcd.io/v1beta1
|
||||
kind: ImageUpdateAutomation
|
||||
metadata:
|
||||
name: pegasus
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 1m0s
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: flux-system
|
||||
git:
|
||||
commit:
|
||||
author:
|
||||
email: ops@bstein.dev
|
||||
name: flux-bot
|
||||
messageTemplate: "chore(pegasus): update image to {{range .Updated.Images}}{{.}}{{end}}"
|
||||
update:
|
||||
strategy: Setters
|
||||
path: ./services/pegasus
|
||||
@ -0,0 +1,19 @@
|
||||
# clusters/atlas/flux-system/applications/pegasus/kustomization.yaml
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: pegasus
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 10m
|
||||
path: ./services/pegasus
|
||||
targetNamespace: jellyfin
|
||||
prune: true
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: flux-system
|
||||
namespace: flux-system
|
||||
dependsOn:
|
||||
- name: core
|
||||
wait: true
|
||||
timeout: 5m
|
||||
@ -0,0 +1,19 @@
|
||||
# clusters/atlas/flux-system/applications/sui-metrics/kustomization.yaml
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: sui-metrics
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 10m
|
||||
path: ./services/sui-metrics/overlays/atlas
|
||||
prune: true
|
||||
dependsOn:
|
||||
- name: monitoring
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: flux-system
|
||||
namespace: flux-system
|
||||
wait: true
|
||||
timeout: 5m
|
||||
targetNamespace: sui-metrics
|
||||
@ -1,4 +1,4 @@
|
||||
# infrastructure/flux-system/kustomization-vault.yaml
|
||||
# clusters/atlas/flux-system/applications/vault/kustomization.yaml
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
@ -1,4 +1,4 @@
|
||||
# infrastructure/flux-system/kustomization-core.yaml
|
||||
# clusters/atlas/flux-system/applications/xmr-miner/kustomization.yaml
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
@ -1,4 +1,4 @@
|
||||
# infrastructure/flux-system/kustomization-zot.yaml
|
||||
# clusters/atlas/flux-system/applications/zot/kustomization.yaml
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
@ -1,6 +1,6 @@
|
||||
---
|
||||
# This manifest was generated by flux. DO NOT EDIT.
|
||||
# Flux Version: v2.5.1
|
||||
# Flux Version: v2.5.1f reconzaq1= zaq1= aq1= 1= w2cile kustomization flux-system --namespace flux-system --with-source
|
||||
# Components: source-controller,kustomize-controller,helm-controller,notification-controller
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
@ -8,7 +8,7 @@ metadata:
|
||||
spec:
|
||||
interval: 1m0s
|
||||
ref:
|
||||
branch: main
|
||||
branch: feature/sso
|
||||
secretRef:
|
||||
name: flux-system-gitea
|
||||
url: ssh://git@scm.bstein.dev:2242/bstein/titan-iac.git
|
||||
@ -20,7 +20,7 @@ metadata:
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 10m0s
|
||||
path: ./
|
||||
path: ./clusters/atlas/flux-system
|
||||
prune: true
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
8
clusters/atlas/flux-system/kustomization.yaml
Normal file
8
clusters/atlas/flux-system/kustomization.yaml
Normal file
@ -0,0 +1,8 @@
|
||||
# clusters/atlas/flux-system/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- gotk-components.yaml
|
||||
- gotk-sync.yaml
|
||||
- platform
|
||||
- applications
|
||||
15
clusters/atlas/flux-system/platform/core/kustomization.yaml
Normal file
15
clusters/atlas/flux-system/platform/core/kustomization.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
# clusters/atlas/flux-system/platform/core/kustomization.yaml
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: core
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 10m
|
||||
path: ./infrastructure/core
|
||||
prune: true
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: flux-system
|
||||
namespace: flux-system
|
||||
wait: false
|
||||
@ -1,4 +1,4 @@
|
||||
# infrastructure/flux-system/kustomization-helm.yaml
|
||||
# clusters/atlas/flux-system/platform/helm/kustomization.yaml
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
9
clusters/atlas/flux-system/platform/kustomization.yaml
Normal file
9
clusters/atlas/flux-system/platform/kustomization.yaml
Normal file
@ -0,0 +1,9 @@
|
||||
# clusters/atlas/flux-system/platform/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- core/kustomization.yaml
|
||||
- helm/kustomization.yaml
|
||||
- traefik/kustomization.yaml
|
||||
- monitoring/kustomization.yaml
|
||||
- longhorn-ui/kustomization.yaml
|
||||
@ -1,3 +1,4 @@
|
||||
# clusters/atlas/flux-system/platform/longhorn-ui/kustomization.yaml
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
@ -0,0 +1,14 @@
|
||||
# clusters/atlas/flux-system/platform/monitoring/kustomization.yaml
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: monitoring
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 10m
|
||||
path: ./services/monitoring
|
||||
prune: true
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: flux-system
|
||||
wait: false
|
||||
@ -0,0 +1,18 @@
|
||||
# clusters/atlas/flux-system/platform/traefik/kustomization.yaml
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: traefik
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 10m
|
||||
path: ./infrastructure/traefik
|
||||
targetNamespace: traefik
|
||||
prune: true
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: flux-system
|
||||
namespace: flux-system
|
||||
dependsOn:
|
||||
- name: core
|
||||
wait: true
|
||||
7
clusters/atlas/platform/kustomization.yaml
Normal file
7
clusters/atlas/platform/kustomization.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
# clusters/atlas/platform/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- ../../../infrastructure/modules/base
|
||||
- ../../../infrastructure/modules/profiles/atlas-ha
|
||||
- ../../../infrastructure/sources/cert-manager/letsencrypt.yaml
|
||||
5
clusters/oceanus/README.md
Normal file
5
clusters/oceanus/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Oceanus Cluster Scaffold
|
||||
|
||||
This directory prepares the Flux and Kustomize layout for a future Oceanus-managed cluster.
|
||||
Populate `flux-system/` with `gotk-components.yaml` and related manifests after running `flux bootstrap`.
|
||||
Define node-specific resources under `infrastructure/modules/profiles/oceanus-validator/` and reference workloads in `applications/` as they come online.
|
||||
4
clusters/oceanus/applications/kustomization.yaml
Normal file
4
clusters/oceanus/applications/kustomization.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
# clusters/oceanus/applications/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources: []
|
||||
9
clusters/oceanus/flux-system/kustomization.yaml
Normal file
9
clusters/oceanus/flux-system/kustomization.yaml
Normal file
@ -0,0 +1,9 @@
|
||||
# clusters/oceanus/flux-system/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
# Populate when oceanus cluster is bootstrapped with Flux.
|
||||
# - gotk-components.yaml
|
||||
# - gotk-sync.yaml
|
||||
- ../platform
|
||||
- ../applications
|
||||
6
clusters/oceanus/platform/kustomization.yaml
Normal file
6
clusters/oceanus/platform/kustomization.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
# clusters/oceanus/platform/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- ../../infrastructure/modules/base
|
||||
- ../../infrastructure/modules/profiles/oceanus-validator
|
||||
16
docs/topology.md
Normal file
16
docs/topology.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Titan Homelab Topology
|
||||
|
||||
| Hostname | Role / Function | Managed By | Notes |
|
||||
|------------|--------------------------------|---------------------|-------|
|
||||
| titan-0a | Kubernetes control-plane | Flux (atlas cluster)| HA leader, tainted for control only |
|
||||
| titan-0b | Kubernetes control-plane | Flux (atlas cluster)| Standby control node |
|
||||
| titan-0c | Kubernetes control-plane | Flux (atlas cluster)| Standby control node |
|
||||
| titan-04-19| Raspberry Pi workers | Flux (atlas cluster)| Workload nodes, labelled per hardware |
|
||||
| titan-22 | GPU mini-PC (Jellyfin) | Flux + Ansible | NVIDIA runtime managed via `modules/profiles/atlas-ha` |
|
||||
| titan-24 | Tethys hybrid node | Flux + Ansible | Runs SUI metrics via K8s, validator via Ansible |
|
||||
| titan-db | HA control plane database | Ansible | PostgreSQL / etcd backing services |
|
||||
| titan-jh | Jumphost & bastion | Ansible | Entry point / future KVM services |
|
||||
| oceanus | Dedicated SUI validator host | Ansible / Flux prep | Baremetal validator workloads, exposes metrics to atlas; Kustomize scaffold under `clusters/oceanus/` |
|
||||
| styx | Air-gapped workstation | Manual / Scripts | Remains isolated, scripts tracked in `hosts/styx` |
|
||||
|
||||
Use the `clusters/` directory for cluster-scoped state and the `hosts/` directory for baremetal orchestration.
|
||||
2
hosts/group_vars/all.yaml
Normal file
2
hosts/group_vars/all.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
# hosts/group_vars/all.yaml
|
||||
validator_version: latest
|
||||
2
hosts/host_vars/titan-24.yaml
Normal file
2
hosts/host_vars/titan-24.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
# hosts/host_vars/titan-24.yaml
|
||||
validator_compose_path: /opt/sui-validator
|
||||
28
hosts/inventory/lab.yaml
Normal file
28
hosts/inventory/lab.yaml
Normal file
@ -0,0 +1,28 @@
|
||||
# hosts/inventory/lab.yaml
|
||||
# Replace ansible_host and ansible_user values with real connectivity details.
|
||||
all:
|
||||
children:
|
||||
atlas:
|
||||
hosts:
|
||||
titan-24:
|
||||
ansible_host: REPLACE_ME
|
||||
ansible_user: ubuntu
|
||||
roleset: tethys_hybrid
|
||||
titan-22:
|
||||
ansible_host: REPLACE_ME
|
||||
ansible_user: debian
|
||||
roleset: minipc_gpu
|
||||
baremetal:
|
||||
hosts:
|
||||
titan-db:
|
||||
ansible_host: REPLACE_ME
|
||||
ansible_user: postgres
|
||||
roleset: database
|
||||
titan-jh:
|
||||
ansible_host: REPLACE_ME
|
||||
ansible_user: jump
|
||||
roleset: jumphost
|
||||
oceanus:
|
||||
ansible_host: REPLACE_ME
|
||||
ansible_user: validator
|
||||
roleset: validator
|
||||
29
hosts/playbooks/site.yaml
Normal file
29
hosts/playbooks/site.yaml
Normal file
@ -0,0 +1,29 @@
|
||||
# hosts/playbooks/site.yaml
|
||||
---
|
||||
- name: Configure titan-db
|
||||
hosts: titan-db
|
||||
gather_facts: true
|
||||
roles:
|
||||
- common
|
||||
- titan_db
|
||||
|
||||
- name: Configure titan-jh
|
||||
hosts: titan-jh
|
||||
gather_facts: true
|
||||
roles:
|
||||
- common
|
||||
- titan_jh
|
||||
|
||||
- name: Configure oceanus validator host
|
||||
hosts: oceanus
|
||||
gather_facts: true
|
||||
roles:
|
||||
- common
|
||||
- oceanus_base
|
||||
|
||||
- name: Prepare hybrid tethys node
|
||||
hosts: titan-24
|
||||
gather_facts: true
|
||||
roles:
|
||||
- common
|
||||
- tethys_canary
|
||||
9
hosts/roles/common/tasks/main.yaml
Normal file
9
hosts/roles/common/tasks/main.yaml
Normal file
@ -0,0 +1,9 @@
|
||||
# hosts/roles/common/tasks/main.yaml
|
||||
---
|
||||
- name: Ensure base packages present
|
||||
ansible.builtin.package:
|
||||
name:
|
||||
- curl
|
||||
- vim
|
||||
state: present
|
||||
tags: ['common', 'packages']
|
||||
6
hosts/roles/oceanus_base/tasks/main.yaml
Normal file
6
hosts/roles/oceanus_base/tasks/main.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
# hosts/roles/oceanus_base/tasks/main.yaml
|
||||
---
|
||||
- name: Placeholder for oceanus base configuration
|
||||
ansible.builtin.debug:
|
||||
msg: "Install validator prerequisites and monitoring exporters here."
|
||||
tags: ['oceanus']
|
||||
6
hosts/roles/tethys_canary/tasks/main.yaml
Normal file
6
hosts/roles/tethys_canary/tasks/main.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
# hosts/roles/tethys_canary/tasks/main.yaml
|
||||
---
|
||||
- name: Placeholder for SUI validator container runtime setup
|
||||
ansible.builtin.debug:
|
||||
msg: "Configure container runtime and validator compose stack here."
|
||||
tags: ['tethys', 'validator']
|
||||
6
hosts/roles/titan_db/tasks/main.yaml
Normal file
6
hosts/roles/titan_db/tasks/main.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
# hosts/roles/titan_db/tasks/main.yaml
|
||||
---
|
||||
- name: Placeholder for titan-db provisioning
|
||||
ansible.builtin.debug:
|
||||
msg: "Install database packages, configure backups, and manage users here."
|
||||
tags: ['titan_db']
|
||||
6
hosts/roles/titan_jh/tasks/main.yaml
Normal file
6
hosts/roles/titan_jh/tasks/main.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
# hosts/roles/titan_jh/tasks/main.yaml
|
||||
---
|
||||
- name: Placeholder for jumphost hardening
|
||||
ansible.builtin.debug:
|
||||
msg: "Harden SSH, manage bastion tooling, and configure audit logging here."
|
||||
tags: ['jumphost']
|
||||
2
hosts/styx/README.md
Normal file
2
hosts/styx/README.md
Normal file
@ -0,0 +1,2 @@
|
||||
# hosts/styx/README.md
|
||||
Styx is air-gapped; provisioning scripts live under `scripts/`.
|
||||
@ -1,5 +0,0 @@
|
||||
# infrastructure/core/gpu/daemonsets/profiles/jetson-only/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- ../../device-plugin-jetson
|
||||
@ -1,6 +0,0 @@
|
||||
# infrastructure/core/gpu/daemonsets/profiles/minipc-and-jetson/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- ../../device-plugin-minipc
|
||||
- ../../device-plugin-jetson
|
||||
@ -1,5 +0,0 @@
|
||||
# infrastructure/core/gpu/daemonsets/profiles/minipc-only/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- ../../device-plugin-minipc
|
||||
@ -2,7 +2,6 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- base
|
||||
# - gpu/profiles/jetson-only
|
||||
# - gpu/profiles/minipc-and-jetson
|
||||
- gpu/profiles/minipc-only
|
||||
- ../modules/base
|
||||
- ../modules/profiles/atlas-ha
|
||||
- ../sources/cert-manager/letsencrypt.yaml
|
||||
|
||||
@ -1,22 +0,0 @@
|
||||
# infrastructure/flux-system/kustomization-core.yaml
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: core
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 10m
|
||||
path: ./infrastructure/core
|
||||
prune: true
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: flux-system
|
||||
namespace: flux-system
|
||||
wait: true
|
||||
|
||||
# Only wait for the NVIDIA device-plugin DaemonSet on titan-22
|
||||
healthChecks:
|
||||
- apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
name: nvidia-device-plugin-minipc
|
||||
namespace: kube-system
|
||||
@ -2,15 +2,4 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- gotk-components.yaml
|
||||
- gotk-sync.yaml
|
||||
- kustomization-zot.yaml
|
||||
- kustomization-core.yaml
|
||||
- kustomization-helm.yaml
|
||||
- kustomization-gitea.yaml
|
||||
- kustomization-vault.yaml
|
||||
- kustomization-crypto.yaml
|
||||
- kustomization-monerod.yaml
|
||||
- kustomization-jellyfin.yaml
|
||||
- kustomization-xmr-miner.yaml
|
||||
- kustomization-longhorn-ui.yaml
|
||||
- ../clusters/atlas/flux-system
|
||||
|
||||
@ -7,7 +7,7 @@ metadata:
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
traefik.ingress.kubernetes.io/router.middlewares: longhorn-system-longhorn-basicauth@kubernetescrd,longhorn-system-longhorn-headers@kubernetescrd
|
||||
traefik.ingress.kubernetes.io/router.middlewares: ""
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
tls:
|
||||
@ -21,6 +21,6 @@ spec:
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: longhorn-frontend
|
||||
name: oauth2-proxy-longhorn
|
||||
port:
|
||||
number: 80
|
||||
|
||||
@ -4,3 +4,4 @@ kind: Kustomization
|
||||
resources:
|
||||
- middleware.yaml
|
||||
- ingress.yaml
|
||||
- oauth2-proxy-longhorn.yaml
|
||||
|
||||
@ -20,3 +20,20 @@ spec:
|
||||
headers:
|
||||
customRequestHeaders:
|
||||
X-Forwarded-Proto: "https"
|
||||
|
||||
---
|
||||
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: longhorn-forward-auth
|
||||
namespace: longhorn-system
|
||||
spec:
|
||||
forwardAuth:
|
||||
address: https://auth.bstein.dev/oauth2/auth
|
||||
trustForwardHeader: true
|
||||
authResponseHeaders:
|
||||
- Authorization
|
||||
- X-Auth-Request-Email
|
||||
- X-Auth-Request-User
|
||||
- X-Auth-Request-Groups
|
||||
|
||||
102
infrastructure/longhorn/ui-ingress/oauth2-proxy-longhorn.yaml
Normal file
102
infrastructure/longhorn/ui-ingress/oauth2-proxy-longhorn.yaml
Normal file
@ -0,0 +1,102 @@
|
||||
# infrastructure/longhorn/ui-ingress/oauth2-proxy-longhorn.yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: oauth2-proxy-longhorn
|
||||
namespace: longhorn-system
|
||||
labels:
|
||||
app: oauth2-proxy-longhorn
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 4180
|
||||
selector:
|
||||
app: oauth2-proxy-longhorn
|
||||
|
||||
---
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: oauth2-proxy-longhorn
|
||||
namespace: longhorn-system
|
||||
labels:
|
||||
app: oauth2-proxy-longhorn
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: oauth2-proxy-longhorn
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: oauth2-proxy-longhorn
|
||||
spec:
|
||||
nodeSelector:
|
||||
node-role.kubernetes.io/worker: "true"
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 90
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: hardware
|
||||
operator: In
|
||||
values: ["rpi5","rpi4"]
|
||||
containers:
|
||||
- name: oauth2-proxy
|
||||
image: quay.io/oauth2-proxy/oauth2-proxy:v7.6.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- --provider=oidc
|
||||
- --redirect-url=https://longhorn.bstein.dev/oauth2/callback
|
||||
- --oidc-issuer-url=https://sso.bstein.dev/realms/atlas
|
||||
- --scope=openid profile email groups
|
||||
- --email-domain=*
|
||||
- --allowed-group=admin
|
||||
- --set-xauthrequest=true
|
||||
- --pass-access-token=true
|
||||
- --set-authorization-header=true
|
||||
- --cookie-secure=true
|
||||
- --cookie-samesite=lax
|
||||
- --cookie-refresh=20m
|
||||
- --cookie-expire=168h
|
||||
- --insecure-oidc-allow-unverified-email=true
|
||||
- --upstream=http://longhorn-frontend.longhorn-system.svc.cluster.local
|
||||
- --http-address=0.0.0.0:4180
|
||||
- --skip-provider-button=true
|
||||
- --skip-jwt-bearer-tokens=true
|
||||
- --oidc-groups-claim=groups
|
||||
- --cookie-domain=longhorn.bstein.dev
|
||||
env:
|
||||
- name: OAUTH2_PROXY_CLIENT_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: oauth2-proxy-longhorn-oidc
|
||||
key: client_id
|
||||
- name: OAUTH2_PROXY_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: oauth2-proxy-longhorn-oidc
|
||||
key: client_secret
|
||||
- name: OAUTH2_PROXY_COOKIE_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: oauth2-proxy-longhorn-oidc
|
||||
key: cookie_secret
|
||||
ports:
|
||||
- containerPort: 4180
|
||||
name: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 4180
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /ping
|
||||
port: 4180
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 20
|
||||
@ -1,4 +1,4 @@
|
||||
# infrastructure/core/base/kustomization.yaml
|
||||
# infrastructure/modules/base/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
@ -1,4 +1,4 @@
|
||||
# infrastructure/core/base/priorityclass/kustomization.yaml
|
||||
# infrastructure/modules/base/priorityclass/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
@ -1,4 +1,4 @@
|
||||
# infrastructure/core/base/priorityclass/scavenger.yaml
|
||||
# infrastructure/modules/base/priorityclass/scavenger.yaml
|
||||
apiVersion: scheduling.k8s.io/v1
|
||||
kind: PriorityClass
|
||||
metadata:
|
||||
@ -1,4 +1,4 @@
|
||||
# infrastructure/core/base/storageclass/kustomization.yaml
|
||||
# infrastructure/modules/base/runtimeclass/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
@ -1,4 +1,4 @@
|
||||
# services/jellyfin/runtimeclass.yaml
|
||||
# infrastructure/modules/base/runtimeclass/runtimeclass.yaml
|
||||
apiVersion: node.k8s.io/v1
|
||||
kind: RuntimeClass
|
||||
metadata:
|
||||
@ -1,4 +1,4 @@
|
||||
# infrastructure/core/base/storageclass/asteria.yaml
|
||||
# infrastructure/modules/base/storageclass/asteria.yaml
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
metadata:
|
||||
@ -8,6 +8,9 @@ parameters:
|
||||
fromBackup: ""
|
||||
numberOfReplicas: "2"
|
||||
staleReplicaTimeout: "30"
|
||||
fsType: "ext4"
|
||||
replicaAutoBalance: "least-effort"
|
||||
dataLocality: "disabled"
|
||||
provisioner: driver.longhorn.io
|
||||
reclaimPolicy: Retain
|
||||
allowVolumeExpansion: true
|
||||
@ -1,4 +1,4 @@
|
||||
# infrastructure/core/base/storageclass/astreae.yaml
|
||||
# infrastructure/modules/base/storageclass/astreae.yaml
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
metadata:
|
||||
@ -1,4 +1,4 @@
|
||||
# infrastructure/core/base/storageclass/kustomization.yaml
|
||||
# infrastructure/modules/base/storageclass/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
@ -0,0 +1,7 @@
|
||||
# infrastructure/modules/profiles/atlas-ha/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- ../components/device-plugin-jetson
|
||||
- ../components/device-plugin-minipc
|
||||
- ../components/device-plugin-tethys
|
||||
@ -1,4 +1,4 @@
|
||||
# infrastructure/core/gpu/daemonsets/device-plugin-jetson/daemonset.yaml
|
||||
# infrastructure/modules/profiles/components/device-plugin-jetson/daemonset.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
@ -1,4 +1,4 @@
|
||||
# infrastructure/core/gpu/daemonsets/device-plugin-jetson/kustomization.yaml
|
||||
# infrastructure/modules/profiles/components/device-plugin-jetson/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
@ -1,4 +1,4 @@
|
||||
# infrastructure/core/gpu/daemonsets/device-plugin-minipc/daemonset.yaml
|
||||
# infrastructure/modules/profiles/components/device-plugin-minipc/daemonset.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
@ -24,7 +24,6 @@ spec:
|
||||
tolerations:
|
||||
- operator: Exists
|
||||
priorityClassName: system-node-critical
|
||||
runtimeClassName: nvidia
|
||||
containers:
|
||||
- name: nvidia-device-plugin-ctr
|
||||
image: nvcr.io/nvidia/k8s-device-plugin:v0.16.2
|
||||
@ -1,4 +1,4 @@
|
||||
# infrastructure/core/gpu/daemonsets/device-plugin-minipc/kustomization.yaml
|
||||
# infrastructure/modules/profiles/components/device-plugin-minipc/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
@ -0,0 +1,49 @@
|
||||
# infrastructure/modules/profiles/components/device-plugin-tethys/daemonset.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: nvidia-device-plugin-tethys
|
||||
namespace: kube-system
|
||||
labels:
|
||||
app.kubernetes.io/name: nvidia-device-plugin
|
||||
app.kubernetes.io/instance: titan24
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: nvidia-device-plugin
|
||||
app.kubernetes.io/instance: titan24
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: nvidia-device-plugin
|
||||
app.kubernetes.io/instance: titan24
|
||||
spec:
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: titan-24
|
||||
kubernetes.io/arch: amd64
|
||||
tolerations:
|
||||
- operator: Exists
|
||||
priorityClassName: system-node-critical
|
||||
runtimeClassName: nvidia
|
||||
containers:
|
||||
- name: nvidia-device-plugin-ctr
|
||||
image: nvcr.io/nvidia/k8s-device-plugin:v0.16.2
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- "--fail-on-init-error=false"
|
||||
- "--device-list-strategy=envvar"
|
||||
- "--mig-strategy=none"
|
||||
securityContext:
|
||||
privileged: true
|
||||
env:
|
||||
- name: NVIDIA_VISIBLE_DEVICES
|
||||
value: "all"
|
||||
- name: NVIDIA_DRIVER_CAPABILITIES
|
||||
value: "compute,video,utility"
|
||||
volumeMounts:
|
||||
- name: device-plugin
|
||||
mountPath: /var/lib/kubelet/device-plugins
|
||||
volumes:
|
||||
- name: device-plugin
|
||||
hostPath:
|
||||
path: /var/lib/kubelet/device-plugins
|
||||
@ -0,0 +1,5 @@
|
||||
# infrastructure/modules/profiles/components/device-plugin-tethys/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- daemonset.yaml
|
||||
@ -0,0 +1,4 @@
|
||||
# infrastructure/modules/profiles/oceanus-validator/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources: []
|
||||
@ -0,0 +1,5 @@
|
||||
# infrastructure/modules/profiles/tethys-hybrid/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- ../components/device-plugin-tethys
|
||||
@ -4,7 +4,7 @@ metadata:
|
||||
name: letsencrypt
|
||||
spec:
|
||||
acme:
|
||||
email: you@bstein.dev
|
||||
email: brad.stein@gmail.com
|
||||
server: https://acme-v02.api.letsencrypt.org/directory
|
||||
privateKeySecretRef:
|
||||
name: letsencrypt-account-key
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
# infrastructure/sources/helm/grafana.yaml
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
# infrastructure/sources/helm/hashicorp.yaml
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
# infrastructure/sources/helm/jetstack.yaml
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
# infrastructure/sources/helm/prometheus.yaml
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
|
||||
9
infrastructure/sources/helm/victoria-metrics.yaml
Normal file
9
infrastructure/sources/helm/victoria-metrics.yaml
Normal file
@ -0,0 +1,9 @@
|
||||
# infrastructure/sources/helm/victoria-metrics.yaml
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: HelmRepository
|
||||
metadata:
|
||||
name: victoria-metrics
|
||||
namespace: flux-system
|
||||
spec:
|
||||
interval: 1h
|
||||
url: https://victoriametrics.github.io/helm-charts/
|
||||
@ -35,6 +35,18 @@ items:
|
||||
- --entrypoints.web.address=:80
|
||||
- --entrypoints.websecure.address=:443
|
||||
- --api.dashboard=true
|
||||
- --metrics.prometheus=true
|
||||
- --metrics.prometheus.addEntryPointsLabels=true
|
||||
- --metrics.prometheus.addRoutersLabels=true
|
||||
- --metrics.prometheus.addServicesLabels=true
|
||||
- --entrypoints.web.transport.respondingTimeouts.readTimeout=0s
|
||||
- --entrypoints.web.transport.respondingTimeouts.writeTimeout=0s
|
||||
- --entrypoints.web.transport.respondingTimeouts.idleTimeout=0s
|
||||
- --entrypoints.websecure.transport.respondingTimeouts.readTimeout=0s
|
||||
- --entrypoints.websecure.transport.respondingTimeouts.writeTimeout=0s
|
||||
- --entrypoints.websecure.transport.respondingTimeouts.idleTimeout=0s
|
||||
- --entrypoints.metrics.address=:9100
|
||||
- --metrics.prometheus.entryPoint=metrics
|
||||
image: traefik:v3.3.3
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: traefik
|
||||
@ -48,6 +60,9 @@ items:
|
||||
- containerPort: 8080
|
||||
name: admin
|
||||
protocol: TCP
|
||||
- containerPort: 9100
|
||||
name: metrics
|
||||
protocol: TCP
|
||||
terminationMessagePath: /dev/termination-log
|
||||
terminationMessagePolicy: File
|
||||
dnsPolicy: ClusterFirst
|
||||
|
||||
@ -9,3 +9,4 @@ resources:
|
||||
- serviceaccount.yaml
|
||||
- clusterrole.yaml
|
||||
- clusterrolebinding.yaml
|
||||
- service.yaml
|
||||
|
||||
20
infrastructure/traefik/service.yaml
Normal file
20
infrastructure/traefik/service.yaml
Normal file
@ -0,0 +1,20 @@
|
||||
# infrastructure/traefik/service.yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: traefik-metrics
|
||||
namespace: traefik
|
||||
labels:
|
||||
app: traefik
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "9100"
|
||||
prometheus.io/path: "/metrics"
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: traefik
|
||||
ports:
|
||||
- name: metrics
|
||||
port: 9100
|
||||
targetPort: metrics
|
||||
1434
scripts/dashboards_render_atlas.py
Normal file
1434
scripts/dashboards_render_atlas.py
Normal file
File diff suppressed because it is too large
Load Diff
2
scripts/longhorn_volume_usage.fish
Normal file → Executable file
2
scripts/longhorn_volume_usage.fish
Normal file → Executable file
@ -1,3 +1,5 @@
|
||||
#!/usr/bin/env fish
|
||||
|
||||
function pvc-usage --description "Show Longhorn PVC usage (human-readable) mapped to namespace/name"
|
||||
begin
|
||||
kubectl -n longhorn-system get volumes.longhorn.io -o json \
|
||||
|
||||
218
scripts/styx_kioskification.sh
Normal file
218
scripts/styx_kioskification.sh
Normal file
@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# 0) Create dedicated user if it doesn't exist
|
||||
if ! id -u styx >/dev/null 2>&1; then
|
||||
sudo useradd -m -s /bin/bash styx
|
||||
echo "Created user 'styx'"
|
||||
fi
|
||||
|
||||
# 1) App directory
|
||||
sudo mkdir -p /opt/styx-kiosk/keys
|
||||
sudo chown -R styx:styx /opt/styx-kiosk
|
||||
|
||||
# 2) Drop the kiosk app (written below) into place
|
||||
sudo tee /opt/styx-kiosk/kiosk.py >/dev/null <<'PY'
|
||||
#!/usr/bin/env python3
|
||||
import base64, json, os, subprocess, threading, tempfile
|
||||
from datetime import datetime
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
|
||||
APP_TITLE = "STYX Airgap Signer"
|
||||
CAMERA_DEV = os.environ.get("ZBAR_DEV", "/dev/video0")
|
||||
KEY_PATH = os.environ.get("STYX_KEY", "/vault/keys/signer_ed25519.pem") # in the LUKS vault
|
||||
ALGO = os.environ.get("STYX_ALGO", "ed25519") # or 'secp256r1'
|
||||
QR_TMP = "/tmp/styx_signed.png"
|
||||
|
||||
def zbar_scan_oneshot():
|
||||
# --raw -> data only; --nodisplay -> no preview window; --oneshot -> exit after first code
|
||||
# (zbarcam supports --oneshot; prints one code and exits). :contentReference[oaicite:2]{index=2}
|
||||
cmd = ["zbarcam", "--raw", "--nodisplay", "--oneshot", CAMERA_DEV]
|
||||
try:
|
||||
out = subprocess.check_output(cmd, text=True, timeout=30)
|
||||
out = out.strip()
|
||||
return out if out else None
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
def openssl_pub_der_b64(key_path):
|
||||
der = subprocess.check_output(["openssl","pkey","-in",key_path,"-pubout","-outform","DER"])
|
||||
return base64.b64encode(der).decode()
|
||||
|
||||
def sign_bytes(msg: bytes, key_path: str, algo: str) -> bytes:
|
||||
with tempfile.NamedTemporaryFile(delete=False) as f:
|
||||
f.write(msg)
|
||||
msg_path = f.name
|
||||
try:
|
||||
if algo.lower() == "ed25519":
|
||||
# Ed25519 expects raw message; OpenSSL handles hashing internally.
|
||||
sig = subprocess.check_output(
|
||||
["openssl","pkeyutl","-sign","-inkey",key_path,"-rawin","-in",msg_path]
|
||||
)
|
||||
return sig
|
||||
elif algo.lower() in ("secp256r1","prime256v1","p256"):
|
||||
# ECDSA over P-256; hash with SHA-256; OpenSSL returns DER-encoded (r,s)
|
||||
sig = subprocess.check_output(
|
||||
["openssl","dgst","-sha256","-sign",key_path,msg_path]
|
||||
)
|
||||
return sig
|
||||
else:
|
||||
raise RuntimeError(f"Unsupported algo: {algo}")
|
||||
finally:
|
||||
try: os.unlink(msg_path)
|
||||
except: pass
|
||||
|
||||
def make_signed_envelope(scanned_text: str, key_path: str, algo: str) -> dict:
|
||||
# Accept either raw string or JSON with 'tx_bytes' (base64) or 'message'
|
||||
try:
|
||||
obj = json.loads(scanned_text)
|
||||
if "tx_bytes" in obj:
|
||||
msg = base64.b64decode(obj["tx_bytes"])
|
||||
elif "message" in obj:
|
||||
msg = obj["message"].encode()
|
||||
else:
|
||||
# If it's JSON but doesn't carry known fields, sign canonical JSON bytes
|
||||
msg = json.dumps(obj, sort_keys=True, separators=(",",":")).encode()
|
||||
request_id = obj.get("request_id")
|
||||
except Exception:
|
||||
# Non-JSON → treat the scanned text as the message to sign
|
||||
msg = scanned_text.encode()
|
||||
request_id = None
|
||||
|
||||
sig = sign_bytes(msg, key_path, algo)
|
||||
env = {
|
||||
"algo": algo.lower(),
|
||||
"signature_b64": base64.b64encode(sig).decode(),
|
||||
"pubkey_spki_der_b64": openssl_pub_der_b64(key_path),
|
||||
"payload_sha256_b64": base64.b64encode(subprocess.check_output(["openssl","dgst","-sha256","-binary"], input=msg)).decode(),
|
||||
"quote_raw": scanned_text,
|
||||
"request_id": request_id,
|
||||
"device": os.uname().nodename,
|
||||
"ts_utc": datetime.utcnow().isoformat(timespec="seconds") + "Z",
|
||||
}
|
||||
return env
|
||||
|
||||
def qrencode_to_file(text: str, path: str):
|
||||
# Use qrencode CLI to render a PNG we can display.
|
||||
subprocess.run(["qrencode","-l","M","-s","16","-t","PNG","-o",path], input=text.encode(), check=True)
|
||||
|
||||
class App(tk.Tk):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.title(APP_TITLE)
|
||||
self.attributes("-fullscreen", True)
|
||||
self.configure(background="black")
|
||||
self.bind("<Escape>", lambda e: self.quit()) # for maintenance only
|
||||
|
||||
s = ttk.Style(self)
|
||||
s.configure("Big.TButton", font=("DejaVu Sans", 48), padding=24)
|
||||
s.configure("Big.TLabel", font=("DejaVu Sans", 32), foreground="white", background="black")
|
||||
|
||||
self.container = tk.Frame(self, bg="black")
|
||||
self.container.pack(expand=True, fill="both")
|
||||
|
||||
self.status = ttk.Label(self.container, text="Ready", style="Big.TLabel")
|
||||
self.status.pack(pady=20)
|
||||
|
||||
self.scan_btn = ttk.Button(self.container, text="SCAN", style="Big.TButton", command=self.start_scan)
|
||||
self.scan_btn.pack(pady=20)
|
||||
|
||||
self.image_label = tk.Label(self.container, bg="black")
|
||||
self.image_label.pack(pady=10)
|
||||
|
||||
self.new_btn = ttk.Button(self.container, text="NEW SCAN", style="Big.TButton", command=self.reset)
|
||||
self.new_btn.pack_forget()
|
||||
|
||||
self.note = ttk.Label(self.container, text="", style="Big.TLabel")
|
||||
self.note.pack(pady=10)
|
||||
|
||||
if not os.path.exists(KEY_PATH):
|
||||
self.status.config(text=f"Key not found at {KEY_PATH}\nInsert/unlock vault to proceed.")
|
||||
|
||||
def reset(self):
|
||||
self.image_label.configure(image="")
|
||||
self.image_label.image = None
|
||||
self.new_btn.pack_forget()
|
||||
self.note.config(text="")
|
||||
self.status.config(text="Ready")
|
||||
self.scan_btn.config(state="normal")
|
||||
|
||||
def start_scan(self):
|
||||
if not os.path.exists(KEY_PATH):
|
||||
messagebox.showerror("Key missing", f"Signing key not found at:\n{KEY_PATH}\nUnlock your vault.")
|
||||
return
|
||||
self.status.config(text="Scanning…")
|
||||
self.scan_btn.config(state="disabled")
|
||||
threading.Thread(target=self._do_scan_and_sign, daemon=True).start()
|
||||
|
||||
def _do_scan_and_sign(self):
|
||||
scanned = zbar_scan_oneshot()
|
||||
if not scanned:
|
||||
self.after(0, self._scan_failed)
|
||||
return
|
||||
try:
|
||||
envelope = make_signed_envelope(scanned, KEY_PATH, ALGO)
|
||||
payload = json.dumps(envelope, separators=(",",":"))
|
||||
qrencode_to_file(payload, QR_TMP)
|
||||
self.after(0, self._show_qr, envelope)
|
||||
except Exception as e:
|
||||
self.after(0, lambda: self._error(str(e)))
|
||||
|
||||
def _scan_failed(self):
|
||||
self.status.config(text="No QR detected. Try again.")
|
||||
self.scan_btn.config(state="normal")
|
||||
|
||||
def _show_qr(self, envelope):
|
||||
# Display the PNG produced by qrencode
|
||||
try:
|
||||
img = tk.PhotoImage(file=QR_TMP)
|
||||
self.image_label.configure(image=img)
|
||||
self.image_label.image = img
|
||||
except Exception as e:
|
||||
self.status.config(text=f"QR render failed: {e}")
|
||||
self.scan_btn.config(state="normal")
|
||||
return
|
||||
self.status.config(text="Signed. Show this QR to your online box.")
|
||||
self.note.config(text=f"Algo: {envelope['algo']} Host: {envelope['device']}")
|
||||
self.new_btn.pack(pady=20)
|
||||
|
||||
if __name__ == "__main__":
|
||||
App().mainloop()
|
||||
PY
|
||||
sudo chmod +x /opt/styx-kiosk/kiosk.py
|
||||
sudo chown -R styx:styx /opt/styx-kiosk
|
||||
|
||||
# 3) Minimal X session: openbox + kiosk; no mouse pointer
|
||||
sudo -u styx tee /home/styx/.xinitrc >/dev/null <<'XRC'
|
||||
xset -dpms
|
||||
xset s off
|
||||
xset s noblank
|
||||
# If 'unclutter' is installed, uncomment the next line to hide cursor:
|
||||
# unclutter -idle 0 -root &
|
||||
openbox-session &
|
||||
/opt/styx-kiosk/kiosk.py
|
||||
XRC
|
||||
sudo chown styx:styx /home/styx/.xinitrc
|
||||
sudo chmod 0755 /home/styx/.xinitrc
|
||||
|
||||
# 4) Autologin the 'styx' user on tty1, auto-start X
|
||||
sudo mkdir -p /etc/systemd/system/getty@tty1.service.d
|
||||
sudo tee /etc/systemd/system/getty@tty1.service.d/override.conf >/dev/null <<'OVR'
|
||||
[Service]
|
||||
ExecStart=
|
||||
ExecStart=-/sbin/agetty --autologin styx --noclear %I $TERM
|
||||
Type=idle
|
||||
OVR
|
||||
|
||||
sudo -u styx tee -a /home/styx/.bash_profile >/dev/null <<'BRC'
|
||||
# Start X on the first tty automatically, headless
|
||||
if [ -z "$DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then
|
||||
exec startx -- -nocursor
|
||||
fi
|
||||
BRC
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable getty@tty1.service
|
||||
|
||||
echo "Done. Reboot to try the kiosk."
|
||||
195
scripts/styx_prep.sh
Executable file
195
scripts/styx_prep.sh
Executable file
@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# === CONFIG ===
|
||||
STYX_USER="styx"
|
||||
STYX_PASS="TempPass#123" # change at first login
|
||||
STYX_HOSTNAME="styx"
|
||||
SSH_PUBKEY="" # e.g., 'ssh-ed25519 AAAA... your@host' (optional)
|
||||
|
||||
# === helpers ===
|
||||
require_root() {
|
||||
if [[ $EUID -ne 0 ]]; then exec sudo -E "$0" "$@"; fi
|
||||
}
|
||||
|
||||
ensure_binfmt_arm64() {
|
||||
# If binfmt for arm64 isn't registered, register it via Docker (idempotent).
|
||||
if [[ ! -e /proc/sys/fs/binfmt_misc/qemu-aarch64 ]]; then
|
||||
command -v docker >/dev/null || { echo "Docker required to register binfmt (sudo pacman -S docker)"; exit 1; }
|
||||
sudo systemctl enable --now docker >/dev/null 2>&1 || true
|
||||
sudo docker run --rm --privileged tonistiigi/binfmt --install arm64
|
||||
fi
|
||||
}
|
||||
|
||||
find_parts() {
|
||||
BOOT=$(lsblk -o LABEL,PATH -nr | awk '$1=="system-boot"{print $2}' | head -n1)
|
||||
ROOT=$(lsblk -o LABEL,PATH -nr | awk '$1=="writable"{print $2}' | head -n1)
|
||||
if [[ -z "${BOOT:-}" || -z "${ROOT:-}" ]]; then
|
||||
echo "Could not find 'system-boot'/'writable' on any device."
|
||||
lsblk -o NAME,SIZE,FSTYPE,LABEL,PATH -nr
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
mount_parts() {
|
||||
mkdir -p /mnt/pi-boot /mnt/pi-root
|
||||
mount "$ROOT" /mnt/pi-root
|
||||
mount "$BOOT" /mnt/pi-boot
|
||||
|
||||
# Bind only what we need (avoid /run to prevent postinst fights)
|
||||
for d in dev dev/pts proc sys; do mount --bind "/$d" "/mnt/pi-root/$d"; done
|
||||
|
||||
# Ubuntu images use a resolv.conf symlink—replace with a real file
|
||||
if [[ -L /mnt/pi-root/etc/resolv.conf || ! -e /mnt/pi-root/etc/resolv.conf ]]; then
|
||||
rm -f /mnt/pi-root/etc/resolv.conf
|
||||
cat /etc/resolv.conf > /mnt/pi-root/etc/resolv.conf
|
||||
fi
|
||||
}
|
||||
|
||||
prep_chroot() {
|
||||
# Block service starts inside chroot (no systemd there)
|
||||
cat >/mnt/pi-root/usr/sbin/policy-rc.d <<'EOF'
|
||||
#!/bin/sh
|
||||
exit 101
|
||||
EOF
|
||||
chmod +x /mnt/pi-root/usr/sbin/policy-rc.d
|
||||
|
||||
# All the work happens inside the ARM64 rootfs
|
||||
CHCMD=$(cat <<'EOS'
|
||||
set -euo pipefail
|
||||
export DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC
|
||||
# Ensure sbin is in PATH so user/group tools work
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
apt-get update
|
||||
apt-get -y full-upgrade
|
||||
|
||||
# Remove snaps and keep them gone (Ubuntu for Pi ships with snaps)
|
||||
apt-get -y purge snapd || true
|
||||
rm -rf /snap /var/snap /var/lib/snapd /home/*/snap || true
|
||||
mkdir -p /etc/apt/preferences.d
|
||||
printf 'Package: snapd\nPin: release *\nPin-Priority: -10\n' > /etc/apt/preferences.d/nosnap.pref
|
||||
|
||||
# Ensure user/group tools exist
|
||||
apt-get install -y passwd adduser || true
|
||||
getent group i2c >/dev/null || /usr/sbin/groupadd i2c
|
||||
|
||||
# Base packages
|
||||
BASE_PKGS="openssh-server git i2c-tools python3-smbus python3-pil zbar-tools qrencode lm-sensors"
|
||||
apt-get install -y $BASE_PKGS
|
||||
|
||||
# ------- OLED (Luma) -------
|
||||
# Prefer distro package; fall back to pip if not present in this release
|
||||
if ! dpkg -s python3-luma.oled >/dev/null 2>&1; then
|
||||
apt-get update
|
||||
if ! apt-get install -y python3-luma.oled; then
|
||||
apt-get install -y python3-pip
|
||||
pip3 install --no-input --break-system-packages luma.oled
|
||||
fi
|
||||
fi
|
||||
|
||||
# ------- Camera apps -------
|
||||
# Ubuntu renamed libcamera-apps -> rpicam-apps for Raspberry Pi.
|
||||
# Try in order; tolerate absence (the box might be display-only).
|
||||
apt-get update
|
||||
if ! apt-get install -y rpicam-apps; then
|
||||
apt-get install -y libcamera-apps || apt-get install -y libcamera-tools || true
|
||||
fi
|
||||
|
||||
# Enable SSH on boot (no systemctl in chroot)
|
||||
mkdir -p /etc/systemd/system/multi-user.target.wants
|
||||
ln -sf /lib/systemd/system/ssh.service /etc/systemd/system/multi-user.target.wants/ssh.service
|
||||
|
||||
# Create user and set password
|
||||
if ! id -u STYX_USER >/dev/null 2>&1; then
|
||||
/usr/sbin/useradd -m -s /bin/bash -G sudo,video,i2c STYX_USER
|
||||
fi
|
||||
echo 'STYX_USER:STYX_PASS' | /usr/sbin/chpasswd
|
||||
|
||||
# Optional: preload SSH key
|
||||
if [ -n 'SSH_PUBKEY' ] && echo 'SSH_PUBKEY' | grep -q 'ssh-'; then
|
||||
install -d -m700 /home/STYX_USER/.ssh
|
||||
echo 'SSH_PUBKEY' >> /home/STYX_USER/.ssh/authorized_keys
|
||||
chmod 600 /home/STYX_USER/.ssh/authorized_keys
|
||||
chown -R STYX_USER:STYX_USER /home/STYX_USER/.ssh
|
||||
fi
|
||||
|
||||
# Freenove code
|
||||
git clone https://github.com/Freenove/Freenove_Computer_Case_Kit_for_Raspberry_Pi.git /opt/freenove || true
|
||||
|
||||
# Hostname
|
||||
echo 'STYX_HOSTNAME' > /etc/hostname
|
||||
if grep -q '^127\.0\.1\.1' /etc/hosts; then
|
||||
sed -i 's/^127\.0\.1\.1.*/127.0.1.1\tSTYX_HOSTNAME/' /etc/hosts
|
||||
else
|
||||
echo -e '127.0.1.1\tSTYX_HOSTNAME' >> /etc/hosts
|
||||
fi
|
||||
|
||||
apt-get clean
|
||||
EOS
|
||||
)
|
||||
# Inject config values safely
|
||||
CHCMD="${CHCMD//STYX_USER/${STYX_USER}}"
|
||||
CHCMD="${CHCMD//STYX_PASS/${STYX_PASS}}"
|
||||
CHCMD="${CHCMD//STYX_HOSTNAME/${STYX_HOSTNAME}}"
|
||||
CHCMD="${CHCMD//SSH_PUBKEY/${SSH_PUBKEY}}"
|
||||
|
||||
chroot /mnt/pi-root /bin/bash -lc "$CHCMD"
|
||||
}
|
||||
|
||||
install_service_host() {
|
||||
# Systemd unit for the Freenove example app
|
||||
mkdir -p /mnt/pi-root/etc/systemd/system/multi-user.target.wants
|
||||
cat >/mnt/pi-root/etc/systemd/system/freenove-case.service <<'SERVICE'
|
||||
[Unit]
|
||||
Description=Freenove Case OLED/Fans/LEDs
|
||||
After=multi-user.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/python3 /opt/freenove/Code/application.py
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
SERVICE
|
||||
ln -sf /etc/systemd/system/freenove-case.service \
|
||||
/mnt/pi-root/etc/systemd/system/multi-user.target.wants/freenove-case.service || true
|
||||
}
|
||||
|
||||
boot_tweaks() {
|
||||
# Enable I2C and set DSI panel on the BOOT partition
|
||||
grep -q 'dtparam=i2c_arm=on' /mnt/pi-boot/config.txt || echo 'dtparam=i2c_arm=on' >> /mnt/pi-boot/config.txt
|
||||
# Append kernel cmdline only once
|
||||
if ! grep -q 'DSI-1:800x480@60D' /mnt/pi-boot/cmdline.txt 2>/dev/null; then
|
||||
sed -i '1 s#$# video=DSI-1:800x480@60D video=HDMI-A-1:off video=HDMI-A-2:off#' /mnt/pi-boot/cmdline.txt || true
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -f /mnt/pi-root/usr/sbin/policy-rc.d || true
|
||||
for d in dev/pts dev proc sys; do umount -lf "/mnt/pi-root/$d" 2>/dev/null || true; done
|
||||
umount -lf /mnt/pi-boot 2>/dev/null || true
|
||||
umount -lf /mnt/pi-root 2>/dev/null || true
|
||||
sync || true
|
||||
}
|
||||
|
||||
main() {
|
||||
require_root
|
||||
ensure_binfmt_arm64
|
||||
find_parts
|
||||
trap 'echo "ERROR at line $LINENO" >&2; cleanup' ERR INT
|
||||
mount_parts
|
||||
prep_chroot
|
||||
install_service_host
|
||||
boot_tweaks
|
||||
cleanup
|
||||
echo "✅ Done. Move the NVMe to the Pi and boot."
|
||||
echo " Login: user '${STYX_USER}' pass '${STYX_PASS}' (change with 'passwd')."
|
||||
echo " Quick checks on the Pi:"
|
||||
echo " sudo i2cdetect -y 1"
|
||||
echo " rpicam-still -n -o test.jpg # (if rpicam-apps installed)"
|
||||
echo " libcamera-still -n -o test.jpg # (if legacy libcamera-apps installed)"
|
||||
echo " systemctl status freenove-case"
|
||||
}
|
||||
main "$@"
|
||||
575
scripts/styx_prep_nvme_luks.sh
Executable file
575
scripts/styx_prep_nvme_luks.sh
Executable file
@ -0,0 +1,575 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# --- CONFIG (edit if needed) ---
|
||||
# Leave NVME empty → script will auto-detect the SSK dock.
|
||||
NVME="${NVME:-}"
|
||||
FLAVOR="${FLAVOR:-desktop}"
|
||||
# Persistent cache so the image survives reboots.
|
||||
IMG_DIR="${IMG_DIR:-/var/cache/styx-rpi}"
|
||||
IMG_FILE="${IMG_FILE:-ubuntu-24.04.3-preinstalled-${FLAVOR}-arm64+raspi.img}"
|
||||
IMG_BOOT_MNT="${IMG_BOOT_MNT:-/mnt/img-boot}"
|
||||
IMG_ROOT_MNT="${IMG_ROOT_MNT:-/mnt/img-root}"
|
||||
TGT_ROOT="/mnt/target-root"
|
||||
TGT_BOOT="/mnt/target-boot"
|
||||
|
||||
STYX_USER="styx"
|
||||
STYX_HOSTNAME="titan-ag"
|
||||
STYX_PASS="TempPass#123" # will be forced to change on first login via cloud-init
|
||||
SSH_PUBKEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOb8oMX6u0z3sH/p/WBGlvPXXdbGETCKzWYwR/dd6fZb titan-bastion"
|
||||
|
||||
# Video / input prefs
|
||||
DSI_FLAGS="video=DSI-1:800x480@60D video=HDMI-A-1:off video=HDMI-A-2:off"
|
||||
|
||||
# --- Helpers ---
|
||||
fatal(){ echo "ERROR: $*" >&2; exit 1; }
|
||||
need(){ command -v "$1" >/dev/null || fatal "Missing tool: $1"; }
|
||||
|
||||
require_root(){ [[ $EUID -eq 0 ]] || exec sudo -E "$0" "$@"; }
|
||||
|
||||
part() {
|
||||
local n="$1"
|
||||
if [[ "$NVME" =~ [0-9]$ ]]; then
|
||||
echo "${NVME}p${n}"
|
||||
else
|
||||
echo "${NVME}${n}"
|
||||
fi
|
||||
}
|
||||
|
||||
auto_detect_target_disk() {
|
||||
# If user already set NVME, validate and return
|
||||
if [[ -n "${NVME:-}" ]]; then
|
||||
[[ -b "$NVME" ]] || fatal "NVME='$NVME' is not a block device"
|
||||
return
|
||||
fi
|
||||
|
||||
# Prefer stable by-id symlinks
|
||||
local byid
|
||||
byid=$(ls -1 /dev/disk/by-id/usb-SSK* 2>/dev/null | head -n1 || true)
|
||||
if [[ -n "$byid" ]]; then
|
||||
NVME=$(readlink -f "$byid")
|
||||
else
|
||||
# Heuristic via lsblk -S: look for USB with SSK/Ingram/Storage in vendor/model
|
||||
NVME=$(lsblk -S -p -o NAME,TRAN,VENDOR,MODEL | \
|
||||
awk '/ usb / && ($3 ~ /SSK|Ingram/i || $4 ~ /SSK|Storage/i){print $1; exit}')
|
||||
fi
|
||||
|
||||
[[ -n "${NVME:-}" && -b "$NVME" ]] || fatal "Could not auto-detect SSK USB NVMe dock. Export NVME=/dev/sdX and re-run."
|
||||
echo "Auto-detected target disk: $NVME"
|
||||
}
|
||||
|
||||
preflight_cleanup() {
|
||||
local img="$IMG_DIR/$IMG_FILE"
|
||||
|
||||
# 1) Unmount image mountpoints and detach only loops for this IMG
|
||||
umount -lf "$IMG_BOOT_MNT" "$IMG_ROOT_MNT" 2>/dev/null || true
|
||||
# losetup -j exits non-zero if no association → tolerate it
|
||||
{ losetup -j "$img" | cut -d: -f1 | xargs -r losetup -d; } 2>/dev/null || true
|
||||
|
||||
# 2) Unmount our target mounts
|
||||
umount -lf "$TGT_ROOT/boot/firmware" "$TGT_BOOT" "$TGT_ROOT" 2>/dev/null || true
|
||||
|
||||
# 3) Unmount the actual target partitions if mounted anywhere (tolerate 'not found')
|
||||
for p in "$(part 1)" "$(part 2)"; do
|
||||
# findmnt returns 1 when no match → capture and iterate if any
|
||||
while read -r mnt; do
|
||||
[ -n "$mnt" ] && umount -lf "$mnt" 2>/dev/null || true
|
||||
done < <(findmnt -rno TARGET -S "$p" 2>/dev/null || true)
|
||||
done
|
||||
|
||||
# 4) Close dm-crypt mapping (if it exists)
|
||||
cryptsetup luksClose cryptroot 2>/dev/null || true
|
||||
dmsetup remove -f cryptroot 2>/dev/null || true
|
||||
|
||||
# 5) Let udev settle
|
||||
command -v udevadm >/dev/null && udevadm settle || true
|
||||
}
|
||||
|
||||
guard_target_device() {
|
||||
# Refuse to operate if NVME appears to be the current system disk
|
||||
local root_src root_disk
|
||||
root_src=$(findmnt -no SOURCE /)
|
||||
root_disk=$(lsblk -no pkname "$root_src" 2>/dev/null || true)
|
||||
if [[ -n "$root_disk" && "/dev/$root_disk" == "$NVME" ]]; then
|
||||
fatal "Refusing to operate on system disk ($NVME). Pick the external NVMe."
|
||||
fi
|
||||
}
|
||||
|
||||
need_host_fido2() {
|
||||
if ! command -v fido2-token >/dev/null 2>&1; then
|
||||
echo "Host is missing fido2-token. On Arch: sudo pacman -S libfido2"
|
||||
echo "On Debian/Ubuntu host: sudo apt-get install fido2-tools"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_image() {
|
||||
mkdir -p "$IMG_DIR"
|
||||
chmod 755 "$IMG_DIR"
|
||||
|
||||
local BASE="https://cdimage.ubuntu.com/releases/noble/release"
|
||||
local XZ="ubuntu-24.04.3-preinstalled-${FLAVOR}-arm64+raspi.img.xz"
|
||||
|
||||
# If the decompressed .img is missing, fetch/decompress into the cache.
|
||||
if [[ ! -f "$IMG_DIR/$IMG_FILE" ]]; then
|
||||
need curl; need unxz # Arch: pacman -S curl xz | Ubuntu: apt-get install curl xz-utils
|
||||
if [[ ! -f "$IMG_DIR/$XZ" ]]; then
|
||||
echo "Fetching image…"
|
||||
curl -fL -o "$IMG_DIR/$XZ" "$BASE/$XZ"
|
||||
fi
|
||||
echo "Decompressing to $IMG_DIR/$IMG_FILE …"
|
||||
# Keep the .xz for future runs; stream-decompress to the .img
|
||||
if command -v unxz >/dev/null 2>&1; then
|
||||
unxz -c "$IMG_DIR/$XZ" > "$IMG_DIR/$IMG_FILE"
|
||||
else
|
||||
need xz
|
||||
xz -dc "$IMG_DIR/$XZ" > "$IMG_DIR/$IMG_FILE"
|
||||
fi
|
||||
sync
|
||||
else
|
||||
echo "Using cached image: $IMG_DIR/$IMG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_binfmt_aarch64(){
|
||||
# Register qemu-aarch64 for chrooted ARM64 apt runs
|
||||
if [[ ! -e /proc/sys/fs/binfmt_misc/qemu-aarch64 ]]; then
|
||||
need docker
|
||||
systemctl enable --now docker >/dev/null 2>&1 || true
|
||||
docker run --rm --privileged tonistiigi/binfmt --install arm64 >/dev/null
|
||||
fi
|
||||
if [[ ! -x /usr/local/bin/qemu-aarch64-static ]]; then
|
||||
docker rm -f qemu-static >/dev/null 2>&1 || true
|
||||
docker create --name qemu-static docker.io/multiarch/qemu-user-static:latest >/dev/null
|
||||
docker cp qemu-static:/usr/bin/qemu-aarch64-static /usr/local/bin/
|
||||
install -D -m755 /usr/local/bin/qemu-aarch64-static /usr/local/bin/qemu-aarch64-static
|
||||
docker rm qemu-static >/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
open_image() {
|
||||
[[ -r "$IMG_DIR/$IMG_FILE" ]] || fatal "Image not found: $IMG_DIR/$IMG_FILE"
|
||||
mkdir -p "$IMG_BOOT_MNT" "$IMG_ROOT_MNT"
|
||||
|
||||
# Pre-clean: detach any previous loop(s) for this image (tolerate absence)
|
||||
umount -lf "$IMG_BOOT_MNT" 2>/dev/null || true
|
||||
umount -lf "$IMG_ROOT_MNT" 2>/dev/null || true
|
||||
# If no loop is attached, losetup -j returns non-zero → swallow it
|
||||
mapfile -t OLD < <({ losetup -j "$IMG_DIR/$IMG_FILE" | cut -d: -f1; } 2>/dev/null || true)
|
||||
for L in "${OLD[@]:-}"; do losetup -d "$L" 2>/dev/null || true; done
|
||||
command -v udevadm >/dev/null && udevadm settle || true
|
||||
|
||||
# Attach with partition scan; wait for partition nodes to exist
|
||||
LOOP=$(losetup --find --show --partscan "$IMG_DIR/$IMG_FILE") || fatal "losetup failed"
|
||||
command -v udevadm >/dev/null && udevadm settle || true
|
||||
for _ in {1..25}; do
|
||||
[[ -b "${LOOP}p1" && -b "${LOOP}p2" ]] && break
|
||||
sleep 0.1
|
||||
command -v udevadm >/dev/null && udevadm settle || true
|
||||
done
|
||||
[[ -b "${LOOP}p1" ]] || fatal "loop partitions not present for $LOOP"
|
||||
|
||||
# Cleanup on exit: unmount first, then detach loop (tolerate absence)
|
||||
trap 'umount -lf "'"$IMG_BOOT_MNT"'" "'"$IMG_ROOT_MNT"'" 2>/dev/null; losetup -d "'"$LOOP"'" 2>/dev/null' EXIT
|
||||
|
||||
# Mount image partitions read-only
|
||||
mount -o ro "${LOOP}p1" "$IMG_BOOT_MNT"
|
||||
mount -o ro "${LOOP}p2" "$IMG_ROOT_MNT"
|
||||
|
||||
# Sanity checks without using failing pipelines
|
||||
# start*.elf must exist
|
||||
if ! compgen -G "$IMG_BOOT_MNT/start*.elf" > /dev/null; then
|
||||
fatal "start*.elf not found in image"
|
||||
fi
|
||||
# vmlinuz-* must exist
|
||||
if ! compgen -G "$IMG_ROOT_MNT/boot/vmlinuz-*" > /dev/null; then
|
||||
fatal "vmlinuz-* not found in image root"
|
||||
fi
|
||||
}
|
||||
|
||||
confirm_and_wipe(){
|
||||
lsblk -o NAME,SIZE,MODEL,TRAN,LABEL "$NVME"
|
||||
read -rp "Type EXACTLY 'WIPE' to destroy ALL DATA on $NVME: " ACK
|
||||
[[ "$ACK" == "WIPE" ]] || fatal "Aborted"
|
||||
wipefs -a "$NVME"
|
||||
sgdisk -Zo "$NVME"
|
||||
# GPT: 1: 1MiB..513MiB vfat ESP; 2: rest LUKS
|
||||
parted -s "$NVME" mklabel gpt \
|
||||
mkpart system-boot fat32 1MiB 513MiB set 1 esp on \
|
||||
mkpart cryptroot 513MiB 100%
|
||||
partprobe "$NVME"; sleep 1
|
||||
mkfs.vfat -F32 -n system-boot "$(part 1)"
|
||||
}
|
||||
|
||||
setup_luks(){
|
||||
echo "Create LUKS2 on $(part 2) (you will be prompted for a passphrase; keep it as fallback)"
|
||||
need cryptsetup
|
||||
cryptsetup luksFormat --type luks2 "$(part 2)"
|
||||
cryptsetup open "$(part 2)" cryptroot
|
||||
mkfs.ext4 -L rootfs /dev/mapper/cryptroot
|
||||
}
|
||||
|
||||
mount_targets(){
|
||||
mkdir -p "$TGT_ROOT" "$TGT_BOOT"
|
||||
mount /dev/mapper/cryptroot "$TGT_ROOT"
|
||||
mkdir -p "$TGT_ROOT/boot/firmware"
|
||||
mount "$(part 1)" "$TGT_BOOT"
|
||||
mount --bind "$TGT_BOOT" "$TGT_ROOT/boot/firmware"
|
||||
}
|
||||
|
||||
rsync_root_and_boot(){
|
||||
need rsync
|
||||
rsync -aAXH --numeric-ids --delete \
|
||||
--exclude='/boot/firmware' --exclude='/boot/firmware/**' \
|
||||
--exclude='/dev/*' --exclude='/proc/*' --exclude='/sys/*' \
|
||||
--exclude='/run/*' --exclude='/tmp/*' --exclude='/mnt/*' \
|
||||
--exclude='/media/*' --exclude='/lost+found' \
|
||||
"$IMG_ROOT_MNT"/ "$TGT_ROOT"/
|
||||
rsync -aH --delete "$IMG_BOOT_MNT"/ "$TGT_ROOT/boot/firmware"/
|
||||
}
|
||||
|
||||
write_crypttab_fstab(){
|
||||
LUUID=$(blkid -s UUID -o value "$(part 2)")
|
||||
printf 'cryptroot UUID=%s none luks,discard,fido2-device=auto\n' "$LUUID" > "$TGT_ROOT/etc/crypttab"
|
||||
cat > "$TGT_ROOT/etc/fstab" <<EOF
|
||||
/dev/mapper/cryptroot / ext4 defaults,discard,errors=remount-ro 0 1
|
||||
LABEL=system-boot /boot/firmware vfat defaults,umask=0077 0 1
|
||||
EOF
|
||||
}
|
||||
|
||||
fix_firmware_files(){
|
||||
local C="$TGT_ROOT/boot/firmware/config.txt"
|
||||
local CL="$TGT_ROOT/boot/firmware/cmdline.txt"
|
||||
[[ -f "$C" ]] || fatal "missing $C"
|
||||
|
||||
# Always boot the uncompressed Pi 5 kernel
|
||||
if grep -q '^kernel=' "$C"; then
|
||||
sed -i 's#^kernel=.*#kernel=kernel_2712.img#' "$C"
|
||||
else
|
||||
sed -i '1i kernel=kernel_2712.img' "$C"
|
||||
fi
|
||||
|
||||
# Ensure initramfs and cmdline indirection are set
|
||||
grep -q '^initramfs ' "$C" || echo 'initramfs initrd.img followkernel' >> "$C"
|
||||
grep -q '^cmdline=cmdline.txt' "$C" || sed -i '1i cmdline=cmdline.txt' "$C"
|
||||
|
||||
# Display & buses (Pi 5)
|
||||
grep -q '^dtoverlay=vc4-kms-v3d-pi5' "$C" || echo 'dtoverlay=vc4-kms-v3d-pi5' >> "$C"
|
||||
grep -q '^dtparam=i2c_arm=on' "$C" || echo 'dtparam=i2c_arm=on' >> "$C"
|
||||
grep -q '^dtparam=pciex1=on' "$C" || echo 'dtparam=pciex1=on' >> "$C"
|
||||
grep -q '^dtparam=pciex1_gen=2' "$C" || echo 'dtparam=pciex1_gen=2' >> "$C"
|
||||
grep -q '^enable_uart=1' "$C" || echo 'enable_uart=1' >> "$C"
|
||||
|
||||
# Minimal, correct dracut hints using the bare UUID
|
||||
local LUUID; LUUID=$(blkid -s UUID -o value "$(part 2)")
|
||||
: > "$CL"
|
||||
{
|
||||
echo -n "rd.luks.uuid=$LUUID rd.luks.name=$LUUID=cryptroot "
|
||||
echo -n "root=/dev/mapper/cryptroot rootfstype=ext4 rootwait fixrtc "
|
||||
echo "console=serial0,115200 console=tty1 ds=nocloud;s=file:///boot/firmware/ ${DSI_FLAGS} rd.debug"
|
||||
} >> "$CL"
|
||||
}
|
||||
|
||||
seed_cloud_init(){
|
||||
# NoCloud seed to create user, lock down SSH, set hostname, and enable avahi.
|
||||
cat > "$TGT_ROOT/boot/firmware/user-data" <<EOF
|
||||
#cloud-config
|
||||
hostname: $STYX_HOSTNAME
|
||||
manage_etc_hosts: true
|
||||
users:
|
||||
- name: $STYX_USER
|
||||
gecos: "$STYX_USER"
|
||||
shell: /bin/bash
|
||||
groups: [sudo,video,i2c]
|
||||
sudo: ALL=(ALL) NOPASSWD:ALL
|
||||
lock_passwd: false
|
||||
ssh_authorized_keys:
|
||||
- $SSH_PUBKEY
|
||||
chpasswd:
|
||||
list: |
|
||||
$STYX_USER:$STYX_PASS
|
||||
expire: true
|
||||
ssh_pwauth: false
|
||||
package_update: true
|
||||
packages: [openssh-server, avahi-daemon]
|
||||
runcmd:
|
||||
- systemctl enable --now ssh
|
||||
- systemctl enable --now avahi-daemon || true
|
||||
EOF
|
||||
|
||||
# Minimal meta-data for NoCloud
|
||||
date +%s | awk '{print "instance-id: iid-titan-ag-"$1"\nlocal-hostname: '"$STYX_HOSTNAME"'"}' \
|
||||
> "$TGT_ROOT/boot/firmware/meta-data"
|
||||
}
|
||||
|
||||
prep_chroot_mounts(){
|
||||
for d in dev proc sys; do mount --bind "/$d" "$TGT_ROOT/$d"; done
|
||||
mount -t devpts devpts "$TGT_ROOT/dev/pts"
|
||||
# Replace the usual resolv.conf symlink with a real file for apt to work
|
||||
rm -f "$TGT_ROOT/etc/resolv.conf"
|
||||
cp /etc/resolv.conf "$TGT_ROOT/etc/resolv.conf"
|
||||
|
||||
# Block service starts (no systemd in chroot)
|
||||
cat > "$TGT_ROOT/usr/sbin/policy-rc.d" <<'EOP'
|
||||
#!/bin/sh
|
||||
exit 101
|
||||
EOP
|
||||
chmod +x "$TGT_ROOT/usr/sbin/policy-rc.d"
|
||||
|
||||
# Ensure qemu static is present inside chroot
|
||||
install -D -m755 /usr/local/bin/qemu-aarch64-static "$TGT_ROOT/usr/bin/qemu-aarch64-static"
|
||||
}
|
||||
|
||||
in_chroot(){
|
||||
chroot "$TGT_ROOT" /usr/bin/qemu-aarch64-static /bin/bash -lc '
|
||||
set -euo pipefail
|
||||
export DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC
|
||||
|
||||
# --- APT sources (ports) ---
|
||||
cat > /etc/apt/sources.list <<'"'"'EOS'"'"'
|
||||
deb http://ports.ubuntu.com/ubuntu-ports noble main restricted universe multiverse
|
||||
deb http://ports.ubuntu.com/ubuntu-ports noble-updates main restricted universe multiverse
|
||||
deb http://ports.ubuntu.com/ubuntu-ports noble-security main restricted universe multiverse
|
||||
EOS
|
||||
|
||||
apt-get update
|
||||
|
||||
# --- Remove snaps and pin them off ---
|
||||
apt-get -y purge snapd || true
|
||||
rm -rf /snap /var/snap /var/lib/snapd /home/*/snap || true
|
||||
mkdir -p /etc/apt/preferences.d
|
||||
cat > /etc/apt/preferences.d/nosnap.pref <<'"'"'EOS'"'"'
|
||||
Package: snapd
|
||||
Pin: release *
|
||||
Pin-Priority: -10
|
||||
EOS
|
||||
|
||||
# --- Base tools (no flash-kernel; we use dracut) ---
|
||||
apt-get install -y --no-install-recommends \
|
||||
openssh-client openssh-server openssh-sftp-server avahi-daemon \
|
||||
cryptsetup dracut fido2-tools libfido2-1 i2c-tools \
|
||||
python3-smbus python3-pil zbar-tools qrencode lm-sensors \
|
||||
file zstd lz4 || true
|
||||
|
||||
# Camera apps: try rpicam-apps; otherwise basic libcamera tools
|
||||
apt-get install -y rpicam-apps || apt-get install -y libcamera-tools || true
|
||||
|
||||
# --- Persistent journal so we can read logs after failed boot ---
|
||||
mkdir -p /etc/systemd/journald.conf.d
|
||||
cat > /etc/systemd/journald.conf.d/99-persistent.conf <<'"'"'EOS'"'"'
|
||||
[Journal]
|
||||
Storage=persistent
|
||||
EOS
|
||||
|
||||
# --- SSH hardening (ensure file exists even if package was half-installed) ---
|
||||
if [ ! -f /etc/ssh/sshd_config ]; then
|
||||
mkdir -p /etc/ssh
|
||||
cat > /etc/ssh/sshd_config <<'"'"'EOS'"'"'
|
||||
PermitRootLogin no
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
PubkeyAuthentication yes
|
||||
# Accept defaults for the rest
|
||||
EOS
|
||||
fi
|
||||
sed -i -e "s/^#\?PasswordAuthentication .*/PasswordAuthentication no/" \
|
||||
-e "s/^#\?KbdInteractiveAuthentication .*/KbdInteractiveAuthentication no/" \
|
||||
-e "s/^#\?PermitRootLogin .*/PermitRootLogin no/" \
|
||||
-e "s/^#\?PubkeyAuthentication .*/PubkeyAuthentication yes/" /etc/ssh/sshd_config || true
|
||||
|
||||
# --- Hostname & hosts ---
|
||||
echo "'"$STYX_HOSTNAME"'" > /etc/hostname
|
||||
if grep -q "^127\\.0\\.1\\.1" /etc/hosts; then
|
||||
sed -i "s/^127\\.0\\.1\\.1.*/127.0.1.1\t'"$STYX_HOSTNAME"'/" /etc/hosts
|
||||
else
|
||||
echo -e "127.0.1.1\t'"$STYX_HOSTNAME"'" >> /etc/hosts
|
||||
fi
|
||||
|
||||
# --- Enable services on first boot ---
|
||||
mkdir -p /etc/systemd/system/multi-user.target.wants
|
||||
ln -sf /lib/systemd/system/ssh.service /etc/systemd/system/multi-user.target.wants/ssh.service
|
||||
ln -sf /lib/systemd/system/avahi-daemon.service /etc/systemd/system/multi-user.target.wants/avahi-daemon.service || true
|
||||
|
||||
# --- Ensure i2c group ---
|
||||
getent group i2c >/dev/null || groupadd i2c
|
||||
|
||||
# --- Dracut configuration (generic, not host-only) ---
|
||||
mkdir -p /etc/dracut.conf.d
|
||||
cat > /etc/dracut.conf.d/00-hostonly.conf <<'"'"'EOS'"'"'
|
||||
hostonly=no
|
||||
EOS
|
||||
cat > /etc/dracut.conf.d/10-systemd-crypt.conf <<'"'"'EOS'"'"'
|
||||
add_dracutmodules+=" systemd crypt "
|
||||
EOS
|
||||
cat > /etc/dracut.conf.d/20-drivers.conf <<'"'"'EOS'"'"'
|
||||
add_drivers+=" nvme xhci_pci xhci_hcd usbhid hid_generic hid "
|
||||
EOS
|
||||
cat > /etc/dracut.conf.d/30-fido2.conf <<'"'"'EOS'"'"'
|
||||
install_items+="/usr/bin/systemd-cryptsetup /usr/bin/fido2-token /usr/lib/*/libfido2.so* /usr/lib/*/libcbor.so*"
|
||||
EOS
|
||||
|
||||
# --- Build initramfs and place it where firmware expects it ---
|
||||
KVER=$(ls -1 /lib/modules | sort -V | tail -n1)
|
||||
dracut --force /boot/initramfs-$KVER.img $KVER
|
||||
ln -sf initramfs-$KVER.img /boot/initrd.img
|
||||
ln -sf initramfs-$KVER.img /boot/initrd.img-$KVER
|
||||
cp -a /boot/initramfs-$KVER.img /boot/firmware/initrd.img
|
||||
|
||||
# --- Create uncompressed kernel for Pi 5 firmware ---
|
||||
if [ -f "/usr/lib/linux-image-$KVER/Image" ]; then
|
||||
cp -a "/usr/lib/linux-image-$KVER/Image" /boot/firmware/kernel_2712.img
|
||||
else
|
||||
FMT=$(file -b "/boot/vmlinuz-$KVER" || true)
|
||||
case "$FMT" in
|
||||
*Zstandard*|*zstd*) zstd -dc "/boot/vmlinuz-$KVER" > /boot/firmware/kernel_2712.img ;;
|
||||
*LZ4*) lz4 -dc "/boot/vmlinuz-$KVER" > /boot/firmware/kernel_2712.img ;;
|
||||
*gzip*) zcat "/boot/vmlinuz-$KVER" > /boot/firmware/kernel_2712.img ;;
|
||||
*) cp -a "/boot/vmlinuz-$KVER" /boot/firmware/kernel_2712.img ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# --- Ensure Pi 5 DTB is present on the boot partition ---
|
||||
DTB=$(find /lib/firmware -type f -name "bcm2712-rpi-5-b.dtb" | sort | tail -n1 || true)
|
||||
[ -n "$DTB" ] && cp -a "$DTB" /boot/firmware/
|
||||
|
||||
# --- Dracut hook to copy rdsosreport.txt to the FAT partition on failure ---
|
||||
mkdir -p /usr/lib/dracut/modules.d/99copylog
|
||||
cat > /usr/lib/dracut/modules.d/99copylog/module-setup.sh <<'"'"'EOS'"'"'
|
||||
#!/bin/bash
|
||||
check() { return 0; }
|
||||
depends() { echo base; return 0; }
|
||||
install() {
|
||||
# Guard $moddir for nounset; derive if absent
|
||||
local mdir="${moddir:-$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)}"
|
||||
inst_hook emergency 99 "$mdir/copylog.sh"
|
||||
}
|
||||
EOS
|
||||
chmod +x /usr/lib/dracut/modules.d/99copylog/module-setup.sh
|
||||
|
||||
cat > /usr/lib/dracut/modules.d/99copylog/copylog.sh <<'"'"'EOS'"'"'
|
||||
#!/bin/sh
|
||||
set -e
|
||||
for dev in /dev/nvme0n1p1 /dev/sda1 /dev/sdb1 /dev/mmcblk0p1; do
|
||||
[ -b "$dev" ] || continue
|
||||
mkdir -p /mnt/bootfat
|
||||
if mount -t vfat "$dev" /mnt/bootfat 2>/dev/null; then
|
||||
if [ -s /run/initramfs/rdsosreport.txt ]; then
|
||||
cp -f /run/initramfs/rdsosreport.txt /mnt/bootfat/rdsosreport.txt 2>/dev/null || true
|
||||
sync || true
|
||||
fi
|
||||
umount /mnt/bootfat || true
|
||||
break
|
||||
fi
|
||||
done
|
||||
EOS
|
||||
chmod +x /usr/lib/dracut/modules.d/99copylog/copylog.sh
|
||||
|
||||
# Rebuild to ensure the copylog module is included
|
||||
dracut --force /boot/initramfs-$KVER.img $KVER
|
||||
ln -sf initramfs-$KVER.img /boot/initrd.img
|
||||
cp -a /boot/initramfs-$KVER.img /boot/firmware/initrd.img
|
||||
|
||||
true
|
||||
'
|
||||
}
|
||||
|
||||
verify_boot_assets(){
|
||||
echo "---- verify boot assets on FAT ----"
|
||||
file "$TGT_ROOT/boot/firmware/kernel_2712.img" || true
|
||||
ls -lh "$TGT_ROOT/boot/firmware/initrd.img" || true
|
||||
echo "-- config.txt (key lines) --"
|
||||
grep -E '^(kernel|initramfs|cmdline)=|^dtoverlay=|^dtparam=' "$TGT_ROOT/boot/firmware/config.txt" || true
|
||||
echo "-- cmdline.txt --"
|
||||
cat "$TGT_ROOT/boot/firmware/cmdline.txt" || true
|
||||
echo "-- firmware blobs (sample) --"
|
||||
ls -1 "$TGT_ROOT/boot/firmware"/start*.elf "$TGT_ROOT/boot/firmware"/fixup*.dat | head -n 8 || true
|
||||
echo "-- Pi5 DTB --"
|
||||
ls -l "$TGT_ROOT/boot/firmware/"*rpi-5-b.dtb || true
|
||||
}
|
||||
|
||||
enroll_fido_tokens(){
|
||||
echo "Enrolling FIDO2 Solo keys into $(part 2) ..."
|
||||
need systemd-cryptenroll
|
||||
need fido2-token
|
||||
|
||||
# Collect all hidraw paths from both output styles (some distros print 'Device: /dev/hidrawX')
|
||||
mapfile -t DEVS < <(
|
||||
fido2-token -L \
|
||||
| sed -n 's,^\(/dev/hidraw[0-9]\+\):.*,\1,p; s,^Device:[[:space:]]\+/dev/hidraw\([0-9]\+\).*,/dev/hidraw\1,p' \
|
||||
| sort -u
|
||||
)
|
||||
|
||||
if (( ${#DEVS[@]} == 0 )); then
|
||||
echo "No FIDO2 tokens detected; skipping enrollment (you can enroll later)."
|
||||
echo "Example later: systemd-cryptenroll $(part 2) --fido2-device=/dev/hidrawX --fido2-with-client-pin=no"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Recommend keeping exactly ONE key plugged during first enrollment to avoid ambiguity.
|
||||
if (( ${#DEVS[@]} > 1 )); then
|
||||
echo "Note: multiple FIDO2 tokens present: ${DEVS[*]}"
|
||||
echo "If enrollment fails, try with only one key inserted."
|
||||
fi
|
||||
|
||||
local rc=0
|
||||
for D in "${DEVS[@]}"; do
|
||||
echo "-> Enrolling $D (you should be asked to touch the key)"
|
||||
if ! SYSTEMD_LOG_LEVEL=debug systemd-cryptenroll "$(part 2)" \
|
||||
--fido2-device="$D" \
|
||||
--fido2-with-client-pin=no \
|
||||
--fido2-with-user-presence=yes \
|
||||
--fido2-with-user-verification=no \
|
||||
--label="solo-$(basename "$D")"; then
|
||||
echo "WARN: enrollment failed for $D"
|
||||
rc=1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Tokens enrolled (if any):"
|
||||
systemd-cryptenroll "$(part 2)" --list || true
|
||||
return $rc
|
||||
}
|
||||
|
||||
cleanup(){
|
||||
rm -f "$TGT_ROOT/usr/sbin/policy-rc.d" || true
|
||||
umount -lf "$TGT_ROOT/dev/pts" 2>/dev/null || true
|
||||
for d in dev proc sys; do umount -lf "$TGT_ROOT/$d" 2>/dev/null || true; done
|
||||
umount -lf "$TGT_ROOT/boot/firmware" 2>/dev/null || true
|
||||
umount -lf "$TGT_BOOT" 2>/dev/null || true
|
||||
umount -lf "$TGT_ROOT" 2>/dev/null || true
|
||||
cryptsetup close cryptroot 2>/dev/null || true
|
||||
umount -lf "$IMG_BOOT_MNT" 2>/dev/null || true
|
||||
umount -lf "$IMG_ROOT_MNT" 2>/dev/null || true
|
||||
}
|
||||
|
||||
main(){
|
||||
require_root
|
||||
need losetup; need parted; need rsync
|
||||
auto_detect_target_disk
|
||||
echo "Target disk: $NVME"
|
||||
ensure_binfmt_aarch64
|
||||
ensure_image
|
||||
preflight_cleanup
|
||||
guard_target_device
|
||||
open_image
|
||||
confirm_and_wipe
|
||||
setup_luks
|
||||
mount_targets
|
||||
rsync_root_and_boot
|
||||
write_crypttab_fstab
|
||||
fix_firmware_files
|
||||
seed_cloud_init
|
||||
prep_chroot_mounts
|
||||
in_chroot
|
||||
verify_boot_assets
|
||||
need_host_fido2
|
||||
enroll_fido_tokens
|
||||
cleanup
|
||||
echo "✅ NVMe prepared."
|
||||
echo " Install in the Pi 5 and boot with no SD."
|
||||
echo " Expect LUKS to unlock automatically with a Solo key inserted;"
|
||||
echo " passphrase fallback remains. Hostname: ${STYX_HOSTNAME} User: ${STYX_USER}"
|
||||
echo " On first boot, reach it via: ssh -i ~/.ssh/id_ed25519_titan styx@titan-ag.local"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@ -23,6 +23,11 @@ spec:
|
||||
spec:
|
||||
nodeSelector:
|
||||
jellyfin: "true"
|
||||
securityContext:
|
||||
runAsUser: 1000
|
||||
fsGroup: 65532
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
runAsGroup: 65532
|
||||
runtimeClassName: nvidia
|
||||
containers:
|
||||
- name: jellyfin
|
||||
@ -36,6 +41,12 @@ spec:
|
||||
value: "compute,video,utility"
|
||||
- name: JELLYFIN_PublishedServerUrl
|
||||
value: "https://stream.bstein.dev"
|
||||
- name: PUID
|
||||
value: "1000"
|
||||
- name: PGID
|
||||
value: "65532"
|
||||
- name: UMASK
|
||||
value: "002"
|
||||
resources:
|
||||
limits:
|
||||
nvidia.com/gpu: 1
|
||||
@ -64,4 +75,4 @@ spec:
|
||||
claimName: jellyfin-cache-astreae
|
||||
- name: media
|
||||
persistentVolumeClaim:
|
||||
claimName: jellyfin-media-asteria
|
||||
claimName: jellyfin-media-asteria-new
|
||||
|
||||
@ -36,5 +36,19 @@ spec:
|
||||
accessModes: ["ReadWriteMany"]
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Ti
|
||||
storage: 4Ti
|
||||
storageClassName: asteria
|
||||
|
||||
---
|
||||
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: jellyfin-media-asteria-new
|
||||
namespace: jellyfin
|
||||
spec:
|
||||
accessModes: ["ReadWriteMany"]
|
||||
resources:
|
||||
requests:
|
||||
storage: 4Ti
|
||||
storageClassName: asteria
|
||||
|
||||
171
services/jitsi/deployment.yaml
Normal file
171
services/jitsi/deployment.yaml
Normal file
@ -0,0 +1,171 @@
|
||||
# services/jitsi/deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: jitsi-prosody
|
||||
namespace: jitsi
|
||||
spec:
|
||||
replicas: 0
|
||||
selector:
|
||||
matchLabels: { app: jitsi-prosody }
|
||||
template:
|
||||
metadata:
|
||||
labels: { app: jitsi-prosody }
|
||||
spec:
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: titan-22
|
||||
kubernetes.io/arch: amd64
|
||||
containers:
|
||||
- name: prosody
|
||||
image: jitsi/prosody:stable
|
||||
ports:
|
||||
- { name: c2s, containerPort: 5222, protocol: TCP }
|
||||
- { name: http, containerPort: 5280, protocol: TCP }
|
||||
- { name: comp, containerPort: 5347, protocol: TCP }
|
||||
env:
|
||||
- { name: XMPP_DOMAIN, value: "meet.jitsi" }
|
||||
- { name: XMPP_AUTH_DOMAIN, value: "auth.meet.jitsi" }
|
||||
- { name: XMPP_MUC_DOMAIN, value: "muc.meet.jitsi" }
|
||||
- { name: XMPP_INTERNAL_MUC_DOMAIN, value: "internal-muc.meet.jitsi" }
|
||||
- { name: ENABLE_AUTH, value: "0" } # open instance, no auth (fastest path)
|
||||
- { name: ENABLE_GUESTS, value: "1" }
|
||||
- { name: JICOFO_AUTH_USER, value: "focus" }
|
||||
- { name: JVB_AUTH_USER, value: "jvb" }
|
||||
- name: JICOFO_AUTH_PASSWORD
|
||||
valueFrom: { secretKeyRef: { name: jitsi-internal-secrets, key: JICOFO_AUTH_PASSWORD } }
|
||||
- name: JICOFO_COMPONENT_SECRET
|
||||
valueFrom: { secretKeyRef: { name: jitsi-internal-secrets, key: JICOFO_COMPONENT_SECRET } }
|
||||
- name: JVB_AUTH_PASSWORD
|
||||
valueFrom: { secretKeyRef: { name: jitsi-internal-secrets, key: JVB_AUTH_PASSWORD } }
|
||||
volumeMounts:
|
||||
- { name: cfg, mountPath: /config }
|
||||
volumes:
|
||||
- name: cfg
|
||||
persistentVolumeClaim: { claimName: jitsi-prosody-config }
|
||||
|
||||
---
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: jitsi-jicofo
|
||||
namespace: jitsi
|
||||
spec:
|
||||
replicas: 0
|
||||
selector:
|
||||
matchLabels: { app: jitsi-jicofo }
|
||||
template:
|
||||
metadata:
|
||||
labels: { app: jitsi-jicofo }
|
||||
spec:
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: titan-22
|
||||
kubernetes.io/arch: amd64
|
||||
containers:
|
||||
- name: jicofo
|
||||
image: jitsi/jicofo:stable
|
||||
env:
|
||||
- { name: XMPP_DOMAIN, value: "meet.jitsi" }
|
||||
- { name: XMPP_AUTH_DOMAIN, value: "auth.meet.jitsi" }
|
||||
- { name: XMPP_MUC_DOMAIN, value: "muc.meet.jitsi" }
|
||||
- { name: XMPP_INTERNAL_MUC_DOMAIN, value: "internal-muc.meet.jitsi" }
|
||||
- { name: XMPP_SERVER, value: "jitsi-prosody.jitsi.svc.cluster.local" }
|
||||
- { name: JICOFO_AUTH_USER, value: "focus" }
|
||||
- name: JICOFO_AUTH_PASSWORD
|
||||
valueFrom: { secretKeyRef: { name: jitsi-internal-secrets, key: JICOFO_AUTH_PASSWORD } }
|
||||
- name: JICOFO_COMPONENT_SECRET
|
||||
valueFrom: { secretKeyRef: { name: jitsi-internal-secrets, key: JICOFO_COMPONENT_SECRET } }
|
||||
- { name: JVB_BREWERY_MUC, value: "jvbbrewery" }
|
||||
volumeMounts:
|
||||
- { name: cfg, mountPath: /config }
|
||||
volumes:
|
||||
- name: cfg
|
||||
persistentVolumeClaim: { claimName: jitsi-jicofo-config }
|
||||
|
||||
---
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: jitsi-jvb
|
||||
namespace: jitsi
|
||||
spec:
|
||||
replicas: 0
|
||||
selector:
|
||||
matchLabels: { app: jitsi-jvb }
|
||||
template:
|
||||
metadata:
|
||||
labels: { app: jitsi-jvb }
|
||||
spec:
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: titan-22
|
||||
kubernetes.io/arch: amd64
|
||||
containers:
|
||||
- name: jvb
|
||||
image: jitsi/jvb:stable
|
||||
ports:
|
||||
- { name: colibri-ws, containerPort: 9090, protocol: TCP } # WebSocket control channel
|
||||
- { name: rtp-udp, containerPort: 10000, hostPort: 10000, protocol: UDP } # media
|
||||
- { name: rtp-tcp, containerPort: 4443, hostPort: 4443, protocol: TCP }
|
||||
env:
|
||||
- { name: XMPP_DOMAIN, value: "meet.jitsi" }
|
||||
- { name: XMPP_AUTH_DOMAIN, value: "auth.meet.jitsi" }
|
||||
- { name: XMPP_MUC_DOMAIN, value: "muc.meet.jitsi" }
|
||||
- { name: XMPP_INTERNAL_MUC_DOMAIN, value: "internal-muc.meet.jitsi" }
|
||||
- { name: XMPP_SERVER, value: "jitsi-prosody.jitsi.svc.cluster.local" }
|
||||
- { name: JVB_AUTH_USER, value: "jvb" }
|
||||
- name: JVB_AUTH_PASSWORD
|
||||
valueFrom: { secretKeyRef: { name: jitsi-internal-secrets, key: JVB_AUTH_PASSWORD } }
|
||||
- { name: JVB_BREWERY_MUC, value: "jvbbrewery" }
|
||||
- { name: JVB_PORT, value: "10000" } # matches hostPort above
|
||||
- { name: ENABLE_COLIBRI_WEBSOCKET, value: "1" } # enables /colibri-ws
|
||||
# - { name: JVB_STUN_SERVERS, value: "stun.l.google.com:19302,stun1.l.google.com:19302,meet-jit-si-turnrelay.jitsi.net:443" }
|
||||
- { name: JVB_ENABLE_APIS, value: "rest,colibri" }
|
||||
- { name: JVB_WS_DOMAIN, value: "meet.bstein.dev:443" }
|
||||
- { name: JVB_WS_TLS, value: "true" }
|
||||
- { name: JVB_ADVERTISE_IPS, value: "38.28.125.112" }
|
||||
- { name: JVB_TCP_HARVESTER_DISABLED, value: "false" }
|
||||
- { name: JVB_TCP_PORT, value: "4443" }
|
||||
volumeMounts:
|
||||
- { name: cfg, mountPath: /config }
|
||||
volumes:
|
||||
- name: cfg
|
||||
persistentVolumeClaim: { claimName: jitsi-jvb-config }
|
||||
|
||||
---
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: jitsi-web
|
||||
namespace: jitsi
|
||||
spec:
|
||||
replicas: 0
|
||||
selector:
|
||||
matchLabels: { app: jitsi-web }
|
||||
template:
|
||||
metadata:
|
||||
labels: { app: jitsi-web }
|
||||
spec:
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: titan-22
|
||||
kubernetes.io/arch: amd64
|
||||
containers:
|
||||
- name: web
|
||||
image: jitsi/web:stable
|
||||
ports:
|
||||
- { name: http, containerPort: 80, protocol: TCP }
|
||||
env:
|
||||
- { name: PUBLIC_URL, value: "https://meet.bstein.dev" }
|
||||
- { name: XMPP_DOMAIN, value: "meet.jitsi" }
|
||||
- { name: XMPP_AUTH_DOMAIN, value: "auth.meet.jitsi" }
|
||||
- { name: XMPP_MUC_DOMAIN, value: "muc.meet.jitsi" }
|
||||
- { name: XMPP_INTERNAL_MUC_DOMAIN, value: "internal-muc.meet.jitsi" }
|
||||
- { name: XMPP_BOSH_URL_BASE, value: "https://meet.bstein.dev" }
|
||||
- { name: ENABLE_XMPP_WEBSOCKET, value: "1" }
|
||||
- { name: ENABLE_COLIBRI_WEBSOCKET, value: "1" }
|
||||
volumeMounts:
|
||||
- { name: cfg, mountPath: /config }
|
||||
volumes:
|
||||
- name: cfg
|
||||
persistentVolumeClaim: { claimName: jitsi-web-config }
|
||||
41
services/jitsi/ingress.yaml
Normal file
41
services/jitsi/ingress.yaml
Normal file
@ -0,0 +1,41 @@
|
||||
# services/jitsi/ingress.yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: jitsi
|
||||
namespace: jitsi
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
tls:
|
||||
- hosts: [ "meet.bstein.dev" ]
|
||||
secretName: jitsi-meet-tls
|
||||
rules:
|
||||
- host: meet.bstein.dev
|
||||
http:
|
||||
paths:
|
||||
- path: /colibri-ws
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: jitsi-jvb
|
||||
port: { number: 9090 }
|
||||
- path: /xmpp-websocket
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: jitsi-prosody
|
||||
port: { number: 5280 }
|
||||
- path: /http-bind
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: jitsi-prosody
|
||||
port: { number: 5280 }
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: jitsi-web
|
||||
port: { number: 80 }
|
||||
10
services/jitsi/kustomization.yaml
Normal file
10
services/jitsi/kustomization.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
# services/jitsi/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
- pvc.yaml
|
||||
- ingress.yaml
|
||||
- secret.yaml
|
||||
5
services/jitsi/namespace.yaml
Normal file
5
services/jitsi/namespace.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
# services/jitsi/namespace.yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: jitsi
|
||||
42
services/jitsi/pvc.yaml
Normal file
42
services/jitsi/pvc.yaml
Normal file
@ -0,0 +1,42 @@
|
||||
# services/jitsi/pvc.yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: jitsi-web-config
|
||||
namespace: jitsi
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources: { requests: { storage: 10Gi } }
|
||||
|
||||
---
|
||||
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: jitsi-prosody-config
|
||||
namespace: jitsi
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources: { requests: { storage: 10Gi } }
|
||||
|
||||
---
|
||||
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: jitsi-jicofo-config
|
||||
namespace: jitsi
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources: { requests: { storage: 10Gi } }
|
||||
|
||||
---
|
||||
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: jitsi-jvb-config
|
||||
namespace: jitsi
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources: { requests: { storage: 10Gi } }
|
||||
36
services/jitsi/service.yaml
Normal file
36
services/jitsi/service.yaml
Normal file
@ -0,0 +1,36 @@
|
||||
# services/jitsi/service.yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: jitsi-prosody
|
||||
namespace: jitsi
|
||||
spec:
|
||||
selector: { app: jitsi-prosody }
|
||||
ports:
|
||||
- { name: c2s, port: 5222, targetPort: 5222, protocol: TCP }
|
||||
- { name: http, port: 5280, targetPort: 5280, protocol: TCP }
|
||||
- { name: comp, port: 5347, targetPort: 5347, protocol: TCP }
|
||||
|
||||
---
|
||||
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: jitsi-jvb
|
||||
namespace: jitsi
|
||||
spec:
|
||||
selector: { app: jitsi-jvb }
|
||||
ports:
|
||||
- { name: colibri-ws, port: 9090, targetPort: 9090, protocol: TCP }
|
||||
|
||||
---
|
||||
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: jitsi-web
|
||||
namespace: jitsi
|
||||
spec:
|
||||
selector: { app: jitsi-web }
|
||||
ports:
|
||||
- { name: http, port: 80, targetPort: 80, protocol: TCP }
|
||||
27
services/keycloak/README.md
Normal file
27
services/keycloak/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
# services/keycloak
|
||||
|
||||
Keycloak is deployed via raw manifests and backed by the shared Postgres (`postgres-service.postgres.svc.cluster.local:5432`). Create these secrets before applying:
|
||||
|
||||
```bash
|
||||
# DB creds (per-service DB/user in shared Postgres)
|
||||
kubectl -n sso create secret generic keycloak-db \
|
||||
--from-literal=username=keycloak \
|
||||
--from-literal=password='<DB_PASSWORD>' \
|
||||
--from-literal=database=keycloak
|
||||
|
||||
# Admin console creds (maps to KC admin user)
|
||||
kubectl -n sso create secret generic keycloak-admin \
|
||||
--from-literal=username=brad@bstein.dev \
|
||||
--from-literal=password='<ADMIN_PASSWORD>'
|
||||
```
|
||||
|
||||
Apply:
|
||||
|
||||
```bash
|
||||
kubectl apply -k services/keycloak
|
||||
```
|
||||
|
||||
Notes
|
||||
- Service: `keycloak.sso.svc:80` (Ingress `sso.bstein.dev`, TLS via cert-manager).
|
||||
- Uses Postgres schema `public`; DB/user should be provisioned in the shared Postgres instance.
|
||||
- Health endpoints on :9000 are wired for probes.
|
||||
132
services/keycloak/deployment.yaml
Normal file
132
services/keycloak/deployment.yaml
Normal file
@ -0,0 +1,132 @@
|
||||
# services/keycloak/deployment.yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: keycloak
|
||||
namespace: sso
|
||||
labels:
|
||||
app: keycloak
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: keycloak
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: keycloak
|
||||
spec:
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: hardware
|
||||
operator: In
|
||||
values: ["rpi5","rpi4"]
|
||||
- key: node-role.kubernetes.io/worker
|
||||
operator: Exists
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/hostname
|
||||
operator: In
|
||||
values: ["titan-24"]
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 90
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: hardware
|
||||
operator: In
|
||||
values: ["rpi5"]
|
||||
- weight: 70
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: hardware
|
||||
operator: In
|
||||
values: ["rpi4"]
|
||||
securityContext:
|
||||
runAsUser: 1000
|
||||
runAsGroup: 0
|
||||
fsGroup: 1000
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
containers:
|
||||
- name: keycloak
|
||||
image: quay.io/keycloak/keycloak:26.0.7
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- start
|
||||
env:
|
||||
- name: KC_DB
|
||||
value: postgres
|
||||
- name: KC_DB_URL_HOST
|
||||
value: postgres-service.postgres.svc.cluster.local
|
||||
- name: KC_DB_URL_DATABASE
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: keycloak-db
|
||||
key: database
|
||||
- name: KC_DB_USERNAME
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: keycloak-db
|
||||
key: username
|
||||
- name: KC_DB_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: keycloak-db
|
||||
key: password
|
||||
- name: KC_DB_SCHEMA
|
||||
value: public
|
||||
- name: KC_HOSTNAME
|
||||
value: sso.bstein.dev
|
||||
- name: KC_HOSTNAME_URL
|
||||
value: https://sso.bstein.dev
|
||||
- name: KC_PROXY
|
||||
value: edge
|
||||
- name: KC_PROXY_HEADERS
|
||||
value: xforwarded
|
||||
- name: KC_HTTP_ENABLED
|
||||
value: "true"
|
||||
- name: KC_HTTP_MANAGEMENT_PORT
|
||||
value: "9000"
|
||||
- name: KC_HTTP_MANAGEMENT_BIND_ADDRESS
|
||||
value: 0.0.0.0
|
||||
- name: KC_HEALTH_ENABLED
|
||||
value: "true"
|
||||
- name: KC_METRICS_ENABLED
|
||||
value: "true"
|
||||
- name: KEYCLOAK_ADMIN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: keycloak-admin
|
||||
key: username
|
||||
- name: KEYCLOAK_ADMIN_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: keycloak-admin
|
||||
key: password
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
name: http
|
||||
- containerPort: 9000
|
||||
name: metrics
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health/ready
|
||||
port: 9000
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
failureThreshold: 6
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health/live
|
||||
port: 9000
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 15
|
||||
failureThreshold: 6
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /opt/keycloak/data
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: keycloak-data
|
||||
24
services/keycloak/ingress.yaml
Normal file
24
services/keycloak/ingress.yaml
Normal file
@ -0,0 +1,24 @@
|
||||
# services/keycloak/ingress.yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: keycloak
|
||||
namespace: sso
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
rules:
|
||||
- host: sso.bstein.dev
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: keycloak
|
||||
port:
|
||||
number: 80
|
||||
tls:
|
||||
- hosts: [sso.bstein.dev]
|
||||
secretName: keycloak-tls
|
||||
10
services/keycloak/kustomization.yaml
Normal file
10
services/keycloak/kustomization.yaml
Normal file
@ -0,0 +1,10 @@
|
||||
# services/keycloak/kustomization.yaml
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: sso
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- pvc.yaml
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
- ingress.yaml
|
||||
5
services/keycloak/namespace.yaml
Normal file
5
services/keycloak/namespace.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
# services/keycloak/namespace.yaml
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: sso
|
||||
12
services/keycloak/pvc.yaml
Normal file
12
services/keycloak/pvc.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
# services/keycloak/pvc.yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: keycloak-data
|
||||
namespace: sso
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
storageClassName: astreae
|
||||
15
services/keycloak/service.yaml
Normal file
15
services/keycloak/service.yaml
Normal file
@ -0,0 +1,15 @@
|
||||
# services/keycloak/service.yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: keycloak
|
||||
namespace: sso
|
||||
labels:
|
||||
app: keycloak
|
||||
spec:
|
||||
selector:
|
||||
app: keycloak
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: http
|
||||
28
services/monitoring/README.md
Normal file
28
services/monitoring/README.md
Normal file
@ -0,0 +1,28 @@
|
||||
# services/monitoring
|
||||
|
||||
## Grafana admin secret
|
||||
|
||||
The Grafana Helm release expects a pre-existing secret named `grafana-admin`
|
||||
in the `monitoring` namespace. Create or rotate it with:
|
||||
|
||||
```bash
|
||||
kubectl create secret generic grafana-admin \
|
||||
--namespace monitoring \
|
||||
--from-literal=admin-user=admin \
|
||||
--from-literal=admin-password='REPLACE_ME'
|
||||
```
|
||||
|
||||
Update the password whenever you rotate credentials.
|
||||
|
||||
## DCGM exporter image
|
||||
|
||||
The NVIDIA GPU metrics DaemonSet expects `registry.bstein.dev/monitoring/dcgm-exporter:4.4.2-4.7.0-ubuntu22.04`, mirrored from `docker.io/nvidia/dcgm-exporter:4.4.2-4.7.0-ubuntu22.04`. Refresh it in Zot when bumping versions:
|
||||
|
||||
```bash
|
||||
skopeo copy \
|
||||
--all \
|
||||
docker://docker.io/nvidia/dcgm-exporter:4.4.2-4.7.0-ubuntu22.04 \
|
||||
docker://registry.bstein.dev/monitoring/dcgm-exporter:4.4.2-4.7.0-ubuntu22.04
|
||||
```
|
||||
|
||||
When finished mirroring from the control-plane, you can remove temporary tooling with `sudo apt-get purge -y skopeo && sudo apt-get autoremove -y` and clear `~/.config/containers/auth.json`.
|
||||
184
services/monitoring/dashboards/atlas-gpu.json
Normal file
184
services/monitoring/dashboards/atlas-gpu.json
Normal file
@ -0,0 +1,184 @@
|
||||
{
|
||||
"uid": "atlas-gpu",
|
||||
"title": "Atlas GPU",
|
||||
"folderUid": "atlas-internal",
|
||||
"editable": true,
|
||||
"panels": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "piechart",
|
||||
"title": "Namespace GPU Share",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "atlas-vm"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "100 * ( ( (sum by (namespace) (max_over_time(DCGM_FI_DEV_GPU_UTIL{namespace!=\"\",pod!=\"\"}[$__range]))) or on(namespace) (sum(rate(container_cpu_usage_seconds_total{namespace!=\"\",pod!=\"\",container!=\"\"}[5m])) by (namespace) * 0) ) and on(namespace) ( (topk(10, ( sum(rate(container_cpu_usage_seconds_total{namespace!=\"\",pod!=\"\",container!=\"\"}[5m])) by (namespace) ) + (sum(container_memory_working_set_bytes{namespace!=\"\",pod!=\"\",container!=\"\"}) by (namespace) / 1e9) + ((sum((kube_pod_container_resource_requests{namespace!=\"\",resource=\"nvidia.com/gpu\"} or kube_pod_container_resource_limits{namespace!=\"\",resource=\"nvidia.com/gpu\"})) by (namespace)) or on(namespace) (sum(rate(container_cpu_usage_seconds_total{namespace!=\"\",pod!=\"\",container!=\"\"}[5m])) by (namespace) * 0) * 100)) >= bool 0) ) ) / clamp_min(sum( ( (sum by (namespace) (max_over_time(DCGM_FI_DEV_GPU_UTIL{namespace!=\"\",pod!=\"\"}[$__range]))) or on(namespace) (sum(rate(container_cpu_usage_seconds_total{namespace!=\"\",pod!=\"\",container!=\"\"}[5m])) by (namespace) * 0) ) and on(namespace) ( (topk(10, ( sum(rate(container_cpu_usage_seconds_total{namespace!=\"\",pod!=\"\",container!=\"\"}[5m])) by (namespace) ) + (sum(container_memory_working_set_bytes{namespace!=\"\",pod!=\"\",container!=\"\"}) by (namespace) / 1e9) + ((sum((kube_pod_container_resource_requests{namespace!=\"\",resource=\"nvidia.com/gpu\"} or kube_pod_container_resource_limits{namespace!=\"\",resource=\"nvidia.com/gpu\"})) by (namespace)) or on(namespace) (sum(rate(container_cpu_usage_seconds_total{namespace!=\"\",pod!=\"\",container!=\"\"}[5m])) by (namespace) * 0) * 100)) >= bool 0) ) ), 1)",
|
||||
"refId": "A",
|
||||
"legendFormat": "{{namespace}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "percent",
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"legend": {
|
||||
"displayMode": "list",
|
||||
"placement": "right"
|
||||
},
|
||||
"pieType": "pie",
|
||||
"displayLabels": [
|
||||
"percent"
|
||||
],
|
||||
"tooltip": {
|
||||
"mode": "single"
|
||||
},
|
||||
"colorScheme": "interpolateSpectral",
|
||||
"colorBy": "value",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"type": "timeseries",
|
||||
"title": "GPU Util by Namespace",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "atlas-vm"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(DCGM_FI_DEV_GPU_UTIL{namespace!=\"\",pod!=\"\"}) by (namespace)",
|
||||
"refId": "A",
|
||||
"legendFormat": "{{namespace}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "percent"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"legend": {
|
||||
"displayMode": "table",
|
||||
"placement": "right"
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"type": "timeseries",
|
||||
"title": "GPU Util by Node",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "atlas-vm"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum by (Hostname) (DCGM_FI_DEV_GPU_UTIL{pod!=\"\"})",
|
||||
"refId": "A",
|
||||
"legendFormat": "{{Hostname}}"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "percent"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"legend": {
|
||||
"displayMode": "table",
|
||||
"placement": "right"
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "table",
|
||||
"title": "Top Pods by GPU Util",
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "atlas-vm"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 8
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"expr": "topk(10, sum(DCGM_FI_DEV_GPU_UTIL{pod!=\"\"}) by (namespace,pod,Hostname))",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"unit": "percent"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"options": {
|
||||
"showHeader": true
|
||||
},
|
||||
"transformations": [
|
||||
{
|
||||
"id": "labelsToFields",
|
||||
"options": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"time": {
|
||||
"from": "now-12h",
|
||||
"to": "now"
|
||||
},
|
||||
"annotations": {
|
||||
"list": []
|
||||
},
|
||||
"schemaVersion": 39,
|
||||
"style": "dark",
|
||||
"tags": [
|
||||
"atlas",
|
||||
"gpu"
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user