1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
|
from fastapi import FastAPI
from typing import Dict
from .fsm import JailStateMachine
import logging
import sys
import asyncio
import statemachine.exceptions
from contextlib import asynccontextmanager
from .hcloud_dns import HCloudDNS
import os
from . import db, models
import random
import secrets
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
logger = logging.getLogger("api")
STATE_MACHINES: Dict[str, JailStateMachine] = {}
DNS_CLIENT = HCloudDNS(os.environ["HCLOUD_DNS_KEY"])
database = db.DB(os.environ["DATABASE_DSN"])
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info("Running migrations")
await database.migrate()
logger.info("Loading servers from database")
jails = await database.get_jails()
logger.info("Loaded %s jails from database", len(jails))
logger.info(jails)
for jail in jails:
logger.info("Loading server %s with state %s", jail.name, jail.state)
_fsm = JailStateMachine(DNS_CLIENT, database, jail.name)
_fsm.current_state_value = jail.state
_fsm.start_on_enter_task()
STATE_MACHINES[jail.name] = _fsm
yield
logger.info("Closing asyncio clients")
await DNS_CLIENT.close()
await database.close()
logger.info("Closed asyncio clients")
app = FastAPI(lifespan=lifespan)
@app.get("/")
async def read_root():
return {"Hello": "World"}
@app.post(
"/jails",
status_code=201,
)
async def create_jail(req: models.CreateJailRequest) -> models.FullJailInfo:
first_part = secrets.token_hex(2)
second_part = secrets.token_hex(2)
name = f"{req.name}-{first_part}-{second_part}"
ip = os.environ["NETWORK_PREFIX"] + "::" + first_part + ":" + second_part
hosts = os.environ["RUNNER_HOSTS"].split(",")
host = random.choice(hosts)
state = "uninitialized"
await database.insert_jail(name, host, ip, state)
for package in req.packages:
await database.insert_jail_package(name, package)
for i, command in enumerate(req.commands):
await database.insert_jail_command(name, command, i)
STATE_MACHINES[name] = JailStateMachine(DNS_CLIENT, database, name)
STATE_MACHINES[name].initialize()
await asyncio.sleep(1)
return models.FullJailInfo(
name=name,
state=state,
ip=ip,
host=host,
packages=req.packages,
commands=req.commands,
)
@app.get("/jails/{name}")
async def get_server(name: str) -> models.FullJailInfo | Dict[str, str]:
if name not in STATE_MACHINES:
return {"error": "server does not exist"}
jail = await database.get_jail(name)
packages = await database.get_jail_packages(name)
commands = await database.get_jail_commands(name)
return models.FullJailInfo(
name=jail.name,
state=jail.state,
ip=jail.ip,
host=jail.host,
packages=packages,
commands=commands,
)
@app.delete("/jails/{name}")
async def delete_server(name: str) -> Dict[str, str]:
if name not in STATE_MACHINES:
return {"error": "jail does not exist"}
try:
STATE_MACHINES[name].remove_jail()
except statemachine.exceptions.TransitionNotAllowed:
return {"error": "jail is not ready"}
await asyncio.sleep(1)
return {"status": "ok"}
|