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/updater/manager.py
CHANGED
@@ -1,82 +1,126 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import asyncio
|
4
|
+
import re
|
4
5
|
from abc import ABC, abstractmethod
|
5
6
|
from contextlib import asynccontextmanager
|
6
|
-
from
|
7
|
+
from datetime import datetime
|
8
|
+
from enum import StrEnum
|
9
|
+
from typing import Callable, Optional
|
7
10
|
|
8
|
-
import
|
11
|
+
from aiocache import Cache, cached
|
12
|
+
from aiocache.serializers import PickleSerializer
|
9
13
|
|
10
|
-
from goosebit.models import
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
+
from goosebit.models import (
|
15
|
+
Device,
|
16
|
+
Firmware,
|
17
|
+
Hardware,
|
18
|
+
Rollout,
|
19
|
+
UpdateModeEnum,
|
20
|
+
UpdateStateEnum,
|
21
|
+
)
|
22
|
+
from goosebit.settings import POLL_TIME, POLL_TIME_UPDATING
|
23
|
+
|
24
|
+
|
25
|
+
class HandlingType(StrEnum):
|
26
|
+
SKIP = "skip"
|
27
|
+
ATTEMPT = "attempt"
|
28
|
+
FORCED = "forced"
|
14
29
|
|
15
30
|
|
16
31
|
class UpdateManager(ABC):
|
32
|
+
device_log_subscriptions: dict[str, list[Callable]] = {}
|
33
|
+
device_poll_time: dict[str, str] = {}
|
34
|
+
|
17
35
|
def __init__(self, dev_id: str):
|
18
36
|
self.dev_id = dev_id
|
19
|
-
self.device = None
|
20
|
-
self.force_update = False
|
21
|
-
self.update_complete = False
|
22
|
-
self.poll_time = POLL_TIME
|
23
|
-
self.log_subscribers: list[Callable] = []
|
24
37
|
|
25
38
|
async def get_device(self) -> Device | None:
|
26
39
|
return
|
27
40
|
|
28
|
-
async def
|
41
|
+
async def update_force_update(self, force_update: bool) -> None:
|
29
42
|
return
|
30
43
|
|
31
44
|
async def update_fw_version(self, version: str) -> None:
|
32
45
|
return
|
33
46
|
|
34
|
-
async def
|
47
|
+
async def update_hardware(self, hardware: Hardware) -> None:
|
48
|
+
return
|
49
|
+
|
50
|
+
async def update_device_state(self, state: UpdateStateEnum) -> None:
|
51
|
+
return
|
52
|
+
|
53
|
+
async def update_last_connection(self, last_seen: int, last_ip: str) -> None:
|
35
54
|
return
|
36
55
|
|
37
|
-
async def
|
56
|
+
async def update_update(self, update_mode: UpdateModeEnum, firmware: Firmware | None):
|
38
57
|
return
|
39
58
|
|
40
|
-
async def
|
59
|
+
async def update_name(self, name: str):
|
41
60
|
return
|
42
61
|
|
43
|
-
async def
|
62
|
+
async def update_feed(self, feed: str):
|
44
63
|
return
|
45
64
|
|
46
|
-
async def
|
65
|
+
async def update_flavor(self, flavor: str):
|
47
66
|
return
|
48
67
|
|
49
|
-
async def
|
50
|
-
return
|
68
|
+
async def update_config_data(self, **kwargs):
|
69
|
+
return
|
51
70
|
|
52
|
-
async def
|
71
|
+
async def update_log_complete(self, log_complete: bool):
|
53
72
|
return
|
54
73
|
|
74
|
+
async def get_rollout(self) -> Optional[Rollout]:
|
75
|
+
return None
|
76
|
+
|
55
77
|
@asynccontextmanager
|
56
78
|
async def subscribe_log(self, callback: Callable):
|
57
79
|
device = await self.get_device()
|
58
|
-
self.log_subscribers
|
80
|
+
subscribers = self.log_subscribers
|
81
|
+
subscribers.append(callback)
|
82
|
+
self.log_subscribers = subscribers
|
59
83
|
await callback(device.last_log)
|
60
84
|
try:
|
61
85
|
yield
|
62
86
|
except asyncio.CancelledError:
|
63
87
|
pass
|
64
88
|
finally:
|
65
|
-
self.log_subscribers
|
89
|
+
subscribers = self.log_subscribers
|
90
|
+
subscribers.remove(callback)
|
91
|
+
self.log_subscribers = subscribers
|
66
92
|
|
67
|
-
|
68
|
-
|
69
|
-
|
93
|
+
@property
|
94
|
+
def poll_seconds(self):
|
95
|
+
time_obj = datetime.strptime(self.poll_time, "%H:%M:%S")
|
96
|
+
return time_obj.hour * 3600 + time_obj.minute * 60 + time_obj.second
|
70
97
|
|
71
98
|
@property
|
72
|
-
def
|
73
|
-
return
|
99
|
+
def log_subscribers(self):
|
100
|
+
return UpdateManager.device_log_subscriptions.get(self.dev_id, [])
|
74
101
|
|
75
|
-
@
|
76
|
-
|
102
|
+
@log_subscribers.setter
|
103
|
+
def log_subscribers(self, value: list):
|
104
|
+
UpdateManager.device_log_subscriptions[self.dev_id] = value
|
105
|
+
|
106
|
+
@property
|
107
|
+
def poll_time(self):
|
108
|
+
return UpdateManager.device_poll_time.get(self.dev_id, POLL_TIME)
|
109
|
+
|
110
|
+
@poll_time.setter
|
111
|
+
def poll_time(self, value: str):
|
112
|
+
if not value == POLL_TIME:
|
113
|
+
UpdateManager.device_poll_time[self.dev_id] = value
|
114
|
+
return
|
115
|
+
if self.dev_id in UpdateManager.device_poll_time:
|
116
|
+
del UpdateManager.device_poll_time[self.dev_id]
|
117
|
+
|
118
|
+
async def publish_log(self, log_data: str | None):
|
119
|
+
for cb in self.log_subscribers:
|
120
|
+
await cb(log_data)
|
77
121
|
|
78
122
|
@abstractmethod
|
79
|
-
async def
|
123
|
+
async def get_update(self) -> tuple[HandlingType, Firmware]: ...
|
80
124
|
|
81
125
|
@abstractmethod
|
82
126
|
async def update_log(self, log_data: str) -> None: ...
|
@@ -85,122 +129,221 @@ class UpdateManager(ABC):
|
|
85
129
|
class UnknownUpdateManager(UpdateManager):
|
86
130
|
def __init__(self, dev_id: str):
|
87
131
|
super().__init__(dev_id)
|
88
|
-
self.poll_time =
|
132
|
+
self.poll_time = POLL_TIME_UPDATING
|
89
133
|
|
90
|
-
async def
|
91
|
-
return
|
134
|
+
async def _get_firmware(self) -> Firmware:
|
135
|
+
return await Firmware.latest(await self.get_device())
|
92
136
|
|
93
|
-
async def
|
94
|
-
|
137
|
+
async def get_update(self) -> tuple[HandlingType, Firmware]:
|
138
|
+
firmware = await self._get_firmware()
|
139
|
+
return HandlingType.FORCED, firmware
|
95
140
|
|
96
141
|
async def update_log(self, log_data: str) -> None:
|
97
142
|
return
|
98
143
|
|
99
144
|
|
100
145
|
class DeviceUpdateManager(UpdateManager):
|
146
|
+
hardware_default = None
|
147
|
+
|
148
|
+
@cached(
|
149
|
+
ttl=600,
|
150
|
+
key_builder=lambda fn, self: self.dev_id,
|
151
|
+
cache=Cache.MEMORY,
|
152
|
+
serializer=PickleSerializer(),
|
153
|
+
namespace="main",
|
154
|
+
)
|
101
155
|
async def get_device(self) -> Device:
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
156
|
+
hardware = DeviceUpdateManager.hardware_default
|
157
|
+
if hardware is None:
|
158
|
+
hardware = (await Hardware.get_or_create(model="default", revision="default"))[0]
|
159
|
+
DeviceUpdateManager.hardware_default = hardware
|
106
160
|
|
107
|
-
|
108
|
-
|
161
|
+
return (await Device.get_or_create(uuid=self.dev_id, defaults={"hardware": hardware}))[0]
|
162
|
+
|
163
|
+
async def save_device(self, device: Device, update_fields: list[str]):
|
164
|
+
cache = Cache(namespace="main")
|
165
|
+
await cache.set(self.dev_id, device, ttl=600)
|
166
|
+
await device.save(update_fields=update_fields)
|
167
|
+
|
168
|
+
async def update_force_update(self, force_update: bool) -> None:
|
169
|
+
device = await self.get_device()
|
170
|
+
device.force_update = force_update
|
171
|
+
await self.save_device(device, update_fields=["force_update"])
|
109
172
|
|
110
173
|
async def update_fw_version(self, version: str) -> None:
|
111
174
|
device = await self.get_device()
|
112
175
|
device.fw_version = version
|
176
|
+
await self.save_device(device, update_fields=["fw_version"])
|
113
177
|
|
114
|
-
async def
|
178
|
+
async def update_hardware(self, hardware: Hardware) -> None:
|
115
179
|
device = await self.get_device()
|
116
|
-
device.
|
180
|
+
device.hardware = hardware
|
181
|
+
await self.save_device(device, update_fields=["hardware"])
|
117
182
|
|
118
|
-
async def
|
183
|
+
async def update_device_state(self, state: UpdateStateEnum) -> None:
|
119
184
|
device = await self.get_device()
|
120
|
-
device.
|
185
|
+
device.last_state = state
|
186
|
+
await self.save_device(device, update_fields=["last_state"])
|
121
187
|
|
122
|
-
async def
|
188
|
+
async def update_last_connection(self, last_seen: int, last_ip: str) -> None:
|
123
189
|
device = await self.get_device()
|
190
|
+
device.last_seen = last_seen
|
124
191
|
if ":" in last_ip:
|
125
192
|
device.last_ipv6 = last_ip
|
193
|
+
await self.save_device(device, update_fields=["last_seen", "last_ipv6"])
|
126
194
|
else:
|
127
195
|
device.last_ip = last_ip
|
196
|
+
await self.save_device(device, update_fields=["last_seen", "last_ip"])
|
128
197
|
|
129
|
-
async def
|
198
|
+
async def update_update(self, update_mode: UpdateModeEnum, firmware: Firmware | None):
|
130
199
|
device = await self.get_device()
|
131
|
-
|
200
|
+
device.assigned_firmware = firmware
|
201
|
+
device.update_mode = update_mode
|
202
|
+
await self.save_device(device, update_fields=["assigned_firmware_id", "update_mode"])
|
132
203
|
|
133
|
-
|
134
|
-
|
135
|
-
|
204
|
+
async def update_name(self, name: str):
|
205
|
+
device = await self.get_device()
|
206
|
+
device.name = name
|
207
|
+
await self.save_device(device, update_fields=["name"])
|
136
208
|
|
137
|
-
async def
|
209
|
+
async def update_feed(self, feed: str):
|
138
210
|
device = await self.get_device()
|
211
|
+
device.feed = feed
|
212
|
+
await self.save_device(device, update_fields=["feed"])
|
139
213
|
|
140
|
-
|
141
|
-
|
142
|
-
|
214
|
+
async def update_flavor(self, flavor: str):
|
215
|
+
device = await self.get_device()
|
216
|
+
device.flavor = flavor
|
217
|
+
await self.save_device(device, update_fields=["flavor"])
|
218
|
+
|
219
|
+
async def update_config_data(self, **kwargs):
|
220
|
+
model = kwargs.get("hw_model") or "default"
|
221
|
+
revision = kwargs.get("hw_revision") or "default"
|
222
|
+
hardware = (await Hardware.get_or_create(model=model, revision=revision))[0]
|
223
|
+
device = await self.get_device()
|
224
|
+
modified = False
|
225
|
+
|
226
|
+
if device.hardware != hardware:
|
227
|
+
device.hardware = hardware
|
228
|
+
modified = True
|
229
|
+
|
230
|
+
if device.last_state == UpdateStateEnum.UNKNOWN:
|
231
|
+
device.last_state = UpdateStateEnum.REGISTERED
|
232
|
+
modified = True
|
233
|
+
|
234
|
+
if modified:
|
235
|
+
await self.save_device(device, update_fields=["hardware_id", "last_state"])
|
236
|
+
|
237
|
+
async def update_log_complete(self, log_complete: bool):
|
238
|
+
device = await self.get_device()
|
239
|
+
device.log_complete = log_complete
|
240
|
+
await self.save_device(device, update_fields=["log_complete"])
|
241
|
+
|
242
|
+
async def get_rollout(self) -> Optional[Rollout]:
|
243
|
+
device = await self.get_device()
|
244
|
+
|
245
|
+
if device.update_mode == UpdateModeEnum.ROLLOUT:
|
246
|
+
return (
|
247
|
+
await Rollout.filter(
|
248
|
+
feed=device.feed,
|
249
|
+
flavor=device.flavor,
|
250
|
+
paused=False,
|
251
|
+
firmware__compatibility__devices__uuid=device.uuid,
|
252
|
+
)
|
253
|
+
.order_by("-created_at")
|
254
|
+
.first()
|
255
|
+
.prefetch_related("firmware")
|
256
|
+
)
|
257
|
+
|
258
|
+
return None
|
259
|
+
|
260
|
+
async def _get_firmware(self) -> Firmware | None:
|
261
|
+
device = await self.get_device()
|
262
|
+
|
263
|
+
if device.update_mode == UpdateModeEnum.ROLLOUT:
|
264
|
+
rollout = await self.get_rollout()
|
265
|
+
if not rollout or rollout.paused:
|
266
|
+
return None
|
267
|
+
await rollout.fetch_related("firmware")
|
268
|
+
return rollout.firmware
|
269
|
+
if device.update_mode == UpdateModeEnum.ASSIGNED:
|
270
|
+
await device.fetch_related("assigned_firmware")
|
271
|
+
return device.assigned_firmware
|
272
|
+
|
273
|
+
if device.update_mode == UpdateModeEnum.LATEST:
|
274
|
+
return await Firmware.latest(device)
|
275
|
+
|
276
|
+
assert device.update_mode == UpdateModeEnum.PINNED
|
277
|
+
return None
|
278
|
+
|
279
|
+
async def get_update(self) -> tuple[HandlingType, Firmware]:
|
280
|
+
device = await self.get_device()
|
281
|
+
firmware = await self._get_firmware()
|
282
|
+
|
283
|
+
if firmware is None:
|
284
|
+
handling_type = HandlingType.SKIP
|
143
285
|
self.poll_time = POLL_TIME
|
144
|
-
|
145
|
-
|
286
|
+
|
287
|
+
elif firmware.version == device.fw_version and not device.force_update:
|
288
|
+
handling_type = HandlingType.SKIP
|
146
289
|
self.poll_time = POLL_TIME
|
147
|
-
else:
|
148
|
-
mode = "forced"
|
149
|
-
self.poll_time = "00:00:05"
|
150
290
|
|
151
|
-
|
152
|
-
|
153
|
-
self.poll_time =
|
291
|
+
elif device.last_state == UpdateStateEnum.ERROR and not device.force_update:
|
292
|
+
handling_type = HandlingType.SKIP
|
293
|
+
self.poll_time = POLL_TIME
|
294
|
+
|
295
|
+
else:
|
296
|
+
handling_type = HandlingType.FORCED
|
297
|
+
self.poll_time = POLL_TIME_UPDATING
|
154
298
|
|
155
|
-
|
156
|
-
|
157
|
-
|
299
|
+
if device.log_complete:
|
300
|
+
await self.update_log_complete(False)
|
301
|
+
await self.clear_log()
|
158
302
|
|
159
|
-
return
|
303
|
+
return handling_type, firmware
|
160
304
|
|
161
305
|
async def update_log(self, log_data: str) -> None:
|
162
306
|
if log_data is None:
|
163
307
|
return
|
164
308
|
device = await self.get_device()
|
309
|
+
|
165
310
|
if device.last_log is None:
|
166
311
|
device.last_log = ""
|
312
|
+
|
313
|
+
matches = re.findall(r"Downloaded (\d+)%", log_data)
|
314
|
+
if matches:
|
315
|
+
device.progress = matches[-1]
|
316
|
+
|
167
317
|
if log_data.startswith("Installing Update Chunk Artifacts."):
|
168
|
-
|
169
|
-
|
170
|
-
self.
|
171
|
-
|
318
|
+
# clear log
|
319
|
+
device.last_log = ""
|
320
|
+
await self.publish_log(None)
|
321
|
+
|
172
322
|
if not log_data == "Skipped Update.":
|
173
323
|
device.last_log += f"{log_data}\n"
|
174
324
|
await self.publish_log(f"{log_data}\n")
|
175
325
|
|
326
|
+
await self.save_device(
|
327
|
+
device,
|
328
|
+
update_fields=["progress", "last_log"],
|
329
|
+
)
|
330
|
+
|
176
331
|
async def clear_log(self) -> None:
|
177
332
|
device = await self.get_device()
|
178
333
|
device.last_log = ""
|
334
|
+
await self.save_device(device, update_fields=["last_log"])
|
179
335
|
await self.publish_log(None)
|
180
336
|
|
181
337
|
|
182
|
-
|
338
|
+
async def get_update_manager(dev_id: str) -> UpdateManager:
|
339
|
+
if dev_id == "unknown":
|
340
|
+
return UnknownUpdateManager("unknown")
|
341
|
+
else:
|
342
|
+
return DeviceUpdateManager(dev_id)
|
183
343
|
|
184
344
|
|
185
|
-
async def
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
def get_update_manager_sync(dev_id: str) -> UpdateManager:
|
193
|
-
global device_managers
|
194
|
-
if device_managers.get(dev_id) is None:
|
195
|
-
device_managers[dev_id] = DeviceUpdateManager(dev_id)
|
196
|
-
return device_managers[dev_id]
|
197
|
-
|
198
|
-
|
199
|
-
async def delete_device(dev_id: str) -> None:
|
200
|
-
global device_managers
|
201
|
-
try:
|
202
|
-
updater = get_update_manager_sync(dev_id)
|
203
|
-
await (await updater.get_device()).delete()
|
204
|
-
del device_managers[dev_id]
|
205
|
-
except KeyError:
|
206
|
-
pass
|
345
|
+
async def delete_devices(ids: list[str]):
|
346
|
+
await Device.filter(id__in=ids).delete()
|
347
|
+
cache = Cache(namespace="main")
|
348
|
+
for dev_id in ids:
|
349
|
+
await cache.delete(dev_id)
|
goosebit/updater/routes.py
CHANGED
@@ -3,22 +3,22 @@ import time
|
|
3
3
|
from fastapi import APIRouter, Depends, HTTPException
|
4
4
|
from fastapi.requests import Request
|
5
5
|
|
6
|
-
from . import
|
7
|
-
|
6
|
+
from goosebit.settings import TENANT
|
7
|
+
|
8
|
+
from . import controller
|
9
|
+
from .manager import get_update_manager
|
8
10
|
|
9
11
|
|
10
12
|
async def verify_tenant(tenant: str):
|
11
|
-
if not tenant ==
|
13
|
+
if not tenant == TENANT:
|
12
14
|
raise HTTPException(404)
|
13
15
|
return tenant
|
14
16
|
|
15
17
|
|
16
18
|
async def log_last_connection(request: Request, dev_id: str):
|
17
19
|
host = request.client.host
|
18
|
-
updater =
|
19
|
-
await updater.
|
20
|
-
await updater.update_last_seen(round(time.time()))
|
21
|
-
await updater.save()
|
20
|
+
updater = await get_update_manager(dev_id)
|
21
|
+
await updater.update_last_connection(round(time.time()), host)
|
22
22
|
|
23
23
|
|
24
24
|
router = APIRouter(
|
@@ -27,4 +27,3 @@ router = APIRouter(
|
|
27
27
|
tags=["ddi"],
|
28
28
|
)
|
29
29
|
router.include_router(controller.router)
|
30
|
-
router.include_router(download.router)
|
@@ -0,0 +1,70 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from urllib.parse import unquote, urlparse
|
3
|
+
from urllib.request import url2pathname
|
4
|
+
|
5
|
+
from fastapi.requests import Request
|
6
|
+
|
7
|
+
from goosebit.models import Firmware, Hardware
|
8
|
+
|
9
|
+
from . import swdesc
|
10
|
+
|
11
|
+
|
12
|
+
async def create_firmware_update(uri: str):
|
13
|
+
parsed_uri = urlparse(uri)
|
14
|
+
|
15
|
+
if parsed_uri.scheme == "file":
|
16
|
+
file_path = Path(url2pathname(unquote(parsed_uri.path)))
|
17
|
+
update_info = await swdesc.parse_file(file_path)
|
18
|
+
if update_info is None:
|
19
|
+
return
|
20
|
+
elif parsed_uri.scheme.startswith("http"):
|
21
|
+
update_info = await swdesc.parse_remote(uri)
|
22
|
+
if update_info is None:
|
23
|
+
return
|
24
|
+
else:
|
25
|
+
return
|
26
|
+
|
27
|
+
firmware = (
|
28
|
+
await Firmware.get_or_create(
|
29
|
+
uri=uri,
|
30
|
+
version=str(update_info["version"]),
|
31
|
+
size=update_info["size"],
|
32
|
+
hash=update_info["hash"],
|
33
|
+
)
|
34
|
+
)[0]
|
35
|
+
|
36
|
+
for comp in update_info["compatibility"]:
|
37
|
+
model = comp.get("hw_model", "default")
|
38
|
+
revision = comp.get("hw_revision", "default")
|
39
|
+
await firmware.compatibility.add((await Hardware.get_or_create(model=model, revision=revision))[0])
|
40
|
+
await firmware.save()
|
41
|
+
return firmware
|
42
|
+
|
43
|
+
|
44
|
+
def generate_chunk(request: Request, firmware: Firmware | None) -> list:
|
45
|
+
if firmware is None:
|
46
|
+
return []
|
47
|
+
if firmware.local:
|
48
|
+
href = str(
|
49
|
+
request.url_for(
|
50
|
+
"download_file",
|
51
|
+
file_id=firmware.id,
|
52
|
+
)
|
53
|
+
)
|
54
|
+
else:
|
55
|
+
href = firmware.uri
|
56
|
+
return [
|
57
|
+
{
|
58
|
+
"part": "os",
|
59
|
+
"version": "1",
|
60
|
+
"name": firmware.path.name,
|
61
|
+
"artifacts": [
|
62
|
+
{
|
63
|
+
"filename": firmware.path.name,
|
64
|
+
"hashes": {"sha1": firmware.hash},
|
65
|
+
"size": firmware.size,
|
66
|
+
"_links": {"download": {"href": href}},
|
67
|
+
}
|
68
|
+
],
|
69
|
+
}
|
70
|
+
]
|
@@ -0,0 +1,83 @@
|
|
1
|
+
import hashlib
|
2
|
+
import logging
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
import aiofiles
|
7
|
+
import httpx
|
8
|
+
import libconf
|
9
|
+
import semver
|
10
|
+
|
11
|
+
from goosebit.settings import UPDATES_DIR
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
def _append_compatibility(boardname, value, compatibility):
|
17
|
+
if "hardware-compatibility" in value:
|
18
|
+
for revision in value["hardware-compatibility"]:
|
19
|
+
compatibility.append({"hw_model": boardname, "hw_revision": revision})
|
20
|
+
|
21
|
+
|
22
|
+
def parse_descriptor(swdesc: libconf.AttrDict[Any, Any | None]):
|
23
|
+
swdesc_attrs = {}
|
24
|
+
try:
|
25
|
+
swdesc_attrs["version"] = semver.Version.parse(swdesc["software"]["version"])
|
26
|
+
compatibility = []
|
27
|
+
_append_compatibility("default", swdesc["software"], compatibility)
|
28
|
+
|
29
|
+
for key in swdesc["software"]:
|
30
|
+
element = swdesc["software"][key]
|
31
|
+
_append_compatibility(key, element, compatibility)
|
32
|
+
|
33
|
+
if isinstance(element, dict):
|
34
|
+
for key2 in element:
|
35
|
+
_append_compatibility(key, element[key2], compatibility)
|
36
|
+
|
37
|
+
swdesc_attrs["compatibility"] = compatibility
|
38
|
+
except KeyError as e:
|
39
|
+
logging.warning(f"Parsing swu descriptor failed, error={e}")
|
40
|
+
raise ValueError("Parsing swu descriptor failed", e)
|
41
|
+
|
42
|
+
return swdesc_attrs
|
43
|
+
|
44
|
+
|
45
|
+
async def parse_file(file: Path):
|
46
|
+
async with aiofiles.open(file, "r+b") as f:
|
47
|
+
# get file size
|
48
|
+
size = int((await f.read(110))[54:62], 16)
|
49
|
+
filename = b""
|
50
|
+
next_byte = await f.read(1)
|
51
|
+
while not next_byte == b"\x00":
|
52
|
+
filename += next_byte
|
53
|
+
next_byte = await f.read(1)
|
54
|
+
# 4 null bytes
|
55
|
+
await f.read(3)
|
56
|
+
|
57
|
+
# should always be the first file
|
58
|
+
if not filename == b"sw-description":
|
59
|
+
return None
|
60
|
+
|
61
|
+
swdesc = libconf.loads((await f.read(size)).decode("utf-8"))
|
62
|
+
|
63
|
+
swdesc_attrs = parse_descriptor(swdesc)
|
64
|
+
swdesc_attrs["size"] = file.stat().st_size
|
65
|
+
swdesc_attrs["hash"] = _sha1_hash_file(file)
|
66
|
+
return swdesc_attrs
|
67
|
+
|
68
|
+
|
69
|
+
async def parse_remote(url: str):
|
70
|
+
async with httpx.AsyncClient() as c:
|
71
|
+
file = await c.get(url)
|
72
|
+
temp_file = UPDATES_DIR.joinpath("temp.swu")
|
73
|
+
async with aiofiles.open(temp_file, "w+b") as f:
|
74
|
+
await f.write(file.content)
|
75
|
+
parsed_file = await parse_file(temp_file)
|
76
|
+
temp_file.unlink()
|
77
|
+
return parsed_file
|
78
|
+
|
79
|
+
|
80
|
+
def _sha1_hash_file(file_path: Path):
|
81
|
+
with file_path.open("rb") as f:
|
82
|
+
sha1_hash = hashlib.file_digest(f, "sha1")
|
83
|
+
return sha1_hash.hexdigest()
|