goosebit 0.2.4__py3-none-any.whl → 0.2.6__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.
Files changed (96) hide show
  1. goosebit/__init__.py +56 -6
  2. goosebit/api/telemetry/metrics.py +1 -5
  3. goosebit/api/v1/devices/device/responses.py +1 -0
  4. goosebit/api/v1/devices/device/routes.py +8 -8
  5. goosebit/api/v1/devices/requests.py +20 -0
  6. goosebit/api/v1/devices/routes.py +83 -8
  7. goosebit/api/v1/download/routes.py +14 -3
  8. goosebit/api/v1/rollouts/routes.py +5 -4
  9. goosebit/api/v1/routes.py +2 -1
  10. goosebit/api/v1/settings/routes.py +14 -0
  11. goosebit/api/v1/settings/users/__init__.py +1 -0
  12. goosebit/api/v1/settings/users/requests.py +16 -0
  13. goosebit/api/v1/settings/users/responses.py +7 -0
  14. goosebit/api/v1/settings/users/routes.py +56 -0
  15. goosebit/api/v1/software/routes.py +18 -14
  16. goosebit/auth/__init__.py +54 -14
  17. goosebit/auth/permissions.py +80 -0
  18. goosebit/db/config.py +57 -1
  19. goosebit/db/migrations/models/1_20241109151811_update.py +11 -0
  20. goosebit/db/migrations/models/2_20241121113728_update.py +11 -0
  21. goosebit/db/migrations/models/3_20241121140210_update.py +11 -0
  22. goosebit/db/migrations/models/4_20250324110331_update.py +16 -0
  23. goosebit/db/migrations/models/4_20250402085235_rename_uuid_to_id.py +11 -0
  24. goosebit/db/migrations/models/5_20250619090242_null_feed.py +83 -0
  25. goosebit/db/models.py +22 -7
  26. goosebit/db/pg_ssl_context.py +51 -0
  27. goosebit/device_manager.py +262 -0
  28. goosebit/plugins/__init__.py +32 -0
  29. goosebit/schema/devices.py +9 -6
  30. goosebit/schema/plugins.py +67 -0
  31. goosebit/schema/updates.py +15 -0
  32. goosebit/schema/users.py +9 -0
  33. goosebit/settings/__init__.py +0 -3
  34. goosebit/settings/schema.py +62 -14
  35. goosebit/storage/__init__.py +62 -0
  36. goosebit/storage/base.py +14 -0
  37. goosebit/storage/filesystem.py +111 -0
  38. goosebit/storage/s3.py +104 -0
  39. goosebit/ui/bff/common/columns.py +50 -0
  40. goosebit/ui/bff/common/requests.py +3 -15
  41. goosebit/ui/bff/common/responses.py +17 -0
  42. goosebit/ui/bff/devices/device/__init__.py +1 -0
  43. goosebit/ui/bff/devices/device/routes.py +17 -0
  44. goosebit/ui/bff/devices/requests.py +1 -0
  45. goosebit/ui/bff/devices/responses.py +6 -2
  46. goosebit/ui/bff/devices/routes.py +71 -17
  47. goosebit/ui/bff/download/routes.py +14 -3
  48. goosebit/ui/bff/rollouts/responses.py +6 -2
  49. goosebit/ui/bff/rollouts/routes.py +32 -4
  50. goosebit/ui/bff/routes.py +6 -3
  51. goosebit/ui/bff/settings/__init__.py +1 -0
  52. goosebit/ui/bff/settings/routes.py +20 -0
  53. goosebit/ui/bff/settings/users/__init__.py +1 -0
  54. goosebit/ui/bff/settings/users/responses.py +33 -0
  55. goosebit/ui/bff/settings/users/routes.py +80 -0
  56. goosebit/ui/bff/software/responses.py +19 -9
  57. goosebit/ui/bff/software/routes.py +40 -12
  58. goosebit/ui/nav.py +12 -2
  59. goosebit/ui/routes.py +70 -26
  60. goosebit/ui/static/js/devices.js +72 -80
  61. goosebit/ui/static/js/login.js +21 -5
  62. goosebit/ui/static/js/logs.js +7 -22
  63. goosebit/ui/static/js/rollouts.js +39 -35
  64. goosebit/ui/static/js/settings.js +322 -0
  65. goosebit/ui/static/js/setup.js +28 -0
  66. goosebit/ui/static/js/software.js +127 -127
  67. goosebit/ui/static/js/util.js +45 -4
  68. goosebit/ui/templates/__init__.py +10 -1
  69. goosebit/ui/templates/devices.html.jinja +0 -20
  70. goosebit/ui/templates/login.html.jinja +5 -0
  71. goosebit/ui/templates/nav.html.jinja +26 -7
  72. goosebit/ui/templates/rollouts.html.jinja +4 -22
  73. goosebit/ui/templates/settings.html.jinja +88 -0
  74. goosebit/ui/templates/setup.html.jinja +71 -0
  75. goosebit/ui/templates/software.html.jinja +0 -11
  76. goosebit/updater/controller/v1/routes.py +120 -72
  77. goosebit/updater/routes.py +86 -7
  78. goosebit/updates/__init__.py +24 -31
  79. goosebit/updates/swdesc.py +15 -8
  80. goosebit/users/__init__.py +63 -0
  81. goosebit/util/__init__.py +0 -0
  82. goosebit/util/path.py +42 -0
  83. goosebit/util/version.py +92 -0
  84. goosebit-0.2.6.dist-info/METADATA +280 -0
  85. goosebit-0.2.6.dist-info/RECORD +133 -0
  86. {goosebit-0.2.4.dist-info → goosebit-0.2.6.dist-info}/WHEEL +1 -1
  87. goosebit-0.2.6.dist-info/entry_points.txt +3 -0
  88. goosebit/realtime/logs.py +0 -42
  89. goosebit/realtime/routes.py +0 -13
  90. goosebit/ui/static/js/index.js +0 -155
  91. goosebit/ui/templates/index.html.jinja +0 -25
  92. goosebit/updater/manager.py +0 -357
  93. goosebit-0.2.4.dist-info/METADATA +0 -181
  94. goosebit-0.2.4.dist-info/RECORD +0 -98
  95. /goosebit/{realtime → api/v1/settings}/__init__.py +0 -0
  96. {goosebit-0.2.4.dist-info → goosebit-0.2.6.dist-info}/LICENSE +0 -0
@@ -1,155 +0,0 @@
1
- let dataTable;
2
-
3
- document.addEventListener("DOMContentLoaded", () => {
4
- dataTable = new DataTable("#device-table", {
5
- responsive: true,
6
- paging: true,
7
- processing: false,
8
- serverSide: true,
9
- scrollCollapse: true,
10
- scroller: true,
11
- scrollY: "65vh",
12
- stateSave: true,
13
- stateLoadParams: (settings, data) => {
14
- // if save state is older than last breaking code change...
15
- if (data.time <= 1722434386000) {
16
- // ... delete it
17
- for (const key of Object.keys(data)) {
18
- delete data[key];
19
- }
20
- }
21
- },
22
- ajax: {
23
- url: "/ui/bff/devices",
24
- contentType: "application/json",
25
- },
26
- initComplete: () => {
27
- updateBtnState();
28
- },
29
- columnDefs: [
30
- {
31
- targets: "_all",
32
- searchable: false,
33
- orderable: false,
34
- render: (data) => data || "-",
35
- },
36
- ],
37
- columns: [
38
- { data: "name", searchable: true, orderable: true },
39
- {
40
- data: "online",
41
- render: (data, type) => {
42
- if (type === "display" || type === "filter") {
43
- const color = data ? "success" : "danger";
44
- return `
45
- <div class="text-${color}">
46
-
47
- </div>
48
- `;
49
- }
50
- return data;
51
- },
52
- },
53
- { data: "uuid", searchable: true, orderable: true },
54
- { data: "sw_version", searchable: true, orderable: true },
55
- {
56
- data: "progress",
57
- render: (data, type) => {
58
- if (type === "display" || type === "filter") {
59
- return data ? `${data}%` : "-";
60
- }
61
- return data;
62
- },
63
- },
64
- { data: "last_ip" },
65
- {
66
- data: "last_seen",
67
- render: (data, type) => {
68
- if (type === "display" || type === "filter") {
69
- return secondsToRecentDate(data);
70
- }
71
- return data;
72
- },
73
- },
74
- ],
75
- select: true,
76
- rowId: "uuid",
77
- layout: {
78
- top1Start: {
79
- buttons: [
80
- {
81
- text: '<i class="bi bi-check-all"></i>',
82
- extend: "selectAll",
83
- titleAttr: "Select All",
84
- },
85
- {
86
- text: '<i class="bi bi-x"></i>',
87
- extend: "selectNone",
88
- titleAttr: "Clear Selection",
89
- },
90
- {
91
- text: '<i class="bi bi-file-earmark-arrow-down"></i>',
92
- action: (e, dt) => {
93
- const selectedDevices = dt.rows({ selected: true }).data().toArray();
94
- downloadLogins(selectedDevices);
95
- },
96
- className: "buttons-export-login",
97
- titleAttr: "Export Login",
98
- },
99
- {
100
- text: '<i class="bi bi-file-text"></i>',
101
- action: (e, dt) => {
102
- const selectedDevice = dt.rows({ selected: true }).data().toArray()[0];
103
- window.location.href = `/ui/logs/${selectedDevice.uuid}`;
104
- },
105
- className: "buttons-logs",
106
- titleAttr: "View Log",
107
- },
108
- ],
109
- },
110
- },
111
- });
112
-
113
- dataTable
114
- .on("select", () => {
115
- updateBtnState();
116
- })
117
- .on("deselect", () => {
118
- updateBtnState();
119
- });
120
-
121
- setInterval(() => {
122
- dataTable.ajax.reload(null, false);
123
- }, TABLE_UPDATE_TIME);
124
- });
125
-
126
- function updateBtnState() {
127
- if (dataTable.rows({ selected: true }).any()) {
128
- document.querySelector("button.buttons-select-none").classList.remove("disabled");
129
- document.querySelector("button.buttons-export-login").classList.remove("disabled");
130
- } else {
131
- document.querySelector("button.buttons-select-none").classList.add("disabled");
132
- document.querySelector("button.buttons-export-login").classList.add("disabled");
133
- }
134
- if (dataTable.rows({ selected: true }).count() === 1) {
135
- document.querySelector("button.buttons-logs").classList.remove("disabled");
136
- } else {
137
- document.querySelector("button.buttons-logs").classList.add("disabled");
138
- }
139
- }
140
-
141
- function downloadLogins(devices) {
142
- const deviceLogins = devices.map((dev) => {
143
- return [dev.name, `https://${dev.uuid}-access.loadsync.io`, dev.uuid];
144
- });
145
- deviceLogins.unshift(["Building", "Access Link", "Serial Number/Wifi SSID", "Login/Wifi Password"]);
146
-
147
- const csvContent = `data:text/csv;charset=utf-8,${deviceLogins.map((e) => e.join(",")).join("\n")}`;
148
- const encodedUri = encodeURI(csvContent);
149
- const link = document.createElement("a");
150
- link.setAttribute("href", encodedUri);
151
- link.setAttribute("download", "LoadsyncLogins-Export.csv");
152
- document.body.appendChild(link);
153
-
154
- link.click();
155
- }
@@ -1,25 +0,0 @@
1
- {% extends "nav.html.jinja" %}
2
- {% block content %}
3
- <div class="container-fluid">
4
- <div class="row p-2 d-flex justify-content-center">
5
- <div class="col">
6
- <table id="device-table" class="table table-hover">
7
- <thead>
8
- <tr>
9
- <th>Name</th>
10
- <th>Up</th>
11
- <th>UUID</th>
12
- <th>Software</th>
13
- <th>Progress</th>
14
- <th>Last IP</th>
15
- <th>Last Seen</th>
16
- </tr>
17
- </thead>
18
- <tbody id="devices-list">
19
- </tbody>
20
- </table>
21
- </div>
22
- </div>
23
- </div>
24
- <script src="{{ url_for('static', path='js/index.js') }}"></script>
25
- {% endblock content %}
@@ -1,357 +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:
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 update_log_complete(self, log_complete: bool):
78
- return
79
-
80
- async def get_rollout(self) -> Rollout | None:
81
- return None
82
-
83
- @asynccontextmanager
84
- async def subscribe_log(self, callback: Callable):
85
- device = await self.get_device()
86
- subscribers = self.log_subscribers
87
- subscribers.append(callback)
88
- self.log_subscribers = subscribers
89
- if device is not None:
90
- await callback(device.last_log)
91
- try:
92
- yield
93
- except asyncio.CancelledError:
94
- pass
95
- finally:
96
- subscribers = self.log_subscribers
97
- subscribers.remove(callback)
98
- self.log_subscribers = subscribers
99
-
100
- @property
101
- def poll_seconds(self):
102
- time_obj = datetime.strptime(self.poll_time, "%H:%M:%S")
103
- return time_obj.hour * 3600 + time_obj.minute * 60 + time_obj.second
104
-
105
- @property
106
- def log_subscribers(self):
107
- return UpdateManager.device_log_subscriptions.get(self.dev_id, [])
108
-
109
- @log_subscribers.setter
110
- def log_subscribers(self, value: list):
111
- UpdateManager.device_log_subscriptions[self.dev_id] = value
112
-
113
- @property
114
- def poll_time(self):
115
- return UpdateManager.device_poll_time.get(self.dev_id, config.poll_time_default)
116
-
117
- @poll_time.setter
118
- def poll_time(self, value: str):
119
- if not value == config.poll_time_default:
120
- UpdateManager.device_poll_time[self.dev_id] = value
121
- return
122
- if self.dev_id in UpdateManager.device_poll_time:
123
- del UpdateManager.device_poll_time[self.dev_id]
124
-
125
- async def publish_log(self, log_data: str | None):
126
- for cb in self.log_subscribers:
127
- await cb(log_data)
128
-
129
- @abstractmethod
130
- async def get_update(self) -> tuple[HandlingType, Software | None]: ...
131
-
132
- @abstractmethod
133
- async def update_log(self, log_data: str) -> None: ...
134
-
135
-
136
- class UnknownUpdateManager(UpdateManager):
137
- def __init__(self, dev_id: str):
138
- super().__init__(dev_id)
139
- self.poll_time = config.poll_time_updating
140
-
141
- async def _get_software(self) -> Software | None:
142
- device = await self.get_device()
143
- if device is None:
144
- return None
145
- return await Software.latest(device)
146
-
147
- async def get_update(self) -> tuple[HandlingType, Software | None]:
148
- software = await self._get_software()
149
- if software is None:
150
- return HandlingType.SKIP, None
151
- return HandlingType.FORCED, software
152
-
153
- async def update_log(self, log_data: str) -> None:
154
- return
155
-
156
-
157
- class DeviceUpdateManager(UpdateManager):
158
- hardware_default = None
159
-
160
- @cached(key_builder=lambda fn, self: self.dev_id, alias="default")
161
- async def get_device(self) -> Device:
162
- hardware = DeviceUpdateManager.hardware_default
163
- if hardware is None:
164
- hardware = (await Hardware.get_or_create(model="default", revision="default"))[0]
165
- DeviceUpdateManager.hardware_default = hardware
166
-
167
- return (await Device.get_or_create(uuid=self.dev_id, defaults={"hardware": hardware}))[0]
168
-
169
- async def save_device(self, device: Device, update_fields: list[str]):
170
- await device.save(update_fields=update_fields)
171
-
172
- # only update cache after a successful database save
173
- result = await caches.get("default").set(self.dev_id, device, ttl=600)
174
- assert result, "device being cached"
175
-
176
- async def update_force_update(self, force_update: bool) -> None:
177
- device = await self.get_device()
178
- device.force_update = force_update
179
- await self.save_device(device, update_fields=["force_update"])
180
-
181
- async def update_sw_version(self, version: str) -> None:
182
- device = await self.get_device()
183
- device.sw_version = version
184
- await self.save_device(device, update_fields=["sw_version"])
185
-
186
- async def update_hardware(self, hardware: Hardware) -> None:
187
- device = await self.get_device()
188
- device.hardware = hardware
189
- await self.save_device(device, update_fields=["hardware"])
190
-
191
- async def update_device_state(self, state: UpdateStateEnum) -> None:
192
- device = await self.get_device()
193
- device.last_state = state
194
- await self.save_device(device, update_fields=["last_state"])
195
-
196
- async def update_last_connection(self, last_seen: int, last_ip: str) -> None:
197
- device = await self.get_device()
198
- device.last_seen = last_seen
199
- if ":" in last_ip:
200
- device.last_ipv6 = last_ip
201
- await self.save_device(device, update_fields=["last_seen", "last_ipv6"])
202
- else:
203
- device.last_ip = last_ip
204
- await self.save_device(device, update_fields=["last_seen", "last_ip"])
205
-
206
- async def update_update(self, update_mode: UpdateModeEnum, software: Software | None):
207
- device = await self.get_device()
208
- device.assigned_software = software
209
- device.update_mode = update_mode
210
- await self.save_device(device, update_fields=["assigned_software_id", "update_mode"])
211
-
212
- async def update_name(self, name: str):
213
- device = await self.get_device()
214
- device.name = name
215
- await self.save_device(device, update_fields=["name"])
216
-
217
- async def update_feed(self, feed: str):
218
- device = await self.get_device()
219
- device.feed = feed
220
- await self.save_device(device, update_fields=["feed"])
221
-
222
- async def update_config_data(self, **kwargs):
223
- model = kwargs.get("hw_boardname") or "default"
224
- revision = kwargs.get("hw_revision") or "default"
225
- sw_version = kwargs.get("sw_version")
226
-
227
- hardware = (await Hardware.get_or_create(model=model, revision=revision))[0]
228
- device = await self.get_device()
229
- modified = False
230
-
231
- if device.hardware != hardware:
232
- device.hardware = hardware
233
- modified = True
234
-
235
- if device.last_state == UpdateStateEnum.UNKNOWN:
236
- device.last_state = UpdateStateEnum.REGISTERED
237
- modified = True
238
-
239
- if device.sw_version != sw_version:
240
- device.sw_version = sw_version
241
- modified = True
242
-
243
- if modified:
244
- await self.save_device(device, update_fields=["hardware_id", "last_state", "sw_version"])
245
-
246
- async def update_log_complete(self, log_complete: bool):
247
- device = await self.get_device()
248
- device.log_complete = log_complete
249
- await self.save_device(device, update_fields=["log_complete"])
250
-
251
- async def get_rollout(self) -> Rollout | None:
252
- device = await self.get_device()
253
-
254
- if device.update_mode == UpdateModeEnum.ROLLOUT:
255
- return (
256
- await Rollout.filter(
257
- feed=device.feed,
258
- paused=False,
259
- software__compatibility__devices__uuid=device.uuid,
260
- )
261
- .order_by("-created_at")
262
- .first()
263
- .prefetch_related("software")
264
- )
265
-
266
- return None
267
-
268
- async def _get_software(self) -> Software | None:
269
- device = await self.get_device()
270
-
271
- if device.update_mode == UpdateModeEnum.ROLLOUT:
272
- rollout = await self.get_rollout()
273
- if not rollout or rollout.paused:
274
- return None
275
- await rollout.fetch_related("software")
276
- return rollout.software
277
- if device.update_mode == UpdateModeEnum.ASSIGNED:
278
- await device.fetch_related("assigned_software")
279
- return device.assigned_software
280
-
281
- if device.update_mode == UpdateModeEnum.LATEST:
282
- return await Software.latest(device)
283
-
284
- assert device.update_mode == UpdateModeEnum.PINNED
285
- return None
286
-
287
- async def get_update(self) -> tuple[HandlingType, Software | None]:
288
- device = await self.get_device()
289
- software = await self._get_software()
290
-
291
- if software is None:
292
- handling_type = HandlingType.SKIP
293
- self.poll_time = config.poll_time_default
294
-
295
- elif software.version == device.sw_version and not device.force_update:
296
- handling_type = HandlingType.SKIP
297
- self.poll_time = config.poll_time_default
298
-
299
- elif device.last_state == UpdateStateEnum.ERROR and not device.force_update:
300
- handling_type = HandlingType.SKIP
301
- self.poll_time = config.poll_time_default
302
-
303
- else:
304
- handling_type = HandlingType.FORCED
305
- self.poll_time = config.poll_time_updating
306
-
307
- if device.log_complete:
308
- await self.update_log_complete(False)
309
- await self.clear_log()
310
-
311
- return handling_type, software
312
-
313
- async def update_log(self, log_data: str) -> None:
314
- if log_data is None:
315
- return
316
- device = await self.get_device()
317
-
318
- if device.last_log is None:
319
- device.last_log = ""
320
-
321
- matches = re.findall(r"Downloaded (\d+)%", log_data)
322
- if matches:
323
- device.progress = matches[-1]
324
-
325
- if log_data.startswith("Installing Update Chunk Artifacts."):
326
- # clear log
327
- device.last_log = ""
328
- await self.publish_log(None)
329
-
330
- if not log_data == "Skipped Update.":
331
- device.last_log += f"{log_data}\n"
332
- await self.publish_log(f"{log_data}\n")
333
-
334
- await self.save_device(
335
- device,
336
- update_fields=["progress", "last_log"],
337
- )
338
-
339
- async def clear_log(self) -> None:
340
- device = await self.get_device()
341
- device.last_log = ""
342
- await self.save_device(device, update_fields=["last_log"])
343
- await self.publish_log(None)
344
-
345
-
346
- async def get_update_manager(dev_id: str) -> UpdateManager:
347
- if dev_id == "unknown":
348
- return UnknownUpdateManager("unknown")
349
- else:
350
- return DeviceUpdateManager(dev_id)
351
-
352
-
353
- async def delete_devices(ids: list[str]):
354
- await Device.filter(uuid__in=ids).delete()
355
- for dev_id in ids:
356
- result = await caches.get("default").delete(dev_id)
357
- assert result == 1, "device has been cached"