240 lines
7.5 KiB
Python
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() |