nullpoint/build.py
2025-05-13 14:12:54 +02:00

240 lines
7.5 KiB
Python

#!/usr/bin/env python3
import os
import sys
import yaml
import subprocess
import json
from pathlib import Path
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', 'hcloud-upload-image', 'jq', 'coreos-installer']
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_ignition_config(config):
"""Generate Ignition configuration."""
system = config['system']
luks = system['luks']
storage = system['storage']
# Generate the Ignition config
ignition_config = {
"ignition": {
"version": "3.4.0",
"config": {
"merge": []
}
},
"storage": {
"disks": [
{
"device": "/dev/sda",
"partitions": [
{
"label": "boot",
"number": 1,
"sizeMiB": storage['boot_size_mib'],
"typeGuid": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
},
{
"label": "root",
"number": 2,
"sizeMiB": 0,
"typeGuid": "21686148-6449-6E6F-744E-656564454649"
}
]
},
{
"device": "/dev/sdb",
"partitions": [
{
"label": "boot2",
"number": 1,
"sizeMiB": storage['boot_size_mib'],
"typeGuid": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
},
{
"label": "root2",
"number": 2,
"sizeMiB": 0,
"typeGuid": "21686148-6449-6E6F-744E-656564454649"
}
]
}
],
"luks": [
{
"name": "root",
"device": "/dev/disk/by-partlabel/root",
"clevis": {
"tpm2": True,
"tang": [
{
"url": luks['tang_url'],
"thumbprint": luks['tang_thumbprint']
}
],
"threshold": 1
}
},
{
"name": "root2",
"device": "/dev/disk/by-partlabel/root2",
"clevis": {
"tpm2": True,
"tang": [
{
"url": luks['tang_url'],
"thumbprint": luks['tang_thumbprint']
}
],
"threshold": 1
}
}
],
"filesystems": [
{
"path": "/boot",
"device": "/dev/disk/by-partlabel/boot",
"format": "vfat",
"label": "boot",
"wipeFilesystem": True
},
{
"path": "/boot2",
"device": "/dev/disk/by-partlabel/boot2",
"format": "vfat",
"label": "boot2",
"wipeFilesystem": True
}
]
},
"systemd": {
"units": [
{
"name": "kernel-sign.service",
"enabled": True,
"contents": "[Unit]\nDescription=Sign new kernel images\nAfter=kernel-install.service\n\n[Service]\nType=oneshot\nExecStart=/usr/local/bin/sign-kernel.sh\nRemainAfterExit=yes\n\n[Install]\nWantedBy=multi-user.target"
}
]
}
}
# Add BTRFS filesystems
for subvol in storage['subvolumes']:
ignition_config['storage']['filesystems'].append({
"path": subvol['path'],
"device": "/dev/disk/by-id/dm-name-root",
"format": "btrfs",
"label": "root",
"wipeFilesystem": subvol['name'] == "@",
"options": [
f"subvol={subvol['name']}",
f"compress={storage['compression']}"
]
})
return json.dumps(ignition_config, indent=2)
def download_fcos_image(config):
"""Download FCOS image."""
image = config['image']
cmd = [
'coreos-installer', 'download',
'-s', image['stream'],
'-p', 'hetzner',
'-a', image['arch'],
'-f', 'raw.xz'
]
subprocess.run(cmd, check=True)
def get_image_file():
"""Get the downloaded image file name."""
files = sorted(Path('.').glob('fedora-coreos-*-hetzner.x86_64.raw.xz'))
if not files:
print("Error: No FCOS image found")
sys.exit(1)
return files[-1]
def delete_existing_image(image_name):
"""Delete existing image if it exists."""
cmd = [
'hcloud', 'image', 'list',
'--type=snapshot',
f'--selector=name={image_name}',
'--output', 'json'
]
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
images = json.loads(result.stdout)
if images:
print("Deleting existing image...")
subprocess.run(['hcloud', 'image', 'delete', str(images[0]['id'])], check=True)
def create_snapshot(config, image_file):
"""Create new snapshot from image."""
image = config['image']
cmd = [
'hcloud-upload-image', 'upload',
'--architecture', image['hetzner_arch'],
'--compression', 'xz',
'--image-path', str(image_file),
'--name', image['name'],
'--labels', f'os=fedora-coreos,channel={image["stream"]}',
'--description', f'Fedora CoreOS ({image["stream"]}, {image["arch"]}) for Nullpoint'
]
subprocess.run(cmd, check=True)
def main():
"""Main entry point."""
# Load config
if not os.path.exists('build-config.yaml'):
print("Error: build-config.yaml not found")
sys.exit(1)
config = load_config('build-config.yaml')
# Check prerequisites
check_prerequisites()
check_hetzner_token()
# Generate Ignition config
print("Generating Ignition config...")
ignition_config = generate_ignition_config(config)
with open('config.ign', 'w') as f:
f.write(ignition_config)
# Download FCOS image
print("Downloading FCOS image...")
download_fcos_image(config)
# Get image file
image_file = get_image_file()
# Delete existing image if it exists
print("Checking for existing image...")
delete_existing_image(config['image']['name'])
# Create new snapshot
print("Creating snapshot from image...")
create_snapshot(config, image_file)
print("Image build complete!")
if __name__ == '__main__':
main()