feat: add infrastructure automation scripts and master improvement plan
This commit is contained in:
179
scripts/deploy_chrony_servers.py
Normal file
179
scripts/deploy_chrony_servers.py
Normal file
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Deploy and configure authoritative Chrony NTP servers:
|
||||
- Primary: aerodrome-ctl01 (140.44.4.11)
|
||||
- Secondary: nutserver (14.4.4.44)
|
||||
|
||||
Includes:
|
||||
- Exterior Stratum 1 & 2 sources (Cloudflare, Google, NIST, pool.ntp.org)
|
||||
- Peer exchange between primary and secondary
|
||||
- Subnet allowances for homelab VLANs
|
||||
- Local stratum 10 fallback for offline WAN resilience
|
||||
- Verification of UDP 123 responsiveness
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import socket
|
||||
import paramiko
|
||||
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
|
||||
PRIV_KEY_PATH = os.path.expanduser("~/.ssh/id_ed25519_snarfnet")
|
||||
|
||||
PRIMARY_CONF = """# Snarfnet Authoritative Internal NTP Server (Primary)
|
||||
# Upstream Stratum 1 & 2 Sources
|
||||
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
|
||||
|
||||
# Peer with Secondary NTP Server
|
||||
peer 14.4.4.44 minpoll 4 maxpoll 8
|
||||
|
||||
# Drift tracking & step adjustment
|
||||
driftfile /var/lib/chrony/chrony.drift
|
||||
makestep 1.0 3
|
||||
rtcsync
|
||||
|
||||
# Allowed Internal Subnets
|
||||
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
|
||||
allow 127.0.0.1
|
||||
allow ::1
|
||||
|
||||
# Local stratum 10 fallback if internet WAN is disconnected
|
||||
local stratum 10
|
||||
|
||||
# Logging
|
||||
logchange 0.5
|
||||
logdir /var/log/chrony
|
||||
"""
|
||||
|
||||
SECONDARY_CONF = """# Snarfnet Authoritative Internal NTP Server (Secondary)
|
||||
# Upstream Stratum 1 & 2 Sources
|
||||
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
|
||||
|
||||
# Peer with Primary NTP Server
|
||||
peer 140.44.4.11 minpoll 4 maxpoll 8
|
||||
|
||||
# Drift tracking & step adjustment
|
||||
driftfile /var/lib/chrony/chrony.drift
|
||||
makestep 1.0 3
|
||||
rtcsync
|
||||
|
||||
# Allowed Internal Subnets
|
||||
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
|
||||
allow 127.0.0.1
|
||||
allow ::1
|
||||
|
||||
# Local stratum 10 fallback if internet WAN is disconnected
|
||||
local stratum 10
|
||||
|
||||
# Logging
|
||||
logchange 0.5
|
||||
logdir /var/log/chrony
|
||||
"""
|
||||
|
||||
SERVERS = [
|
||||
("aerodrome-ctl01 (Primary)", "140.44.4.11", "root", PRIMARY_CONF),
|
||||
("nutserver (Secondary)", "14.4.4.44", "snarf", SECONDARY_CONF),
|
||||
]
|
||||
|
||||
def check_udp_123(host):
|
||||
msg = b'\x1b' + 47 * b'\0'
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(3.0)
|
||||
try:
|
||||
sock.sendto(msg, (host, 123))
|
||||
data, addr = sock.recvfrom(1024)
|
||||
return True, len(data)
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
def main():
|
||||
pkey = paramiko.Ed25519Key.from_private_key_file(PRIV_KEY_PATH)
|
||||
|
||||
for name, ip, user, conf_content in SERVERS:
|
||||
print(f"\n==================================================")
|
||||
print(f"Deploying Chrony on {name} [{ip}] as {user}...")
|
||||
print(f"==================================================")
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
try:
|
||||
client.connect(ip, username=user, pkey=pkey, timeout=15)
|
||||
|
||||
# 1. Stop systemd-timesyncd if present, update apt & install chrony
|
||||
install_cmd = """
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
if [ "$(id -u)" -ne 0 ]; then SUDO="sudo"; else SUDO=""; fi
|
||||
$SUDO systemctl stop systemd-timesyncd 2>/dev/null || true
|
||||
$SUDO systemctl disable systemd-timesyncd 2>/dev/null || true
|
||||
$SUDO apt-get update -y
|
||||
$SUDO apt-get install -y chrony
|
||||
"""
|
||||
print("Installing chrony package...")
|
||||
stdin, stdout, stderr = client.exec_command(install_cmd)
|
||||
stdout.channel.recv_exit_status()
|
||||
|
||||
# 2. Write configuration
|
||||
print("Writing /etc/chrony/chrony.conf...")
|
||||
write_cmd = f"""
|
||||
if [ "$(id -u)" -ne 0 ]; then SUDO="sudo"; else SUDO=""; fi
|
||||
$SUDO mkdir -p /etc/chrony /var/lib/chrony /var/log/chrony
|
||||
$SUDO tee /etc/chrony/chrony.conf << 'EOF' > /dev/null
|
||||
{conf_content}
|
||||
EOF
|
||||
$SUDO systemctl unmask chrony 2>/dev/null || true
|
||||
$SUDO systemctl restart chrony
|
||||
$SUDO systemctl enable chrony
|
||||
"""
|
||||
stdin, stdout, stderr = client.exec_command(write_cmd)
|
||||
stdout.channel.recv_exit_status()
|
||||
|
||||
# 3. Wait briefly for chronyd to bind and acquire sources
|
||||
print("Waiting 5 seconds for chronyd stabilization...")
|
||||
time.sleep(5)
|
||||
|
||||
# 4. Check chronyc sources and tracking
|
||||
verify_cmd = """
|
||||
if [ "$(id -u)" -ne 0 ]; then SUDO="sudo"; else SUDO=""; fi
|
||||
echo "--- Chrony Sources ---"
|
||||
$SUDO chronyc sources -v
|
||||
echo "--- Chrony Tracking ---"
|
||||
$SUDO chronyc tracking
|
||||
"""
|
||||
stdin, stdout, stderr = client.exec_command(verify_cmd)
|
||||
out = stdout.read().decode('utf-8', errors='replace')
|
||||
print(out)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error on {name}: {e}")
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
print("\n==================================================")
|
||||
print("Verifying UDP 123 Reachability from Network...")
|
||||
print("==================================================")
|
||||
for name, ip, _, _ in SERVERS:
|
||||
ok, res = check_udp_123(ip)
|
||||
print(f"NTP Query to {name} ({ip}:123) -> {'SUCCESS (' + str(res) + ' bytes returned)' if ok else 'FAILED: ' + str(res)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user