goosebit 0.1.0__py3-none-any.whl → 0.1.2__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 +8 -5
- goosebit/api/__init__.py +1 -1
- goosebit/api/devices.py +60 -36
- goosebit/api/download.py +28 -14
- goosebit/api/firmware.py +37 -44
- goosebit/api/helper.py +30 -0
- goosebit/api/rollouts.py +87 -0
- goosebit/api/routes.py +15 -7
- goosebit/auth/__init__.py +37 -21
- goosebit/db.py +5 -0
- goosebit/models.py +125 -6
- goosebit/permissions.py +33 -13
- goosebit/realtime/__init__.py +1 -1
- goosebit/realtime/logs.py +4 -6
- goosebit/settings.py +38 -29
- goosebit/telemetry/__init__.py +28 -0
- goosebit/telemetry/prometheus.py +10 -0
- goosebit/ui/__init__.py +1 -1
- goosebit/ui/routes.py +36 -39
- goosebit/ui/static/js/devices.js +191 -239
- goosebit/ui/static/js/firmware.js +234 -88
- goosebit/ui/static/js/index.js +83 -84
- goosebit/ui/static/js/logs.js +17 -10
- goosebit/ui/static/js/rollouts.js +198 -0
- goosebit/ui/static/js/util.js +66 -0
- goosebit/ui/templates/devices.html +75 -42
- goosebit/ui/templates/firmware.html +150 -34
- goosebit/ui/templates/index.html +9 -23
- goosebit/ui/templates/login.html +58 -27
- goosebit/ui/templates/logs.html +18 -3
- goosebit/ui/templates/nav.html +78 -25
- goosebit/ui/templates/rollouts.html +76 -0
- goosebit/updater/__init__.py +1 -1
- goosebit/updater/controller/__init__.py +1 -1
- goosebit/updater/controller/v1/__init__.py +1 -1
- goosebit/updater/controller/v1/routes.py +112 -24
- goosebit/updater/manager.py +237 -94
- goosebit/updater/routes.py +7 -8
- goosebit/updates/__init__.py +70 -0
- goosebit/updates/swdesc.py +83 -0
- goosebit-0.1.2.dist-info/METADATA +123 -0
- goosebit-0.1.2.dist-info/RECORD +51 -0
- goosebit/updater/download/__init__.py +0 -1
- goosebit/updater/download/routes.py +0 -6
- goosebit/updater/download/v1/__init__.py +0 -1
- goosebit/updater/download/v1/routes.py +0 -26
- goosebit/updater/misc.py +0 -69
- goosebit/updater/updates.py +0 -93
- goosebit-0.1.0.dist-info/METADATA +0 -37
- goosebit-0.1.0.dist-info/RECORD +0 -48
- {goosebit-0.1.0.dist-info → goosebit-0.1.2.dist-info}/LICENSE +0 -0
- {goosebit-0.1.0.dist-info → goosebit-0.1.2.dist-info}/WHEEL +0 -0
goosebit/models.py
CHANGED
@@ -1,21 +1,140 @@
|
|
1
|
+
from enum import IntEnum
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import Self
|
4
|
+
from urllib.parse import unquote, urlparse
|
5
|
+
from urllib.request import url2pathname
|
6
|
+
|
7
|
+
import semver
|
1
8
|
from tortoise import Model, fields
|
2
9
|
|
10
|
+
from goosebit.telemetry import devices_count
|
11
|
+
|
12
|
+
|
13
|
+
class UpdateModeEnum(IntEnum):
|
14
|
+
NONE = 0
|
15
|
+
LATEST = 1
|
16
|
+
PINNED = 2
|
17
|
+
ROLLOUT = 3
|
18
|
+
ASSIGNED = 4
|
19
|
+
|
20
|
+
def __str__(self):
|
21
|
+
return self.name.capitalize()
|
22
|
+
|
23
|
+
@classmethod
|
24
|
+
def from_str(cls, name):
|
25
|
+
try:
|
26
|
+
return cls[name.upper()]
|
27
|
+
except KeyError:
|
28
|
+
return cls.NONE
|
29
|
+
|
30
|
+
|
31
|
+
class UpdateStateEnum(IntEnum):
|
32
|
+
NONE = 0
|
33
|
+
UNKNOWN = 1
|
34
|
+
REGISTERED = 2
|
35
|
+
RUNNING = 3
|
36
|
+
ERROR = 4
|
37
|
+
FINISHED = 5
|
38
|
+
|
39
|
+
def __str__(self):
|
40
|
+
return self.name.capitalize()
|
41
|
+
|
42
|
+
@classmethod
|
43
|
+
def from_str(cls, name):
|
44
|
+
try:
|
45
|
+
return cls[name.upper()]
|
46
|
+
except KeyError:
|
47
|
+
return cls.NONE
|
48
|
+
|
3
49
|
|
4
50
|
class Tag(Model):
|
5
|
-
id = fields.IntField(
|
51
|
+
id = fields.IntField(primary_key=True)
|
6
52
|
name = fields.CharField(max_length=255)
|
7
53
|
|
8
54
|
|
9
55
|
class Device(Model):
|
10
|
-
uuid = fields.CharField(max_length=255,
|
56
|
+
uuid = fields.CharField(max_length=255, primary_key=True)
|
11
57
|
name = fields.CharField(max_length=255, null=True)
|
12
|
-
|
58
|
+
assigned_firmware = fields.ForeignKeyField("models.Firmware", related_name="assigned_devices", null=True)
|
59
|
+
force_update = fields.BooleanField(default=False)
|
13
60
|
fw_version = fields.CharField(max_length=255, null=True)
|
14
|
-
|
61
|
+
hardware = fields.ForeignKeyField("models.Hardware", related_name="devices")
|
62
|
+
feed = fields.CharField(max_length=255, default="default")
|
63
|
+
flavor = fields.CharField(max_length=255, default="default")
|
64
|
+
update_mode = fields.IntEnumField(UpdateModeEnum, default=UpdateModeEnum.ROLLOUT)
|
65
|
+
last_state = fields.IntEnumField(UpdateStateEnum, default=UpdateStateEnum.UNKNOWN)
|
66
|
+
progress = fields.IntField(null=True)
|
67
|
+
log_complete = fields.BooleanField(default=False)
|
15
68
|
last_log = fields.TextField(null=True)
|
16
69
|
last_seen = fields.BigIntField(null=True)
|
17
70
|
last_ip = fields.CharField(max_length=15, null=True)
|
18
71
|
last_ipv6 = fields.CharField(max_length=40, null=True)
|
19
|
-
tags = fields.ManyToManyField(
|
20
|
-
|
72
|
+
tags = fields.ManyToManyField("models.Tag", related_name="devices", through="device_tags")
|
73
|
+
|
74
|
+
async def save(self, *args, **kwargs):
|
75
|
+
is_new = self._saved_in_db is False
|
76
|
+
await super().save(*args, **kwargs)
|
77
|
+
if is_new:
|
78
|
+
await self.notify_created()
|
79
|
+
|
80
|
+
async def delete(self, *args, **kwargs):
|
81
|
+
await super().delete(*args, **kwargs)
|
82
|
+
await self.notify_deleted()
|
83
|
+
|
84
|
+
@staticmethod
|
85
|
+
async def notify_created():
|
86
|
+
devices_count.set(await Device.all().count())
|
87
|
+
|
88
|
+
@staticmethod
|
89
|
+
async def notify_deleted():
|
90
|
+
devices_count.set(await Device.all().count())
|
91
|
+
|
92
|
+
|
93
|
+
class Rollout(Model):
|
94
|
+
id = fields.IntField(primary_key=True)
|
95
|
+
created_at = fields.DatetimeField(auto_now_add=True)
|
96
|
+
name = fields.CharField(max_length=255, null=True)
|
97
|
+
feed = fields.CharField(max_length=255, default="default")
|
98
|
+
flavor = fields.CharField(max_length=255, default="default")
|
99
|
+
firmware = fields.ForeignKeyField("models.Firmware", related_name="rollouts")
|
100
|
+
paused = fields.BooleanField(default=False)
|
101
|
+
success_count = fields.IntField(default=0)
|
102
|
+
failure_count = fields.IntField(default=0)
|
103
|
+
|
104
|
+
|
105
|
+
class Hardware(Model):
|
106
|
+
id = fields.IntField(primary_key=True)
|
107
|
+
model = fields.CharField(max_length=255)
|
108
|
+
revision = fields.CharField(max_length=255)
|
109
|
+
|
110
|
+
|
111
|
+
class Firmware(Model):
|
112
|
+
id = fields.IntField(primary_key=True)
|
113
|
+
uri = fields.CharField(max_length=255)
|
114
|
+
size = fields.BigIntField()
|
115
|
+
hash = fields.CharField(max_length=255)
|
116
|
+
version = fields.CharField(max_length=255)
|
117
|
+
compatibility = fields.ManyToManyField(
|
118
|
+
"models.Hardware",
|
119
|
+
related_name="firmwares",
|
120
|
+
through="firmware_compatibility",
|
21
121
|
)
|
122
|
+
|
123
|
+
@classmethod
|
124
|
+
async def latest(cls, device: Device) -> Self | None:
|
125
|
+
updates = await cls.filter(compatibility__devices__uuid=device.uuid)
|
126
|
+
if len(updates) == 0:
|
127
|
+
return None
|
128
|
+
return sorted(
|
129
|
+
updates,
|
130
|
+
key=lambda x: semver.Version.parse(x.version),
|
131
|
+
reverse=True,
|
132
|
+
)[0]
|
133
|
+
|
134
|
+
@property
|
135
|
+
def path(self):
|
136
|
+
return Path(url2pathname(unquote(urlparse(self.uri).path)))
|
137
|
+
|
138
|
+
@property
|
139
|
+
def local(self):
|
140
|
+
return urlparse(self.uri).scheme == "file"
|
goosebit/permissions.py
CHANGED
@@ -1,10 +1,16 @@
|
|
1
1
|
from enum import Enum
|
2
|
+
from typing import TypeVar, cast
|
3
|
+
|
4
|
+
T = TypeVar("T", bound="PermissionsBase")
|
2
5
|
|
3
6
|
|
4
7
|
class PermissionsBase(str, Enum):
|
5
8
|
@classmethod
|
6
|
-
def full(cls) ->
|
7
|
-
|
9
|
+
def full(cls) -> set[T]:
|
10
|
+
all_items = set[T]()
|
11
|
+
for permission in cls:
|
12
|
+
all_items.add(cast(T, permission))
|
13
|
+
return all_items
|
8
14
|
|
9
15
|
def __str__(self):
|
10
16
|
return self.value
|
@@ -17,15 +23,15 @@ class FirmwarePermissions(PermissionsBase):
|
|
17
23
|
|
18
24
|
|
19
25
|
class DevicePermissions(PermissionsBase):
|
20
|
-
READ = "
|
21
|
-
WRITE = "
|
22
|
-
DELETE = "
|
26
|
+
READ = "device.read"
|
27
|
+
WRITE = "device.write"
|
28
|
+
DELETE = "device.delete"
|
23
29
|
|
24
30
|
|
25
|
-
class
|
26
|
-
READ = "
|
27
|
-
WRITE = "
|
28
|
-
DELETE = "
|
31
|
+
class RolloutPermissions(PermissionsBase):
|
32
|
+
READ = "rollout.read"
|
33
|
+
WRITE = "rollout.write"
|
34
|
+
DELETE = "rollout.delete"
|
29
35
|
|
30
36
|
|
31
37
|
class HomePermissions(PermissionsBase):
|
@@ -36,14 +42,28 @@ class Permissions:
|
|
36
42
|
HOME = HomePermissions
|
37
43
|
FIRMWARE = FirmwarePermissions
|
38
44
|
DEVICE = DevicePermissions
|
39
|
-
|
45
|
+
ROLLOUT = RolloutPermissions
|
40
46
|
|
41
47
|
@classmethod
|
42
|
-
def full(cls):
|
48
|
+
def full(cls) -> set[T]:
|
43
49
|
all_items = set()
|
44
|
-
for item in [cls.HOME, cls.FIRMWARE, cls.DEVICE, cls.
|
50
|
+
for item in [cls.HOME, cls.FIRMWARE, cls.DEVICE, cls.ROLLOUT]:
|
45
51
|
all_items.update(item.full())
|
46
|
-
return
|
52
|
+
return all_items
|
53
|
+
|
54
|
+
@classmethod
|
55
|
+
def from_str(cls, permission: str) -> set[T]:
|
56
|
+
if permission == "*":
|
57
|
+
return cls.full()
|
58
|
+
area, action = permission.upper().split(".")
|
59
|
+
if area == "FIRMWARE":
|
60
|
+
return {FirmwarePermissions[action]}
|
61
|
+
if area == "DEVICE":
|
62
|
+
return {DevicePermissions[action]}
|
63
|
+
if area == "ROLLOUT":
|
64
|
+
return {RolloutPermissions[action]}
|
65
|
+
if area == "HOME":
|
66
|
+
return {HomePermissions[action]}
|
47
67
|
|
48
68
|
|
49
69
|
ADMIN = Permissions.full()
|
goosebit/realtime/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
from .routes import router
|
1
|
+
from .routes import router # noqa: F401
|
goosebit/realtime/logs.py
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
1
|
from fastapi import APIRouter, Security
|
4
2
|
from fastapi.websockets import WebSocket, WebSocketDisconnect
|
5
3
|
from pydantic import BaseModel
|
@@ -14,22 +12,22 @@ router = APIRouter(prefix="/logs")
|
|
14
12
|
|
15
13
|
class RealtimeLogModel(BaseModel):
|
16
14
|
log: str | None
|
15
|
+
progress: int | None
|
17
16
|
clear: bool = False
|
18
17
|
|
19
18
|
|
20
19
|
@router.websocket(
|
21
20
|
"/{dev_id}",
|
22
|
-
dependencies=[
|
23
|
-
Security(validate_ws_user_permissions, scopes=[Permissions.HOME.READ])
|
24
|
-
],
|
21
|
+
dependencies=[Security(validate_ws_user_permissions, scopes=[Permissions.HOME.READ])],
|
25
22
|
)
|
26
23
|
async def device_logs(websocket: WebSocket, dev_id: str):
|
27
24
|
await websocket.accept()
|
28
25
|
|
29
26
|
manager = await get_update_manager(dev_id)
|
27
|
+
device = await manager.get_device()
|
30
28
|
|
31
29
|
async def callback(log_update):
|
32
|
-
data = RealtimeLogModel(log=log_update)
|
30
|
+
data = RealtimeLogModel(log=log_update, progress=device.progress)
|
33
31
|
if log_update is None:
|
34
32
|
data.clear = True
|
35
33
|
data.log = ""
|
goosebit/settings.py
CHANGED
@@ -1,55 +1,64 @@
|
|
1
|
+
import secrets
|
1
2
|
from dataclasses import dataclass
|
2
3
|
from pathlib import Path
|
3
4
|
|
4
|
-
|
5
|
+
import yaml
|
6
|
+
from argon2 import PasswordHasher
|
7
|
+
from joserfc.rfc7518.oct_key import OctKey
|
5
8
|
|
6
|
-
from goosebit import
|
9
|
+
from goosebit.permissions import Permissions
|
7
10
|
|
8
|
-
|
9
|
-
POLL_MINUTES = 1
|
10
|
-
POLL_HOURS = 0
|
11
|
-
POLL_TIME = ":".join(
|
12
|
-
[
|
13
|
-
str(POLL_HOURS).rjust(2, "0"),
|
14
|
-
str(POLL_MINUTES).rjust(2, "0"),
|
15
|
-
str(POLL_SECONDS).rjust(2, "0"),
|
16
|
-
]
|
17
|
-
)
|
18
|
-
|
19
|
-
BASE_DIR = Path(__file__).resolve().parent
|
11
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
20
12
|
TOKEN_SWU_DIR = BASE_DIR.joinpath("swugen")
|
21
13
|
SWUPDATE_FILES_DIR = BASE_DIR.joinpath("swupdate")
|
22
14
|
UPDATES_DIR = BASE_DIR.joinpath("updates")
|
23
|
-
DB_LOC = BASE_DIR.joinpath("db.sqlite3")
|
24
15
|
DB_MIGRATIONS_LOC = BASE_DIR.joinpath("migrations")
|
25
|
-
DB_URI = f"sqlite:///{DB_LOC}"
|
26
16
|
|
27
|
-
SECRET =
|
28
|
-
PWD_CXT =
|
17
|
+
SECRET = OctKey.import_key(secrets.token_hex(16))
|
18
|
+
PWD_CXT = PasswordHasher()
|
19
|
+
|
20
|
+
with open(BASE_DIR.joinpath("settings.yaml"), "r") as f:
|
21
|
+
config = yaml.safe_load(f.read())
|
22
|
+
|
23
|
+
LOGGING = config.get("logging", {})
|
24
|
+
|
25
|
+
TENANT = config.get("tenant", "DEFAULT")
|
29
26
|
|
30
|
-
|
27
|
+
POLL_TIME = config.get("poll_time_default", "00:01:00")
|
28
|
+
POLL_TIME_UPDATING = config.get("poll_time_updating", "00:00:05")
|
29
|
+
POLL_TIME_REGISTRATION = config.get("poll_time_registration", "00:00:10")
|
30
|
+
|
31
|
+
DB_LOC = BASE_DIR.joinpath(config.get("db_location", "db.sqlite3"))
|
32
|
+
DB_URI = f"sqlite:///{DB_LOC}"
|
31
33
|
|
32
34
|
|
33
35
|
@dataclass
|
34
36
|
class User:
|
35
37
|
username: str
|
36
38
|
hashed_pwd: str
|
37
|
-
permissions:
|
39
|
+
permissions: set
|
38
40
|
|
39
41
|
def get_json_permissions(self):
|
40
42
|
return [str(p) for p in self.permissions]
|
41
43
|
|
42
44
|
|
43
|
-
|
44
|
-
|
45
|
+
users: dict[str, User] = {}
|
46
|
+
|
45
47
|
|
48
|
+
def add_user(u: User):
|
49
|
+
users[u.username] = u
|
46
50
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
51
|
+
|
52
|
+
for user in config.get("users", []):
|
53
|
+
permissions = set()
|
54
|
+
for p in user["permissions"]:
|
55
|
+
permissions.update(Permissions.from_str(p))
|
56
|
+
add_user(
|
57
|
+
User(
|
58
|
+
username=user["email"],
|
59
|
+
hashed_pwd=PWD_CXT.hash(user["password"]),
|
60
|
+
permissions=permissions,
|
61
|
+
)
|
53
62
|
)
|
54
|
-
|
63
|
+
|
55
64
|
USERS = users
|
@@ -0,0 +1,28 @@
|
|
1
|
+
from opentelemetry import metrics
|
2
|
+
from opentelemetry.sdk.metrics import MeterProvider
|
3
|
+
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
|
4
|
+
|
5
|
+
from goosebit import settings
|
6
|
+
|
7
|
+
from . import prometheus
|
8
|
+
|
9
|
+
resource = Resource(attributes={SERVICE_NAME: "goosebit"})
|
10
|
+
|
11
|
+
provider = MeterProvider(resource=resource, metric_readers=[prometheus.reader])
|
12
|
+
metrics.set_meter_provider(provider)
|
13
|
+
|
14
|
+
meter = metrics.get_meter("goosebit.meter")
|
15
|
+
|
16
|
+
devices_count = meter.create_gauge(
|
17
|
+
"devices.count",
|
18
|
+
description="The number of connected devices",
|
19
|
+
)
|
20
|
+
|
21
|
+
users_count = meter.create_gauge(
|
22
|
+
"users.count",
|
23
|
+
description="The number of registered users",
|
24
|
+
)
|
25
|
+
|
26
|
+
|
27
|
+
async def init():
|
28
|
+
users_count.set(len(settings.USERS))
|
@@ -0,0 +1,10 @@
|
|
1
|
+
from opentelemetry.exporter.prometheus import PrometheusMetricReader
|
2
|
+
from prometheus_client import start_http_server
|
3
|
+
|
4
|
+
from goosebit import settings
|
5
|
+
|
6
|
+
PROMETHEUS_PORT = settings.config.get("metrics", {}).get("prometheus", {}).get("port", 9090)
|
7
|
+
|
8
|
+
# separate file to enable it as a feature later.
|
9
|
+
reader = PrometheusMetricReader()
|
10
|
+
start_http_server(port=PROMETHEUS_PORT, addr="0.0.0.0")
|
goosebit/ui/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
from .routes import router
|
1
|
+
from .routes import router # noqa: F401
|
goosebit/ui/routes.py
CHANGED
@@ -5,15 +5,15 @@ from fastapi.responses import RedirectResponse
|
|
5
5
|
from fastapi.security import OAuth2PasswordBearer
|
6
6
|
|
7
7
|
from goosebit.auth import authenticate_session, validate_user_permissions
|
8
|
+
from goosebit.models import Firmware
|
8
9
|
from goosebit.permissions import Permissions
|
9
10
|
from goosebit.settings import UPDATES_DIR
|
10
11
|
from goosebit.ui.templates import templates
|
12
|
+
from goosebit.updates import create_firmware_update
|
11
13
|
|
12
14
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
|
13
15
|
|
14
|
-
router = APIRouter(
|
15
|
-
prefix="/ui", dependencies=[Depends(authenticate_session)], include_in_schema=False
|
16
|
-
)
|
16
|
+
router = APIRouter(prefix="/ui", dependencies=[Depends(authenticate_session)], include_in_schema=False)
|
17
17
|
|
18
18
|
|
19
19
|
@router.get("/")
|
@@ -23,23 +23,17 @@ async def ui_root(request: Request):
|
|
23
23
|
|
24
24
|
@router.get(
|
25
25
|
"/firmware",
|
26
|
-
dependencies=[
|
27
|
-
Security(validate_user_permissions, scopes=[Permissions.FIRMWARE.READ])
|
28
|
-
],
|
26
|
+
dependencies=[Security(validate_user_permissions, scopes=[Permissions.FIRMWARE.READ])],
|
29
27
|
)
|
30
28
|
async def firmware_ui(request: Request):
|
31
|
-
return templates.TemplateResponse(
|
32
|
-
"firmware.html", context={"request": request, "title": "Firmware"}
|
33
|
-
)
|
29
|
+
return templates.TemplateResponse(request, "firmware.html", context={"title": "Firmware"})
|
34
30
|
|
35
31
|
|
36
32
|
@router.post(
|
37
|
-
"/upload",
|
38
|
-
dependencies=[
|
39
|
-
Security(validate_user_permissions, scopes=[Permissions.FIRMWARE.WRITE])
|
40
|
-
],
|
33
|
+
"/upload/local",
|
34
|
+
dependencies=[Security(validate_user_permissions, scopes=[Permissions.FIRMWARE.WRITE])],
|
41
35
|
)
|
42
|
-
async def
|
36
|
+
async def upload_update_local(
|
43
37
|
request: Request,
|
44
38
|
chunk: UploadFile = Form(...),
|
45
39
|
init: bool = Form(...),
|
@@ -47,6 +41,10 @@ async def upload_update(
|
|
47
41
|
filename: str = Form(...),
|
48
42
|
):
|
49
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
|
+
|
50
48
|
tmpfile = file.with_suffix(".tmp")
|
51
49
|
contents = await chunk.read()
|
52
50
|
if init:
|
@@ -56,6 +54,19 @@ async def upload_update(
|
|
56
54
|
await f.write(contents)
|
57
55
|
if done:
|
58
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)
|
59
70
|
|
60
71
|
|
61
72
|
@router.get(
|
@@ -63,42 +74,28 @@ async def upload_update(
|
|
63
74
|
dependencies=[Security(validate_user_permissions, scopes=[Permissions.HOME.READ])],
|
64
75
|
)
|
65
76
|
async def home_ui(request: Request):
|
66
|
-
return templates.TemplateResponse(
|
67
|
-
"index.html", context={"request": request, "title": "Home"}
|
68
|
-
)
|
77
|
+
return templates.TemplateResponse(request, "index.html", context={"title": "Home"})
|
69
78
|
|
70
79
|
|
71
80
|
@router.get(
|
72
81
|
"/devices",
|
73
|
-
dependencies=[
|
74
|
-
Security(validate_user_permissions, scopes=[Permissions.DEVICE.READ])
|
75
|
-
],
|
82
|
+
dependencies=[Security(validate_user_permissions, scopes=[Permissions.DEVICE.READ])],
|
76
83
|
)
|
77
84
|
async def devices_ui(request: Request):
|
78
|
-
return templates.TemplateResponse(
|
79
|
-
"devices.html", context={"request": request, "title": "Devices"}
|
80
|
-
)
|
85
|
+
return templates.TemplateResponse(request, "devices.html", context={"title": "Devices"})
|
81
86
|
|
82
87
|
|
83
88
|
@router.get(
|
84
|
-
"/
|
85
|
-
dependencies=[
|
86
|
-
Security(validate_user_permissions, scopes=[Permissions.DEVICE.READ])
|
87
|
-
],
|
89
|
+
"/rollouts",
|
90
|
+
dependencies=[Security(validate_user_permissions, scopes=[Permissions.ROLLOUT.READ])],
|
88
91
|
)
|
89
|
-
async def
|
90
|
-
return templates.TemplateResponse(
|
91
|
-
"logs.html", context={"request": request, "title": "Log", "device": dev_id}
|
92
|
-
)
|
92
|
+
async def rollouts_ui(request: Request):
|
93
|
+
return templates.TemplateResponse(request, "rollouts.html", context={"title": "Rollouts"})
|
93
94
|
|
94
95
|
|
95
96
|
@router.get(
|
96
|
-
"/
|
97
|
-
dependencies=[
|
98
|
-
Security(validate_user_permissions, scopes=[Permissions.TUNNEL.READ])
|
99
|
-
],
|
97
|
+
"/logs/{dev_id}",
|
98
|
+
dependencies=[Security(validate_user_permissions, scopes=[Permissions.DEVICE.READ])],
|
100
99
|
)
|
101
|
-
async def
|
102
|
-
return templates.TemplateResponse(
|
103
|
-
"tunnels.html", context={"request": request, "title": "Tunnels"}
|
104
|
-
)
|
100
|
+
async def logs_ui(request: Request, dev_id: str):
|
101
|
+
return templates.TemplateResponse(request, "logs.html", context={"title": "Log", "device": dev_id})
|