aboutsummaryrefslogtreecommitdiffstats
path: root/desmo_api/api.py
blob: 750904dfdaa9a67083bdfae517d3afe4442d2327 (plain)
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"}