Initial commit
This commit is contained in:
commit
dbb7983283
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Config files
|
||||||
|
build-config.yaml
|
||||||
|
deploy-config.yaml
|
||||||
|
config.ign
|
||||||
|
|
||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# Downloaded images
|
||||||
|
fedora-coreos-*-hetzner.x86_64.raw.xz
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
96
MASTER_README.md
Normal file
96
MASTER_README.md
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
# Tang Server Setup
|
||||||
|
|
||||||
|
Tang server for remote LUKS unlock. Runs on-premise with logging for future approval system integration.
|
||||||
|
|
||||||
|
## Quick Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Tang
|
||||||
|
# Fedora/CentOS:
|
||||||
|
sudo dnf install tang
|
||||||
|
# Ubuntu:
|
||||||
|
sudo apt install tang
|
||||||
|
|
||||||
|
# Enable and start Tang service
|
||||||
|
sudo systemctl enable tangd.socket
|
||||||
|
sudo systemctl start tangd.socket
|
||||||
|
|
||||||
|
# Generate keys
|
||||||
|
sudo mkdir -p /var/db/tang
|
||||||
|
sudo tangd-keygen /var/db/tang
|
||||||
|
|
||||||
|
# Get thumbprint for Ignition config
|
||||||
|
sudo tang-show-keys /var/db/tang
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
### Connection Security
|
||||||
|
- Tang uses HTTPS for all connections
|
||||||
|
- Each connection is encrypted end-to-end
|
||||||
|
- Tang verifies client identity through challenge-response
|
||||||
|
- Client verifies Tang's identity through signed advertisements
|
||||||
|
|
||||||
|
### Request Logging
|
||||||
|
To log all unlock requests (for future approval system):
|
||||||
|
|
||||||
|
1. Create a wrapper script:
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# /usr/local/bin/tangd-wrapper
|
||||||
|
|
||||||
|
# Get client info
|
||||||
|
CLIENT_IP="$SOCAT_PEERADDR"
|
||||||
|
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
|
echo "$TIMESTAMP: Unlock request from $CLIENT_IP" >> /var/log/tang-requests.log
|
||||||
|
wall "Tang unlock request from $CLIENT_IP at $TIMESTAMP" # Notify all TTYs
|
||||||
|
exec /usr/libexec/tangd "$@"
|
||||||
|
echo "$TIMESTAMP: Request auto-approved" >> /var/log/tang-requests.log
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Make it executable:
|
||||||
|
```bash
|
||||||
|
sudo chmod +x /usr/local/bin/tangd-wrapper
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Configure systemd to use the wrapper:
|
||||||
|
```bash
|
||||||
|
# Create override directory
|
||||||
|
sudo mkdir -p /etc/systemd/system/tangd.socket.d/
|
||||||
|
|
||||||
|
# Create override file
|
||||||
|
sudo tee /etc/systemd/system/tangd.socket.d/override.conf << EOF
|
||||||
|
[Socket]
|
||||||
|
ExecStart=
|
||||||
|
ExecStart=/usr/local/bin/tangd-wrapper
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Reload and restart
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl restart tangd.socket
|
||||||
|
```
|
||||||
|
|
||||||
|
Now when a server requests an unlock:
|
||||||
|
1. A message appears on all TTYs (including SSH sessions)
|
||||||
|
2. The request is logged to `/var/log/tang-requests.log`
|
||||||
|
3. The request is automatically approved
|
||||||
|
4. All actions are logged with timestamps
|
||||||
|
|
||||||
|
Future integration points:
|
||||||
|
- Add webhook support to notify Slack/Discord
|
||||||
|
- Add approval via web interface
|
||||||
|
- Add rate limiting
|
||||||
|
- Add client whitelisting
|
||||||
|
|
||||||
|
## Backup
|
||||||
|
```bash
|
||||||
|
# Backup keys
|
||||||
|
sudo tar -czf tang-keys-$(date +%Y%m%d).tar.gz /var/db/tang/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Recovery
|
||||||
|
If keys are lost:
|
||||||
|
1. Generate new keys
|
||||||
|
2. Update all client configurations
|
||||||
|
3. Re-encrypt all client systems
|
82
README.md
Normal file
82
README.md
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
# Project Nullpoint
|
||||||
|
|
||||||
|
Secure Fedora CoreOS server setup with LUKS encryption, TPM, and BTRFS RAID1.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Fedora CoreOS base
|
||||||
|
- Full disk encryption with LUKS
|
||||||
|
- Remote unlock via Tang server
|
||||||
|
- TPM-based boot verification
|
||||||
|
- BTRFS RAID1 storage
|
||||||
|
- Secure Boot with custom kernel signing
|
||||||
|
|
||||||
|
## Security Model
|
||||||
|
|
||||||
|
### Tang Server Operation
|
||||||
|
The Tang server provides secure remote unlocking of LUKS volumes:
|
||||||
|
1. First connection: Client verifies Tang's public key advertisement
|
||||||
|
2. Boot time: Client sends encrypted challenge to Tang
|
||||||
|
3. Tang proves identity by decrypting challenge
|
||||||
|
4. Client receives key to unlock LUKS volume
|
||||||
|
|
||||||
|
### TPM Integration
|
||||||
|
- TPM2 chip verifies boot integrity
|
||||||
|
- PCR measurements ensure system hasn't been tampered with
|
||||||
|
- Combined with Tang for defense in depth
|
||||||
|
|
||||||
|
## Repository Structure
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── deploy.sh # Deployment script for Hetzner
|
||||||
|
├── generate_ignition.py # Python-based Ignition config generator
|
||||||
|
├── MASTER_README.md # Tang server setup documentation
|
||||||
|
├── README.md # Main project documentation
|
||||||
|
├── requirements.txt # Python dependencies
|
||||||
|
└── settings.yaml # Configuration settings
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install tools
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/hetznercloud/cli/master/install.sh | bash
|
||||||
|
go install github.com/hetznercloud/hcloud-upload-image@latest
|
||||||
|
dnf install jq coreos-installer python3-pyyaml
|
||||||
|
|
||||||
|
# Configure Hetzner
|
||||||
|
export HCLOUD_TOKEN="your-token-here"
|
||||||
|
hcloud ssh-key create --name "fedora-coreos-hetzner" --public-key "$(cat ~/.ssh/id_ed25519.pub)"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. **Configure Build Settings**
|
||||||
|
```bash
|
||||||
|
cp build-config.yaml.example build-config.yaml
|
||||||
|
vim build-config.yaml # Edit LUKS, storage, and image settings
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Build Base Image** (one-time setup)
|
||||||
|
```bash
|
||||||
|
python3 build.py # Creates and uploads FCOS image to Hetzner
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Configure Deployment Settings**
|
||||||
|
```bash
|
||||||
|
cp deploy-config.yaml.example deploy-config.yaml
|
||||||
|
vim deploy-config.yaml # Edit server type, location, and hostname settings
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Deploy Server**
|
||||||
|
```bash
|
||||||
|
python3 deploy.py # Creates new server from base image
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Verify**
|
||||||
|
```bash
|
||||||
|
ssh core@your-server
|
||||||
|
systemctl status clevis-luks-askpass
|
||||||
|
lsblk
|
||||||
|
clevis-luks-list -d /dev/sda2
|
||||||
|
```
|
25
build-config.yaml.example
Normal file
25
build-config.yaml.example
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Build Configuration
|
||||||
|
image:
|
||||||
|
name: fedora-coreos-nullpoint
|
||||||
|
stream: stable
|
||||||
|
arch: x86_64
|
||||||
|
hetzner_arch: x86
|
||||||
|
|
||||||
|
# System Configuration
|
||||||
|
system:
|
||||||
|
# LUKS Configuration
|
||||||
|
luks:
|
||||||
|
tang_url: https://tang.example.com
|
||||||
|
tang_thumbprint: your-tang-thumbprint
|
||||||
|
|
||||||
|
# Storage Configuration
|
||||||
|
storage:
|
||||||
|
boot_size_mib: 512
|
||||||
|
compression: zstd
|
||||||
|
subvolumes:
|
||||||
|
- name: "@"
|
||||||
|
path: "/"
|
||||||
|
- name: "@home"
|
||||||
|
path: "/home"
|
||||||
|
- name: "@var"
|
||||||
|
path: "/var"
|
240
build.py
Normal file
240
build.py
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
#!/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()
|
10
deploy-config.yaml.example
Normal file
10
deploy-config.yaml.example
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Deployment Configuration
|
||||||
|
hetzner:
|
||||||
|
datacenter: nbg1
|
||||||
|
server_type: cx31
|
||||||
|
ssh_key_name: fedora-coreos-hetzner
|
||||||
|
|
||||||
|
# Hostname Configuration
|
||||||
|
hostname:
|
||||||
|
prefix: nullpoint
|
||||||
|
format: "{prefix}-{date}-{random}" # date format: YYMMDD, random: 000-999
|
104
deploy.py
Normal file
104
deploy.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import yaml
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
from datetime import datetime
|
||||||
|
import random
|
||||||
|
|
||||||
|
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(config):
|
||||||
|
"""Generate a unique hostname."""
|
||||||
|
prefix = config['hostname']['prefix']
|
||||||
|
timestamp = datetime.now().strftime('%y%m%d')
|
||||||
|
random_num = random.randint(0, 999)
|
||||||
|
return f"{prefix}-{timestamp}-{random_num:03d}"
|
||||||
|
|
||||||
|
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 create_server(config, hostname, image_id):
|
||||||
|
"""Create a new server."""
|
||||||
|
hetzner = config['hetzner']
|
||||||
|
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', 'config.ign'
|
||||||
|
]
|
||||||
|
subprocess.run(cmd, check=True)
|
||||||
|
|
||||||
|
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(config)
|
||||||
|
|
||||||
|
# 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()
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
pyyaml>=6.0.1
|
Loading…
Reference in New Issue
Block a user