goosebit 0.1.1__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 +5 -2
- goosebit/api/__init__.py +1 -1
- goosebit/api/devices.py +59 -39
- goosebit/api/download.py +28 -14
- goosebit/api/firmware.py +40 -34
- goosebit/api/helper.py +30 -0
- goosebit/api/rollouts.py +64 -13
- goosebit/api/routes.py +14 -7
- goosebit/auth/__init__.py +14 -6
- goosebit/db.py +5 -0
- goosebit/models.py +110 -10
- goosebit/permissions.py +26 -20
- goosebit/realtime/__init__.py +1 -1
- goosebit/realtime/logs.py +3 -6
- goosebit/settings.py +4 -6
- goosebit/telemetry/__init__.py +28 -0
- goosebit/telemetry/prometheus.py +10 -0
- goosebit/ui/__init__.py +1 -1
- goosebit/ui/routes.py +33 -40
- goosebit/ui/static/js/devices.js +187 -250
- goosebit/ui/static/js/firmware.js +229 -92
- goosebit/ui/static/js/index.js +79 -90
- goosebit/ui/static/js/logs.js +14 -11
- goosebit/ui/static/js/rollouts.js +169 -27
- goosebit/ui/static/js/util.js +66 -0
- goosebit/ui/templates/devices.html +75 -51
- goosebit/ui/templates/firmware.html +149 -35
- goosebit/ui/templates/index.html +9 -26
- goosebit/ui/templates/login.html +58 -27
- goosebit/ui/templates/logs.html +15 -5
- goosebit/ui/templates/nav.html +77 -26
- goosebit/ui/templates/rollouts.html +62 -39
- 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 +53 -35
- goosebit/updater/manager.py +205 -103
- goosebit/updater/routes.py +4 -7
- goosebit/updates/__init__.py +70 -0
- goosebit/updates/swdesc.py +83 -0
- {goosebit-0.1.1.dist-info → goosebit-0.1.2.dist-info}/METADATA +53 -3
- 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 -13
- goosebit/updater/misc.py +0 -57
- goosebit/updates/artifacts.py +0 -89
- goosebit/updates/version.py +0 -38
- goosebit-0.1.1.dist-info/RECORD +0 -53
- {goosebit-0.1.1.dist-info → goosebit-0.1.2.dist-info}/LICENSE +0 -0
- {goosebit-0.1.1.dist-info → goosebit-0.1.2.dist-info}/WHEEL +0 -0
goosebit/updater/manager.py
CHANGED
@@ -5,87 +5,122 @@ import re
|
|
5
5
|
from abc import ABC, abstractmethod
|
6
6
|
from contextlib import asynccontextmanager
|
7
7
|
from datetime import datetime
|
8
|
+
from enum import StrEnum
|
8
9
|
from typing import Callable, Optional
|
9
10
|
|
10
|
-
from
|
11
|
+
from aiocache import Cache, cached
|
12
|
+
from aiocache.serializers import PickleSerializer
|
13
|
+
|
14
|
+
from goosebit.models import (
|
15
|
+
Device,
|
16
|
+
Firmware,
|
17
|
+
Hardware,
|
18
|
+
Rollout,
|
19
|
+
UpdateModeEnum,
|
20
|
+
UpdateStateEnum,
|
21
|
+
)
|
11
22
|
from goosebit.settings import POLL_TIME, POLL_TIME_UPDATING
|
12
|
-
|
23
|
+
|
24
|
+
|
25
|
+
class HandlingType(StrEnum):
|
26
|
+
SKIP = "skip"
|
27
|
+
ATTEMPT = "attempt"
|
28
|
+
FORCED = "forced"
|
13
29
|
|
14
30
|
|
15
31
|
class UpdateManager(ABC):
|
32
|
+
device_log_subscriptions: dict[str, list[Callable]] = {}
|
33
|
+
device_poll_time: dict[str, str] = {}
|
34
|
+
|
16
35
|
def __init__(self, dev_id: str):
|
17
36
|
self.dev_id = dev_id
|
18
|
-
self.config_data = {}
|
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:
|
35
48
|
return
|
36
49
|
|
37
|
-
async def
|
50
|
+
async def update_device_state(self, state: UpdateStateEnum) -> None:
|
38
51
|
return
|
39
52
|
|
40
|
-
async def
|
53
|
+
async def update_last_connection(self, last_seen: int, last_ip: str) -> None:
|
41
54
|
return
|
42
55
|
|
43
|
-
async def
|
56
|
+
async def update_update(self, update_mode: UpdateModeEnum, firmware: Firmware | None):
|
44
57
|
return
|
45
58
|
|
46
|
-
async def
|
59
|
+
async def update_name(self, name: str):
|
47
60
|
return
|
48
61
|
|
49
|
-
async def
|
50
|
-
return
|
62
|
+
async def update_feed(self, feed: str):
|
63
|
+
return
|
64
|
+
|
65
|
+
async def update_flavor(self, flavor: str):
|
66
|
+
return
|
51
67
|
|
52
68
|
async def update_config_data(self, **kwargs):
|
53
|
-
|
54
|
-
await self.update_hw_revision(kwargs.get("hw_revision") or "default")
|
69
|
+
return
|
55
70
|
|
56
|
-
|
57
|
-
|
58
|
-
await self.update_device_state("registered")
|
59
|
-
await self.save()
|
71
|
+
async def update_log_complete(self, log_complete: bool):
|
72
|
+
return
|
60
73
|
|
61
|
-
|
74
|
+
async def get_rollout(self) -> Optional[Rollout]:
|
75
|
+
return None
|
62
76
|
|
63
77
|
@asynccontextmanager
|
64
78
|
async def subscribe_log(self, callback: Callable):
|
65
79
|
device = await self.get_device()
|
66
|
-
self.log_subscribers
|
80
|
+
subscribers = self.log_subscribers
|
81
|
+
subscribers.append(callback)
|
82
|
+
self.log_subscribers = subscribers
|
67
83
|
await callback(device.last_log)
|
68
84
|
try:
|
69
85
|
yield
|
70
86
|
except asyncio.CancelledError:
|
71
87
|
pass
|
72
88
|
finally:
|
73
|
-
self.log_subscribers
|
89
|
+
subscribers = self.log_subscribers
|
90
|
+
subscribers.remove(callback)
|
91
|
+
self.log_subscribers = subscribers
|
74
92
|
|
75
93
|
@property
|
76
94
|
def poll_seconds(self):
|
77
95
|
time_obj = datetime.strptime(self.poll_time, "%H:%M:%S")
|
78
96
|
return time_obj.hour * 3600 + time_obj.minute * 60 + time_obj.second
|
79
97
|
|
98
|
+
@property
|
99
|
+
def log_subscribers(self):
|
100
|
+
return UpdateManager.device_log_subscriptions.get(self.dev_id, [])
|
101
|
+
|
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
|
+
|
80
118
|
async def publish_log(self, log_data: str | None):
|
81
119
|
for cb in self.log_subscribers:
|
82
120
|
await cb(log_data)
|
83
121
|
|
84
122
|
@abstractmethod
|
85
|
-
async def
|
86
|
-
|
87
|
-
@abstractmethod
|
88
|
-
async def get_update_mode(self) -> str: ...
|
123
|
+
async def get_update(self) -> tuple[HandlingType, Firmware]: ...
|
89
124
|
|
90
125
|
@abstractmethod
|
91
126
|
async def update_log(self, log_data: str) -> None: ...
|
@@ -96,152 +131,219 @@ class UnknownUpdateManager(UpdateManager):
|
|
96
131
|
super().__init__(dev_id)
|
97
132
|
self.poll_time = POLL_TIME_UPDATING
|
98
133
|
|
99
|
-
async def
|
100
|
-
return
|
134
|
+
async def _get_firmware(self) -> Firmware:
|
135
|
+
return await Firmware.latest(await self.get_device())
|
101
136
|
|
102
|
-
async def
|
103
|
-
|
137
|
+
async def get_update(self) -> tuple[HandlingType, Firmware]:
|
138
|
+
firmware = await self._get_firmware()
|
139
|
+
return HandlingType.FORCED, firmware
|
104
140
|
|
105
141
|
async def update_log(self, log_data: str) -> None:
|
106
142
|
return
|
107
143
|
|
108
144
|
|
109
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
|
+
)
|
110
155
|
async def get_device(self) -> Device:
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
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
|
115
160
|
|
116
|
-
|
117
|
-
await self.device.save()
|
161
|
+
return (await Device.get_or_create(uuid=self.dev_id, defaults={"hardware": hardware}))[0]
|
118
162
|
|
119
|
-
async def
|
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:
|
120
169
|
device = await self.get_device()
|
121
|
-
device.
|
170
|
+
device.force_update = force_update
|
171
|
+
await self.save_device(device, update_fields=["force_update"])
|
122
172
|
|
123
|
-
async def
|
173
|
+
async def update_fw_version(self, version: str) -> None:
|
124
174
|
device = await self.get_device()
|
125
|
-
device.
|
175
|
+
device.fw_version = version
|
176
|
+
await self.save_device(device, update_fields=["fw_version"])
|
126
177
|
|
127
|
-
async def
|
178
|
+
async def update_hardware(self, hardware: Hardware) -> None:
|
128
179
|
device = await self.get_device()
|
129
|
-
device.
|
180
|
+
device.hardware = hardware
|
181
|
+
await self.save_device(device, update_fields=["hardware"])
|
130
182
|
|
131
|
-
async def update_device_state(self, state:
|
183
|
+
async def update_device_state(self, state: UpdateStateEnum) -> None:
|
132
184
|
device = await self.get_device()
|
133
185
|
device.last_state = state
|
186
|
+
await self.save_device(device, update_fields=["last_state"])
|
134
187
|
|
135
|
-
async def
|
188
|
+
async def update_last_connection(self, last_seen: int, last_ip: str) -> None:
|
136
189
|
device = await self.get_device()
|
137
190
|
device.last_seen = last_seen
|
138
|
-
|
139
|
-
async def update_last_ip(self, last_ip: str) -> None:
|
140
|
-
device = await self.get_device()
|
141
191
|
if ":" in last_ip:
|
142
192
|
device.last_ipv6 = last_ip
|
193
|
+
await self.save_device(device, update_fields=["last_seen", "last_ipv6"])
|
143
194
|
else:
|
144
195
|
device.last_ip = last_ip
|
196
|
+
await self.save_device(device, update_fields=["last_seen", "last_ip"])
|
197
|
+
|
198
|
+
async def update_update(self, update_mode: UpdateModeEnum, firmware: Firmware | None):
|
199
|
+
device = await self.get_device()
|
200
|
+
device.assigned_firmware = firmware
|
201
|
+
device.update_mode = update_mode
|
202
|
+
await self.save_device(device, update_fields=["assigned_firmware_id", "update_mode"])
|
203
|
+
|
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"])
|
208
|
+
|
209
|
+
async def update_feed(self, feed: str):
|
210
|
+
device = await self.get_device()
|
211
|
+
device.feed = feed
|
212
|
+
await self.save_device(device, update_fields=["feed"])
|
213
|
+
|
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"])
|
145
241
|
|
146
242
|
async def get_rollout(self) -> Optional[Rollout]:
|
147
243
|
device = await self.get_device()
|
148
244
|
|
149
|
-
if device.
|
245
|
+
if device.update_mode == UpdateModeEnum.ROLLOUT:
|
150
246
|
return (
|
151
247
|
await Rollout.filter(
|
152
|
-
hw_model=device.hw_model,
|
153
|
-
hw_revision=device.hw_revision,
|
154
248
|
feed=device.feed,
|
155
249
|
flavor=device.flavor,
|
250
|
+
paused=False,
|
251
|
+
firmware__compatibility__devices__uuid=device.uuid,
|
156
252
|
)
|
157
253
|
.order_by("-created_at")
|
158
254
|
.first()
|
255
|
+
.prefetch_related("firmware")
|
159
256
|
)
|
160
257
|
|
161
258
|
return None
|
162
259
|
|
163
|
-
async def
|
260
|
+
async def _get_firmware(self) -> Firmware | None:
|
164
261
|
device = await self.get_device()
|
165
|
-
file = device.fw_file
|
166
262
|
|
167
|
-
if
|
263
|
+
if device.update_mode == UpdateModeEnum.ROLLOUT:
|
168
264
|
rollout = await self.get_rollout()
|
169
|
-
if rollout
|
170
|
-
|
171
|
-
|
172
|
-
|
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
|
173
278
|
|
174
|
-
async def
|
279
|
+
async def get_update(self) -> tuple[HandlingType, Firmware]:
|
175
280
|
device = await self.get_device()
|
281
|
+
firmware = await self._get_firmware()
|
176
282
|
|
177
|
-
|
178
|
-
|
179
|
-
mode = "skip"
|
283
|
+
if firmware is None:
|
284
|
+
handling_type = HandlingType.SKIP
|
180
285
|
self.poll_time = POLL_TIME
|
181
|
-
|
182
|
-
|
286
|
+
|
287
|
+
elif firmware.version == device.fw_version and not device.force_update:
|
288
|
+
handling_type = HandlingType.SKIP
|
183
289
|
self.poll_time = POLL_TIME
|
184
|
-
|
185
|
-
|
290
|
+
|
291
|
+
elif device.last_state == UpdateStateEnum.ERROR and not device.force_update:
|
292
|
+
handling_type = HandlingType.SKIP
|
186
293
|
self.poll_time = POLL_TIME
|
294
|
+
|
187
295
|
else:
|
188
|
-
|
296
|
+
handling_type = HandlingType.FORCED
|
189
297
|
self.poll_time = POLL_TIME_UPDATING
|
190
298
|
|
191
|
-
if
|
192
|
-
self.
|
299
|
+
if device.log_complete:
|
300
|
+
await self.update_log_complete(False)
|
193
301
|
await self.clear_log()
|
194
302
|
|
195
|
-
return
|
303
|
+
return handling_type, firmware
|
196
304
|
|
197
305
|
async def update_log(self, log_data: str) -> None:
|
198
306
|
if log_data is None:
|
199
307
|
return
|
200
308
|
device = await self.get_device()
|
309
|
+
|
310
|
+
if device.last_log is None:
|
311
|
+
device.last_log = ""
|
312
|
+
|
201
313
|
matches = re.findall(r"Downloaded (\d+)%", log_data)
|
202
314
|
if matches:
|
203
315
|
device.progress = matches[-1]
|
204
|
-
|
205
|
-
device.last_log = ""
|
316
|
+
|
206
317
|
if log_data.startswith("Installing Update Chunk Artifacts."):
|
207
|
-
|
208
|
-
|
209
|
-
self.
|
210
|
-
|
318
|
+
# clear log
|
319
|
+
device.last_log = ""
|
320
|
+
await self.publish_log(None)
|
321
|
+
|
211
322
|
if not log_data == "Skipped Update.":
|
212
323
|
device.last_log += f"{log_data}\n"
|
213
324
|
await self.publish_log(f"{log_data}\n")
|
214
|
-
|
325
|
+
|
326
|
+
await self.save_device(
|
327
|
+
device,
|
328
|
+
update_fields=["progress", "last_log"],
|
329
|
+
)
|
215
330
|
|
216
331
|
async def clear_log(self) -> None:
|
217
332
|
device = await self.get_device()
|
218
333
|
device.last_log = ""
|
219
|
-
await
|
334
|
+
await self.save_device(device, update_fields=["last_log"])
|
220
335
|
await self.publish_log(None)
|
221
336
|
|
222
337
|
|
223
|
-
|
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)
|
224
343
|
|
225
344
|
|
226
|
-
async def
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
def get_update_manager_sync(dev_id: str) -> UpdateManager:
|
234
|
-
global device_managers
|
235
|
-
if device_managers.get(dev_id) is None:
|
236
|
-
device_managers[dev_id] = DeviceUpdateManager(dev_id)
|
237
|
-
return device_managers[dev_id]
|
238
|
-
|
239
|
-
|
240
|
-
async def delete_device(dev_id: str) -> None:
|
241
|
-
global device_managers
|
242
|
-
try:
|
243
|
-
updater = get_update_manager_sync(dev_id)
|
244
|
-
await (await updater.get_device()).delete()
|
245
|
-
del device_managers[dev_id]
|
246
|
-
except KeyError:
|
247
|
-
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
@@ -5,8 +5,8 @@ from fastapi.requests import Request
|
|
5
5
|
|
6
6
|
from goosebit.settings import TENANT
|
7
7
|
|
8
|
-
from . import controller
|
9
|
-
from .manager import
|
8
|
+
from . import controller
|
9
|
+
from .manager import get_update_manager
|
10
10
|
|
11
11
|
|
12
12
|
async def verify_tenant(tenant: str):
|
@@ -17,10 +17,8 @@ async def verify_tenant(tenant: str):
|
|
17
17
|
|
18
18
|
async def log_last_connection(request: Request, dev_id: str):
|
19
19
|
host = request.client.host
|
20
|
-
updater =
|
21
|
-
await updater.
|
22
|
-
await updater.update_last_seen(round(time.time()))
|
23
|
-
await updater.save()
|
20
|
+
updater = await get_update_manager(dev_id)
|
21
|
+
await updater.update_last_connection(round(time.time()), host)
|
24
22
|
|
25
23
|
|
26
24
|
router = APIRouter(
|
@@ -29,4 +27,3 @@ router = APIRouter(
|
|
29
27
|
tags=["ddi"],
|
30
28
|
)
|
31
29
|
router.include_router(controller.router)
|
32
|
-
router.include_router(download.router)
|
goosebit/updates/__init__.py
CHANGED
@@ -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()
|