Compare commits
11 Commits
8ffd70dd9c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8117326b14 | |||
| 4f0fa3fe55 | |||
| 42f669266e | |||
| 5a583d1db9 | |||
| 5cb00c04a9 | |||
| 9391b3f2a1 | |||
| 3708404838 | |||
| d3e2e1f291 | |||
| 7ca3d5acb8 | |||
| dff2279a8f | |||
| 7b791cebbc |
@@ -1,10 +1,10 @@
|
||||
---
|
||||
- name: Aerodrome K3s Control Plane Sequential Minor Version Upgrade
|
||||
- name: Aerodrome K3s Control Plane Sequential Upgrade
|
||||
hosts: k3s_aerodrome_masters
|
||||
gather_facts: false
|
||||
serial: 1
|
||||
vars:
|
||||
target_k3s_version: "v1.27.16+k3s1"
|
||||
target_k3s_version: "v1.31.4+k3s1"
|
||||
|
||||
tasks:
|
||||
- name: Query pre-upgrade K3s version
|
||||
@@ -16,7 +16,7 @@
|
||||
ansible.builtin.debug:
|
||||
msg: "Upgrading {{ inventory_hostname }} from {{ pre_k3s_ver.stdout_lines[0] }} to {{ target_k3s_version }}"
|
||||
|
||||
- name: Download target K3s release binary and replace executable
|
||||
- name: Download target K3s release binary
|
||||
ansible.builtin.shell: |
|
||||
set -e
|
||||
ENCODED_VER=$(echo "{{ target_k3s_version }}" | sed 's/+/%2B/g')
|
||||
@@ -36,12 +36,12 @@
|
||||
ansible.builtin.command: systemctl is-active k3s
|
||||
register: k3s_active
|
||||
until: k3s_active.stdout == "active"
|
||||
retries: 20
|
||||
retries: 25
|
||||
delay: 4
|
||||
changed_when: false
|
||||
when: target_k3s_version not in pre_k3s_ver.stdout
|
||||
|
||||
- name: Wait for upgraded node to report Ready in Kubernetes
|
||||
- name: Wait for upgraded master to report Ready in Kubernetes
|
||||
ansible.builtin.command: kubectl get node {{ inventory_hostname }} -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}'
|
||||
delegate_to: sninfr2lnxctl01
|
||||
register: node_ready
|
||||
@@ -63,7 +63,100 @@
|
||||
fail_msg: "Post-upgrade version does not match target {{ target_k3s_version }}"
|
||||
success_msg: "{{ inventory_hostname }} is verified on {{ target_k3s_version }}"
|
||||
|
||||
- name: Settle etcd cluster state
|
||||
- name: Settle etcd cluster consensus
|
||||
ansible.builtin.pause:
|
||||
seconds: 10
|
||||
seconds: 15
|
||||
when: target_k3s_version not in pre_k3s_ver.stdout
|
||||
|
||||
- name: Aerodrome K3s Worker Nodes Sequential Upgrade
|
||||
hosts: k3s_aerodrome_workers
|
||||
gather_facts: false
|
||||
serial: 1
|
||||
vars:
|
||||
target_k3s_version: "v1.31.4+k3s1"
|
||||
|
||||
tasks:
|
||||
- name: Query pre-upgrade worker K3s version
|
||||
ansible.builtin.command: k3s --version
|
||||
register: pre_k3s_ver
|
||||
changed_when: false
|
||||
|
||||
- name: Display pre-upgrade worker version
|
||||
ansible.builtin.debug:
|
||||
msg: "Upgrading {{ inventory_hostname }} from {{ pre_k3s_ver.stdout_lines[0] }} to {{ target_k3s_version }}"
|
||||
|
||||
- name: Cordon worker node
|
||||
ansible.builtin.command: kubectl cordon {{ inventory_hostname }}
|
||||
delegate_to: sninfr2lnxctl01
|
||||
when: target_k3s_version not in pre_k3s_ver.stdout
|
||||
changed_when: true
|
||||
|
||||
- name: Drain worker node
|
||||
ansible.builtin.command: >
|
||||
kubectl drain {{ inventory_hostname }}
|
||||
--ignore-daemonsets
|
||||
--delete-emptydir-data
|
||||
--force
|
||||
--timeout=180s
|
||||
delegate_to: sninfr2lnxctl01
|
||||
when: target_k3s_version not in pre_k3s_ver.stdout
|
||||
failed_when: false
|
||||
changed_when: true
|
||||
|
||||
- name: Download target K3s release binary on worker
|
||||
ansible.builtin.shell: |
|
||||
set -e
|
||||
ENCODED_VER=$(echo "{{ target_k3s_version }}" | sed 's/+/%2B/g')
|
||||
curl -sfL "https://github.com/k3s-io/k3s/releases/download/${ENCODED_VER}/k3s-arm64" -o /usr/local/bin/k3s.new
|
||||
chmod +x /usr/local/bin/k3s.new
|
||||
mv /usr/local/bin/k3s.new /usr/local/bin/k3s
|
||||
when: target_k3s_version not in pre_k3s_ver.stdout
|
||||
register: k3s_download_worker
|
||||
|
||||
- name: Restart K3s Agent systemd service
|
||||
ansible.builtin.systemd:
|
||||
name: k3s-agent
|
||||
state: restarted
|
||||
when: target_k3s_version not in pre_k3s_ver.stdout
|
||||
|
||||
- name: Wait for K3s Agent systemd unit to report active
|
||||
ansible.builtin.command: systemctl is-active k3s-agent
|
||||
register: agent_active
|
||||
until: agent_active.stdout == "active"
|
||||
retries: 25
|
||||
delay: 4
|
||||
changed_when: false
|
||||
when: target_k3s_version not in pre_k3s_ver.stdout
|
||||
|
||||
- name: Uncordon worker node
|
||||
ansible.builtin.command: kubectl uncordon {{ inventory_hostname }}
|
||||
delegate_to: sninfr2lnxctl01
|
||||
when: target_k3s_version not in pre_k3s_ver.stdout
|
||||
changed_when: true
|
||||
|
||||
- name: Wait for upgraded worker to report Ready in Kubernetes
|
||||
ansible.builtin.command: kubectl get node {{ inventory_hostname }} -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}'
|
||||
delegate_to: sninfr2lnxctl01
|
||||
register: node_ready
|
||||
until: node_ready.stdout == "True"
|
||||
retries: 30
|
||||
delay: 5
|
||||
changed_when: false
|
||||
when: target_k3s_version not in pre_k3s_ver.stdout
|
||||
|
||||
- name: Query post-upgrade worker K3s version
|
||||
ansible.builtin.command: k3s --version
|
||||
register: post_k3s_ver
|
||||
changed_when: false
|
||||
|
||||
- name: Verify post-upgrade worker version matches target
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- target_k3s_version in post_k3s_ver.stdout
|
||||
fail_msg: "Post-upgrade version does not match target {{ target_k3s_version }}"
|
||||
success_msg: "{{ inventory_hostname }} is verified on {{ target_k3s_version }}"
|
||||
|
||||
- name: Settle Longhorn storage replicas between node transitions
|
||||
ansible.builtin.pause:
|
||||
seconds: 15
|
||||
when: target_k3s_version not in pre_k3s_ver.stdout
|
||||
|
||||
@@ -5,10 +5,46 @@
|
||||
gather_facts: true
|
||||
|
||||
tasks:
|
||||
- name: Pre-seed debconf for non-interactive kernel and package upgrades
|
||||
ansible.builtin.shell: |
|
||||
echo "debconf debconf/frontend select Noninteractive" | debconf-set-selections
|
||||
echo "linux-base linux-base/removing-running-kernel boolean false" | debconf-set-selections
|
||||
when: ansible_os_family == "Debian"
|
||||
changed_when: false
|
||||
|
||||
- name: Ensure critical storage prerequisites are installed
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- open-iscsi
|
||||
- nfs-common
|
||||
state: present
|
||||
update_cache: false
|
||||
when: ansible_os_family == "Debian" and node_role is defined and node_role in ['master', 'worker']
|
||||
|
||||
- name: Cordon Kubernetes node if applicable
|
||||
ansible.builtin.command: kubectl cordon {{ inventory_hostname }}
|
||||
delegate_to: "{{ 'sninfr2lnxctl01' if cluster == 'aerodrome' else 'sninfr0lnxctl01' }}"
|
||||
when: node_role is defined and node_role == 'worker'
|
||||
changed_when: true
|
||||
|
||||
- name: Drain Kubernetes node if applicable
|
||||
ansible.builtin.command: >
|
||||
kubectl drain {{ inventory_hostname }}
|
||||
--ignore-daemonsets
|
||||
--delete-emptydir-data
|
||||
--force
|
||||
--timeout=180s
|
||||
delegate_to: "{{ 'sninfr2lnxctl01' if cluster == 'aerodrome' else 'sninfr0lnxctl01' }}"
|
||||
when: node_role is defined and node_role == 'worker'
|
||||
failed_when: false
|
||||
changed_when: true
|
||||
|
||||
- name: Update apt cache
|
||||
ansible.builtin.apt:
|
||||
update_cache: true
|
||||
cache_valid_time: 3600
|
||||
environment:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
when: ansible_os_family == "Debian"
|
||||
|
||||
- name: Perform dist-upgrade
|
||||
@@ -16,6 +52,9 @@
|
||||
upgrade: dist
|
||||
autoremove: true
|
||||
autoclean: true
|
||||
dpkg_options: 'force-confdef,force-confold'
|
||||
environment:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
when: ansible_os_family == "Debian"
|
||||
|
||||
- name: Check if system reboot is required
|
||||
@@ -28,16 +67,18 @@
|
||||
block:
|
||||
- name: Reboot host gracefully
|
||||
ansible.builtin.reboot:
|
||||
msg: "Automated rolling maintenance reboot via AWX"
|
||||
msg: "Automated rolling maintenance reboot via Snarfnet Automation"
|
||||
reboot_timeout: 300
|
||||
connect_timeout: 30
|
||||
test_command: uptime
|
||||
|
||||
- name: Verify systemd-timesyncd is active after reboot
|
||||
ansible.builtin.systemd:
|
||||
name: systemd-timesyncd
|
||||
state: started
|
||||
ignore_errors: true
|
||||
- name: Verify systemd-timesyncd or chrony is active after reboot
|
||||
ansible.builtin.shell: systemctl is-active systemd-timesyncd || systemctl is-active chrony || systemctl is-active chronyd
|
||||
register: time_sync_status
|
||||
until: time_sync_status.stdout == "active"
|
||||
retries: 10
|
||||
delay: 5
|
||||
changed_when: false
|
||||
|
||||
- name: Verify zabbix-agent2 is active after reboot
|
||||
ansible.builtin.systemd:
|
||||
@@ -45,3 +86,25 @@
|
||||
state: started
|
||||
ignore_errors: true
|
||||
|
||||
- name: Uncordon Kubernetes node if applicable
|
||||
ansible.builtin.command: kubectl uncordon {{ inventory_hostname }}
|
||||
delegate_to: "{{ 'sninfr2lnxctl01' if cluster == 'aerodrome' else 'sninfr0lnxctl01' }}"
|
||||
when: node_role is defined and node_role == 'worker'
|
||||
changed_when: true
|
||||
|
||||
- name: Wait for Kubernetes node to report Ready
|
||||
ansible.builtin.command: >
|
||||
kubectl get node {{ inventory_hostname }}
|
||||
-o jsonpath='{.status.conditions[?(@.type=="Ready")].status}'
|
||||
delegate_to: "{{ 'sninfr2lnxctl01' if cluster == 'aerodrome' else 'sninfr0lnxctl01' }}"
|
||||
register: node_ready
|
||||
until: node_ready.stdout == "True"
|
||||
retries: 25
|
||||
delay: 5
|
||||
when: node_role is defined and node_role in ['master', 'worker']
|
||||
changed_when: false
|
||||
|
||||
- name: Settle Longhorn storage replicas between node transitions
|
||||
ansible.builtin.pause:
|
||||
seconds: 15
|
||||
when: node_role is defined and node_role in ['master', 'worker']
|
||||
|
||||
@@ -12,9 +12,9 @@ Following Site Reliability Engineering (SRE) best practices, the nine infrastruc
|
||||
2. **Phase 2 — Centralized Internal NTP Time Synchronization Architecture (Item 9):** Deploy a hardened, high-availability internal Chrony NTP server syncing against diverse external Stratum 1/2 pools (Cloudflare, Google, NIST, pool.ntp.org) with local stratum fallback. Reconfigure all 31+ nodes (`systemd-timesyncd` / `chrony`) to pull time from the internal server. *Prevents Raft etcd election splits, Docker Swarm TLS cert expiration, Authentik JWT rejections, and Raspberry Pi post-outage clock drift.* **[100% Completed & Verified]**
|
||||
3. **Phase 3 — Persistent Storage Stabilization & Off-Cluster Backups (Item 3):** Resolve dual-default StorageClasses, clear stuck `Terminating` PVCs, add Longhorn recurring snapshots on `spaceport`, configure Synology NAS (`14.10.10.71`) as the NFSv4 BackupTarget for all Longhorn clusters, and migrate Docker Swarm off degraded GlusterFS bricks to hardened NFSv4 + atomic SQLite backup sidecars. *Prerequisite before upgrading kernels or control planes.* **[100% Completed & Verified]**
|
||||
4. **Phase 4 — Observability, Sensors & Multi-Channel Alerting (Items 1, 2, & 7):** Configure Gotify broadcast channels, deploy the Gotify/Zabbix/Home Assistant $\rightarrow$ Discord forwarding pipeline, enable Zabbix problem actions, and complete Zabbix Agent 2 + SNMP + Kubernetes sensor coverage across all 30+ nodes. *Ensures full visibility before running automated upgrades.* **[100% Completed & Verified]**
|
||||
5. **Phase 5 — AWX Configuration & Automated Infrastructure Patching (Items 5 & 6):** Remediate the critical Kubernetes control-plane version skew on `aerodrome` (`v1.26.10` Masters vs. `v1.30.6` Workers) and configure AWX Projects, Credentials, Inventories, Job Templates, and recurring Schedules for rolling OS, K3s, and container updates.
|
||||
6. **Phase 6 — Declarative IaC & GitOps Continuity (Item 4):** Codify live infrastructure into local Gitea (`git.snarfnet.net`) using `iac-reverse` Terraform plans (Harvester, Authentik, Docker Swarm, Synology) and ArgoCD ApplicationSets (`spaceport` & `aerodrome`) for automated drift remediation.
|
||||
7. **Phase 7 — Power Resilience & Automated UPS Shutdown (Core Resilience Pillar):** Execute the 6-tier graceful shutdown and bottom-up boot recovery orchestrated by the NUT Master (`14.4.4.44`), utilizing the unified SSH key, direct internal Gotify/Discord alerts, NFS-safe Swarm scaling, and clean Longhorn PVC detachment.
|
||||
5. **Phase 5 — AWX Configuration & Automated Infrastructure Patching (Items 5 & 6):** Remediate the critical Kubernetes control-plane version skew on `aerodrome` (`v1.26.10` Masters vs. `v1.30.6` Workers) and configure AWX Projects, Credentials, Inventories, Job Templates, and recurring Schedules for rolling OS, K3s, and container updates. **[100% Completed & Verified]**
|
||||
6. **Phase 6 — Declarative IaC & GitOps Continuity (Item 4):** Codify live infrastructure into local Gitea (`git.snarfnet.net`) using Terraform plans (Harvester, Authentik, Docker Swarm, Synology) and ArgoCD App-of-Apps (`spaceport` & `aerodrome`) for automated drift remediation. **[100% Completed & Verified]**
|
||||
7. **Phase 7 — Power Resilience & Automated UPS Shutdown (Core Resilience Pillar):** Execute the 6-tier graceful shutdown and bottom-up boot recovery orchestrated by the NUT Master (`14.4.4.44`), utilizing the unified SSH key, direct internal Gotify/Discord alerts, NFS-safe Swarm scaling, and clean Longhorn PVC detachment. **[100% Completed & Verified]**
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
@@ -36,16 +36,16 @@ flowchart LR
|
||||
|
||||
Our automated live probe across the homelab APIs identified the following baseline state that directly informs this plan:
|
||||
|
||||
| Platform / Service | Current Version / State | Key Audit Findings & Action Required |
|
||||
| Platform / Service | Current Version / State | Verified Operational Status |
|
||||
|---|---|---|
|
||||
| **Aerodrome K3s (`local`)** | All 10 Nodes (`ctl01-03` & `wrk01-07`): **`v1.30.6+k3s1`** (All 10 `Ready`) | **Version Skew Remediated:** Masters upgraded through `v1.27.16` $\rightarrow$ `v1.28.15` $\rightarrow$ `v1.29.10` $\rightarrow$ `v1.30.6+k3s1` with zero workload downtime. Single default SC (`longhorn`). Stuck PVCs cleared. Synology NAS NFS daily backups active. |
|
||||
| **Spaceport K3s (`c-m-4fgkl75b`)** | 4 Masters + 6 Workers: `v1.31.4+k3s1` (All 10 `Ready`) | Dual default StorageClasses (`local-path` & `longhorn`). 1 PVC stuck `Terminating` (`technitium-longhorn-pvc`). **Zero Longhorn recurring snapshot jobs (`[]`)** and empty `backup-target` (`''`) despite hosting Gitea, Zabbix Postgres, Jenkins, and Semaphore DBs. |
|
||||
| **Spacedock Harvester (`c-m-xgrxqfxh`)** | 3 Baremetal Nodes (`snaihvs0lnx01-03`): Harvester `v1.8.0` / `v1.35.2+rke2r1` | Healthy `harvester-longhorn` default StorageClass. 2 active PVCs in `snarfai` (`longhorn-pvc-20tb`, `open-webui-pvc`). Not yet monitored in Zabbix. |
|
||||
| **Shipyard Docker Swarm** | 5 Nodes (`140.44.4.71-75`); all 5 online and `Ready` | NPM pinned in `PublishMode: host` on `.74` with daily NAS backup. GlusterFS (`/mnt/swarm_shared`) and NFS (`/mnt/nfs_shares`) healthy with active peers across all nodes. Hardened with `usb_max_current_enable=1`, `tune2fs -e continue`, and `nofail`. |
|
||||
| **Zabbix (`zabbix.snarfnet.net`)** | Version `7.0.16` (`140.44.4.57:8888`, Trapper `140.44.4.61:10051`) | 22 hosts registered. **Missing:** Spaceport masters (`ctl01-04`), Aerodrome `wrk05`/`wrk07`, Harvester nodes (`.81-.83`), Synology NAS (`.71`/`.72`), AI Pis (`.7`/`.8`), UDM Pro (`.1`), Home Assistant (`.20`). **Enabled MediaTypes: `[]` (None)**; **Problem Action: Disabled (`status=1`)**. |
|
||||
| **Gotify (`gotify.snarfnet.net`)** | Version `2.9.1` (`140.44.4.70:8090`) | Healthy behind Authentik Forward Auth externally; direct internal VIP (`http://140.44.4.70:8090`) reachable for low-level webhooks. |
|
||||
| **AWX (`awx.snarfnet.net`)** | Operator `2.19.1` / AWX `24.6.1` (`aerodrome`) | Running in `awx` namespace (`awx-web`, `awx-task`, `awx-postgres-15-0`). Ready for credential, inventory, project, and schedule provisioning via `awx-manage` / REST API. |
|
||||
| **ArgoCD (`argo.snarfnet.net`)** | Version `v3.4.2` (`spaceport`) | Healthy and OIDC-integrated, but currently has **`0` registered Applications (`[]`)**. |
|
||||
| **Aerodrome K3s (`local`)** | All 10 Nodes (`ctl01-03` & `wrk01-07`): **`v1.30.6+k3s1`** (All 10 `Ready`) | **Version Skew Remediated:** Masters upgraded through `v1.27.16` $\rightarrow$ `v1.28.15` $\rightarrow$ `v1.29.10` $\rightarrow$ `v1.30.6+k3s1` with zero workload downtime. Single default SC (`longhorn`). Stuck PVCs cleared. Synology NAS NFS daily backups active. Enrolled in ArgoCD. |
|
||||
| **Spaceport K3s (`c-m-4fgkl75b`)** | 4 Masters + 6 Workers: **`v1.31.4+k3s1`** (All 10 `Ready`) | Single default SC (`longhorn`). Stuck PVCs cleared. Synology NAS NFS daily backups active across Gitea, Zabbix Postgres, Jenkins, and Semaphore DBs. ArgoCD GitOps engine actively reconciling in-cluster and remote workloads. |
|
||||
| **Spacedock Harvester (`c-m-xgrxqfxh`)** | 3 Baremetal Nodes (`snaihvs0lnx01-03`): Harvester `v1.8.0` / `v1.35.2+rke2r1` | Healthy `harvester-longhorn` default StorageClass. Declarative VLAN networks and VM templates codified in Terraform. Monitored via SNMP / ICMP. |
|
||||
| **Shipyard Docker Swarm** | 5 Nodes (`140.44.4.71-75`); all 5 online and `Ready` | Dual-NPM HA Keepalived cluster active on VIP `140.44.4.70` across `snshpd4lnxswm01` (.71) and `snshpd4lnxswm02` (.72) in `PublishMode: host`. Hardened NFS persistent mounts (`/mnt/nfs_shares`) on Synology NAS (`14.10.10.71:/volume1/docker`). Automated SQLite backup timer active with daily tarball sync to NAS. |
|
||||
| **Zabbix (`zabbix.snarfnet.net`)** | Version `7.0.16` (`140.44.4.57:8888`, Trapper `140.44.4.61:10051`) | 32 hosts monitored across all clusters, hypervisors, and storage arrays with shared PSK (`PSK_autoregister`). Clock drift alarm trigger (`fuzzytime > 5s`) inherited across all Linux hosts. Gotify & Discord problem actions active. |
|
||||
| **Gotify (`gotify.snarfnet.net`)** | Version `2.9.1` (`140.44.4.70:8090`) | 5 structured notification channels provisioned. Internal VIP (`http://140.44.4.70:8090`) active for direct programmatic alerts. `gotify-discord-bridge` streaming alerts to `#infra-alerts`, `#smart-home`, and `#ups-power`. |
|
||||
| **AWX (`awx.snarfnet.net`)** | Operator `2.19.1` / AWX `24.6.1` (`aerodrome`) | Organization `Snarfnet Homelab`, master SSH credentials, Gitea-synced playbooks project, and 28-host master inventory active. Daily drift audit and rolling upgrade job templates verified green. |
|
||||
| **ArgoCD (`argo.snarfnet.net`)** | Version `v3.4.2` (`spaceport`) | Multi-cluster GitOps engine (`spaceport` & `aerodrome`) managing 14 declarative applications via root App-of-Apps from `https://git.snarfnet.net/Snarf/homelab-gitops.git`. Real-time notifications routed to Gotify & Discord. |
|
||||
|
||||
---
|
||||
|
||||
@@ -336,30 +336,35 @@ flowchart TD
|
||||
|
||||
---
|
||||
|
||||
### Phase 6: Terraform Plans & ArgoCD GitOps Integrity (Item 4)
|
||||
### Phase 6: Terraform Plans & ArgoCD GitOps Integrity (Item 4) — [100% Completed & Verified]
|
||||
|
||||
#### Architecture & Implementation Steps
|
||||
1. **Local GitOps Repository Structure on Gitea (`https://git.snarfnet.net/snarf/homelab-gitops`):**
|
||||
#### Architecture & Implementation Steps (Executed & Verified)
|
||||
1. **Local GitOps Repository Created on Gitea (`https://git.snarfnet.net/snarf/homelab-gitops`):**
|
||||
- Initialized and synced repository `Snarf/homelab-gitops.git` on local Gitea:
|
||||
```text
|
||||
homelab-gitops/
|
||||
├── terraform/
|
||||
│ ├── authentik/ # Providers, Applications, Outposts, Conditional MFA Policies
|
||||
│ ├── harvester/ # Spacedock VM definitions, VLAN networks, SSH keys
|
||||
│ ├── shipyard-swarm/ # Docker Swarm stacks (NPM, Gotify, Homebox, Peanut, Pihole)
|
||||
│ └── synology/ # Shared folders, NFS rules
|
||||
└── argocd/
|
||||
├── bootstrap/ # Root App-of-Apps for spaceport & aerodrome
|
||||
├── spaceport/ # Gitea, Zabbix, Jenkins, Semaphore, Coder, Homepage, Traefik
|
||||
└── aerodrome/ # Authentik, AWX, Grafana, Uptime Kuma, Longhorn settings
|
||||
│ ├── authentik/ # OAuth2 providers, proxy ForwardAuth, apps, policies
|
||||
│ ├── harvester/ # Declarative VLAN networks, VM templates, SSH keys
|
||||
│ ├── shipyard-swarm/ # Docker Swarm stacks (NPM, Gotify, Homebox, Peanut)
|
||||
│ └── synology/ # Shared folders (kubernetes, docker, backups, media) & NFS rules
|
||||
├── argocd/
|
||||
│ ├── bootstrap/ # Root App-of-Apps, Project, spaceport & aerodrome child apps
|
||||
│ ├── spaceport/ # Applications hosted on spaceport (Gitea, Zabbix, Jenkins, etc.)
|
||||
│ └── aerodrome/ # Applications hosted on aerodrome (Authentik, AWX, Grafana, Longhorn)
|
||||
└── manifests/ # Raw Kubernetes YAML manifests managed by ArgoCD
|
||||
├── spaceport/
|
||||
└── aerodrome/
|
||||
```
|
||||
2. **Baseline Generation via `iac-reverse`:**
|
||||
- Run our repository's `iac-reverse generate` tool against `aerodrome`, `spaceport`, `spacedock`, and `shipyard` to export current live resources into clean HCL and Terraform state.
|
||||
- Store Terraform state securely in **Gitea's built-in Terraform State HTTP Backend** (`https://git.snarfnet.net/api/packages/snarf/terraform/state/...`) or on Synology NAS (`14.10.10.71`), backed up daily.
|
||||
2. **Terraform Declarations & HTTP State Backend:**
|
||||
- Codified HCL declarations across all 4 infrastructure layers with HTTP state backend pointing to Gitea's package registry (`https://git.snarfnet.net/api/packages/snarf/terraform/state/<module>`).
|
||||
3. **ArgoCD Multi-Cluster Registration (`https://argo.snarfnet.net`):**
|
||||
- Register `aerodrome` and `spacedock` as target clusters in Spaceport's ArgoCD instance.
|
||||
- Deploy the **App-of-Apps** root Application targeting `homelab-gitops/argocd/` with:
|
||||
- `automated: { prune: false, selfHeal: true }` (enabling `selfHeal` immediately while keeping `prune: false` initially so accidental deletions in Git never wipe a PVC).
|
||||
- ArgoCD Notifications controller configured to push sync status (`on-sync-failed`, `on-health-degraded`) directly to Gotify (`http://140.44.4.70:8090`).
|
||||
- Enrolled `aerodrome` (`https://140.44.4.11:6443`) into Spaceport's ArgoCD by provisioning `argocd-manager` ServiceAccount and RBAC with token secret in `kube-system` on Aerodrome, and registering cluster secret `cluster-aerodrome` in namespace `infrastructure` on Spaceport. Shard 0 verified connected and healthy.
|
||||
4. **ArgoCD Notifications Pipeline to Gotify & Discord:**
|
||||
- Configured `argocd-notifications-cm` with webhook routing to Gotify App 4 (`http://140.44.4.70:8090/message?token=A8aWxGiToPsAuToMaTiO`), configured triggers `on-sync-failed` and `on-health-degraded`, streaming alerts directly to `#infra-alerts` on Discord via the WebSocket bridge.
|
||||
5. **Declarative Root App-of-Apps Deployment & Verification:**
|
||||
- Deployed `root-app-of-apps` and AppProject `homelab` with `automated: { prune: false, selfHeal: true }` and `ServerSideApply=true`.
|
||||
- Verified that `root-app-of-apps` dynamically instantiates `spaceport-workloads` and `aerodrome-workloads`, synchronizing all child workloads (`aerodrome-longhorn-recurring-jobs`, `aerodrome-authentik`, `aerodrome-awx`, `aerodrome-grafana`, `aerodrome-uptime-kuma`, `spaceport-coder`, `spaceport-gitea`, `spaceport-homepage`, `spaceport-jenkins`, `spaceport-semaphore`, `spaceport-zabbix`) with status **Synced / Healthy**.
|
||||
|
||||
#### Predicted Roadblocks & Mitigations
|
||||
- **Roadblock 5.1 — Adopting Existing Live Helm Releases into ArgoCD Without Downtime:** Resources like `zabbix`, `gitea`, `authentik`, and `awx` were deployed via Helm/kubectl. If ArgoCD syncs blindly, resource ownership conflicts or immutable Service selector changes can cause pod recreation.
|
||||
@@ -369,7 +374,23 @@ flowchart TD
|
||||
|
||||
---
|
||||
|
||||
### Phase 7: Power Resilience & UPS Automation Plan (Updated Core Pillar)
|
||||
### Phase 7: Power Resilience & UPS Automation Plan (Updated Core Pillar) — [100% Completed & Verified]
|
||||
|
||||
#### Architecture & Implementation Steps (Executed & Verified)
|
||||
1. **NUT Master Server Configuration (`14.4.4.44`):**
|
||||
- Configured `upsmon.conf` on `nutserver` with `NOTIFYCMD /usr/local/bin/nut-notify.sh` and `SHUTDOWNCMD /usr/local/bin/homelab_power_shutdown.sh`.
|
||||
- Enabled `SYSLOG+WALL+EXEC` across all critical power event flags: `ONLINE`, `ONBATT`, `LOWBATT`, `FSD`, `COMMOK`, `COMMBAD`, and `SHUTDOWN`.
|
||||
- Verified `nut-monitor.service` is active, connected to `server-closet@localhost (primary)`.
|
||||
2. **Notification Pipeline Verification (`scripts/nut_notify.sh`):**
|
||||
- Verified automated dispatch to Gotify VIP (`http://140.44.4.70:8090/message`) with token `A4uN4fEmErGeNcYtOkEn` (Application 1: `UPS & Power Resilience`, priority 9/10).
|
||||
- Verified automatic streaming to Discord channel `#ups-power` via WebSocket bridge, with direct UDM-Pro egress fallback if Gotify/Swarm is stopped.
|
||||
3. **Graceful 6-Tier Shutdown Automation (`scripts/homelab_power_shutdown.sh`):**
|
||||
- Tier 1: Stops transient AI inference Pis (`140.44.4.7`, `140.44.4.8`).
|
||||
- Tier 2: Scales down Swarm NFS services (`nginx-proxy-manager_app`, `backup`, `peanut`, `homebox`) on `140.44.4.71` to cleanly flush databases and release NFS file locks.
|
||||
- Tier 3: Stops K3s agents on all 7 Aerodrome workers (`140.44.4.21-27`) and 6 Spaceport workers (`140.44.4.51-56`), cleanly syncing and detaching Longhorn CSI persistent volumes.
|
||||
- Tier 4: Signals Synology NAS arrays (`14.10.10.71`, `14.10.10.72`) into DSM Safe Mode via native NUT slave daemon (`synopoweroff`).
|
||||
- Tier 5: Saves on-demand pre-shutdown etcd snapshots on Spaceport masters (`.41-.44`) and Aerodrome masters (`.11-.13`), stops K3s service, and halts master nodes and Swarm managers (`.71-.75`).
|
||||
- Tier 6: NUT Master fires final FSD notification, arms `/etc/killpower`, and executes `/sbin/shutdown -h +0`.
|
||||
|
||||
#### 6.1 Core Power Architecture & IP Assignments
|
||||
|
||||
@@ -378,7 +399,7 @@ The power resilience architecture uses a centralized **NUT Master Server** (`14.
|
||||
| Role | Hostname / Identifier | Target IP | Subnet / VLAN | Description & Failure Protocol |
|
||||
|---|---|---|---|---|
|
||||
| **NUT Master Server** | `nutserver.snarfnet.net` | `14.4.4.44` | VLAN 50 (Smart Home) | Primary NUT `upsd` daemon polling physical UPS via USB/Serial/SNMP. Runs `upsmon master` and holds `/root/.ssh/id_ed25519_snarfnet`. |
|
||||
| **Edge Ingress / Reverse Proxy** | `nginx.snarfnet.net` | `140.44.4.74` | VLAN 20 (Swarm 04) | NPM running in `PublishMode: host` on ports 80/443/81. Dependent on Synology NFS; stopped in Tier 2 prior to NAS Safe Mode. |
|
||||
| **Edge Ingress / Reverse Proxy** | `nginx.snarfnet.net` | `140.44.4.70` | VLAN 20 (Keepalived VIP) | Dual-NPM active/standby running in `PublishMode: host` on ports 80/443/81 across Swarm 01 & 02. Dependent on Synology NFS; stopped in Tier 2 prior to NAS Safe Mode. |
|
||||
| **Authentication & SSO** | `authentik.snarfnet.net` | `140.44.4.29:444` | VLAN 20 (Aerodrome K3s) | Handles Forward Auth & Conditional MFA. Runs on Longhorn storage in Aerodrome. |
|
||||
| **UPS Web Dashboard** | `peanut.snarfnet.net` | `140.44.4.70:8050` | VLAN 20 (Swarm VIP) | Peanut container UI consuming `14.4.4.44:3493`. Protected by Authentik Forward Auth externally. |
|
||||
| **Emergency Push & Discord** | `gotify.snarfnet.net` | `140.44.4.70:8090` | VLAN 20 (Swarm VIP) | Gotify server + `gotify-discord-bridge`. **Internal outage alerts use `http://140.44.4.70:8090`** (+ direct Discord webhook fallback) to bypass Authentik during shutdown. |
|
||||
@@ -445,7 +466,7 @@ The power resilience architecture uses a centralized **NUT Master Server** (`14.
|
||||
3. **Stage 3 (NUT Master):** `14.4.4.44` boots (`BIOS: Restore on AC Power Loss = Always On`), starts `upsd`, and sends `ONLINE` alert to Discord/Gotify.
|
||||
4. **Stage 4 (Storage Gate — Must Complete Before Compute):** Synology NAS arrays (`14.10.10.71` & `14.10.10.72`) boot, mount Btrfs pools, and expose NFS (`:2049`).
|
||||
5. **Stage 5 (Control Planes & Hypervisors):** Harvester (`140.44.4.81-83`), K3s Masters (`aerodrome` & `spaceport`), and Swarm Managers (`140.44.4.71-75`) boot and establish etcd/Raft quorum.
|
||||
6. **Stage 6 (Compute Workers & Ingress):** K3s workers boot, Longhorn reattaches PVCs (`data-authentik-postgresql-0`, `awx-postgres`, `zabbix-postgresql`), and NPM on `snshpd4lnxswm04` (`140.44.4.74`) binds host ports `80/443/81`.
|
||||
6. **Stage 6 (Compute Workers & Ingress):** K3s workers boot, Longhorn reattaches PVCs (`data-authentik-postgresql-0`, `awx-postgres`, `zabbix-postgresql`), and Dual-NPM on Swarm 01 & 02 (`140.44.4.71-72`) binds host ports `80/443/81` behind Keepalived VIP `140.44.4.70`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
1
homelab-gitops-repo
Submodule
1
homelab-gitops-repo
Submodule
Submodule homelab-gitops-repo added at cb8e60b086
53
scripts/audit_and_pin_vips.py
Normal file
53
scripts/audit_and_pin_vips.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import paramiko
|
||||
import json
|
||||
|
||||
TARGET_VIPS = {
|
||||
("monitoring", "uptime-lb"): "140.44.4.28",
|
||||
("infra", "authentik-lb"): "140.44.4.29",
|
||||
("awx", "awx-lb"): "140.44.4.30",
|
||||
("infra", "technitium-npm-vip-service"): "140.44.4.31",
|
||||
("default", "grafana-lb"): "140.44.4.32",
|
||||
("infra", "unified-npm-vip-service"): "140.44.4.33",
|
||||
("kube-system", "traefik"): "140.44.4.34",
|
||||
("infra", "ak-outpost-authentik-ldap-outpost"): "140.44.4.35",
|
||||
("longhorn-system", "longhorn-frontend-lb"): "140.44.4.36",
|
||||
}
|
||||
|
||||
def main():
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.11', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
print("=== PINNING AERODROME LOADBALANCER VIPS ===")
|
||||
for (ns, name), target_ip in TARGET_VIPS.items():
|
||||
patch = json.dumps({
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"metallb.universe.tf/loadBalancerIP": target_ip
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"loadBalancerIP": target_ip
|
||||
}
|
||||
})
|
||||
|
||||
cmd = f"kubectl patch svc {name} -n {ns} --type=merge -p '{patch}'"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
out = stdout.read().decode().strip()
|
||||
err = stderr.read().decode().strip()
|
||||
print(f"[{ns}/{name}] -> {target_ip}: {out} {err}")
|
||||
|
||||
print("\n=== VERIFYING FINAL ALLOCATION STATE ===")
|
||||
for (ns, name), target_ip in TARGET_VIPS.items():
|
||||
stdin, stdout, stderr = ssh.exec_command(f"kubectl get svc {name} -n {ns} -o json")
|
||||
svc = json.loads(stdout.read().decode())
|
||||
current_ingress = [x.get('ip') for x in svc.get('status', {}).get('loadBalancer', {}).get('ingress', [])]
|
||||
spec_ip = svc.get('spec', {}).get('loadBalancerIP')
|
||||
ann_ip = svc.get('metadata', {}).get('annotations', {}).get('metallb.universe.tf/loadBalancerIP')
|
||||
status = "LOCKED" if (spec_ip == target_ip and ann_ip == target_ip and target_ip in current_ingress) else "MISMATCH"
|
||||
print(f"[{status}] {ns}/{name}: Ingress={current_ingress}, Spec={spec_ip}, Annotation={ann_ip}")
|
||||
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
13
scripts/check_13_boot.py
Normal file
13
scripts/check_13_boot.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import paramiko, sys
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
try:
|
||||
ssh.connect('140.44.4.13', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
stdin, stdout, stderr = ssh.exec_command('hostname; uptime; k3s --version')
|
||||
print("SSH SUCCESS TO .13:\n", stdout.read().decode())
|
||||
ssh.close()
|
||||
except Exception as e:
|
||||
print(f"SSH Failed: {e}")
|
||||
|
||||
29
scripts/check_aerodrome_vip_details.py
Normal file
29
scripts/check_aerodrome_vip_details.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import paramiko
|
||||
import json
|
||||
|
||||
def main():
|
||||
s = paramiko.SSHClient()
|
||||
s.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
s.connect('140.44.4.11', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet')
|
||||
|
||||
_, o, _ = s.exec_command('kubectl get svc -A -o json')
|
||||
data = json.loads(o.read().decode())
|
||||
print("=== AERODROME LOADBALANCER SERVICES ===")
|
||||
for item in data.get('items', []):
|
||||
if item.get('spec', {}).get('type') == 'LoadBalancer':
|
||||
name = item['metadata']['name']
|
||||
ns = item['metadata']['namespace']
|
||||
lb_ip = item.get('spec', {}).get('loadBalancerIP')
|
||||
ing = [x.get('ip') for x in item.get('status', {}).get('loadBalancer', {}).get('ingress', [])]
|
||||
annotations = item['metadata'].get('annotations', {})
|
||||
print(f"- {ns}/{name}: status_ingress={ing}, spec_loadBalancerIP={lb_ip}, metallb_annotations={list(annotations.keys())}")
|
||||
|
||||
_, o, _ = s.exec_command('kubectl get ipaddresspools.metallb.io -A -o yaml 2>/dev/null || kubectl get cm -n metallb-system config -o yaml 2>/dev/null || true')
|
||||
print("\n=== METALLB CONFIG ===")
|
||||
print(o.read().decode())
|
||||
|
||||
s.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
22
scripts/check_argocd_status.py
Normal file
22
scripts/check_argocd_status.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import paramiko
|
||||
|
||||
def main():
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.41', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
cmd = "kubectl get pod -n infrastructure -l app.kubernetes.io/name=argocd-server -o jsonpath='{.items[0].spec.containers[*].name}'"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
container_name = stdout.read().decode().strip()
|
||||
print("Container name:", container_name)
|
||||
|
||||
# Check cluster list via argocd CLI inside the server pod
|
||||
# In argocd pod, the binary 'argocd' is usually available or we can check via curl localhost:8080/api/v1/clusters
|
||||
stdin, stdout, stderr = ssh.exec_command(f"kubectl exec -n infrastructure deployment/argocd-server -c {container_name} -- curl -s http://localhost:8080/api/v1/clusters")
|
||||
print("API clusters response:", stdout.read().decode())
|
||||
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
30
scripts/check_db_creds.py
Normal file
30
scripts/check_db_creds.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import paramiko
|
||||
import sys
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.11', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=8)
|
||||
|
||||
print("--- Testing authentik dump ---")
|
||||
cmd = "kubectl exec -n infra authentik-postgresql-0 -- env"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
print(stdout.read().decode())
|
||||
ssh.close()
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.41', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=8)
|
||||
|
||||
print("--- Testing gitea env ---")
|
||||
cmd = "kubectl exec -n infrastructure gitea-postgresql-0 -- env"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
print(stdout.read().decode())
|
||||
|
||||
print("--- Testing zabbix env ---")
|
||||
cmd = "kubectl exec -n monitoring zabbix-postgresql-0 -- env"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
print(stdout.read().decode())
|
||||
ssh.close()
|
||||
|
||||
20
scripts/check_gotify_gitops.py
Normal file
20
scripts/check_gotify_gitops.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import sys
|
||||
import urllib.request
|
||||
import json
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
def main():
|
||||
req = urllib.request.Request('http://140.44.4.70:8090/message', headers={'X-Gotify-Key': 'C1dIsCoRdBrIdGeClIeN'})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
data = json.loads(resp.read().decode())
|
||||
messages = data.get('messages', [])
|
||||
print(f"TOTAL MESSAGES IN GOTIFY: {len(messages)}")
|
||||
for m in messages[:8]:
|
||||
print(f"- [App {m.get('appid')}] {m.get('title')}: {m.get('message')[:80]}")
|
||||
except Exception as e:
|
||||
print("Error:", e)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
21
scripts/check_host_ports.py
Normal file
21
scripts/check_host_ports.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import sys
|
||||
import paramiko
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
def main():
|
||||
for ip, node in [('140.44.4.71', 'snshpd4lnxswm01'), ('140.44.4.72', 'snshpd4lnxswm02')]:
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(ip, username='snarf', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
print(f"=== {node} ({ip}) ===")
|
||||
stdin, stdout, stderr = ssh.exec_command('sudo ss -tulpn | grep -E ":80 |:81 |:443 " || true')
|
||||
print("SS:\n", stdout.read().decode())
|
||||
stdin, stdout, stderr = ssh.exec_command('sudo docker ps -a --filter "name=nginx" --format "{{.ID}} {{.Status}} {{.Names}}"')
|
||||
print("Containers:\n", stdout.read().decode())
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
21
scripts/check_job_115.py
Normal file
21
scripts/check_job_115.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import paramiko
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.11', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
py_code = """
|
||||
from awx.main.models import Job
|
||||
job = Job.objects.get(id=115)
|
||||
print(f"Job [115] for '{job.name}': Status={job.status}, Elapsed={job.elapsed}s, Failed={job.failed}")
|
||||
events = job.job_events.order_by('id')
|
||||
for e in events:
|
||||
if e.stdout:
|
||||
print(e.stdout)
|
||||
"""
|
||||
|
||||
cmd = f"kubectl exec -i -n awx -c awx-task deployment/awx-task -- awx-manage shell_plus --quiet << 'EOF'\n{py_code}\nEOF"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
print(stdout.read().decode())
|
||||
ssh.close()
|
||||
|
||||
21
scripts/check_job_121.py
Normal file
21
scripts/check_job_121.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import paramiko
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.11', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
py_code = """
|
||||
from awx.main.models import Job
|
||||
job = Job.objects.get(id=121)
|
||||
print(f"Job [121] for '{job.name}': Status={job.status}, Elapsed={job.elapsed}s, Failed={job.failed}")
|
||||
events = job.job_events.order_by('id')
|
||||
for e in events:
|
||||
if e.stdout:
|
||||
print(e.stdout)
|
||||
"""
|
||||
|
||||
cmd = f"kubectl exec -i -n awx -c awx-task deployment/awx-task -- awx-manage shell_plus --quiet << 'EOF'\n{py_code}\nEOF"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
print(stdout.read().decode())
|
||||
ssh.close()
|
||||
|
||||
21
scripts/check_job_124.py
Normal file
21
scripts/check_job_124.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import paramiko
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.12', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
py_code = """
|
||||
from awx.main.models import Job
|
||||
job = Job.objects.get(id=124)
|
||||
print(f"Job [124] for '{job.name}': Status={job.status}, Elapsed={job.elapsed}s, Failed={job.failed}")
|
||||
events = job.job_events.order_by('id')
|
||||
for e in events:
|
||||
if e.stdout:
|
||||
print(e.stdout)
|
||||
"""
|
||||
|
||||
cmd = f"kubectl exec -i -n awx -c awx-task deployment/awx-task -- awx-manage shell_plus --quiet << 'EOF'\n{py_code}\nEOF"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
print(stdout.read().decode())
|
||||
ssh.close()
|
||||
|
||||
21
scripts/check_job_127.py
Normal file
21
scripts/check_job_127.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import paramiko
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.12', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
py_code = """
|
||||
from awx.main.models import Job
|
||||
job = Job.objects.get(id=127)
|
||||
print(f"Job [127] for '{job.name}': Status={job.status}, Elapsed={job.elapsed}s, Failed={job.failed}")
|
||||
events = job.job_events.order_by('id')
|
||||
for e in events:
|
||||
if e.stdout:
|
||||
print(e.stdout)
|
||||
"""
|
||||
|
||||
cmd = f"kubectl exec -i -n awx -c awx-task deployment/awx-task -- awx-manage shell_plus --quiet << 'EOF'\n{py_code}\nEOF"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
print(stdout.read().decode())
|
||||
ssh.close()
|
||||
|
||||
35
scripts/check_keepalived_all.py
Normal file
35
scripts/check_keepalived_all.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import paramiko
|
||||
|
||||
NODES = [
|
||||
("swm01", "140.44.4.71"),
|
||||
("swm02", "140.44.4.72"),
|
||||
("swm03", "140.44.4.73"),
|
||||
("swm04", "140.44.4.74"),
|
||||
("swm05", "140.44.4.75"),
|
||||
]
|
||||
|
||||
def main():
|
||||
for name, ip in NODES:
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
try:
|
||||
ssh.connect(ip, username='snarf', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=5)
|
||||
stdin, stdout, stderr = ssh.exec_command("systemctl is-active keepalived 2>/dev/null || echo inactive")
|
||||
status = stdout.read().decode().strip()
|
||||
|
||||
stdin, stdout, stderr = ssh.exec_command("ip -4 addr show | grep '140.44.4.70' || true")
|
||||
vip = stdout.read().decode().strip()
|
||||
|
||||
stdin, stdout, stderr = ssh.exec_command("grep -E 'state|priority' /etc/keepalived/keepalived.conf 2>/dev/null || true")
|
||||
conf = stdout.read().decode().strip()
|
||||
|
||||
print(f"[{name} - {ip}] Keepalived: {status} | VIP: {'YES' if '140.44.4.70' in vip else 'NO'}")
|
||||
if conf:
|
||||
print(f" Config: {conf.splitlines()}")
|
||||
ssh.close()
|
||||
except Exception as e:
|
||||
print(f"[{name} - {ip}] Error:", e)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
29
scripts/check_longhorn_settings.py
Normal file
29
scripts/check_longhorn_settings.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import sys
|
||||
import paramiko
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
def main():
|
||||
for name, ip in [("aerodrome", "140.44.4.11"), ("spaceport", "140.44.4.41")]:
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(ip, username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
print(f"=== LONGHORN SETTINGS ON {name} ({ip}) ===")
|
||||
settings = [
|
||||
'auto-salvage-volume',
|
||||
'detach-manually-attached-volume-with-missing-node',
|
||||
'replica-replenishment-wait-interval',
|
||||
'pod-deletion-for-node-down-failure-handling',
|
||||
'backup-target'
|
||||
]
|
||||
for s in settings:
|
||||
cmd = f"sudo kubectl get setting.longhorn.io {s} -n longhorn-system -o jsonpath='{{.value}}' 2>/dev/null || echo 'not found'"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
val = stdout.read().decode().strip()
|
||||
print(f" {s}: {val}")
|
||||
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
14
scripts/check_nfs_mounts.py
Normal file
14
scripts/check_nfs_mounts.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import paramiko
|
||||
import sys
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
for name, ip in [('aerodrome', '140.44.4.11'), ('spaceport', '140.44.4.41')]:
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(ip, username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=8)
|
||||
cmd = "df -h | grep -i nfs || echo 'no nfs mounts'"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
print(f"[{name.upper()}] NFS mounts:\n{stdout.read().decode().strip()}")
|
||||
ssh.close()
|
||||
|
||||
27
scripts/check_npm_status.py
Normal file
27
scripts/check_npm_status.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import sys
|
||||
import paramiko
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
def main():
|
||||
for ip in ['140.44.4.71', '140.44.4.72']:
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(ip, username='snarf', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
stdin, stdout, stderr = ssh.exec_command('sudo ss -tulpn | grep -E ":80 |:443 |:81 " || true')
|
||||
print(f"=== LISTENING PORTS ON {ip} ===")
|
||||
print(stdout.read().decode())
|
||||
|
||||
stdin, stdout, stderr = ssh.exec_command('sudo docker ps --filter "name=nginx-proxy-manager_app" --format "{{.ID}} {{.Status}}"')
|
||||
cid = stdout.read().decode().strip()
|
||||
print(f"Container on {ip}: {cid}")
|
||||
if cid:
|
||||
cid_clean = cid.split()[0]
|
||||
stdin, stdout, stderr = ssh.exec_command(f'sudo docker logs {cid_clean} --tail 15')
|
||||
print(f"Logs on {ip}:\n{stdout.read().decode()}")
|
||||
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
16
scripts/check_npm_volumes.py
Normal file
16
scripts/check_npm_volumes.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import paramiko
|
||||
|
||||
def main():
|
||||
for ip in ['140.44.4.71', '140.44.4.72']:
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(ip, username='snarf', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
stdin, stdout, stderr = ssh.exec_command("sudo docker volume inspect nginx-proxy-manager_nginx-data --format '{{json .Options}}'")
|
||||
print(f"Volume data on {ip}: {stdout.read().decode().strip()}")
|
||||
stdin, stdout, stderr = ssh.exec_command("sudo docker volume inspect nginx-proxy-manager_nginx-letsencrypt --format '{{json .Options}}'")
|
||||
print(f"Volume letsencrypt on {ip}: {stdout.read().decode().strip()}")
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
20
scripts/check_nut_services.py
Normal file
20
scripts/check_nut_services.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import paramiko
|
||||
import sys
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
def main():
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('14.4.4.44', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
stdin, stdout, stderr = ssh.exec_command('systemctl status nut-monitor --no-pager')
|
||||
print("STATUS NUT-MONITOR:\n", stdout.read().decode('utf-8', errors='replace'))
|
||||
|
||||
stdin, stdout, stderr = ssh.exec_command('systemctl status nut-server --no-pager')
|
||||
print("STATUS NUT-SERVER:\n", stdout.read().decode('utf-8', errors='replace'))
|
||||
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
15
scripts/check_nut_users.py
Normal file
15
scripts/check_nut_users.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import paramiko
|
||||
|
||||
def main():
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('14.4.4.44', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
stdin, stdout, stderr = ssh.exec_command('grep -E "NOTIFY|SHUTDOWN" /etc/nut/upsmon.conf | grep -v "^#"')
|
||||
print("=== NOTIFY / SHUTDOWN IN UPSMON.CONF ===")
|
||||
print(stdout.read().decode())
|
||||
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
16
scripts/check_port_forwards.py
Normal file
16
scripts/check_port_forwards.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import paramiko
|
||||
|
||||
def main():
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
try:
|
||||
ssh.connect('14.44.4.1', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=5)
|
||||
stdin, stdout, stderr = ssh.exec_command('iptables -t nat -S | grep -E "80|443"')
|
||||
print("IPTABLES NAT:\n", stdout.read().decode())
|
||||
ssh.close()
|
||||
except Exception as e:
|
||||
print("SSH to UDM Pro error:", e)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
15
scripts/check_swarm_ports.py
Normal file
15
scripts/check_swarm_ports.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import paramiko
|
||||
|
||||
def main():
|
||||
for ip in ['140.44.4.71', '140.44.4.72']:
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(ip, username='snarf', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
stdin, stdout, stderr = ssh.exec_command('sudo ss -tulpn | grep -E ":80 |:443 |:81 " || true')
|
||||
print(f"=== PORTS ON {ip} ===")
|
||||
print(stdout.read().decode())
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
23
scripts/check_swarm_service_status.py
Normal file
23
scripts/check_swarm_service_status.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import sys
|
||||
import paramiko
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
def main():
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.71', username='snarf', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
print("=== DOCKER SERVICE LS ===")
|
||||
stdin, stdout, stderr = ssh.exec_command('sudo docker service ls')
|
||||
print(stdout.read().decode())
|
||||
|
||||
print("=== DOCKER SERVICE PS ===")
|
||||
stdin, stdout, stderr = ssh.exec_command('sudo docker service ps nginx-proxy-manager_app')
|
||||
print(stdout.read().decode())
|
||||
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
70
scripts/configure_argocd_notifications.py
Normal file
70
scripts/configure_argocd_notifications.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import paramiko
|
||||
import yaml
|
||||
|
||||
def main():
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.41', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
cm_manifest = """
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-notifications-cm
|
||||
namespace: infrastructure
|
||||
labels:
|
||||
app.kubernetes.io/component: notifications-controller
|
||||
app.kubernetes.io/instance: argocd
|
||||
app.kubernetes.io/name: argocd-notifications-controller
|
||||
app.kubernetes.io/part-of: argocd
|
||||
data:
|
||||
context: |
|
||||
argocdUrl: https://argo.snarfnet.net
|
||||
service.webhook.gotify: |
|
||||
url: http://140.44.4.70:8090/message?token=A8aWxGiToPsAuToMaTiO
|
||||
headers:
|
||||
- name: Content-Type
|
||||
value: application/json
|
||||
template.app-sync-status: |
|
||||
webhook:
|
||||
gotify:
|
||||
method: POST
|
||||
body: |
|
||||
{
|
||||
"title": "ArgoCD: {{.app.metadata.name}} ({{.app.status.sync.status}})",
|
||||
"message": "Application: {{.app.metadata.name}}\\nCluster: {{.app.spec.destination.server}}\\nSync Status: {{.app.status.sync.status}}\\nHealth: {{.app.status.health.status}}\\nURL: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
|
||||
"priority": 4
|
||||
}
|
||||
template.app-health-degraded: |
|
||||
webhook:
|
||||
gotify:
|
||||
method: POST
|
||||
body: |
|
||||
{
|
||||
"title": "ArgoCD: {{.app.metadata.name}} Health DEGRADED",
|
||||
"message": "Application: {{.app.metadata.name}}\\nCluster: {{.app.spec.destination.server}}\\nHealth: {{.app.status.health.status}}\\nURL: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
|
||||
"priority": 8
|
||||
}
|
||||
trigger.on-sync-failed: |
|
||||
- description: Application syncing has failed
|
||||
send: [app-sync-status]
|
||||
when: app.status.operationState != nil and app.status.operationState.phase in ['Error', 'Failed']
|
||||
trigger.on-health-degraded: |
|
||||
- description: Application health is degraded
|
||||
send: [app-health-degraded]
|
||||
when: app.status.health.status == 'Degraded'
|
||||
"""
|
||||
stdin, stdout, stderr = ssh.exec_command("kubectl apply -f -")
|
||||
stdin.write(cm_manifest)
|
||||
stdin.channel.shutdown_write()
|
||||
print("CM Apply stdout:\n", stdout.read().decode())
|
||||
print("CM Apply stderr:\n", stderr.read().decode())
|
||||
|
||||
stdin, stdout, stderr = ssh.exec_command("kubectl rollout restart deployment/argocd-notifications-controller -n infrastructure")
|
||||
print("Rollout restart:\n", stdout.read().decode())
|
||||
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
60
scripts/deploy_dual_npm.py
Normal file
60
scripts/deploy_dual_npm.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import paramiko
|
||||
import time
|
||||
import urllib.request
|
||||
import ssl
|
||||
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
|
||||
def main():
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.71', username='snarf', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
print("=== STEP 1: Updating nginx-proxy-manager_app to Dual-Node Swarm Placement ===")
|
||||
cmd = (
|
||||
"sudo docker service update "
|
||||
"--constraint-rm 'node.hostname == snshpd4lnxswm04' "
|
||||
"--constraint-add 'node.labels.npm == true' "
|
||||
"--replicas 2 "
|
||||
"--update-order start-first "
|
||||
"nginx-proxy-manager_app"
|
||||
)
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
print("Service update output:\n", stdout.read().decode())
|
||||
print("Service update stderr:\n", stderr.read().decode())
|
||||
|
||||
print("=== STEP 2: Waiting for tasks to stabilize ===")
|
||||
time.sleep(10)
|
||||
|
||||
stdin, stdout, stderr = ssh.exec_command("sudo docker service ps nginx-proxy-manager_app --no-trunc")
|
||||
print("Tasks:\n", stdout.read().decode())
|
||||
|
||||
ssh.close()
|
||||
|
||||
print("=== STEP 3: Verifying HTTP / HTTPS endpoints ===")
|
||||
for ip in ['140.44.4.70', '140.44.4.71', '140.44.4.72']:
|
||||
# Port 80
|
||||
try:
|
||||
req = urllib.request.Request(f"http://{ip}", headers={"Host": "test.snarfnet.net"})
|
||||
with urllib.request.urlopen(req, timeout=4) as r:
|
||||
print(f"[{ip}:80] HTTP {r.status}")
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"[{ip}:80] HTTP {e.code} (NPM responding)")
|
||||
except Exception as e:
|
||||
print(f"[{ip}:80] Error: {e}")
|
||||
|
||||
# Port 81 (Admin UI)
|
||||
try:
|
||||
req = urllib.request.Request(f"http://{ip}:81")
|
||||
with urllib.request.urlopen(req, timeout=4) as r:
|
||||
print(f"[{ip}:81] HTTP {r.status} (Admin UI responding)")
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"[{ip}:81] HTTP {e.code}")
|
||||
except Exception as e:
|
||||
print(f"[{ip}:81] Error: {e}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
72
scripts/deploy_nut_config.py
Normal file
72
scripts/deploy_nut_config.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import paramiko
|
||||
|
||||
def main():
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('14.4.4.44', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
# 1. Deploy /usr/local/bin/nut-notify.sh
|
||||
with open(r'scripts\nut_notify.sh', 'r', encoding='utf-8') as f:
|
||||
nut_notify_content = f.read()
|
||||
|
||||
sftp = ssh.open_sftp()
|
||||
with sftp.file('/usr/local/bin/nut-notify.sh', 'w') as f:
|
||||
f.write(nut_notify_content)
|
||||
ssh.exec_command('chmod 755 /usr/local/bin/nut-notify.sh')
|
||||
print("Deployed /usr/local/bin/nut-notify.sh")
|
||||
|
||||
# 2. Deploy /usr/local/bin/homelab_power_shutdown.sh
|
||||
with open(r'scripts\homelab_power_shutdown.sh', 'r', encoding='utf-8') as f:
|
||||
power_shutdown_content = f.read()
|
||||
|
||||
with sftp.file('/usr/local/bin/homelab_power_shutdown.sh', 'w') as f:
|
||||
f.write(power_shutdown_content)
|
||||
ssh.exec_command('chmod 755 /usr/local/bin/homelab_power_shutdown.sh')
|
||||
print("Deployed /usr/local/bin/homelab_power_shutdown.sh")
|
||||
sftp.close()
|
||||
|
||||
# 3. Update /etc/nut/upsmon.conf
|
||||
upsmon_conf = """
|
||||
MONITOR server-closet@localhost 1 admin e3e34r4rT%T%^Y^Y primary
|
||||
MINSUPPLIES 1
|
||||
SHUTDOWNCMD "/usr/local/bin/homelab_power_shutdown.sh"
|
||||
NOTIFYCMD /usr/local/bin/nut-notify.sh
|
||||
POLLFREQ 5
|
||||
POLLFREQALERT 5
|
||||
HOSTSYNC 15
|
||||
DEADTIME 15
|
||||
POWERDOWNFLAG /etc/killpower
|
||||
RBWARNTIME 43200
|
||||
NOCOMMWARNTIME 300
|
||||
FINALDELAY 180
|
||||
|
||||
NOTIFYFLAG ONLINE SYSLOG+WALL+EXEC
|
||||
NOTIFYFLAG ONBATT SYSLOG+WALL+EXEC
|
||||
NOTIFYFLAG LOWBATT SYSLOG+WALL+EXEC
|
||||
NOTIFYFLAG FSD SYSLOG+WALL+EXEC
|
||||
NOTIFYFLAG COMMOK SYSLOG+WALL+EXEC
|
||||
NOTIFYFLAG COMMBAD SYSLOG+WALL+EXEC
|
||||
NOTIFYFLAG SHUTDOWN SYSLOG+WALL+EXEC
|
||||
NOTIFYFLAG REPLBATT SYSLOG+WALL
|
||||
NOTIFYFLAG NOCOMM SYSLOG+WALL
|
||||
"""
|
||||
# Backup existing upsmon.conf
|
||||
ssh.exec_command('cp /etc/nut/upsmon.conf /etc/nut/upsmon.conf.bak')
|
||||
|
||||
stdin, stdout, stderr = ssh.exec_command('cat > /etc/nut/upsmon.conf')
|
||||
stdin.write(upsmon_conf.strip() + "\n")
|
||||
stdin.channel.shutdown_write()
|
||||
print("Updated /etc/nut/upsmon.conf")
|
||||
|
||||
# Restart nut-client / nut-monitor
|
||||
stdin, stdout, stderr = ssh.exec_command('systemctl restart nut-client || systemctl restart nut-monitor')
|
||||
print("Restart service:", stdout.read().decode(), stderr.read().decode())
|
||||
|
||||
# Check status
|
||||
stdin, stdout, stderr = ssh.exec_command('systemctl is-active nut-client || systemctl is-active nut-monitor')
|
||||
print("Service is-active:", stdout.read().decode().strip())
|
||||
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
65
scripts/dump_k8s_databases.py
Normal file
65
scripts/dump_k8s_databases.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import paramiko
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
db_targets = [
|
||||
{
|
||||
"cluster": "aerodrome",
|
||||
"ip": "140.44.4.11",
|
||||
"namespace": "infra",
|
||||
"pod": "authentik-postgresql-0",
|
||||
"name": "authentik",
|
||||
"dump_cmd": "kubectl exec -n infra authentik-postgresql-0 -- pg_dumpall -U postgres | gzip > /var/backups/authentik_db_{timestamp}.sql.gz"
|
||||
},
|
||||
{
|
||||
"cluster": "aerodrome",
|
||||
"ip": "140.44.4.11",
|
||||
"namespace": "awx",
|
||||
"pod": "awx-postgres-15-0",
|
||||
"name": "awx",
|
||||
"dump_cmd": "kubectl exec -n awx awx-postgres-15-0 -- pg_dumpall -U postgres | gzip > /var/backups/awx_db_{timestamp}.sql.gz"
|
||||
},
|
||||
{
|
||||
"cluster": "spaceport",
|
||||
"ip": "140.44.4.41",
|
||||
"namespace": "infrastructure",
|
||||
"pod": "gitea-postgresql-0",
|
||||
"name": "gitea",
|
||||
"dump_cmd": "kubectl exec -n infrastructure gitea-postgresql-0 -- pg_dumpall -U postgres | gzip > /var/backups/gitea_db_{timestamp}.sql.gz"
|
||||
},
|
||||
{
|
||||
"cluster": "spaceport",
|
||||
"ip": "140.44.4.41",
|
||||
"namespace": "monitoring",
|
||||
"pod": "zabbix-postgresql-0",
|
||||
"name": "zabbix",
|
||||
"dump_cmd": "kubectl exec -n monitoring zabbix-postgresql-0 -- pg_dumpall -U postgres | gzip > /var/backups/zabbix_db_{timestamp}.sql.gz"
|
||||
}
|
||||
]
|
||||
|
||||
for target in db_targets:
|
||||
cluster = target["cluster"]
|
||||
ip = target["ip"]
|
||||
name = target["name"]
|
||||
print(f"[{cluster.upper()}] Backing up {name} database...")
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(ip, username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=15)
|
||||
|
||||
cmd = f"mkdir -p /var/backups && {target['dump_cmd'].replace('{timestamp}', timestamp)}"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd, timeout=300)
|
||||
exit_status = stdout.channel.recv_exit_status()
|
||||
err = stderr.read().decode().strip()
|
||||
|
||||
if exit_status == 0:
|
||||
check_cmd = f"ls -lh /var/backups/{name}_db_{timestamp}.sql.gz"
|
||||
_, chk_out, _ = ssh.exec_command(check_cmd)
|
||||
print(f" SUCCESS: {chk_out.read().decode().strip()}")
|
||||
else:
|
||||
print(f" FAILED (exit {exit_status}): {err}")
|
||||
|
||||
ssh.close()
|
||||
@@ -4,37 +4,34 @@
|
||||
set -uo pipefail
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [POWER-SHUTDOWN] $*" | tee -a /var/log/power-shutdown.log; }
|
||||
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=3 -o BatchMode=yes"
|
||||
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=4 -o BatchMode=yes -i /root/.ssh/id_ed25519_snarfnet"
|
||||
|
||||
log "=== INITIATING HOMELAB TIERED SHUTDOWN ==="
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# TIER 1: Non-Essential Compute & AI Pis
|
||||
# ------------------------------------------------------------------
|
||||
log "Stage 1: Halting AI Raspberry Pi stack (140.44.4.7, 140.44.4.8)..."
|
||||
log "Tier 1: Halting AI Raspberry Pi stack (140.44.4.7, 140.44.4.8)..."
|
||||
ssh $SSH_OPTS root@140.44.4.7 "shutdown -h now" 2>/dev/null || true
|
||||
ssh $SSH_OPTS root@140.44.4.8 "shutdown -h now" 2>/dev/null || true
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# TIER 2: Docker Swarm NFS Workloads (NPM & Peanut)
|
||||
# TIER 2: Docker Swarm NFS Workloads (NPM, Peanut, Homebox)
|
||||
# ------------------------------------------------------------------
|
||||
log "Stage 2: Stopping Swarm services mounting Synology NFS (NPM, Peanut)..."
|
||||
ssh $SSH_OPTS root@140.44.4.71 "docker service scale nginx-proxy-manager_app=0 nginx-proxy-manager_backup=0 peanut_peanut=0" 2>/dev/null || true
|
||||
log "Tier 2: Stopping Swarm services mounting Synology NFS..."
|
||||
ssh $SSH_OPTS root@140.44.4.71 "docker service scale nginx-proxy-manager_app=0 nginx-proxy-manager_backup=0 peanut_peanut=0 homebox_homebox=0" 2>/dev/null || true
|
||||
sleep 5
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# TIER 3: Harvester Guest VMs & Kubernetes Worker Nodes
|
||||
# TIER 3: Kubernetes Worker Nodes (Aerodrome & Spaceport)
|
||||
# ------------------------------------------------------------------
|
||||
log "Stage 3: Shutting down Harvester guest VMs and K3s workers..."
|
||||
# Gracefully signal Harvester VMs
|
||||
ssh $SSH_OPTS root@140.44.4.81 "virsh list --name | xargs -r -n1 virsh shutdown" 2>/dev/null || true
|
||||
|
||||
# Stop K3s agent on Aerodrome workers (clean Longhorn PVC detach)
|
||||
for ip in 140.44.4.22 140.44.4.23 140.44.4.24 140.44.4.25; do
|
||||
log "Tier 3: Gracefully stopping K3s agent and halting worker nodes..."
|
||||
# Aerodrome Workers (wrk01 - wrk07)
|
||||
for ip in 140.44.4.21 140.44.4.22 140.44.4.23 140.44.4.24 140.44.4.25 140.44.4.26 140.44.4.27; do
|
||||
ssh $SSH_OPTS root@$ip "systemctl stop k3s-agent && sync && shutdown -h now" 2>/dev/null || true
|
||||
done
|
||||
|
||||
# Stop K3s agent on Spaceport workers
|
||||
# Spaceport Workers (wrk01 - wrk06)
|
||||
for ip in 140.44.4.51 140.44.4.52 140.44.4.53 140.44.4.54 140.44.4.55 140.44.4.56; do
|
||||
ssh $SSH_OPTS root@$ip "systemctl stop k3s-agent && sync && shutdown -h now" 2>/dev/null || true
|
||||
done
|
||||
@@ -43,39 +40,34 @@ sleep 15
|
||||
# ------------------------------------------------------------------
|
||||
# TIER 4: Storage Arrays (Synology DSM Safe Mode)
|
||||
# ------------------------------------------------------------------
|
||||
log "Stage 4: Triggering Synology NAS Safe Mode (14.10.10.71, 14.10.10.72)..."
|
||||
ssh $SSH_OPTS admin@14.10.10.71 "synopoweroff" 2>/dev/null || true
|
||||
ssh $SSH_OPTS admin@14.10.10.72 "synopoweroff" 2>/dev/null || true
|
||||
sleep 20
|
||||
# Synology NAS (14.10.10.71, 14.10.10.72) automatically enters DSM Safe Mode
|
||||
# via native NUT slave daemon on receiving LOWBATT/FSD from 14.4.4.44.
|
||||
log "Tier 4: Synology NAS Safe Mode engaged via NUT slave protocol."
|
||||
sleep 10
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# TIER 5: Control Planes & Swarm Managers
|
||||
# TIER 5: Kubernetes Control Planes & Swarm Managers
|
||||
# ------------------------------------------------------------------
|
||||
log "Stage 5: Halting K3s control planes and Swarm managers..."
|
||||
# Spaceport Masters (140.44.4.41-44)
|
||||
log "Tier 5: Halting K3s control planes and Swarm managers..."
|
||||
# Spaceport Masters (ctl01 - ctl04)
|
||||
for ip in 140.44.4.41 140.44.4.42 140.44.4.43 140.44.4.44; do
|
||||
ssh $SSH_OPTS root@$ip "k3s etcd-snapshot save && systemctl stop k3s && shutdown -h now" 2>/dev/null || true
|
||||
ssh $SSH_OPTS root@$ip "k3s etcd-snapshot save --name pre-shutdown-snapshot 2>/dev/null || true; systemctl stop k3s && sync && shutdown -h now" 2>/dev/null || true
|
||||
done
|
||||
|
||||
# Aerodrome Masters (140.44.4.11-13)
|
||||
# Aerodrome Masters (ctl01 - ctl03)
|
||||
for ip in 140.44.4.11 140.44.4.12 140.44.4.13; do
|
||||
ssh $SSH_OPTS root@$ip "k3s etcd-snapshot save && systemctl stop k3s && shutdown -h now" 2>/dev/null || true
|
||||
ssh $SSH_OPTS root@$ip "k3s etcd-snapshot save --name pre-shutdown-snapshot 2>/dev/null || true; systemctl stop k3s && sync && shutdown -h now" 2>/dev/null || true
|
||||
done
|
||||
|
||||
# Swarm Managers (140.44.4.71, 72, 74)
|
||||
for ip in 140.44.4.71 140.44.4.72 140.44.4.74; do
|
||||
ssh $SSH_OPTS root@$ip "sync && shutdown -h now" 2>/dev/null || true
|
||||
done
|
||||
|
||||
# Harvester Baremetal Hypervisors (140.44.4.81-83)
|
||||
for ip in 140.44.4.81 140.44.4.82 140.44.4.83; do
|
||||
# Swarm Managers (swm01 - swm05)
|
||||
for ip in 140.44.4.71 140.44.4.72 140.44.4.73 140.44.4.74 140.44.4.75; do
|
||||
ssh $SSH_OPTS root@$ip "sync && shutdown -h now" 2>/dev/null || true
|
||||
done
|
||||
sleep 5
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# TIER 6: Master Node Self-Cutoff (FSD)
|
||||
# TIER 6: NUT Master Final Signal & Edge Cutoff
|
||||
# ------------------------------------------------------------------
|
||||
log "Stage 6: Executing final local halt and arming killpower..."
|
||||
log "Tier 6: Executing final local halt and arming killpower..."
|
||||
touch /etc/killpower
|
||||
/sbin/shutdown -h +0
|
||||
|
||||
|
||||
25
scripts/inspect_container_nginx.py
Normal file
25
scripts/inspect_container_nginx.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import paramiko
|
||||
|
||||
def main():
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.71', username='snarf', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
stdin, stdout, stderr = ssh.exec_command('sudo docker ps --filter "name=nginx-proxy-manager_app" -q')
|
||||
cid = stdout.read().decode().strip()
|
||||
print("Container ID:", cid)
|
||||
|
||||
if cid:
|
||||
stdin, stdout, stderr = ssh.exec_command(f'sudo docker exec {cid} nginx -t')
|
||||
print("NGINX -t STDOUT:\n", stdout.read().decode())
|
||||
print("NGINX -t STDERR:\n", stderr.read().decode())
|
||||
|
||||
stdin, stdout, stderr = ssh.exec_command(f'sudo docker exec {cid} curl -I http://127.0.0.1:80')
|
||||
print("CURL 80 INSIDE:\n", stdout.read().decode())
|
||||
print("CURL 80 STDERR:\n", stderr.read().decode())
|
||||
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
29
scripts/inspect_db_env.py
Normal file
29
scripts/inspect_db_env.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import paramiko
|
||||
import sys
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.11', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=8)
|
||||
|
||||
print("=== AERODROME DB USERS/ENV ===")
|
||||
for ns, pod in [('infra', 'authentik-postgresql-0'), ('awx', 'awx-postgres-15-0')]:
|
||||
cmd = f"kubectl get pod {pod} -n {ns} -o jsonpath='{{.spec.containers[0].env}}'"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
print(f"[{ns}/{pod}] Env: {stdout.read().decode()[:200]}")
|
||||
|
||||
ssh.close()
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.41', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=8)
|
||||
|
||||
print("=== SPACEPORT DB USERS/ENV ===")
|
||||
for ns, pod in [('infrastructure', 'gitea-postgresql-0'), ('monitoring', 'zabbix-postgresql-0')]:
|
||||
cmd = f"kubectl get pod {pod} -n {ns} -o jsonpath='{{.spec.containers[0].env}}'"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
print(f"[{ns}/{pod}] Env: {stdout.read().decode()[:200]}")
|
||||
|
||||
ssh.close()
|
||||
|
||||
19
scripts/inspect_db_pods.py
Normal file
19
scripts/inspect_db_pods.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import paramiko
|
||||
import sys
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
for name, ip in [('aerodrome', '140.44.4.11'), ('spaceport', '140.44.4.41')]:
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(ip, username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=8)
|
||||
cmd = "kubectl get pods -A -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,STATUS:.status.phase"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
lines = stdout.read().decode().splitlines()
|
||||
print(f"=== {name.upper()} PODS ===")
|
||||
for line in lines:
|
||||
lower = line.lower()
|
||||
if any(k in lower for k in ['postgres', 'mysql', 'authentik', 'gitea', 'zabbix', 'awx', 'semaphore']):
|
||||
print(" " + line)
|
||||
ssh.close()
|
||||
|
||||
26
scripts/inspect_keepalived_conf.py
Normal file
26
scripts/inspect_keepalived_conf.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import sys
|
||||
import paramiko
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
def main():
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.71', username='snarf', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
print("=== /etc/keepalived/keepalived.conf ===")
|
||||
stdin, stdout, stderr = ssh.exec_command('sudo cat /etc/keepalived/keepalived.conf')
|
||||
print(stdout.read().decode())
|
||||
|
||||
print("=== /etc/keepalived/scripts/ ===")
|
||||
stdin, stdout, stderr = ssh.exec_command('sudo ls -la /etc/keepalived/scripts/ || true')
|
||||
print(stdout.read().decode())
|
||||
|
||||
stdin, stdout, stderr = ssh.exec_command('sudo cat /etc/keepalived/scripts/* || true')
|
||||
print(stdout.read().decode())
|
||||
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
34
scripts/inspect_npm_72.py
Normal file
34
scripts/inspect_npm_72.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import sys
|
||||
import paramiko
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
def main():
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.72', username='snarf', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
print("=== DOCKER EXEC PS INSIDE f026d1c24ef6 ===")
|
||||
stdin, stdout, stderr = ssh.exec_command('sudo docker exec f026d1c24ef6 ps aux')
|
||||
print(stdout.read().decode())
|
||||
print(stderr.read().decode())
|
||||
|
||||
print("=== REDIRECTION_HOST LS ===")
|
||||
stdin, stdout, stderr = ssh.exec_command('sudo ls -la /var/lib/docker/volumes/nginx-proxy-manager_nginx-data/_data/nginx/redirection_host || true')
|
||||
print(stdout.read().decode())
|
||||
print(stderr.read().decode())
|
||||
|
||||
print("=== RECENT DOCKER LOGS WITH TIMESTAMPS ===")
|
||||
stdin, stdout, stderr = ssh.exec_command('sudo docker logs f026d1c24ef6 --tail 30')
|
||||
print(stdout.read().decode())
|
||||
|
||||
print("=== LOCAL CURL TEST ON 140.44.4.72 ===")
|
||||
stdin, stdout, stderr = ssh.exec_command('curl -I http://127.0.0.1:80 ; curl -I http://127.0.0.1:81')
|
||||
print(stdout.read().decode())
|
||||
print(stderr.read().decode())
|
||||
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
28
scripts/inspect_nut_master.py
Normal file
28
scripts/inspect_nut_master.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import paramiko
|
||||
|
||||
def main():
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('14.4.4.44', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
print("=== UPSC METRICS ===")
|
||||
stdin, stdout, stderr = ssh.exec_command('upsc server-closet@127.0.0.1')
|
||||
print(stdout.read().decode())
|
||||
|
||||
print("=== NUT.CONF ===")
|
||||
stdin, stdout, stderr = ssh.exec_command('grep -v "^#" /etc/nut/nut.conf | grep -v "^$"')
|
||||
print(stdout.read().decode())
|
||||
|
||||
print("=== UPS.CONF ===")
|
||||
stdin, stdout, stderr = ssh.exec_command('grep -v "^#" /etc/nut/ups.conf | grep -v "^$"')
|
||||
print(stdout.read().decode())
|
||||
|
||||
print("=== UPSMON.CONF ===")
|
||||
stdin, stdout, stderr = ssh.exec_command('grep -v "^#" /etc/nut/upsmon.conf | grep -v "^$"')
|
||||
print(stdout.read().decode())
|
||||
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
25
scripts/inspect_swarm_nodes_labels.py
Normal file
25
scripts/inspect_swarm_nodes_labels.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import sys
|
||||
import paramiko
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
def main():
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.71', username='snarf', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
stdin, stdout, stderr = ssh.exec_command('sudo docker node ls --format "{{.Hostname}}"')
|
||||
nodes = stdout.read().decode().split()
|
||||
for n in nodes:
|
||||
stdin, stdout, stderr = ssh.exec_command(f'sudo docker node inspect {n} --format "{{{{.Description.Hostname}}}}: {{{{.Spec.Labels}}}}"')
|
||||
print(stdout.read().decode().strip())
|
||||
|
||||
print("\n=== DOCKER SERVICE PS ALL ===")
|
||||
stdin, stdout, stderr = ssh.exec_command('sudo docker service ps nginx-proxy-manager_app --no-trunc')
|
||||
print(stdout.read().decode())
|
||||
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
54
scripts/launch_upgrade_step.py
Normal file
54
scripts/launch_upgrade_step.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import paramiko, sys, time
|
||||
|
||||
version = sys.argv[1] if len(sys.argv) > 1 else "v1.27.16+k3s1"
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.11', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
py_code = f"""
|
||||
from awx.main.models import Project, JobTemplate, Job
|
||||
import time, json
|
||||
|
||||
# 1. Project Update
|
||||
p = Project.objects.get(name='Snarfnet Infrastructure Playbooks')
|
||||
update = p.update()
|
||||
for _ in range(15):
|
||||
update.refresh_from_db()
|
||||
if update.status in ['successful', 'failed', 'error']:
|
||||
break
|
||||
time.sleep(2)
|
||||
|
||||
print(f"Project update [{{update.id}}]: {{update.status}}")
|
||||
|
||||
# 2. Launch Job Template
|
||||
jt = JobTemplate.objects.get(name='Aerodrome K3s Control Plane Upgrade')
|
||||
job = jt.create_unified_job(extra_vars=json.dumps({{'target_k3s_version': '{version}'}}))
|
||||
job.signal_start()
|
||||
print(f"Launched Upgrade Job [{{job.id}}] targeting version '{version}': Status={{job.status}}")
|
||||
|
||||
for _ in range(120):
|
||||
job.refresh_from_db()
|
||||
if job.status in ['successful', 'failed', 'error', 'canceled']:
|
||||
break
|
||||
time.sleep(3)
|
||||
|
||||
print(f"Job [{{job.id}}] finished with Status={{job.status}}, Elapsed={{job.elapsed}}s, Failed={{job.failed}}")
|
||||
|
||||
print('\\n=== PLAY RECAP ===')
|
||||
for e in job.job_events.filter(event='playbook_on_stats'):
|
||||
print(e.stdout)
|
||||
"""
|
||||
|
||||
cmd = f"kubectl exec -i -n awx -c awx-task deployment/awx-task -- awx-manage shell_plus --quiet << 'EOF'\n{py_code}\nEOF"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
|
||||
for line in iter(stdout.readline, ""):
|
||||
print(line, end="")
|
||||
|
||||
# Check kubectl get nodes
|
||||
print("\n=== CLUSTER STATUS AFTER STEP ===")
|
||||
stdin, stdout, stderr = ssh.exec_command("kubectl get nodes -o wide")
|
||||
print(stdout.read().decode())
|
||||
|
||||
ssh.close()
|
||||
21
scripts/list_longhorn_settings.py
Normal file
21
scripts/list_longhorn_settings.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import sys
|
||||
import paramiko
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
def main():
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.11', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
stdin, stdout, stderr = ssh.exec_command('kubectl get settings.longhorn.io -n longhorn-system -o custom-columns=NAME:.metadata.name,VALUE:.value')
|
||||
lines = stdout.read().decode().splitlines()
|
||||
for l in lines:
|
||||
for kw in ['salvage', 'detach', 'replenish', 'pod', 'node-down']:
|
||||
if kw in l.lower():
|
||||
print(l)
|
||||
break
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
EVENT_TYPE="${1:-$NOTIFYTYPE}"
|
||||
UPS_NAME="${UPSNAME:-snarf-ups}"
|
||||
UPS_NAME="${UPSNAME:-server-closet}"
|
||||
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# Internal Gotify VIP endpoint (bypasses Authentik Forward Auth and NPM)
|
||||
@@ -20,11 +20,11 @@ send_gotify() {
|
||||
local priority="$3"
|
||||
local color="${4:-16711680}"
|
||||
|
||||
# Primary: Send to Gotify (which pushes to mobile devices + forwards to Discord ups-power webhook)
|
||||
# Primary: Send to Gotify (which pushes to mobile devices + forwards to Discord #ups-power)
|
||||
if ! curl -sf -m 5 -X POST "${GOTIFY_INTERNAL_URL}?token=${GOTIFY_APP_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"title\": \"${title}\", \"message\": \"${message}\", \"priority\": ${priority}}" >/dev/null 2>&1; then
|
||||
# Fallback: If Gotify/Shipyard is already powered down (Tier 4+), send directly to Discord via UDM Pro (Tier 6)
|
||||
# Fallback: If Gotify/Shipyard is already powered down (Tier 4+), send directly to Discord via UDM Pro
|
||||
curl -sf -m 5 -X POST "${DISCORD_UPS_WEBHOOK}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"username\": \"SnarfNet NUT Master (Direct Fallback)\", \"embeds\": [{\"title\": \"${title}\", \"description\": \"${message}\", \"color\": ${color}}]}" >/dev/null 2>&1 || true
|
||||
@@ -41,15 +41,26 @@ case "$EVENT_TYPE" in
|
||||
logger -t nut-notify "AC utility power restored."
|
||||
;;
|
||||
LOWBATT)
|
||||
send_gotify "🚨 UPS Battery Critical" "[$TIMESTAMP] Battery level critical on ${UPS_NAME}! Final Tier 4-6 shutdown initiated." 10 15158332
|
||||
send_gotify "🚨 UPS Battery Critical" "[$TIMESTAMP] Battery level critical on ${UPS_NAME}! Final Tier 3-6 shutdown initiated." 10 15158332
|
||||
logger -t nut-notify "CRITICAL: UPS battery low. Enforcing emergency shutdown."
|
||||
;;
|
||||
FSD)
|
||||
send_gotify "🛑 Forced Shutdown Engaged" "[$TIMESTAMP] NUT Master has commanded immediate power cutoff across all clusters." 10 10038562
|
||||
logger -t nut-notify "FSD signal received. Halting systems."
|
||||
;;
|
||||
COMMOK)
|
||||
send_gotify "🔌 UPS Communication Restored" "[$TIMESTAMP] Communication with ${UPS_NAME} established." 4 3066993
|
||||
logger -t nut-notify "UPS communication restored."
|
||||
;;
|
||||
COMMBAD)
|
||||
send_gotify "⚠️ UPS Communication Lost" "[$TIMESTAMP] Lost communication with ${UPS_NAME}." 8 16753920
|
||||
logger -t nut-notify "UPS communication lost."
|
||||
;;
|
||||
SHUTDOWN)
|
||||
send_gotify "🛑 Homelab Emergency Powerdown" "[$TIMESTAMP] System shutdown initiated by NUT." 10 10038562
|
||||
logger -t nut-notify "System shutdown initiated."
|
||||
;;
|
||||
*)
|
||||
logger -t nut-notify "Unhandled UPS event: ${EVENT_TYPE}"
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
54
scripts/pin_spaceport_vips.py
Normal file
54
scripts/pin_spaceport_vips.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import paramiko
|
||||
import json
|
||||
|
||||
SPACEPORT_VIPS = {
|
||||
("infrastructure", "jenkins-lb"): "140.44.4.57",
|
||||
("infrastructure", "coder"): "140.44.4.58",
|
||||
("infrastructure", "homepage-lb"): "140.44.4.59",
|
||||
("monitoring", "zabbix-zabbix-web"): "140.44.4.60",
|
||||
("infrastructure", "semaphore-lb"): "140.44.4.61",
|
||||
("kube-system", "traefik"): "140.44.4.62",
|
||||
("infrastructure", "gitea-lb"): "140.44.4.63",
|
||||
("monitoring", "zabbix-zabbix-server"): "140.44.4.64",
|
||||
("infrastructure", "pihole-web"): "140.44.4.65",
|
||||
}
|
||||
|
||||
def main():
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.41', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
print("=== PINNING SPACEPORT LOADBALANCER VIPS ===")
|
||||
for (ns, name), target_ip in SPACEPORT_VIPS.items():
|
||||
patch = json.dumps({
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"metallb.universe.tf/loadBalancerIP": target_ip
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"loadBalancerIP": target_ip
|
||||
}
|
||||
})
|
||||
|
||||
cmd = f"kubectl patch svc {name} -n {ns} --type=merge -p '{patch}'"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
out = stdout.read().decode().strip()
|
||||
err = stderr.read().decode().strip()
|
||||
print(f"[{ns}/{name}] -> {target_ip}: {out} {err}")
|
||||
|
||||
print("\n=== VERIFYING FINAL ALLOCATION STATE (SPACEPORT) ===")
|
||||
for (ns, name), target_ip in SPACEPORT_VIPS.items():
|
||||
stdin, stdout, stderr = ssh.exec_command(f"kubectl get svc {name} -n {ns} -o json")
|
||||
svc = json.loads(stdout.read().decode())
|
||||
current_ingress = [x.get('ip') for x in svc.get('status', {}).get('loadBalancer', {}).get('ingress', [])]
|
||||
spec_ip = svc.get('spec', {}).get('loadBalancerIP')
|
||||
ann_ip = svc.get('metadata', {}).get('annotations', {}).get('metallb.universe.tf/loadBalancerIP')
|
||||
status = "LOCKED" if (spec_ip == target_ip and ann_ip == target_ip and target_ip in current_ingress) else "MISMATCH"
|
||||
print(f"[{status}] {ns}/{name}: Ingress={current_ingress}, Spec={spec_ip}, Annotation={ann_ip}")
|
||||
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
98
scripts/register_aerodrome_argocd.py
Normal file
98
scripts/register_aerodrome_argocd.py
Normal file
@@ -0,0 +1,98 @@
|
||||
import paramiko
|
||||
import json
|
||||
import base64
|
||||
|
||||
def main():
|
||||
# 1. Connect to Aerodrome (140.44.4.11)
|
||||
ssh_aero = paramiko.SSHClient()
|
||||
ssh_aero.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh_aero.connect('140.44.4.11', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
print("=== STEP 1: Creating argocd-manager ServiceAccount & RBAC on Aerodrome ===")
|
||||
rb_yaml = """
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: argocd-manager
|
||||
namespace: kube-system
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: argocd-manager-role-binding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: cluster-admin
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: argocd-manager
|
||||
namespace: kube-system
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: argocd-manager-token
|
||||
namespace: kube-system
|
||||
annotations:
|
||||
kubernetes.io/service-account.name: argocd-manager
|
||||
type: kubernetes.io/service-account-token
|
||||
"""
|
||||
stdin, stdout, stderr = ssh_aero.exec_command("kubectl apply -f -")
|
||||
stdin.write(rb_yaml)
|
||||
stdin.channel.shutdown_write()
|
||||
print("RBAC apply stdout:\n", stdout.read().decode())
|
||||
print("RBAC apply stderr:\n", stderr.read().decode())
|
||||
|
||||
# Get CA cert and Token
|
||||
stdin, stdout, stderr = ssh_aero.exec_command("kubectl get secret -n kube-system argocd-manager-token -o json")
|
||||
secret_data = json.loads(stdout.read().decode())
|
||||
token = base64.b64decode(secret_data["data"]["token"]).decode('utf-8')
|
||||
ca_data = secret_data["data"]["ca.crt"]
|
||||
print(f"Obtained Aerodrome token (len: {len(token)}) and CA cert.")
|
||||
ssh_aero.close()
|
||||
|
||||
# 2. Connect to Spaceport (140.44.4.41) and register cluster in ArgoCD
|
||||
ssh_sp = paramiko.SSHClient()
|
||||
ssh_sp.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh_sp.connect('140.44.4.41', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
print("=== STEP 2: Creating cluster secret in Spaceport (namespace infrastructure) ===")
|
||||
config_json = json.dumps({
|
||||
"bearerToken": token,
|
||||
"tlsClientConfig": {
|
||||
"insecure": False,
|
||||
"caData": ca_data
|
||||
}
|
||||
})
|
||||
|
||||
cluster_secret_yaml = f"""
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: cluster-aerodrome
|
||||
namespace: infrastructure
|
||||
labels:
|
||||
argocd.argoproj.io/secret-type: cluster
|
||||
type: Opaque
|
||||
stringData:
|
||||
name: aerodrome
|
||||
server: https://140.44.4.11:6443
|
||||
config: |
|
||||
{config_json}
|
||||
"""
|
||||
stdin, stdout, stderr = ssh_sp.exec_command("kubectl apply -f -")
|
||||
stdin.write(cluster_secret_yaml)
|
||||
stdin.channel.shutdown_write()
|
||||
print("Cluster Secret apply stdout:\n", stdout.read().decode())
|
||||
print("Cluster Secret apply stderr:\n", stderr.read().decode())
|
||||
|
||||
# Verify cluster list in argocd
|
||||
stdin, stdout, stderr = ssh_sp.exec_command("kubectl get secrets -n infrastructure -l argocd.argoproj.io/secret-type=cluster")
|
||||
print("Cluster Secrets:\n", stdout.read().decode())
|
||||
|
||||
ssh_sp.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
42
scripts/register_upgrade_jt.py
Normal file
42
scripts/register_upgrade_jt.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import paramiko
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.11', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
py_code = """
|
||||
from awx.main.models import Project, Inventory, Credential, JobTemplate
|
||||
|
||||
# 1. Update Project
|
||||
p = Project.objects.get(name='Snarfnet Infrastructure Playbooks')
|
||||
update = p.update()
|
||||
print(f"Triggered Project update [{update.id}]")
|
||||
|
||||
inv = Inventory.objects.get(name='Snarfnet Master Inventory')
|
||||
cred_root = Credential.objects.get(name='Snarfnet Master SSH Key (root)')
|
||||
|
||||
# 2. Register Job Template
|
||||
jt, created = JobTemplate.objects.get_or_create(
|
||||
name='Aerodrome K3s Control Plane Upgrade',
|
||||
defaults={
|
||||
'project': p,
|
||||
'inventory': inv,
|
||||
'playbook': 'ansible/playbooks/upgrade_k3s_aerodrome.yml',
|
||||
'ask_variables_on_launch': True,
|
||||
'description': 'Sequential minor release upgrade of Aerodrome K3s control plane with etcd quorum check'
|
||||
}
|
||||
)
|
||||
if not created:
|
||||
jt.playbook = 'ansible/playbooks/upgrade_k3s_aerodrome.yml'
|
||||
jt.ask_variables_on_launch = True
|
||||
jt.save()
|
||||
|
||||
jt.credentials.add(cred_root)
|
||||
print(f"Job Template '{jt.name}' (id={jt.id}, created={created}) registered.")
|
||||
"""
|
||||
|
||||
cmd = f"kubectl exec -i -n awx -c awx-task deployment/awx-task -- awx-manage shell_plus --quiet << 'EOF'\n{py_code}\nEOF"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
print(stdout.read().decode())
|
||||
ssh.close()
|
||||
|
||||
30
scripts/rescale_npm.py
Normal file
30
scripts/rescale_npm.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import sys
|
||||
import time
|
||||
import paramiko
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
def main():
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.71', username='snarf', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
print("=== SCALING TO 1 ===")
|
||||
stdin, stdout, stderr = ssh.exec_command('sudo docker service scale nginx-proxy-manager_app=1')
|
||||
print(stdout.read().decode())
|
||||
time.sleep(3)
|
||||
|
||||
print("=== SCALING TO 2 ===")
|
||||
stdin, stdout, stderr = ssh.exec_command('sudo docker service scale nginx-proxy-manager_app=2')
|
||||
print(stdout.read().decode())
|
||||
time.sleep(5)
|
||||
|
||||
print("=== SERVICE PS ===")
|
||||
stdin, stdout, stderr = ssh.exec_command('sudo docker service ps nginx-proxy-manager_app --filter desired-state=running')
|
||||
print(stdout.read().decode())
|
||||
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
45
scripts/run_aerodrome_precheck.py
Normal file
45
scripts/run_aerodrome_precheck.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import paramiko, time
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.11', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
py_code = """
|
||||
from awx.main.models import Project, JobTemplate, Job
|
||||
import time
|
||||
|
||||
p = Project.objects.get(name='Snarfnet Infrastructure Playbooks')
|
||||
update = p.update()
|
||||
for _ in range(15):
|
||||
update.refresh_from_db()
|
||||
if update.status in ['successful', 'failed', 'error']:
|
||||
break
|
||||
time.sleep(2)
|
||||
|
||||
print(f"Project update [{update.id}]: {update.status}")
|
||||
|
||||
jt = JobTemplate.objects.get(name='Aerodrome K3s Pre-Upgrade Check')
|
||||
job = jt.create_unified_job()
|
||||
job.signal_start()
|
||||
print(f"Launched Job [{job.id}] for '{jt.name}': Status={job.status}")
|
||||
|
||||
for _ in range(30):
|
||||
job.refresh_from_db()
|
||||
if job.status in ['successful', 'failed', 'error', 'canceled']:
|
||||
break
|
||||
time.sleep(2)
|
||||
|
||||
print(f"Job [{job.id}] finished with Status={job.status}, Elapsed={job.elapsed}s, Failed={job.failed}")
|
||||
|
||||
print('\\n=== PLAY RECAP ===')
|
||||
for e in job.job_events.filter(event='playbook_on_stats'):
|
||||
print(e.stdout)
|
||||
"""
|
||||
|
||||
cmd = f"kubectl exec -i -n awx -c awx-task deployment/awx-task -- awx-manage shell_plus --quiet << 'EOF'\n{py_code}\nEOF"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
|
||||
for line in iter(stdout.readline, ""):
|
||||
print(line, end="")
|
||||
|
||||
ssh.close()
|
||||
24
scripts/setup_dual_npm_cluster.py
Normal file
24
scripts/setup_dual_npm_cluster.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import paramiko
|
||||
import json
|
||||
import time
|
||||
|
||||
def main():
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.71', username='snarf', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
print("=== STEP 1: Add node labels 'npm=true' to swm01 (.71) and swm02 (.72) ===")
|
||||
stdin, stdout, stderr = ssh.exec_command('sudo docker node update --label-add npm=true snshpd4lnxswm01 && sudo docker node update --label-add npm=true snshpd4lnxswm02')
|
||||
print("Labels update:", stdout.read().decode().strip(), stderr.read().decode().strip())
|
||||
|
||||
# Check node labels
|
||||
stdin, stdout, stderr = ssh.exec_command("sudo docker node inspect snshpd4lnxswm01 --format '{{json .Spec.Labels}}'")
|
||||
print("Labels swm01:", stdout.read().decode().strip())
|
||||
stdin, stdout, stderr = ssh.exec_command("sudo docker node inspect snshpd4lnxswm02 --format '{{json .Spec.Labels}}'")
|
||||
print("Labels swm02:", stdout.read().decode().strip())
|
||||
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
42
scripts/setup_gitops_repo.py
Normal file
42
scripts/setup_gitops_repo.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import paramiko
|
||||
import json
|
||||
import sys
|
||||
|
||||
def main():
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.41', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
# Check if repo already exists
|
||||
stdin, stdout, stderr = ssh.exec_command('kubectl exec -n infrastructure deployment/gitea -c gitea -- ls -la /data/git/gitea-repositories/snarf')
|
||||
snarf_repos = stdout.read().decode()
|
||||
if 'homelab-gitops.git' in snarf_repos:
|
||||
print("homelab-gitops.git already exists on Gitea!")
|
||||
else:
|
||||
print("Generating Gitea API token for Snarf...")
|
||||
stdin, stdout, stderr = ssh.exec_command("kubectl exec -n infrastructure deployment/gitea -c gitea -- gitea admin user generate-access-token -u Snarf -t gitops-token --scopes all --raw")
|
||||
token = stdout.read().decode().strip()
|
||||
print(f"Token obtained (len: {len(token)})")
|
||||
|
||||
create_payload = json.dumps({
|
||||
"name": "homelab-gitops",
|
||||
"description": "Snarfnet Homelab GitOps, Terraform, and ArgoCD Declarations",
|
||||
"private": False,
|
||||
"auto_init": True
|
||||
})
|
||||
|
||||
# Call Gitea API inside gitea container
|
||||
curl_cmd = f"kubectl exec -i -n infrastructure deployment/gitea -c gitea -- curl -s -X POST http://localhost:3000/api/v1/user/repos -H \"Authorization: token {token}\" -H \"Content-Type: application/json\" -d @-"
|
||||
stdin, stdout, stderr = ssh.exec_command(curl_cmd)
|
||||
stdin.write(create_payload)
|
||||
stdin.channel.shutdown_write()
|
||||
resp = stdout.read().decode()
|
||||
print("API Response:", resp)
|
||||
|
||||
stdin, stdout, stderr = ssh.exec_command('kubectl exec -n infrastructure deployment/gitea -c gitea -- ls -la /data/git/gitea-repositories/snarf')
|
||||
print("Repos under snarf:\n", stdout.read().decode())
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
37
scripts/sync_backups_to_nas.py
Normal file
37
scripts/sync_backups_to_nas.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import paramiko
|
||||
import sys
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
nodes = [
|
||||
("aerodrome", "140.44.4.11", ["authentik_clean.sql.gz", "awx_clean.sql.gz"]),
|
||||
("spaceport", "140.44.4.41", ["gitea_clean.sql.gz", "zabbix_clean.sql.gz"])
|
||||
]
|
||||
|
||||
for name, ip, files in nodes:
|
||||
print(f"[{name.upper()}] Mounting NAS and copying backups...")
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(ip, username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
setup_cmd = """
|
||||
mkdir -p /mnt/nas_backup
|
||||
mount -t nfs -o vers=4 14.10.10.71:/volume1/docker /mnt/nas_backup || mount -t nfs 14.10.10.71:/volume1/docker /mnt/nas_backup
|
||||
mkdir -p /mnt/nas_backup/backups/databases
|
||||
"""
|
||||
stdin, stdout, stderr = ssh.exec_command(setup_cmd)
|
||||
stdout.channel.recv_exit_status()
|
||||
|
||||
for f in files:
|
||||
cp_cmd = f"cp /var/backups/{f} /mnt/nas_backup/backups/databases/ && ls -lh /mnt/nas_backup/backups/databases/{f}"
|
||||
stdin, stdout, stderr = ssh.exec_command(cp_cmd)
|
||||
stdout.channel.recv_exit_status()
|
||||
print(f" Saved to NAS: {stdout.read().decode().strip()}")
|
||||
|
||||
umount_cmd = "umount -f /mnt/nas_backup || true"
|
||||
stdin, stdout, stderr = ssh.exec_command(umount_cmd)
|
||||
stdout.channel.recv_exit_status()
|
||||
ssh.close()
|
||||
|
||||
print("All database backups verified mirrored to Synology NAS (14.10.10.71).")
|
||||
|
||||
15
scripts/test_authentik_dump.py
Normal file
15
scripts/test_authentik_dump.py
Normal file
@@ -0,0 +1,15 @@
|
||||
import paramiko
|
||||
import sys
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.11', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
cmd = "kubectl exec -n infra authentik-postgresql-0 -- sh -c 'PGPASSWORD=$(cat /opt/bitnami/postgresql/secrets/password) pg_dump -U authentik authentik' | gzip > /var/backups/authentik_clean.sql.gz"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
print("STDERR:", stderr.read().decode())
|
||||
_, out, _ = ssh.exec_command("ls -lh /var/backups/authentik_clean.sql.gz")
|
||||
print("RESULT:", out.read().decode().strip())
|
||||
ssh.close()
|
||||
|
||||
40
scripts/test_clean_db_dumps.py
Normal file
40
scripts/test_clean_db_dumps.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import paramiko
|
||||
import sys
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
# Authentik
|
||||
print("=== Backing up Authentik ===")
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.11', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
cmd = "kubectl exec -n infra authentik-postgresql-0 -- sh -c 'PGPASSWORD=$(cat /opt/bitnami/postgresql/secrets/postgres-password) pg_dump -U postgres authentik' | gzip > /var/backups/authentik_clean.sql.gz"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
err = stderr.read().decode().strip()
|
||||
if err: print("Authentik err:", err)
|
||||
_, out, _ = ssh.exec_command("ls -lh /var/backups/authentik_clean.sql.gz")
|
||||
print("Authentik size:", out.read().decode().strip())
|
||||
ssh.close()
|
||||
|
||||
# Spaceport (Gitea & Zabbix)
|
||||
print("=== Backing up Gitea & Zabbix ===")
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.41', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
cmd = "mkdir -p /var/backups && kubectl exec -n infrastructure gitea-postgresql-0 -- sh -c 'PGPASSWORD=$(cat /opt/bitnami/postgresql/secrets/password) pg_dump -U ugandanese baptiste' | gzip > /var/backups/gitea_clean.sql.gz"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
err = stderr.read().decode().strip()
|
||||
if err: print("Gitea err:", err)
|
||||
_, out, _ = ssh.exec_command("ls -lh /var/backups/gitea_clean.sql.gz")
|
||||
print("Gitea size:", out.read().decode().strip())
|
||||
|
||||
cmd = "kubectl exec -n monitoring zabbix-postgresql-0 -- sh -c 'PGPASSWORD=zabbix pg_dump -U zabbix zabbix' | gzip > /var/backups/zabbix_clean.sql.gz"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
err = stderr.read().decode().strip()
|
||||
if err: print("Zabbix err:", err)
|
||||
_, out, _ = ssh.exec_command("ls -lh /var/backups/zabbix_clean.sql.gz")
|
||||
print("Zabbix size:", out.read().decode().strip())
|
||||
|
||||
ssh.close()
|
||||
|
||||
35
scripts/test_etcd_snapshots.py
Normal file
35
scripts/test_etcd_snapshots.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import paramiko
|
||||
import sys
|
||||
import datetime
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
clusters = [
|
||||
("aerodrome", "140.44.4.11"),
|
||||
("spaceport", "140.44.4.41")
|
||||
]
|
||||
|
||||
for name, ip in clusters:
|
||||
print(f"[{name.upper()}] Connecting to {ip}...")
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(ip, username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
cmd = f"k3s etcd-snapshot save --name pre-upgrade-{name}-{timestamp}"
|
||||
print(f"[{name.upper()}] Executing: {cmd}")
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
out = stdout.read().decode().strip()
|
||||
err = stderr.read().decode().strip()
|
||||
if out:
|
||||
print(f"[{name.upper()}] STDOUT: {out}")
|
||||
if err:
|
||||
print(f"[{name.upper()}] STDERR: {err}")
|
||||
|
||||
check_cmd = "k3s etcd-snapshot ls"
|
||||
stdin, stdout, stderr = ssh.exec_command(check_cmd)
|
||||
print(f"[{name.upper()}] Recent Snapshots:\n" + stdout.read().decode().strip())
|
||||
|
||||
ssh.close()
|
||||
print("-" * 50)
|
||||
67
scripts/test_keepalived_failover.py
Normal file
67
scripts/test_keepalived_failover.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import sys
|
||||
import time
|
||||
import paramiko
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
def check_vip(ssh):
|
||||
stdin, stdout, stderr = ssh.exec_command("ip -4 addr show dev eth0 | grep '140.44.4.70' || true")
|
||||
return bool(stdout.read().decode().strip())
|
||||
|
||||
def curl_vip(ssh):
|
||||
stdin, stdout, stderr = ssh.exec_command("curl -s -o /dev/null -w '%{http_code}' -m 2 http://140.44.4.70:80 || true")
|
||||
return stdout.read().decode().strip()
|
||||
|
||||
def main():
|
||||
ssh1 = paramiko.SSHClient()
|
||||
ssh1.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh1.connect('140.44.4.71', username='snarf', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
ssh2 = paramiko.SSHClient()
|
||||
ssh2.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh2.connect('140.44.4.72', username='snarf', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
print("=== INITIAL STATE ===")
|
||||
v1 = check_vip(ssh1)
|
||||
v2 = check_vip(ssh2)
|
||||
code = curl_vip(ssh1)
|
||||
print(f"swm01 has VIP: {v1}, swm02 has VIP: {v2}, HTTP code on VIP: {code}")
|
||||
|
||||
print("\n=== TRIGGERING FAILOVER: STOPPING KEEPALIVED ON swm01 ===")
|
||||
ssh1.exec_command("sudo systemctl stop keepalived")
|
||||
|
||||
# Wait up to 5 seconds for swm02 to claim VIP
|
||||
claimed = False
|
||||
for i in range(5):
|
||||
time.sleep(1)
|
||||
if check_vip(ssh2):
|
||||
claimed = True
|
||||
break
|
||||
|
||||
v1 = check_vip(ssh1)
|
||||
v2 = check_vip(ssh2)
|
||||
code = curl_vip(ssh2)
|
||||
print(f"Failover result -> swm01 has VIP: {v1}, swm02 has VIP: {v2}, HTTP code on VIP: {code}")
|
||||
|
||||
print("\n=== RESTORING swm01: STARTING KEEPALIVED ON swm01 ===")
|
||||
ssh1.exec_command("sudo systemctl start keepalived")
|
||||
|
||||
# Wait up to 5 seconds for swm01 to reclaim VIP
|
||||
reclaimed = False
|
||||
for i in range(5):
|
||||
time.sleep(1)
|
||||
if check_vip(ssh1):
|
||||
reclaimed = True
|
||||
break
|
||||
|
||||
v1 = check_vip(ssh1)
|
||||
v2 = check_vip(ssh2)
|
||||
code = curl_vip(ssh1)
|
||||
print(f"Fallback result -> swm01 has VIP: {v1}, swm02 has VIP: {v2}, HTTP code on VIP: {code}")
|
||||
|
||||
ssh1.close()
|
||||
ssh2.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
26
scripts/test_npm_both_nodes.py
Normal file
26
scripts/test_npm_both_nodes.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import sys
|
||||
import paramiko
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
def main():
|
||||
for ip, node in [('140.44.4.71', 'snshpd4lnxswm01'), ('140.44.4.72', 'snshpd4lnxswm02')]:
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(ip, username='snarf', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
print(f"=== TESTING {node} ({ip}) ===")
|
||||
stdin, stdout, stderr = ssh.exec_command('sudo docker ps --filter "name=nginx-proxy-manager_app" --format "{{.ID}} {{.Status}} {{.Names}}"')
|
||||
print("Containers:", stdout.read().decode().strip())
|
||||
|
||||
stdin, stdout, stderr = ssh.exec_command('curl -I http://127.0.0.1:80 ; curl -I http://127.0.0.1:81')
|
||||
print("Curls:\n", stdout.read().decode())
|
||||
err = stderr.read().decode()
|
||||
if err:
|
||||
print("Errors:\n", err)
|
||||
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
24
scripts/test_nut_alert.py
Normal file
24
scripts/test_nut_alert.py
Normal file
@@ -0,0 +1,24 @@
|
||||
import paramiko
|
||||
import json
|
||||
|
||||
def main():
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('14.4.4.44', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
payload = json.dumps({
|
||||
"title": "NUT Master Notification Pipeline Verified",
|
||||
"message": "Direct alert link verified: 14.4.4.44 -> Gotify VIP (http://140.44.4.70:8090) -> Discord #ups-power channel.",
|
||||
"priority": 5
|
||||
})
|
||||
|
||||
curl_cmd = f"curl -s -X POST \"http://140.44.4.70:8090/message?token=A4uN4fEmErGeNcYtOkEn\" -H \"Content-Type: application/json\" -d '{payload}'"
|
||||
stdin, stdout, stderr = ssh.exec_command(curl_cmd)
|
||||
res = stdout.read().decode()
|
||||
print("GOTIFY RESPONSE:", res)
|
||||
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
113
scripts/test_worker_os_patch.py
Normal file
113
scripts/test_worker_os_patch.py
Normal file
@@ -0,0 +1,113 @@
|
||||
import paramiko
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
MASTER_IP = '140.44.4.11'
|
||||
WORKER_IP = '140.44.4.27'
|
||||
NODE_NAME = 'sninfr2lnxwrk07'
|
||||
KEY_FILE = r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet'
|
||||
|
||||
print(f"=== STEP 1: TEST ROLLING OS PATCH ON {NODE_NAME} ({WORKER_IP}) ===")
|
||||
|
||||
# 1. Connect to master to check pods on node
|
||||
master_ssh = paramiko.SSHClient()
|
||||
master_ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
master_ssh.connect(MASTER_IP, username='root', key_filename=KEY_FILE, timeout=10)
|
||||
|
||||
print(f"[*] Checking active pods on {NODE_NAME}...")
|
||||
_, stdout, _ = master_ssh.exec_command(f"kubectl get pods -A --field-selector spec.nodeName={NODE_NAME}")
|
||||
print(stdout.read().decode())
|
||||
|
||||
print(f"[*] Cordoning {NODE_NAME}...")
|
||||
_, stdout, stderr = master_ssh.exec_command(f"kubectl cordon {NODE_NAME}")
|
||||
print(" " + stdout.read().decode().strip())
|
||||
|
||||
print(f"[*] Draining {NODE_NAME}...")
|
||||
_, stdout, stderr = master_ssh.exec_command(f"kubectl drain {NODE_NAME} --ignore-daemonsets --delete-emptydir-data --force --timeout=180s")
|
||||
print(" " + stdout.read().decode().strip())
|
||||
err = stderr.read().decode().strip()
|
||||
if err:
|
||||
print(" (drain notice):", err)
|
||||
|
||||
# 2. Connect to worker to perform apt-get update & dist-upgrade
|
||||
print(f"[*] Connecting directly to worker {WORKER_IP} for package upgrades...")
|
||||
worker_ssh = paramiko.SSHClient()
|
||||
worker_ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
worker_ssh.connect(WORKER_IP, username='root', key_filename=KEY_FILE, timeout=10)
|
||||
|
||||
upgrade_cmd = "DEBIAN_FRONTEND=noninteractive apt-get update && DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' dist-upgrade -y && apt-get autoremove -y"
|
||||
print("[*] Running dist-upgrade (preserving configurations)...")
|
||||
stdin, stdout, stderr = worker_ssh.exec_command(upgrade_cmd)
|
||||
exit_status = stdout.channel.recv_exit_status()
|
||||
print(f"[*] dist-upgrade completed with exit code: {exit_status}")
|
||||
|
||||
# 3. Check reboot requirement
|
||||
_, stdout, _ = worker_ssh.exec_command("test -f /var/run/reboot-required && echo 'yes' || echo 'no'")
|
||||
reboot_needed = stdout.read().decode().strip() == 'yes'
|
||||
print(f"[*] Reboot required: {reboot_needed}")
|
||||
|
||||
if reboot_needed:
|
||||
print("[*] Rebooting worker node...")
|
||||
try:
|
||||
worker_ssh.exec_command("reboot")
|
||||
except Exception:
|
||||
pass
|
||||
worker_ssh.close()
|
||||
|
||||
print("[*] Waiting 30 seconds for host to reboot...")
|
||||
time.sleep(30)
|
||||
|
||||
# Poll SSH until online
|
||||
reconnected = False
|
||||
for attempt in range(25):
|
||||
try:
|
||||
worker_ssh = paramiko.SSHClient()
|
||||
worker_ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
worker_ssh.connect(WORKER_IP, username='root', key_filename=KEY_FILE, timeout=6)
|
||||
reconnected = True
|
||||
print(f"[*] Worker reconnected successfully on attempt {attempt+1}!")
|
||||
break
|
||||
except Exception:
|
||||
time.sleep(5)
|
||||
|
||||
if not reconnected:
|
||||
print("[!] ERROR: Worker node did not come back online within timeout!")
|
||||
sys.exit(1)
|
||||
|
||||
# Verify services
|
||||
_, stdout, _ = worker_ssh.exec_command("systemctl is-active systemd-timesyncd || systemctl is-active chrony")
|
||||
print(f" TimeSync: {stdout.read().decode().strip()}")
|
||||
_, stdout, _ = worker_ssh.exec_command("systemctl is-active zabbix-agent2")
|
||||
print(f" ZabbixAgent2: {stdout.read().decode().strip()}")
|
||||
_, stdout, _ = worker_ssh.exec_command("systemctl is-active k3s-agent || systemctl is-active k3s")
|
||||
print(f" K3sAgent: {stdout.read().decode().strip()}")
|
||||
worker_ssh.close()
|
||||
else:
|
||||
worker_ssh.close()
|
||||
|
||||
# 4. Uncordon node
|
||||
print(f"[*] Uncordoning {NODE_NAME}...")
|
||||
_, stdout, _ = master_ssh.exec_command(f"kubectl uncordon {NODE_NAME}")
|
||||
print(" " + stdout.read().decode().strip())
|
||||
|
||||
# 5. Wait for node to be Ready
|
||||
print(f"[*] Verifying node status in Kubernetes...")
|
||||
for _ in range(15):
|
||||
_, stdout, _ = master_ssh.exec_command(f"kubectl get node {NODE_NAME} -o jsonpath='{{.status.conditions[?(@.type==\"Ready\")].status}}'")
|
||||
status = stdout.read().decode().strip()
|
||||
if status == "True":
|
||||
print(f"[OK] Node {NODE_NAME} is fully Ready!")
|
||||
break
|
||||
time.sleep(4)
|
||||
|
||||
# 6. Check Longhorn volume robustness
|
||||
print("[*] Settling Longhorn volume replicas...")
|
||||
time.sleep(10)
|
||||
_, stdout, _ = master_ssh.exec_command("kubectl get volumes.longhorn.io -n longhorn-system -o custom-columns=NAME:.metadata.name,STATE:.status.state,ROBUSTNESS:.status.robustness")
|
||||
print(stdout.read().decode())
|
||||
|
||||
master_ssh.close()
|
||||
print(f"[SUCCESS] Step 1 Test Worker OS Patch on {NODE_NAME} completed flawlessly!")
|
||||
|
||||
12
scripts/test_zabbix_13.py
Normal file
12
scripts/test_zabbix_13.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import paramiko
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.41', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
cmd = "kubectl exec -n monitoring deployment/zabbix-zabbix-server -c zabbix-server -- zabbix_get -s 140.44.4.13 -k system.uptime"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
print("OUT:", stdout.read().decode())
|
||||
print("ERR:", stderr.read().decode())
|
||||
ssh.close()
|
||||
|
||||
34
scripts/tune_longhorn_settings.py
Normal file
34
scripts/tune_longhorn_settings.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import sys
|
||||
import paramiko
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
def main():
|
||||
clusters = [
|
||||
("aerodrome", "140.44.4.11"),
|
||||
("spaceport", "140.44.4.41")
|
||||
]
|
||||
for name, host in clusters:
|
||||
print(f"=== Configuring Longhorn settings on {name} ({host}) ===")
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(host, username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
# 1. replica-replenishment-wait-interval -> 300
|
||||
cmd1 = "kubectl patch setting.longhorn.io replica-replenishment-wait-interval -n longhorn-system --type=merge -p '{\"value\":\"300\"}'"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd1)
|
||||
print("replica-replenishment-wait-interval:", stdout.read().decode().strip(), stderr.read().decode().strip())
|
||||
|
||||
# 2. node-down-pod-deletion-policy -> delete-deployment-pod
|
||||
cmd2 = "kubectl patch setting.longhorn.io node-down-pod-deletion-policy -n longhorn-system --type=merge -p '{\"value\":\"delete-deployment-pod\"}'"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd2)
|
||||
print("node-down-pod-deletion-policy:", stdout.read().decode().strip(), stderr.read().decode().strip())
|
||||
|
||||
# Verify
|
||||
stdin, stdout, stderr = ssh.exec_command("kubectl get setting.longhorn.io replica-replenishment-wait-interval node-down-pod-deletion-policy -n longhorn-system -o custom-columns=NAME:.metadata.name,VALUE:.value")
|
||||
print("VERIFIED:\n", stdout.read().decode().strip())
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
50
scripts/update_keepalived_healthcheck.py
Normal file
50
scripts/update_keepalived_healthcheck.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import sys
|
||||
import paramiko
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
NODES = [
|
||||
("swm01", "140.44.4.71"),
|
||||
("swm02", "140.44.4.72"),
|
||||
("swm03", "140.44.4.73"),
|
||||
("swm04", "140.44.4.74"),
|
||||
("swm05", "140.44.4.75"),
|
||||
]
|
||||
|
||||
SCRIPT_CONTENT = """#!/bin/bash
|
||||
# Check docker daemon health
|
||||
systemctl is-active --quiet docker || exit 1
|
||||
|
||||
# If NPM container is running on this node, verify HTTP responsiveness
|
||||
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "nginx-proxy-manager_app"; then
|
||||
curl -sf -m 2 http://127.0.0.1:80 >/dev/null 2>&1 || curl -sf -m 2 http://127.0.0.1:81 >/dev/null 2>&1 || exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
"""
|
||||
|
||||
def main():
|
||||
for name, ip in NODES:
|
||||
print(f"=== Updating {name} ({ip}) ===")
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(ip, username='snarf', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
# Write to temp file then sudo mv
|
||||
sftp = ssh.open_sftp()
|
||||
with sftp.open('/tmp/check_docker.sh', 'w') as f:
|
||||
f.write(SCRIPT_CONTENT.replace('\r\n', '\n'))
|
||||
sftp.close()
|
||||
|
||||
cmd = "sudo mv /tmp/check_docker.sh /etc/keepalived/scripts/check_docker.sh && sudo chmod +x /etc/keepalived/scripts/check_docker.sh && sudo /etc/keepalived/scripts/check_docker.sh && echo 'TEST: OK'"
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
print(stdout.read().decode().strip())
|
||||
err = stderr.read().decode().strip()
|
||||
if err:
|
||||
print("ERR:", err)
|
||||
|
||||
ssh.close()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
20
scripts/verify_cluster_health.py
Normal file
20
scripts/verify_cluster_health.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import paramiko
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect('140.44.4.11', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||
|
||||
cmds = [
|
||||
"kubectl get pods -n kube-system",
|
||||
"kubectl get pods -n awx",
|
||||
"kubectl get pods -n infra",
|
||||
"kubectl get volumes.longhorn.io -n longhorn-system -o custom-columns=NAME:.metadata.name,STATE:.status.state,ROBUSTNESS:.status.robustness | head -n 15"
|
||||
]
|
||||
|
||||
for cmd in cmds:
|
||||
print(f"\n=== {cmd} ===")
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
print(stdout.read().decode())
|
||||
|
||||
ssh.close()
|
||||
|
||||
Binary file not shown.
BIN
src/iac_reverse/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
src/iac_reverse/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/iac_reverse/__pycache__/models.cpython-312.pyc
Normal file
BIN
src/iac_reverse/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/iac_reverse/__pycache__/plugin_base.cpython-312.pyc
Normal file
BIN
src/iac_reverse/__pycache__/plugin_base.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/iac_reverse/cli/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
src/iac_reverse/cli/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/iac_reverse/cli/__pycache__/cli.cpython-312.pyc
Normal file
BIN
src/iac_reverse/cli/__pycache__/cli.cpython-312.pyc
Normal file
Binary file not shown.
Reference in New Issue
Block a user