goosebit 0.2.5__py3-none-any.whl → 0.2.7__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 +41 -7
- goosebit/api/telemetry/metrics.py +1 -5
- goosebit/api/v1/devices/device/responses.py +1 -0
- goosebit/api/v1/devices/device/routes.py +8 -8
- goosebit/api/v1/devices/requests.py +20 -0
- goosebit/api/v1/devices/routes.py +68 -8
- goosebit/api/v1/download/routes.py +14 -3
- goosebit/api/v1/rollouts/routes.py +5 -4
- goosebit/api/v1/routes.py +2 -1
- goosebit/api/v1/settings/routes.py +14 -0
- goosebit/api/v1/settings/users/__init__.py +1 -0
- goosebit/api/v1/settings/users/requests.py +16 -0
- goosebit/api/v1/settings/users/responses.py +7 -0
- goosebit/api/v1/settings/users/routes.py +56 -0
- goosebit/api/v1/software/routes.py +18 -14
- goosebit/auth/__init__.py +49 -13
- goosebit/auth/permissions.py +80 -0
- goosebit/db/config.py +57 -1
- goosebit/db/migrations/models/2_20241121113728_update.py +11 -0
- goosebit/db/migrations/models/3_20241121140210_update.py +11 -0
- goosebit/db/migrations/models/4_20250324110331_update.py +16 -0
- goosebit/db/migrations/models/4_20250402085235_rename_uuid_to_id.py +11 -0
- goosebit/db/migrations/models/5_20250619090242_null_feed.py +83 -0
- goosebit/db/models.py +19 -8
- goosebit/db/pg_ssl_context.py +51 -0
- goosebit/device_manager.py +262 -0
- goosebit/plugins/__init__.py +32 -0
- goosebit/schema/devices.py +8 -5
- goosebit/schema/plugins.py +67 -0
- goosebit/schema/updates.py +15 -0
- goosebit/schema/users.py +9 -0
- goosebit/settings/__init__.py +0 -3
- goosebit/settings/schema.py +60 -14
- goosebit/storage/__init__.py +62 -0
- goosebit/storage/base.py +14 -0
- goosebit/storage/filesystem.py +111 -0
- goosebit/storage/s3.py +104 -0
- goosebit/ui/bff/common/columns.py +50 -0
- goosebit/ui/bff/common/responses.py +1 -0
- goosebit/ui/bff/devices/device/__init__.py +1 -0
- goosebit/ui/bff/devices/device/routes.py +17 -0
- goosebit/ui/bff/devices/requests.py +1 -0
- goosebit/ui/bff/devices/routes.py +49 -46
- goosebit/ui/bff/download/routes.py +14 -3
- goosebit/ui/bff/rollouts/routes.py +32 -4
- goosebit/ui/bff/routes.py +2 -1
- goosebit/ui/bff/settings/__init__.py +1 -0
- goosebit/ui/bff/settings/routes.py +20 -0
- goosebit/ui/bff/settings/users/__init__.py +1 -0
- goosebit/ui/bff/settings/users/responses.py +33 -0
- goosebit/ui/bff/settings/users/routes.py +80 -0
- goosebit/ui/bff/software/routes.py +40 -12
- goosebit/ui/nav.py +12 -2
- goosebit/ui/routes.py +66 -13
- goosebit/ui/static/js/devices.js +32 -24
- goosebit/ui/static/js/login.js +21 -5
- goosebit/ui/static/js/logs.js +7 -22
- goosebit/ui/static/js/rollouts.js +31 -30
- goosebit/ui/static/js/settings.js +322 -0
- goosebit/ui/static/js/setup.js +28 -0
- goosebit/ui/static/js/software.js +127 -121
- goosebit/ui/static/js/util.js +25 -4
- goosebit/ui/templates/__init__.py +10 -1
- goosebit/ui/templates/login.html.jinja +5 -0
- goosebit/ui/templates/nav.html.jinja +13 -5
- goosebit/ui/templates/rollouts.html.jinja +4 -22
- goosebit/ui/templates/settings.html.jinja +88 -0
- goosebit/ui/templates/setup.html.jinja +71 -0
- goosebit/ui/templates/software.html.jinja +0 -11
- goosebit/updater/controller/v1/routes.py +119 -77
- goosebit/updater/routes.py +83 -8
- goosebit/updates/__init__.py +24 -31
- goosebit/updates/swdesc.py +15 -8
- goosebit/users/__init__.py +63 -0
- goosebit/util/__init__.py +0 -0
- goosebit/util/path.py +42 -0
- goosebit/util/version.py +92 -0
- goosebit-0.2.7.dist-info/METADATA +280 -0
- goosebit-0.2.7.dist-info/RECORD +133 -0
- {goosebit-0.2.5.dist-info → goosebit-0.2.7.dist-info}/WHEEL +1 -1
- goosebit/realtime/logs.py +0 -42
- goosebit/realtime/routes.py +0 -13
- goosebit/updater/manager.py +0 -325
- goosebit-0.2.5.dist-info/METADATA +0 -189
- goosebit-0.2.5.dist-info/RECORD +0 -99
- /goosebit/{realtime → api/v1/settings}/__init__.py +0 -0
- {goosebit-0.2.5.dist-info → goosebit-0.2.7.dist-info}/LICENSE +0 -0
- {goosebit-0.2.5.dist-info → goosebit-0.2.7.dist-info}/entry_points.txt +0 -0
goosebit/updater/manager.py
DELETED
@@ -1,325 +0,0 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
|
-
import asyncio
|
4
|
-
import re
|
5
|
-
from abc import ABC, abstractmethod
|
6
|
-
from contextlib import asynccontextmanager
|
7
|
-
from datetime import datetime
|
8
|
-
from enum import StrEnum
|
9
|
-
from typing import Callable
|
10
|
-
|
11
|
-
from aiocache import cached, caches
|
12
|
-
|
13
|
-
from goosebit.db.models import (
|
14
|
-
Device,
|
15
|
-
Hardware,
|
16
|
-
Rollout,
|
17
|
-
Software,
|
18
|
-
UpdateModeEnum,
|
19
|
-
UpdateStateEnum,
|
20
|
-
)
|
21
|
-
from goosebit.settings import config
|
22
|
-
|
23
|
-
caches.set_config(
|
24
|
-
{
|
25
|
-
"default": {
|
26
|
-
"cache": "aiocache.SimpleMemoryCache",
|
27
|
-
"serializer": {"class": "aiocache.serializers.PickleSerializer"},
|
28
|
-
"ttl": 600,
|
29
|
-
},
|
30
|
-
}
|
31
|
-
)
|
32
|
-
|
33
|
-
|
34
|
-
class HandlingType(StrEnum):
|
35
|
-
SKIP = "skip"
|
36
|
-
ATTEMPT = "attempt"
|
37
|
-
FORCED = "forced"
|
38
|
-
|
39
|
-
|
40
|
-
class UpdateManager(ABC):
|
41
|
-
device_log_subscriptions: dict[str, list[Callable]] = {}
|
42
|
-
device_poll_time: dict[str, str] = {}
|
43
|
-
|
44
|
-
def __init__(self, dev_id: str):
|
45
|
-
self.dev_id = dev_id
|
46
|
-
|
47
|
-
async def get_device(self) -> Device | None:
|
48
|
-
return None
|
49
|
-
|
50
|
-
async def update_force_update(self, force_update: bool) -> None:
|
51
|
-
return
|
52
|
-
|
53
|
-
async def update_sw_version(self, version: str) -> None:
|
54
|
-
return
|
55
|
-
|
56
|
-
async def update_hardware(self, hardware: Hardware) -> None:
|
57
|
-
return
|
58
|
-
|
59
|
-
async def update_device_state(self, state: UpdateStateEnum) -> None:
|
60
|
-
return
|
61
|
-
|
62
|
-
async def update_last_connection(self, last_seen: int, last_ip: str | None = None) -> None:
|
63
|
-
return
|
64
|
-
|
65
|
-
async def update_update(self, update_mode: UpdateModeEnum, software: Software | None):
|
66
|
-
return
|
67
|
-
|
68
|
-
async def update_name(self, name: str):
|
69
|
-
return
|
70
|
-
|
71
|
-
async def update_feed(self, feed: str):
|
72
|
-
return
|
73
|
-
|
74
|
-
async def update_config_data(self, **kwargs):
|
75
|
-
return
|
76
|
-
|
77
|
-
async def deployment_action_success(self):
|
78
|
-
return
|
79
|
-
|
80
|
-
async def clear_log(self) -> None:
|
81
|
-
return
|
82
|
-
|
83
|
-
async def get_rollout(self) -> Rollout | None:
|
84
|
-
return None
|
85
|
-
|
86
|
-
@asynccontextmanager
|
87
|
-
async def subscribe_log(self, callback: Callable):
|
88
|
-
device = await self.get_device()
|
89
|
-
# do not modify, breaks when combined
|
90
|
-
subscribers = self.log_subscribers
|
91
|
-
subscribers.append(callback)
|
92
|
-
self.log_subscribers = subscribers
|
93
|
-
|
94
|
-
if device is not None:
|
95
|
-
await callback(device.last_log)
|
96
|
-
try:
|
97
|
-
yield
|
98
|
-
except asyncio.CancelledError:
|
99
|
-
pass
|
100
|
-
finally:
|
101
|
-
# do not modify, breaks when combined
|
102
|
-
subscribers = self.log_subscribers
|
103
|
-
subscribers.remove(callback)
|
104
|
-
self.log_subscribers = subscribers
|
105
|
-
|
106
|
-
@property
|
107
|
-
def poll_seconds(self):
|
108
|
-
time_obj = datetime.strptime(self.poll_time, "%H:%M:%S")
|
109
|
-
return time_obj.hour * 3600 + time_obj.minute * 60 + time_obj.second
|
110
|
-
|
111
|
-
@property
|
112
|
-
def log_subscribers(self):
|
113
|
-
return UpdateManager.device_log_subscriptions.get(self.dev_id, [])
|
114
|
-
|
115
|
-
@log_subscribers.setter
|
116
|
-
def log_subscribers(self, value: list):
|
117
|
-
UpdateManager.device_log_subscriptions[self.dev_id] = value
|
118
|
-
|
119
|
-
@property
|
120
|
-
def poll_time(self):
|
121
|
-
return UpdateManager.device_poll_time.get(self.dev_id, config.poll_time_default)
|
122
|
-
|
123
|
-
@poll_time.setter
|
124
|
-
def poll_time(self, value: str):
|
125
|
-
if not value == config.poll_time_default:
|
126
|
-
UpdateManager.device_poll_time[self.dev_id] = value
|
127
|
-
return
|
128
|
-
if self.dev_id in UpdateManager.device_poll_time:
|
129
|
-
del UpdateManager.device_poll_time[self.dev_id]
|
130
|
-
|
131
|
-
async def publish_log(self, log_data: str | None):
|
132
|
-
for cb in self.log_subscribers:
|
133
|
-
await cb(log_data)
|
134
|
-
|
135
|
-
@abstractmethod
|
136
|
-
async def get_update(self) -> tuple[HandlingType, Software | None]: ...
|
137
|
-
|
138
|
-
@abstractmethod
|
139
|
-
async def update_log(self, log_data: str) -> None: ...
|
140
|
-
|
141
|
-
|
142
|
-
class DeviceUpdateManager(UpdateManager):
|
143
|
-
hardware_default = None
|
144
|
-
|
145
|
-
@cached(key_builder=lambda fn, self: self.dev_id, alias="default")
|
146
|
-
async def get_device(self) -> Device:
|
147
|
-
hardware = DeviceUpdateManager.hardware_default
|
148
|
-
if hardware is None:
|
149
|
-
hardware = (await Hardware.get_or_create(model="default", revision="default"))[0]
|
150
|
-
DeviceUpdateManager.hardware_default = hardware
|
151
|
-
|
152
|
-
return (await Device.get_or_create(uuid=self.dev_id, defaults={"hardware": hardware}))[0]
|
153
|
-
|
154
|
-
async def save_device(self, device: Device, update_fields: list[str]):
|
155
|
-
await device.save(update_fields=update_fields)
|
156
|
-
|
157
|
-
# only update cache after a successful database save
|
158
|
-
result = await caches.get("default").set(self.dev_id, device, ttl=600)
|
159
|
-
assert result, "device being cached"
|
160
|
-
|
161
|
-
async def update_force_update(self, force_update: bool) -> None:
|
162
|
-
device = await self.get_device()
|
163
|
-
device.force_update = force_update
|
164
|
-
await self.save_device(device, update_fields=["force_update"])
|
165
|
-
|
166
|
-
async def update_sw_version(self, version: str) -> None:
|
167
|
-
device = await self.get_device()
|
168
|
-
device.sw_version = version
|
169
|
-
await self.save_device(device, update_fields=["sw_version"])
|
170
|
-
|
171
|
-
async def update_hardware(self, hardware: Hardware) -> None:
|
172
|
-
device = await self.get_device()
|
173
|
-
device.hardware = hardware
|
174
|
-
await self.save_device(device, update_fields=["hardware"])
|
175
|
-
|
176
|
-
async def update_device_state(self, state: UpdateStateEnum) -> None:
|
177
|
-
device = await self.get_device()
|
178
|
-
device.last_state = state
|
179
|
-
await self.save_device(device, update_fields=["last_state"])
|
180
|
-
|
181
|
-
async def update_last_connection(self, last_seen: int, last_ip: str | None = None) -> None:
|
182
|
-
device = await self.get_device()
|
183
|
-
device.last_seen = last_seen
|
184
|
-
if last_ip is None:
|
185
|
-
await self.save_device(device, update_fields=["last_seen"])
|
186
|
-
elif ":" in last_ip:
|
187
|
-
device.last_ipv6 = last_ip
|
188
|
-
await self.save_device(device, update_fields=["last_seen", "last_ipv6"])
|
189
|
-
else:
|
190
|
-
device.last_ip = last_ip
|
191
|
-
await self.save_device(device, update_fields=["last_seen", "last_ip"])
|
192
|
-
|
193
|
-
async def update_update(self, update_mode: UpdateModeEnum, software: Software | None):
|
194
|
-
device = await self.get_device()
|
195
|
-
device.assigned_software = software
|
196
|
-
device.update_mode = update_mode
|
197
|
-
await self.save_device(device, update_fields=["assigned_software_id", "update_mode"])
|
198
|
-
|
199
|
-
async def update_name(self, name: str):
|
200
|
-
device = await self.get_device()
|
201
|
-
device.name = name
|
202
|
-
await self.save_device(device, update_fields=["name"])
|
203
|
-
|
204
|
-
async def update_feed(self, feed: str):
|
205
|
-
device = await self.get_device()
|
206
|
-
device.feed = feed
|
207
|
-
await self.save_device(device, update_fields=["feed"])
|
208
|
-
|
209
|
-
async def update_config_data(self, **kwargs):
|
210
|
-
model = kwargs.get("hw_boardname") or "default"
|
211
|
-
revision = kwargs.get("hw_revision") or "default"
|
212
|
-
sw_version = kwargs.get("sw_version")
|
213
|
-
|
214
|
-
hardware = (await Hardware.get_or_create(model=model, revision=revision))[0]
|
215
|
-
device = await self.get_device()
|
216
|
-
modified = False
|
217
|
-
|
218
|
-
if device.hardware != hardware:
|
219
|
-
device.hardware = hardware
|
220
|
-
modified = True
|
221
|
-
|
222
|
-
if device.last_state == UpdateStateEnum.UNKNOWN:
|
223
|
-
device.last_state = UpdateStateEnum.REGISTERED
|
224
|
-
modified = True
|
225
|
-
|
226
|
-
if device.sw_version != sw_version:
|
227
|
-
device.sw_version = sw_version
|
228
|
-
modified = True
|
229
|
-
|
230
|
-
if modified:
|
231
|
-
await self.save_device(device, update_fields=["hardware_id", "last_state", "sw_version"])
|
232
|
-
|
233
|
-
async def deployment_action_success(self):
|
234
|
-
device = await self.get_device()
|
235
|
-
device.progress = 100
|
236
|
-
await self.save_device(device, update_fields=["progress"])
|
237
|
-
|
238
|
-
async def get_rollout(self) -> Rollout | None:
|
239
|
-
device = await self.get_device()
|
240
|
-
|
241
|
-
if device.update_mode == UpdateModeEnum.ROLLOUT:
|
242
|
-
return (
|
243
|
-
await Rollout.filter(
|
244
|
-
feed=device.feed,
|
245
|
-
paused=False,
|
246
|
-
software__compatibility__devices__uuid=device.uuid,
|
247
|
-
)
|
248
|
-
.order_by("-created_at")
|
249
|
-
.first()
|
250
|
-
.prefetch_related("software")
|
251
|
-
)
|
252
|
-
|
253
|
-
return None
|
254
|
-
|
255
|
-
async def _get_software(self) -> Software | None:
|
256
|
-
device = await self.get_device()
|
257
|
-
|
258
|
-
if device.update_mode == UpdateModeEnum.ROLLOUT:
|
259
|
-
rollout = await self.get_rollout()
|
260
|
-
if not rollout or rollout.paused:
|
261
|
-
return None
|
262
|
-
await rollout.fetch_related("software")
|
263
|
-
return rollout.software
|
264
|
-
if device.update_mode == UpdateModeEnum.ASSIGNED:
|
265
|
-
await device.fetch_related("assigned_software")
|
266
|
-
return device.assigned_software
|
267
|
-
|
268
|
-
if device.update_mode == UpdateModeEnum.LATEST:
|
269
|
-
return await Software.latest(device)
|
270
|
-
|
271
|
-
assert device.update_mode == UpdateModeEnum.PINNED
|
272
|
-
return None
|
273
|
-
|
274
|
-
async def get_update(self) -> tuple[HandlingType, Software | None]:
|
275
|
-
device = await self.get_device()
|
276
|
-
software = await self._get_software()
|
277
|
-
|
278
|
-
if software is None:
|
279
|
-
handling_type = HandlingType.SKIP
|
280
|
-
|
281
|
-
elif software.version == device.sw_version and not device.force_update:
|
282
|
-
handling_type = HandlingType.SKIP
|
283
|
-
|
284
|
-
elif device.last_state == UpdateStateEnum.ERROR and not device.force_update:
|
285
|
-
handling_type = HandlingType.SKIP
|
286
|
-
|
287
|
-
else:
|
288
|
-
handling_type = HandlingType.FORCED
|
289
|
-
|
290
|
-
return handling_type, software
|
291
|
-
|
292
|
-
async def update_log(self, log_data: str) -> None:
|
293
|
-
if log_data is None:
|
294
|
-
return
|
295
|
-
device = await self.get_device()
|
296
|
-
|
297
|
-
if device.last_log is None:
|
298
|
-
device.last_log = ""
|
299
|
-
|
300
|
-
# SWUpdate-specific log parsing to report progress
|
301
|
-
matches = re.findall(r"Downloaded (\d+)%", log_data)
|
302
|
-
if matches:
|
303
|
-
device.progress = matches[-1]
|
304
|
-
|
305
|
-
device.last_log += f"{log_data}\n"
|
306
|
-
await self.publish_log(f"{log_data}\n")
|
307
|
-
|
308
|
-
await self.save_device(device, update_fields=["progress", "last_log"])
|
309
|
-
|
310
|
-
async def clear_log(self) -> None:
|
311
|
-
device = await self.get_device()
|
312
|
-
device.last_log = ""
|
313
|
-
await self.save_device(device, update_fields=["last_log"])
|
314
|
-
await self.publish_log(None)
|
315
|
-
|
316
|
-
|
317
|
-
async def get_update_manager(dev_id: str) -> UpdateManager:
|
318
|
-
return DeviceUpdateManager(dev_id)
|
319
|
-
|
320
|
-
|
321
|
-
async def delete_devices(ids: list[str]):
|
322
|
-
await Device.filter(uuid__in=ids).delete()
|
323
|
-
for dev_id in ids:
|
324
|
-
result = await caches.get("default").delete(dev_id)
|
325
|
-
assert result == 1, "device has been cached"
|
@@ -1,189 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.1
|
2
|
-
Name: goosebit
|
3
|
-
Version: 0.2.5
|
4
|
-
Summary:
|
5
|
-
Author: Upstream Data
|
6
|
-
Author-email: brett@upstreamdata.ca
|
7
|
-
Requires-Python: >=3.11,<4.0
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
9
|
-
Classifier: Programming Language :: Python :: 3.11
|
10
|
-
Classifier: Programming Language :: Python :: 3.12
|
11
|
-
Classifier: Programming Language :: Python :: 3.13
|
12
|
-
Provides-Extra: postgresql
|
13
|
-
Requires-Dist: aerich (>=0.7.2,<0.8.0)
|
14
|
-
Requires-Dist: aiocache (>=0.12.2,<0.13.0)
|
15
|
-
Requires-Dist: argon2-cffi (>=23.1.0,<24.0.0)
|
16
|
-
Requires-Dist: asyncpg (>=0.29.0,<0.30.0) ; extra == "postgresql"
|
17
|
-
Requires-Dist: fastapi[uvicorn] (>=0.111.0,<0.112.0)
|
18
|
-
Requires-Dist: httpx (>=0.27.0,<0.28.0)
|
19
|
-
Requires-Dist: itsdangerous (>=2.2.0,<3.0.0)
|
20
|
-
Requires-Dist: jinja2 (>=3.1.4,<4.0.0)
|
21
|
-
Requires-Dist: joserfc (>=1.0.0,<2.0.0)
|
22
|
-
Requires-Dist: libconf (>=2.0.1,<3.0.0)
|
23
|
-
Requires-Dist: opentelemetry-distro (>=0.49b1,<0.50)
|
24
|
-
Requires-Dist: opentelemetry-exporter-prometheus (>=0.49b1,<0.50)
|
25
|
-
Requires-Dist: opentelemetry-instrumentation-fastapi (>=0.49b1,<0.50)
|
26
|
-
Requires-Dist: pydantic-settings (>=2.4.0,<3.0.0)
|
27
|
-
Requires-Dist: python-multipart (>=0.0.9,<0.0.10)
|
28
|
-
Requires-Dist: semver (>=3.0.2,<4.0.0)
|
29
|
-
Requires-Dist: tortoise-orm (>=0.21.4,<0.22.0)
|
30
|
-
Requires-Dist: websockets (>=12.0,<13.0)
|
31
|
-
Description-Content-Type: text/markdown
|
32
|
-
|
33
|
-
# gooseBit
|
34
|
-
|
35
|
-
<img src="docs/img/goosebit-logo.png" style="width: 100px; height: 100px; display: block;">
|
36
|
-
|
37
|
-
[](https://scorecard.dev/viewer/?uri=github.com/UpstreamDataInc/goosebit)
|
38
|
-
|
39
|
-
---
|
40
|
-
|
41
|
-
A simplistic, opinionated remote update server implementing hawkBit™'s [DDI API](https://eclipse.dev/hawkbit/apis/ddi_api/).
|
42
|
-
|
43
|
-
## Quick Start
|
44
|
-
|
45
|
-
### Installation
|
46
|
-
|
47
|
-
1. Install dependencies using [Poetry](https://python-poetry.org/):
|
48
|
-
|
49
|
-
```bash
|
50
|
-
poetry install
|
51
|
-
```
|
52
|
-
|
53
|
-
2. Create the database:
|
54
|
-
|
55
|
-
```bash
|
56
|
-
poetry run aerich upgrade
|
57
|
-
```
|
58
|
-
|
59
|
-
3. Launch gooseBit:
|
60
|
-
```bash
|
61
|
-
python main.py
|
62
|
-
```
|
63
|
-
|
64
|
-
### Initial Configuration
|
65
|
-
|
66
|
-
Before running gooseBit for the first time, update the default credentials in `settings.yaml`. The default login for testing purposes is:
|
67
|
-
|
68
|
-
- **Username:** `admin@goosebit.local`
|
69
|
-
- **Password:** `admin`
|
70
|
-
|
71
|
-
## Assumptions
|
72
|
-
|
73
|
-
- Devices use [SWUpdate](https://swupdate.org) for managing software updates.
|
74
|
-
|
75
|
-
## Features
|
76
|
-
|
77
|
-
### Device Registry
|
78
|
-
|
79
|
-
When a device connects to gooseBit for the first time, it is automatically added to the device registry. The server will then request the device's configuration data, including:
|
80
|
-
|
81
|
-
- `hw_model` and `hw_revision`: Used to match compatible software.
|
82
|
-
- `sw_version`: Indicates the currently installed software version.
|
83
|
-
|
84
|
-
The registry tracks each device's status, including the last online timestamp, installed software version, update state, and more.
|
85
|
-
|
86
|
-
### Software Repository
|
87
|
-
|
88
|
-
Software packages (`*.swu` files) can be hosted directly on the gooseBit server or on an external server. gooseBit parses the software metadata to determine compatibility with specific hardware models and revisions.
|
89
|
-
|
90
|
-
### Device Update Modes
|
91
|
-
|
92
|
-
Devices can be configured with different update modes. The default mode is `Rollout`.
|
93
|
-
|
94
|
-
#### 1. Manual Update to Specified Software
|
95
|
-
|
96
|
-
Assign specific software to a device manually. Once installed, no further updates will be triggered.
|
97
|
-
|
98
|
-
#### 2. Automatic Update to Latest Software
|
99
|
-
|
100
|
-
Automatically updates the device to the latest compatible software, based on the reported `hw_model` and `hw_revision`. Note: versions are interpreted as [SemVer](https://semver.org) versions.
|
101
|
-
|
102
|
-
#### 3. Software Rollout
|
103
|
-
|
104
|
-
Rollouts target all devices with a specified "feed" value, ensuring that the assigned software is installed on all matching devices. Rollouts also track success and error rates, with future plans for automatic aborts. If multiple rollouts exist for the same feed, the most recent rollout takes precedence.
|
105
|
-
|
106
|
-
### Pause Updates
|
107
|
-
|
108
|
-
Devices can be pinned to their current software version, preventing any updates from being applied.
|
109
|
-
|
110
|
-
### Real-time Update Logs
|
111
|
-
|
112
|
-
While updates are in progress, gooseBit captures real-time logs, which are accessible through the device repository.
|
113
|
-
|
114
|
-
## Development
|
115
|
-
|
116
|
-
### Database
|
117
|
-
|
118
|
-
Create or upgrade database
|
119
|
-
|
120
|
-
```bash
|
121
|
-
poetry run aerich upgrade
|
122
|
-
```
|
123
|
-
|
124
|
-
After a model change create the migration
|
125
|
-
|
126
|
-
```bash
|
127
|
-
poetry run aerich migrate
|
128
|
-
```
|
129
|
-
|
130
|
-
To seed some sample data (attention: drops all current data) use
|
131
|
-
|
132
|
-
```bash
|
133
|
-
poetry run generate-sample-data
|
134
|
-
```
|
135
|
-
|
136
|
-
### Code formatting and linting
|
137
|
-
|
138
|
-
Code is formatted using different tools
|
139
|
-
|
140
|
-
- black and isort for `*.py`
|
141
|
-
- biomejs for `*.js`, `*.json`
|
142
|
-
- prettier for `*.html`, `*.md`, `*.yml`, `*.yaml`
|
143
|
-
|
144
|
-
Code is linted using different tools as well
|
145
|
-
|
146
|
-
- flake8 for `*.py`
|
147
|
-
- biomejs for `*.js`
|
148
|
-
|
149
|
-
Best to have pre-commit install git hooks that run all those tools before a commit:
|
150
|
-
|
151
|
-
```bash
|
152
|
-
poetry run pre-commit install
|
153
|
-
```
|
154
|
-
|
155
|
-
To manually apply the hooks to all files use:
|
156
|
-
|
157
|
-
```bash
|
158
|
-
poetry run pre-commit run --all-files
|
159
|
-
```
|
160
|
-
|
161
|
-
### Testing
|
162
|
-
|
163
|
-
Tests are implemented using pytest. To run all tests
|
164
|
-
|
165
|
-
```bash
|
166
|
-
poetry run pytest
|
167
|
-
```
|
168
|
-
|
169
|
-
### Structure
|
170
|
-
|
171
|
-
The structure of gooseBit is as follows:
|
172
|
-
|
173
|
-
- `api`: Files for the API.
|
174
|
-
- `ui`: Files for the UI.
|
175
|
-
- `bff`: Backend for frontend API.
|
176
|
-
- `static`: Static files.
|
177
|
-
- `templates`: Jinja2 formatted templates.
|
178
|
-
- `nav`: Navbar handler.
|
179
|
-
- `updater`: DDI API handler and device update manager.
|
180
|
-
- `updates`: SWUpdate file parsing.
|
181
|
-
- `realtime`: Realtime API functionality with websockets.
|
182
|
-
- `auth`: Authentication functions and permission handling.
|
183
|
-
- `models`: Database models.
|
184
|
-
- `db`: Database config and initialization.
|
185
|
-
- `schema`: Pydantic models used for API type hinting.
|
186
|
-
- `settings`: Settings loader and handler.
|
187
|
-
- `telemetry`: Telemetry data handlers.
|
188
|
-
- `routes`: Routes for a giving endpoint, including the router.
|
189
|
-
|
goosebit-0.2.5.dist-info/RECORD
DELETED
@@ -1,99 +0,0 @@
|
|
1
|
-
goosebit/__init__.py,sha256=qHY0WDT5n5PdX6D6tv2bAsBwBfMw-bcjEjKotIQDAzg,4158
|
2
|
-
goosebit/__main__.py,sha256=ezSLOobcrbnBspFOSbyjuATEdk8B3aAj9cgEu647ujk,161
|
3
|
-
goosebit/api/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH4KROTGd2S4AoI,41
|
4
|
-
goosebit/api/responses.py,sha256=HXLpFgL-iv5Ts6LKa3AMt1XGV1GbDE5GzjK66_3Gz5o,84
|
5
|
-
goosebit/api/routes.py,sha256=KOA1r8Ripl9nhZV4gZpuQrRi0fG9I3VEqcAAMeBGqBk,272
|
6
|
-
goosebit/api/telemetry/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH4KROTGd2S4AoI,41
|
7
|
-
goosebit/api/telemetry/metrics.py,sha256=aJ2viqBcxU41wJ0tDwPXj0Dsv6YEHaWv7EBDqkUilGI,791
|
8
|
-
goosebit/api/telemetry/prometheus/__init__.py,sha256=nvVtx3DsqBhoXE4Y4L6k7nYoQTl1VV0afyVW2YHLQGs,83
|
9
|
-
goosebit/api/telemetry/prometheus/readers.py,sha256=TKD7LT-lYS53xdt-tLMxqyWAeya62299cJtok9Q2MSE,104
|
10
|
-
goosebit/api/telemetry/prometheus/routes.py,sha256=91TNk_pbxi5BfCLo_OVojubL9kxFuL84dJ55RmsZ0eE,676
|
11
|
-
goosebit/api/telemetry/routes.py,sha256=nuv9bClyo_P8TAkcp3huqxZoUn-ATlRAMnl0OXbWNpw,217
|
12
|
-
goosebit/api/v1/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH4KROTGd2S4AoI,41
|
13
|
-
goosebit/api/v1/devices/__init__.py,sha256=kzt5TBA-bJmDZFXYPohggbMOW7GhklyflaRKZNC0yDY,42
|
14
|
-
goosebit/api/v1/devices/device/__init__.py,sha256=kzt5TBA-bJmDZFXYPohggbMOW7GhklyflaRKZNC0yDY,42
|
15
|
-
goosebit/api/v1/devices/device/responses.py,sha256=THIPMzc6mwl7GmIXuqAeLLesFHfssvPrMJeHe8tV4mA,222
|
16
|
-
goosebit/api/v1/devices/device/routes.py,sha256=rxdu7A2_NRcPV1ofXntrqE1elH9H2GuKE5kO07xQtXM,1173
|
17
|
-
goosebit/api/v1/devices/requests.py,sha256=zx9eWXQanqBnmy-XboA_lrzInfx9ozOB4vG2jRTwnJA,131
|
18
|
-
goosebit/api/v1/devices/responses.py,sha256=NmaxpXKJ_YsQ92mvoTXG8HYB39_HLFmZaHo_uvXFmhg,185
|
19
|
-
goosebit/api/v1/devices/routes.py,sha256=pcQx5U-KJXnGjbwheINOtdWtdcJOz3mvnRVcefAph10,1683
|
20
|
-
goosebit/api/v1/download/__init__.py,sha256=kzt5TBA-bJmDZFXYPohggbMOW7GhklyflaRKZNC0yDY,42
|
21
|
-
goosebit/api/v1/download/routes.py,sha256=wdeXA-dGO4D9FxaUP6OnUSW4_2NZhi_VgIxMcSRL2x4,675
|
22
|
-
goosebit/api/v1/rollouts/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH4KROTGd2S4AoI,41
|
23
|
-
goosebit/api/v1/rollouts/requests.py,sha256=EF1VUwaK-f-2SSKxl2klKrMGfBkBo72iWKtw3cAzzW0,257
|
24
|
-
goosebit/api/v1/rollouts/responses.py,sha256=yKljabI1wQB6MXNXrF4aIi5csKRCo0A96RqTW6OwAjE,311
|
25
|
-
goosebit/api/v1/rollouts/routes.py,sha256=afUQTjzrpLbHA68CEpMDMZ93C6-jp8JLrdjUEYUFm7o,1927
|
26
|
-
goosebit/api/v1/routes.py,sha256=g9vL0VOuFSdNG3FnK3y_03trwv4X2OLN9ustJeCgc58,272
|
27
|
-
goosebit/api/v1/software/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH4KROTGd2S4AoI,41
|
28
|
-
goosebit/api/v1/software/requests.py,sha256=visd81BMeWDKJADcC10NBJDs7Z3xQC-5nod9z_WHiPg,101
|
29
|
-
goosebit/api/v1/software/responses.py,sha256=l41-JWqyJmmEslXkx4NEJsGOqMrAB__nBUZS3E_9le0,192
|
30
|
-
goosebit/api/v1/software/routes.py,sha256=r8lSPg1eQajPF6990DwnbJpDsuzW53rC5ktLuG3zyjA,3209
|
31
|
-
goosebit/auth/__init__.py,sha256=VWrDFMfYfQpGV6Pc4xkb2zbUiQATpcsv5PbTiXQ9FoU,4703
|
32
|
-
goosebit/db/__init__.py,sha256=ktnV4302iWzzTr-3oHXv3ud-23qTcAJ4tPLAXyTZZJw,462
|
33
|
-
goosebit/db/config.py,sha256=YCtF52lQ5bEVtKsrqkSzQpbKmCbD79O8Tz0RE4thV3c,220
|
34
|
-
goosebit/db/migrations/models/0_20240830054046_init.py,sha256=EBig7b0-AOWt_AE10rQf_0ZrserjKTm0r53wYInK5Co,5526
|
35
|
-
goosebit/db/migrations/models/1_20241109151811_update.py,sha256=cPbX-8Ga7HYKHQkESR5FXNlS-s-bsx4bsLrXvcfVIkQ,309
|
36
|
-
goosebit/db/models.py,sha256=4gi-FsNJFLiAVNxV61Eb4qU8_Qn3zDNQmc2ngqDt_uU,5150
|
37
|
-
goosebit/realtime/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH4KROTGd2S4AoI,41
|
38
|
-
goosebit/realtime/logs.py,sha256=sjGg2n7H-4j4i02Kbq_YlECLFp33mmOmaoAZXeE8J9Y,1225
|
39
|
-
goosebit/realtime/routes.py,sha256=yLHrA-BHHU5cpUNvxAPVxWC3KcB9aUrV31HT_th24Ls,265
|
40
|
-
goosebit/schema/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
41
|
-
goosebit/schema/devices.py,sha256=mUmPLLEF506eKAxVPuUv5echqeHrkLIEuhNCHxL5y0Y,2659
|
42
|
-
goosebit/schema/rollouts.py,sha256=Kp--XRC39SDs1bKf59q8fBHzitoW88ZMN1GVlbCQotQ,882
|
43
|
-
goosebit/schema/software.py,sha256=W02rA0guQ7zeeVMTdwxt6EjCnTdf-9JewVJRqdN_GK0,951
|
44
|
-
goosebit/settings/__init__.py,sha256=UkJa_G_VJckNf16Em0zz8sXgEi5pzxHFvZ96oa9MC3k,360
|
45
|
-
goosebit/settings/const.py,sha256=hOpeA9pGmbU4V4mqcfhpHV7cmEjf2GIenmzCSz2Jpj8,779
|
46
|
-
goosebit/settings/schema.py,sha256=ffDhMsyW6_TGIReJRiw3hisKUretdf1PnISTJ0yBe0o,2577
|
47
|
-
goosebit/ui/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH4KROTGd2S4AoI,41
|
48
|
-
goosebit/ui/bff/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH4KROTGd2S4AoI,41
|
49
|
-
goosebit/ui/bff/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
50
|
-
goosebit/ui/bff/common/requests.py,sha256=Db8AohkRfsjUbx3pbdKVva7ZkLhAfBiDwAA221OXd5M,1183
|
51
|
-
goosebit/ui/bff/common/responses.py,sha256=3LLIrBhsjdELnZ4kFc5-QDDYwdn7OvcD_2CObiJ6NME,303
|
52
|
-
goosebit/ui/bff/common/util.py,sha256=_hz89EFLAL1UvbIvJJJSF80PL4T1mz1Vk7354wHBzOE,1144
|
53
|
-
goosebit/ui/bff/devices/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH4KROTGd2S4AoI,41
|
54
|
-
goosebit/ui/bff/devices/requests.py,sha256=YbTUsyVLQZaUnn3dJ6AQe0mZl_H5aQC6QKCRxhCjGJc,286
|
55
|
-
goosebit/ui/bff/devices/responses.py,sha256=4279xeu-soqgyOc4ncKZpai6juBQ3dl096p2LgtIu2s,1208
|
56
|
-
goosebit/ui/bff/devices/routes.py,sha256=9WoGBheOkOT5-UDlQ0rtK-MJF4MNPuHxz3mBxdVhbWU,5260
|
57
|
-
goosebit/ui/bff/download/__init__.py,sha256=kzt5TBA-bJmDZFXYPohggbMOW7GhklyflaRKZNC0yDY,42
|
58
|
-
goosebit/ui/bff/download/routes.py,sha256=wdeXA-dGO4D9FxaUP6OnUSW4_2NZhi_VgIxMcSRL2x4,675
|
59
|
-
goosebit/ui/bff/rollouts/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH4KROTGd2S4AoI,41
|
60
|
-
goosebit/ui/bff/rollouts/responses.py,sha256=gFEzWkFUBIka98lrA0ivdXBiTqaU5AugfJm2F3p33uk,1180
|
61
|
-
goosebit/ui/bff/rollouts/routes.py,sha256=ZO-mkDzTgzssBtIbhOqSxrPgZYMVPsMW4EXw3f8YA1o,1553
|
62
|
-
goosebit/ui/bff/routes.py,sha256=wx7so5Kfleop7ZdTEnMFSQ6P_Eck8iX6TnAqyetd3wM,428
|
63
|
-
goosebit/ui/bff/software/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH4KROTGd2S4AoI,41
|
64
|
-
goosebit/ui/bff/software/responses.py,sha256=e7zMP8q2uWgKRy1q5Jp0HTNx-lggljOy2BFQRJglDSo,1920
|
65
|
-
goosebit/ui/bff/software/routes.py,sha256=xwLsI6wTJzu_-eWAV1f8DlEWw67i22LK72D-uYLRMu0,3220
|
66
|
-
goosebit/ui/nav.py,sha256=l-MCY2AN5v6jDCSDZoka3qjQ2_24SjLqUi1dYtxCjIM,378
|
67
|
-
goosebit/ui/routes.py,sha256=J6Nkh1yShO810IRU3vjFlZCwYOy_qgwPCiOhHP3szbw,2015
|
68
|
-
goosebit/ui/static/__init__.py,sha256=AsZiM3h9chQ_YaBbAD6TVFf0244Q9DetkDMl70q8swQ,135
|
69
|
-
goosebit/ui/static/favicon.ico,sha256=MKAqEjW7F1-j2dmW4s08y4U0dFteIlU7rpLKakU0Lxk,4286
|
70
|
-
goosebit/ui/static/favicon.svg,sha256=8zvM7o2Ob3YmTEV6Rj7rCTgSajvY6wIaAQAAjWwlDsw,8733
|
71
|
-
goosebit/ui/static/js/devices.js,sha256=zseDgA1PnnreNajQRx7F38GuS_BjQugVtQLNPUhI3xg,13635
|
72
|
-
goosebit/ui/static/js/login.js,sha256=9qlXki3udm5CmAO8UXUJe94Kc_-SJekdKjfH9TzJhgk,579
|
73
|
-
goosebit/ui/static/js/logs.js,sha256=DgOWLKUvK-mM-oYRA0JJTIhtiCiegb5qCJ7pE9H2I3k,863
|
74
|
-
goosebit/ui/static/js/rollouts.js,sha256=bswZSdPvDGSctpxyw2qhnuslTPugXDqkIMVBMYiY-Gc,7484
|
75
|
-
goosebit/ui/static/js/software.js,sha256=3B9F6mLkvKhl0K1YJn4qkw5Wj4u_V9gabbz3ix84tDI,8728
|
76
|
-
goosebit/ui/static/js/util.js,sha256=GlF2eVL6iGUWEbBgxzmLX4r3x5bLYh1YiBN7okX9dVE,4593
|
77
|
-
goosebit/ui/static/svg/goosebit-logo.svg,sha256=8zvM7o2Ob3YmTEV6Rj7rCTgSajvY6wIaAQAAjWwlDsw,8733
|
78
|
-
goosebit/ui/templates/__init__.py,sha256=xHCio6TnuNnYKLOZ3VhvbxyJPZ2ddzTSle_RmiuLrrA,378
|
79
|
-
goosebit/ui/templates/devices.html.jinja,sha256=G2t3pKIe0e7LDkm9H0YwDddSvyXY4PaSPM11Rl9d1BY,5773
|
80
|
-
goosebit/ui/templates/login.html.jinja,sha256=eQX1ypZ5iJ2L4v4lw8VB7GKluUjUYmpoUyUy8jY98Og,3050
|
81
|
-
goosebit/ui/templates/logs.html.jinja,sha256=JLHogVHWcP_Y321dd-gHexNqix-PFfGVpEKbNmzsMU8,1310
|
82
|
-
goosebit/ui/templates/nav.html.jinja,sha256=cdAM-9RZG_taugisf9Sh-UcRT3jLHAqNGbf9r7mk8ow,5725
|
83
|
-
goosebit/ui/templates/rollouts.html.jinja,sha256=s9kIxRYO8OypqIQH8tQUwZJDDjsex89piuV4kh1eqwE,4728
|
84
|
-
goosebit/ui/templates/software.html.jinja,sha256=CTIeNkPMJMvtDVWbcKl_BjvtAFV3bskpCcNg-lCJIZM,7551
|
85
|
-
goosebit/updater/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH4KROTGd2S4AoI,41
|
86
|
-
goosebit/updater/controller/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH4KROTGd2S4AoI,41
|
87
|
-
goosebit/updater/controller/routes.py,sha256=8CnLb-kDuO-yFeWdu4apIyctCf9OzvJ011Az3QDGemU,123
|
88
|
-
goosebit/updater/controller/v1/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH4KROTGd2S4AoI,41
|
89
|
-
goosebit/updater/controller/v1/routes.py,sha256=lJFx3gi01iI7ep_omPg5pI4TSVRa13kqfFSeXKuGIpY,7273
|
90
|
-
goosebit/updater/controller/v1/schema.py,sha256=NwSyPx3A38iFabfOfzaVtxPozJQNacikP5WOhxMHqdg,1228
|
91
|
-
goosebit/updater/manager.py,sha256=b_OqH5TBHicd1cZNzvkwlgpjXTa5N4BOe6oqiNXgYLI,10692
|
92
|
-
goosebit/updater/routes.py,sha256=BYH8qdSUO99MQo09IQOkPNnmNZS74O5IZqIKcuNkNkY,647
|
93
|
-
goosebit/updates/__init__.py,sha256=RIq3m7x0-ByWjU21llKZSnj4a0Hg27iSNMfO1G4Kjwc,4284
|
94
|
-
goosebit/updates/swdesc.py,sha256=f2GxYwDvxgNsCTV_3RBlQ5oOWfB9cpNZuQBMMuZyZn8,3154
|
95
|
-
goosebit-0.2.5.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
96
|
-
goosebit-0.2.5.dist-info/METADATA,sha256=nHUxHj5Qy3KK8K542dcMbzTxSZ3wtTErq-OCGejBqz8,5801
|
97
|
-
goosebit-0.2.5.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
98
|
-
goosebit-0.2.5.dist-info/entry_points.txt,sha256=5p3wNB9_WEljksEBgZmOxO0DrBVhRxP20JD9JJ_lpb4,57
|
99
|
-
goosebit-0.2.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|