Compare changes

Choose any two refs to compare.

+1
.gitignore
···
terraform.rc
# End of https://www.toptal.com/developers/gitignore/api/ansible,terraform
+
.envrc
+1 -1
config/deploy.yaml
···
---
- name: Deploy Tangled Knot
-
hosts: all
+
hosts: knot_servers
become: true
roles:
+10
config/inventory/dev/incus.yaml
···
+
---
+
plugin: community.general.incus
+
strict: true
+
+
remotes:
+
- local:tangled
+
+
compose:
+
ansible_connection: "'community.general.incus'"
+
ansible_python_interpreter: "'auto_silent'"
+6
config/inventory/dev/incus_groups.yaml
···
+
---
+
all:
+
children:
+
knot_servers:
+
hosts:
+
nudo0.tangled.local: {}
-10
config/inventory/incus.yaml
···
-
---
-
plugin: community.general.incus
-
strict: true
-
-
remotes:
-
- local:tangled
-
-
compose:
-
ansible_connection: "'community.general.incus'"
-
ansible_python_interpreter: "'auto_silent'"
+127
config/inventory/prod/clouding.py
···
+
#!/usr/bin/env python3
+
"""
+
Dynamic inventory script for Clouding.io servers.
+
Fetches server list from Clouding API and generates Ansible inventory.
+
+
Requirements:
+
- CLOUDING_TOKEN environment variable with API token
+
+
Usage:
+
ansible-inventory -i inventory/prod/clouding.py --list
+
ansible-playbook -i inventory/prod/clouding.py playbook.yaml
+
"""
+
+
from __future__ import annotations
+
+
import json
+
import os
+
import sys
+
from urllib.request import Request, urlopen
+
from urllib.error import URLError, HTTPError
+
from dataclasses import dataclass
+
+
+
@dataclass
+
class CloudingInventory:
+
"""Dynamic inventory for Clouding.io servers."""
+
+
inventory: dict
+
api_token: str
+
endpoint: str = "https://api.clouding.io/v1/servers/"
+
+
@classmethod
+
def create(cls, endpoint: str | None = None) -> CloudingInventory:
+
api_token = os.environ.get("CLOUDING_TOKEN")
+
if not api_token:
+
print("Error: CLOUDING_TOKEN environment variable not set", file=sys.stderr)
+
sys.exit(1)
+
+
inventory = {"_meta": {"hostvars": {}}, "all": {"children": ["ungrouped"]}}
+
+
if endpoint:
+
return cls(inventory, api_token, endpoint)
+
else:
+
return cls(inventory, api_token)
+
+
def fetch_servers(self):
+
"""Fetch server list from Clouding API."""
+
headers = {"X-API-KEY": self.api_token, "Accept": "application/json"}
+
+
try:
+
request = Request(self.endpoint, headers=headers)
+
with urlopen(request) as response:
+
data = json.loads(response.read().decode("utf-8"))
+
return data.get("servers", [])
+
except HTTPError as e:
+
print(f"HTTP Error {e.code}: {e.reason}", file=sys.stderr)
+
sys.exit(1)
+
except URLError as e:
+
print(f"URL Error: {e.reason}", file=sys.stderr)
+
sys.exit(1)
+
except Exception as e:
+
print(f"Error fetching servers: {e}", file=sys.stderr)
+
sys.exit(1)
+
+
def add_server_to_inventory(self, server):
+
"""Add a server to the inventory."""
+
server_id = server.get("id")
+
server_name = server.get("name")
+
status = server.get("status")
+
power_state = server.get("powerState")
+
public_ip = server.get("publicIp")
+
+
if status != "Active" or power_state != "Running":
+
return
+
+
if not public_ip:
+
return
+
+
self.inventory["_meta"]["hostvars"][server_name] = {
+
"ansible_host": public_ip,
+
"ansible_user": "root", # Clouding uses root by default
+
"ansible_python_interpreter": "auto_silent",
+
"clouding_id": server_id,
+
"clouding_hostname": server.get("hostname"),
+
"clouding_status": status,
+
"clouding_power_state": power_state,
+
"clouding_vcores": server.get("vCores"),
+
"clouding_ram_gb": server.get("ramGb"),
+
"clouding_flavor": server.get("flavor"),
+
"clouding_volume_size_gb": server.get("volumeSizeGb"),
+
"clouding_dns_address": server.get("dnsAddress"),
+
}
+
+
image = server.get("image", {})
+
if image:
+
self.inventory["_meta"]["hostvars"][server_name].update(
+
{
+
"clouding_image_id": image.get("id"),
+
"clouding_image_name": image.get("name"),
+
}
+
)
+
+
# Add to 'clouding' group
+
if "clouding" not in self.inventory:
+
self.inventory["clouding"] = {"hosts": []}
+
self.inventory["all"]["children"].append("clouding")
+
+
if server_name not in self.inventory["clouding"]["hosts"]:
+
self.inventory["clouding"]["hosts"].append(server_name)
+
+
def generate_inventory(self):
+
"""Generate the complete inventory."""
+
servers = self.fetch_servers()
+
for server in servers:
+
self.add_server_to_inventory(server)
+
return self.inventory
+
+
+
def main():
+
"""Main entry point."""
+
inventory = CloudingInventory.create()
+
result = inventory.generate_inventory()
+
print(json.dumps(result, indent=2))
+
+
+
if __name__ == "__main__":
+
main()
+9
config/inventory/prod/clouding_groups.yaml
···
+
---
+
all:
+
children:
+
knot_servers:
+
hosts:
+
nudo0:
+
knot_enable_caddy: true
+
knot_server_hostname: "knot.juanlu.space"
+
knot_server_owner: "did:plc:p7v4p6njfpdv6gen4bllnkqm"
-7
config/ping.yaml
···
-
---
-
- name: Test inventory and connectivity
-
hosts: all
-
gather_facts: false
-
tasks:
-
- name: Ping
-
ansible.builtin.ping:
+16 -6
infra/prod/main.tf
···
source_ip = "0.0.0.0/0"
}
-
# Allow Knot server (port 5555)
-
resource "clouding_firewall_rule" "knot_server" {
+
# Allow HTTP (port 80) for Let's Encrypt certificate challenges
+
resource "clouding_firewall_rule" "http" {
firewall_id = clouding_firewall.knot.id
-
description = "Allow Knot server"
+
description = "Allow HTTP (Let's Encrypt)"
protocol = "tcp"
-
port_range_min = 5555
-
port_range_max = 5555
+
port_range_min = 80
+
port_range_max = 80
+
source_ip = "0.0.0.0/0"
+
}
+
+
# Allow HTTPS (port 443) for Caddy SSL proxy
+
resource "clouding_firewall_rule" "https" {
+
firewall_id = clouding_firewall.knot.id
+
description = "Allow HTTPS (Caddy)"
+
protocol = "tcp"
+
port_range_min = 443
+
port_range_max = 443
source_ip = "0.0.0.0/0"
}
···
}
output "knot0_ipv4" {
-
value = try(clouding_server.knot0.hostname, "Not yet assigned")
+
value = try(clouding_server.knot0.public_ip, "Not yet assigned")
}