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:
+25
infra/prod/.terraform.lock.hcl
···
+
# This file is maintained automatically by "tofu init".
+
# Manual edits may be lost in future updates.
+
+
provider "registry.opentofu.org/renemontilva/clouding" {
+
version = "1.0.1"
+
constraints = ">= 1.0.0"
+
hashes = [
+
"h1:YlLCHWZ0KDPYLw6VPBict8KKsHZVcm0pvWW/kNGjIno=",
+
"zh:187eb96cd2a0768727d735a1a2d0795e46fd783405c133376251d9f24c8effe4",
+
"zh:5ac07c6342f45dccedb7fdb2ab8f62ab008b13c0d9b0290b7ae8af59bae55613",
+
"zh:700f9e08c12ac505307ca08a5516b546a0e5c3f392aae5909fde9f2fa3992000",
+
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
+
"zh:9189d1869e76cf376bca9cc29568a75074d7783035f181e1951daca002cf016f",
+
"zh:9381dc7d860f76688a0aaf390b2e9fa74cdb6a2e75cf1d7b98d9abe6896be316",
+
"zh:9e151d6ae3e105d9d09147e6e340e863489b7e4f5cdb03bb7bc891ba61976a35",
+
"zh:a43a7350787cd2f58753b41b7c1f9164b8c921590be7f45b9ebb96bff5f93363",
+
"zh:a8736ca52ff81e74d820aaea8cb881432bb12d7d9576968f97dc887ac871d048",
+
"zh:d9eaf212086176c75fd954f76c62628b59456913033b5bc1c733335af1bf5f57",
+
"zh:de283437e2d335cf9249c527c77f56a888d870125d3623f7b51c56fc2f5a7281",
+
"zh:dfdad92f93b603f1863b2d66c72acdf5d044e37488fe2d45c83e3ba512412862",
+
"zh:e2c5446a7705d34f3e44037c88eec213fc291d11d810315ca9942fd0cfda8071",
+
"zh:ebb9bc6639d8bfa9edfb3927f31ab012b76f158c95f96055af3dbfbfbb99e10d",
+
"zh:ec431afc740d6b1c1c29f37f3de3a751c55ce18cae2bbd215e435ec79a64ce65",
+
]
+
}
+98
infra/prod/main.tf
···
+
terraform {
+
required_providers {
+
clouding = {
+
source = "astrojuanlu/clouding"
+
version = "1.0.1"
+
}
+
}
+
}
+
+
provider "clouding" {}
+
+
data "clouding_sshkey" "main" {
+
id = "LQbN5nv9krK9JaeZ"
+
}
+
+
data "clouding_image" "ubuntu_24_04" {
+
id = "p06Wq42PGkneDVEb"
+
}
+
+
resource "clouding_firewall" "knot" {
+
name = "Knot Firewall"
+
description = "Firewall rules for Knot server (SSH, Git SSH, Web)"
+
}
+
+
# Allow SSH (port 22)
+
resource "clouding_firewall_rule" "ssh" {
+
firewall_id = clouding_firewall.knot.id
+
description = "Allow SSH"
+
protocol = "tcp"
+
port_range_min = 22
+
port_range_max = 22
+
source_ip = "0.0.0.0/0"
+
}
+
+
# Allow Git SSH (port 2222)
+
resource "clouding_firewall_rule" "git_ssh" {
+
firewall_id = clouding_firewall.knot.id
+
description = "Allow Git SSH"
+
protocol = "tcp"
+
port_range_min = 2222
+
port_range_max = 2222
+
source_ip = "0.0.0.0/0"
+
}
+
+
# Allow HTTP (port 80) for Let's Encrypt certificate challenges
+
resource "clouding_firewall_rule" "http" {
+
firewall_id = clouding_firewall.knot.id
+
description = "Allow HTTP (Let's Encrypt)"
+
protocol = "tcp"
+
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"
+
}
+
+
# Create a server for Knot
+
resource "clouding_server" "knot0" {
+
name = "nudo0"
+
hostname = "nudo0"
+
flavor_id = "0.5x1"
+
firewall_id = clouding_firewall.knot.id
+
+
volume = {
+
source = "image"
+
id = data.clouding_image.ubuntu_24_04.id
+
ssd_gb = 20
+
}
+
+
access_configuration = {
+
ssh_key_id = data.clouding_sshkey.main.id
+
}
+
+
enable_strict_antiddos_filtering = false
+
+
# backup_preference = {
+
# frequency = "OneWeek"
+
# slots = 4
+
# }
+
+
# user_data = file("${path.module}/cloud-init.yaml")
+
+
timeouts = {
+
create = "10m"
+
}
+
}
+
+
output "knot0_ipv4" {
+
value = try(clouding_server.knot0.public_ip, "Not yet assigned")
+
}