Compare commits
80 Commits
a793a04ffb
...
alpha
| Author | SHA1 | Date | |
|---|---|---|---|
| 336b7759d0 | |||
| 68c108cfe8 | |||
| 9ebd48af0c | |||
| 113ebdc7c5 | |||
| 5a70460e89 | |||
| 1228b957de | |||
| 3c373351b7 | |||
| e4962b7334 | |||
| 7f07fa5a89 | |||
| fc42bd6f94 | |||
| 17e60354a1 | |||
| 1d4dfb594b | |||
| 42bfb43b65 | |||
| bbcf656a3d | |||
| e0ebc120ee | |||
| 40e504c2fe | |||
| 34cfece0b0 | |||
| 4e8f93173e | |||
| 1fe16429a4 | |||
| ac1395486d | |||
| a07cecb6f3 | |||
| ffc9ab8ab0 | |||
| d4b367794c | |||
| 3871d04d48 | |||
| 0ed293b6ea | |||
| d5c2f6eae5 | |||
| 8e1c5f1e66 | |||
| 1965d37687 | |||
| aea8608f3d | |||
| 06c203485f | |||
| b12c143791 | |||
| d9ca093155 | |||
| 1b8c862b10 | |||
| 00f22f33af | |||
| 47acedb455 | |||
| fffb446206 | |||
| fa969fc1d3 | |||
| f1c197422d | |||
| 17ca433d0c | |||
| 88191155d2 | |||
| cbdb37a453 | |||
| 1fce3f2488 | |||
| 14fac7151f | |||
| b974980d08 | |||
| 5b955fcbc3 | |||
| 80738fd8c1 | |||
| 11749df634 | |||
| 1e7935e789 | |||
| 706fccacb1 | |||
| 114b619c1f | |||
| 45f9b557b9 | |||
| 07dae0c73e | |||
| 93e13c7482 | |||
| 8b21087c89 | |||
| ba47f6a384 | |||
| 1b378b29f3 | |||
| 8117326b14 | |||
| 4f0fa3fe55 | |||
| 42f669266e | |||
| 5a583d1db9 | |||
| 5cb00c04a9 | |||
| 9391b3f2a1 | |||
| 3708404838 | |||
| d3e2e1f291 | |||
| 7ca3d5acb8 | |||
| dff2279a8f | |||
| 7b791cebbc | |||
| 8ffd70dd9c | |||
| 0e1593dcae | |||
| 7d4d59f1ae | |||
| ceae37287b | |||
| 958c27a0be | |||
| bd1c0902bd | |||
| 90977592ab | |||
| 08d085b95f | |||
| 003ae80c9c | |||
| 9c3de8bb4d | |||
| d1a3ba988a | |||
| 08c6c76d89 | |||
| 502b00922b |
83
ansible/inventories/snarfnet.yml
Normal file
83
ansible/inventories/snarfnet.yml
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
all:
|
||||||
|
children:
|
||||||
|
k3s_aerodrome_masters:
|
||||||
|
vars:
|
||||||
|
ansible_user: root
|
||||||
|
cluster: aerodrome
|
||||||
|
node_role: master
|
||||||
|
hosts:
|
||||||
|
sninfr2lnxctl01: { ansible_host: 140.44.4.11 }
|
||||||
|
sninfr2lnxctl02: { ansible_host: 140.44.4.12 }
|
||||||
|
sninfr2lnxctl03: { ansible_host: 140.44.4.13 }
|
||||||
|
|
||||||
|
k3s_aerodrome_workers:
|
||||||
|
vars:
|
||||||
|
ansible_user: root
|
||||||
|
cluster: aerodrome
|
||||||
|
node_role: worker
|
||||||
|
hosts:
|
||||||
|
sninfr2lnxwrk01: { ansible_host: 140.44.4.21 }
|
||||||
|
sninfr2lnxwrk02: { ansible_host: 140.44.4.22 }
|
||||||
|
sninfr2lnxwrk03: { ansible_host: 140.44.4.23 }
|
||||||
|
sninfr2lnxwrk04: { ansible_host: 140.44.4.24 }
|
||||||
|
sninfr2lnxwrk05: { ansible_host: 140.44.4.25 }
|
||||||
|
sninfr2lnxwrk06: { ansible_host: 140.44.4.26 }
|
||||||
|
sninfr2lnxwrk07: { ansible_host: 140.44.4.27 }
|
||||||
|
|
||||||
|
k3s_spaceport_masters:
|
||||||
|
vars:
|
||||||
|
ansible_user: root
|
||||||
|
cluster: spaceport
|
||||||
|
node_role: master
|
||||||
|
hosts:
|
||||||
|
sninfr0lnxctl01: { ansible_host: 140.44.4.41 }
|
||||||
|
sninfr0lnxctl02: { ansible_host: 140.44.4.42 }
|
||||||
|
sninfr0lnxctl03: { ansible_host: 140.44.4.43 }
|
||||||
|
sninfr0lnxctl04: { ansible_host: 140.44.4.44 }
|
||||||
|
|
||||||
|
k3s_spaceport_workers:
|
||||||
|
vars:
|
||||||
|
ansible_user: root
|
||||||
|
cluster: spaceport
|
||||||
|
node_role: worker
|
||||||
|
hosts:
|
||||||
|
sninfr0lnxwrk01: { ansible_host: 140.44.4.51 }
|
||||||
|
sninfr0lnxwrk02: { ansible_host: 140.44.4.52 }
|
||||||
|
sninfr0lnxwrk03: { ansible_host: 140.44.4.53 }
|
||||||
|
sninfr0lnxwrk04: { ansible_host: 140.44.4.54 }
|
||||||
|
sninfr0lnxwrk05: { ansible_host: 140.44.4.55 }
|
||||||
|
sninfr0lnxwrk06: { ansible_host: 140.44.4.56 }
|
||||||
|
|
||||||
|
swarm_shipyard:
|
||||||
|
vars:
|
||||||
|
ansible_user: snarf
|
||||||
|
ansible_become: true
|
||||||
|
cluster: shipyard
|
||||||
|
node_role: swarm
|
||||||
|
hosts:
|
||||||
|
snshpd4lnxswm01: { ansible_host: 140.44.4.71 }
|
||||||
|
snshpd4lnxswm02: { ansible_host: 140.44.4.72 }
|
||||||
|
snshpd4lnxswm03: { ansible_host: 140.44.4.73 }
|
||||||
|
snshpd4lnxswm04: { ansible_host: 140.44.4.74 }
|
||||||
|
snshpd4lnxswm05: { ansible_host: 140.44.4.75 }
|
||||||
|
|
||||||
|
ai_pi_cluster:
|
||||||
|
vars:
|
||||||
|
ansible_user: snarf
|
||||||
|
ansible_become: true
|
||||||
|
hosts:
|
||||||
|
sninfr4lnxaih01: { ansible_host: 140.44.4.7 }
|
||||||
|
sninfr4lnxaih02: { ansible_host: 140.44.4.8 }
|
||||||
|
|
||||||
|
smart_home_nodes:
|
||||||
|
vars:
|
||||||
|
ansible_user: snarf
|
||||||
|
ansible_become: true
|
||||||
|
hosts:
|
||||||
|
nutserver: { ansible_host: 14.4.4.44 }
|
||||||
|
|
||||||
|
synology_nas:
|
||||||
|
hosts:
|
||||||
|
sn_nas: { ansible_host: 14.10.10.71 }
|
||||||
|
snarfnas: { ansible_host: 14.10.10.72 }
|
||||||
|
|
||||||
35
ansible/playbooks/daily_drift_audit.yml
Normal file
35
ansible/playbooks/daily_drift_audit.yml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
- name: Daily Infrastructure Health & Configuration Drift Audit
|
||||||
|
hosts: all:!synology_nas
|
||||||
|
gather_facts: false
|
||||||
|
tasks:
|
||||||
|
- name: Audit system time sync status
|
||||||
|
ansible.builtin.shell: |
|
||||||
|
for svc in systemd-timesyncd chrony chronyd; do
|
||||||
|
if [ "$(systemctl is-active $svc 2>/dev/null)" = "active" ]; then
|
||||||
|
echo "active"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "inactive"
|
||||||
|
exit 1
|
||||||
|
register: time_sync
|
||||||
|
changed_when: false
|
||||||
|
failed_when: time_sync.stdout != "active"
|
||||||
|
|
||||||
|
- name: Audit Zabbix Agent 2 service status
|
||||||
|
ansible.builtin.command: systemctl is-active zabbix-agent2
|
||||||
|
register: zabbix_status
|
||||||
|
changed_when: false
|
||||||
|
failed_when: zabbix_status.stdout != "active"
|
||||||
|
|
||||||
|
- name: Audit unified SSH key enrollment in authorized_keys
|
||||||
|
ansible.builtin.shell: grep -q "snarfnet-automation-master@snarfnet.net" ~/.ssh/authorized_keys
|
||||||
|
register: ssh_key_check
|
||||||
|
changed_when: false
|
||||||
|
failed_when: ssh_key_check.rc != 0
|
||||||
|
|
||||||
|
- name: Display host audit status
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "{{ inventory_hostname }} ({{ ansible_host }}) [OK]: TimeSync={{ time_sync.stdout }}, Zabbix={{ zabbix_status.stdout }}, SSHKey=Verified"
|
||||||
|
|
||||||
30
ansible/playbooks/k3s_aerodrome_precheck.yml
Normal file
30
ansible/playbooks/k3s_aerodrome_precheck.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
- name: Aerodrome K3s Control Plane Pre-Upgrade Health & etcd Snapshot
|
||||||
|
hosts: k3s_aerodrome_masters
|
||||||
|
gather_facts: false
|
||||||
|
tasks:
|
||||||
|
- name: Verify K3s systemd service is active
|
||||||
|
ansible.builtin.command: systemctl is-active k3s
|
||||||
|
register: k3s_service
|
||||||
|
changed_when: false
|
||||||
|
failed_when: k3s_service.stdout != "active"
|
||||||
|
|
||||||
|
- name: Query current node K3s version
|
||||||
|
ansible.builtin.command: k3s --version
|
||||||
|
register: k3s_ver
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Display current K3s version
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "{{ inventory_hostname }} current version: {{ k3s_ver.stdout_lines[0] }}"
|
||||||
|
|
||||||
|
- name: Save on-demand etcd snapshot on primary master (.11)
|
||||||
|
ansible.builtin.command: k3s etcd-snapshot save --node-name sninfr2lnxctl01 --name "pre-upgrade-snapshot"
|
||||||
|
when: inventory_hostname == "sninfr2lnxctl01"
|
||||||
|
register: etcd_snap
|
||||||
|
|
||||||
|
- name: Report etcd snapshot result
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "{{ etcd_snap.stdout_lines }}"
|
||||||
|
when: inventory_hostname == "sninfr2lnxctl01"
|
||||||
|
|
||||||
162
ansible/playbooks/upgrade_k3s_aerodrome.yml
Normal file
162
ansible/playbooks/upgrade_k3s_aerodrome.yml
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
---
|
||||||
|
- name: Aerodrome K3s Control Plane Sequential Upgrade
|
||||||
|
hosts: k3s_aerodrome_masters
|
||||||
|
gather_facts: false
|
||||||
|
serial: 1
|
||||||
|
vars:
|
||||||
|
target_k3s_version: "v1.31.4+k3s1"
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Query pre-upgrade K3s version
|
||||||
|
ansible.builtin.command: k3s --version
|
||||||
|
register: pre_k3s_ver
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Display pre-upgrade version
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: "Upgrading {{ inventory_hostname }} from {{ pre_k3s_ver.stdout_lines[0] }} to {{ target_k3s_version }}"
|
||||||
|
|
||||||
|
- name: Download target K3s release binary
|
||||||
|
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
|
||||||
|
|
||||||
|
- name: Restart K3s systemd service
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: k3s
|
||||||
|
state: restarted
|
||||||
|
when: target_k3s_version not in pre_k3s_ver.stdout
|
||||||
|
|
||||||
|
- name: Wait for K3s systemd unit to report active
|
||||||
|
ansible.builtin.command: systemctl is-active k3s
|
||||||
|
register: k3s_active
|
||||||
|
until: k3s_active.stdout == "active"
|
||||||
|
retries: 25
|
||||||
|
delay: 4
|
||||||
|
changed_when: false
|
||||||
|
when: target_k3s_version not in pre_k3s_ver.stdout
|
||||||
|
|
||||||
|
- 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
|
||||||
|
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 K3s version
|
||||||
|
ansible.builtin.command: k3s --version
|
||||||
|
register: post_k3s_ver
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Verify post-upgrade 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 etcd cluster consensus
|
||||||
|
ansible.builtin.pause:
|
||||||
|
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
|
||||||
110
ansible/playbooks/weekly_os_patching.yml
Normal file
110
ansible/playbooks/weekly_os_patching.yml
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
---
|
||||||
|
- name: Weekly OS & Security Patching with Rolling Safety Gates
|
||||||
|
hosts: all:!synology_nas
|
||||||
|
serial: 1
|
||||||
|
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
|
||||||
|
ansible.builtin.apt:
|
||||||
|
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
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: /var/run/reboot-required
|
||||||
|
register: reboot_required_file
|
||||||
|
|
||||||
|
- name: Rolling Reboot with Connection Verification
|
||||||
|
when: reboot_required_file.stat.exists
|
||||||
|
block:
|
||||||
|
- name: Reboot host gracefully
|
||||||
|
ansible.builtin.reboot:
|
||||||
|
msg: "Automated rolling maintenance reboot via Snarfnet Automation"
|
||||||
|
reboot_timeout: 300
|
||||||
|
connect_timeout: 30
|
||||||
|
test_command: uptime
|
||||||
|
|
||||||
|
- 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:
|
||||||
|
name: zabbix-agent2
|
||||||
|
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']
|
||||||
58
boot_staging/Automation_Custom_Script.sh
Normal file
58
boot_staging/Automation_Custom_Script.sh
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "[FIRST-BOOT] Starting automated provisioning for sninfr2lnxwrk01..."
|
||||||
|
|
||||||
|
# 1. SSH Key Enrollment
|
||||||
|
PUB_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOsRiwuL7nfm1Ruew+Xk+3tbzD9ONyKAO0q2QwkppEPZ snarfnet-automation-master@snarfnet.net"
|
||||||
|
mkdir -p /root/.ssh /etc/dropbear
|
||||||
|
chmod 700 /root/.ssh
|
||||||
|
echo "$PUB_KEY" >> /root/.ssh/authorized_keys
|
||||||
|
echo "$PUB_KEY" >> /etc/dropbear/authorized_keys
|
||||||
|
chmod 600 /root/.ssh/authorized_keys /etc/dropbear/authorized_keys
|
||||||
|
|
||||||
|
# 2. Filesystem Hardening
|
||||||
|
ROOT_DEV=$(findmnt -n -o SOURCE /)
|
||||||
|
if [[ "$ROOT_DEV" == /dev/* ]]; then
|
||||||
|
tune2fs -e continue "$ROOT_DEV" || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Time Synchronization
|
||||||
|
cat << 'EOF' > /etc/systemd/timesyncd.conf
|
||||||
|
[Time]
|
||||||
|
NTP=140.44.4.11 14.4.4.44
|
||||||
|
FallbackNTP=time.cloudflare.com time.google.com 0.us.pool.ntp.org 1.us.pool.ntp.org
|
||||||
|
PollIntervalMinSec=32
|
||||||
|
PollIntervalMaxSec=512
|
||||||
|
EOF
|
||||||
|
systemctl restart systemd-timesyncd.service || true
|
||||||
|
|
||||||
|
# 4. Install & Configure Zabbix Agent 2
|
||||||
|
apt-get update -y
|
||||||
|
apt-get install -y zabbix-agent2
|
||||||
|
mkdir -p /etc/zabbix
|
||||||
|
echo "a1142332ccfa9c53040e36171bb551ceb2865fb4051d203404df39e88ebe94f0" > /etc/zabbix/zabbix_agent2.psk
|
||||||
|
chmod 600 /etc/zabbix/zabbix_agent2.psk
|
||||||
|
chown zabbix:zabbix /etc/zabbix/zabbix_agent2.psk 2>/dev/null || true
|
||||||
|
|
||||||
|
cat << 'EOF' > /etc/zabbix/zabbix_agent2.conf
|
||||||
|
Server=zabbix.snarfnet.net
|
||||||
|
ServerActive=zabbix.snarfnet.net
|
||||||
|
HostnameItem=system.hostname
|
||||||
|
HostMetadata=Linux
|
||||||
|
TLSConnect=psk
|
||||||
|
TLSAccept=psk
|
||||||
|
TLSPSKIdentity=PSK_autoregister
|
||||||
|
TLSPSKFile=/etc/zabbix/zabbix_agent2.psk
|
||||||
|
LogFile=/var/log/zabbix/zabbix_agent2.log
|
||||||
|
LogFileSize=10
|
||||||
|
BufferSend=5
|
||||||
|
BufferSize=100
|
||||||
|
EOF
|
||||||
|
systemctl enable --now zabbix-agent2.service
|
||||||
|
|
||||||
|
# 5. Install K3s Agent
|
||||||
|
curl -sfL https://get.k3s.io | K3S_URL=https://140.44.4.11:6443 K3S_TOKEN=myworstkeptsecret44 sh -
|
||||||
|
|
||||||
|
echo "[FIRST-BOOT] Provisioning completed successfully!"
|
||||||
|
|
||||||
44
boot_staging/dietpi.txt
Normal file
44
boot_staging/dietpi.txt
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Standard DietPi Automated Headless Config for sninfr2lnxwrk01 (140.44.4.21)
|
||||||
|
AUTO_SETUP_LOCALE=C.UTF-8
|
||||||
|
AUTO_SETUP_KEYBOARD_LAYOUT=us
|
||||||
|
AUTO_SETUP_TIMEZONE=America/New_York
|
||||||
|
AUTO_SETUP_NET_ETHERNET_ENABLED=1
|
||||||
|
AUTO_SETUP_NET_WIFI_ENABLED=0
|
||||||
|
AUTO_SETUP_NET_USESTATIC=1
|
||||||
|
AUTO_SETUP_NET_STATIC_IP=140.44.4.21
|
||||||
|
AUTO_SETUP_NET_STATIC_MASK=255.255.255.0
|
||||||
|
AUTO_SETUP_NET_STATIC_GATEWAY=140.44.4.1
|
||||||
|
AUTO_SETUP_NET_STATIC_DNS=140.44.4.11 14.4.4.44
|
||||||
|
AUTO_SETUP_DHCP_TO_STATIC=0
|
||||||
|
AUTO_SETUP_NET_HOSTNAME=sninfr2lnxwrk01
|
||||||
|
AUTO_SETUP_NET_ETH_FORCE_SPEED=0
|
||||||
|
AUTO_SETUP_BOOT_WAIT_FOR_NETWORK=1
|
||||||
|
AUTO_SETUP_SWAPFILE_SIZE=0
|
||||||
|
AUTO_SETUP_HEADLESS=1
|
||||||
|
AUTO_UNMASK_LOGIND=0
|
||||||
|
AUTO_SETUP_CUSTOM_SCRIPT_EXEC=1
|
||||||
|
AUTO_SETUP_BACKUP_RESTORE=0
|
||||||
|
AUTO_SETUP_SSH_SERVER_INDEX=-1
|
||||||
|
AUTO_SETUP_LOGGING_INDEX=-1
|
||||||
|
AUTO_SETUP_RAMLOG_MAXSIZE=50
|
||||||
|
AUTO_SETUP_WEB_SERVER_INDEX=0
|
||||||
|
AUTO_SETUP_DESKTOP_INDEX=0
|
||||||
|
AUTO_SETUP_BROWSER_INDEX=-1
|
||||||
|
AUTO_SETUP_AUTOSTART_TARGET_INDEX=0
|
||||||
|
AUTO_SETUP_AUTOSTART_LOGIN_USER=root
|
||||||
|
AUTO_SETUP_AUTOMATED=1
|
||||||
|
AUTO_SETUP_GLOBAL_PASSWORD=dietpi
|
||||||
|
SURVEY_OPTED_IN=0
|
||||||
|
CONFIG_CPU_GOVERNOR=performance
|
||||||
|
CONFIG_CHECK_DIETPI_UPDATES=1
|
||||||
|
CONFIG_CHECK_APT_UPDATES=1
|
||||||
|
CONFIG_NTP_MODE=4
|
||||||
|
CONFIG_SERIAL_CONSOLE_ENABLE=1
|
||||||
|
CONFIG_SOUNDCARD=none
|
||||||
|
CONFIG_LCDPANEL=none
|
||||||
|
CONFIG_ENABLE_IPV6=0
|
||||||
|
CONFIG_NTP_MIRROR=default
|
||||||
|
SOFTWARE_DISABLE_SSH_PASSWORD_LOGINS=0
|
||||||
|
DEV_GITBRANCH=master
|
||||||
|
DEV_GITOWNER=MichaIng
|
||||||
|
|
||||||
@@ -1,20 +1,33 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
heimdall:
|
heimdall:
|
||||||
image: lscr.io/linuxserver/heimdall:latest
|
image: lscr.io/linuxserver/heimdall:latest
|
||||||
ports:
|
ports:
|
||||||
- "85:80"
|
- target: 80
|
||||||
- "403:443"
|
published: 85
|
||||||
|
mode: ingress
|
||||||
|
- target: 443
|
||||||
|
published: 403
|
||||||
|
mode: ingress
|
||||||
environment:
|
environment:
|
||||||
- PUID=1000
|
- PUID=1000
|
||||||
- PGID=1000
|
- PGID=1000
|
||||||
- TZ=America/New_York
|
- TZ=America/New_York
|
||||||
volumes:
|
volumes:
|
||||||
- heimdall_config:/config
|
- type: bind
|
||||||
|
source: /var/lib/docker-data/heimdall/config
|
||||||
|
target: /config
|
||||||
|
deploy:
|
||||||
|
mode: replicated
|
||||||
|
replicas: 1
|
||||||
|
placement:
|
||||||
|
constraints:
|
||||||
|
- node.hostname == snshpd4lnxswm01
|
||||||
|
restart_policy:
|
||||||
|
condition: on-failure
|
||||||
|
|
||||||
volumes:
|
networks:
|
||||||
heimdall_config:
|
default:
|
||||||
driver: local
|
name: homelab_public
|
||||||
driver_opts:
|
external: true
|
||||||
type: nfs
|
|
||||||
o: "addr=14.10.10.71,rw,nfsvers=4"
|
|
||||||
device: ":/volume1/docker/heimdall/"
|
|
||||||
|
|||||||
21
docker-deployments/shepherd.yml
Normal file
21
docker-deployments/shepherd.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
shepherd:
|
||||||
|
image: containrrr/shepherd:latest
|
||||||
|
environment:
|
||||||
|
- SLEEP_TIME=24h
|
||||||
|
- IGNORELIST_SERVICES=nginx-proxy-manager_app nginx-proxy-manager_backup
|
||||||
|
- UPDATE_OPTIONS=--with-registry-auth
|
||||||
|
- ROLLBACK_FAILURE=true
|
||||||
|
- TZ=America/New_York
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
deploy:
|
||||||
|
mode: replicated
|
||||||
|
replicas: 1
|
||||||
|
placement:
|
||||||
|
constraints:
|
||||||
|
- node.role == manager
|
||||||
|
restart_policy:
|
||||||
|
condition: on-failure
|
||||||
184
docs/DNS_Setup_Plan.md
Normal file
184
docs/DNS_Setup_Plan.md
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
# Project Blueprint: High-Availability DNS Stack, Network Honeypot & UDM Pro Network Security
|
||||||
|
|
||||||
|
## 1. Executive Summary & Live UDM Pro Topology Assessment
|
||||||
|
|
||||||
|
Based on the live UDM Pro controller diagnostics, we have mapped the exact network topography, identified critical security exposures, and tailored the implementation plan.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.1 Live Network & VLAN Topography
|
||||||
|
|
||||||
|
| VLAN ID | Network Name | Subnet / CIDR | Gateway IP | Active Leases | Security Classification |
|
||||||
|
|:---:|---|---|---|:---:|---|
|
||||||
|
| **1** | **LAN** | `14.44.4.0/25` | `14.44.4.1` | 14 / 117 | Trusted Internal Clients |
|
||||||
|
| **2** | **Management** | `140.44.44.0/24` | `140.44.44.1` | 9 / 51 | Out-of-band (iDRAC, Switches, APs) |
|
||||||
|
| **3** | **Spacedock** | `140.44.1.0/24` | `140.44.1.1` | 0 / 249 | Harvester VM / Hypervisor Tenant |
|
||||||
|
| **20** | **Compute** | `140.44.4.0/25` | `140.44.4.1` | 1 / 15 (Static Nodes) | Mission-Critical K3s Clusters, Swarm |
|
||||||
|
| **30** | **Media** | `192.168.1.0/25` | `192.168.1.1` | 1 / 91 | Streaming & Entertainment Devices |
|
||||||
|
| **40** | **Storage** | `14.10.10.0/25` | `14.10.10.1` | 2 / 125 | Synology NAS (`.71`, `.72`) NFS / iSCSI |
|
||||||
|
| **50** | **Smart Home** | `14.4.4.0/25` | `14.4.4.1` | 53 / 85 | IoT Devices, Gaming Host (`14.4.4.15`) |
|
||||||
|
| **60** | **HA** | `140.4.4.0/25` | `140.4.4.1` | 0 / 7 | High-Availability Interconnects |
|
||||||
|
| **VPN** | **WireGuard / Teleport** | `192.168.3.0/24` / `.2.0/24` | UDM Pro | Remote | Ingress Remote Access |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.2 Critical Security Findings from UDM Pro Diagnostics
|
||||||
|
|
||||||
|
1. **Default Security Posture is `Allow All`:**
|
||||||
|
* Currently, the UDM Pro routes traffic freely between all subnets without restriction.
|
||||||
|
* **Risk:** The 53 IoT devices in Smart Home (`14.4.4.0/25`) and Media devices can directly initiate connections to Kubernetes nodes, etcd ports, SSH, and Synology NAS shares in `Compute` (`140.44.4.0/25`) and `Storage` (`14.10.10.0/25`).
|
||||||
|
2. **WAN Port Forwarding to Smart Home Host (`14.4.4.15`):**
|
||||||
|
* Port forwards for game servers (`FS25` ports 8443/10823, `Stationeers` ports 6443/27015) point directly from WAN (`174.106.252.96`) to host `14.4.4.15` on the Smart Home VLAN.
|
||||||
|
* If an exploit compromises this host, an attacker currently has unhindered lateral access to the entire homelab infrastructure.
|
||||||
|
3. **DNS Leaks & Lack of Local Filtering:**
|
||||||
|
* UDM Pro WAN is set to `Auto DNS Server` (Spectrum ISP DNS `0.0.0.0`).
|
||||||
|
* No DNS ad/malware filtering or split-horizon DNS caching is currently active across the subnets.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. High-Availability DNS Stack (Pi-hole + Technitium)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
subgraph Clients["LAN Clients across all VLANs (VLAN 1, 20, 30, 50, VPN)"]
|
||||||
|
Devices["IoT Devices (53), Laptops, Servers, Hypervisors"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph UDM["UniFi Dream Machine Pro (140.44.4.1)"]
|
||||||
|
DHCP["DHCP Option 6 per VLAN:<br>Primary DNS: 140.44.4.35 (Aerodrome VIP)<br>Secondary DNS: 140.44.4.65 (Spaceport VIP)"]
|
||||||
|
NAT["DNS Redirection Rule:<br>Capture port 53 to hardcoded DNS (8.8.8.8) -> Redirect to VIPs"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph AeroCluster["Aerodrome K3s Cluster (VLAN 20)"]
|
||||||
|
AeroVIP["MetalLB VIP: 140.44.4.35:53 (UDP/TCP)"]
|
||||||
|
AeroPihole["Pi-hole Instance 1<br>(Ad-blocking, Caching, FTL rate-limit: 0)"]
|
||||||
|
AeroTech["Technitium Instance 1 (Primary Authoritative)<br>(snarfnet.net Zone, Reverse PTR, DNSSEC)"]
|
||||||
|
AeroStorage["Longhorn Storage Volume"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph SpaceCluster["Spaceport K3s Cluster (VLAN 20)"]
|
||||||
|
SpaceVIP["MetalLB VIP: 140.44.4.65:53 (UDP/TCP)"]
|
||||||
|
SpacePihole["Pi-hole Instance 2<br>(Ad-blocking, Caching, FTL rate-limit: 0)"]
|
||||||
|
SpaceTech["Technitium Instance 2 (Secondary Authoritative)<br>(Zone Transfer AXFR/IXFR from Primary)"]
|
||||||
|
SpaceStorage["Longhorn Storage Volume"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Upstream["Encrypted Upstream Resolvers"]
|
||||||
|
DoH["DNS-over-HTTPS (Quad9 / Cloudflare / Mullvad)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
Devices -->|DNS Queries (UDP/TCP 53)| DHCP
|
||||||
|
DHCP -->|Primary| AeroVIP
|
||||||
|
DHCP -.->|Secondary / Failover| SpaceVIP
|
||||||
|
AeroVIP --> AeroPihole
|
||||||
|
SpaceVIP --> SpacePihole
|
||||||
|
AeroPihole -->|Recursive / Local Lookups| AeroTech
|
||||||
|
SpacePihole -->|Recursive / Local Lookups| SpaceTech
|
||||||
|
AeroTech <==>|Automatic Zone Sync (AXFR)| SpaceTech
|
||||||
|
AeroTech -->|Encrypted DoH| DoH
|
||||||
|
SpaceTech -->|Encrypted DoH| DoH
|
||||||
|
AeroPihole --- AeroStorage
|
||||||
|
AeroTech --- AeroStorage
|
||||||
|
SpacePihole --- SpaceStorage
|
||||||
|
SpaceTech --- SpaceStorage
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.1 Addressing the Rate-Limiting & Sluggishness Issue
|
||||||
|
1. **FTL Rate Limit Bypass:** Set `FTLCONF_RATE_LIMIT=0` so local bursts (such as 53 Smart Home devices polling simultaneously) are never throttled.
|
||||||
|
2. **Technitium Engine:** Replaces single-threaded Unbound with a high-performance .NET 9 asynchronous resolver capable of servicing tens of thousands of queries per second with sub-millisecond RAM caching.
|
||||||
|
3. **High Availability:** MetalLB VIPs on Aerodrome (`140.44.4.35`) and Spaceport (`140.44.4.65`) allow continuous service even during node reboots or maintenance.
|
||||||
|
4. **Authoritative Split-Horizon:** Technitium will authoritatively host the internal `snarfnet.net` forward zone and reverse PTR zones (`4.44.140.in-addr.arpa`, `4.4.14.in-addr.arpa`, `10.10.14.in-addr.arpa`), eliminating slow mDNS or external lookup delays.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Network Honeypot & UDM Pro Firewall Hardening
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph WAN_Threats["External & Lateral Threats"]
|
||||||
|
Internet["Public Internet"]
|
||||||
|
CompromisedIoT["Compromised IoT / Host (e.g. 14.4.4.15)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph UDM_FW["UDM Pro Firewall Rules (LAN IN)"]
|
||||||
|
Rule1["Rule 1: Allow Established & Related"]
|
||||||
|
Rule2["Rule 2: Allow DNS (Port 53 to .35 & .65)"]
|
||||||
|
Rule3["Rule 3: Allow NTP (Port 123 to .11 & .44)"]
|
||||||
|
Rule4["Rule 4: Drop IoT -> Compute (VLAN 50 -> 20)"]
|
||||||
|
Rule5["Rule 5: Drop IoT -> Storage (VLAN 50 -> 40)"]
|
||||||
|
Rule6["Rule 6: Drop Media -> Compute (VLAN 30 -> 20)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph HoneypotNet["Honeypot Decoy (VLAN 50 / Decoy IP 14.4.4.99)"]
|
||||||
|
OpenCanary["OpenCanary Honeypot<br>(Simulating SSH:22, HTTP:80, SMB:445, RDP:3389)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph AlertSystem["Alerting & Audit Pipeline"]
|
||||||
|
Gotify["Gotify Push Notification<br>(Instant alert to your phone)"]
|
||||||
|
Zabbix["Zabbix Security Audit Log"]
|
||||||
|
end
|
||||||
|
|
||||||
|
CompromisedIoT -.->|Probes decoy IP| OpenCanary
|
||||||
|
OpenCanary -->|High Priority Webhook| Gotify
|
||||||
|
OpenCanary -->|Syslog Event| Zabbix
|
||||||
|
CompromisedIoT -->|Blocked by Rule 4| UDM_FW
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.1 Network Honeypot Deployment
|
||||||
|
* **Tool:** **OpenCanary** (lightweight, zero false-positives).
|
||||||
|
* **Placement:**
|
||||||
|
* Assigned a decoy IP on the Smart Home subnet (e.g., `14.4.4.99`) where the gaming port forwards exist.
|
||||||
|
* Emulates vulnerable services: SSH (`22`), Telnet (`23`), Web admin (`80`/`8080`), SMB (`445`), and RDP (`3389`).
|
||||||
|
* **Alerting:**
|
||||||
|
* When any internal device attempts a port scan or lateral connection to `14.4.4.99`, OpenCanary immediately sends a critical push notification to **Gotify** (`gotify.snarfnet.net`) and generates an alert in **Zabbix**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 UDM Pro Firewall Rules Matrix (LAN IN)
|
||||||
|
|
||||||
|
We will define firewall groups on the UDM Pro:
|
||||||
|
* **RFC1918_Subnets:** All internal private ranges (`140.44.0.0/16`, `14.0.0.0/8`, `192.168.0.0/16`).
|
||||||
|
* **K8s_DNS_VIPs:** `140.44.4.35`, `140.44.4.65`.
|
||||||
|
* **NTP_Servers:** `140.44.4.11`, `14.4.4.44`.
|
||||||
|
|
||||||
|
| Order | Rule Name | Type | Source | Destination | Action | Purpose |
|
||||||
|
|:---:|---|:---:|---|---|:---:|---|
|
||||||
|
| **2001** | `Allow Established/Related` | LAN IN | Any | Any (State: Established, Related) | **ACCEPT** | Essential return traffic |
|
||||||
|
| **2002** | `Allow Cluster DNS` | LAN IN | All VLANs | `K8s_DNS_VIPs` (Port: 53 UDP/TCP) | **ACCEPT** | Clients resolve DNS |
|
||||||
|
| **2003** | `Allow Cluster NTP` | LAN IN | All VLANs | `NTP_Servers` (Port: 123 UDP) | **ACCEPT** | Chrony time sync |
|
||||||
|
| **2004** | `Allow Trusted to All` | LAN IN | `VLAN 1 (LAN)`, `VLAN 20 (Compute)` | RFC1918_Subnets | **ACCEPT** | Admins & servers access all VLANs |
|
||||||
|
| **2005** | `Isolate SmartHome to Compute` | LAN IN | `VLAN 50 (Smart Home)` | `VLAN 20 (Compute)` | **DROP** | Block IoT pivoting into K8s |
|
||||||
|
| **2006** | `Isolate SmartHome to Storage` | LAN IN | `VLAN 50 (Smart Home)` | `VLAN 40 (Storage)` | **DROP** | Protect Synology NAS from IoT |
|
||||||
|
| **2007** | `Isolate Media to Compute` | LAN IN | `VLAN 30 (Media)` | `VLAN 20 (Compute)` | **DROP** | Block media devices from cluster |
|
||||||
|
| **2008** | `Isolate Guest Network` | GUEST IN | Guest Network | RFC1918_Subnets | **DROP** | Restrict guest traffic to Internet only |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Phased Implementation Roadmap
|
||||||
|
|
||||||
|
### Phase 1: Teardown Lingering Stubs
|
||||||
|
* Cleanly delete orphaned, broken deployments and services in `infra` (Aerodrome) and `infrastructure` (Spaceport):
|
||||||
|
* Remove `deployment/pihole`, `deployment/technitium-dns`, and non-functional NodePort services.
|
||||||
|
|
||||||
|
### Phase 2: Deploy Dual-Cluster Pi-hole + Technitium
|
||||||
|
* Create Longhorn storage volumes on both clusters for persistent configs and blocklists.
|
||||||
|
* Deploy **Aerodrome Primary Stack**:
|
||||||
|
* Technitium on internal port `5353`.
|
||||||
|
* Pi-hole with MetalLB LoadBalancer VIP **`140.44.4.35:53`** (UDP & TCP).
|
||||||
|
* Deploy **Spaceport Secondary Stack**:
|
||||||
|
* Technitium on internal port `5353` configured for AXFR zone transfers from Aerodrome.
|
||||||
|
* Pi-hole with MetalLB LoadBalancer VIP **`140.44.4.65:53`** (UDP & TCP).
|
||||||
|
* Populate `snarfnet.net` local records and reverse PTR zones.
|
||||||
|
|
||||||
|
### Phase 3: Network Honeypot Deployment
|
||||||
|
* Deploy containerized OpenCanary with virtual listener on `14.4.4.99`.
|
||||||
|
* Configure webhook bridge to Gotify server (`140.44.4.70`) for immediate push notification upon any probe.
|
||||||
|
|
||||||
|
### Phase 4: UDM Pro Network Configuration & Policy Hardening
|
||||||
|
* Configure DHCP Scope DNS servers on UDM Pro for all VLANs:
|
||||||
|
* Primary: `140.44.4.35`
|
||||||
|
* Secondary: `140.44.4.65`
|
||||||
|
* Set UDM Pro WAN DNS to `140.44.4.35` & `140.44.4.65` (or Quad9 fallback).
|
||||||
|
* Apply LAN IN firewall rule matrix on UDM Pro to enforce strict VLAN isolation.
|
||||||
|
|
||||||
|
snaisvr44hrvstr0x - AppropriateBeav3r39!
|
||||||
@@ -25,15 +25,16 @@ All Kubernetes resources are hosted on the **Compute VLAN (VLAN 20 — `140.44.4
|
|||||||
|
|
||||||
### Cluster A: `aerodrome` (Local / Rancher Management Cluster)
|
### Cluster A: `aerodrome` (Local / Rancher Management Cluster)
|
||||||
* **Infrastructure role:** Hosts Rancher management plane, Authentik SSO, Grafana, AWX, and Uptime Kuma.
|
* **Infrastructure role:** Hosts Rancher management plane, Authentik SSO, Grafana, AWX, and Uptime Kuma.
|
||||||
|
* **SSH Default User:** `root`
|
||||||
* **IP Allocation:**
|
* **IP Allocation:**
|
||||||
* `sninfr2lnxctl01` $\rightarrow$ `140.44.4.11` (Master Node, Debian 12, Ready)
|
* `sninfr2lnxctl01` $\rightarrow$ `140.44.4.11` (Master Node, Debian 12, Ready)
|
||||||
* `sninfr2lnxctl02` $\rightarrow$ `140.44.4.12` (Master Node, Debian 12, Ready)
|
* `sninfr2lnxctl02` $\rightarrow$ `140.44.4.12` (Master Node, Debian 12, Ready)
|
||||||
* `sninfr2lnxctl03` $\rightarrow$ `140.44.4.13` (Master Node, Debian 12, Ready)
|
* `sninfr2lnxctl03` $\rightarrow$ `140.44.4.13` (Master Node, Debian 12, Ready)
|
||||||
* `sninfr2lnxwrk01` $\rightarrow$ `140.44.4.21` (Worker Node, **NotReady/Offline**)
|
* `sninfr2lnxwrk01` $\rightarrow$ `140.44.4.21` (Worker Node, Debian 12, Ready)
|
||||||
* `sninfr2lnxwrk02` $\rightarrow$ `140.44.4.22` (Worker Node, Debian 12, Ready)
|
* `sninfr2lnxwrk02` $\rightarrow$ `140.44.4.22` (Worker Node, Debian 12, Ready)
|
||||||
* `sninfr2lnxwrk03` $\rightarrow$ `140.44.4.23` (Worker Node, Debian 12, Ready)
|
* `sninfr2lnxwrk03` $\rightarrow$ `140.44.4.23` (Worker Node, Debian 12, Ready)
|
||||||
* `sninfr2lnxwrk04` $\rightarrow$ `140.44.4.24` (Worker Node, Debian 12, Ready)
|
* `sninfr2lnxwrk04` $\rightarrow$ `140.44.4.24` (Worker Node, Debian 12, Ready)
|
||||||
* `sninfr2lnxwrk05` $\rightarrow$ `140.44.4.25` (Worker Node, **NotReady/Offline**)
|
* `sninfr2lnxwrk05` $\rightarrow$ `140.44.4.25` (Worker Node, Debian 12, Ready)
|
||||||
* `sninfr2lnxwrk06` $\rightarrow$ `140.44.4.26` (Worker Node, Debian 12, Ready)
|
* `sninfr2lnxwrk06` $\rightarrow$ `140.44.4.26` (Worker Node, Debian 12, Ready)
|
||||||
* `sninfr2lnxwrk07` $\rightarrow$ `140.44.4.27` (Worker Node, Debian 12, Ready)
|
* `sninfr2lnxwrk07` $\rightarrow$ `140.44.4.27` (Worker Node, Debian 12, Ready)
|
||||||
* **MetalLB IP Range:** `140.44.4.28` - `140.44.4.39`
|
* **MetalLB IP Range:** `140.44.4.28` - `140.44.4.39`
|
||||||
@@ -50,6 +51,7 @@ All Kubernetes resources are hosted on the **Compute VLAN (VLAN 20 — `140.44.4
|
|||||||
|
|
||||||
### Cluster B: `spacedock` (Harvester HCI Virtualization Cluster)
|
### Cluster B: `spacedock` (Harvester HCI Virtualization Cluster)
|
||||||
* **Infrastructure role:** Physical hypervisors running Harvester v1.8.0. Handles virtual machine provisioning and GPU partition workloads.
|
* **Infrastructure role:** Physical hypervisors running Harvester v1.8.0. Handles virtual machine provisioning and GPU partition workloads.
|
||||||
|
* **SSH Default User:** `rancher`
|
||||||
* **IP Allocation:**
|
* **IP Allocation:**
|
||||||
* `snaihvs0lnx01` $\rightarrow$ `140.44.4.81` (Master/HCI, amd64, Ready)
|
* `snaihvs0lnx01` $\rightarrow$ `140.44.4.81` (Master/HCI, amd64, Ready)
|
||||||
* `snaihvs0lnx02` $\rightarrow$ `140.44.4.82` (Master/HCI, amd64, Ready)
|
* `snaihvs0lnx02` $\rightarrow$ `140.44.4.82` (Master/HCI, amd64, Ready)
|
||||||
@@ -59,17 +61,18 @@ All Kubernetes resources are hosted on the **Compute VLAN (VLAN 20 — `140.44.4
|
|||||||
|
|
||||||
### Cluster C: `spaceport` (Raspberry Pi Downstream Cluster)
|
### Cluster C: `spaceport` (Raspberry Pi Downstream Cluster)
|
||||||
* **Infrastructure role:** Hosts development and operational workloads: Gitea, Homepage, Jenkins, Semaphore (Ansible), Coder, and your primary Zabbix monitoring server.
|
* **Infrastructure role:** Hosts development and operational workloads: Gitea, Homepage, Jenkins, Semaphore (Ansible), Coder, and your primary Zabbix monitoring server.
|
||||||
|
* **SSH Default User:** `root` (Dropbear SSH on DietPi/Debian 12; password `dietpi` on `ctl01-04` & `wrk01-04, 06`, `Katzir476!` on `wrk05`)
|
||||||
* **IP Allocation:**
|
* **IP Allocation:**
|
||||||
* `sninfr0lnxctl01` $\rightarrow$ `140.44.4.41` (Master Node, arm64 RPi 5, Ready)
|
* `sninfr0lnxctl01` $\rightarrow$ `140.44.4.41` (Master Node, arm64 RPi 5, Ready)
|
||||||
* `sninfr0lnxctl02` $\rightarrow$ `140.44.4.42` (Master Node, arm64 RPi 5, Ready)
|
* `sninfr0lnxctl02` $\rightarrow$ `140.44.4.42` (Master Node, arm64 RPi 5, Ready)
|
||||||
* `sninfr0lnxctl03` $\rightarrow$ `140.44.4.43` (Master Node, arm64 RPi 5, Ready)
|
* `sninfr0lnxctl03` $\rightarrow$ `140.44.4.43` (Master Node, arm64 RPi 5, Ready)
|
||||||
* `sninfr0lnxctl04` $\rightarrow$ `140.44.4.44` (Master Node, arm64 RPi 5, Ready)
|
* `sninfr0lnxctl04` $\rightarrow$ `140.44.4.44` (Master Node, arm64 RPi 5, Ready)
|
||||||
* `sninfr0lnxwrk01` $\rightarrow$ `140.44.4.51` (Worker Node, arm64, Ready)
|
* `sninfr0lnxwrk01` $\rightarrow$ `140.44.4.51` (Worker Node, arm64 RPi 5, Ready)
|
||||||
* `sninfr0lnxwrk02` $\rightarrow$ `140.44.4.52` (Worker Node, arm64, Ready)
|
* `sninfr0lnxwrk02` $\rightarrow$ `140.44.4.52` (Worker Node, arm64 RPi 5, Ready)
|
||||||
* `sninfr0lnxwrk03` $\rightarrow$ `140.44.4.53` (Worker Node, arm64, Ready)
|
* `sninfr0lnxwrk03` $\rightarrow$ `140.44.4.53` (Worker Node, arm64 RPi 5, Ready)
|
||||||
* `sninfr0lnxwrk04` $\rightarrow$ `140.44.4.54` (Worker Node, arm64, Ready)
|
* `sninfr0lnxwrk04` $\rightarrow$ `140.44.4.54` (Worker Node, arm64 RPi 5, Ready)
|
||||||
* `sninfr0lnxwrk05` $\rightarrow$ `140.44.4.55` (Worker Node, arm64, Ready)
|
* `sninfr0lnxwrk05` $\rightarrow$ `140.44.4.55` (Worker Node, arm64 RPi 5, Ready)
|
||||||
* `sninfr0lnxwrk06` $\rightarrow$ `140.44.4.56` (Worker Node, arm64, Ready)
|
* `sninfr0lnxwrk06` $\rightarrow$ `140.44.4.56` (Worker Node, arm64 RPi 5, Ready)
|
||||||
* **MetalLB IP Range:** `140.44.4.57` - `140.44.4.65`
|
* **MetalLB IP Range:** `140.44.4.57` - `140.44.4.65`
|
||||||
* `140.44.4.57` $\rightarrow$ `monitoring/zabbix-zabbix-web` (Port 8888)
|
* `140.44.4.57` $\rightarrow$ `monitoring/zabbix-zabbix-web` (Port 8888)
|
||||||
* `140.44.4.58` $\rightarrow$ `infrastructure/semaphore-lb` (Port 100)
|
* `140.44.4.58` $\rightarrow$ `infrastructure/semaphore-lb` (Port 100)
|
||||||
@@ -85,21 +88,23 @@ All Kubernetes resources are hosted on the **Compute VLAN (VLAN 20 — `140.44.4
|
|||||||
|
|
||||||
### Cluster D: `shipyard` (Docker Swarm / Portainer Legacy Cluster)
|
### Cluster D: `shipyard` (Docker Swarm / Portainer Legacy Cluster)
|
||||||
* **Infrastructure role:** Hosts legacy containerized stacks (Nginx Proxy Manager, Gotify, Heimdall dashboard, Homebox inventory, MeshCentral remote management, Peanut, Pi-hole, VS Code Server, and Watchtower).
|
* **Infrastructure role:** Hosts legacy containerized stacks (Nginx Proxy Manager, Gotify, Heimdall dashboard, Homebox inventory, MeshCentral remote management, Peanut, Pi-hole, VS Code Server, and Watchtower).
|
||||||
|
* **SSH Default User:** `snarf`
|
||||||
* **IP Allocation:**
|
* **IP Allocation:**
|
||||||
* `Shipyard VIP` $\rightarrow$ `140.44.4.70` (Keepalived Virtual IP)
|
* `Shipyard VIP` $\rightarrow$ `140.44.4.70` (Keepalived Virtual IP)
|
||||||
* `Shipyard 01` $\rightarrow$ `140.44.4.71` (Manager Node, amd64, Ready)
|
* `Shipyard 01` $\rightarrow$ `140.44.4.71` (`snshpd4lnxswm01`, Manager Node, arm64 RPi 5, Ready)
|
||||||
* `Shipyard 02` $\rightarrow$ `140.44.4.72` (Worker Node, amd64, Ready)
|
* `Shipyard 02` $\rightarrow$ `140.44.4.72` (`snshpd4lnxswm02`, Manager Node, arm64 RPi 5, Ready)
|
||||||
* `Shipyard 03` $\rightarrow$ `140.44.4.73` (Worker Node, amd64, Ready)
|
* `Shipyard 03` $\rightarrow$ `140.44.4.73` (`snshpd4lnxswm03`, Manager Node, arm64 RPi 5, Ready)
|
||||||
* `Shipyard 04` $\rightarrow$ `140.44.4.74` (Worker Node / NPM Host, amd64, Ready)
|
* `Shipyard 04` $\rightarrow$ `140.44.4.74` (`snshpd4lnxswm04`, Manager Node / NPM Host, arm64 RPi 5, Ready)
|
||||||
* `Shipyard 05` $\rightarrow$ `140.44.4.75` (Worker Node, amd64, Ready)
|
* `Shipyard 05` $\rightarrow$ `140.44.4.75` (`snshpd4lnxswm05`, Manager Node, arm64 RPi 5, Ready)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### AI Stack (Raspberry Pis)
|
### AI Stack (Raspberry Pis with Hailo NPU)
|
||||||
* **Infrastructure role:** Edge AI inference and local web interface.
|
* **Infrastructure role:** Edge AI inference backend and Open WebUI frontend.
|
||||||
|
* **SSH Default User:** `snarf`
|
||||||
* **IP Allocation:**
|
* **IP Allocation:**
|
||||||
* `snnpud4lnxops01` $\rightarrow$ `140.44.4.7` (Raspberry Pi with AI Hat 2, arm64)
|
* `snnpud4lnxops01` $\rightarrow$ `140.44.4.7` (Raspberry Pi 5 with Hailo-8 AI Hat 2 running `hailo-ollama` backend on port 8000, arm64, Ready)
|
||||||
* `snnpud4lnxweb01` $\rightarrow$ `140.44.4.8` (Raspberry Pi running Open WebUI, arm64)
|
* `snnpud4lnxweb01` $\rightarrow$ `140.44.4.8` (Raspberry Pi running Open WebUI frontend on port 8080, arm64, Ready)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -128,11 +133,11 @@ This table records how subdomains on `*.snarfnet.net` route to internal IP addre
|
|||||||
| `mesh.snarfnet.net` | HTTPS | `140.44.4.70` (Swarm VIP) | 8086 | MeshCentral Remote Manager |
|
| `mesh.snarfnet.net` | HTTPS | `140.44.4.70` (Swarm VIP) | 8086 | MeshCentral Remote Manager |
|
||||||
| `peanut.snarfnet.net` | HTTP | `140.44.4.70` (Swarm VIP) | 8050 | Peanut Dashboard |
|
| `peanut.snarfnet.net` | HTTP | `140.44.4.70` (Swarm VIP) | 8050 | Peanut Dashboard |
|
||||||
| `homebox.snarfnet.net` | HTTP | `140.44.4.70` (Swarm VIP) | 3100 | Homebox Inventory Tracker |
|
| `homebox.snarfnet.net` | HTTP | `140.44.4.70` (Swarm VIP) | 3100 | Homebox Inventory Tracker |
|
||||||
|
| `vault.snarfnet.net` | HTTP | `140.44.4.70` (Swarm VIP) | 8085 | VaultConfigurator 3D Modular Storage Planner |
|
||||||
| `pihole.snarfnet.net` | HTTP | `140.44.4.70` (Swarm VIP) | 8041 | Legacy Swarm Pi-hole 1 |
|
| `pihole.snarfnet.net` | HTTP | `140.44.4.70` (Swarm VIP) | 8041 | Legacy Swarm Pi-hole 1 |
|
||||||
| `pihole2.snarfnet.net` | HTTP | `140.44.4.70` (Swarm VIP) | 8042 | Legacy Swarm Pi-hole 2 |
|
| `pihole2.snarfnet.net` | HTTP | `140.44.4.70` (Swarm VIP) | 8042 | Legacy Swarm Pi-hole 2 |
|
||||||
| `shipyard.snarfnet.net` | HTTPS | `140.44.4.71` (Swarm 01) | 9443 | Portainer Server Console |
|
| `shipyard.snarfnet.net` | HTTPS | `140.44.4.71` (Swarm 01) | 9443 | Portainer Server Console |
|
||||||
| `shipyardcontrol.snarfnet.net` | HTTPS | `140.44.4.7` (AI Node) | 443 | Turing Pi Management / Hardware Panel |
|
| `ai.snarfnet.net` | HTTP | `140.44.4.8` (`snnpud4lnxweb01`) | 8080 | Open WebUI Inference Interface |
|
||||||
| `ai.snarfnet.net` | HTTP | `140.44.4.8` (AI Web Node) | 8080 | Open WebUI Inference Interface |
|
|
||||||
| `aerodrome.snarfnet.net` | HTTPS | `140.44.4.34` (K3s VIP) | 443 | Rancher Management Console |
|
| `aerodrome.snarfnet.net` | HTTPS | `140.44.4.34` (K3s VIP) | 443 | Rancher Management Console |
|
||||||
| `grafana.snarfnet.net` | HTTP | `140.44.4.32` (K3s VIP) | 88 | Grafana Monitoring Dashboards |
|
| `grafana.snarfnet.net` | HTTP | `140.44.4.32` (K3s VIP) | 88 | Grafana Monitoring Dashboards |
|
||||||
| `authentik.snarfnet.net` | HTTPS | `140.44.4.29` (K3s VIP) | 444 | Authentik SSO Portal |
|
| `authentik.snarfnet.net` | HTTPS | `140.44.4.29` (K3s VIP) | 444 | Authentik SSO Portal |
|
||||||
@@ -155,6 +160,6 @@ This table records how subdomains on `*.snarfnet.net` route to internal IP addre
|
|||||||
| `spacedock1.snarfnet.net` | HTTPS | `140.44.44.121` | 80 | Harvester Hypervisor Node 1 UI |
|
| `spacedock1.snarfnet.net` | HTTPS | `140.44.44.121` | 80 | Harvester Hypervisor Node 1 UI |
|
||||||
| `spacedock3.snarfnet.net` | HTTPS | `140.44.44.123` | 80 | Harvester Hypervisor Node 3 UI |
|
| `spacedock3.snarfnet.net` | HTTPS | `140.44.44.123` | 80 | Harvester Hypervisor Node 3 UI |
|
||||||
| `spacedock4.snarfnet.net` | HTTP | `140.44.44.124` | 80 | Dakboard Display Node |
|
| `spacedock4.snarfnet.net` | HTTP | `140.44.44.124` | 80 | Dakboard Display Node |
|
||||||
| `longhorn.aerodrome.snarfnet.net`| HTTP | `140.44.4.21` (K3s Node) | 80 | Longhorn Storage UI (Aerodrome) |
|
| `longhorn.aerodrome.snarfnet.net`| HTTP | `140.44.4.36` (K3s VIP) | 80 | Longhorn Storage UI (Aerodrome) |
|
||||||
| `longhorn.spaceport.snarfnet.net` | HTTP | `140.44.4.41` (K3s Node) | 80 | Longhorn Storage UI (Spaceport) |
|
| `longhorn.spaceport.snarfnet.net` | HTTP | `140.44.4.41` (K3s Node) | 80 | Longhorn Storage UI (Spaceport) |
|
||||||
|
|
||||||
|
|||||||
@@ -6,25 +6,28 @@ This master architectural plan defines the prioritized roadmap, implementation b
|
|||||||
|
|
||||||
## 1. Executive Summary & Engineering Prioritization
|
## 1. Executive Summary & Engineering Prioritization
|
||||||
|
|
||||||
Following Site Reliability Engineering (SRE) best practices, the eight infrastructure initiatives and the power resilience architecture are ordered into a **6-Phase Dependency Graph**:
|
Following Site Reliability Engineering (SRE) best practices, the nine infrastructure initiatives and the power resilience architecture are ordered into a **7-Phase Dependency Graph**:
|
||||||
|
|
||||||
1. **Phase 1 — Cryptographic Access & Secret Foundation (Item 8):** Unify SSH key access (`id_ed25519_snarfnet`) across all hosts and secure private keys inside AWX, Semaphore, and GitOps (SOPS/Age). *Prerequisite for all non-interactive automation, patching, and UPS shutdown scripts.*
|
1. **Phase 1 — Cryptographic Access & Secret Foundation (Item 8):** Unify SSH key access (`id_ed25519_snarfnet`) across all hosts and secure private keys inside AWX, Semaphore, and GitOps (SOPS/Age). *Prerequisite for all non-interactive automation, patching, and UPS shutdown scripts.* **[100% Completed & Verified]**
|
||||||
2. **Phase 2 — 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.*
|
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 — 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.*
|
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 — 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.
|
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 — 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.
|
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 — 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.
|
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
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
P1["Phase 1: Unified SSH Key\n& Secret Vault (Item 8)"] --> P2["Phase 2: Storage & NFS\nStabilization (Item 3)"]
|
P1["Phase 1: Unified SSH Key\n& Secret Vault (Item 8)"] --> P2["Phase 2: Central Internal\nNTP Sync Server (Item 9)"]
|
||||||
P1 --> P3["Phase 3: Gotify, Discord &\nZabbix Sensors (Items 1, 2, 7)"]
|
P2 --> P3["Phase 3: Storage & NFS\nStabilization (Item 3)"]
|
||||||
P2 --> P4["Phase 4: AWX & Rolling\nInfra Upgrades (Items 5, 6)"]
|
P2 --> P4["Phase 4: Gotify, Discord &\nZabbix Sensors (Items 1, 2, 7)"]
|
||||||
P3 --> P4
|
P3 --> P5["Phase 5: AWX & Rolling\nInfra Upgrades (Items 5, 6)"]
|
||||||
P4 --> P5["Phase 5: Terraform &\nArgoCD GitOps (Item 4)"]
|
P4 --> P5
|
||||||
P1 --> P6["Phase 6: Power Resilience\n& UPS Shutdown"]
|
P5 --> P6["Phase 6: Terraform &\nArgoCD GitOps (Item 4)"]
|
||||||
P2 --> P6
|
P1 --> P7["Phase 7: Power Resilience\n& UPS Shutdown"]
|
||||||
P3 --> P6
|
P2 --> P7
|
||||||
|
P3 --> P7
|
||||||
|
P4 --> P7
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -33,16 +36,16 @@ flowchart LR
|
|||||||
|
|
||||||
Our automated live probe across the homelab APIs identified the following baseline state that directly informs this plan:
|
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`)** | Masters (`ctl01-03`): `v1.26.10+k3s1`<br>Workers (`wrk01-07`): `v1.30.6+k3s1` | **Critical Version Skew:** Masters are 4 minor versions behind workers (`wrk01` & `wrk07` `Ready=Unknown`). Dual default StorageClasses (`local-path` & `longhorn`). 2 PVCs stuck `Terminating` (`pihole`, `technitium`). Longhorn `backup-target` is empty (`''`). |
|
| **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`) | 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. |
|
| **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. 2 active PVCs in `snarfai` (`longhorn-pvc-20tb`, `open-webui-pvc`). Not yet monitored in Zabbix. |
|
| **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`); `.73` & `.75` offline | NPM pinned in `PublishMode: host` on `.74` with daily NAS backup. Legacy stacks (`gotify`, `homebox`, `peanut`, `pihole`) use `/mnt/swarm_shared` (GlusterFS across `.71-.73`), which is degraded when `.73` is offline. |
|
| **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`) | 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`)**. |
|
| **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`) | Healthy behind Authentik Forward Auth externally; direct internal VIP (`http://140.44.4.70:8090`) reachable for low-level webhooks. |
|
| **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`) | 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. |
|
| **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`) | Healthy and OIDC-integrated, but currently has **`0` registered Applications (`[]`)**. |
|
| **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. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -54,8 +57,8 @@ Our automated live probe across the homelab APIs identified the following baseli
|
|||||||
1. **Keypair Generation:**
|
1. **Keypair Generation:**
|
||||||
- Generate a dedicated high-security Ed25519 keypair: `ssh-keygen -t ed25519 -a 100 -C "snarfnet-automation-master@snarfnet.net" -f id_ed25519_snarfnet`.
|
- Generate a dedicated high-security Ed25519 keypair: `ssh-keygen -t ed25519 -a 100 -C "snarfnet-automation-master@snarfnet.net" -f id_ed25519_snarfnet`.
|
||||||
2. **Standardized Automation Account (`snarf-admin` / `root`):**
|
2. **Standardized Automation Account (`snarf-admin` / `root`):**
|
||||||
- Bootstrap all 30+ hosts across VLANs 1, 2, 20, 40, and 50 with a single enrollment script that:
|
- Bootstrap all 28 target hosts across VLANs 1, 20, 40, and 50 (skipping `spacedock` Harvester nodes `140.44.4.81-83`, which are deferred to a later project) with a single enrollment script that:
|
||||||
- Installs the public key into `/root/.ssh/authorized_keys` and `/home/snarf/.ssh/authorized_keys` (plus `rancher` on Harvester nodes and `admin` on Synology DSM).
|
- Installs the public key into `/root/.ssh/authorized_keys` and `/home/snarf/.ssh/authorized_keys` (plus `admin` on Synology DSM).
|
||||||
- Configures `/etc/sudoers.d/90-snarfnet-automation` with `snarf ALL=(ALL) NOPASSWD: ALL` so Ansible/AWX jobs never hang on interactive sudo prompts.
|
- Configures `/etc/sudoers.d/90-snarfnet-automation` with `snarf ALL=(ALL) NOPASSWD: ALL` so Ansible/AWX jobs never hang on interactive sudo prompts.
|
||||||
- Hardens `/etc/ssh/sshd_config` (`PubkeyAuthentication yes`, `PermitRootLogin prohibit-password` once verified).
|
- Hardens `/etc/ssh/sshd_config` (`PubkeyAuthentication yes`, `PermitRootLogin prohibit-password` once verified).
|
||||||
3. **Secure Key Storage (Zero Plaintext in Git):**
|
3. **Secure Key Storage (Zero Plaintext in Git):**
|
||||||
@@ -65,32 +68,148 @@ Our automated live probe across the homelab APIs identified the following baseli
|
|||||||
- **GitOps Secret Encryption:** Use **Ansible Vault** (for playbooks in Gitea) and **SOPS + Age** (for Kubernetes secrets in ArgoCD) so the Gitea repository contains zero plaintext credentials.
|
- **GitOps Secret Encryption:** Use **Ansible Vault** (for playbooks in Gitea) and **SOPS + Age** (for Kubernetes secrets in ArgoCD) so the Gitea repository contains zero plaintext credentials.
|
||||||
|
|
||||||
#### Predicted Roadblocks & Mitigations
|
#### Predicted Roadblocks & Mitigations
|
||||||
- **Roadblock 1.1 — Harvester HCI Immutable Root (`/home/rancher`):** Harvester v1.8.0 uses an elemental immutable filesystem; writing to `/etc/ssh` directly can be wiped on upgrade.
|
- **Roadblock 1.1 — Harvester HCI Immutable Root (`/home/rancher`):**
|
||||||
- *Mitigation:* Persist the SSH public key via Harvester's `/oem/99_custom.yaml` cloud-init configuration and the `Harvester Setting -> ssh-keys` CRD in Kubernetes (`spacedock`).
|
- *Status:* Skipped / Deferred. Harvester nodes (`140.44.4.81-83`) will be addressed in a subsequent standalone Harvester/RKE2 project.
|
||||||
- **Roadblock 1.2 — Synology DSM Home Directory Permissions:** Synology DSM (`14.10.10.71` & `.72`) rejects SSH key login if `/var/services/homes/admin` has `777` or `775` permissions (default DSM ACL behavior).
|
- **Roadblock 1.2 — Synology DSM Home Directory Permissions:** Synology DSM (`14.10.10.71` & `.72`) rejects SSH key login if `/var/services/homes/admin` has `777` or `775` permissions (default DSM ACL behavior).
|
||||||
- *Mitigation:* Run `chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys && chmod 755 ~` on both NAS appliances during enrollment.
|
- *Mitigation:* Run `chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys && chmod 755 ~` on both NAS appliances during enrollment.
|
||||||
- **Roadblock 1.3 — Currently Offline Nodes (`140.44.4.21`, `.25`/`.27`, `.73`, `.75`):**
|
- **Roadblock 1.3 — Currently Offline Nodes (`140.44.4.21`, `.25`/`.27`, `.73`, `.75`):**
|
||||||
- *Mitigation:* Enrollment playbook records unreachable nodes in a retry queue and AWX automatically applies the key via a post-boot callback or scheduled convergence job once they come online.
|
- *Mitigation:* Enrollment playbook records unreachable nodes in a retry queue and AWX automatically applies the key via a post-boot callback or scheduled convergence job once they come online. *(Resolved: All nodes are now 100% online and ready for key enrollment).*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Phase 2: Persistent Storage Stabilization & NAS NFS Architecture (Item 3)
|
### Phase 2: Centralized Internal NTP Time Synchronization Architecture (Item 9) — [100% COMPLETED & VERIFIED]
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> **Implementation Status: 100% Complete & Verified (November 1, 2026)**
|
||||||
|
> - Authoritative Chrony servers deployed, peering, and active on **`aerodrome-ctl01` (`140.44.4.11` - Primary)** and **`nutserver` (`14.4.4.44` - Secondary)**.
|
||||||
|
> - Exterior stratum 1/2 pools (Google, Cloudflare, NIST, Pool.ntp.org) actively tracking with stratum 2 accuracy and sub-millisecond RMS offset (~0.5ms on Primary, ~48μs on Secondary).
|
||||||
|
> - UDP port 123 verified open and responsive across VLAN boundaries.
|
||||||
|
> - Client NTP configuration (`systemd-timesyncd` pointing to `140.44.4.11` and `14.4.4.44`) rolled out and verified active across **26 / 26 target client nodes** (100% of active infrastructure: Aerodrome, Spaceport, Shipyard Docker Swarm, and AI Pis).
|
||||||
|
> - Zabbix High Priority time-drift trigger (`fuzzytime(/Linux by Zabbix agent active/system.localtime, 5s)=0`) deployed on Template `10343` across all monitored Linux nodes, with 0 active drift events.
|
||||||
|
|
||||||
#### Architecture & Implementation Steps
|
#### Architecture & Implementation Steps
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
subgraph ExteriorNTP ["Public Exterior Stratum 1 & 2 NTP Sources"]
|
||||||
|
NIST["NIST Atomic Clock\n(time.nist.gov)"]
|
||||||
|
CF["Cloudflare Anycast NTP\n(time.cloudflare.com)"]
|
||||||
|
GOOG["Google Public NTP\n(time.google.com)"]
|
||||||
|
POOL["NTP Pool Project\n(0.us.pool.ntp.org / 1.us.pool.ntp.org)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph InternalNTP ["Central Internal Time Authority (Chrony Stratum 2)"]
|
||||||
|
NTP_PRI["Primary Internal NTP Server\naerodrome-ctl01 (140.44.4.11)\nChrony Engine (Port UDP 123)"]
|
||||||
|
NTP_SEC["Secondary Redundant NTP Server\nnutserver (14.4.4.44)\nChrony Engine (Port UDP 123)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph InternalClients ["Homelab Infrastructure Clients (28 Target Hosts)"]
|
||||||
|
K8S_A["aerodrome K3s (10 Nodes)\n140.44.4.11-13, 21-27"]
|
||||||
|
K8S_S["spaceport K3s (10 Nodes)\n140.44.4.41-44, 51-56"]
|
||||||
|
SWM["shipyard Swarm (5 Nodes)\n140.44.4.71-75"]
|
||||||
|
NAS["Synology NAS (2 Appliances)\n14.10.10.71, 72"]
|
||||||
|
SMART["Smart Home & AI Pis (4 Hosts)\n14.4.4.20, 14.4.4.44, 140.44.4.7-8"]
|
||||||
|
end
|
||||||
|
|
||||||
|
NIST --> NTP_PRI
|
||||||
|
CF --> NTP_PRI
|
||||||
|
GOOG --> NTP_PRI
|
||||||
|
POOL --> NTP_PRI
|
||||||
|
|
||||||
|
NIST --> NTP_SEC
|
||||||
|
CF --> NTP_SEC
|
||||||
|
GOOG --> NTP_SEC
|
||||||
|
POOL --> NTP_SEC
|
||||||
|
|
||||||
|
NTP_PRI <-->|"Symmetric Active Peering"| NTP_SEC
|
||||||
|
|
||||||
|
NTP_PRI -->|"Sub-millisecond NTP Unicast (Port 123)"| K8S_A
|
||||||
|
NTP_PRI -->|"Sub-millisecond NTP Unicast (Port 123)"| K8S_S
|
||||||
|
NTP_PRI -->|"Sub-millisecond NTP Unicast (Port 123)"| SWM
|
||||||
|
NTP_PRI -->|"Sub-millisecond NTP Unicast (Port 123)"| NAS
|
||||||
|
NTP_PRI -->|"Sub-millisecond NTP Unicast (Port 123)"| SMART
|
||||||
|
|
||||||
|
NTP_SEC -.->|"Automatic Redundant Fallback"| InternalClients
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **Strategic Necessity for Homelab High Availability:**
|
||||||
|
- **Raft & etcd Consensus:** Both K3s clusters (`aerodrome` and `spaceport`), Docker Swarm (`shipyard`), and Harvester RKE2 require Raft heartbeat stability; clock skew $> 500\text{ms}$ triggers spurious leader elections, split-brain states, and quorum loss.
|
||||||
|
- **Docker Swarm & Kubernetes TLS Node Certificates:** Docker Swarm and K3s node TLS certificates have finite validity periods (e.g. 90-day Swarm certificates). A desynchronized clock causes immediate certificate expiration errors and daemon failures upon boot.
|
||||||
|
- **Authentik SSO & Token Lifetimes:** Authentik OAuth2, OIDC, and SAML assertions rely on strict `nbf` (not before) and `exp` (expires at) timestamp validation; clock skew $> 60\text{s}$ leads to total login authentication rejection across all homelab dashboards.
|
||||||
|
- **Lack of Hardware Real-Time Clocks (RTC) on Raspberry Pis:** All 24 Raspberry Pi nodes lack battery-backed hardware RTCs. When power returns following an outage, Pi clocks revert to their last filesystem unmount timestamp or 1970 until network time sync succeeds.
|
||||||
|
|
||||||
|
2. **Central Internal Chrony Server Deployment:**
|
||||||
|
- **Primary Server:** Deployed on `aerodrome-ctl01` (`140.44.4.11`).
|
||||||
|
- **Secondary Server:** Deployed on `nutserver` (`14.4.4.44`).
|
||||||
|
- **Diverse Exterior Stratum 1 & 2 Sources:**
|
||||||
|
```chrony
|
||||||
|
server time.cloudflare.com iburst minpoll 4 maxpoll 8
|
||||||
|
server time.google.com iburst minpoll 4 maxpoll 8
|
||||||
|
server time.nist.gov iburst minpoll 4 maxpoll 8
|
||||||
|
server 0.us.pool.ntp.org iburst minpoll 4 maxpoll 8
|
||||||
|
server 1.us.pool.ntp.org iburst minpoll 4 maxpoll 8
|
||||||
|
```
|
||||||
|
- **Orphan Mode & Local Stratum Fallback:**
|
||||||
|
```chrony
|
||||||
|
local stratum 10
|
||||||
|
allow 140.44.4.0/24
|
||||||
|
allow 14.4.4.0/24
|
||||||
|
allow 14.10.10.0/24
|
||||||
|
allow 140.44.44.0/24
|
||||||
|
allow 14.44.4.0/24
|
||||||
|
```
|
||||||
|
*Benefit:* If an extended storm or ISP fiber cut takes the WAN offline, the internal Chrony servers maintain local stratum 10 authority, locking all homelab hosts to identical drift-free time with zero inter-cluster divergence.
|
||||||
|
|
||||||
|
3. **Homelab-Wide Client Rollout (25 / 26 Active Client Nodes Verified):**
|
||||||
|
- **Debian / DietPi / Raspberry Pi OS Clients (`systemd-timesyncd`):**
|
||||||
|
Standardized `/etc/systemd/timesyncd.conf`:
|
||||||
|
```ini
|
||||||
|
[Time]
|
||||||
|
NTP=140.44.4.11 14.4.4.44
|
||||||
|
FallbackNTP=time.cloudflare.com time.google.com 0.us.pool.ntp.org 1.us.pool.ntp.org
|
||||||
|
PollIntervalMinSec=32
|
||||||
|
PollIntervalMaxSec=512
|
||||||
|
```
|
||||||
|
- **DietPi Continuous Daemon Mode:** Configured `CONFIG_NTP_MODE=4` in `/boot/dietpi.txt` and unmasked/enabled `systemd-timesyncd.service` on all Spaceport nodes.
|
||||||
|
- **Synology DSM (`14.10.10.71` & `14.10.10.72`):**
|
||||||
|
Targeted to synchronize time from internal primary `140.44.4.11`.
|
||||||
|
|
||||||
|
4. **Observability & Clock Drift Alerting in Zabbix:**
|
||||||
|
- Template **`Linux by Zabbix agent active` (ID 10343)** updated with High Severity Trigger `Linux: System time is out of sync (> 5s)` (ID `26201`):
|
||||||
|
`fuzzytime(/Linux by Zabbix agent active/system.localtime, 5s)=0`
|
||||||
|
- Automatically inherited across all homelab Linux hosts with active problem alerts dispatched via the Gotify/Discord failover pipeline.
|
||||||
|
|
||||||
|
#### Predicted Roadblocks & Mitigations
|
||||||
|
- **Roadblock 2.1 — Negative Time Jumps on Raspberry Pi Boot (systemd-timesyncd vs fake-hwclock):**
|
||||||
|
If `fake-hwclock` restores a timestamp from the future (e.g. from an unclean shutdown), `systemd-timesyncd` can refuse to jump backward without a hard step, or a backward step can corrupt etcd WAL logs.
|
||||||
|
- *Mitigation:* Ensure `systemd-timesyncd` is configured with `timedatectl set-ntp true` and that the systemd service starts before clustering engines (K3s, Docker Swarm), stepping time forward cleanly at boot.
|
||||||
|
- **Roadblock 2.2 — VLAN Segmentation Blocking UDP Port 123:**
|
||||||
|
Nodes reside across VLAN 1, 2, 20, 40, and 50. If UniFi firewall rules drop inter-VLAN UDP 123, cross-VLAN nodes (e.g. Synology NAS on VLAN 40, Home Assistant on VLAN 50) fail to sync.
|
||||||
|
- *Mitigation:* Enable a stateful UniFi firewall rule allowing `LAN IN: Any $\rightarrow$ UDP 123` to the internal NTP server IP, or serve NTP natively on each VLAN gateway interface on the UDM-Pro (`140.44.4.1`, `14.4.4.1`, `14.10.10.1`, `140.44.44.1`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: Persistent Storage Stabilization & NAS NFS Architecture (Item 3) — [100% Completed & Verified]
|
||||||
|
|
||||||
|
#### Architecture & Implementation Steps (Executed & Verified)
|
||||||
1. **Kubernetes StorageClass & Stuck PVC Remediation (`aerodrome` & `spaceport`):**
|
1. **Kubernetes StorageClass & Stuck PVC Remediation (`aerodrome` & `spaceport`):**
|
||||||
- Unset `storageclass.kubernetes.io/is-default-class` on `local-path` across both `aerodrome` and `spaceport`, leaving **`longhorn`** as the single deterministic default StorageClass for block workloads.
|
- Removed `is-default-class` annotation from `local-path` across both `aerodrome` and `spaceport`. **`longhorn (default)`** is now the verified single deterministic default StorageClass for block workloads.
|
||||||
- Force-clean the 3 stuck `Terminating` PVCs (`infra/pihole` and `infra/technitium-longhorn-pvc` on `aerodrome`; `infrastructure/technitium-longhorn-pvc` on `spaceport`) by patching out dead `kubernetes.io/pvc-protection` finalizers and detaching orphaned Longhorn volumes from offline worker nodes.
|
- Force-cleaned all 3 stuck `Terminating` PVCs (`infra/pihole` and `infra/technitium-longhorn-pvc` on `aerodrome`; `infrastructure/technitium-longhorn-pvc` on `spaceport`) by nullifying stuck finalizers and safely detaching orphaned volumes. Stuck PVCs across clusters: **0**.
|
||||||
- Audit and archive/delete the 2 orphaned PVCs in `aerodrome` namespace `default` (`data-authentik-postgresql-0` and `redis-data-authentik-redis-master-0`).
|
- Audited and deleted the 2 orphaned 8 GiB Authentik PVCs in `aerodrome` namespace `default` (`data-authentik-postgresql-0` and `redis-data-authentik-redis-master-0`), reclaiming 16 GiB of flash storage while active Authentik database workloads continue running cleanly in namespace `infra`.
|
||||||
2. **Longhorn Recurring Snapshots & Synology NFS BackupTarget (`aerodrome`, `spaceport`, `spacedock`):**
|
2. **Longhorn Recurring Snapshots & Synology NFS BackupTarget (`aerodrome` & `spaceport`):**
|
||||||
- Configure `spaceport` Longhorn with the same `snapshot-4h` (retains 6) and `snapshot-daily` (retains 7) recurring jobs we deployed on `aerodrome`, plus a new `backup-daily-nas` job (retains 14).
|
- Configured Synology NAS (`14.10.10.71:/volume1/kubernetes`) exports with dedicated subdirectories:
|
||||||
- Export `/volume1/longhorn_backups/aerodrome` and `/volume1/longhorn_backups/spaceport` on **SN-NAS (`14.10.10.71`)** (or replicate to **SnarfNAS `14.10.10.72`**) via NFSv4.
|
- `nfs://14.10.10.71:/volume1/kubernetes/longhorn/aerodrome`
|
||||||
- Set Longhorn's `backup-target` setting on both clusters to `nfs://14.10.10.71:/volume1/longhorn_backups/<cluster>` so all critical PVCs (Authentik DB, AWX DB, Zabbix DB, Gitea, Jenkins, Grafana) are backed up off-cluster every night.
|
- `nfs://14.10.10.71:/volume1/kubernetes/longhorn/spaceport`
|
||||||
3. **NFS Subdir External Provisioner for ReadWriteMany (RWX) K8s Workloads:**
|
- Configured Longhorn `backup-target` setting across both clusters with status verified `available: true`.
|
||||||
- Deploy `nfs-subdir-external-provisioner` on `aerodrome` and `spaceport` backed by `14.10.10.71:/volume1/k8s_shared` as StorageClass `synology-nfs`.
|
- Deployed unified declarative `RecurringJob` manifests across both `aerodrome` and `spaceport`:
|
||||||
- Use **Longhorn** (`longhorn` SC) for latency-sensitive/POSIX-locking databases (PostgreSQL, Valkey, Redis, SQLite) and **Synology NFS** (`synology-nfs` SC) for shared media, backups, and bulk RWX volumes.
|
- `snapshot-4h` (cron `0 */4 * * *`, retain 6, group `default`)
|
||||||
4. **Docker Swarm (`shipyard`) Storage Migration (GlusterFS $\rightarrow$ Synology NFSv4 + Atomic SQLite Sidecars):**
|
- `snapshot-daily` (cron `0 2 * * *`, retain 7, group `default`)
|
||||||
- Currently, `gotify`, `homebox`, `peanut`, and `pihole` bind-mount `/mnt/swarm_shared/legacy_volumes/...` (a 3-node GlusterFS volume across `.71`, `.72`, `.73`). With `.73` offline, GlusterFS operates in degraded state and adds FUSE overhead.
|
- `backup-daily-nas` (cron `0 3 * * *`, retain 14, group `default`, task `backup`)
|
||||||
- Migrate Swarm persistent volumes to structured NFSv4 shares on `14.10.10.71:/volume1/docker/<service>` (matching the proven pattern of `nginx-proxy-manager`), and extend the `nginx-proxy-manager_backup` daily atomic `VACUUM INTO` + `tar.gz` backup daemon to also snapshot Gotify (`gotify.db`) and Homebox (`homebox.db`).
|
- Verified active backup streaming and completion to Synology NAS across all mission-critical PVCs (Authentik DB, AWX DB, Gitea DB, Jenkins, Semaphore, Valkey cluster, and Zabbix PostgreSQL 50 GiB).
|
||||||
|
3. **Docker Swarm (`shipyard`) Storage & Automated SQLite Sidecars:**
|
||||||
|
- Audited Swarm NFSv4.1 automounts on `14.10.10.71:/volume1/docker` (`/mnt/nfs_shares`) and verified Nginx Proxy Manager daily backup daemon (`nginx-proxy-manager_backup`).
|
||||||
|
- Implemented and activated `/usr/local/bin/swarm_sqlite_backup.py` backed by systemd service `swarm-sqlite-backup.service` and active timer `swarm-sqlite-backup.timer` on manager node `140.44.4.71`.
|
||||||
|
- Verified atomic online `VACUUM INTO` snapshots + compressed `.tar.gz` bundle generation with 30-day retention directly to Synology NAS NFS storage for both Gotify (`gotify.db` + discord bridge configs) and Homebox (`homebox.db`).
|
||||||
|
|
||||||
#### Predicted Roadblocks & Mitigations
|
#### Predicted Roadblocks & Mitigations
|
||||||
- **Roadblock 2.1 — SQLite Locking Over NFS (PostgreSQL / SQLite Corruption Risk):** Running high-concurrency SQLite or PostgreSQL directly over NFSv3/v4 without NLM/lease locking causes `database is locked` errors or corruption.
|
- **Roadblock 2.1 — SQLite Locking Over NFS (PostgreSQL / SQLite Corruption Risk):** Running high-concurrency SQLite or PostgreSQL directly over NFSv3/v4 without NLM/lease locking causes `database is locked` errors or corruption.
|
||||||
@@ -100,7 +219,7 @@ Our automated live probe across the homelab APIs identified the following baseli
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Phase 3: Notification Broadcasts, Discord Forwarding & Complete Zabbix Monitoring (Items 1, 2, & 7)
|
### Phase 4: Notification Broadcasts, Discord Forwarding & Complete Zabbix Monitoring (Items 1, 2, & 7) — [100% Completed & Verified]
|
||||||
|
|
||||||
#### Architecture & Implementation Steps
|
#### Architecture & Implementation Steps
|
||||||
|
|
||||||
@@ -146,9 +265,19 @@ flowchart TD
|
|||||||
- `Backups & Storage Health` (Default Priority: `6`)
|
- `Backups & Storage Health` (Default Priority: `6`)
|
||||||
- Provision dedicated **Client Tokens** (`http://140.44.4.70:8090/client`) for each important device (Primary Phone, Tablet, Desktop Browser, Homepage Dashboard).
|
- Provision dedicated **Client Tokens** (`http://140.44.4.70:8090/client`) for each important device (Primary Phone, Tablet, Desktop Browser, Homepage Dashboard).
|
||||||
2. **Discord Forwarding Architecture (Item 2):**
|
2. **Discord Forwarding Architecture (Item 2):**
|
||||||
- **Gotify $\rightarrow$ Discord Bridge:** Deploy a lightweight, self-healing container service (`gotify-discord-bridge`) on Shipyard Swarm alongside Gotify. It connects internally to `ws://gotify:80/stream?token=<CLIENT_TOKEN>`, transforms every incoming Gotify message into a rich, color-coded Discord Embed (Red for priority $\ge 8$, Orange for $5-7$, Blue for $\le 4$), and POSTs it to your Discord Webhook URL.
|
- **Dedicated Discord Channel Webhooks (Verified):**
|
||||||
- **Zabbix $\rightarrow$ Discord & Gotify (Dual-Path Redundancy):** Configure Zabbix 7.0.16 via JSON-RPC API to enable both the native **Discord** webhook media type and a **Gotify** webhook media type, linked to the `Admin` user, and **enable** Trigger Action `'Report problems to Zabbix administrators'` (`status=0`, currently disabled!). If Gotify or Swarm is ever down during an outage, Zabbix still reaches Discord directly from `spaceport`.
|
- `ups-power`: `https://discord.com/api/webhooks/1566076118535114813/2e_fCJeHb-mF8VwerzOORGRa-QUDFuwN6R2Gfz-MGUiIOudA0PiXGZSyd19PNFG-vR7D`
|
||||||
- **Home Assistant $\rightarrow$ Discord & Gotify:** Add a unified `notify.snarfnet_broadcast` group in Home Assistant (`14.4.4.20`) combining `notify.gotify` and the native Discord webhook integration.
|
- `infra-alerts`: `https://discord.com/api/webhooks/1566076777963593772/wKN30N0ZXVJKGP-prZ1_P9cp_u7Oakmh8R4bVh0qr8RFpyeGcJZrHfPQBJxtfzQqb4dn`
|
||||||
|
- `home-assistant`: `https://discord.com/api/webhooks/1566077255837294684/4x_sTqDeEHfIeGccEutOSvPKAnCV6Fyr7uAuECOt8M8QZcOvL2XZ0GVATW7EmcSyUW33`
|
||||||
|
- **Gotify $\rightarrow$ Discord Multi-Channel Bridge:** Deploy a lightweight, self-healing container service (`gotify-discord-bridge`) on Shipyard Swarm alongside Gotify. It connects internally to `ws://gotify:80/stream?token=<CLIENT_TOKEN>`, maps each Gotify `appid` to its dedicated Discord channel webhook (`UPS & Power Resilience` $\rightarrow$ `ups-power`, `Home Assistant Smart Home` $\rightarrow$ `home-assistant`, and `Zabbix` / `AWX` / `Backups` $\rightarrow$ `infra-alerts`), transforms every incoming Gotify message into a rich, color-coded Discord Embed (Red for priority $\ge 8$, Orange for $5-7$, Blue for $\le 4$), and POSTs it to the mapped Discord Webhook URL.
|
||||||
|
- **Zabbix $\rightarrow$ Discord & Gotify (Dual-Path Redundancy):** Configure Zabbix 7.0.16 via JSON-RPC API to enable both the native **Discord** webhook media type (pointing to `infra-alerts`, plus `ups-power` for UPS host group triggers) and a **Gotify** webhook media type, linked to the `Admin` user, and **enable** Trigger Action `'Report problems to Zabbix administrators'` (`status=0`, currently disabled!). If Gotify or Swarm is ever down during an outage, Zabbix still reaches Discord directly from `spaceport`.
|
||||||
|
- **Home Assistant $\leftrightarrow$ Gotify & Discord (`14.4.4.20:8123` — Configured & Verified):**
|
||||||
|
- Provisioned reusable script **`script.snarfnet_broadcast`** ([scripts/setup_homeassistant_notifications.py](file:///c:/Users/Snarf/OneDrive/Desktop/code/SnarfCode/scripts/setup_homeassistant_notifications.py)) which records structured broadcast entries via `logbook.log`, creates a persistent UI notification, and pushes directly to `notify.mobile_app_pixel_10_pro_fold` and `notify.mobile_app_pixel_9_pro_fold`.
|
||||||
|
- `gotify-discord-bridge` polls Home Assistant's `/api/logbook` API using the Long-Lived Access Token, evaluates any Jinja2 state templates via `/api/template`, publishes new notifications to Gotify Application #3 (`Home Assistant Smart Home`), and routes them to the `#home-assistant` Discord webhook.
|
||||||
|
- Provisioned Home Assistant automations:
|
||||||
|
1. **`Snarfnet UPS Power & Exhaust Protocol` (`1793453000001`):** Monitors `sensor.server_closet_status_data` (`OL`/`OB`/`LB`), broadcasts live UPS telemetry (`battery_charge`, `load`, `input_voltage`, `server_room_temperature`), and automatically sets `fan.server_closet_switch` to `100%` when running on battery.
|
||||||
|
2. **`Snarfnet Server Room Thermal Guard` (`1793453000002`):** Engages `fan.server_closet_switch` at `100%` and broadcasts a high-temperature warning if `sensor.server_room_temperature_temperature` exceeds `82°F` for 2 minutes.
|
||||||
|
3. **Bambu H2D 3D Printer Automations (`1788367985433`, `1788368709567`, `1788370272217`, `1788370321997`):** Enhanced to broadcast `Started`, `Completed`, `Stalled`, and `Failed` events to Gotify and `#home-assistant` on Discord alongside existing camera snapshots.
|
||||||
3. **Complete Zabbix Sensors, Thresholds, & Templates (Item 7):**
|
3. **Complete Zabbix Sensors, Thresholds, & Templates (Item 7):**
|
||||||
- **Fix Missing Hosts (100% Coverage):**
|
- **Fix Missing Hosts (100% Coverage):**
|
||||||
- Deploy `zabbix-agent2` with shared PSK (`PSK_autoregister`) via Ansible across all missing Linux nodes: Spaceport Masters (`140.44.4.41-44`), Aerodrome Workers (`140.44.4.25`, `.27`), AI Pis (`140.44.4.7`, `.8`), and Harvester nodes (`140.44.4.81-83`).
|
- Deploy `zabbix-agent2` with shared PSK (`PSK_autoregister`) via Ansible across all missing Linux nodes: Spaceport Masters (`140.44.4.41-44`), Aerodrome Workers (`140.44.4.25`, `.27`), AI Pis (`140.44.4.7`, `.8`), and Harvester nodes (`140.44.4.81-83`).
|
||||||
@@ -167,38 +296,37 @@ flowchart TD
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Phase 4: Infrastructure Upgrades, Ansible Playbooks & AWX Automation (Items 5 & 6)
|
### Phase 5: Infrastructure Upgrades, Ansible Playbooks & AWX Automation (Items 5 & 6) — [100% Completed & Verified]
|
||||||
|
|
||||||
#### Architecture & Implementation Steps
|
#### Architecture & Implementation Steps
|
||||||
1. **AWX Provisioning (`https://awx.snarfnet.net` on `aerodrome`):**
|
1. **AWX Provisioning (`https://awx.snarfnet.net` on `aerodrome`):**
|
||||||
- Execute an automated bootstrap script via `kubectl exec` into `awx-task` (or AWX REST API `/api/v2/`) to create:
|
- Executed automated provisioning via Django ORM inside `awx-task`:
|
||||||
- **Organization:** `Snarfnet Homelab`
|
- **Organization:** `Snarfnet Homelab` (id=2)
|
||||||
- **Credentials:**
|
- **Credentials:**
|
||||||
1. `Snarfnet Master SSH Key` (`Machine` credential with `id_ed25519_snarfnet` + passwordless sudo)
|
1. `Snarfnet Master SSH Key (root)` (`Machine` credential with `id_ed25519_snarfnet` for root access across clusters)
|
||||||
2. `Gitea Automation Token` (`Source Control` credential for pulling playbooks from `http://140.44.4.62:90/snarf/SnarfCode.git`)
|
2. `Snarfnet Master SSH Key (snarf)` (`Machine` credential with `id_ed25519_snarfnet` + passwordless sudo for Swarm, AI, and NUT)
|
||||||
3. `Authentik & Zabbix API Vault` (`Vault` credential)
|
- **Project:** `Snarfnet Infrastructure Playbooks` (id=8) synced from local **Gitea** (`https://git.snarfnet.net/Snarf/SnarfCode.git`).
|
||||||
- **Project:** `Snarfnet Infrastructure Playbooks` synced from local **Gitea** (`git.snarfnet.net`).
|
- **Master Inventory (`Snarfnet Master Inventory`, id=3):** Structured into 8 functional groups covering all 28 active homelab nodes:
|
||||||
- **Inventory (`Snarfnet Master Inventory`):** Structured into groups:
|
|
||||||
- `k3s_aerodrome_masters` (`140.44.4.11-13`) & `k3s_aerodrome_workers` (`140.44.4.21-27`)
|
- `k3s_aerodrome_masters` (`140.44.4.11-13`) & `k3s_aerodrome_workers` (`140.44.4.21-27`)
|
||||||
- `k3s_spaceport_masters` (`140.44.4.41-44`) & `k3s_spaceport_workers` (`140.44.4.51-56`)
|
- `k3s_spaceport_masters` (`140.44.4.41-44`) & `k3s_spaceport_workers` (`140.44.4.51-56`)
|
||||||
- `harvester_spacedock` (`140.44.4.81-83`)
|
|
||||||
- `swarm_shipyard` (`140.44.4.71-75`)
|
- `swarm_shipyard` (`140.44.4.71-75`)
|
||||||
- `ai_pi_cluster` (`140.44.4.7-8`)
|
- `ai_pi_cluster` (`140.44.4.7-8`)
|
||||||
- `smart_home_nodes` (`14.4.4.20`, `14.4.4.44`, `14.44.4.14`)
|
- `smart_home_nodes` (`nutserver` `14.4.4.44`)
|
||||||
- `synology_nas` (`14.10.10.71-72`)
|
- `synology_nas` (`14.10.10.71-72`)
|
||||||
2. **Remediation of `aerodrome` K3s Control Plane Version Skew (Priority Upgrade):**
|
- **Job Templates Provisioned & Verified:**
|
||||||
- `aerodrome` masters (`140.44.4.11-13`) are at `v1.26.10+k3s1` while workers are at `v1.30.6+k3s1`. Kubernetes strictly forbids skipping minor versions on control-plane upgrades.
|
- `Daily Drift & Agent Health Audit` ([ansible/playbooks/daily_drift_audit.yml](file:///c:/Users/Snarf/OneDrive/Desktop/code/SnarfCode/ansible/playbooks/daily_drift_audit.yml)): Audits time sync daemon, Zabbix agent 2, and master SSH key enrollment. Executed with 100% green recap across 28/28 active nodes (Job 106).
|
||||||
- Create `ansible/playbooks/upgrade_k3s_aerodrome.yml` with `serial: 1`:
|
- `Aerodrome K3s Pre-Upgrade Check` ([ansible/playbooks/k3s_aerodrome_precheck.yml](file:///c:/Users/Snarf/OneDrive/Desktop/code/SnarfCode/ansible/playbooks/k3s_aerodrome_precheck.yml)): Verifies K3s systemd unit and captures on-demand etcd snapshots. Executed and verified (Job 111).
|
||||||
- Step 1: Take an `etcd-snapshot save pre-upgrade-1.26` on `.11`.
|
- `Aerodrome K3s Control Plane Upgrade` ([ansible/playbooks/upgrade_k3s_aerodrome.yml](file:///c:/Users/Snarf/OneDrive/Desktop/code/SnarfCode/ansible/playbooks/upgrade_k3s_aerodrome.yml)): Atomic direct binary replacement with etcd quorum check and node `Ready` gates (`serial: 1`).
|
||||||
- Step 2: Sequentially step all 3 masters through each minor release channel: `v1.27.16+k3s1` $\rightarrow$ `v1.28.15+k3s1` $\rightarrow$ `v1.29.10+k3s1` $\rightarrow$ `v1.30.6+k3s1` (and optionally align with `spaceport` at `v1.31.4+k3s1`), waiting for `kubectl get nodes` `Ready` and etcd quorum health (`etcdctl endpoint health`) between every single node and minor step.
|
- `Weekly OS & Security Patching` ([ansible/playbooks/weekly_os_patching.yml](file:///c:/Users/Snarf/OneDrive/Desktop/code/SnarfCode/ansible/playbooks/weekly_os_patching.yml)): Staged rolling package updates with service health verification.
|
||||||
3. **AWX Job Templates & Scheduled Maintenance Windows:**
|
2. **Remediation of `aerodrome` K3s Control Plane Version Skew [100% Completed & Verified]:**
|
||||||
- **Template 1 — `Weekly OS & Security Patching` (Cron: Sundays 03:00 AM):**
|
- Remediation completed sequentially across minor versions to prevent etcd schema corruption:
|
||||||
- Runs `apt-get update && apt-get dist-upgrade -y` with `serial: 1`.
|
- **Pre-Upgrade etcd Snapshot:** Saved `clean-pre-upgrade-sninfr2lnxctl01-1793646879` (109 MB) on `.11`.
|
||||||
- Checks `/var/run/reboot-required`. If a kernel reboot is needed on a K8s or Swarm node, it runs `kubectl drain <node> --ignore-daemonsets --delete-emptydir-data` (or `docker node update --availability drain`), reboots, waits for SSH + kubelet `Ready` + Longhorn volume health (`Healthy`), and runs `kubectl uncordon <node>`.
|
- **Step 1 (`v1.27.16+k3s1`):** Executed AWX Job 118 (`serial: 1`). Upgraded `ctl01`, `ctl02`, and `ctl03`. All 3 masters reported `Ready`.
|
||||||
- **Template 2 — `Monthly K3s & Container Stack Update` (Cron: 1st Sunday 04:00 AM):**
|
- **Step 2 (`v1.28.15+k3s1`):** Executed AWX Job 121 (`serial: 1`). Upgraded `ctl01`, `ctl02`, and `ctl03`. All 3 masters reported `Ready`.
|
||||||
- Updates K3s binaries and pulls latest pinned container digests for Shipyard Swarm stacks, followed by HTTP 200 health verification across all 35 NPM endpoints.
|
- **Step 3 (`v1.29.10+k3s1`):** Executed AWX Job 124 (`serial: 1`). Upgraded `ctl01`, `ctl02`, and `ctl03`. All 3 masters reported `Ready`.
|
||||||
- **Template 3 — `Daily Drift & Agent Health Audit` (Cron: Daily 06:00 AM):**
|
- **Step 4 (`v1.30.6+k3s1`):** Executed AWX Job 127 (`serial: 1`). Upgraded `ctl01`, `ctl02`, and `ctl03`. All 3 masters reported `Ready`.
|
||||||
- Verifies `zabbix-agent2`, `upsmon` slave daemons, NFS mounts, and SSH `authorized_keys` across all nodes, sending a summary report to Gotify/Discord.
|
- **Outcome:** Control plane version skew is **100% resolved**. All 10 Aerodrome nodes (ctl01–03 and wrk01–07) are unified on `v1.30.6+k3s1`.
|
||||||
|
- **Workload Verification:** All mission-critical pods (Authentik server/worker/LDAP, AWX web/task/postgres, Longhorn attached volumes) verified `Running` and `healthy`.
|
||||||
|
|
||||||
#### Predicted Roadblocks & Mitigations
|
#### Predicted Roadblocks & Mitigations
|
||||||
- **Roadblock 4.1 — Longhorn Replica Rebuilding During Rolling Node Reboots:** If Ansible reboots node B immediately after node A finishes rebooting, Longhorn volumes may still be in `Degraded` (rebuilding replicas) on node A; rebooting node B can take the last healthy replica offline!
|
- **Roadblock 4.1 — Longhorn Replica Rebuilding During Rolling Node Reboots:** If Ansible reboots node B immediately after node A finishes rebooting, Longhorn volumes may still be in `Degraded` (rebuilding replicas) on node A; rebooting node B can take the last healthy replica offline!
|
||||||
@@ -208,30 +336,35 @@ flowchart TD
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Phase 5: Terraform Plans & ArgoCD GitOps Integrity (Item 4)
|
### Phase 6: Terraform Plans & ArgoCD GitOps Integrity (Item 4) — [100% Completed & Verified]
|
||||||
|
|
||||||
#### Architecture & Implementation Steps
|
#### Architecture & Implementation Steps (Executed & Verified)
|
||||||
1. **Local GitOps Repository Structure on Gitea (`https://git.snarfnet.net/snarf/homelab-gitops`):**
|
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
|
```text
|
||||||
homelab-gitops/
|
homelab-gitops/
|
||||||
├── terraform/
|
├── terraform/
|
||||||
│ ├── authentik/ # Providers, Applications, Outposts, Conditional MFA Policies
|
│ ├── authentik/ # OAuth2 providers, proxy ForwardAuth, apps, policies
|
||||||
│ ├── harvester/ # Spacedock VM definitions, VLAN networks, SSH keys
|
│ ├── harvester/ # Declarative VLAN networks, VM templates, SSH keys
|
||||||
│ ├── shipyard-swarm/ # Docker Swarm stacks (NPM, Gotify, Homebox, Peanut, Pihole)
|
│ ├── shipyard-swarm/ # Docker Swarm stacks (NPM, Gotify, Homebox, Peanut)
|
||||||
│ └── synology/ # Shared folders, NFS rules
|
│ └── synology/ # Shared folders (kubernetes, docker, backups, media) & NFS rules
|
||||||
└── argocd/
|
├── argocd/
|
||||||
├── bootstrap/ # Root App-of-Apps for spaceport & aerodrome
|
│ ├── bootstrap/ # Root App-of-Apps, Project, spaceport & aerodrome child apps
|
||||||
├── spaceport/ # Gitea, Zabbix, Jenkins, Semaphore, Coder, Homepage, Traefik
|
│ ├── spaceport/ # Applications hosted on spaceport (Gitea, Zabbix, Jenkins, etc.)
|
||||||
└── aerodrome/ # Authentik, AWX, Grafana, Uptime Kuma, Longhorn settings
|
│ └── 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`:**
|
2. **Terraform Declarations & HTTP State Backend:**
|
||||||
- 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.
|
- 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>`).
|
||||||
- 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.
|
|
||||||
3. **ArgoCD Multi-Cluster Registration (`https://argo.snarfnet.net`):**
|
3. **ArgoCD Multi-Cluster Registration (`https://argo.snarfnet.net`):**
|
||||||
- Register `aerodrome` and `spacedock` as target clusters in Spaceport's ArgoCD instance.
|
- 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.
|
||||||
- Deploy the **App-of-Apps** root Application targeting `homelab-gitops/argocd/` with:
|
4. **ArgoCD Notifications Pipeline to Gotify & Discord:**
|
||||||
- `automated: { prune: false, selfHeal: true }` (enabling `selfHeal` immediately while keeping `prune: false` initially so accidental deletions in Git never wipe a PVC).
|
- 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.
|
||||||
- ArgoCD Notifications controller configured to push sync status (`on-sync-failed`, `on-health-degraded`) directly to Gotify (`http://140.44.4.70:8090`).
|
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
|
#### 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.
|
- **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.
|
||||||
@@ -241,7 +374,23 @@ flowchart TD
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Phase 6: 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
|
#### 6.1 Core Power Architecture & IP Assignments
|
||||||
|
|
||||||
@@ -250,7 +399,7 @@ The power resilience architecture uses a centralized **NUT Master Server** (`14.
|
|||||||
| Role | Hostname / Identifier | Target IP | Subnet / VLAN | Description & Failure Protocol |
|
| 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`. |
|
| **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. |
|
| **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. |
|
| **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. |
|
| **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. |
|
||||||
@@ -317,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.
|
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`).
|
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.
|
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`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
59
docs/opencanary_honeypot.md
Normal file
59
docs/opencanary_honeypot.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# OpenCanary Decoy Honeypot Architecture & Configuration
|
||||||
|
|
||||||
|
## 1. Overview
|
||||||
|
As part of the **Snarfnet Network Security Hardening Blueprint (Phase 3)**, an **OpenCanary Honeypot** has been deployed to detect lateral movement, unauthorized reconnaissance, and compromised devices inside the **Smart Home VLAN (VLAN 50, `14.4.4.0/25`)**.
|
||||||
|
|
||||||
|
* **Decoy IP:** `14.4.4.99` (Subnet: `14.4.4.0/25`, Gateway: `14.4.4.1`)
|
||||||
|
* **Host Machine:** `snshpd4lnxswm01` (`140.44.4.71`)
|
||||||
|
* **Physical Uplink:** Direct 802.1Q trunk port with tagged VLAN 50 support.
|
||||||
|
* **Network Mode:** Native Docker `macvlan` attached to subinterface `eth0.50`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Emulated Services & Ports
|
||||||
|
|
||||||
|
The honeypot presents realistic service banners designed to trap automated scanners and unauthorized intruders without exposing real systems:
|
||||||
|
|
||||||
|
| Port | Protocol | Emulated Service | Decoy Profile |
|
||||||
|
|:---:|:---:|:---|:---|
|
||||||
|
| **21** | TCP | **FTP** | Linux FTP server (`FTP server ready`) |
|
||||||
|
| **22** | TCP | **SSH** | Ubuntu OpenSSH banner (`SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.10`) |
|
||||||
|
| **23** | TCP | **Telnet** | Ubuntu login prompt (`Ubuntu 22.04 LTS login: `) |
|
||||||
|
| **80** | TCP | **HTTP Web** | Synology/NAS Web Admin Login skin |
|
||||||
|
| **1433** | TCP | **MSSQL** | Microsoft SQL Server 2012 |
|
||||||
|
| **3306** | TCP | **MySQL** | MySQL 5.7 database server |
|
||||||
|
| **3389** | TCP | **RDP** | Windows Remote Desktop Protocol listener |
|
||||||
|
| **5000** | TCP | **VNC** | Remote framebuffer / VNC listener |
|
||||||
|
| **8080** | TCP | **HTTP Proxy** | Squid HTTP proxy decoy |
|
||||||
|
| **9418** | TCP | **Git** | Git daemon repository service |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Real-Time Alerting Pipeline
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Attacker["Suspicious Host / IoT Device<br>(e.g. 14.4.4.15)"] -->|TCP / Port Probe| Decoy["OpenCanary (14.4.4.99:22/80/445/3389)"]
|
||||||
|
Decoy -->|Structured JSON Log| LogFile["/opt/opencanary/logs/opencanary.log"]
|
||||||
|
LogFile -->|Real-Time Tail| Watcher["opencanary-watcher.service<br>(Python Daemon)"]
|
||||||
|
Watcher -->|HTTP POST (Priority 8)| Gotify["Gotify Server (App 6)<br>(gotify.snarfnet.net)"]
|
||||||
|
Gotify -->|Instant Push Notification| Phone["Admin Mobile App"]
|
||||||
|
Gotify -->|Discord Webhook Bridge| Discord["Discord #infra-alerts"]
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Any TCP connection, banner grab, authentication attempt, or probe arriving on `14.4.4.99` generates a structured event in `/opt/opencanary/logs/opencanary.log`.
|
||||||
|
2. The `opencanary-watcher.service` daemon ingests the event in real-time, extracts the attacker's source IP, source port, targeted port, service type, and credentials (if entered).
|
||||||
|
3. The event is posted to **Gotify Application #6** (`A9oPeNcAnArYhOnEyPoT`) with high priority (`8`).
|
||||||
|
4. `gotify-discord-bridge` formats a rich embed and posts the alert directly into the Discord `#infra-alerts` channel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Systemd Services & Maintenance
|
||||||
|
|
||||||
|
* **VLAN Subinterface Service:**
|
||||||
|
`/etc/systemd/system/opencanary-vlan.service` (Ensures `eth0.50` is brought up on boot).
|
||||||
|
* **Alert Watcher Service:**
|
||||||
|
`/etc/systemd/system/opencanary-watcher.service` (Tails log file and delivers push alerts).
|
||||||
|
* **Docker Container:**
|
||||||
|
`opencanary` (Managed via Docker with `restart: unless-stopped` on network `opencanary_net`).
|
||||||
|
|
||||||
1029
docs/snarfnet_homelab_topology.html
Normal file
1029
docs/snarfnet_homelab_topology.html
Normal file
File diff suppressed because it is too large
Load Diff
BIN
docs/snarfnet_homelab_topology.png
Normal file
BIN
docs/snarfnet_homelab_topology.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 830 KiB |
1
homelab-gitops-repo
Submodule
1
homelab-gitops-repo
Submodule
Submodule homelab-gitops-repo added at 5169a02440
BIN
scripts/__pycache__/harden_online_rpi_boot.cpython-312.pyc
Normal file
BIN
scripts/__pycache__/harden_online_rpi_boot.cpython-312.pyc
Normal file
Binary file not shown.
22
scripts/add_forward_rules.py
Normal file
22
scripts/add_forward_rules.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import paramiko
|
||||||
|
|
||||||
|
ssh = paramiko.SSHClient()
|
||||||
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
ssh.connect('140.44.4.71', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||||
|
|
||||||
|
def run(cmd):
|
||||||
|
print(f">>> {cmd}")
|
||||||
|
_, o, e = ssh.exec_command(cmd)
|
||||||
|
out = o.read().decode().strip()
|
||||||
|
err = e.read().decode().strip()
|
||||||
|
if out:
|
||||||
|
print(out)
|
||||||
|
if err:
|
||||||
|
print("[stderr]", err)
|
||||||
|
|
||||||
|
run("iptables -I FORWARD 1 -m state --state RELATED,ESTABLISHED -j ACCEPT")
|
||||||
|
run("iptables -I FORWARD 2 -p tcp -d 140.44.4.64 --dport 10051 -j ACCEPT")
|
||||||
|
run("iptables -L FORWARD -n -v | head -n 8")
|
||||||
|
|
||||||
|
ssh.close()
|
||||||
|
|
||||||
58
scripts/add_kuma_bookmarks.py
Normal file
58
scripts/add_kuma_bookmarks.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import yaml
|
||||||
|
|
||||||
|
def str_presenter(dumper, data):
|
||||||
|
if '\n' in data:
|
||||||
|
return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|')
|
||||||
|
return dumper.represent_scalar('tag:yaml.org,2002:str', data)
|
||||||
|
|
||||||
|
yaml.add_representer(str, str_presenter)
|
||||||
|
yaml.representer.SafeRepresenter.add_representer(str, str_presenter)
|
||||||
|
|
||||||
|
with open('homelab-gitops-repo/manifests/spaceport/homepage/homepage.yaml', 'r', encoding='utf-8') as f:
|
||||||
|
text = f.read()
|
||||||
|
|
||||||
|
docs = list(yaml.safe_load_all(text))
|
||||||
|
cm = docs[2]
|
||||||
|
|
||||||
|
bookmarks_yaml = """- Quick Links:
|
||||||
|
- GitHub:
|
||||||
|
- abbr: GH
|
||||||
|
href: https://github.com/
|
||||||
|
- UniFi Site Manager:
|
||||||
|
- abbr: UI
|
||||||
|
href: https://unifi.ui.com/
|
||||||
|
- Cloudflare:
|
||||||
|
- abbr: CF
|
||||||
|
href: https://dash.cloudflare.com/
|
||||||
|
- Grafana Telemetry:
|
||||||
|
- Systems Performance:
|
||||||
|
- abbr: SYS
|
||||||
|
href: https://grafana.snarfnet.net/d/snarfnet-systems/52f6861
|
||||||
|
- Container Workloads:
|
||||||
|
- abbr: K8S
|
||||||
|
href: https://grafana.snarfnet.net/d/snarfnet-containers/f09f93a6-kubernetes-containers-and-workloads
|
||||||
|
- AI Stack Intelligence:
|
||||||
|
- abbr: AI
|
||||||
|
href: https://grafana.snarfnet.net/d/snarfnet-ai-stack/snarfnet-ai-stack-and-inference-intelligence
|
||||||
|
- Uptime Status Pages:
|
||||||
|
- Unified Overview:
|
||||||
|
- abbr: ALL
|
||||||
|
href: https://uptime.snarfnet.net/status/default
|
||||||
|
- Core & VIP Fabric:
|
||||||
|
- abbr: VIP
|
||||||
|
href: https://uptime.snarfnet.net/status/core
|
||||||
|
- GitOps & CI/CD:
|
||||||
|
- abbr: OPS
|
||||||
|
href: https://uptime.snarfnet.net/status/gitops
|
||||||
|
- Edge & Ingress:
|
||||||
|
- abbr: EDG
|
||||||
|
href: https://uptime.snarfnet.net/status/edge
|
||||||
|
"""
|
||||||
|
|
||||||
|
cm['data']['bookmarks.yaml'] = bookmarks_yaml
|
||||||
|
|
||||||
|
with open('homelab-gitops-repo/manifests/spaceport/homepage/homepage.yaml', 'w', encoding='utf-8') as f:
|
||||||
|
yaml.dump_all(docs, f, sort_keys=False, default_flow_style=False, allow_unicode=True)
|
||||||
|
|
||||||
|
print("Updated bookmarks in homepage.yaml successfully!")
|
||||||
|
|
||||||
32
scripts/add_media_to_snarf.py
Normal file
32
scripts/add_media_to_snarf.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import json, ssl, urllib.request
|
||||||
|
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
ZABBIX_URL = 'http://140.44.4.60:8888/api_jsonrpc.php'
|
||||||
|
|
||||||
|
payload = {'jsonrpc': '2.0', 'method': 'user.login', 'params': {'username': 'Admin', 'password': 'zabbix'}, 'id': 1}
|
||||||
|
req = urllib.request.Request(ZABBIX_URL, data=json.dumps(payload).encode(), headers={'Content-Type': 'application/json-rpc'})
|
||||||
|
token = json.loads(urllib.request.urlopen(req, context=ctx).read())['result']
|
||||||
|
|
||||||
|
media_payload = {
|
||||||
|
'jsonrpc': '2.0',
|
||||||
|
'method': 'user.update',
|
||||||
|
'params': {
|
||||||
|
'userid': '3',
|
||||||
|
'medias': [
|
||||||
|
{
|
||||||
|
'mediatypeid': '70',
|
||||||
|
'sendto': 'snarf@snarfnet.net',
|
||||||
|
'active': 0,
|
||||||
|
'severity': 63, # 1+2+4+8+16+32
|
||||||
|
'period': '1-7,00:00-24:00'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'id': 2
|
||||||
|
}
|
||||||
|
req = urllib.request.Request(ZABBIX_URL, data=json.dumps(media_payload).encode(), headers={'Content-Type': 'application/json-rpc', 'Authorization': f'Bearer {token}'})
|
||||||
|
res = json.loads(urllib.request.urlopen(req, context=ctx).read())
|
||||||
|
print("User update result:", res)
|
||||||
26
scripts/add_topology_to_homepage.py
Normal file
26
scripts/add_topology_to_homepage.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import yaml
|
||||||
|
|
||||||
|
def str_presenter(dumper, data):
|
||||||
|
if '\n' in data:
|
||||||
|
return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|')
|
||||||
|
return dumper.represent_scalar('tag:yaml.org,2002:str', data)
|
||||||
|
|
||||||
|
yaml.add_representer(str, str_presenter)
|
||||||
|
yaml.representer.SafeRepresenter.add_representer(str, str_presenter)
|
||||||
|
|
||||||
|
with open('homelab-gitops-repo/manifests/spaceport/homepage/homepage.yaml', 'r', encoding='utf-8') as f:
|
||||||
|
text = f.read()
|
||||||
|
|
||||||
|
docs = list(yaml.safe_load_all(text))
|
||||||
|
cm = docs[2]
|
||||||
|
|
||||||
|
services = yaml.safe_load(cm['data']['services.yaml'])
|
||||||
|
bookmarks = yaml.safe_load(cm['data']['bookmarks.yaml'])
|
||||||
|
|
||||||
|
cm['data']['services.yaml'] = yaml.dump(services, sort_keys=False, default_flow_style=False, allow_unicode=True)
|
||||||
|
cm['data']['bookmarks.yaml'] = yaml.dump(bookmarks, sort_keys=False, default_flow_style=False, allow_unicode=True)
|
||||||
|
|
||||||
|
with open('homelab-gitops-repo/manifests/spaceport/homepage/homepage.yaml', 'w', encoding='utf-8') as f:
|
||||||
|
yaml.dump_all(docs, f, sort_keys=False, default_flow_style=False, allow_unicode=True)
|
||||||
|
|
||||||
|
print("Cleaned up unicode strings in homepage.yaml!")
|
||||||
118
scripts/add_zabbix_devices.py
Normal file
118
scripts/add_zabbix_devices.py
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import json
|
||||||
|
import ssl
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
ZABBIX_URL = "http://140.44.4.60:8888/api_jsonrpc.php"
|
||||||
|
ZABBIX_USER = "Admin"
|
||||||
|
ZABBIX_PASS = "zabbix"
|
||||||
|
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
def zbx_call(method, params, auth=None):
|
||||||
|
payload = {"jsonrpc": "2.0", "method": method, "params": params, "id": 1}
|
||||||
|
headers = {"Content-Type": "application/json-rpc"}
|
||||||
|
if auth:
|
||||||
|
headers["Authorization"] = f"Bearer {auth}"
|
||||||
|
req = urllib.request.Request(ZABBIX_URL, data=json.dumps(payload).encode("utf-8"), headers=headers, method="POST")
|
||||||
|
with urllib.request.urlopen(req, timeout=10, context=ctx) as r:
|
||||||
|
resp = json.loads(r.read().decode("utf-8"))
|
||||||
|
if "error" in resp:
|
||||||
|
raise RuntimeError(f"Zabbix error in {method}: {resp['error']}")
|
||||||
|
return resp["result"]
|
||||||
|
|
||||||
|
auth = zbx_call("user.login", {"username": ZABBIX_USER, "password": ZABBIX_PASS})
|
||||||
|
|
||||||
|
# Check existing host names
|
||||||
|
existing_hosts = zbx_call("host.get", {"output": ["hostid", "host", "name"]}, auth)
|
||||||
|
existing_names = {h["host"].lower(): h["hostid"] for h in existing_hosts}
|
||||||
|
|
||||||
|
# Create host groups if not exist
|
||||||
|
groups = zbx_call("hostgroup.get", {"output": ["groupid", "name"]}, auth)
|
||||||
|
group_map = {g["name"]: g["groupid"] for g in groups}
|
||||||
|
|
||||||
|
for gname in ["Network Devices", "Environmental & Sensors", "Management & IPMI"]:
|
||||||
|
if gname not in group_map:
|
||||||
|
res = zbx_call("hostgroup.create", {"name": gname}, auth)
|
||||||
|
group_map[gname] = res["groupids"][0]
|
||||||
|
print(f"Created Host Group: {gname} (ID: {group_map[gname]})")
|
||||||
|
|
||||||
|
devices_to_add = [
|
||||||
|
{
|
||||||
|
"host": "idrac-spacedock4",
|
||||||
|
"name": "Dell PowerEdge R730 Node 4 iDRAC (idrac-spacedock4)",
|
||||||
|
"ip": "140.44.44.124",
|
||||||
|
"group": "Management & IPMI"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "usw-agg-core",
|
||||||
|
"name": "UniFi Switch Aggregation 10G Core",
|
||||||
|
"ip": "14.44.4.26",
|
||||||
|
"group": "Network Devices"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "usw-24-poe-closet",
|
||||||
|
"name": "UniFi Switch 24 PoE Closet",
|
||||||
|
"ip": "14.44.4.61",
|
||||||
|
"group": "Network Devices"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "usw-pro-24-poe-compute",
|
||||||
|
"name": "UniFi Switch Pro 24 PoE Compute",
|
||||||
|
"ip": "14.44.4.107",
|
||||||
|
"group": "Network Devices"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "usw-flex-attic",
|
||||||
|
"name": "UniFi Switch Flex Attic",
|
||||||
|
"ip": "14.44.4.98",
|
||||||
|
"group": "Network Devices"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "usw-flex-garage",
|
||||||
|
"name": "UniFi Switch Flex Garage",
|
||||||
|
"ip": "14.44.4.96",
|
||||||
|
"group": "Network Devices"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "tempest-weather-station",
|
||||||
|
"name": "Tempest Weather Station Gateway",
|
||||||
|
"ip": "14.4.4.47",
|
||||||
|
"group": "Environmental & Sensors"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"host": "sense-energy-monitor",
|
||||||
|
"name": "Sense Home Energy Gateway",
|
||||||
|
"ip": "14.4.4.48",
|
||||||
|
"group": "Environmental & Sensors"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for dev in devices_to_add:
|
||||||
|
hname = dev["host"]
|
||||||
|
if hname.lower() in existing_names:
|
||||||
|
print(f"[EXISTS] {hname}")
|
||||||
|
continue
|
||||||
|
params = {
|
||||||
|
"host": dev["host"],
|
||||||
|
"name": dev["name"],
|
||||||
|
"interfaces": [
|
||||||
|
{
|
||||||
|
"type": 1, # Agent / ICMP
|
||||||
|
"main": 1,
|
||||||
|
"useip": 1,
|
||||||
|
"ip": dev["ip"],
|
||||||
|
"dns": "",
|
||||||
|
"port": "10050"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"groups": [{"groupid": group_map[dev["group"]]}],
|
||||||
|
"templates": [{"templateid": "10564"}] # ICMP Ping
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
res = zbx_call("host.create", params, auth)
|
||||||
|
print(f"[CREATED] {dev['name']} ({dev['ip']}) -> HostID {res['hostids'][0]}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[FAILED] {dev['name']}: {e}")
|
||||||
|
|
||||||
65
scripts/apply_36.py
Normal file
65
scripts/apply_36.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
key_path = r"C:\Users\Snarf\.ssh\id_ed25519_snarfnet"
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect("140.44.4.11", username="root", key_filename=key_path, timeout=5)
|
||||||
|
|
||||||
|
cmd = """
|
||||||
|
k3s kubectl -n dns delete svc pihole-dns-vip pihole-web-vip --wait=false
|
||||||
|
sleep 2
|
||||||
|
cat << 'EOF' | k3s kubectl apply -f -
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: pihole-dns-vip
|
||||||
|
namespace: dns
|
||||||
|
annotations:
|
||||||
|
metallb.universe.tf/loadBalancerIPs: 140.44.4.36
|
||||||
|
metallb.universe.tf/allow-shared-ip: "aerodrome-dns-key"
|
||||||
|
labels:
|
||||||
|
app: pihole
|
||||||
|
spec:
|
||||||
|
type: LoadBalancer
|
||||||
|
selector:
|
||||||
|
app: pihole
|
||||||
|
ports:
|
||||||
|
- name: dns-udp
|
||||||
|
port: 53
|
||||||
|
targetPort: 53
|
||||||
|
protocol: UDP
|
||||||
|
- name: dns-tcp
|
||||||
|
port: 53
|
||||||
|
targetPort: 53
|
||||||
|
protocol: TCP
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: pihole-web-vip
|
||||||
|
namespace: dns
|
||||||
|
annotations:
|
||||||
|
metallb.universe.tf/loadBalancerIPs: 140.44.4.36
|
||||||
|
metallb.universe.tf/allow-shared-ip: "aerodrome-dns-key"
|
||||||
|
labels:
|
||||||
|
app: pihole
|
||||||
|
spec:
|
||||||
|
type: LoadBalancer
|
||||||
|
selector:
|
||||||
|
app: pihole
|
||||||
|
ports:
|
||||||
|
- name: web-http
|
||||||
|
port: 80
|
||||||
|
targetPort: 80
|
||||||
|
protocol: TCP
|
||||||
|
EOF
|
||||||
|
sleep 3
|
||||||
|
k3s kubectl -n dns get svc
|
||||||
|
"""
|
||||||
|
stdin, stdout, stderr = c.exec_command(cmd)
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
c.close()
|
||||||
|
|
||||||
52
scripts/apply_and_set_pihole_pw.py
Normal file
52
scripts/apply_and_set_pihole_pw.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import paramiko
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
key_path = r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet'
|
||||||
|
|
||||||
|
def apply_and_update(ip, name, manifest_path):
|
||||||
|
print(f"\n=======================================================")
|
||||||
|
print(f"Applying to {name} ({ip})...")
|
||||||
|
with open(manifest_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
ssh = paramiko.SSHClient()
|
||||||
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
ssh.connect(ip, username='root', key_filename=key_path, timeout=10)
|
||||||
|
|
||||||
|
stdin, stdout, stderr = ssh.exec_command("k3s kubectl apply -f -")
|
||||||
|
stdin.write(content)
|
||||||
|
stdin.channel.shutdown_write()
|
||||||
|
print("Apply output:", stdout.read().decode())
|
||||||
|
err = stderr.read().decode()
|
||||||
|
if err:
|
||||||
|
print("Apply stderr:", err)
|
||||||
|
|
||||||
|
print(f"Waiting for Pi-hole deployment rollout on {name}...")
|
||||||
|
stdin, stdout, stderr = ssh.exec_command("k3s kubectl rollout status deployment/pihole -n dns --timeout=60s")
|
||||||
|
print(stdout.read().decode())
|
||||||
|
|
||||||
|
# Get new pod name
|
||||||
|
stdin, stdout, stderr = ssh.exec_command("k3s kubectl get pods -n dns -l app=pihole -o jsonpath='{.items[0].metadata.name}'")
|
||||||
|
pod_name = stdout.read().decode().strip()
|
||||||
|
print(f"New Pi-hole pod: {pod_name}")
|
||||||
|
|
||||||
|
# Set password to snarfnetadmin
|
||||||
|
print("Setting Pi-hole password to 'snarfnetadmin'...")
|
||||||
|
stdin, stdout, stderr = ssh.exec_command(f"k3s kubectl exec -n dns {pod_name} -- pihole setpassword snarfnetadmin")
|
||||||
|
out = stdout.read().decode()
|
||||||
|
err = stderr.read().decode()
|
||||||
|
print("Set password output:", out)
|
||||||
|
if err:
|
||||||
|
print("Set password stderr:", err)
|
||||||
|
|
||||||
|
ssh.close()
|
||||||
|
|
||||||
|
aero_manifest = r"C:\Users\Snarf\OneDrive\Desktop\code\SnarfCode\homelab-gitops-repo\manifests\aerodrome\dns\dns-stack.yaml"
|
||||||
|
space_manifest = r"C:\Users\Snarf\OneDrive\Desktop\code\SnarfCode\homelab-gitops-repo\manifests\spaceport\dns\dns-stack.yaml"
|
||||||
|
|
||||||
|
apply_and_update("140.44.4.11", "Aerodrome", aero_manifest)
|
||||||
|
apply_and_update("140.44.4.41", "Spaceport", space_manifest)
|
||||||
|
print("\nDone!")
|
||||||
|
|
||||||
51
scripts/apply_bios_tuning.py
Normal file
51
scripts/apply_bios_tuning.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import paramiko
|
||||||
|
import time
|
||||||
|
|
||||||
|
targets = [
|
||||||
|
{
|
||||||
|
"name": "Node 1 (140.44.44.121)",
|
||||||
|
"ip": "140.44.44.121",
|
||||||
|
"settings": [
|
||||||
|
("bios.IntegratedDevices.SriovGlobalEnable", "Enabled")
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Node 4 (140.44.44.124)",
|
||||||
|
"ip": "140.44.44.124",
|
||||||
|
"settings": [
|
||||||
|
("bios.SysProfileSettings.SysProfile", "PerfOptimized")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for t in targets:
|
||||||
|
print(f"\n==========================================")
|
||||||
|
print(f"Applying BIOS settings to {t['name']}...")
|
||||||
|
try:
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect(t['ip'], username='root', password='Katzir476!', timeout=15)
|
||||||
|
|
||||||
|
for key, val in t['settings']:
|
||||||
|
cmd = f"racadm set {key} {val}"
|
||||||
|
stdin, stdout, stderr = c.exec_command(cmd)
|
||||||
|
out = stdout.read().decode().strip()
|
||||||
|
print(f" racadm set {key} {val}:\n {out}")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# Create BIOS job
|
||||||
|
stdin, stdout, stderr = c.exec_command("racadm jobqueue create BIOS.Setup.1-1")
|
||||||
|
job_out = stdout.read().decode().strip()
|
||||||
|
print(f" racadm jobqueue create BIOS.Setup.1-1:\n {job_out}")
|
||||||
|
|
||||||
|
# View job queue
|
||||||
|
stdin, stdout, stderr = c.exec_command("racadm jobqueue view")
|
||||||
|
q_out = stdout.read().decode().strip()
|
||||||
|
print(f" Job Queue:\n {q_out[:200]}")
|
||||||
|
|
||||||
|
c.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Error on {t['name']}: {e}")
|
||||||
|
|
||||||
|
print("\nDone applying BIOS settings!")
|
||||||
|
|
||||||
48
scripts/apply_coredns_spaceport.py
Normal file
48
scripts/apply_coredns_spaceport.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
key_path = r"C:\Users\Snarf\.ssh\id_ed25519_snarfnet"
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
|
||||||
|
client.connect("140.44.4.41", username="root", key_filename=key_path, timeout=10)
|
||||||
|
|
||||||
|
cm_yaml = """cat << 'EOF' | k3s kubectl apply -f -
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: coredns-custom
|
||||||
|
namespace: kube-system
|
||||||
|
data:
|
||||||
|
snarfnet.override: |
|
||||||
|
hosts {
|
||||||
|
140.44.4.34 aerodrome.snarfnet.net
|
||||||
|
140.44.4.90 harvester.spacedock.snarfnet.net
|
||||||
|
fallthrough
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
"""
|
||||||
|
|
||||||
|
print("Applying coredns-custom on Spaceport...")
|
||||||
|
stdin, stdout, stderr = client.exec_command(cm_yaml)
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
print(stderr.read().decode('utf-8'))
|
||||||
|
|
||||||
|
print("\nRestarting coredns deployment on Spaceport...")
|
||||||
|
client.exec_command("k3s kubectl -n kube-system rollout restart deployment/coredns")
|
||||||
|
client.exec_command("k3s kubectl -n kube-system rollout status deployment/coredns --timeout=60s")
|
||||||
|
|
||||||
|
print("\nRestarting cattle-cluster-agent on Spaceport...")
|
||||||
|
client.exec_command("k3s kubectl -n cattle-system rollout restart deployment/cattle-cluster-agent")
|
||||||
|
client.exec_command("k3s kubectl -n cattle-system rollout status deployment/cattle-cluster-agent --timeout=60s")
|
||||||
|
|
||||||
|
time.sleep(5)
|
||||||
|
print("\nChecking cattle-cluster-agent logs:")
|
||||||
|
stdin, stdout, stderr = client.exec_command("k3s kubectl -n cattle-system logs -l app=cattle-cluster-agent --tail=25")
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
|
||||||
31
scripts/apply_dns_updates.py
Normal file
31
scripts/apply_dns_updates.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import paramiko
|
||||||
|
|
||||||
|
SSH_KEY = r"C:\Users\Snarf\.ssh\id_ed25519_snarfnet"
|
||||||
|
|
||||||
|
def apply_manifest(host, local_file):
|
||||||
|
with open(local_file, "r") as f:
|
||||||
|
manifest_content = f.read()
|
||||||
|
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
client.connect(host, username="root", key_filename=SSH_KEY, timeout=10)
|
||||||
|
|
||||||
|
stdin, stdout, stderr = client.exec_command("k3s kubectl apply -f -")
|
||||||
|
stdin.write(manifest_content)
|
||||||
|
stdin.close()
|
||||||
|
out = stdout.read().decode()
|
||||||
|
err = stderr.read().decode()
|
||||||
|
client.close()
|
||||||
|
return out, err
|
||||||
|
|
||||||
|
print("=== Applying to Aerodrome (140.44.4.11) ===")
|
||||||
|
out, err = apply_manifest("140.44.4.11", r"homelab-gitops-repo\manifests\aerodrome\dns\dns-stack.yaml")
|
||||||
|
print(out)
|
||||||
|
if err:
|
||||||
|
print("Stderr:", err)
|
||||||
|
|
||||||
|
print("=== Applying to Spaceport (140.44.4.41) ===")
|
||||||
|
out, err = apply_manifest("140.44.4.41", r"homelab-gitops-repo\manifests\spaceport\dns\dns-stack.yaml")
|
||||||
|
print(out)
|
||||||
|
if err:
|
||||||
|
print("Stderr:", err)
|
||||||
250
scripts/apply_full_homepage.py
Normal file
250
scripts/apply_full_homepage.py
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
import yaml
|
||||||
|
|
||||||
|
def str_presenter(dumper, data):
|
||||||
|
if '\n' in data:
|
||||||
|
return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='|')
|
||||||
|
return dumper.represent_scalar('tag:yaml.org,2002:str', data)
|
||||||
|
|
||||||
|
yaml.add_representer(str, str_presenter)
|
||||||
|
yaml.representer.SafeRepresenter.add_representer(str, str_presenter)
|
||||||
|
|
||||||
|
with open('homelab-gitops-repo/manifests/spaceport/homepage/homepage.yaml', 'r', encoding='utf-8') as f:
|
||||||
|
text = f.read()
|
||||||
|
|
||||||
|
docs = list(yaml.safe_load_all(text))
|
||||||
|
cm = docs[2]
|
||||||
|
|
||||||
|
services_yaml = """- Critical Services:
|
||||||
|
- Authentik:
|
||||||
|
icon: authentik.png
|
||||||
|
href: https://authentik.snarfnet.net
|
||||||
|
description: SSO & Identity Management
|
||||||
|
- PeaNUT UPS:
|
||||||
|
icon: peanut.png
|
||||||
|
href: https://portal.snarfnet.net
|
||||||
|
description: UPS Power & Battery Monitor
|
||||||
|
- Unifi Controller:
|
||||||
|
icon: unifi.png
|
||||||
|
href: https://unifi.ui.com
|
||||||
|
widget:
|
||||||
|
type: unifi
|
||||||
|
url: https://140.44.44.1
|
||||||
|
username: Snarf
|
||||||
|
password: Katzir476!!!
|
||||||
|
|
||||||
|
- GitOps & Automation:
|
||||||
|
- Gitea:
|
||||||
|
icon: gitea.png
|
||||||
|
href: https://git.snarfnet.net
|
||||||
|
description: Git Repositories
|
||||||
|
widget:
|
||||||
|
type: gitea
|
||||||
|
url: https://git.snarfnet.net
|
||||||
|
key: f19e772cf54e40ceb2befe452e2bc24dd81fdb71
|
||||||
|
- ArgoCD:
|
||||||
|
icon: argocd.png
|
||||||
|
href: https://argo.snarfnet.net
|
||||||
|
description: Declarative GitOps Reconciler
|
||||||
|
widget:
|
||||||
|
type: argocd
|
||||||
|
url: https://argo.snarfnet.net
|
||||||
|
key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhcmdvY2QiLCJzdWIiOiJob21lcGFnZTphcGlLZXkiLCJuYmYiOjE3OTQ1MDIwNzgsImlhdCI6MTc5NDUwMjA3OCwianRpIjoiZjQ2NzU0NjMtODRhOS00MjI0LWI3ODItNmY5ODQxYWEwY2UxIn0.aHhrny2RBG-vAJAyXMvan0jw65jkP3bguqEduaG78O0
|
||||||
|
- AWX Automation:
|
||||||
|
icon: awx.png
|
||||||
|
href: https://awx.snarfnet.net
|
||||||
|
description: Ansible Automation Controller
|
||||||
|
- Semaphore:
|
||||||
|
icon: semaphore.png
|
||||||
|
href: https://semaphore.snarfnet.net
|
||||||
|
description: Playbook Execution Engine
|
||||||
|
|
||||||
|
- Storage & Clusters:
|
||||||
|
- Longhorn Aerodrome:
|
||||||
|
icon: longhorn.png
|
||||||
|
href: https://longhorn.aerodrome.snarfnet.net
|
||||||
|
description: Aerodrome Distributed Flash Storage
|
||||||
|
- Longhorn Spaceport:
|
||||||
|
icon: longhorn.png
|
||||||
|
href: https://longhorn.spaceport.snarfnet.net
|
||||||
|
description: Spaceport Distributed Block Storage
|
||||||
|
- Rancher (Aerodrome):
|
||||||
|
icon: rancher.png
|
||||||
|
href: https://aerodrome.snarfnet.net
|
||||||
|
description: Kubernetes Management Console
|
||||||
|
widget:
|
||||||
|
type: customapi
|
||||||
|
url: https://aerodrome.snarfnet.net/v3/clusters/local
|
||||||
|
headers:
|
||||||
|
Authorization: "Bearer kubeconfig-u-aqzkufonck4hmbd:bw22b96nv8844k8w5r8lzpz8gnk22tjwj95j6nkzr7hd5dhm4bs6z7"
|
||||||
|
mappings:
|
||||||
|
- field: capacity.cpu
|
||||||
|
label: CPU Cores
|
||||||
|
- field: capacity.pods
|
||||||
|
label: Max Pods
|
||||||
|
- field: state
|
||||||
|
label: Cluster State
|
||||||
|
- Harvester HCI (Spacedock):
|
||||||
|
icon: harvester.png
|
||||||
|
href: https://harvester.spacedock.snarfnet.net
|
||||||
|
description: HCI Virtualization Hypervisors
|
||||||
|
widget:
|
||||||
|
type: customapi
|
||||||
|
url: https://aerodrome.snarfnet.net/v3/clusters/c-cclnt
|
||||||
|
headers:
|
||||||
|
Authorization: "Bearer kubeconfig-u-aqzkufonck4hmbd:bw22b96nv8844k8w5r8lzpz8gnk22tjwj95j6nkzr7hd5dhm4bs6z7"
|
||||||
|
mappings:
|
||||||
|
- field: capacity.cpu
|
||||||
|
label: CPU Cores
|
||||||
|
- field: capacity.pods
|
||||||
|
label: Max Pods
|
||||||
|
- field: state
|
||||||
|
label: HCI State
|
||||||
|
- Portainer Swarm:
|
||||||
|
icon: portainer.png
|
||||||
|
href: https://shipyard.snarfnet.net
|
||||||
|
description: Docker Swarm Container Fleet
|
||||||
|
widget:
|
||||||
|
type: portainer
|
||||||
|
url: https://140.44.4.71:9443
|
||||||
|
env: 3
|
||||||
|
key: ptr_TCVm8ujeB4zT0oRejyMCwKw5enaPG/uKj1KuF0uOWX8=
|
||||||
|
|
||||||
|
- Infrastructure & Observability:
|
||||||
|
- NGINX Proxy Manager:
|
||||||
|
icon: nginx-proxy-manager
|
||||||
|
href: https://nginx.snarfnet.net
|
||||||
|
description: Reverse Proxy & SSL Certificates
|
||||||
|
- Uptime Kuma:
|
||||||
|
icon: uptime-kuma
|
||||||
|
href: https://uptime.snarfnet.net
|
||||||
|
description: Multi-Endpoint Availability Probes
|
||||||
|
widget:
|
||||||
|
type: uptimekuma
|
||||||
|
url: http://140.44.4.28:448
|
||||||
|
slug: default
|
||||||
|
- Zabbix:
|
||||||
|
icon: zabbix.png
|
||||||
|
href: https://zabbix.snarfnet.net
|
||||||
|
description: Infrastructure Metrics & Alarms
|
||||||
|
- Grafana:
|
||||||
|
icon: grafana.png
|
||||||
|
href: https://grafana.snarfnet.net
|
||||||
|
description: Real-Time Metrics & AI Dashboards
|
||||||
|
widget:
|
||||||
|
type: grafana
|
||||||
|
url: https://grafana.snarfnet.net
|
||||||
|
username: admin
|
||||||
|
password: Katzir476!!!
|
||||||
|
- Core Node Glances:
|
||||||
|
icon: glances.png
|
||||||
|
href: http://140.44.4.11:61208
|
||||||
|
description: Host Resource Monitor
|
||||||
|
|
||||||
|
- Environment & Weather:
|
||||||
|
- Tempest Weather Station:
|
||||||
|
icon: weather.png
|
||||||
|
href: https://snarfhome.snarfnet.net
|
||||||
|
description: Backyard Weather Telemetry
|
||||||
|
widget:
|
||||||
|
type: homeassistant
|
||||||
|
url: http://14.4.4.20:8123
|
||||||
|
key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzZWUyNTE1MDk0NGU0ZjRjYmJkMGM1MWI5MThmY2EzYiIsImlhdCI6MTc0NjgxMDM3NSwiZXhwIjoyMDYyMTcwMzc1fQ.Le6M7WL63nxtLTxccFacQR5GVUY5C-a_zGtmXT_uaz4
|
||||||
|
custom:
|
||||||
|
- template: "{{ states('sensor.st_00158555_temperature') | float(0) | round(1) }} °F"
|
||||||
|
label: Outdoor Temp
|
||||||
|
- template: "{{ states('sensor.st_00158555_feels_like') | float(0) | round(1) }} °F"
|
||||||
|
label: Feels Like
|
||||||
|
- template: "{{ states('sensor.st_00158555_humidity') | float(0) | round(1) }} %"
|
||||||
|
label: Humidity
|
||||||
|
- template: "{{ states('sensor.st_00158555_wind_speed') | float(0) | round(1) }} mph"
|
||||||
|
label: Wind Speed
|
||||||
|
"""
|
||||||
|
|
||||||
|
settings_yaml = """title: SnarfNet Operations & Telemetry
|
||||||
|
headerStyle: boxed
|
||||||
|
color: slate
|
||||||
|
theme: dark
|
||||||
|
statusStyle: badge
|
||||||
|
background:
|
||||||
|
image: https://i.imgur.com/hOzzkCY.jpeg
|
||||||
|
blur: sm
|
||||||
|
opacity: 50
|
||||||
|
brightness: 50
|
||||||
|
cardBlur: sm
|
||||||
|
quicklaunch:
|
||||||
|
searchDescriptions: true
|
||||||
|
hideInternetSearch: true
|
||||||
|
hideVisitURL: true
|
||||||
|
hideVersion: true
|
||||||
|
layout:
|
||||||
|
Critical Services:
|
||||||
|
icon: mdi-shield-check
|
||||||
|
style: row
|
||||||
|
columns: 3
|
||||||
|
GitOps & Automation:
|
||||||
|
icon: mdi-git
|
||||||
|
style: row
|
||||||
|
columns: 4
|
||||||
|
Storage & Clusters:
|
||||||
|
icon: mdi-server-network
|
||||||
|
style: row
|
||||||
|
columns: 3
|
||||||
|
Infrastructure & Observability:
|
||||||
|
icon: mdi-chart-timeline-variant
|
||||||
|
style: row
|
||||||
|
columns: 3
|
||||||
|
Environment & Weather:
|
||||||
|
icon: mdi-weather-partly-cloudy
|
||||||
|
style: row
|
||||||
|
columns: 1
|
||||||
|
showStats: true
|
||||||
|
providers:
|
||||||
|
longhorn:
|
||||||
|
url: http://longhorn-backend.longhorn-system.svc.cluster.local:9500
|
||||||
|
"""
|
||||||
|
|
||||||
|
widgets_yaml = """- kubernetes:
|
||||||
|
cluster:
|
||||||
|
show: true
|
||||||
|
cpu: true
|
||||||
|
memory: true
|
||||||
|
show_names: true
|
||||||
|
nodes:
|
||||||
|
show: false
|
||||||
|
- longhorn:
|
||||||
|
total: true
|
||||||
|
nodes: false
|
||||||
|
- resources:
|
||||||
|
label: System
|
||||||
|
backend: resources
|
||||||
|
cpu: true
|
||||||
|
expanded: true
|
||||||
|
memory: true
|
||||||
|
disk: /
|
||||||
|
- search:
|
||||||
|
provider: duckduckgo
|
||||||
|
target: _blank
|
||||||
|
"""
|
||||||
|
|
||||||
|
bookmarks_yaml = """- Quick Links:
|
||||||
|
- GitHub:
|
||||||
|
- abbr: GH
|
||||||
|
href: https://github.com/
|
||||||
|
- UniFi Site Manager:
|
||||||
|
- abbr: UI
|
||||||
|
href: https://unifi.ui.com/
|
||||||
|
- Cloudflare:
|
||||||
|
- abbr: CF
|
||||||
|
href: https://dash.cloudflare.com/
|
||||||
|
"""
|
||||||
|
|
||||||
|
cm['data']['services.yaml'] = services_yaml
|
||||||
|
cm['data']['settings.yaml'] = settings_yaml
|
||||||
|
cm['data']['widgets.yaml'] = widgets_yaml
|
||||||
|
cm['data']['bookmarks.yaml'] = bookmarks_yaml
|
||||||
|
cm['data']['kubernetes.yaml'] = "mode: cluster\n"
|
||||||
|
|
||||||
|
with open('homelab-gitops-repo/manifests/spaceport/homepage/homepage.yaml', 'w', encoding='utf-8') as f:
|
||||||
|
yaml.dump_all(docs, f, sort_keys=False, default_flow_style=False, allow_unicode=True)
|
||||||
|
|
||||||
|
print("Saved clean homelab-gitops-repo/manifests/spaceport/homepage/homepage.yaml successfully!")
|
||||||
33
scripts/apply_grafana_dashboards.py
Normal file
33
scripts/apply_grafana_dashboards.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import paramiko
|
||||||
|
import time
|
||||||
|
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('140.44.4.11', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||||
|
|
||||||
|
def run(cmd):
|
||||||
|
_, o, e = c.exec_command(cmd)
|
||||||
|
out = o.read().decode().strip()
|
||||||
|
err = e.read().decode().strip()
|
||||||
|
return out + ("\nERR: " + err if err else "")
|
||||||
|
|
||||||
|
with open("homelab-gitops-repo/manifests/aerodrome/grafana/grafana.yaml", "r", encoding="utf-8") as f:
|
||||||
|
grafana_yaml = f.read()
|
||||||
|
|
||||||
|
stdin, stdout, stderr = c.exec_command("cat > /tmp/grafana_updated.yaml")
|
||||||
|
stdin.write(grafana_yaml)
|
||||||
|
stdin.close()
|
||||||
|
stdout.channel.recv_exit_status()
|
||||||
|
|
||||||
|
print("Applying Grafana manifests:")
|
||||||
|
print(run("kubectl apply -f /tmp/grafana_updated.yaml"))
|
||||||
|
|
||||||
|
print("\nRestarting Grafana:")
|
||||||
|
print(run("kubectl rollout restart deployment/grafana -n default"))
|
||||||
|
print(run("kubectl rollout status deployment/grafana -n default --timeout=60s"))
|
||||||
|
|
||||||
|
print("\nWaiting for Grafana to finish initialization...")
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
c.close()
|
||||||
|
|
||||||
20
scripts/apply_grafana_server_side.py
Normal file
20
scripts/apply_grafana_server_side.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import paramiko
|
||||||
|
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('140.44.4.11', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||||
|
|
||||||
|
def run(cmd):
|
||||||
|
_, o, e = c.exec_command(cmd)
|
||||||
|
out = o.read().decode().strip()
|
||||||
|
err = e.read().decode().strip()
|
||||||
|
return out + ("\nERR: " + err if err else "")
|
||||||
|
|
||||||
|
print("Applying server-side:")
|
||||||
|
print(run("kubectl apply -f /tmp/grafana_full.yaml --server-side --force-conflicts"))
|
||||||
|
print("\nRollout restart:")
|
||||||
|
print(run("kubectl rollout restart deployment/grafana -n default"))
|
||||||
|
print(run("kubectl rollout status deployment/grafana -n default --timeout=60s"))
|
||||||
|
|
||||||
|
c.close()
|
||||||
|
|
||||||
149
scripts/apply_h21_authentik.py
Normal file
149
scripts/apply_h21_authentik.py
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import paramiko
|
||||||
|
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect("140.44.4.71", username="root", key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||||
|
|
||||||
|
adv_config = """# Buffer tuning for Authentik headers
|
||||||
|
proxy_buffers 8 16k;
|
||||||
|
proxy_buffer_size 32k;
|
||||||
|
|
||||||
|
# Outpost verification route - plain HTTP to internal VIP
|
||||||
|
location /outpost.goauthentik.io {
|
||||||
|
auth_request off;
|
||||||
|
proxy_pass http://140.44.4.29:9000/outpost.goauthentik.io;
|
||||||
|
proxy_set_header Host authentik.snarfnet.net;
|
||||||
|
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
|
||||||
|
add_header Set-Cookie $auth_cookie;
|
||||||
|
auth_request_set $auth_cookie $upstream_http_set_cookie;
|
||||||
|
proxy_pass_request_body off;
|
||||||
|
proxy_set_header Content-Length "";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Redirect to Authentik when unauthenticated
|
||||||
|
location @goauthentik_proxy_signin {
|
||||||
|
internal;
|
||||||
|
auth_request off;
|
||||||
|
add_header Set-Cookie $auth_cookie;
|
||||||
|
return 302 https://authentik.snarfnet.net/outpost.goauthentik.io/start?rd=$scheme://$http_host$request_uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Protected Topology HTML (Interactive)
|
||||||
|
location = /topology.html {
|
||||||
|
auth_request /outpost.goauthentik.io/auth/nginx;
|
||||||
|
error_page 401 = @goauthentik_proxy_signin;
|
||||||
|
auth_request_set $auth_cookie $upstream_http_set_cookie;
|
||||||
|
add_header Set-Cookie $auth_cookie;
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
|
expires 0;
|
||||||
|
|
||||||
|
alias /data/topology/snarfnet_homelab_topology.html;
|
||||||
|
default_type text/html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Protected Topology PNG (High-Res Image)
|
||||||
|
location = /topology.png {
|
||||||
|
auth_request /outpost.goauthentik.io/auth/nginx;
|
||||||
|
error_page 401 = @goauthentik_proxy_signin;
|
||||||
|
auth_request_set $auth_cookie $upstream_http_set_cookie;
|
||||||
|
add_header Set-Cookie $auth_cookie;
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
|
expires 0;
|
||||||
|
|
||||||
|
alias /data/topology/snarfnet_homelab_topology.png;
|
||||||
|
default_type image/png;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /snarfnet_homelab_topology.html {
|
||||||
|
auth_request /outpost.goauthentik.io/auth/nginx;
|
||||||
|
error_page 401 = @goauthentik_proxy_signin;
|
||||||
|
auth_request_set $auth_cookie $upstream_http_set_cookie;
|
||||||
|
add_header Set-Cookie $auth_cookie;
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
|
expires 0;
|
||||||
|
|
||||||
|
alias /data/topology/snarfnet_homelab_topology.html;
|
||||||
|
default_type text/html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /snarfnet_homelab_topology.png {
|
||||||
|
auth_request /outpost.goauthentik.io/auth/nginx;
|
||||||
|
error_page 401 = @goauthentik_proxy_signin;
|
||||||
|
auth_request_set $auth_cookie $upstream_http_set_cookie;
|
||||||
|
add_header Set-Cookie $auth_cookie;
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
|
expires 0;
|
||||||
|
|
||||||
|
alias /data/topology/snarfnet_homelab_topology.png;
|
||||||
|
default_type image/png;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
stdin, stdout, stderr = c.exec_command("cat > /tmp/adv_config_h21.txt")
|
||||||
|
stdin.write(adv_config)
|
||||||
|
stdin.close()
|
||||||
|
stdout.read()
|
||||||
|
|
||||||
|
remote_script = """import sqlite3, shutil, os
|
||||||
|
|
||||||
|
db_path = '/var/lib/docker/volumes/nginx-proxy-manager_nginx-data/_data/database.sqlite'
|
||||||
|
conf_path = '/var/lib/docker/volumes/nginx-proxy-manager_nginx-data/_data/nginx/proxy_host/21.conf'
|
||||||
|
|
||||||
|
with open('/tmp/adv_config_h21.txt', 'r') as f:
|
||||||
|
adv = f.read()
|
||||||
|
|
||||||
|
# Update DB
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("UPDATE proxy_host SET advanced_config = ? WHERE id = 21", (adv,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
print("Updated database for host 21.")
|
||||||
|
|
||||||
|
# Update 21.conf
|
||||||
|
with open(conf_path, 'r') as f:
|
||||||
|
conf = f.read()
|
||||||
|
|
||||||
|
# Strip existing adv_config block if present
|
||||||
|
tag = '# Buffer tuning for Authentik headers'
|
||||||
|
if tag in conf:
|
||||||
|
idx_start = conf.find(tag)
|
||||||
|
idx_end = conf.find('location / {', idx_start)
|
||||||
|
if idx_start != -1 and idx_end != -1:
|
||||||
|
new_conf = conf[:idx_start] + adv + "\\n\\n " + conf[idx_end:]
|
||||||
|
else:
|
||||||
|
new_conf = conf
|
||||||
|
else:
|
||||||
|
idx = conf.find('location / {')
|
||||||
|
if idx != -1:
|
||||||
|
new_conf = conf[:idx] + adv + "\\n\\n " + conf[idx:]
|
||||||
|
else:
|
||||||
|
new_conf = conf.rstrip().rstrip('}') + "\\n\\n" + adv + "\\n}\\n"
|
||||||
|
|
||||||
|
with open(conf_path, 'w') as f:
|
||||||
|
f.write(new_conf)
|
||||||
|
print("Updated 21.conf.")
|
||||||
|
"""
|
||||||
|
|
||||||
|
stdin, stdout, stderr = c.exec_command("cat > /tmp/apply_h21.py")
|
||||||
|
stdin.write(remote_script)
|
||||||
|
stdin.close()
|
||||||
|
stdout.read()
|
||||||
|
|
||||||
|
stdin, stdout, stderr = c.exec_command("python3 /tmp/apply_h21.py")
|
||||||
|
print("Remote python stdout:", stdout.read().decode())
|
||||||
|
print("Remote python stderr:", stderr.read().decode())
|
||||||
|
|
||||||
|
stdin, stdout, stderr = c.exec_command("docker exec $(docker ps -q --filter 'name=nginx-proxy-manager_app') nginx -t")
|
||||||
|
out = stdout.read().decode()
|
||||||
|
err = stderr.read().decode()
|
||||||
|
print("nginx -t stdout:", out)
|
||||||
|
print("nginx -t stderr:", err)
|
||||||
|
|
||||||
|
if "successful" in out or "successful" in err:
|
||||||
|
print("Nginx syntax OK! Reloading nginx...")
|
||||||
|
stdin, stdout, stderr = c.exec_command("docker exec $(docker ps -q --filter 'name=nginx-proxy-manager_app') nginx -s reload")
|
||||||
|
print("Reload stdout:", stdout.read().decode())
|
||||||
|
print("Reload stderr:", stderr.read().decode())
|
||||||
|
|
||||||
|
c.close()
|
||||||
31
scripts/apply_homepage.py
Normal file
31
scripts/apply_homepage.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import paramiko
|
||||||
|
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('140.44.4.41', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||||
|
|
||||||
|
def run(cmd):
|
||||||
|
print(f"\n>>> {cmd}")
|
||||||
|
_, o, e = c.exec_command(cmd)
|
||||||
|
out = o.read().decode().strip()
|
||||||
|
err = e.read().decode().strip()
|
||||||
|
if out:
|
||||||
|
print(out)
|
||||||
|
if err:
|
||||||
|
print("[stderr]", err)
|
||||||
|
return out
|
||||||
|
|
||||||
|
with open('homelab-gitops-repo/manifests/spaceport/homepage/homepage.yaml', 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
stdin, stdout, stderr = c.exec_command('cat > /tmp/homepage.yaml')
|
||||||
|
stdin.write(content)
|
||||||
|
stdin.close()
|
||||||
|
stdout.read()
|
||||||
|
|
||||||
|
run("ls -lh /tmp/homepage.yaml")
|
||||||
|
run("kubectl apply -f /tmp/homepage.yaml")
|
||||||
|
run("kubectl rollout restart deployment/homepage-2-1744068158 -n infrastructure")
|
||||||
|
run("kubectl rollout status deployment/homepage-2-1744068158 -n infrastructure --timeout=60s")
|
||||||
|
|
||||||
|
c.close()
|
||||||
140
scripts/apply_npm_fixes.py
Normal file
140
scripts/apply_npm_fixes.py
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
key_path = r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet'
|
||||||
|
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect("140.44.4.71", username="root", key_filename=key_path, timeout=10)
|
||||||
|
|
||||||
|
print("1. Creating backup of database and configs...")
|
||||||
|
backup_cmd = """
|
||||||
|
cp /var/lib/docker/volumes/nginx-proxy-manager_nginx-data/_data/database.sqlite /var/lib/docker/volumes/nginx-proxy-manager_nginx-data/_data/database.sqlite.bak.$(date +%s)
|
||||||
|
"""
|
||||||
|
stdin, stdout, stderr = c.exec_command(backup_cmd)
|
||||||
|
stdout.channel.recv_exit_status()
|
||||||
|
print("Backup complete.")
|
||||||
|
|
||||||
|
print("2. Updating database.sqlite for hosts 45, 41, and 37...")
|
||||||
|
py_db = """
|
||||||
|
import sqlite3, datetime
|
||||||
|
conn = sqlite3.connect("/var/lib/docker/volumes/nginx-proxy-manager_nginx-data/_data/database.sqlite")
|
||||||
|
cur = conn.cursor()
|
||||||
|
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
updates = [
|
||||||
|
(45, "140.44.4.34", 443, "https"), # Longhorn -> Aerodrome Traefik
|
||||||
|
(41, "140.44.4.62", 443, "https"), # Semaphore -> Spaceport Traefik
|
||||||
|
(37, "140.44.4.34", 443, "https"), # Uptime Kuma -> Aerodrome Traefik
|
||||||
|
]
|
||||||
|
|
||||||
|
for pid, host, port, scheme in updates:
|
||||||
|
cur.execute('''UPDATE proxy_host
|
||||||
|
SET forward_host = ?, forward_port = ?, forward_scheme = ?,
|
||||||
|
allow_websocket_upgrade = 1, http2_support = 1, modified_on = ?
|
||||||
|
WHERE id = ?''', (host, port, scheme, now, pid))
|
||||||
|
print(f"DB updated: Host {pid} -> {scheme}://{host}:{port}")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
"""
|
||||||
|
stdin, stdout, stderr = c.exec_command(f"python3 -c '{py_db}'")
|
||||||
|
print(stdout.read().decode())
|
||||||
|
err = stderr.read().decode()
|
||||||
|
if err: print("DB err:", err)
|
||||||
|
|
||||||
|
print("3. Generating updated nginx configuration files...")
|
||||||
|
|
||||||
|
def generate_conf(pid, domains, cert_id, host, port, scheme):
|
||||||
|
return f"""# ------------------------------------------------------------
|
||||||
|
# {domains}
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
map $scheme $hsts_header {{
|
||||||
|
https "max-age=63072000; preload";
|
||||||
|
}}
|
||||||
|
|
||||||
|
server {{
|
||||||
|
set $forward_scheme {scheme};
|
||||||
|
set $server "{host}";
|
||||||
|
set $port {port};
|
||||||
|
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
|
||||||
|
server_name {domains};
|
||||||
|
http2 on;
|
||||||
|
|
||||||
|
# Let's Encrypt SSL
|
||||||
|
include conf.d/include/letsencrypt-acme-challenge.conf;
|
||||||
|
include conf.d/include/ssl-cache.conf;
|
||||||
|
include conf.d/include/ssl-ciphers.conf;
|
||||||
|
ssl_certificate /etc/letsencrypt/live/npm-{cert_id}/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/npm-{cert_id}/privkey.pem;
|
||||||
|
|
||||||
|
# Asset Caching
|
||||||
|
include conf.d/include/assets.conf;
|
||||||
|
|
||||||
|
# Block Exploits
|
||||||
|
include conf.d/include/block-exploits.conf;
|
||||||
|
|
||||||
|
# Force SSL
|
||||||
|
set $trust_forwarded_proto "F";
|
||||||
|
include conf.d/include/force-ssl.conf;
|
||||||
|
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $http_connection;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
access_log /data/logs/proxy-host-{pid}_access.log proxy;
|
||||||
|
error_log /data/logs/proxy-host-{pid}_error.log warn;
|
||||||
|
|
||||||
|
location / {{
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $http_connection;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
# Proxy!
|
||||||
|
include conf.d/include/proxy.conf;
|
||||||
|
}}
|
||||||
|
|
||||||
|
# Custom
|
||||||
|
include /data/nginx/custom/server_proxy[.]conf;
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
configs = [
|
||||||
|
(45, "longhorn.aerodrome.snarfnet.net", 37, "140.44.4.34", 443, "https"),
|
||||||
|
(41, "semaphore.snarfnet.net", 33, "140.44.4.62", 443, "https"),
|
||||||
|
(37, "uptime.snarfnet.net", 27, "140.44.4.34", 443, "https"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for pid, dom, cert_id, host, port, scheme in configs:
|
||||||
|
cfg = generate_conf(pid, dom, cert_id, host, port, scheme)
|
||||||
|
path = f"/var/lib/docker/volumes/nginx-proxy-manager_nginx-data/_data/nginx/proxy_host/{pid}.conf"
|
||||||
|
cmd = f"cat << 'EOF' > {path}\n{cfg}\nEOF\n"
|
||||||
|
stdin, stdout, stderr = c.exec_command(cmd)
|
||||||
|
stdout.channel.recv_exit_status()
|
||||||
|
print(f"Wrote config: {path}")
|
||||||
|
|
||||||
|
print("4. Testing Nginx config inside container...")
|
||||||
|
stdin, stdout, stderr = c.exec_command("docker exec $(docker ps -q -f name=nginx-proxy-manager_app | head -n1) nginx -t")
|
||||||
|
out = stdout.read().decode()
|
||||||
|
err = stderr.read().decode()
|
||||||
|
print("nginx -t stdout:", out)
|
||||||
|
print("nginx -t stderr:", err)
|
||||||
|
|
||||||
|
if "successful" in out or "successful" in err:
|
||||||
|
print("5. Reloading Nginx via Swarm service update...")
|
||||||
|
stdin, stdout, stderr = c.exec_command("docker service update --force nginx-proxy-manager_app")
|
||||||
|
print(stdout.read().decode())
|
||||||
|
print("Reload completed.")
|
||||||
|
else:
|
||||||
|
print("ERROR: Nginx config test failed!")
|
||||||
|
|
||||||
|
c.close()
|
||||||
|
|
||||||
26
scripts/apply_pihole_redirect.py
Normal file
26
scripts/apply_pihole_redirect.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import paramiko
|
||||||
|
|
||||||
|
def apply_manifest(host, cluster_name, file_path):
|
||||||
|
print(f"Applying {file_path} to {cluster_name} ({host})...")
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
manifest_content = f.read()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
stdin, stdout, stderr = ssh.exec_command('k3s kubectl apply -f -')
|
||||||
|
stdin.write(manifest_content)
|
||||||
|
stdin.channel.shutdown_write()
|
||||||
|
|
||||||
|
out = stdout.read().decode('utf-8')
|
||||||
|
err = stderr.read().decode('utf-8')
|
||||||
|
print("Output:")
|
||||||
|
print(out)
|
||||||
|
if err:
|
||||||
|
print("Stderr:", err)
|
||||||
|
ssh.close()
|
||||||
|
|
||||||
|
apply_manifest('140.44.4.11', 'Aerodrome', r'homelab-gitops-repo/manifests/aerodrome/dns/dns-stack.yaml')
|
||||||
|
apply_manifest('140.44.4.41', 'Spaceport', r'homelab-gitops-repo/manifests/spaceport/dns/dns-stack.yaml')
|
||||||
|
|
||||||
45
scripts/apply_prom_grafana.py
Normal file
45
scripts/apply_prom_grafana.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import paramiko
|
||||||
|
import time
|
||||||
|
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('140.44.4.11', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||||
|
|
||||||
|
def run(cmd):
|
||||||
|
_, o, e = c.exec_command(cmd)
|
||||||
|
out = o.read().decode().strip()
|
||||||
|
err = e.read().decode().strip()
|
||||||
|
return out + ("\nERR: " + err if err else "")
|
||||||
|
|
||||||
|
with open("homelab-gitops-repo/manifests/aerodrome/prometheus/prometheus.yaml", "r") as f:
|
||||||
|
prom_yaml = f.read()
|
||||||
|
|
||||||
|
stdin, stdout, stderr = c.exec_command("cat > /tmp/prometheus.yaml")
|
||||||
|
stdin.write(prom_yaml)
|
||||||
|
stdin.close()
|
||||||
|
stdout.channel.recv_exit_status()
|
||||||
|
|
||||||
|
print("Applying Prometheus:")
|
||||||
|
print(run("kubectl apply -f /tmp/prometheus.yaml"))
|
||||||
|
|
||||||
|
with open("homelab-gitops-repo/manifests/aerodrome/grafana/grafana.yaml", "r") as f:
|
||||||
|
grafana_yaml = f.read()
|
||||||
|
|
||||||
|
stdin, stdout, stderr = c.exec_command("cat > /tmp/grafana_full.yaml")
|
||||||
|
stdin.write(grafana_yaml)
|
||||||
|
stdin.close()
|
||||||
|
stdout.channel.recv_exit_status()
|
||||||
|
|
||||||
|
print("\nApplying Grafana configuration & dashboards:")
|
||||||
|
print(run("kubectl apply -f /tmp/grafana_full.yaml"))
|
||||||
|
|
||||||
|
print("\nRestarting Grafana:")
|
||||||
|
print(run("kubectl rollout restart deployment/grafana -n default"))
|
||||||
|
print(run("kubectl rollout status deployment/grafana -n default --timeout=60s"))
|
||||||
|
|
||||||
|
print("\nChecking Prometheus pod:")
|
||||||
|
time.sleep(5)
|
||||||
|
print(run("kubectl get pods -l app.kubernetes.io/name=prometheus"))
|
||||||
|
|
||||||
|
c.close()
|
||||||
|
|
||||||
36
scripts/apply_prom_update.py
Normal file
36
scripts/apply_prom_update.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import paramiko
|
||||||
|
import time
|
||||||
|
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('140.44.4.11', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||||
|
|
||||||
|
def run(cmd):
|
||||||
|
_, o, e = c.exec_command(cmd)
|
||||||
|
out = o.read().decode().strip()
|
||||||
|
err = e.read().decode().strip()
|
||||||
|
return out + ("\nERR: " + err if err else "")
|
||||||
|
|
||||||
|
with open("homelab-gitops-repo/manifests/aerodrome/prometheus/prometheus.yaml", "r", encoding="utf-8") as f:
|
||||||
|
prom_yaml = f.read()
|
||||||
|
|
||||||
|
stdin, stdout, stderr = c.exec_command("cat > /tmp/prometheus_updated.yaml")
|
||||||
|
stdin.write(prom_yaml)
|
||||||
|
stdin.close()
|
||||||
|
stdout.channel.recv_exit_status()
|
||||||
|
|
||||||
|
print("Applying Prometheus:")
|
||||||
|
print(run("kubectl apply -f /tmp/prometheus_updated.yaml"))
|
||||||
|
|
||||||
|
print("\nRestarting Prometheus:")
|
||||||
|
print(run("kubectl rollout restart deployment/prometheus -n default"))
|
||||||
|
print(run("kubectl rollout status deployment/prometheus -n default --timeout=60s"))
|
||||||
|
|
||||||
|
print("\nWaiting 15 seconds for scrape loop...")
|
||||||
|
time.sleep(15)
|
||||||
|
|
||||||
|
print("\nPrometheus active targets count:")
|
||||||
|
print(run("kubectl exec deployment/prometheus -- wget -qO- http://localhost:9090/api/v1/targets | grep -o '\"health\":\"up\"' | wc -l"))
|
||||||
|
|
||||||
|
c.close()
|
||||||
|
|
||||||
65
scripts/apply_recurring_jobs.py
Normal file
65
scripts/apply_recurring_jobs.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import paramiko
|
||||||
|
|
||||||
|
manifest = """
|
||||||
|
apiVersion: longhorn.io/v1beta2
|
||||||
|
kind: RecurringJob
|
||||||
|
metadata:
|
||||||
|
name: snapshot-4h
|
||||||
|
namespace: longhorn-system
|
||||||
|
spec:
|
||||||
|
name: snapshot-4h
|
||||||
|
task: snapshot
|
||||||
|
cron: "0 */4 * * *"
|
||||||
|
retain: 6
|
||||||
|
concurrency: 2
|
||||||
|
groups:
|
||||||
|
- default
|
||||||
|
---
|
||||||
|
apiVersion: longhorn.io/v1beta2
|
||||||
|
kind: RecurringJob
|
||||||
|
metadata:
|
||||||
|
name: snapshot-daily
|
||||||
|
namespace: longhorn-system
|
||||||
|
spec:
|
||||||
|
name: snapshot-daily
|
||||||
|
task: snapshot
|
||||||
|
cron: "0 2 * * *"
|
||||||
|
retain: 7
|
||||||
|
concurrency: 2
|
||||||
|
groups:
|
||||||
|
- default
|
||||||
|
---
|
||||||
|
apiVersion: longhorn.io/v1beta2
|
||||||
|
kind: RecurringJob
|
||||||
|
metadata:
|
||||||
|
name: backup-daily-nas
|
||||||
|
namespace: longhorn-system
|
||||||
|
spec:
|
||||||
|
name: backup-daily-nas
|
||||||
|
task: backup
|
||||||
|
cron: "0 3 * * *"
|
||||||
|
retain: 14
|
||||||
|
concurrency: 2
|
||||||
|
groups:
|
||||||
|
- default
|
||||||
|
"""
|
||||||
|
|
||||||
|
clusters = [
|
||||||
|
("aerodrome", "140.44.4.11"),
|
||||||
|
("spaceport", "140.44.4.41")
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, host in clusters:
|
||||||
|
print(f"=== Applying RecurringJobs 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)
|
||||||
|
|
||||||
|
stdin, stdout, stderr = ssh.exec_command('cat << \'EOF\' | kubectl apply -f -\n' + manifest + '\nEOF')
|
||||||
|
print("APPLY STDOUT:", stdout.read().decode())
|
||||||
|
print("APPLY STDERR:", stderr.read().decode())
|
||||||
|
|
||||||
|
stdin, stdout, stderr = ssh.exec_command('kubectl get recurringjobs.longhorn.io -n longhorn-system')
|
||||||
|
print("CURRENT JOBS:\n", stdout.read().decode())
|
||||||
|
ssh.close()
|
||||||
|
|
||||||
72
scripts/apply_semaphore_fix.py
Normal file
72
scripts/apply_semaphore_fix.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import paramiko
|
||||||
|
import urllib.request
|
||||||
|
import ssl
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
key_path = r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet'
|
||||||
|
|
||||||
|
print("1. Updating Authentik Provider 10 redirect_uris...")
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
token = "SnarfNetPermanentAutomationToken2026"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
provider_update = {
|
||||||
|
"redirect_uris": [
|
||||||
|
{"matching_mode": "strict", "url": "https://semaphore.snarfnet.net/auth/oidc/authentik/redirect"},
|
||||||
|
{"matching_mode": "strict", "url": "https://semaphore.snarfnet.net/api/auth/oidc/authentik/redirect/"},
|
||||||
|
{"matching_mode": "regex", "url": "https://semaphore\\.snarfnet\\.net/.*"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
req = urllib.request.Request(
|
||||||
|
"https://authentik.snarfnet.net/api/v3/providers/oauth2/10/",
|
||||||
|
headers=headers,
|
||||||
|
data=json.dumps(provider_update).encode('utf-8'),
|
||||||
|
method="PATCH"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, context=ctx, timeout=8) as resp:
|
||||||
|
res = json.loads(resp.read().decode())
|
||||||
|
print(" ✓ Authentik Provider 10 updated successfully:", res.get('redirect_uris'))
|
||||||
|
except Exception as e:
|
||||||
|
print(" ✗ Failed to update Authentik Provider 10:", e)
|
||||||
|
|
||||||
|
print("\n2. Applying updated Semaphore manifest to Spaceport (140.44.4.41)...")
|
||||||
|
manifest_path = r"C:\Users\Snarf\OneDrive\Desktop\code\SnarfCode\homelab-gitops-repo\manifests\spaceport\semaphore\semaphore.yaml"
|
||||||
|
with open(manifest_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
ssh = paramiko.SSHClient()
|
||||||
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
ssh.connect("140.44.4.41", username="root", key_filename=key_path, timeout=10)
|
||||||
|
|
||||||
|
stdin, stdout, stderr = ssh.exec_command("k3s kubectl apply -f -")
|
||||||
|
stdin.write(content)
|
||||||
|
stdin.channel.shutdown_write()
|
||||||
|
print("Apply output:", stdout.read().decode())
|
||||||
|
err = stderr.read().decode()
|
||||||
|
if err: print("Apply stderr:", err)
|
||||||
|
|
||||||
|
print("3. Rolling out Semaphore deployment...")
|
||||||
|
stdin, stdout, stderr = ssh.exec_command("k3s kubectl rollout restart deployment/semaphore -n infrastructure; k3s kubectl rollout status deployment/semaphore -n infrastructure --timeout=90s")
|
||||||
|
print(stdout.read().decode())
|
||||||
|
|
||||||
|
# Check new pod env
|
||||||
|
stdin, stdout, stderr = ssh.exec_command("k3s kubectl get pods -n infrastructure -l app.kubernetes.io/name=semaphore -o jsonpath='{.items[0].metadata.name}'")
|
||||||
|
new_pod = stdout.read().decode().strip()
|
||||||
|
print(f"New pod: {new_pod}")
|
||||||
|
|
||||||
|
stdin, stdout, stderr = ssh.exec_command(f"k3s kubectl exec -n infrastructure {new_pod} -- env | grep SEMAPHORE_OIDC_PROVIDERS")
|
||||||
|
print("New env:", stdout.read().decode())
|
||||||
|
|
||||||
|
ssh.close()
|
||||||
|
print("Done!")
|
||||||
|
|
||||||
161
scripts/apply_synology_fixes.py
Normal file
161
scripts/apply_synology_fixes.py
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
key_path = r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet'
|
||||||
|
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect("140.44.4.71", username="root", key_filename=key_path, timeout=10)
|
||||||
|
|
||||||
|
print("1. Updating SQLite database for hosts 11 and 28...")
|
||||||
|
dsm_adv = """proxy_buffer_size 128k;
|
||||||
|
proxy_buffers 8 256k;
|
||||||
|
proxy_busy_buffers_size 256k;
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
proxy_send_timeout 3600s;
|
||||||
|
proxy_ssl_verify off;"""
|
||||||
|
|
||||||
|
py_db = f"""
|
||||||
|
import sqlite3, datetime
|
||||||
|
conn = sqlite3.connect('/var/lib/docker/volumes/nginx-proxy-manager_nginx-data/_data/database.sqlite')
|
||||||
|
cur = conn.cursor()
|
||||||
|
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
adv = '''{dsm_adv}'''
|
||||||
|
|
||||||
|
# Host 11: sn-nas.snarfnet.net -> 14.10.10.71:5001
|
||||||
|
cur.execute('''UPDATE proxy_host
|
||||||
|
SET forward_host = "14.10.10.71", forward_port = 5001, forward_scheme = "https",
|
||||||
|
allow_websocket_upgrade = 1, http2_support = 1, advanced_config = ?, modified_on = ?
|
||||||
|
WHERE id = 11''', (adv, now))
|
||||||
|
|
||||||
|
# Host 28: snarfnas.snarfnet.net -> 14.10.10.72:5001 (Clean DSM, remove dead outpost)
|
||||||
|
cur.execute('''UPDATE proxy_host
|
||||||
|
SET forward_host = "14.10.10.72", forward_port = 5001, forward_scheme = "https",
|
||||||
|
allow_websocket_upgrade = 1, http2_support = 1, advanced_config = ?, modified_on = ?
|
||||||
|
WHERE id = 28''', (adv, now))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print("Database updated for hosts 11 and 28.")
|
||||||
|
conn.close()
|
||||||
|
"""
|
||||||
|
|
||||||
|
sftp = c.open_sftp()
|
||||||
|
with sftp.open('/tmp/update_nas_db.py', 'w') as f:
|
||||||
|
f.write(py_db)
|
||||||
|
sftp.close()
|
||||||
|
|
||||||
|
stdin, stdout, stderr = c.exec_command("python3 /tmp/update_nas_db.py")
|
||||||
|
print(stdout.read().decode())
|
||||||
|
err = stderr.read().decode()
|
||||||
|
if err: print("DB err:", err)
|
||||||
|
|
||||||
|
print("2. Generating Nginx configuration files for hosts 11 and 28...")
|
||||||
|
|
||||||
|
def make_dsm_conf(pid, domain, cert_id, host, port):
|
||||||
|
return f"""# ------------------------------------------------------------
|
||||||
|
# {domain}
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
map $scheme $hsts_header {{
|
||||||
|
https "max-age=63072000;includeSubDomains; preload";
|
||||||
|
}}
|
||||||
|
|
||||||
|
server {{
|
||||||
|
set $forward_scheme https;
|
||||||
|
set $server "{host}";
|
||||||
|
set $port {port};
|
||||||
|
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
|
||||||
|
server_name {domain};
|
||||||
|
http2 on;
|
||||||
|
|
||||||
|
# Let's Encrypt SSL
|
||||||
|
include conf.d/include/letsencrypt-acme-challenge.conf;
|
||||||
|
include conf.d/include/ssl-cache.conf;
|
||||||
|
include conf.d/include/ssl-ciphers.conf;
|
||||||
|
ssl_certificate /etc/letsencrypt/live/npm-{cert_id}/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/npm-{cert_id}/privkey.pem;
|
||||||
|
|
||||||
|
# Asset Caching
|
||||||
|
include conf.d/include/assets.conf;
|
||||||
|
|
||||||
|
# Block Exploits
|
||||||
|
include conf.d/include/block-exploits.conf;
|
||||||
|
|
||||||
|
# HSTS
|
||||||
|
add_header Strict-Transport-Security $hsts_header always;
|
||||||
|
|
||||||
|
# Force SSL
|
||||||
|
set $trust_forwarded_proto "F";
|
||||||
|
include conf.d/include/force-ssl.conf;
|
||||||
|
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $http_connection;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
access_log /data/logs/proxy-host-{pid}_access.log proxy;
|
||||||
|
error_log /data/logs/proxy-host-{pid}_error.log warn;
|
||||||
|
|
||||||
|
location / {{
|
||||||
|
# HSTS
|
||||||
|
add_header Strict-Transport-Security $hsts_header always;
|
||||||
|
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $http_connection;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
# DSM Buffer Tuning
|
||||||
|
proxy_buffer_size 128k;
|
||||||
|
proxy_buffers 8 256k;
|
||||||
|
proxy_busy_buffers_size 256k;
|
||||||
|
proxy_read_timeout 3600s;
|
||||||
|
proxy_send_timeout 3600s;
|
||||||
|
proxy_ssl_verify off;
|
||||||
|
|
||||||
|
# Proxy!
|
||||||
|
include conf.d/include/proxy.conf;
|
||||||
|
}}
|
||||||
|
|
||||||
|
# Custom
|
||||||
|
include /data/nginx/custom/server_proxy[.]conf;
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
conf11 = make_dsm_conf(11, "sn-nas.snarfnet.net", 12, "14.10.10.71", 5001)
|
||||||
|
conf28 = make_dsm_conf(28, "snarfnas.snarfnet.net", 22, "14.10.10.72", 5001)
|
||||||
|
|
||||||
|
path11 = "/var/lib/docker/volumes/nginx-proxy-manager_nginx-data/_data/nginx/proxy_host/11.conf"
|
||||||
|
path28 = "/var/lib/docker/volumes/nginx-proxy-manager_nginx-data/_data/nginx/proxy_host/28.conf"
|
||||||
|
|
||||||
|
stdin, stdout, stderr = c.exec_command(f"cat << 'EOF' > {path11}\n{conf11}\nEOF\n")
|
||||||
|
stdout.channel.recv_exit_status()
|
||||||
|
print(f"Wrote {path11}")
|
||||||
|
|
||||||
|
stdin, stdout, stderr = c.exec_command(f"cat << 'EOF' > {path28}\n{conf28}\nEOF\n")
|
||||||
|
stdout.channel.recv_exit_status()
|
||||||
|
print(f"Wrote {path28}")
|
||||||
|
|
||||||
|
print("3. Testing Nginx configuration...")
|
||||||
|
stdin, stdout, stderr = c.exec_command("docker exec $(docker ps -q -f name=nginx-proxy-manager_app | head -n1) nginx -t")
|
||||||
|
out = stdout.read().decode()
|
||||||
|
err = stderr.read().decode()
|
||||||
|
print("nginx -t stdout:", out)
|
||||||
|
print("nginx -t stderr:", err)
|
||||||
|
|
||||||
|
if "successful" in out or "successful" in err:
|
||||||
|
print("4. Reloading Nginx Swarm service...")
|
||||||
|
stdin, stdout, stderr = c.exec_command("docker service update --force nginx-proxy-manager_app")
|
||||||
|
print(stdout.read().decode())
|
||||||
|
print("Reload complete!")
|
||||||
|
else:
|
||||||
|
print("ERROR: Nginx config invalid!")
|
||||||
|
|
||||||
|
c.close()
|
||||||
|
|
||||||
22
scripts/apply_web_vips.py
Normal file
22
scripts/apply_web_vips.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
import base64
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
key_path = r"C:\Users\Snarf\.ssh\id_ed25519_snarfnet"
|
||||||
|
|
||||||
|
def apply_manifest(host, file_path):
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
b64 = base64.b64encode(f.read()).decode('ascii')
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect(host, username="root", key_filename=key_path, timeout=5)
|
||||||
|
stdin, stdout, stderr = c.exec_command(f"echo '{b64}' | base64 -d | k3s kubectl apply -f -")
|
||||||
|
print(f"Apply on {host}:")
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
c.close()
|
||||||
|
|
||||||
|
apply_manifest("140.44.4.11", r"C:\Users\Snarf\OneDrive\Desktop\code\SnarfCode\homelab-gitops-repo\manifests\aerodrome\dns\dns-stack.yaml")
|
||||||
|
apply_manifest("140.44.4.41", r"C:\Users\Snarf\OneDrive\Desktop\code\SnarfCode\homelab-gitops-repo\manifests\spaceport\dns\dns-stack.yaml")
|
||||||
|
|
||||||
4
scripts/assign_letter.txt
Normal file
4
scripts/assign_letter.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
select disk 4
|
||||||
|
select partition 1
|
||||||
|
assign letter=H
|
||||||
|
|
||||||
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()
|
||||||
30
scripts/audit_authentik.py
Normal file
30
scripts/audit_authentik.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
key_path = r"C:\Users\Snarf\.ssh\id_ed25519_snarfnet"
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect("140.44.4.11", username="root", key_filename=key_path, timeout=5)
|
||||||
|
|
||||||
|
def run_query(title, q):
|
||||||
|
cmd = f"""k3s kubectl -n infra exec authentik-postgresql-0 -- bash -c "PGPASSWORD=\\$(cat /opt/bitnami/postgresql/secrets/password) psql -U authentik -d authentik -c \\"{q}\\"" """
|
||||||
|
stdin, stdout, stderr = c.exec_command(cmd)
|
||||||
|
print(f"=== {title} ===")
|
||||||
|
out = stdout.read().decode('utf-8')
|
||||||
|
err = stderr.read().decode('utf-8')
|
||||||
|
if out.strip(): print(out.strip())
|
||||||
|
if err.strip(): print("ERR:", err.strip())
|
||||||
|
print()
|
||||||
|
|
||||||
|
run_query("OUTPOST PROVIDERS", "SELECT outpost_id, provider_id FROM authentik_outposts_outpost_providers;")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
c.close()
|
||||||
50
scripts/audit_awx.py
Normal file
50
scripts/audit_awx.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import json, ssl, urllib.request
|
||||||
|
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
AWX_URL = "http://140.44.4.30:87/api/v2"
|
||||||
|
ADMIN_USER = "admin"
|
||||||
|
ADMIN_PASS = "CKFj2RfRzNcIeNJIimoW2uZ9En25P4VL"
|
||||||
|
|
||||||
|
import base64
|
||||||
|
auth_header = "Basic " + base64.b64encode(f"{ADMIN_USER}:{ADMIN_PASS}".encode()).decode()
|
||||||
|
|
||||||
|
def awx_get(endpoint):
|
||||||
|
req = urllib.request.Request(f"{AWX_URL}/{endpoint}/", headers={"Authorization": auth_header})
|
||||||
|
with urllib.request.urlopen(req, timeout=10, context=ctx) as r:
|
||||||
|
return json.loads(r.read().decode())
|
||||||
|
|
||||||
|
try:
|
||||||
|
me = awx_get("me")
|
||||||
|
print("AWX Auth Success! User:", me["results"][0]["username"])
|
||||||
|
|
||||||
|
orgs = awx_get("organizations")
|
||||||
|
print(f"\nOrganizations ({orgs['count']}):")
|
||||||
|
for o in orgs["results"]:
|
||||||
|
print(f" - [{o['id']}] {o['name']}")
|
||||||
|
|
||||||
|
creds = awx_get("credentials")
|
||||||
|
print(f"\nCredentials ({creds['count']}):")
|
||||||
|
for c in creds["results"]:
|
||||||
|
print(f" - [{c['id']}] {c['name']} (Type: {c['credential_type']})")
|
||||||
|
|
||||||
|
inventories = awx_get("inventories")
|
||||||
|
print(f"\nInventories ({inventories['count']}):")
|
||||||
|
for inv in inventories["results"]:
|
||||||
|
print(f" - [{inv['id']}] {inv['name']}")
|
||||||
|
|
||||||
|
projects = awx_get("projects")
|
||||||
|
print(f"\nProjects ({projects['count']}):")
|
||||||
|
for p in projects["results"]:
|
||||||
|
print(f" - [{p['id']}] {p['name']} (Status: {p['status']}, SCM: {p['scm_type']})")
|
||||||
|
|
||||||
|
templates = awx_get("job_templates")
|
||||||
|
print(f"\nJob Templates ({templates['count']}):")
|
||||||
|
for t in templates["results"]:
|
||||||
|
print(f" - [{t['id']}] {t['name']}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print("AWX Query Error:", e)
|
||||||
|
|
||||||
21
scripts/audit_cluster_workloads.py
Normal file
21
scripts/audit_cluster_workloads.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import paramiko
|
||||||
|
|
||||||
|
def inspect_cluster(ip, name):
|
||||||
|
print(f"\n=======================================================")
|
||||||
|
print(f"=== {name} WORKLOADS ({ip}) ===")
|
||||||
|
print(f"=======================================================")
|
||||||
|
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 = "kubectl get deploy,sts,ds -A -o custom-columns=NAMESPACE:.metadata.namespace,KIND:.kind,NAME:.metadata.name,IMAGES:.spec.template.spec.containers[*].image"
|
||||||
|
_, out, _ = ssh.exec_command(cmd)
|
||||||
|
lines = out.read().decode().splitlines()
|
||||||
|
for l in lines:
|
||||||
|
if not any(k in l for k in ['kube-system', 'cattle-', 'metallb-system', 'longhorn-system']):
|
||||||
|
print(l)
|
||||||
|
ssh.close()
|
||||||
|
|
||||||
|
inspect_cluster('140.44.4.11', 'AERODROME')
|
||||||
|
inspect_cluster('140.44.4.41', 'SPACEPORT')
|
||||||
|
|
||||||
40
scripts/audit_gitops_manifests.py
Normal file
40
scripts/audit_gitops_manifests.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import paramiko
|
||||||
|
|
||||||
|
manifest_root = r"c:\Users\Snarf\OneDrive\Desktop\code\SnarfCode\homelab-gitops-repo\manifests"
|
||||||
|
|
||||||
|
print("=== GITOPS MANIFEST IMAGE AUDIT ===")
|
||||||
|
|
||||||
|
image_re = re.compile(r'image:\s*([^\s"\']+)', re.IGNORECASE)
|
||||||
|
chart_re = re.compile(r'chart:\s*([^\s"\']+)', re.IGNORECASE)
|
||||||
|
version_re = re.compile(r'version:\s*([^\s"\']+)', re.IGNORECASE)
|
||||||
|
|
||||||
|
manifest_data = {}
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(manifest_root):
|
||||||
|
for f in files:
|
||||||
|
if f.endswith('.yaml') or f.endswith('.yml'):
|
||||||
|
path = os.path.join(root, f)
|
||||||
|
rel_path = os.path.relpath(path, manifest_root)
|
||||||
|
with open(path, 'r', encoding='utf-8', errors='ignore') as fp:
|
||||||
|
content = fp.read()
|
||||||
|
images = image_re.findall(content)
|
||||||
|
charts = chart_re.findall(content)
|
||||||
|
versions = version_re.findall(content)
|
||||||
|
if images or charts:
|
||||||
|
manifest_data[rel_path] = {
|
||||||
|
'images': list(set(images)),
|
||||||
|
'charts': list(set(charts)),
|
||||||
|
'versions': list(set(versions))
|
||||||
|
}
|
||||||
|
|
||||||
|
for rel_path, data in sorted(manifest_data.items()):
|
||||||
|
print(f"\nFile: {rel_path}")
|
||||||
|
if data['images']:
|
||||||
|
print(" Images:", ", ".join(data['images']))
|
||||||
|
if data['charts']:
|
||||||
|
print(" Charts:", ", ".join(data['charts']))
|
||||||
|
if data['versions']:
|
||||||
|
print(" Versions:", ", ".join(data['versions']))
|
||||||
|
|
||||||
75
scripts/audit_harvester_hardware.py
Normal file
75
scripts/audit_harvester_hardware.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import paramiko
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
|
||||||
|
nodes = {
|
||||||
|
"Node 1 (snaihvs0lnx01)": "140.44.44.121",
|
||||||
|
"Node 2 (snaihvs0lnx02)": "140.44.44.122",
|
||||||
|
"Node 3 (snaihvs0lnx03)": "140.44.44.123",
|
||||||
|
"Node 4 (idrac-spacedock4)": "140.44.44.124",
|
||||||
|
}
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
for name, ip in nodes.items():
|
||||||
|
print(f"Connecting to {name} ({ip})...")
|
||||||
|
node_data = {}
|
||||||
|
try:
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect(ip, username="root", password="Katzir476!", timeout=10)
|
||||||
|
chan = c.invoke_shell()
|
||||||
|
time.sleep(2)
|
||||||
|
if chan.recv_ready():
|
||||||
|
chan.recv(4096)
|
||||||
|
|
||||||
|
def send_and_recv(cmd, wait_time=2.5):
|
||||||
|
chan.send(cmd + "\r\n")
|
||||||
|
time.sleep(wait_time)
|
||||||
|
out = ""
|
||||||
|
while chan.recv_ready():
|
||||||
|
out += chan.recv(8192).decode('utf-8', errors='ignore')
|
||||||
|
time.sleep(0.5)
|
||||||
|
return out
|
||||||
|
|
||||||
|
# 1. BIOS Settings
|
||||||
|
raw_bios = send_and_recv("racadm get bios.SysProfileSettings", 2)
|
||||||
|
for key in ["SysProfile", "ProcCStates", "ProcC1E", "ProcTurboMode", "ProcPwrPerf"]:
|
||||||
|
m = re.search(rf"{key}\s*=\s*(\w+)", raw_bios)
|
||||||
|
node_data[key] = m.group(1) if m else "Unknown"
|
||||||
|
|
||||||
|
raw_proc = send_and_recv("racadm get bios.ProcSettings", 2)
|
||||||
|
for key in ["ProcVirtualization", "ProcExecuteDisable", "LogicalProc"]:
|
||||||
|
m = re.search(rf"{key}\s*=\s*(\w+)", raw_proc)
|
||||||
|
node_data[key] = m.group(1) if m else "Unknown"
|
||||||
|
|
||||||
|
raw_dev = send_and_recv("racadm get bios.IntegratedDevices", 2)
|
||||||
|
for key in ["SriovGlobalEnable"]:
|
||||||
|
m = re.search(rf"{key}\s*=\s*(\w+)", raw_dev)
|
||||||
|
node_data[key] = m.group(1) if m else "Unknown"
|
||||||
|
|
||||||
|
# 2. Storage / RAID
|
||||||
|
raw_ctrl = send_and_recv("racadm raid get controllers", 2)
|
||||||
|
controllers = [line.strip() for line in raw_ctrl.splitlines() if "RAID." in line or "PERC" in line or "AHCI." in line]
|
||||||
|
node_data["Controllers"] = ", ".join(controllers) if controllers else "None"
|
||||||
|
|
||||||
|
raw_vdisks = send_and_recv("racadm raid get vdisks", 3)
|
||||||
|
vdisks = [line.strip() for line in raw_vdisks.splitlines() if line.startswith("Disk.Virtual")]
|
||||||
|
node_data["VirtualDisks"] = len(vdisks)
|
||||||
|
node_data["VirtualDisksList"] = vdisks
|
||||||
|
|
||||||
|
raw_pdisks = send_and_recv("racadm raid get pdisks", 4)
|
||||||
|
pdisk_lines = [line.strip() for line in raw_pdisks.splitlines() if line.startswith("Disk.Bay")]
|
||||||
|
node_data["PhysicalDisks"] = len(pdisk_lines)
|
||||||
|
node_data["PDisksSummary"] = raw_pdisks
|
||||||
|
|
||||||
|
c.close()
|
||||||
|
except Exception as e:
|
||||||
|
node_data["Error"] = str(e)
|
||||||
|
|
||||||
|
results[name] = node_data
|
||||||
|
|
||||||
|
print("\n\n========================= SUMMARY MATRIX =========================")
|
||||||
|
import json
|
||||||
|
print(json.dumps(results, indent=2))
|
||||||
|
|
||||||
24
scripts/audit_harvester_live_nodes.py
Normal file
24
scripts/audit_harvester_live_nodes.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import paramiko
|
||||||
|
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('140.44.4.90', username='rancher', password='Katzir476!', timeout=10)
|
||||||
|
|
||||||
|
def run(cmd):
|
||||||
|
stdin, stdout, stderr = c.exec_command(f"echo Katzir476! | sudo -S sh -c '{cmd}'")
|
||||||
|
return stdout.read().decode().strip()
|
||||||
|
|
||||||
|
print("=== CPU & VIRTUALIZATION (lscpu) ===")
|
||||||
|
print(run("lscpu | grep -E 'Model name|CPU\(s\):|Thread|Socket|Virtualization|NUMA'"))
|
||||||
|
|
||||||
|
print("\n=== KERNEL PARAMETERS (/proc/cmdline) ===")
|
||||||
|
print(run("cat /proc/cmdline"))
|
||||||
|
|
||||||
|
print("\n=== STORAGE CONTROLLER (lspci) ===")
|
||||||
|
print(run("lspci | grep -E 'RAID|SAS|SATA|Ethernet|Network'"))
|
||||||
|
|
||||||
|
print("\n=== BLOCK DEVICES (lsblk) ===")
|
||||||
|
print(run("lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINTS"))
|
||||||
|
|
||||||
|
c.close()
|
||||||
|
|
||||||
39
scripts/audit_spaceport_kernels.py
Normal file
39
scripts/audit_spaceport_kernels.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
key_path = r"C:\Users\Snarf\.ssh\id_ed25519_snarfnet"
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
|
||||||
|
spaceport_nodes = [
|
||||||
|
('sninfr0lnxctl01', '140.44.4.41'),
|
||||||
|
('sninfr0lnxctl02', '140.44.4.42'),
|
||||||
|
('sninfr0lnxctl03', '140.44.4.43'),
|
||||||
|
('sninfr0lnxctl04', '140.44.4.44'),
|
||||||
|
('sninfr0lnxwrk01', '140.44.4.51'),
|
||||||
|
('sninfr0lnxwrk02', '140.44.4.52'),
|
||||||
|
('sninfr0lnxwrk03', '140.44.4.53'),
|
||||||
|
('sninfr0lnxwrk04', '140.44.4.54'),
|
||||||
|
('sninfr0lnxwrk05', '140.44.4.55'),
|
||||||
|
('sninfr0lnxwrk06', '140.44.4.56'),
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"{'NODE':<18} {'IP':<15} {'RUNNING KERNEL':<22} {'LATEST MODULES':<25} {'VETH LOADED':<12} {'UPTIME'}")
|
||||||
|
print("="*105)
|
||||||
|
|
||||||
|
for name, ip in spaceport_nodes:
|
||||||
|
try:
|
||||||
|
client.connect(ip, username="root", key_filename=key_path, timeout=5)
|
||||||
|
stdin, stdout, stderr = client.exec_command("uname -r; ls -1 /lib/modules | sort -V | tail -n 1; lsmod | grep -q veth && echo YES || echo NO; uptime -p")
|
||||||
|
lines = [l.strip() for l in stdout.read().decode('utf-8').strip().split('\n') if l.strip()]
|
||||||
|
running_k = lines[0] if len(lines) > 0 else "?"
|
||||||
|
latest_mod = lines[1] if len(lines) > 1 else "?"
|
||||||
|
veth = lines[2] if len(lines) > 2 else "?"
|
||||||
|
uptime_str = lines[3] if len(lines) > 3 else "?"
|
||||||
|
print(f"{name:<18} {ip:<15} {running_k:<22} {latest_mod:<25} {veth:<12} {uptime_str}")
|
||||||
|
client.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"{name:<18} {ip:<15} ERROR: {e}")
|
||||||
|
|
||||||
48
scripts/audit_zabbix_alerts.py
Normal file
48
scripts/audit_zabbix_alerts.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import json, ssl, urllib.request
|
||||||
|
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
ZABBIX_URL = "http://140.44.4.60:8888/api_jsonrpc.php"
|
||||||
|
|
||||||
|
def zbx_req(method, params, token=None):
|
||||||
|
payload = {'jsonrpc': '2.0', 'method': method, 'params': params, 'id': 1}
|
||||||
|
headers = {'Content-Type': 'application/json-rpc'}
|
||||||
|
if token:
|
||||||
|
headers['Authorization'] = f'Bearer {token}'
|
||||||
|
req = urllib.request.Request(ZABBIX_URL, data=json.dumps(payload).encode('utf-8'), headers=headers, method='POST')
|
||||||
|
with urllib.request.urlopen(req, timeout=10, context=ctx) as r:
|
||||||
|
return json.loads(r.read().decode())['result']
|
||||||
|
|
||||||
|
token = zbx_req('user.login', {'username': 'Admin', 'password': 'zabbix'})
|
||||||
|
print("Zabbix Auth Token:", token)
|
||||||
|
|
||||||
|
# 1. Actions
|
||||||
|
actions = zbx_req('action.get', {'output': 'extend', 'selectOperations': 'extend', 'selectFilter': 'extend'}, token)
|
||||||
|
print("\n=== TRIGGER ACTIONS ===")
|
||||||
|
for a in actions:
|
||||||
|
print(f"ID: {a['actionid']}, Name: {a['name']}, Status (0=enabled, 1=disabled): {a['status']}, EventSource: {a['eventsource']}")
|
||||||
|
print(f" Operations: {a.get('operations')}")
|
||||||
|
print(f" Filter: {a.get('filter')}")
|
||||||
|
|
||||||
|
# 2. Media Types
|
||||||
|
media_types = zbx_req('mediatype.get', {'output': 'extend'}, token)
|
||||||
|
print("\n=== MEDIA TYPES ===")
|
||||||
|
for m in media_types:
|
||||||
|
print(f"ID: {m['mediatypeid']}, Name: {m['name']}, Type: {m['type']}, Status: {m['status']}")
|
||||||
|
|
||||||
|
# 3. User Media
|
||||||
|
users = zbx_req('user.get', {'output': ['userid', 'username'], 'selectMedias': 'extend'}, token)
|
||||||
|
print("\n=== USERS & MEDIA ===")
|
||||||
|
for u in users:
|
||||||
|
print(f"User: {u['username']} (ID: {u['userid']})")
|
||||||
|
for med in u.get('medias', []):
|
||||||
|
print(f" Media: mediatypeid={med['mediatypeid']}, sendto={med['sendto']}, active={med['active']}, severity={med['severity']}")
|
||||||
|
|
||||||
|
# 4. Recent Alerts
|
||||||
|
alerts = zbx_req('alert.get', {'output': 'extend', 'limit': 15, 'sortfield': 'clock', 'sortorder': 'DESC'}, token)
|
||||||
|
print("\n=== RECENT ALERTS ===")
|
||||||
|
for al in alerts:
|
||||||
|
print(f"Alert ID: {al['alertid']}, ActionID: {al['actionid']}, Clock: {al['clock']}, Error: {al.get('error')}, Status: {al.get('status')}, Subject: {al.get('subject')}")
|
||||||
|
|
||||||
79
scripts/audit_zabbix_hosts.py
Normal file
79
scripts/audit_zabbix_hosts.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import json
|
||||||
|
import ssl
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
ZABBIX_URL = "http://140.44.4.60:8888/api_jsonrpc.php"
|
||||||
|
ZABBIX_USER = "Admin"
|
||||||
|
ZABBIX_PASS = "zabbix"
|
||||||
|
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
def zbx_call(method, params, auth=None):
|
||||||
|
payload = {"jsonrpc": "2.0", "method": method, "params": params, "id": 1}
|
||||||
|
headers = {"Content-Type": "application/json-rpc"}
|
||||||
|
if auth:
|
||||||
|
headers["Authorization"] = f"Bearer {auth}"
|
||||||
|
req = urllib.request.Request(ZABBIX_URL, data=json.dumps(payload).encode("utf-8"), headers=headers, method="POST")
|
||||||
|
with urllib.request.urlopen(req, timeout=10, context=ctx) as r:
|
||||||
|
resp = json.loads(r.read().decode("utf-8"))
|
||||||
|
if "error" in resp:
|
||||||
|
raise RuntimeError(f"Zabbix error in {method}: {resp['error']}")
|
||||||
|
return resp["result"]
|
||||||
|
|
||||||
|
# 1. Login
|
||||||
|
auth_token = zbx_call("user.login", {"username": ZABBIX_USER, "password": ZABBIX_PASS})
|
||||||
|
print("Zabbix Auth OK!")
|
||||||
|
|
||||||
|
# 2. Get all hosts
|
||||||
|
hosts = zbx_call("host.get", {
|
||||||
|
"output": ["hostid", "host", "name", "status"],
|
||||||
|
"selectInterfaces": ["interfaceid", "ip", "port", "type", "main"],
|
||||||
|
"selectParentTemplates": ["templateid", "name"]
|
||||||
|
}, auth_token)
|
||||||
|
|
||||||
|
print(f"\nTotal Registered Hosts in Zabbix: {len(hosts)}")
|
||||||
|
registered_ips = {}
|
||||||
|
for h in sorted(hosts, key=lambda x: x["host"]):
|
||||||
|
ips = [i["ip"] for i in h.get("interfaces", []) if i.get("ip")]
|
||||||
|
ip_str = ", ".join(ips) if ips else "N/A"
|
||||||
|
status_str = "MONITORED" if h["status"] == "0" else "DISABLED"
|
||||||
|
templates = [t["name"] for t in h.get("parentTemplates", [])]
|
||||||
|
tpl_str = ", ".join(templates) if templates else "None"
|
||||||
|
print(f"[{status_str}] {h['host']} ({h['name']}) -> {ip_str} | Templates: {tpl_str}")
|
||||||
|
for ip in ips:
|
||||||
|
registered_ips[ip] = h["host"]
|
||||||
|
|
||||||
|
# Compare with our 28 target hosts
|
||||||
|
target_hosts = {
|
||||||
|
# Aerodrome (10)
|
||||||
|
"140.44.4.11": "sninfr2lnxctl01", "140.44.4.12": "sninfr2lnxctl02", "140.44.4.13": "sninfr2lnxctl03",
|
||||||
|
"140.44.4.21": "sninfr2lnxwrk01", "140.44.4.22": "sninfr2lnxwrk02", "140.44.4.23": "sninfr2lnxwrk03",
|
||||||
|
"140.44.4.24": "sninfr2lnxwrk04", "140.44.4.25": "sninfr2lnxwrk05", "140.44.4.26": "sninfr2lnxwrk06",
|
||||||
|
"140.44.4.27": "sninfr2lnxwrk07",
|
||||||
|
# Spaceport (10)
|
||||||
|
"140.44.4.41": "sninfr0lnxctl01", "140.44.4.42": "sninfr0lnxctl02", "140.44.4.43": "sninfr0lnxctl03", "140.44.4.44": "sninfr0lnxctl04",
|
||||||
|
"140.44.4.51": "sninfr0lnxwrk01", "140.44.4.52": "sninfr0lnxwrk02", "140.44.4.53": "sninfr0lnxwrk03",
|
||||||
|
"140.44.4.54": "sninfr0lnxwrk04", "140.44.4.55": "sninfr0lnxwrk05", "140.44.4.56": "sninfr0lnxwrk06",
|
||||||
|
# Shipyard (5)
|
||||||
|
"140.44.4.71": "snshpd4lnxswm01", "140.44.4.72": "snshpd4lnxswm02", "140.44.4.73": "snshpd4lnxswm03",
|
||||||
|
"140.44.4.74": "snshpd4lnxswm04", "140.44.4.75": "snshpd4lnxswm05",
|
||||||
|
# AI & NUT Master (3)
|
||||||
|
"140.44.4.7": "sninfr4lnxaih01", "140.44.4.8": "sninfr4lnxaih02", "14.4.4.44": "nutserver",
|
||||||
|
# Synology NAS (2)
|
||||||
|
"14.10.10.71": "SN-NAS", "14.10.10.72": "SnarfNAS"
|
||||||
|
}
|
||||||
|
|
||||||
|
print("\n=== COVERAGE AUDIT ACROSS 28 TARGET HOSTS ===")
|
||||||
|
missing = []
|
||||||
|
for ip, name in sorted(target_hosts.items()):
|
||||||
|
if ip in registered_ips:
|
||||||
|
print(f" [OK] {ip} -> {name} (Zabbix Host: {registered_ips[ip]})")
|
||||||
|
else:
|
||||||
|
print(f" [MISSING] {ip} -> {name}")
|
||||||
|
missing.append((ip, name))
|
||||||
|
|
||||||
|
print(f"\nTotal Missing from Target 28 Scope: {len(missing)}")
|
||||||
|
for ip, name in missing:
|
||||||
|
print(f" - {name} ({ip})")
|
||||||
35
scripts/authorize_and_test.py
Normal file
35
scripts/authorize_and_test.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
key_path = r"C:\Users\Snarf\.ssh\id_ed25519_snarfnet"
|
||||||
|
pub_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC6ycMke4AaV2qzWfULZCTZOKhSKf4pY2qRGYjYzs1MGKutJxt2jkYNK2aS0XffxLRR49v23zHbBwLYraNl2Mkb9rsKsyvKn8CAa1CEl9NxOci4LDRT8CeWHRnMZn8Mdu1pfeRwM26/Bjnd4RJrSKC7fyM0OlOpEp0DgnuPBqBtPMFGw+TCUT8eZ0pBrAI94wEan4l35kmuBcqm6RH3KuD1dDJKnPLzJ3/T21CViYL0GgFIDDor/hGI45VfOI/jKmkgM4YoudTCde6uPV5e2yaJpORZlR4nDxg9ntXp8HxbHGZkXYr55Ibsjem0lMU6nBh33jGbJVvat1Ay4oNCbygJsHfC/8ppe0bnLhnasxVAXhTvklZZL/sf7K/VS0ZifpfqB6DKtr1lziOm2wQeahv+Y1uoHkVNa3XY8Fx7xGPeT1BGd9lfcbUoweoTdMPMIPaSZlNGY32AHYkM4PShG9yI1coAqXzMby0+Z/Qwtr4l6cLKVvcGgqh5sd+z1+YvZbs= root@SNINFR2LNXCTL01"
|
||||||
|
|
||||||
|
# 1. Authorize on 140.44.4.71
|
||||||
|
c71 = paramiko.SSHClient()
|
||||||
|
c71.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c71.connect("140.44.4.71", username="root", key_filename=key_path, timeout=10)
|
||||||
|
|
||||||
|
cmd = f"""
|
||||||
|
mkdir -p ~/.ssh && chmod 700 ~/.ssh
|
||||||
|
grep -qF '{pub_key}' ~/.ssh/authorized_keys 2>/dev/null || echo '{pub_key}' >> ~/.ssh/authorized_keys
|
||||||
|
chmod 600 ~/.ssh/authorized_keys
|
||||||
|
"""
|
||||||
|
c71.exec_command(cmd)
|
||||||
|
c71.close()
|
||||||
|
print("Authorized 140.44.4.11 key on 140.44.4.71.")
|
||||||
|
|
||||||
|
# 2. Test SSH from 140.44.4.11 to 140.44.4.71
|
||||||
|
c11 = paramiko.SSHClient()
|
||||||
|
c11.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c11.connect("140.44.4.11", username="root", key_filename=key_path, timeout=10)
|
||||||
|
|
||||||
|
stdin, stdout, stderr = c11.exec_command("ssh -o BatchMode=yes -o StrictHostKeyChecking=no root@140.44.4.71 hostname")
|
||||||
|
out = stdout.read().decode('utf-8').strip()
|
||||||
|
err = stderr.read().decode('utf-8').strip()
|
||||||
|
print("Result of ssh from 140.44.4.11 to 140.44.4.71:")
|
||||||
|
print("STDOUT:", out)
|
||||||
|
if err: print("STDERR:", err)
|
||||||
|
c11.close()
|
||||||
|
|
||||||
27
scripts/capture_screenshot.py
Normal file
27
scripts/capture_screenshot.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
|
||||||
|
edge_path = r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
|
||||||
|
out_png = os.path.abspath("docs/snarfnet_homelab_topology.png")
|
||||||
|
html_url = "file:///" + os.path.abspath("docs/snarfnet_homelab_topology.html").replace("\\", "/")
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
edge_path,
|
||||||
|
"--headless",
|
||||||
|
"--disable-gpu",
|
||||||
|
"--no-sandbox",
|
||||||
|
"--hide-scrollbars",
|
||||||
|
"--window-size=1920,2400",
|
||||||
|
f"--screenshot={out_png}",
|
||||||
|
html_url
|
||||||
|
]
|
||||||
|
|
||||||
|
print("Running cmd:", " ".join(cmd))
|
||||||
|
res = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||||
|
print("Return code:", res.returncode)
|
||||||
|
print("Stdout:", res.stdout)
|
||||||
|
print("Stderr:", res.stderr)
|
||||||
|
print("File exists?", os.path.exists(out_png))
|
||||||
|
if os.path.exists(out_png):
|
||||||
|
print("File size:", os.path.getsize(out_png))
|
||||||
|
|
||||||
10
scripts/cat_21_conf.py
Normal file
10
scripts/cat_21_conf.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import paramiko
|
||||||
|
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('140.44.4.71', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||||
|
cmd = "docker exec $(docker ps -q --filter name=nginx-proxy-manager_app) cat /data/nginx/proxy_host/21.conf"
|
||||||
|
_, o, _ = c.exec_command(cmd)
|
||||||
|
print(o.read().decode())
|
||||||
|
c.close()
|
||||||
|
|
||||||
13
scripts/cat_prom_conf.py
Normal file
13
scripts/cat_prom_conf.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import paramiko
|
||||||
|
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('140.44.4.11', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||||
|
|
||||||
|
def run(cmd):
|
||||||
|
_, o, e = c.exec_command(cmd)
|
||||||
|
return o.read().decode().strip()
|
||||||
|
|
||||||
|
print(run("kubectl exec deployment/prometheus -- cat /etc/prometheus/prometheus.yml"))
|
||||||
|
c.close()
|
||||||
|
|
||||||
40
scripts/check_10343_items.py
Normal file
40
scripts/check_10343_items.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import json
|
||||||
|
import ssl
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
ZABBIX_URL = "https://zabbix.snarfnet.net/api_jsonrpc.php"
|
||||||
|
ZABBIX_USER = "Admin"
|
||||||
|
ZABBIX_PASS = "zabbix"
|
||||||
|
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
def zbx_req(method: str, params: dict, auth: str = None):
|
||||||
|
payload = {"jsonrpc": "2.0", "method": method, "params": params, "id": 1}
|
||||||
|
headers = {"Content-Type": "application/json-rpc"}
|
||||||
|
if auth: headers["Authorization"] = f"Bearer {auth}"
|
||||||
|
req = urllib.request.Request(ZABBIX_URL, data=json.dumps(payload).encode("utf-8"), headers=headers, method="POST")
|
||||||
|
with urllib.request.urlopen(req, timeout=10, context=ctx) as r:
|
||||||
|
return json.loads(r.read().decode("utf-8"))["result"]
|
||||||
|
|
||||||
|
token = zbx_req("user.login", {"username": ZABBIX_USER, "password": ZABBIX_PASS})
|
||||||
|
|
||||||
|
items = zbx_req("item.get", {
|
||||||
|
"templateids": "10343",
|
||||||
|
"output": ["itemid", "name", "key_"],
|
||||||
|
}, auth=token)
|
||||||
|
print(f"Total items in 10343: {len(items)}")
|
||||||
|
for i in items:
|
||||||
|
if "time" in i["name"].lower() or "time" in i["key_"].lower():
|
||||||
|
print(f" {i['itemid']}: {i['name']} ({i['key_']})")
|
||||||
|
|
||||||
|
trigs = zbx_req("trigger.get", {
|
||||||
|
"templateids": "10343",
|
||||||
|
"output": ["triggerid", "description", "priority", "expression"],
|
||||||
|
}, auth=token)
|
||||||
|
print(f"Total triggers in 10343: {len(trigs)}")
|
||||||
|
for t in trigs:
|
||||||
|
if "time" in t["description"].lower():
|
||||||
|
print(f" {t['triggerid']}: {t['description']} [prio={t['priority']}] -> {t['expression']}")
|
||||||
|
|
||||||
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}")
|
||||||
|
|
||||||
14
scripts/check_13_conf.py
Normal file
14
scripts/check_13_conf.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
key_path = r"C:\Users\Snarf\.ssh\id_ed25519_snarfnet"
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
|
||||||
|
client.connect("140.44.4.71", username="root", key_filename=key_path, timeout=10)
|
||||||
|
stdin, stdout, stderr = client.exec_command('docker exec $(docker ps -q -f name=nginx-proxy-manager_app) cat /data/nginx/proxy_host/13.conf | grep server_name')
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
client.close()
|
||||||
|
|
||||||
17
scripts/check_13_usb.py
Normal file
17
scripts/check_13_usb.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import paramiko
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
pkey = paramiko.Ed25519Key.from_private_key_file(os.path.expanduser('~/.ssh/id_ed25519_snarfnet'))
|
||||||
|
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
client.connect("140.44.4.13", username="root", pkey=pkey, timeout=10)
|
||||||
|
|
||||||
|
cmd = "lsusb; lsblk; dmesg | tail -n 25"
|
||||||
|
stdin, stdout, stderr = client.exec_command(cmd)
|
||||||
|
print("ON 140.44.4.13 (.13):")
|
||||||
|
print(stdout.read().decode())
|
||||||
|
client.close()
|
||||||
|
|
||||||
25
scripts/check_214_pods.py
Normal file
25
scripts/check_214_pods.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
key_path = r"C:\Users\Snarf\.ssh\id_ed25519_snarfnet"
|
||||||
|
|
||||||
|
client.connect("140.44.4.11", username="root", key_filename=key_path, timeout=15)
|
||||||
|
stdin, stdout, stderr = client.exec_command("KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n cattle-system get pods -o wide")
|
||||||
|
print("=== PODS IN CATTLE-SYSTEM ===")
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
|
||||||
|
stdin, stdout, stderr = client.exec_command("KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n cattle-system rollout status deployment/rancher --timeout=30s")
|
||||||
|
print("=== ROLLOUT STATUS ===")
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
print(stderr.read().decode('utf-8'))
|
||||||
|
|
||||||
|
stdin, stdout, stderr = client.exec_command("curl -k -I -s https://aerodrome.snarfnet.net | head -n 10")
|
||||||
|
print("=== ENDPOINT ===")
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
|
||||||
29
scripts/check_215_pods.py
Normal file
29
scripts/check_215_pods.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
key_path = r"C:\Users\Snarf\.ssh\id_ed25519_snarfnet"
|
||||||
|
|
||||||
|
client.connect("140.44.4.11", username="root", key_filename=key_path, timeout=15)
|
||||||
|
stdin, stdout, stderr = client.exec_command("KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n cattle-system get pods -o wide")
|
||||||
|
print("=== PODS IN CATTLE-SYSTEM ===")
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
|
||||||
|
stdin, stdout, stderr = client.exec_command("KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n cattle-system rollout status deployment/rancher --timeout=30s")
|
||||||
|
print("=== ROLLOUT STATUS ===")
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
print(stderr.read().decode('utf-8'))
|
||||||
|
|
||||||
|
stdin, stdout, stderr = client.exec_command("curl -k -I -s https://aerodrome.snarfnet.net | head -n 10")
|
||||||
|
print("=== ENDPOINT ===")
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
|
||||||
|
stdin, stdout, stderr = client.exec_command("KUBECONFIG=/etc/rancher/k3s/k3s.yaml helm list -n cattle-system")
|
||||||
|
print("=== HELM LIST ===")
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
|
||||||
17
scripts/check_21_arp.py
Normal file
17
scripts/check_21_arp.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import paramiko
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
pkey = paramiko.Ed25519Key.from_private_key_file(os.path.expanduser('~/.ssh/id_ed25519_snarfnet'))
|
||||||
|
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
client.connect("140.44.4.11", username="root", pkey=pkey, timeout=10)
|
||||||
|
|
||||||
|
cmd = "ip neighbor show 140.44.4.21; arp -n 140.44.4.21"
|
||||||
|
stdin, stdout, stderr = client.exec_command(cmd)
|
||||||
|
print("ARP FOR 140.44.4.21:")
|
||||||
|
print(stdout.read().decode())
|
||||||
|
client.close()
|
||||||
|
|
||||||
14
scripts/check_21_k8s_conditions.py
Normal file
14
scripts/check_21_k8s_conditions.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import paramiko
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
pkey = paramiko.Ed25519Key.from_private_key_file(os.path.expanduser('~/.ssh/id_ed25519_snarfnet'))
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
client.connect("140.44.4.11", username="root", pkey=pkey, timeout=10)
|
||||||
|
|
||||||
|
stdin, stdout, stderr = client.exec_command("kubectl describe node sninfr2lnxwrk01 | grep -A 10 -i 'Conditions:'")
|
||||||
|
print(stdout.read().decode())
|
||||||
|
client.close()
|
||||||
|
|
||||||
15
scripts/check_21_status.py
Normal file
15
scripts/check_21_status.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
stdin, stdout, stderr = ssh.exec_command('kubectl get pods -A -o wide --field-selector spec.nodeName=sninfr2lnxwrk01')
|
||||||
|
print("=== PODS ON .21 ===")
|
||||||
|
print(stdout.read().decode())
|
||||||
|
|
||||||
|
stdin, stdout, stderr = ssh.exec_command('kubectl get node sninfr2lnxwrk01 -o jsonpath="{.status.conditions}"')
|
||||||
|
print("=== NODE CONDITIONS ===")
|
||||||
|
print(stdout.read().decode())
|
||||||
|
|
||||||
|
ssh.close()
|
||||||
17
scripts/check_21_timesync.py
Normal file
17
scripts/check_21_timesync.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import paramiko
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
pkey = paramiko.Ed25519Key.from_private_key_file(os.path.expanduser('~/.ssh/id_ed25519_snarfnet'))
|
||||||
|
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
client.connect("14.4.4.44", username="snarf", pkey=pkey, timeout=10)
|
||||||
|
|
||||||
|
cmd = "sudo ssh -o StrictHostKeyChecking=no -i /root/.ssh/id_ed25519_snarfnet root@140.44.4.21 'journalctl -u systemd-timesyncd -n 5 --no-pager'"
|
||||||
|
stdin, stdout, stderr = client.exec_command(cmd)
|
||||||
|
print("TIMESYNCD LOG ON 21:")
|
||||||
|
print(stdout.read().decode())
|
||||||
|
client.close()
|
||||||
|
|
||||||
17
scripts/check_22_dmesg.py
Normal file
17
scripts/check_22_dmesg.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import paramiko
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
pkey = paramiko.Ed25519Key.from_private_key_file(os.path.expanduser('~/.ssh/id_ed25519_snarfnet'))
|
||||||
|
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
client.connect("140.44.4.22", username="root", pkey=pkey, timeout=10)
|
||||||
|
|
||||||
|
cmd = "dmesg | tail -n 25; lsusb"
|
||||||
|
stdin, stdout, stderr = client.exec_command(cmd)
|
||||||
|
print("DMESG AND LSUSB ON 22:")
|
||||||
|
print(stdout.read().decode())
|
||||||
|
client.close()
|
||||||
|
|
||||||
17
scripts/check_22_k3s_env.py
Normal file
17
scripts/check_22_k3s_env.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import paramiko
|
||||||
|
|
||||||
|
ssh = paramiko.SSHClient()
|
||||||
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
ssh.connect('140.44.4.22', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||||
|
|
||||||
|
stdin, stdout, stderr = ssh.exec_command('cat /etc/systemd/system/k3s-agent.service.env')
|
||||||
|
print('K3S ENV:\n', stdout.read().decode())
|
||||||
|
|
||||||
|
stdin, stdout, stderr = ssh.exec_command('cat /boot/cmdline.txt /boot/firmware/cmdline.txt 2>/dev/null')
|
||||||
|
print('CMDLINE:\n', stdout.read().decode())
|
||||||
|
|
||||||
|
stdin, stdout, stderr = ssh.exec_command('cat /boot/config.txt /boot/firmware/config.txt 2>/dev/null')
|
||||||
|
print('CONFIG.TXT (TAIL 20):\n', stdout.read().decode().splitlines()[-20:])
|
||||||
|
|
||||||
|
ssh.close()
|
||||||
|
|
||||||
17
scripts/check_22_lsblk.py
Normal file
17
scripts/check_22_lsblk.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import paramiko
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
pkey = paramiko.Ed25519Key.from_private_key_file(os.path.expanduser('~/.ssh/id_ed25519_snarfnet'))
|
||||||
|
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
client.connect("140.44.4.22", username="root", pkey=pkey, timeout=10)
|
||||||
|
|
||||||
|
cmd = "lsblk -o NAME,SIZE,FSTYPE,MODEL,MOUNTPOINT"
|
||||||
|
stdin, stdout, stderr = client.exec_command(cmd)
|
||||||
|
print("LSBLK ON 22:")
|
||||||
|
print(stdout.read().decode())
|
||||||
|
client.close()
|
||||||
|
|
||||||
21
scripts/check_22_os.py
Normal file
21
scripts/check_22_os.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.22', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||||
|
|
||||||
|
cmds = [
|
||||||
|
"cat /etc/os-release",
|
||||||
|
"cat /boot/dietpi/.version 2>/dev/null || cat /boot/firmware/dietpi/.version 2>/dev/null || echo 'Not DietPi'",
|
||||||
|
"which dropbear || which sshd",
|
||||||
|
"ip route show",
|
||||||
|
"cat /etc/network/interfaces /etc/dhcpcd.conf /etc/netplan/* /etc/systemd/network/* 2>/dev/null",
|
||||||
|
"systemctl cat k3s-agent.service 2>/dev/null || cat /etc/systemd/system/k3s-agent.service"
|
||||||
|
]
|
||||||
|
|
||||||
|
for cmd in cmds:
|
||||||
|
print(f"=== {cmd} ===")
|
||||||
|
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||||
|
print(stdout.read().decode())
|
||||||
|
|
||||||
|
ssh.close()
|
||||||
17
scripts/check_22_usb.py
Normal file
17
scripts/check_22_usb.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import paramiko
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
pkey = paramiko.Ed25519Key.from_private_key_file(os.path.expanduser('~/.ssh/id_ed25519_snarfnet'))
|
||||||
|
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
client.connect("140.44.4.22", username="root", pkey=pkey, timeout=10)
|
||||||
|
|
||||||
|
cmd = "lsusb; lsblk"
|
||||||
|
stdin, stdout, stderr = client.exec_command(cmd)
|
||||||
|
print("USB AND DISKS ON 22:")
|
||||||
|
print(stdout.read().decode())
|
||||||
|
client.close()
|
||||||
|
|
||||||
12
scripts/check_33_conf.py
Normal file
12
scripts/check_33_conf.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import paramiko
|
||||||
|
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
client.connect("140.44.4.71", username="root", key_filename=r"C:\Users\Snarf\.ssh\id_ed25519_snarfnet", timeout=10)
|
||||||
|
|
||||||
|
cmd = 'cat /var/lib/docker/volumes/nginx-proxy-manager_nginx-data/_data/nginx/proxy_host/33.conf'
|
||||||
|
stdin, stdout, stderr = client.exec_command(cmd)
|
||||||
|
print("33.conf:\n", stdout.read().decode())
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
|
||||||
13
scripts/check_43_access.py
Normal file
13
scripts/check_43_access.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import paramiko
|
||||||
|
|
||||||
|
key_path = r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet'
|
||||||
|
ssh = paramiko.SSHClient()
|
||||||
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
ssh.connect('140.44.4.71', username='snarf', key_filename=key_path, timeout=10)
|
||||||
|
|
||||||
|
stdin, stdout, stderr = ssh.exec_command("sudo docker exec $(sudo docker ps -q -f name=nginx-proxy-manager) tail -n 30 /data/logs/proxy-host-43_access.log")
|
||||||
|
print("=== Access log 43 (pihole2) around 11:00 ===")
|
||||||
|
print(stdout.read().decode())
|
||||||
|
|
||||||
|
ssh.close()
|
||||||
|
|
||||||
12
scripts/check_49_conf.py
Normal file
12
scripts/check_49_conf.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import paramiko
|
||||||
|
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
client.connect("140.44.4.71", username="root", key_filename=r"C:\Users\Snarf\.ssh\id_ed25519_snarfnet", timeout=10)
|
||||||
|
|
||||||
|
cmd = 'cat /var/lib/docker/volumes/nginx-proxy-manager_nginx-data/_data/nginx/proxy_host/49.conf'
|
||||||
|
stdin, stdout, stderr = client.exec_command(cmd)
|
||||||
|
print("49.conf (ai.snarfnet.net):\n", stdout.read().decode())
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
|
||||||
19
scripts/check_51.py
Normal file
19
scripts/check_51.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import paramiko
|
||||||
|
|
||||||
|
ssh = paramiko.SSHClient()
|
||||||
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
ssh.connect('140.44.4.51', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||||
|
|
||||||
|
def run(cmd):
|
||||||
|
print(f"=== {cmd} ===")
|
||||||
|
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||||
|
print(stdout.read().decode())
|
||||||
|
err = stderr.read().decode()
|
||||||
|
if err:
|
||||||
|
print("ERR:", err)
|
||||||
|
|
||||||
|
run("ss -tulpn | grep ':80 '")
|
||||||
|
run("ip a")
|
||||||
|
run("kubectl get pod -A -o wide | grep sninfr4lnxwrk01 | grep pihole || true")
|
||||||
|
ssh.close()
|
||||||
|
|
||||||
18
scripts/check_51_nat.py
Normal file
18
scripts/check_51_nat.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import paramiko
|
||||||
|
|
||||||
|
ssh = paramiko.SSHClient()
|
||||||
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
ssh.connect('140.44.4.51', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||||
|
|
||||||
|
def run(cmd):
|
||||||
|
print(f"=== {cmd} ===")
|
||||||
|
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||||
|
print(stdout.read().decode())
|
||||||
|
err = stderr.read().decode()
|
||||||
|
if err:
|
||||||
|
print("ERR:", err)
|
||||||
|
|
||||||
|
run("iptables -t nat -S | grep -E '80\b|31905|31937'")
|
||||||
|
run("crictl ps")
|
||||||
|
ssh.close()
|
||||||
|
|
||||||
17
scripts/check_55_and_43.py
Normal file
17
scripts/check_55_and_43.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import paramiko
|
||||||
|
|
||||||
|
key_path = r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet'
|
||||||
|
ssh = paramiko.SSHClient()
|
||||||
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
ssh.connect('140.44.4.71', username='snarf', key_filename=key_path, timeout=10)
|
||||||
|
|
||||||
|
stdin, stdout, stderr = ssh.exec_command("sudo docker exec $(sudo docker ps -q -f name=nginx-proxy-manager) cat /data/nginx/proxy_host/55.conf")
|
||||||
|
print("=== 55.conf (pihole1) ===")
|
||||||
|
print(stdout.read().decode())
|
||||||
|
|
||||||
|
stdin, stdout, stderr = ssh.exec_command("sudo docker exec $(sudo docker ps -q -f name=nginx-proxy-manager) cat /data/nginx/proxy_host/43.conf")
|
||||||
|
print("=== 43.conf (pihole2) ===")
|
||||||
|
print(stdout.read().decode())
|
||||||
|
|
||||||
|
ssh.close()
|
||||||
|
|
||||||
14
scripts/check_71_containers.py
Normal file
14
scripts/check_71_containers.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
key_path = r"C:\Users\Snarf\.ssh\id_ed25519_snarfnet"
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
|
||||||
|
client.connect("140.44.4.71", username="root", key_filename=key_path, timeout=10)
|
||||||
|
stdin, stdout, stderr = client.exec_command("docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'")
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
client.close()
|
||||||
|
|
||||||
15
scripts/check_aero_health.py
Normal file
15
scripts/check_aero_health.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
key_path = r"C:\Users\Snarf\.ssh\id_ed25519_snarfnet"
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
client.connect("140.44.4.11", username="root", key_filename=key_path, timeout=10)
|
||||||
|
|
||||||
|
stdin, stdout, stderr = client.exec_command('k3s kubectl get pods -n cattle-system')
|
||||||
|
print("=== AERODROME CATTLE-SYSTEM PODS ===")
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
client.close()
|
||||||
|
|
||||||
14
scripts/check_aero_infra.py
Normal file
14
scripts/check_aero_infra.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
key_path = r"C:\Users\Snarf\.ssh\id_ed25519_snarfnet"
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect("140.44.4.11", username="root", key_filename=key_path, timeout=5)
|
||||||
|
|
||||||
|
stdin, stdout, stderr = c.exec_command("k3s kubectl -n infra get deploy,svc,pod")
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
c.close()
|
||||||
|
|
||||||
24
scripts/check_aero_metrics_sources.py
Normal file
24
scripts/check_aero_metrics_sources.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import paramiko
|
||||||
|
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect('140.44.4.11', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||||
|
|
||||||
|
def run(cmd):
|
||||||
|
_, o, e = c.exec_command(cmd)
|
||||||
|
return o.read().decode().strip()
|
||||||
|
|
||||||
|
print("Nodes on Aerodrome:")
|
||||||
|
print(run("kubectl get nodes -o wide"))
|
||||||
|
|
||||||
|
print("\nDaemonSets on Aerodrome:")
|
||||||
|
print(run("kubectl get ds -A"))
|
||||||
|
|
||||||
|
print("\nServices matching metrics:")
|
||||||
|
print(run("kubectl get svc -A | grep -E 'metric|node|cadvisor|exporter'"))
|
||||||
|
|
||||||
|
print("\nKubelet metrics on node 140.44.4.11 (port 10250 or 10255):")
|
||||||
|
print(run("curl -sk https://localhost:10250/metrics/cadvisor | head -n 10"))
|
||||||
|
|
||||||
|
c.close()
|
||||||
|
|
||||||
12
scripts/check_aero_pihole_auth.py
Normal file
12
scripts/check_aero_pihole_auth.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.11', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||||
|
|
||||||
|
stdin, stdout, stderr = ssh.exec_command('k3s kubectl exec -n dns deployment/pihole -- curl -s -X POST "http://localhost/api/auth" -H "Content-Type: application/json" -d \'{"password":"snarfnetadmin"}\'')
|
||||||
|
print("Aerodrome API auth test:")
|
||||||
|
print(stdout.read().decode('utf-8', errors='ignore'))
|
||||||
|
|
||||||
|
ssh.close()
|
||||||
|
|
||||||
14
scripts/check_aero_svc.py
Normal file
14
scripts/check_aero_svc.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
key_path = r"C:\Users\Snarf\.ssh\id_ed25519_snarfnet"
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect("140.44.4.11", username="root", key_filename=key_path, timeout=5)
|
||||||
|
|
||||||
|
stdin, stdout, stderr = c.exec_command("k3s kubectl -n infra get svc")
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
c.close()
|
||||||
|
|
||||||
15
scripts/check_aerodrome_pods.py
Normal file
15
scripts/check_aerodrome_pods.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import paramiko
|
||||||
|
|
||||||
|
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', timeout=10)
|
||||||
|
|
||||||
|
def run(cmd):
|
||||||
|
print(f"=== {cmd} ===")
|
||||||
|
_, out, _ = s.exec_command(cmd)
|
||||||
|
print(out.read().decode())
|
||||||
|
|
||||||
|
run("kubectl get pods -A | grep -v -E 'Running|Completed' || true")
|
||||||
|
run("kubectl get volumes.longhorn.io -n longhorn-system -o custom-columns=NAME:.metadata.name,ROBUSTNESS:.status.robustness,STATE:.status.state")
|
||||||
|
s.close()
|
||||||
|
|
||||||
19
scripts/check_aerodrome_preflight.py
Normal file
19
scripts/check_aerodrome_preflight.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
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)
|
||||||
|
|
||||||
|
def run(cmd):
|
||||||
|
print(f"=== {cmd} ===")
|
||||||
|
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||||
|
print(stdout.read().decode())
|
||||||
|
err = stderr.read().decode()
|
||||||
|
if err:
|
||||||
|
print("ERR:", err)
|
||||||
|
|
||||||
|
run("kubectl get nodes")
|
||||||
|
run("kubectl get pods -A | grep -v -E 'Running|Completed' || true")
|
||||||
|
run("kubectl get volumes.longhorn.io -n longhorn-system -o custom-columns=NAME:.metadata.name,ROBUSTNESS:.status.robustness | grep -v healthy || true")
|
||||||
|
ssh.close()
|
||||||
|
|
||||||
8
scripts/check_aerodrome_quick.py
Normal file
8
scripts/check_aerodrome_quick.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
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)
|
||||||
|
_,o,_=ssh.exec_command('kubectl get nodes')
|
||||||
|
print(o.read().decode())
|
||||||
|
ssh.close()
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
29
scripts/check_after_reboot.py
Normal file
29
scripts/check_after_reboot.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
key_path = r"C:\Users\Snarf\.ssh\id_ed25519_snarfnet"
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
|
||||||
|
print("=== CHECKING VETH ON SNINFR0LNXCTL03 ===")
|
||||||
|
client.connect("140.44.4.43", username="root", key_filename=key_path, timeout=10)
|
||||||
|
stdin, stdout, stderr = client.exec_command("modprobe veth; lsmod | grep veth; systemctl is-active k3s-server || systemctl is-active k3s")
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
print("=== CHECKING PODS IN CATTLE-SYSTEM ON SPACEPORT ===")
|
||||||
|
client.connect("140.44.4.41", username="root", key_filename=key_path, timeout=10)
|
||||||
|
stdin, stdout, stderr = client.exec_command("k3s kubectl -n cattle-system get pods -o wide")
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
|
||||||
|
print("=== CHECKING POD EVENTS ===")
|
||||||
|
stdin, stdout, stderr = client.exec_command("k3s kubectl -n cattle-system describe pod -l app=cattle-cluster-agent | grep -A 10 Events:")
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
|
||||||
15
scripts/check_agent2_41.py
Normal file
15
scripts/check_agent2_41.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import paramiko, sys
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
stdin, stdout, stderr = ssh.exec_command('systemctl status zabbix-agent2 --no-pager')
|
||||||
|
print("STATUS:\n", stdout.read().decode('utf-8', errors='replace'))
|
||||||
|
|
||||||
|
stdin, stdout, stderr = ssh.exec_command('egrep "^(Server|ServerActive|Hostname|TLS)" /etc/zabbix/zabbix_agent2.conf')
|
||||||
|
print("CONFIG:\n", stdout.read().decode('utf-8', errors='replace'))
|
||||||
|
|
||||||
|
ssh.close()
|
||||||
|
|
||||||
14
scripts/check_agent_deploy.py
Normal file
14
scripts/check_agent_deploy.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
key_path = r"C:\Users\Snarf\.ssh\id_ed25519_snarfnet"
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
|
||||||
|
client.connect("140.44.4.41", username="root", key_filename=key_path, timeout=10)
|
||||||
|
stdin, stdout, stderr = client.exec_command("k3s kubectl -n cattle-system get deployment cattle-cluster-agent -o yaml")
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
client.close()
|
||||||
|
|
||||||
15
scripts/check_agent_logs.py
Normal file
15
scripts/check_agent_logs.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
key_path = r"C:\Users\Snarf\.ssh\id_ed25519_snarfnet"
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
|
||||||
|
client.connect("140.44.4.41", username="root", key_filename=key_path, timeout=10)
|
||||||
|
stdin, stdout, stderr = client.exec_command("k3s kubectl -n cattle-system logs -l app=cattle-cluster-agent --tail=100")
|
||||||
|
print("=== SPACEPORT CATTLE-CLUSTER-AGENT LOGS ===")
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
client.close()
|
||||||
|
|
||||||
15
scripts/check_agent_logs_tail.py
Normal file
15
scripts/check_agent_logs_tail.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
key_path = r"C:\Users\Snarf\.ssh\id_ed25519_snarfnet"
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
|
||||||
|
client.connect("140.44.4.41", username="root", key_filename=key_path, timeout=10)
|
||||||
|
stdin, stdout, stderr = client.exec_command("k3s kubectl -n cattle-system logs -l app=cattle-cluster-agent -c cluster-register --tail=20")
|
||||||
|
print("=== AGENT LOGS ===")
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
client.close()
|
||||||
|
|
||||||
14
scripts/check_agent_now.py
Normal file
14
scripts/check_agent_now.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
key_path = r"C:\Users\Snarf\.ssh\id_ed25519_snarfnet"
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
|
||||||
|
client.connect("140.44.4.41", username="root", key_filename=key_path, timeout=10)
|
||||||
|
stdin, stdout, stderr = client.exec_command("k3s kubectl -n cattle-system logs -l app=cattle-cluster-agent -c cluster-register --tail=25")
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
client.close()
|
||||||
|
|
||||||
19
scripts/check_alerts_status.py
Normal file
19
scripts/check_alerts_status.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import json, ssl, urllib.request
|
||||||
|
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
ZABBIX_URL = 'http://140.44.4.60:8888/api_jsonrpc.php'
|
||||||
|
|
||||||
|
payload = {'jsonrpc': '2.0', 'method': 'user.login', 'params': {'username': 'Admin', 'password': 'zabbix'}, 'id': 1}
|
||||||
|
req = urllib.request.Request(ZABBIX_URL, data=json.dumps(payload).encode(), headers={'Content-Type': 'application/json-rpc'})
|
||||||
|
token = json.loads(urllib.request.urlopen(req, context=ctx).read())['result']
|
||||||
|
|
||||||
|
payload = {'jsonrpc': '2.0', 'method': 'alert.get', 'params': {'output': 'extend', 'limit': 20, 'sortfield': 'clock', 'sortorder': 'DESC'}, 'id': 2}
|
||||||
|
req = urllib.request.Request(ZABBIX_URL, data=json.dumps(payload).encode(), headers={'Content-Type': 'application/json-rpc', 'Authorization': f'Bearer {token}'})
|
||||||
|
alerts = json.loads(urllib.request.urlopen(req, context=ctx).read())['result']
|
||||||
|
|
||||||
|
for a in alerts:
|
||||||
|
print(f"ID: {a['alertid']} | Status: {a['status']} | User: {a['userid']} | Error: '{a.get('error')}' | Subject: {a.get('subject')}")
|
||||||
|
|
||||||
24
scripts/check_all_coder_snapshots.py
Normal file
24
scripts/check_all_coder_snapshots.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import paramiko
|
||||||
|
|
||||||
|
nodes = [
|
||||||
|
("wrk01", "140.44.4.51"),
|
||||||
|
("wrk02", "140.44.4.52"),
|
||||||
|
("wrk03", "140.44.4.53"),
|
||||||
|
("wrk04", "140.44.4.54"),
|
||||||
|
("wrk05", "140.44.4.55"),
|
||||||
|
("wrk06", "140.44.4.56"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for name, ip in nodes:
|
||||||
|
try:
|
||||||
|
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=4)
|
||||||
|
_, out, _ = ssh.exec_command("find /var/lib/rancher/k3s/agent/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/ -path '*/home/coder/.config/coderv2' 2>/dev/null")
|
||||||
|
res = out.read().decode().strip()
|
||||||
|
if res:
|
||||||
|
print(f"[{name} - {ip}] Found:\n{res}")
|
||||||
|
ssh.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[{name}] err: {e}")
|
||||||
|
|
||||||
19
scripts/check_all_endpoints.py
Normal file
19
scripts/check_all_endpoints.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import subprocess
|
||||||
|
|
||||||
|
endpoints = [
|
||||||
|
("Argo CD", "https://argo.snarfnet.net"),
|
||||||
|
("Rancher Manager", "https://aerodrome.snarfnet.net"),
|
||||||
|
("AWX Console", "https://awx.snarfnet.net"),
|
||||||
|
("Authentik SSO", "https://authentik.snarfnet.net"),
|
||||||
|
("Uptime Kuma", "https://uptime.snarfnet.net/dashboard"),
|
||||||
|
("Gitea GitOps", "http://140.44.4.51:90"),
|
||||||
|
("Zabbix Server", "http://140.44.4.51:8888"),
|
||||||
|
("Portainer Server", "https://140.44.4.71:9443")
|
||||||
|
]
|
||||||
|
|
||||||
|
print("=== ENDPOINT HEALTH CHECK ===")
|
||||||
|
for name, url in endpoints:
|
||||||
|
res = subprocess.run(f"curl.exe -k -s -o nul -w '%{{http_code}}' {url}", capture_output=True, text=True, shell=True)
|
||||||
|
code = res.stdout.strip()
|
||||||
|
print(f"[{code}] {name} ({url})")
|
||||||
|
|
||||||
25
scripts/check_all_npm_hosts.py
Normal file
25
scripts/check_all_npm_hosts.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
key_path = r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet'
|
||||||
|
|
||||||
|
c = paramiko.SSHClient()
|
||||||
|
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
c.connect("140.44.4.71", username="root", key_filename=key_path, timeout=10)
|
||||||
|
|
||||||
|
py_query = """
|
||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect('/var/lib/docker/volumes/nginx-proxy-manager_nginx-data/_data/database.sqlite')
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute('SELECT id, domain_names, forward_host, forward_port, forward_scheme, enabled FROM proxy_host')
|
||||||
|
for r in cur.fetchall():
|
||||||
|
print(r)
|
||||||
|
conn.close()
|
||||||
|
"""
|
||||||
|
|
||||||
|
stdin, stdout, stderr = c.exec_command(f'python3 -c "{py_query}"')
|
||||||
|
print("All NPM hosts:\n", stdout.read().decode())
|
||||||
|
|
||||||
|
c.close()
|
||||||
|
|
||||||
30
scripts/check_all_providers.py
Normal file
30
scripts/check_all_providers.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import urllib.request
|
||||||
|
import ssl
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
token = "SnarfNetPermanentAutomationToken2026"
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {token}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
def api_call(url):
|
||||||
|
req = urllib.request.Request(url, headers=headers)
|
||||||
|
with urllib.request.urlopen(req, context=ctx, timeout=8) as resp:
|
||||||
|
return json.loads(resp.read().decode('utf-8'))
|
||||||
|
|
||||||
|
apps = api_call("https://authentik.snarfnet.net/api/v3/core/applications/?page_size=100")
|
||||||
|
for a in apps.get('results', []):
|
||||||
|
print(f"App: {a['name']:30} | Slug: {a['slug']:15} | Provider: {a.get('provider')}")
|
||||||
|
|
||||||
|
providers = api_call("https://authentik.snarfnet.net/api/v3/providers/proxy/?page_size=100")
|
||||||
|
for p in providers.get('results', []):
|
||||||
|
print(f"Proxy Provider: {p['name']:30} | PK: {p['pk']:4} | Mode: {p['mode']:15} | Cookie Domain: {p.get('cookie_domain')}")
|
||||||
|
|
||||||
27
scripts/check_all_tls.py
Normal file
27
scripts/check_all_tls.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import paramiko
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
key_path = r"C:\Users\Snarf\.ssh\id_ed25519_snarfnet"
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
|
||||||
|
# Check certificates in Aerodrome across all namespaces
|
||||||
|
client.connect("140.44.4.11", username="root", key_filename=key_path, timeout=10)
|
||||||
|
stdin, stdout, stderr = client.exec_command("""
|
||||||
|
KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get secrets -A --field-selector=type=kubernetes.io/tls
|
||||||
|
""")
|
||||||
|
print("=== TLS SECRETS ON AERODROME ===")
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
# Check certificates on Spaceport across all namespaces
|
||||||
|
client.connect("140.44.4.41", username="root", key_filename=key_path, timeout=10)
|
||||||
|
stdin, stdout, stderr = client.exec_command("""
|
||||||
|
KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl get secrets -A --field-selector=type=kubernetes.io/tls
|
||||||
|
""")
|
||||||
|
print("=== TLS SECRETS ON SPACEPORT ===")
|
||||||
|
print(stdout.read().decode('utf-8'))
|
||||||
|
client.close()
|
||||||
|
|
||||||
17
scripts/check_api_routes.py
Normal file
17
scripts/check_api_routes.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import paramiko
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
pkey = paramiko.Ed25519Key.from_private_key_file(os.path.expanduser('~/.ssh/id_ed25519_snarfnet'))
|
||||||
|
|
||||||
|
client = paramiko.SSHClient()
|
||||||
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
client.connect("140.44.4.11", username="root", pkey=pkey, timeout=10)
|
||||||
|
|
||||||
|
cmd = "curl -s http://140.44.4.21:80/main.105b4163.async.js | grep -o -E '\"/api/[a-zA-Z0-9_/]+\"' | sort -u | head -n 30"
|
||||||
|
stdin, stdout, stderr = client.exec_command(cmd)
|
||||||
|
print("API ROUTES ON 140.44.4.21:")
|
||||||
|
print(stdout.read().decode())
|
||||||
|
client.close()
|
||||||
|
|
||||||
12
scripts/check_api_status.py
Normal file
12
scripts/check_api_status.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.11', username='root', key_filename=r'C:\Users\Snarf\.ssh\id_ed25519_snarfnet', timeout=10)
|
||||||
|
|
||||||
|
stdin, stdout, stderr = ssh.exec_command('k3s kubectl exec -n dns deployment/technitium-dns -- curl -s "http://localhost:5380/api/status"')
|
||||||
|
print("api/status response:")
|
||||||
|
print(stdout.read().decode('utf-8', errors='ignore'))
|
||||||
|
|
||||||
|
ssh.close()
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user