176 lines
5.6 KiB
Python
176 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import os
|
|
import sys
|
|
import yaml
|
|
import subprocess
|
|
import json
|
|
import shutil
|
|
from datetime import datetime
|
|
import random
|
|
import tempfile
|
|
import secrets
|
|
import string
|
|
|
|
# List of adjectives for hostname generation
|
|
ADJECTIVES = [
|
|
# Scientific/Sci-fi
|
|
'quantum', 'atomic', 'plasma', 'fusion', 'ionic', 'magnetic', 'cosmic',
|
|
'stellar', 'nebular', 'pulsar', 'quasar', 'warp', 'phaser', 'hyper',
|
|
'temporal', 'spatial', 'dimensional', 'subspace', 'transwarp',
|
|
# Cool/Interesting
|
|
'abysmal', 'adamant', 'aerial', 'arcane', 'astral', 'azure', 'celestial',
|
|
'crimson', 'cryptic', 'crystalline', 'dormant', 'eerie', 'eldritch',
|
|
'ethereal', 'fractal', 'frozen', 'ghostly', 'gilded', 'singular',
|
|
'hollow', 'infernal', 'lunar', 'mystic', 'nebulous', 'obsidian',
|
|
'occult', 'prismatic', 'radiant', 'shadow', 'solar', 'spectral',
|
|
'stellar', 'sublime', 'titanic', 'twilight', 'void', 'volcanic'
|
|
]
|
|
|
|
NOUNS = [
|
|
# Star Trek
|
|
'enterprise', 'voyager', 'galaxy', 'intrepid', 'nova', 'warbird', 'falcon',
|
|
'aegis', 'nemesis', 'equinox', 'stargazer', 'challenger', 'discovery',
|
|
'prometheus', 'odyssey', 'daedalus', 'apollo', 'korolev', 'phoenix', 'orion',
|
|
# Space
|
|
'nebula', 'pulsar', 'quasar', 'nova', 'supernova', 'blackhole',
|
|
'wormhole', 'singularity', 'galaxy', 'void', 'rift', 'nexus',
|
|
# Cool Concepts
|
|
'abyss', 'aether', 'anomaly', 'artifact', 'beacon', 'cipher', 'crystal',
|
|
'echo', 'enigma', 'essence', 'fractal', 'horizon', 'infinity',
|
|
'paradox', 'phoenix', 'prism', 'spectrum', 'tesseract',
|
|
'vector', 'vertex', 'vortex', 'zenith'
|
|
]
|
|
|
|
def load_config(config_file):
|
|
"""Load and parse YAML config file."""
|
|
with open(config_file, 'r') as f:
|
|
return yaml.safe_load(f)
|
|
|
|
def check_prerequisites():
|
|
"""Check if required tools are installed."""
|
|
required_tools = ['hcloud', 'jq']
|
|
for tool in required_tools:
|
|
if not shutil.which(tool):
|
|
print(f"Error: {tool} not found. Please install it first.")
|
|
sys.exit(1)
|
|
|
|
def check_hetzner_token():
|
|
"""Check if HCLOUD_TOKEN is set."""
|
|
if not os.environ.get('HCLOUD_TOKEN'):
|
|
print("Error: HCLOUD_TOKEN environment variable not set")
|
|
sys.exit(1)
|
|
|
|
def generate_hostname():
|
|
"""Generate a unique hostname in adjective-noun-date-time format."""
|
|
adjective = random.choice(ADJECTIVES)
|
|
noun = random.choice(NOUNS)
|
|
timestamp = datetime.now().strftime('%y%m%d-%H%M')
|
|
return f"{adjective}-{noun}-{timestamp}"
|
|
|
|
def get_image_id():
|
|
"""Get the base image ID."""
|
|
cmd = [
|
|
'hcloud', 'image', 'list',
|
|
'--type=snapshot',
|
|
'--selector=name=fedora-coreos-nullpoint',
|
|
'--output', 'json'
|
|
]
|
|
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
images = json.loads(result.stdout)
|
|
|
|
if not images:
|
|
print("Error: Base image not found. Run build.py first.")
|
|
sys.exit(1)
|
|
|
|
return images[0]['id']
|
|
|
|
def generate_secure_passphrase(length=32):
|
|
"""Generate a secure random passphrase."""
|
|
alphabet = string.ascii_letters + string.digits + string.punctuation
|
|
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
|
|
|
def create_server(config, hostname, image_id):
|
|
"""Create a new server."""
|
|
hetzner = config['hetzner']
|
|
|
|
# Generate secure passphrase
|
|
passphrase = generate_secure_passphrase()
|
|
|
|
# Generate cloud-init config for this specific server
|
|
cloud_init = {
|
|
'hostname': hostname,
|
|
'timezone': 'UTC',
|
|
'users': config.get('users', []),
|
|
'package_update': True,
|
|
'package_upgrade': True,
|
|
'write_files': [
|
|
{
|
|
'path': '/root/luks-passphrase.txt',
|
|
'content': passphrase,
|
|
'permissions': '0600'
|
|
}
|
|
],
|
|
'runcmd': [
|
|
f'export LUKS_PASSPHRASE="{passphrase}"'
|
|
]
|
|
}
|
|
|
|
# Create temporary cloud-init config
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml') as f:
|
|
yaml.dump(cloud_init, f)
|
|
f.flush()
|
|
|
|
cmd = [
|
|
'hcloud', 'server', 'create',
|
|
'--name', hostname,
|
|
'--type', hetzner['server_type'],
|
|
'--datacenter', hetzner['datacenter'],
|
|
'--image', str(image_id),
|
|
'--ssh-key', hetzner['ssh_key_name'],
|
|
'--user-data-from-file', f.name
|
|
]
|
|
subprocess.run(cmd, check=True)
|
|
|
|
# Print passphrase for user to save
|
|
print(f"\nIMPORTANT: Save this LUKS passphrase for {hostname}:")
|
|
print(f"{passphrase}\n")
|
|
print("This passphrase will be needed if TPM+Tang unlock fails.")
|
|
print("It is also saved in /root/luks-passphrase.txt on the server.")
|
|
|
|
def get_server_ip(hostname):
|
|
"""Get the server's IP address."""
|
|
cmd = ['hcloud', 'server', 'ip', hostname]
|
|
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
return result.stdout.strip()
|
|
|
|
def main():
|
|
"""Main entry point."""
|
|
# Load config
|
|
if not os.path.exists('deploy-config.yaml'):
|
|
print("Error: deploy-config.yaml not found")
|
|
sys.exit(1)
|
|
|
|
config = load_config('deploy-config.yaml')
|
|
|
|
# Check prerequisites
|
|
check_prerequisites()
|
|
check_hetzner_token()
|
|
|
|
# Generate hostname
|
|
hostname = generate_hostname()
|
|
|
|
# Get image ID
|
|
image_id = get_image_id()
|
|
|
|
# Create server
|
|
print(f"Creating server '{hostname}'...")
|
|
create_server(config, hostname, image_id)
|
|
|
|
# Get server IP
|
|
server_ip = get_server_ip(hostname)
|
|
print(f"Server created! IP: {server_ip}")
|
|
print(f"You can connect using: ssh core@{server_ip}")
|
|
|
|
if __name__ == '__main__':
|
|
main() |