goosebit 0.1.2__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- goosebit/__init__.py +50 -19
- goosebit/__main__.py +7 -0
- goosebit/api/responses.py +5 -0
- goosebit/api/routes.py +5 -15
- goosebit/api/telemetry/__init__.py +1 -0
- goosebit/{telemetry/__init__.py → api/telemetry/metrics.py} +9 -3
- goosebit/api/telemetry/prometheus/__init__.py +2 -0
- goosebit/api/telemetry/prometheus/readers.py +3 -0
- goosebit/api/telemetry/prometheus/routes.py +18 -0
- goosebit/api/telemetry/routes.py +9 -0
- goosebit/api/v1/__init__.py +1 -0
- goosebit/api/v1/devices/__init__.py +1 -0
- goosebit/api/v1/devices/device/__init__.py +1 -0
- goosebit/api/v1/devices/device/responses.py +13 -0
- goosebit/api/v1/devices/device/routes.py +27 -0
- goosebit/api/v1/devices/requests.py +7 -0
- goosebit/api/v1/devices/responses.py +16 -0
- goosebit/api/v1/devices/routes.py +35 -0
- goosebit/api/v1/download/__init__.py +1 -0
- goosebit/api/v1/download/routes.py +22 -0
- goosebit/api/v1/rollouts/__init__.py +1 -0
- goosebit/api/v1/rollouts/requests.py +16 -0
- goosebit/api/v1/rollouts/responses.py +19 -0
- goosebit/api/v1/rollouts/routes.py +50 -0
- goosebit/api/v1/routes.py +9 -0
- goosebit/api/v1/software/__init__.py +1 -0
- goosebit/api/v1/software/requests.py +5 -0
- goosebit/api/v1/software/responses.py +16 -0
- goosebit/api/v1/software/routes.py +77 -0
- goosebit/auth/__init__.py +101 -101
- goosebit/db/__init__.py +11 -0
- goosebit/db/config.py +10 -0
- goosebit/db/migrations/models/0_20240830054046_init.py +136 -0
- goosebit/{models.py → db/models.py} +17 -10
- goosebit/realtime/logs.py +4 -3
- goosebit/realtime/routes.py +2 -2
- goosebit/schema/__init__.py +0 -0
- goosebit/schema/devices.py +73 -0
- goosebit/schema/rollouts.py +31 -0
- goosebit/schema/software.py +37 -0
- goosebit/settings/__init__.py +17 -0
- goosebit/settings/const.py +21 -0
- goosebit/settings/schema.py +86 -0
- goosebit/ui/bff/__init__.py +1 -0
- goosebit/ui/bff/devices/__init__.py +1 -0
- goosebit/ui/bff/devices/requests.py +12 -0
- goosebit/ui/bff/devices/responses.py +39 -0
- goosebit/ui/bff/devices/routes.py +72 -0
- goosebit/ui/bff/download/__init__.py +1 -0
- goosebit/ui/bff/download/routes.py +22 -0
- goosebit/ui/bff/rollouts/__init__.py +1 -0
- goosebit/ui/bff/rollouts/responses.py +37 -0
- goosebit/ui/bff/rollouts/routes.py +52 -0
- goosebit/ui/bff/routes.py +11 -0
- goosebit/ui/bff/software/__init__.py +1 -0
- goosebit/ui/bff/software/responses.py +37 -0
- goosebit/ui/bff/software/routes.py +83 -0
- goosebit/ui/nav.py +16 -0
- goosebit/ui/routes.py +29 -66
- goosebit/ui/static/favicon.ico +0 -0
- goosebit/ui/static/favicon.svg +1 -1
- goosebit/ui/static/js/devices.js +47 -71
- goosebit/ui/static/js/index.js +4 -9
- goosebit/ui/static/js/login.js +23 -0
- goosebit/ui/static/js/logs.js +1 -1
- goosebit/ui/static/js/rollouts.js +33 -19
- goosebit/ui/static/js/{firmware.js → software.js} +87 -86
- goosebit/ui/static/js/util.js +60 -6
- goosebit/ui/static/svg/goosebit-logo.svg +1 -1
- goosebit/ui/templates/__init__.py +9 -1
- goosebit/ui/templates/devices.html.jinja +75 -0
- goosebit/ui/templates/index.html.jinja +25 -0
- goosebit/ui/templates/login.html.jinja +57 -0
- goosebit/ui/templates/logs.html.jinja +31 -0
- goosebit/ui/templates/nav.html.jinja +84 -0
- goosebit/ui/templates/rollouts.html.jinja +93 -0
- goosebit/ui/templates/software.html.jinja +139 -0
- goosebit/updater/controller/v1/routes.py +101 -96
- goosebit/updater/controller/v1/schema.py +56 -0
- goosebit/updater/manager.py +65 -65
- goosebit/updater/routes.py +3 -11
- goosebit/updates/__init__.py +91 -32
- goosebit/updates/swdesc.py +2 -7
- goosebit-0.2.1.dist-info/METADATA +173 -0
- goosebit-0.2.1.dist-info/RECORD +95 -0
- goosebit/api/devices.py +0 -136
- goosebit/api/download.py +0 -34
- goosebit/api/firmware.py +0 -57
- goosebit/api/helper.py +0 -30
- goosebit/api/rollouts.py +0 -87
- goosebit/db.py +0 -37
- goosebit/permissions.py +0 -75
- goosebit/settings.py +0 -64
- goosebit/telemetry/prometheus.py +0 -10
- goosebit/ui/templates/devices.html +0 -115
- goosebit/ui/templates/firmware.html +0 -163
- goosebit/ui/templates/index.html +0 -23
- goosebit/ui/templates/login.html +0 -65
- goosebit/ui/templates/logs.html +0 -36
- goosebit/ui/templates/nav.html +0 -117
- goosebit/ui/templates/rollouts.html +0 -76
- goosebit-0.1.2.dist-info/METADATA +0 -123
- goosebit-0.1.2.dist-info/RECORD +0 -51
- {goosebit-0.1.2.dist-info → goosebit-0.2.1.dist-info}/LICENSE +0 -0
- {goosebit-0.1.2.dist-info → goosebit-0.2.1.dist-info}/WHEEL +0 -0
goosebit/__init__.py
CHANGED
@@ -1,19 +1,18 @@
|
|
1
|
+
import importlib.metadata
|
1
2
|
from contextlib import asynccontextmanager
|
2
3
|
from typing import Annotated
|
3
4
|
|
4
5
|
from fastapi import Depends, FastAPI
|
6
|
+
from fastapi.openapi.docs import get_swagger_ui_html
|
5
7
|
from fastapi.requests import Request
|
6
8
|
from fastapi.responses import RedirectResponse
|
7
9
|
from fastapi.security import OAuth2PasswordRequestForm
|
8
10
|
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor as Instrumentor
|
9
11
|
|
10
|
-
from goosebit import api, db, realtime,
|
11
|
-
from goosebit.
|
12
|
-
|
13
|
-
|
14
|
-
create_session,
|
15
|
-
get_current_user,
|
16
|
-
)
|
12
|
+
from goosebit import api, db, realtime, ui, updater
|
13
|
+
from goosebit.api.telemetry import metrics
|
14
|
+
from goosebit.auth import get_user_from_request, login_user, redirect_if_authenticated
|
15
|
+
from goosebit.ui.nav import nav
|
17
16
|
from goosebit.ui.static import static
|
18
17
|
from goosebit.ui.templates import templates
|
19
18
|
|
@@ -21,12 +20,30 @@ from goosebit.ui.templates import templates
|
|
21
20
|
@asynccontextmanager
|
22
21
|
async def lifespan(_: FastAPI):
|
23
22
|
await db.init()
|
24
|
-
await
|
23
|
+
await metrics.init()
|
25
24
|
yield
|
26
25
|
await db.close()
|
27
26
|
|
28
27
|
|
29
|
-
app = FastAPI(
|
28
|
+
app = FastAPI(
|
29
|
+
title="gooseBit",
|
30
|
+
summary="A simplistic, opinionated remote update server implementing hawkBit™'s DDI API.",
|
31
|
+
version=importlib.metadata.version("goosebit"),
|
32
|
+
lifespan=lifespan,
|
33
|
+
license_info={
|
34
|
+
"name": "Apache 2.0",
|
35
|
+
"identifier": "Apache-2.0",
|
36
|
+
},
|
37
|
+
redoc_url=None,
|
38
|
+
docs_url=None,
|
39
|
+
openapi_tags=[
|
40
|
+
{
|
41
|
+
"name": "login",
|
42
|
+
"description": "API authentication. "
|
43
|
+
"Can be used in the `authorization` header, in the format `{token_type} {access_token}`.",
|
44
|
+
}
|
45
|
+
],
|
46
|
+
)
|
30
47
|
app.include_router(updater.router)
|
31
48
|
app.include_router(ui.router)
|
32
49
|
app.include_router(api.router)
|
@@ -37,7 +54,13 @@ Instrumentor.instrument_app(app)
|
|
37
54
|
|
38
55
|
@app.middleware("http")
|
39
56
|
async def attach_user(request: Request, call_next):
|
40
|
-
request.scope["user"] =
|
57
|
+
request.scope["user"] = await get_user_from_request(request)
|
58
|
+
return await call_next(request)
|
59
|
+
|
60
|
+
|
61
|
+
@app.middleware("http")
|
62
|
+
async def attach_nav(request: Request, call_next):
|
63
|
+
request.scope["nav"] = nav.get()
|
41
64
|
return await call_next(request)
|
42
65
|
|
43
66
|
|
@@ -46,20 +69,28 @@ def root_redirect(request: Request):
|
|
46
69
|
return RedirectResponse(request.url_for("ui_root"))
|
47
70
|
|
48
71
|
|
49
|
-
@app.get("/login", dependencies=[Depends(
|
50
|
-
async def
|
51
|
-
return templates.TemplateResponse(request, "login.html")
|
72
|
+
@app.get("/login", include_in_schema=False, dependencies=[Depends(redirect_if_authenticated)])
|
73
|
+
async def login_get(request: Request):
|
74
|
+
return templates.TemplateResponse(request, "login.html.jinja")
|
52
75
|
|
53
76
|
|
54
|
-
@app.post("/login",
|
55
|
-
async def
|
56
|
-
|
57
|
-
resp.set_cookie(key="session_id", value=create_session(form_data.username))
|
58
|
-
return resp
|
77
|
+
@app.post("/login", tags=["login"])
|
78
|
+
async def login_post(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
|
79
|
+
return {"access_token": login_user(form_data.username, form_data.password), "token_type": "bearer"}
|
59
80
|
|
60
81
|
|
61
82
|
@app.get("/logout", include_in_schema=False)
|
62
83
|
async def logout(request: Request):
|
63
|
-
resp = RedirectResponse(request.url_for("
|
84
|
+
resp = RedirectResponse(request.url_for("login_get"), status_code=302)
|
64
85
|
resp.delete_cookie(key="session_id")
|
65
86
|
return resp
|
87
|
+
|
88
|
+
|
89
|
+
@app.get("/docs")
|
90
|
+
async def swagger_docs(request: Request):
|
91
|
+
return get_swagger_ui_html(
|
92
|
+
title="gooseBit docs",
|
93
|
+
openapi_url="/openapi.json",
|
94
|
+
swagger_favicon_url=str(request.url_for("static", path="/favicon.svg")),
|
95
|
+
swagger_ui_parameters={"operationsSorter": "alpha"},
|
96
|
+
)
|
goosebit/__main__.py
ADDED
goosebit/api/routes.py
CHANGED
@@ -1,19 +1,9 @@
|
|
1
1
|
from fastapi import APIRouter, Depends
|
2
2
|
|
3
|
-
from goosebit.
|
4
|
-
from goosebit.auth import authenticate_api_session
|
3
|
+
from goosebit.auth import validate_current_user
|
5
4
|
|
6
|
-
|
7
|
-
main_router = APIRouter(prefix="/api", dependencies=[Depends(authenticate_api_session)], tags=["api"])
|
8
|
-
main_router.include_router(firmware.router)
|
9
|
-
main_router.include_router(devices.router)
|
10
|
-
main_router.include_router(rollouts.router)
|
5
|
+
from . import telemetry, v1
|
11
6
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
# include both routers
|
17
|
-
router = APIRouter()
|
18
|
-
router.include_router(main_router)
|
19
|
-
router.include_router(download_router)
|
7
|
+
router = APIRouter(prefix="/api", dependencies=[Depends(validate_current_user)])
|
8
|
+
router.include_router(telemetry.router)
|
9
|
+
router.include_router(v1.router)
|
@@ -0,0 +1 @@
|
|
1
|
+
from .routes import router # noqa: F401
|
@@ -2,13 +2,19 @@ from opentelemetry import metrics
|
|
2
2
|
from opentelemetry.sdk.metrics import MeterProvider
|
3
3
|
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
|
4
4
|
|
5
|
-
from goosebit import
|
5
|
+
from goosebit.settings import USERS, config
|
6
6
|
|
7
7
|
from . import prometheus
|
8
8
|
|
9
|
+
readers = []
|
10
|
+
|
11
|
+
if config.metrics.prometheus.enable:
|
12
|
+
readers.append(prometheus.reader)
|
13
|
+
|
14
|
+
|
9
15
|
resource = Resource(attributes={SERVICE_NAME: "goosebit"})
|
10
16
|
|
11
|
-
provider = MeterProvider(resource=resource, metric_readers=
|
17
|
+
provider = MeterProvider(resource=resource, metric_readers=readers)
|
12
18
|
metrics.set_meter_provider(provider)
|
13
19
|
|
14
20
|
meter = metrics.get_meter("goosebit.meter")
|
@@ -25,4 +31,4 @@ users_count = meter.create_gauge(
|
|
25
31
|
|
26
32
|
|
27
33
|
async def init():
|
28
|
-
users_count.set(len(
|
34
|
+
users_count.set(len(USERS))
|
@@ -0,0 +1,18 @@
|
|
1
|
+
from fastapi import APIRouter, Header
|
2
|
+
from fastapi.requests import Request
|
3
|
+
from fastapi.responses import Response
|
4
|
+
from prometheus_client import REGISTRY
|
5
|
+
from prometheus_client.exposition import _bake_output
|
6
|
+
|
7
|
+
router = APIRouter(prefix="/prometheus", tags=["prometheus"])
|
8
|
+
|
9
|
+
|
10
|
+
@router.get("/metrics")
|
11
|
+
async def metrics(
|
12
|
+
request: Request, accept: list[str] = Header(None), accept_encoding: list[str] = Header(None)
|
13
|
+
) -> Response:
|
14
|
+
status, http_headers, output = _bake_output(
|
15
|
+
REGISTRY, ",".join(accept), ",".join(accept_encoding), request.query_params, False
|
16
|
+
)
|
17
|
+
headers = {h[0]: h[1] for h in http_headers}
|
18
|
+
return Response(content=output, headers=headers)
|
@@ -0,0 +1 @@
|
|
1
|
+
from .routes import router # noqa: F401
|
@@ -0,0 +1 @@
|
|
1
|
+
from .routes import router # noqa : F401
|
@@ -0,0 +1 @@
|
|
1
|
+
from .routes import router # noqa : F401
|
@@ -0,0 +1,27 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from fastapi import APIRouter, Depends, Security
|
4
|
+
from fastapi.requests import Request
|
5
|
+
|
6
|
+
from goosebit.api.v1.devices.device.responses import DeviceLogResponse, DeviceResponse
|
7
|
+
from goosebit.auth import validate_user_permissions
|
8
|
+
from goosebit.updater.manager import UpdateManager, get_update_manager
|
9
|
+
|
10
|
+
router = APIRouter(prefix="/{dev_id}")
|
11
|
+
|
12
|
+
|
13
|
+
@router.get(
|
14
|
+
"",
|
15
|
+
dependencies=[Security(validate_user_permissions, scopes=["home.read"])],
|
16
|
+
)
|
17
|
+
async def device_get(_: Request, updater: UpdateManager = Depends(get_update_manager)) -> DeviceResponse:
|
18
|
+
return await DeviceResponse.convert(await updater.get_device())
|
19
|
+
|
20
|
+
|
21
|
+
@router.get(
|
22
|
+
"/log",
|
23
|
+
dependencies=[Security(validate_user_permissions, scopes=["home.read"])],
|
24
|
+
)
|
25
|
+
async def device_logs(_: Request, updater: UpdateManager = Depends(get_update_manager)) -> DeviceLogResponse:
|
26
|
+
device = await updater.get_device()
|
27
|
+
return DeviceLogResponse(log=device.last_log)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
|
5
|
+
from pydantic import BaseModel
|
6
|
+
|
7
|
+
from goosebit.db.models import Device
|
8
|
+
from goosebit.schema.devices import DeviceSchema
|
9
|
+
|
10
|
+
|
11
|
+
class DevicesResponse(BaseModel):
|
12
|
+
devices: list[DeviceSchema]
|
13
|
+
|
14
|
+
@classmethod
|
15
|
+
async def convert(cls, devices: list[Device]):
|
16
|
+
return cls(devices=await asyncio.gather(*[DeviceSchema.convert(d) for d in devices]))
|
@@ -0,0 +1,35 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from fastapi import APIRouter, Security
|
4
|
+
from fastapi.requests import Request
|
5
|
+
|
6
|
+
from goosebit.api.responses import StatusResponse
|
7
|
+
from goosebit.auth import validate_user_permissions
|
8
|
+
from goosebit.db.models import Device
|
9
|
+
from goosebit.updater.manager import delete_devices
|
10
|
+
|
11
|
+
from . import device
|
12
|
+
from .requests import DevicesDeleteRequest
|
13
|
+
from .responses import DevicesResponse
|
14
|
+
|
15
|
+
router = APIRouter(prefix="/devices", tags=["devices"])
|
16
|
+
|
17
|
+
|
18
|
+
@router.get(
|
19
|
+
"",
|
20
|
+
dependencies=[Security(validate_user_permissions, scopes=["home.read"])],
|
21
|
+
)
|
22
|
+
async def devices_get(_: Request) -> DevicesResponse:
|
23
|
+
return await DevicesResponse.convert(await Device.all().prefetch_related("assigned_software", "hardware"))
|
24
|
+
|
25
|
+
|
26
|
+
@router.delete(
|
27
|
+
"",
|
28
|
+
dependencies=[Security(validate_user_permissions, scopes=["device.delete"])],
|
29
|
+
)
|
30
|
+
async def devices_delete(_: Request, config: DevicesDeleteRequest) -> StatusResponse:
|
31
|
+
await delete_devices(config.devices)
|
32
|
+
return StatusResponse(success=True)
|
33
|
+
|
34
|
+
|
35
|
+
router.include_router(device.router)
|
@@ -0,0 +1 @@
|
|
1
|
+
from .routes import router # noqa : F401
|
@@ -0,0 +1,22 @@
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
2
|
+
from fastapi.requests import Request
|
3
|
+
from fastapi.responses import FileResponse, RedirectResponse
|
4
|
+
|
5
|
+
from goosebit.db.models import Software
|
6
|
+
|
7
|
+
router = APIRouter(prefix="/download", tags=["download"])
|
8
|
+
|
9
|
+
|
10
|
+
@router.get("/{file_id}")
|
11
|
+
async def download_file(_: Request, file_id: int):
|
12
|
+
software = await Software.get_or_none(id=file_id)
|
13
|
+
if software is None:
|
14
|
+
raise HTTPException(404)
|
15
|
+
if software.local:
|
16
|
+
return FileResponse(
|
17
|
+
software.path,
|
18
|
+
media_type="application/octet-stream",
|
19
|
+
filename=software.path.name,
|
20
|
+
)
|
21
|
+
else:
|
22
|
+
return RedirectResponse(url=software.uri)
|
@@ -0,0 +1 @@
|
|
1
|
+
from .routes import router # noqa: F401
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from pydantic import BaseModel
|
2
|
+
|
3
|
+
|
4
|
+
class RolloutsPutRequest(BaseModel):
|
5
|
+
name: str
|
6
|
+
feed: str
|
7
|
+
software_id: int
|
8
|
+
|
9
|
+
|
10
|
+
class RolloutsPatchRequest(BaseModel):
|
11
|
+
ids: list[int]
|
12
|
+
paused: bool
|
13
|
+
|
14
|
+
|
15
|
+
class RolloutsDeleteRequest(BaseModel):
|
16
|
+
ids: list[int]
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import asyncio
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
|
5
|
+
from goosebit.api.responses import StatusResponse
|
6
|
+
from goosebit.db.models import Rollout
|
7
|
+
from goosebit.schema.rollouts import RolloutSchema
|
8
|
+
|
9
|
+
|
10
|
+
class RolloutsPutResponse(StatusResponse):
|
11
|
+
id: int
|
12
|
+
|
13
|
+
|
14
|
+
class RolloutsResponse(BaseModel):
|
15
|
+
rollouts: list[RolloutSchema]
|
16
|
+
|
17
|
+
@classmethod
|
18
|
+
async def convert(cls, devices: list[Rollout]):
|
19
|
+
return cls(rollouts=await asyncio.gather(*[RolloutSchema.convert(d) for d in devices]))
|
@@ -0,0 +1,50 @@
|
|
1
|
+
from fastapi import APIRouter, Security
|
2
|
+
from fastapi.requests import Request
|
3
|
+
|
4
|
+
from goosebit.api.responses import StatusResponse
|
5
|
+
from goosebit.auth import validate_user_permissions
|
6
|
+
from goosebit.db.models import Rollout
|
7
|
+
|
8
|
+
from .requests import RolloutsDeleteRequest, RolloutsPatchRequest, RolloutsPutRequest
|
9
|
+
from .responses import RolloutsPutResponse, RolloutsResponse
|
10
|
+
|
11
|
+
router = APIRouter(prefix="/rollouts", tags=["rollouts"])
|
12
|
+
|
13
|
+
|
14
|
+
@router.get(
|
15
|
+
"",
|
16
|
+
dependencies=[Security(validate_user_permissions, scopes=["rollout.read"])],
|
17
|
+
)
|
18
|
+
async def rollouts_get(_: Request) -> RolloutsResponse:
|
19
|
+
return await RolloutsResponse.convert(await Rollout.all().prefetch_related("software"))
|
20
|
+
|
21
|
+
|
22
|
+
@router.post(
|
23
|
+
"",
|
24
|
+
dependencies=[Security(validate_user_permissions, scopes=["rollout.write"])],
|
25
|
+
)
|
26
|
+
async def rollouts_put(_: Request, rollout: RolloutsPutRequest) -> RolloutsPutResponse:
|
27
|
+
rollout = await Rollout.create(
|
28
|
+
name=rollout.name,
|
29
|
+
feed=rollout.feed,
|
30
|
+
software_id=rollout.software_id,
|
31
|
+
)
|
32
|
+
return RolloutsPutResponse(success=True, id=rollout.id)
|
33
|
+
|
34
|
+
|
35
|
+
@router.patch(
|
36
|
+
"",
|
37
|
+
dependencies=[Security(validate_user_permissions, scopes=["rollout.write"])],
|
38
|
+
)
|
39
|
+
async def rollouts_patch(_: Request, rollouts: RolloutsPatchRequest) -> StatusResponse:
|
40
|
+
await Rollout.filter(id__in=rollouts.ids).update(paused=rollouts.paused)
|
41
|
+
return StatusResponse(success=True)
|
42
|
+
|
43
|
+
|
44
|
+
@router.delete(
|
45
|
+
"",
|
46
|
+
dependencies=[Security(validate_user_permissions, scopes=["rollout.delete"])],
|
47
|
+
)
|
48
|
+
async def rollouts_delete(_: Request, rollouts: RolloutsDeleteRequest) -> StatusResponse:
|
49
|
+
await Rollout.filter(id__in=rollouts.ids).delete()
|
50
|
+
return StatusResponse(success=True)
|
@@ -0,0 +1,9 @@
|
|
1
|
+
from fastapi import APIRouter
|
2
|
+
|
3
|
+
from . import devices, download, rollouts, software
|
4
|
+
|
5
|
+
router = APIRouter(prefix="/v1")
|
6
|
+
router.include_router(software.router)
|
7
|
+
router.include_router(devices.router)
|
8
|
+
router.include_router(rollouts.router)
|
9
|
+
router.include_router(download.router)
|
@@ -0,0 +1 @@
|
|
1
|
+
from .routes import router # noqa: F401
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
|
5
|
+
from pydantic import BaseModel
|
6
|
+
|
7
|
+
from goosebit.db.models import Software
|
8
|
+
from goosebit.schema.software import SoftwareSchema
|
9
|
+
|
10
|
+
|
11
|
+
class SoftwareResponse(BaseModel):
|
12
|
+
software: list[SoftwareSchema]
|
13
|
+
|
14
|
+
@classmethod
|
15
|
+
async def convert(cls, software: list[Software]):
|
16
|
+
return cls(software=await asyncio.gather(*[SoftwareSchema.convert(f) for f in software]))
|
@@ -0,0 +1,77 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
|
3
|
+
import aiofiles
|
4
|
+
from fastapi import APIRouter, File, Form, HTTPException, Security, UploadFile
|
5
|
+
from fastapi.requests import Request
|
6
|
+
|
7
|
+
from goosebit.api.responses import StatusResponse
|
8
|
+
from goosebit.auth import validate_user_permissions
|
9
|
+
from goosebit.db.models import Rollout, Software
|
10
|
+
from goosebit.settings import config
|
11
|
+
from goosebit.updates import create_software_update
|
12
|
+
|
13
|
+
from .requests import SoftwareDeleteRequest
|
14
|
+
from .responses import SoftwareResponse
|
15
|
+
|
16
|
+
router = APIRouter(prefix="/software", tags=["software"])
|
17
|
+
|
18
|
+
|
19
|
+
@router.get(
|
20
|
+
"",
|
21
|
+
dependencies=[Security(validate_user_permissions, scopes=["software.read"])],
|
22
|
+
)
|
23
|
+
async def software_get(_: Request) -> SoftwareResponse:
|
24
|
+
return await SoftwareResponse.convert(await Software.all().prefetch_related("compatibility"))
|
25
|
+
|
26
|
+
|
27
|
+
@router.delete(
|
28
|
+
"",
|
29
|
+
dependencies=[Security(validate_user_permissions, scopes=["software.delete"])],
|
30
|
+
)
|
31
|
+
async def software_delete(_: Request, delete_req: SoftwareDeleteRequest) -> StatusResponse:
|
32
|
+
success = False
|
33
|
+
for software_id in delete_req.software_ids:
|
34
|
+
software = await Software.get_or_none(id=software_id)
|
35
|
+
|
36
|
+
if software is None:
|
37
|
+
continue
|
38
|
+
|
39
|
+
rollout_count = await Rollout.filter(software=software).count()
|
40
|
+
if rollout_count > 0:
|
41
|
+
raise HTTPException(409, "Software is referenced by rollout")
|
42
|
+
|
43
|
+
if software.local:
|
44
|
+
path = software.path
|
45
|
+
if path.exists():
|
46
|
+
path.unlink()
|
47
|
+
|
48
|
+
await software.delete()
|
49
|
+
success = True
|
50
|
+
return StatusResponse(success=success)
|
51
|
+
|
52
|
+
|
53
|
+
@router.post(
|
54
|
+
"",
|
55
|
+
dependencies=[Security(validate_user_permissions, scopes=["software.write"])],
|
56
|
+
)
|
57
|
+
async def post_update(_: Request, file: UploadFile | None = File(None), url: str | None = Form(None)):
|
58
|
+
if url is not None:
|
59
|
+
# remote file
|
60
|
+
software = await Software.get_or_none(uri=url)
|
61
|
+
if software is not None:
|
62
|
+
rollout_count = await Rollout.filter(software=software).count()
|
63
|
+
if rollout_count == 0:
|
64
|
+
await software.delete()
|
65
|
+
else:
|
66
|
+
raise HTTPException(409, "Software with same URL already exists and is referenced by rollout")
|
67
|
+
|
68
|
+
software = await create_software_update(url, None)
|
69
|
+
else:
|
70
|
+
# local file
|
71
|
+
file_path = config.artifacts_dir.joinpath(file.filename)
|
72
|
+
|
73
|
+
async with aiofiles.tempfile.NamedTemporaryFile("w+b") as f:
|
74
|
+
await f.write(await file.read())
|
75
|
+
software = await create_software_update(file_path.absolute().as_uri(), Path(f.name))
|
76
|
+
|
77
|
+
return {"id": software.id}
|