goosebit 0.2.4__py3-none-any.whl → 0.2.5__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 +16 -0
- goosebit/api/v1/devices/device/routes.py +2 -2
- goosebit/api/v1/devices/routes.py +19 -4
- goosebit/auth/__init__.py +5 -1
- goosebit/db/migrations/models/1_20241109151811_update.py +11 -0
- goosebit/db/models.py +6 -2
- goosebit/realtime/logs.py +1 -1
- goosebit/schema/devices.py +1 -1
- goosebit/settings/schema.py +2 -0
- goosebit/ui/bff/common/requests.py +3 -15
- goosebit/ui/bff/common/responses.py +16 -0
- goosebit/ui/bff/devices/responses.py +6 -2
- goosebit/ui/bff/devices/routes.py +53 -2
- goosebit/ui/bff/rollouts/responses.py +6 -2
- goosebit/ui/bff/routes.py +4 -2
- goosebit/ui/bff/software/responses.py +19 -9
- goosebit/ui/routes.py +7 -16
- goosebit/ui/static/js/devices.js +53 -69
- goosebit/ui/static/js/rollouts.js +16 -13
- goosebit/ui/static/js/software.js +5 -11
- goosebit/ui/static/js/util.js +21 -1
- goosebit/ui/templates/devices.html.jinja +0 -20
- goosebit/ui/templates/nav.html.jinja +13 -2
- goosebit/updater/controller/v1/routes.py +26 -20
- goosebit/updater/manager.py +20 -52
- goosebit/updater/routes.py +6 -2
- goosebit/updates/swdesc.py +1 -1
- {goosebit-0.2.4.dist-info → goosebit-0.2.5.dist-info}/METADATA +14 -6
- {goosebit-0.2.4.dist-info → goosebit-0.2.5.dist-info}/RECORD +32 -31
- {goosebit-0.2.4.dist-info → goosebit-0.2.5.dist-info}/WHEEL +1 -1
- goosebit-0.2.5.dist-info/entry_points.txt +3 -0
- goosebit/ui/static/js/index.js +0 -155
- goosebit/ui/templates/index.html.jinja +0 -25
- {goosebit-0.2.4.dist-info → goosebit-0.2.5.dist-info}/LICENSE +0 -0
@@ -6,24 +6,22 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
6
6
|
paging: true,
|
7
7
|
processing: true,
|
8
8
|
serverSide: true,
|
9
|
-
order:
|
9
|
+
order: {
|
10
|
+
name: "created_at",
|
11
|
+
dir: "desc",
|
12
|
+
},
|
10
13
|
scrollCollapse: true,
|
11
14
|
scroller: true,
|
12
15
|
scrollY: "65vh",
|
13
16
|
stateSave: true,
|
14
|
-
stateLoadParams: (settings, data) => {
|
15
|
-
// if save state is older than last breaking code change...
|
16
|
-
if (data.time <= 1722413708000) {
|
17
|
-
// ... delete it
|
18
|
-
for (const key of Object.keys(data)) {
|
19
|
-
delete data[key];
|
20
|
-
}
|
21
|
-
}
|
22
|
-
},
|
23
17
|
select: true,
|
24
18
|
rowId: "id",
|
25
19
|
ajax: {
|
26
20
|
url: "/ui/bff/rollouts",
|
21
|
+
data: (data) => {
|
22
|
+
// biome-ignore lint/performance/noDelete: really has to be deleted
|
23
|
+
delete data.columns;
|
24
|
+
},
|
27
25
|
contentType: "application/json",
|
28
26
|
},
|
29
27
|
initComplete: () => {
|
@@ -38,9 +36,14 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
38
36
|
],
|
39
37
|
columns: [
|
40
38
|
{ data: "id", visible: false },
|
41
|
-
{
|
42
|
-
|
43
|
-
|
39
|
+
{
|
40
|
+
data: "created_at",
|
41
|
+
name: "created_at",
|
42
|
+
orderable: true,
|
43
|
+
render: (data) => new Date(data).toLocaleString(),
|
44
|
+
},
|
45
|
+
{ data: "name", name: "name", searchable: true, orderable: true },
|
46
|
+
{ data: "feed", name: "feed", searchable: true, orderable: true },
|
44
47
|
{ data: "sw_file" },
|
45
48
|
{ data: "sw_version" },
|
46
49
|
{
|
@@ -172,22 +172,16 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
172
172
|
paging: true,
|
173
173
|
processing: false,
|
174
174
|
serverSide: true,
|
175
|
-
order: [2, "desc"],
|
176
175
|
scrollCollapse: true,
|
177
176
|
scroller: true,
|
178
177
|
scrollY: "60vh",
|
179
178
|
stateSave: true,
|
180
|
-
stateLoadParams: (settings, data) => {
|
181
|
-
// if save state is older than last breaking code change...
|
182
|
-
if (data.time <= 1722415428000) {
|
183
|
-
// ... delete it
|
184
|
-
for (const key of Object.keys(data)) {
|
185
|
-
delete data[key];
|
186
|
-
}
|
187
|
-
}
|
188
|
-
},
|
189
179
|
ajax: {
|
190
180
|
url: "/ui/bff/software",
|
181
|
+
data: (data) => {
|
182
|
+
// biome-ignore lint/performance/noDelete: really has to be deleted
|
183
|
+
delete data.columns;
|
184
|
+
},
|
191
185
|
contentType: "application/json",
|
192
186
|
},
|
193
187
|
initComplete: () => {
|
@@ -204,7 +198,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
204
198
|
columns: [
|
205
199
|
{ data: "id", visible: false },
|
206
200
|
{ data: "name" },
|
207
|
-
{ data: "version", searchable: true, orderable: true },
|
201
|
+
{ data: "version", name: "version", searchable: true, orderable: true },
|
208
202
|
{
|
209
203
|
data: "compatibility",
|
210
204
|
render: (data) => {
|
goosebit/ui/static/js/util.js
CHANGED
@@ -22,7 +22,7 @@ function secondsToRecentDate(t) {
|
|
22
22
|
|
23
23
|
async function updateSoftwareSelection(devices = null) {
|
24
24
|
try {
|
25
|
-
const url = new URL("/ui/bff/software", window.location.origin);
|
25
|
+
const url = new URL("/ui/bff/software?order[0][dir]=desc&order[0][name]=version", window.location.origin);
|
26
26
|
if (devices != null) {
|
27
27
|
for (const device of devices) {
|
28
28
|
url.searchParams.append("uuids", device.uuid);
|
@@ -64,6 +64,26 @@ async function updateSoftwareSelection(devices = null) {
|
|
64
64
|
}
|
65
65
|
}
|
66
66
|
|
67
|
+
async function get_request(url) {
|
68
|
+
const response = await fetch(url, {
|
69
|
+
method: "GET",
|
70
|
+
});
|
71
|
+
|
72
|
+
const result = await response.json();
|
73
|
+
if (!response.ok) {
|
74
|
+
if (result.detail) {
|
75
|
+
Swal.fire({
|
76
|
+
title: "Warning",
|
77
|
+
text: result.detail,
|
78
|
+
icon: "warning",
|
79
|
+
confirmButtonText: "Understood",
|
80
|
+
});
|
81
|
+
}
|
82
|
+
|
83
|
+
throw new Error(`POST ${url} failed for ${JSON.stringify(object)}`);
|
84
|
+
}
|
85
|
+
return result;
|
86
|
+
}
|
67
87
|
async function post_request(url, object) {
|
68
88
|
const response = await fetch(url, {
|
69
89
|
method: "POST",
|
@@ -4,26 +4,6 @@
|
|
4
4
|
<div class="row p-2 d-flex justify-content-center">
|
5
5
|
<div class="col">
|
6
6
|
<table id="device-table" class="table table-hover">
|
7
|
-
<thead>
|
8
|
-
<tr>
|
9
|
-
<th>Up</th>
|
10
|
-
<th>UUID</th>
|
11
|
-
<th>Name</th>
|
12
|
-
<th>Model</th>
|
13
|
-
<th>Revision</th>
|
14
|
-
<th>Feed</th>
|
15
|
-
<th>Installed Software</th>
|
16
|
-
<th>Target Software</th>
|
17
|
-
<th>Update Mode</th>
|
18
|
-
<th>State</th>
|
19
|
-
<th>Force Update</th>
|
20
|
-
<th>Progress</th>
|
21
|
-
<th>Last IP</th>
|
22
|
-
<th>Last Seen</th>
|
23
|
-
</tr>
|
24
|
-
</thead>
|
25
|
-
<tbody id="devices-list">
|
26
|
-
</tbody>
|
27
7
|
</table>
|
28
8
|
</div>
|
29
9
|
</div>
|
@@ -60,13 +60,24 @@
|
|
60
60
|
box-shadow: none!important;
|
61
61
|
}
|
62
62
|
</style>
|
63
|
-
<script>
|
63
|
+
<script>
|
64
|
+
const TABLE_UPDATE_TIME = 3000;
|
65
|
+
DataTable.ext.errMode = function ( e, settings, helpPage, message ) {
|
66
|
+
if (e.jqXHR.status == 401) {
|
67
|
+
window.location.reload();
|
68
|
+
} else if (e.jqXHR.status >= 200 && e.jqXHR.status < 300) {
|
69
|
+
console.error("AJAX query error when reloading datatable");
|
70
|
+
}
|
71
|
+
console.error("Unknown error when reloading datatable");
|
72
|
+
};
|
73
|
+
|
74
|
+
</script>
|
64
75
|
<script src="{{ url_for('static', path='js/util.js') }}"></script>
|
65
76
|
</head>
|
66
77
|
<body data-bs-theme="dark">
|
67
78
|
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
68
79
|
<div class="container-fluid">
|
69
|
-
<a class="navbar-brand" href="{{ request.url_for('
|
80
|
+
<a class="navbar-brand" href="{{ request.url_for('devices_ui') }}">
|
70
81
|
<img src="{{ request.url_for('static', path='svg/goosebit-logo.svg') }}"
|
71
82
|
class="me-2"
|
72
83
|
height="30px"
|
@@ -2,7 +2,7 @@ import logging
|
|
2
2
|
|
3
3
|
from fastapi import APIRouter, Depends, HTTPException
|
4
4
|
from fastapi.requests import Request
|
5
|
-
from fastapi.responses import FileResponse,
|
5
|
+
from fastapi.responses import FileResponse, Response
|
6
6
|
|
7
7
|
from goosebit.db.models import Software, UpdateStateEnum
|
8
8
|
from goosebit.settings import config
|
@@ -25,7 +25,6 @@ router = APIRouter(prefix="/v1")
|
|
25
25
|
async def polling(request: Request, dev_id: str, updater: UpdateManager = Depends(get_update_manager)):
|
26
26
|
links: dict[str, dict[str, str]] = {}
|
27
27
|
|
28
|
-
sleep = updater.poll_time
|
29
28
|
device = await updater.get_device()
|
30
29
|
|
31
30
|
if device is None:
|
@@ -45,14 +44,15 @@ async def polling(request: Request, dev_id: str, updater: UpdateManager = Depend
|
|
45
44
|
logger.info(f"Skip: registration required, device={updater.dev_id}")
|
46
45
|
|
47
46
|
elif device.last_state == UpdateStateEnum.ERROR and not device.force_update:
|
48
|
-
|
49
|
-
|
47
|
+
sleep = config.poll_time_default
|
48
|
+
logger.info(f"Skip: device in error state, device={updater.dev_id}")
|
50
49
|
|
51
50
|
else:
|
52
51
|
# provide update if available. Note: this is also required while in state "running", otherwise swupdate
|
53
52
|
# won't confirm a successful testing (might be a bug/problem in swupdate)
|
54
53
|
handling_type, software = await updater.get_update()
|
55
54
|
if handling_type != HandlingType.SKIP and software is not None:
|
55
|
+
sleep = config.poll_time_updating
|
56
56
|
links["deploymentBase"] = {
|
57
57
|
"href": str(
|
58
58
|
request.url_for(
|
@@ -63,6 +63,11 @@ async def polling(request: Request, dev_id: str, updater: UpdateManager = Depend
|
|
63
63
|
)
|
64
64
|
}
|
65
65
|
logger.info(f"Forced: update available, device={updater.dev_id}")
|
66
|
+
else:
|
67
|
+
sleep = config.poll_time_default
|
68
|
+
|
69
|
+
# update poll time on manager so that UI can properly display if device is overdue
|
70
|
+
updater.poll_time = sleep
|
66
71
|
|
67
72
|
return {
|
68
73
|
"config": {"polling": {"sleep": sleep}},
|
@@ -102,54 +107,55 @@ async def deployment_feedback(
|
|
102
107
|
_: Request, data: FeedbackSchema, action_id: int, updater: UpdateManager = Depends(get_update_manager)
|
103
108
|
):
|
104
109
|
if data.status.execution == FeedbackStatusExecutionState.PROCEEDING:
|
105
|
-
await updater.
|
110
|
+
device = await updater.get_device()
|
111
|
+
if device and device.last_state != UpdateStateEnum.RUNNING:
|
112
|
+
await updater.clear_log()
|
113
|
+
await updater.update_device_state(UpdateStateEnum.RUNNING)
|
114
|
+
|
106
115
|
logger.debug(f"Installation in progress, device={updater.dev_id}")
|
107
116
|
|
108
117
|
elif data.status.execution == FeedbackStatusExecutionState.CLOSED:
|
109
118
|
await updater.update_force_update(False)
|
110
|
-
await updater.update_log_complete(True)
|
111
119
|
|
112
120
|
reported_software = await Software.get_or_none(id=action_id)
|
113
121
|
|
114
122
|
# From hawkBit docu: DDI defines also a status NONE which will not be interpreted by the update server
|
115
123
|
# and handled like SUCCESS.
|
116
124
|
if data.status.result.finished in [FeedbackStatusResultFinished.SUCCESS, FeedbackStatusResultFinished.NONE]:
|
125
|
+
await updater.deployment_action_success()
|
117
126
|
await updater.update_device_state(UpdateStateEnum.FINISHED)
|
118
127
|
|
119
|
-
# not guaranteed to be the correct rollout - see next comment.
|
120
128
|
rollout = await updater.get_rollout()
|
121
129
|
if rollout:
|
122
130
|
if rollout.software == reported_software:
|
123
131
|
rollout.success_count += 1
|
124
132
|
await rollout.save()
|
125
133
|
else:
|
134
|
+
# edge case where device update mode got changed while update was running
|
126
135
|
logging.warning(
|
127
136
|
f"Updating rollout success stats failed, software={reported_software.id}, device={updater.dev_id}" # noqa: E501
|
128
137
|
)
|
129
138
|
|
130
|
-
# setting the currently installed version based on the current assigned software / existing rollouts
|
131
|
-
# is problematic. Better to assign custom action_id for each update (rollout id? software id? new id?).
|
132
|
-
# Alternatively - but requires customization on the gateway side - use version reported by the gateway.
|
133
139
|
await updater.update_sw_version(reported_software.version)
|
134
140
|
logger.debug(f"Installation successful, software={reported_software.version}, device={updater.dev_id}")
|
135
141
|
|
136
142
|
elif data.status.result.finished == FeedbackStatusResultFinished.FAILURE:
|
137
143
|
await updater.update_device_state(UpdateStateEnum.ERROR)
|
138
144
|
|
139
|
-
# not guaranteed to be the correct rollout - see comment above.
|
140
145
|
rollout = await updater.get_rollout()
|
141
146
|
if rollout:
|
142
147
|
if rollout.software == reported_software:
|
143
148
|
rollout.failure_count += 1
|
144
149
|
await rollout.save()
|
145
150
|
else:
|
151
|
+
# edge case where device update mode got changed while update was running
|
146
152
|
logging.warning(
|
147
153
|
f"Updating rollout failure stats failed, software={reported_software.id}, device={updater.dev_id}" # noqa: E501
|
148
154
|
)
|
149
155
|
|
150
156
|
logger.debug(f"Installation failed, software={reported_software.version}, device={updater.dev_id}")
|
151
157
|
else:
|
152
|
-
logging.
|
158
|
+
logging.error(
|
153
159
|
f"Device reported unhandled execution state, state={data.status.execution}, device={updater.dev_id}"
|
154
160
|
)
|
155
161
|
|
@@ -179,11 +185,11 @@ async def download_artifact(_: Request, updater: UpdateManager = Depends(get_upd
|
|
179
185
|
_, software = await updater.get_update()
|
180
186
|
if software is None:
|
181
187
|
raise HTTPException(404)
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
188
|
+
|
189
|
+
assert software.local, "device requests local software to download"
|
190
|
+
|
191
|
+
return FileResponse(
|
192
|
+
software.path,
|
193
|
+
media_type="application/octet-stream",
|
194
|
+
filename=software.path.name,
|
195
|
+
)
|
goosebit/updater/manager.py
CHANGED
@@ -59,7 +59,7 @@ class UpdateManager(ABC):
|
|
59
59
|
async def update_device_state(self, state: UpdateStateEnum) -> None:
|
60
60
|
return
|
61
61
|
|
62
|
-
async def update_last_connection(self, last_seen: int, last_ip: str) -> None:
|
62
|
+
async def update_last_connection(self, last_seen: int, last_ip: str | None = None) -> None:
|
63
63
|
return
|
64
64
|
|
65
65
|
async def update_update(self, update_mode: UpdateModeEnum, software: Software | None):
|
@@ -74,7 +74,10 @@ class UpdateManager(ABC):
|
|
74
74
|
async def update_config_data(self, **kwargs):
|
75
75
|
return
|
76
76
|
|
77
|
-
async def
|
77
|
+
async def deployment_action_success(self):
|
78
|
+
return
|
79
|
+
|
80
|
+
async def clear_log(self) -> None:
|
78
81
|
return
|
79
82
|
|
80
83
|
async def get_rollout(self) -> Rollout | None:
|
@@ -83,9 +86,11 @@ class UpdateManager(ABC):
|
|
83
86
|
@asynccontextmanager
|
84
87
|
async def subscribe_log(self, callback: Callable):
|
85
88
|
device = await self.get_device()
|
89
|
+
# do not modify, breaks when combined
|
86
90
|
subscribers = self.log_subscribers
|
87
91
|
subscribers.append(callback)
|
88
92
|
self.log_subscribers = subscribers
|
93
|
+
|
89
94
|
if device is not None:
|
90
95
|
await callback(device.last_log)
|
91
96
|
try:
|
@@ -93,6 +98,7 @@ class UpdateManager(ABC):
|
|
93
98
|
except asyncio.CancelledError:
|
94
99
|
pass
|
95
100
|
finally:
|
101
|
+
# do not modify, breaks when combined
|
96
102
|
subscribers = self.log_subscribers
|
97
103
|
subscribers.remove(callback)
|
98
104
|
self.log_subscribers = subscribers
|
@@ -133,27 +139,6 @@ class UpdateManager(ABC):
|
|
133
139
|
async def update_log(self, log_data: str) -> None: ...
|
134
140
|
|
135
141
|
|
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
142
|
class DeviceUpdateManager(UpdateManager):
|
158
143
|
hardware_default = None
|
159
144
|
|
@@ -193,10 +178,12 @@ class DeviceUpdateManager(UpdateManager):
|
|
193
178
|
device.last_state = state
|
194
179
|
await self.save_device(device, update_fields=["last_state"])
|
195
180
|
|
196
|
-
async def update_last_connection(self, last_seen: int, last_ip: str) -> None:
|
181
|
+
async def update_last_connection(self, last_seen: int, last_ip: str | None = None) -> None:
|
197
182
|
device = await self.get_device()
|
198
183
|
device.last_seen = last_seen
|
199
|
-
if
|
184
|
+
if last_ip is None:
|
185
|
+
await self.save_device(device, update_fields=["last_seen"])
|
186
|
+
elif ":" in last_ip:
|
200
187
|
device.last_ipv6 = last_ip
|
201
188
|
await self.save_device(device, update_fields=["last_seen", "last_ipv6"])
|
202
189
|
else:
|
@@ -243,10 +230,10 @@ class DeviceUpdateManager(UpdateManager):
|
|
243
230
|
if modified:
|
244
231
|
await self.save_device(device, update_fields=["hardware_id", "last_state", "sw_version"])
|
245
232
|
|
246
|
-
async def
|
233
|
+
async def deployment_action_success(self):
|
247
234
|
device = await self.get_device()
|
248
|
-
device.
|
249
|
-
await self.save_device(device, update_fields=["
|
235
|
+
device.progress = 100
|
236
|
+
await self.save_device(device, update_fields=["progress"])
|
250
237
|
|
251
238
|
async def get_rollout(self) -> Rollout | None:
|
252
239
|
device = await self.get_device()
|
@@ -290,23 +277,15 @@ class DeviceUpdateManager(UpdateManager):
|
|
290
277
|
|
291
278
|
if software is None:
|
292
279
|
handling_type = HandlingType.SKIP
|
293
|
-
self.poll_time = config.poll_time_default
|
294
280
|
|
295
281
|
elif software.version == device.sw_version and not device.force_update:
|
296
282
|
handling_type = HandlingType.SKIP
|
297
|
-
self.poll_time = config.poll_time_default
|
298
283
|
|
299
284
|
elif device.last_state == UpdateStateEnum.ERROR and not device.force_update:
|
300
285
|
handling_type = HandlingType.SKIP
|
301
|
-
self.poll_time = config.poll_time_default
|
302
286
|
|
303
287
|
else:
|
304
288
|
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
289
|
|
311
290
|
return handling_type, software
|
312
291
|
|
@@ -318,23 +297,15 @@ class DeviceUpdateManager(UpdateManager):
|
|
318
297
|
if device.last_log is None:
|
319
298
|
device.last_log = ""
|
320
299
|
|
300
|
+
# SWUpdate-specific log parsing to report progress
|
321
301
|
matches = re.findall(r"Downloaded (\d+)%", log_data)
|
322
302
|
if matches:
|
323
303
|
device.progress = matches[-1]
|
324
304
|
|
325
|
-
|
326
|
-
|
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")
|
305
|
+
device.last_log += f"{log_data}\n"
|
306
|
+
await self.publish_log(f"{log_data}\n")
|
333
307
|
|
334
|
-
await self.save_device(
|
335
|
-
device,
|
336
|
-
update_fields=["progress", "last_log"],
|
337
|
-
)
|
308
|
+
await self.save_device(device, update_fields=["progress", "last_log"])
|
338
309
|
|
339
310
|
async def clear_log(self) -> None:
|
340
311
|
device = await self.get_device()
|
@@ -344,10 +315,7 @@ class DeviceUpdateManager(UpdateManager):
|
|
344
315
|
|
345
316
|
|
346
317
|
async def get_update_manager(dev_id: str) -> UpdateManager:
|
347
|
-
|
348
|
-
return UnknownUpdateManager("unknown")
|
349
|
-
else:
|
350
|
-
return DeviceUpdateManager(dev_id)
|
318
|
+
return DeviceUpdateManager(dev_id)
|
351
319
|
|
352
320
|
|
353
321
|
async def delete_devices(ids: list[str]):
|
goosebit/updater/routes.py
CHANGED
@@ -3,14 +3,18 @@ import time
|
|
3
3
|
from fastapi import APIRouter, Depends
|
4
4
|
from fastapi.requests import Request
|
5
5
|
|
6
|
+
from goosebit.settings import config
|
7
|
+
|
6
8
|
from . import controller
|
7
9
|
from .manager import get_update_manager
|
8
10
|
|
9
11
|
|
10
12
|
async def log_last_connection(request: Request, dev_id: str):
|
11
|
-
host = request.client.host
|
12
13
|
updater = await get_update_manager(dev_id)
|
13
|
-
|
14
|
+
if config.track_device_ip:
|
15
|
+
await updater.update_last_connection(round(time.time()), request.client.host)
|
16
|
+
else:
|
17
|
+
await updater.update_last_connection(round(time.time()))
|
14
18
|
|
15
19
|
|
16
20
|
router = APIRouter(
|
goosebit/updates/swdesc.py
CHANGED
@@ -23,7 +23,7 @@ def _append_compatibility(boardname, value, compatibility):
|
|
23
23
|
def parse_descriptor(swdesc: libconf.AttrDict[Any, Any | None]):
|
24
24
|
swdesc_attrs = {}
|
25
25
|
try:
|
26
|
-
swdesc_attrs["version"] = semver.Version.parse(swdesc["software"]["version"])
|
26
|
+
swdesc_attrs["version"] = semver.Version.parse(swdesc["software"]["version"], optional_minor_and_patch=True)
|
27
27
|
compatibility: list[dict[str, str]] = []
|
28
28
|
_append_compatibility("default", swdesc["software"], compatibility)
|
29
29
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: goosebit
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.5
|
4
4
|
Summary:
|
5
5
|
Author: Upstream Data
|
6
6
|
Author-email: brett@upstreamdata.ca
|
@@ -8,6 +8,7 @@ Requires-Python: >=3.11,<4.0
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
9
9
|
Classifier: Programming Language :: Python :: 3.11
|
10
10
|
Classifier: Programming Language :: Python :: 3.12
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
11
12
|
Provides-Extra: postgresql
|
12
13
|
Requires-Dist: aerich (>=0.7.2,<0.8.0)
|
13
14
|
Requires-Dist: aiocache (>=0.12.2,<0.13.0)
|
@@ -19,9 +20,9 @@ Requires-Dist: itsdangerous (>=2.2.0,<3.0.0)
|
|
19
20
|
Requires-Dist: jinja2 (>=3.1.4,<4.0.0)
|
20
21
|
Requires-Dist: joserfc (>=1.0.0,<2.0.0)
|
21
22
|
Requires-Dist: libconf (>=2.0.1,<3.0.0)
|
22
|
-
Requires-Dist: opentelemetry-distro (>=0.
|
23
|
-
Requires-Dist: opentelemetry-exporter-prometheus (>=0.
|
24
|
-
Requires-Dist: opentelemetry-instrumentation-fastapi (>=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)
|
25
26
|
Requires-Dist: pydantic-settings (>=2.4.0,<3.0.0)
|
26
27
|
Requires-Dist: python-multipart (>=0.0.9,<0.0.10)
|
27
28
|
Requires-Dist: semver (>=3.0.2,<4.0.0)
|
@@ -33,6 +34,8 @@ Description-Content-Type: text/markdown
|
|
33
34
|
|
34
35
|
<img src="docs/img/goosebit-logo.png" style="width: 100px; height: 100px; display: block;">
|
35
36
|
|
37
|
+
[](https://scorecard.dev/viewer/?uri=github.com/UpstreamDataInc/goosebit)
|
38
|
+
|
36
39
|
---
|
37
40
|
|
38
41
|
A simplistic, opinionated remote update server implementing hawkBit™'s [DDI API](https://eclipse.dev/hawkbit/apis/ddi_api/).
|
@@ -50,7 +53,6 @@ A simplistic, opinionated remote update server implementing hawkBit™'s [DDI AP
|
|
50
53
|
2. Create the database:
|
51
54
|
|
52
55
|
```bash
|
53
|
-
poetry run aerich init -t goosebit.db.config
|
54
56
|
poetry run aerich upgrade
|
55
57
|
```
|
56
58
|
|
@@ -125,6 +127,12 @@ After a model change create the migration
|
|
125
127
|
poetry run aerich migrate
|
126
128
|
```
|
127
129
|
|
130
|
+
To seed some sample data (attention: drops all current data) use
|
131
|
+
|
132
|
+
```bash
|
133
|
+
poetry run generate-sample-data
|
134
|
+
```
|
135
|
+
|
128
136
|
### Code formatting and linting
|
129
137
|
|
130
138
|
Code is formatted using different tools
|
@@ -147,7 +155,7 @@ poetry run pre-commit install
|
|
147
155
|
To manually apply the hooks to all files use:
|
148
156
|
|
149
157
|
```bash
|
150
|
-
pre-commit run --all-files
|
158
|
+
poetry run pre-commit run --all-files
|
151
159
|
```
|
152
160
|
|
153
161
|
### Testing
|