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
@@ -0,0 +1,21 @@
|
|
1
|
+
import os
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
from argon2 import PasswordHasher
|
5
|
+
|
6
|
+
GOOSEBIT_ROOT_DIR = Path(__file__).resolve().parent.parent.parent
|
7
|
+
CURRENT_DIR = Path(os.getcwd())
|
8
|
+
|
9
|
+
PWD_CXT = PasswordHasher()
|
10
|
+
|
11
|
+
LOGGING_DEFAULT = {
|
12
|
+
"version": 1,
|
13
|
+
"formatters": {"simple": {"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"}},
|
14
|
+
"handlers": {"console": {"class": "logging.StreamHandler", "formatter": "simple", "level": "DEBUG"}},
|
15
|
+
"loggers": {
|
16
|
+
"tortoise": {"handlers": ["console"], "level": "WARNING", "propagate": True},
|
17
|
+
"aiosqlite": {"handlers": ["console"], "level": "WARNING", "propagate": True},
|
18
|
+
"multipart": {"handlers": ["console"], "level": "INFO", "propagate": True},
|
19
|
+
},
|
20
|
+
"root": {"level": "INFO", "handlers": ["console"]},
|
21
|
+
}
|
@@ -0,0 +1,86 @@
|
|
1
|
+
import os
|
2
|
+
import secrets
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import Annotated
|
5
|
+
|
6
|
+
from joserfc.rfc7518.oct_key import OctKey
|
7
|
+
from pydantic import BaseModel, BeforeValidator, Field
|
8
|
+
from pydantic_settings import (
|
9
|
+
BaseSettings,
|
10
|
+
PydanticBaseSettingsSource,
|
11
|
+
SettingsConfigDict,
|
12
|
+
YamlConfigSettingsSource,
|
13
|
+
)
|
14
|
+
|
15
|
+
from .const import CURRENT_DIR, GOOSEBIT_ROOT_DIR, LOGGING_DEFAULT, PWD_CXT
|
16
|
+
|
17
|
+
|
18
|
+
class User(BaseModel):
|
19
|
+
username: str
|
20
|
+
hashed_pwd: Annotated[str, BeforeValidator(PWD_CXT.hash)] = Field(validation_alias="password")
|
21
|
+
permissions: set[str]
|
22
|
+
|
23
|
+
def get_json_permissions(self):
|
24
|
+
return [str(p) for p in self.permissions]
|
25
|
+
|
26
|
+
|
27
|
+
class PrometheusSettings(BaseModel):
|
28
|
+
enable: bool = False
|
29
|
+
|
30
|
+
|
31
|
+
class MetricsSettings(BaseModel):
|
32
|
+
prometheus: PrometheusSettings = PrometheusSettings()
|
33
|
+
|
34
|
+
|
35
|
+
class GooseBitSettings(BaseSettings):
|
36
|
+
model_config = SettingsConfigDict(env_prefix="GOOSEBIT_")
|
37
|
+
|
38
|
+
port: int = 60053 # GOOSE
|
39
|
+
|
40
|
+
poll_time_default: str = "00:01:00"
|
41
|
+
poll_time_updating: str = "00:00:05"
|
42
|
+
poll_time_registration: str = "00:00:10"
|
43
|
+
|
44
|
+
secret_key: Annotated[OctKey, BeforeValidator(OctKey.import_key)] = secrets.token_hex(16)
|
45
|
+
|
46
|
+
users: list[User] = []
|
47
|
+
|
48
|
+
db_uri: str = f"sqlite:///{GOOSEBIT_ROOT_DIR.joinpath('db.sqlite3')}"
|
49
|
+
artifacts_dir: Path = GOOSEBIT_ROOT_DIR.joinpath("artifacts")
|
50
|
+
|
51
|
+
metrics: MetricsSettings = MetricsSettings()
|
52
|
+
|
53
|
+
logging: dict = LOGGING_DEFAULT
|
54
|
+
|
55
|
+
@classmethod
|
56
|
+
def settings_customise_sources(
|
57
|
+
cls,
|
58
|
+
settings_cls: type[BaseSettings],
|
59
|
+
init_settings: PydanticBaseSettingsSource,
|
60
|
+
env_settings: PydanticBaseSettingsSource,
|
61
|
+
dotenv_settings: PydanticBaseSettingsSource,
|
62
|
+
file_secret_settings: PydanticBaseSettingsSource,
|
63
|
+
) -> tuple[PydanticBaseSettingsSource, ...]:
|
64
|
+
settings_sources = [env_settings]
|
65
|
+
config_files = []
|
66
|
+
|
67
|
+
if (path := os.getenv("GOOSEBIT_SETTINGS")) is not None:
|
68
|
+
config_files.append(Path(path))
|
69
|
+
|
70
|
+
config_files.extend(
|
71
|
+
[
|
72
|
+
CURRENT_DIR.joinpath("goosebit.yaml"),
|
73
|
+
GOOSEBIT_ROOT_DIR.joinpath("goosebit.yaml"),
|
74
|
+
Path("/etc/goosebit.yaml"),
|
75
|
+
]
|
76
|
+
)
|
77
|
+
|
78
|
+
cls.config_file = None
|
79
|
+
for config_file in config_files:
|
80
|
+
if config_file.exists():
|
81
|
+
settings_sources.append(
|
82
|
+
YamlConfigSettingsSource(settings_cls, config_file),
|
83
|
+
)
|
84
|
+
cls.config_file = config_file
|
85
|
+
break
|
86
|
+
return tuple(settings_sources)
|
@@ -0,0 +1 @@
|
|
1
|
+
from .routes import router # noqa: F401
|
@@ -0,0 +1 @@
|
|
1
|
+
from .routes import router # noqa: F401
|
@@ -0,0 +1,12 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
|
5
|
+
|
6
|
+
class DevicesPatchRequest(BaseModel):
|
7
|
+
devices: list[str]
|
8
|
+
software: str | None = None
|
9
|
+
name: str | None = None
|
10
|
+
pinned: bool | None = None
|
11
|
+
feed: str | None = None
|
12
|
+
force_update: bool | None = None
|
@@ -0,0 +1,39 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
|
5
|
+
from fastapi.requests import Request
|
6
|
+
from pydantic import BaseModel, Field
|
7
|
+
|
8
|
+
from goosebit.schema.devices import DeviceSchema
|
9
|
+
|
10
|
+
|
11
|
+
class BFFDeviceResponse(BaseModel):
|
12
|
+
data: list[DeviceSchema]
|
13
|
+
draw: int
|
14
|
+
records_total: int = Field(serialization_alias="recordsTotal")
|
15
|
+
records_filtered: int = Field(serialization_alias="recordsFiltered")
|
16
|
+
|
17
|
+
@classmethod
|
18
|
+
async def convert(cls, request: Request, query, search_filter, total_records):
|
19
|
+
params = request.query_params
|
20
|
+
|
21
|
+
draw = int(params.get("draw", 1))
|
22
|
+
start = int(params.get("start", 0))
|
23
|
+
length = int(params.get("length", 10))
|
24
|
+
search_value = params.get("search[value]", None)
|
25
|
+
order_column_index = params.get("order[0][column]", None)
|
26
|
+
order_column = params.get(f"columns[{order_column_index}][data]", None)
|
27
|
+
order_dir = params.get("order[0][dir]", None)
|
28
|
+
|
29
|
+
if search_value:
|
30
|
+
query = query.filter(search_filter(search_value))
|
31
|
+
|
32
|
+
if order_column:
|
33
|
+
query = query.order_by(f"{'-' if order_dir == 'desc' else ''}{order_column}")
|
34
|
+
|
35
|
+
filtered_records = await query.count()
|
36
|
+
devices = await query.offset(start).limit(length).all()
|
37
|
+
data = await asyncio.gather(*[DeviceSchema.convert(d) for d in devices])
|
38
|
+
|
39
|
+
return cls(data=data, draw=draw, records_total=total_records, records_filtered=filtered_records)
|
@@ -0,0 +1,72 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from fastapi import APIRouter, Security
|
4
|
+
from fastapi.requests import Request
|
5
|
+
from tortoise.expressions import Q
|
6
|
+
|
7
|
+
from goosebit.api.responses import StatusResponse
|
8
|
+
from goosebit.api.v1.devices import routes
|
9
|
+
from goosebit.auth import validate_user_permissions
|
10
|
+
from goosebit.db.models import Device, Software, UpdateModeEnum, UpdateStateEnum
|
11
|
+
from goosebit.updater.manager import get_update_manager
|
12
|
+
|
13
|
+
from .requests import DevicesPatchRequest
|
14
|
+
from .responses import BFFDeviceResponse
|
15
|
+
|
16
|
+
router = APIRouter(prefix="/devices")
|
17
|
+
|
18
|
+
|
19
|
+
@router.get(
|
20
|
+
"",
|
21
|
+
dependencies=[Security(validate_user_permissions, scopes=["home.read"])],
|
22
|
+
)
|
23
|
+
async def devices_get(request: Request) -> BFFDeviceResponse:
|
24
|
+
def search_filter(search_value):
|
25
|
+
return (
|
26
|
+
Q(uuid__icontains=search_value)
|
27
|
+
| Q(name__icontains=search_value)
|
28
|
+
| Q(feed__icontains=search_value)
|
29
|
+
| Q(sw_version__icontains=search_value)
|
30
|
+
| Q(update_mode=int(UpdateModeEnum.from_str(search_value)))
|
31
|
+
| Q(last_state=int(UpdateStateEnum.from_str(search_value)))
|
32
|
+
)
|
33
|
+
|
34
|
+
query = Device.all().prefetch_related("assigned_software", "hardware")
|
35
|
+
total_records = await Device.all().count()
|
36
|
+
|
37
|
+
return await BFFDeviceResponse.convert(request, query, search_filter, total_records)
|
38
|
+
|
39
|
+
|
40
|
+
@router.patch(
|
41
|
+
"",
|
42
|
+
dependencies=[Security(validate_user_permissions, scopes=["device.write"])],
|
43
|
+
)
|
44
|
+
async def devices_patch(_: Request, config: DevicesPatchRequest) -> StatusResponse:
|
45
|
+
for uuid in config.devices:
|
46
|
+
updater = await get_update_manager(uuid)
|
47
|
+
if config.software is not None:
|
48
|
+
if config.software == "rollout":
|
49
|
+
await updater.update_update(UpdateModeEnum.ROLLOUT, None)
|
50
|
+
elif config.software == "latest":
|
51
|
+
await updater.update_update(UpdateModeEnum.LATEST, None)
|
52
|
+
else:
|
53
|
+
software = await Software.get_or_none(id=config.software)
|
54
|
+
await updater.update_update(UpdateModeEnum.ASSIGNED, software)
|
55
|
+
if config.pinned is not None:
|
56
|
+
await updater.update_update(UpdateModeEnum.PINNED, None)
|
57
|
+
if config.name is not None:
|
58
|
+
await updater.update_name(config.name)
|
59
|
+
if config.feed is not None:
|
60
|
+
await updater.update_feed(config.feed)
|
61
|
+
if config.force_update is not None:
|
62
|
+
await updater.update_force_update(config.force_update)
|
63
|
+
return StatusResponse(success=True)
|
64
|
+
|
65
|
+
|
66
|
+
router.add_api_route(
|
67
|
+
"",
|
68
|
+
routes.devices_delete,
|
69
|
+
methods=["DELETE"],
|
70
|
+
dependencies=[Security(validate_user_permissions, scopes=["device.delete"])],
|
71
|
+
name="bff_devices_delete",
|
72
|
+
)
|
@@ -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,37 @@
|
|
1
|
+
import asyncio
|
2
|
+
|
3
|
+
from fastapi.requests import Request
|
4
|
+
from pydantic import BaseModel, Field
|
5
|
+
|
6
|
+
from goosebit.schema.rollouts import RolloutSchema
|
7
|
+
|
8
|
+
|
9
|
+
class BFFRolloutsResponse(BaseModel):
|
10
|
+
data: list[RolloutSchema]
|
11
|
+
draw: int
|
12
|
+
records_total: int = Field(serialization_alias="recordsTotal")
|
13
|
+
records_filtered: int = Field(serialization_alias="recordsFiltered")
|
14
|
+
|
15
|
+
@classmethod
|
16
|
+
async def convert(cls, request: Request, query, search_filter, total_records):
|
17
|
+
params = request.query_params
|
18
|
+
|
19
|
+
draw = int(params.get("draw", 1))
|
20
|
+
start = int(params.get("start", 0))
|
21
|
+
length = int(params.get("length", 10))
|
22
|
+
search_value = params.get("search[value]", None)
|
23
|
+
order_column_index = params.get("order[0][column]", None)
|
24
|
+
order_column = params.get(f"columns[{order_column_index}][data]", None)
|
25
|
+
order_dir = params.get("order[0][dir]", None)
|
26
|
+
|
27
|
+
if search_value:
|
28
|
+
query = query.filter(search_filter(search_value))
|
29
|
+
|
30
|
+
if order_column:
|
31
|
+
query = query.order_by(f"{'-' if order_dir == 'desc' else ''}{order_column}")
|
32
|
+
|
33
|
+
filtered_records = await query.count()
|
34
|
+
rollouts = await query.offset(start).limit(length).all()
|
35
|
+
data = await asyncio.gather(*[RolloutSchema.convert(r) for r in rollouts])
|
36
|
+
|
37
|
+
return cls(data=data, draw=draw, records_total=total_records, records_filtered=filtered_records)
|
@@ -0,0 +1,52 @@
|
|
1
|
+
from fastapi import APIRouter, Security
|
2
|
+
from fastapi.requests import Request
|
3
|
+
from tortoise.expressions import Q
|
4
|
+
|
5
|
+
from goosebit.api.v1.rollouts import routes
|
6
|
+
from goosebit.auth import validate_user_permissions
|
7
|
+
from goosebit.db.models import Rollout
|
8
|
+
|
9
|
+
from .responses import BFFRolloutsResponse
|
10
|
+
|
11
|
+
router = APIRouter(prefix="/rollouts")
|
12
|
+
|
13
|
+
|
14
|
+
@router.get(
|
15
|
+
"",
|
16
|
+
dependencies=[Security(validate_user_permissions, scopes=["rollout.read"])],
|
17
|
+
)
|
18
|
+
async def rollouts_get(request: Request) -> BFFRolloutsResponse:
|
19
|
+
def search_filter(search_value):
|
20
|
+
return Q(name__icontains=search_value) | Q(feed__icontains=search_value)
|
21
|
+
|
22
|
+
query = Rollout.all().prefetch_related("software")
|
23
|
+
total_records = await Rollout.all().count()
|
24
|
+
|
25
|
+
return await BFFRolloutsResponse.convert(request, query, search_filter, total_records)
|
26
|
+
|
27
|
+
|
28
|
+
router.add_api_route(
|
29
|
+
"",
|
30
|
+
routes.rollouts_put,
|
31
|
+
methods=["POST"],
|
32
|
+
dependencies=[Security(validate_user_permissions, scopes=["rollout.write"])],
|
33
|
+
name="bff_rollouts_post",
|
34
|
+
)
|
35
|
+
|
36
|
+
|
37
|
+
router.add_api_route(
|
38
|
+
"",
|
39
|
+
routes.rollouts_patch,
|
40
|
+
methods=["PATCH"],
|
41
|
+
dependencies=[Security(validate_user_permissions, scopes=["rollout.write"])],
|
42
|
+
name="bff_rollouts_patch",
|
43
|
+
)
|
44
|
+
|
45
|
+
|
46
|
+
router.add_api_route(
|
47
|
+
"",
|
48
|
+
routes.rollouts_delete,
|
49
|
+
methods=["DELETE"],
|
50
|
+
dependencies=[Security(validate_user_permissions, scopes=["rollout.delete"])],
|
51
|
+
name="bff_rollouts_delete",
|
52
|
+
)
|
@@ -0,0 +1,11 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from fastapi import APIRouter
|
4
|
+
|
5
|
+
from . import devices, download, rollouts, software
|
6
|
+
|
7
|
+
router = APIRouter(prefix="/bff", tags=["bff"])
|
8
|
+
router.include_router(devices.router)
|
9
|
+
router.include_router(software.router)
|
10
|
+
router.include_router(rollouts.router)
|
11
|
+
router.include_router(download.router)
|
@@ -0,0 +1 @@
|
|
1
|
+
from .routes import router # noqa: F401
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import asyncio
|
2
|
+
|
3
|
+
from fastapi.requests import Request
|
4
|
+
from pydantic import BaseModel, Field
|
5
|
+
|
6
|
+
from goosebit.schema.software import SoftwareSchema
|
7
|
+
|
8
|
+
|
9
|
+
class BFFSoftwareResponse(BaseModel):
|
10
|
+
data: list[SoftwareSchema]
|
11
|
+
draw: int
|
12
|
+
records_total: int = Field(serialization_alias="recordsTotal")
|
13
|
+
records_filtered: int = Field(serialization_alias="recordsFiltered")
|
14
|
+
|
15
|
+
@classmethod
|
16
|
+
async def convert(cls, request: Request, query, search_filter, total_records):
|
17
|
+
params = request.query_params
|
18
|
+
|
19
|
+
draw = int(params.get("draw", 1))
|
20
|
+
start = int(params.get("start", 0))
|
21
|
+
length = int(params.get("length", 10))
|
22
|
+
search_value = params.get("search[value]", None)
|
23
|
+
order_column_index = params.get("order[0][column]", None)
|
24
|
+
order_column = params.get(f"columns[{order_column_index}][data]", None)
|
25
|
+
order_dir = params.get("order[0][dir]", None)
|
26
|
+
|
27
|
+
if search_value:
|
28
|
+
query = query.filter(search_filter(search_value))
|
29
|
+
|
30
|
+
if order_column:
|
31
|
+
query = query.order_by(f"{'-' if order_dir == 'desc' else ''}{order_column}")
|
32
|
+
|
33
|
+
filtered_records = await query.count()
|
34
|
+
devices = await query.offset(start).limit(length).all()
|
35
|
+
data = await asyncio.gather(*[SoftwareSchema.convert(d) for d in devices])
|
36
|
+
|
37
|
+
return cls(data=data, draw=draw, records_total=total_records, records_filtered=filtered_records)
|
@@ -0,0 +1,83 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import aiofiles
|
4
|
+
from fastapi import APIRouter, Form, HTTPException, Security, UploadFile
|
5
|
+
from fastapi.requests import Request
|
6
|
+
from tortoise.expressions import Q
|
7
|
+
|
8
|
+
from goosebit.api.v1.software import routes
|
9
|
+
from goosebit.auth import validate_user_permissions
|
10
|
+
from goosebit.db.models import Rollout, Software
|
11
|
+
from goosebit.settings import config
|
12
|
+
from goosebit.updates import create_software_update
|
13
|
+
|
14
|
+
from .responses import BFFSoftwareResponse
|
15
|
+
|
16
|
+
router = APIRouter(prefix="/software")
|
17
|
+
|
18
|
+
|
19
|
+
@router.get(
|
20
|
+
"",
|
21
|
+
dependencies=[Security(validate_user_permissions, scopes=["software.read"])],
|
22
|
+
)
|
23
|
+
async def software_get(request: Request) -> BFFSoftwareResponse:
|
24
|
+
def search_filter(search_value):
|
25
|
+
return Q(uri__icontains=search_value) | Q(version__icontains=search_value)
|
26
|
+
|
27
|
+
query = Software.all().prefetch_related("compatibility")
|
28
|
+
total_records = await Software.all().count()
|
29
|
+
|
30
|
+
return await BFFSoftwareResponse.convert(request, query, search_filter, total_records)
|
31
|
+
|
32
|
+
|
33
|
+
router.add_api_route(
|
34
|
+
"",
|
35
|
+
routes.software_delete,
|
36
|
+
methods=["DELETE"],
|
37
|
+
dependencies=[Security(validate_user_permissions, scopes=["software.delete"])],
|
38
|
+
name="bff_software_delete",
|
39
|
+
)
|
40
|
+
|
41
|
+
|
42
|
+
@router.post(
|
43
|
+
"",
|
44
|
+
dependencies=[Security(validate_user_permissions, scopes=["software.write"])],
|
45
|
+
)
|
46
|
+
async def post_update(
|
47
|
+
request: Request,
|
48
|
+
url: str = Form(default=None),
|
49
|
+
chunk: UploadFile = Form(default=None),
|
50
|
+
init: bool = Form(default=None),
|
51
|
+
done: bool = Form(default=None),
|
52
|
+
filename: str = Form(default=None),
|
53
|
+
):
|
54
|
+
if url is not None:
|
55
|
+
# remote file
|
56
|
+
software = await Software.get_or_none(uri=url)
|
57
|
+
if software is not None:
|
58
|
+
rollout_count = await Rollout.filter(software=software).count()
|
59
|
+
if rollout_count == 0:
|
60
|
+
await software.delete()
|
61
|
+
else:
|
62
|
+
raise HTTPException(409, "Software with same URL already exists and is referenced by rollout")
|
63
|
+
|
64
|
+
await create_software_update(url, None)
|
65
|
+
else:
|
66
|
+
# local file
|
67
|
+
file = config.artifacts_dir.joinpath(filename)
|
68
|
+
config.artifacts_dir.mkdir(parents=True, exist_ok=True)
|
69
|
+
|
70
|
+
temp_file = file.with_suffix(".tmp")
|
71
|
+
if init:
|
72
|
+
temp_file.unlink(missing_ok=True)
|
73
|
+
|
74
|
+
contents = await chunk.read()
|
75
|
+
|
76
|
+
async with aiofiles.open(temp_file, mode="ab") as f:
|
77
|
+
await f.write(contents)
|
78
|
+
|
79
|
+
if done:
|
80
|
+
try:
|
81
|
+
await create_software_update(file.absolute().as_uri(), temp_file)
|
82
|
+
finally:
|
83
|
+
temp_file.unlink(missing_ok=True)
|
goosebit/ui/nav.py
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
class Navigation:
|
2
|
+
def __init__(self):
|
3
|
+
self.items = []
|
4
|
+
|
5
|
+
def route(self, text: str, permissions: str = None):
|
6
|
+
def decorator(func):
|
7
|
+
self.items.append({"function": func.__name__, "text": text, "permissions": permissions})
|
8
|
+
return func
|
9
|
+
|
10
|
+
return decorator
|
11
|
+
|
12
|
+
def get(self):
|
13
|
+
return self.items
|
14
|
+
|
15
|
+
|
16
|
+
nav = Navigation()
|
goosebit/ui/routes.py
CHANGED
@@ -1,101 +1,64 @@
|
|
1
|
-
import
|
2
|
-
from fastapi import APIRouter, Depends, Form, Security, UploadFile
|
1
|
+
from fastapi import APIRouter, Depends, Security
|
3
2
|
from fastapi.requests import Request
|
4
3
|
from fastapi.responses import RedirectResponse
|
5
4
|
from fastapi.security import OAuth2PasswordBearer
|
6
5
|
|
7
|
-
from goosebit.auth import
|
8
|
-
from goosebit.
|
9
|
-
|
10
|
-
from
|
11
|
-
from
|
12
|
-
from goosebit.updates import create_firmware_update
|
6
|
+
from goosebit.auth import redirect_if_unauthenticated, validate_user_permissions
|
7
|
+
from goosebit.ui.nav import nav
|
8
|
+
|
9
|
+
from . import bff
|
10
|
+
from .templates import templates
|
13
11
|
|
14
12
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
|
15
13
|
|
16
|
-
router = APIRouter(prefix="/ui", dependencies=[Depends(
|
14
|
+
router = APIRouter(prefix="/ui", dependencies=[Depends(redirect_if_unauthenticated)], include_in_schema=False)
|
15
|
+
router.include_router(bff.router)
|
17
16
|
|
18
17
|
|
19
|
-
@router.get("
|
18
|
+
@router.get("")
|
20
19
|
async def ui_root(request: Request):
|
21
20
|
return RedirectResponse(request.url_for("home_ui"))
|
22
21
|
|
23
22
|
|
24
|
-
@router.get(
|
25
|
-
"/firmware",
|
26
|
-
dependencies=[Security(validate_user_permissions, scopes=[Permissions.FIRMWARE.READ])],
|
27
|
-
)
|
28
|
-
async def firmware_ui(request: Request):
|
29
|
-
return templates.TemplateResponse(request, "firmware.html", context={"title": "Firmware"})
|
30
|
-
|
31
|
-
|
32
|
-
@router.post(
|
33
|
-
"/upload/local",
|
34
|
-
dependencies=[Security(validate_user_permissions, scopes=[Permissions.FIRMWARE.WRITE])],
|
35
|
-
)
|
36
|
-
async def upload_update_local(
|
37
|
-
request: Request,
|
38
|
-
chunk: UploadFile = Form(...),
|
39
|
-
init: bool = Form(...),
|
40
|
-
done: bool = Form(...),
|
41
|
-
filename: str = Form(...),
|
42
|
-
):
|
43
|
-
file = UPDATES_DIR.joinpath(filename)
|
44
|
-
firmware = await Firmware.get_or_none(uri=file.absolute().as_uri())
|
45
|
-
if firmware is not None:
|
46
|
-
await firmware.delete()
|
47
|
-
|
48
|
-
tmpfile = file.with_suffix(".tmp")
|
49
|
-
contents = await chunk.read()
|
50
|
-
if init:
|
51
|
-
file.unlink(missing_ok=True)
|
52
|
-
|
53
|
-
async with aiofiles.open(tmpfile, mode="ab") as f:
|
54
|
-
await f.write(contents)
|
55
|
-
if done:
|
56
|
-
tmpfile.replace(file)
|
57
|
-
await create_firmware_update(file.absolute().as_uri())
|
58
|
-
|
59
|
-
|
60
|
-
@router.post(
|
61
|
-
"/upload/remote",
|
62
|
-
dependencies=[Security(validate_user_permissions, scopes=[Permissions.FIRMWARE.WRITE])],
|
63
|
-
)
|
64
|
-
async def upload_update_remote(request: Request, url: str = Form(...)):
|
65
|
-
firmware = await Firmware.get_or_none(uri=url)
|
66
|
-
if firmware is not None:
|
67
|
-
await firmware.delete()
|
68
|
-
|
69
|
-
await create_firmware_update(url)
|
70
|
-
|
71
|
-
|
72
23
|
@router.get(
|
73
24
|
"/home",
|
74
|
-
dependencies=[Security(validate_user_permissions, scopes=[
|
25
|
+
dependencies=[Security(validate_user_permissions, scopes=["home.read"])],
|
75
26
|
)
|
27
|
+
@nav.route("Home", permissions=["home.read"])
|
76
28
|
async def home_ui(request: Request):
|
77
|
-
return templates.TemplateResponse(request, "index.html", context={"title": "Home"})
|
29
|
+
return templates.TemplateResponse(request, "index.html.jinja", context={"title": "Home"})
|
78
30
|
|
79
31
|
|
80
32
|
@router.get(
|
81
33
|
"/devices",
|
82
|
-
dependencies=[Security(validate_user_permissions, scopes=[
|
34
|
+
dependencies=[Security(validate_user_permissions, scopes=["device.read"])],
|
83
35
|
)
|
36
|
+
@nav.route("Devices", permissions=["device.read"])
|
84
37
|
async def devices_ui(request: Request):
|
85
|
-
return templates.TemplateResponse(request, "devices.html", context={"title": "Devices"})
|
38
|
+
return templates.TemplateResponse(request, "devices.html.jinja", context={"title": "Devices"})
|
39
|
+
|
40
|
+
|
41
|
+
@router.get(
|
42
|
+
"/software",
|
43
|
+
dependencies=[Security(validate_user_permissions, scopes=["software.read"])],
|
44
|
+
)
|
45
|
+
@nav.route("Software", permissions=["software.read"])
|
46
|
+
async def software_ui(request: Request):
|
47
|
+
return templates.TemplateResponse(request, "software.html.jinja", context={"title": "Software"})
|
86
48
|
|
87
49
|
|
88
50
|
@router.get(
|
89
51
|
"/rollouts",
|
90
|
-
dependencies=[Security(validate_user_permissions, scopes=[
|
52
|
+
dependencies=[Security(validate_user_permissions, scopes=["rollout.read"])],
|
91
53
|
)
|
54
|
+
@nav.route("Rollouts", permissions=["rollout.read"])
|
92
55
|
async def rollouts_ui(request: Request):
|
93
|
-
return templates.TemplateResponse(request, "rollouts.html", context={"title": "Rollouts"})
|
56
|
+
return templates.TemplateResponse(request, "rollouts.html.jinja", context={"title": "Rollouts"})
|
94
57
|
|
95
58
|
|
96
59
|
@router.get(
|
97
60
|
"/logs/{dev_id}",
|
98
|
-
dependencies=[Security(validate_user_permissions, scopes=[
|
61
|
+
dependencies=[Security(validate_user_permissions, scopes=["device.read"])],
|
99
62
|
)
|
100
63
|
async def logs_ui(request: Request, dev_id: str):
|
101
|
-
return templates.TemplateResponse(request, "logs.html", context={"title": "Log", "device": dev_id})
|
64
|
+
return templates.TemplateResponse(request, "logs.html.jinja", context={"title": "Log", "device": dev_id})
|
goosebit/ui/static/favicon.ico
CHANGED
Binary file
|