goosebit 0.2.2__py3-none-any.whl → 0.2.4__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 -3
- goosebit/api/v1/devices/device/routes.py +8 -2
- goosebit/api/v1/devices/responses.py +0 -7
- goosebit/api/v1/devices/routes.py +2 -1
- goosebit/api/v1/rollouts/responses.py +2 -7
- goosebit/api/v1/rollouts/routes.py +7 -3
- goosebit/api/v1/software/responses.py +0 -7
- goosebit/api/v1/software/routes.py +24 -11
- goosebit/auth/__init__.py +7 -7
- goosebit/db/__init__.py +12 -1
- goosebit/db/models.py +13 -2
- goosebit/schema/devices.py +41 -37
- goosebit/schema/rollouts.py +21 -18
- goosebit/schema/software.py +24 -19
- goosebit/ui/bff/common/__init__.py +0 -0
- goosebit/ui/bff/common/requests.py +56 -0
- goosebit/ui/bff/common/util.py +32 -0
- goosebit/ui/bff/devices/responses.py +12 -20
- goosebit/ui/bff/devices/routes.py +9 -6
- goosebit/ui/bff/rollouts/responses.py +12 -20
- goosebit/ui/bff/rollouts/routes.py +8 -6
- goosebit/ui/bff/software/responses.py +19 -19
- goosebit/ui/bff/software/routes.py +29 -16
- goosebit/ui/nav.py +1 -1
- goosebit/ui/routes.py +4 -4
- goosebit/ui/static/js/devices.js +135 -25
- goosebit/ui/static/js/rollouts.js +4 -0
- goosebit/ui/static/js/util.js +23 -14
- goosebit/ui/templates/devices.html.jinja +77 -29
- goosebit/ui/templates/nav.html.jinja +22 -2
- goosebit/ui/templates/rollouts.html.jinja +23 -23
- goosebit/updater/controller/v1/routes.py +7 -3
- goosebit/updater/controller/v1/schema.py +4 -4
- goosebit/updater/manager.py +16 -8
- goosebit/updates/__init__.py +14 -21
- goosebit/updates/swdesc.py +35 -14
- {goosebit-0.2.2.dist-info → goosebit-0.2.4.dist-info}/METADATA +11 -3
- {goosebit-0.2.2.dist-info → goosebit-0.2.4.dist-info}/RECORD +40 -37
- {goosebit-0.2.2.dist-info → goosebit-0.2.4.dist-info}/LICENSE +0 -0
- {goosebit-0.2.2.dist-info → goosebit-0.2.4.dist-info}/WHEEL +0 -0
@@ -28,46 +28,94 @@
|
|
28
28
|
</div>
|
29
29
|
</div>
|
30
30
|
</div>
|
31
|
-
<div class="modal" id="device-config-modal">
|
32
|
-
<div class="modal-dialog modal-
|
31
|
+
<div class="modal modal-lg fade" id="device-config-modal">
|
32
|
+
<div class="modal-dialog modal-dialog-centered modal-xl">
|
33
33
|
<div class="modal-content">
|
34
34
|
<div class="modal-header">
|
35
|
-
<h5 class="modal-title">
|
35
|
+
<h5 class="modal-title">Edit Devices</h5>
|
36
36
|
<button type="button"
|
37
37
|
class="btn-close"
|
38
38
|
data-bs-dismiss="modal"
|
39
39
|
aria-label="Close"></button>
|
40
40
|
</div>
|
41
|
-
<
|
42
|
-
<
|
43
|
-
<div class="
|
44
|
-
<
|
45
|
-
<input id="device-
|
41
|
+
<div class="modal-body">
|
42
|
+
<form id="device-name-form">
|
43
|
+
<div class="input-group mb-3">
|
44
|
+
<span class="input-group-text">Name</span>
|
45
|
+
<input id="device-name" class="form-control" />
|
46
|
+
<button type="submit" class="btn btn-outline-light">Apply</button>
|
46
47
|
</div>
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
48
|
+
</form>
|
49
|
+
<hr>
|
50
|
+
<ul class="nav nav-underline nav-justified w-100" role="tablist">
|
51
|
+
<li class="nav-item">
|
52
|
+
<button class="nav-link active"
|
53
|
+
aria-current="page"
|
54
|
+
id="rollout-tab"
|
55
|
+
data-bs-toggle="tab"
|
56
|
+
data-bs-target="#rollout-tab-content"
|
57
|
+
type="button"
|
58
|
+
role="tab">Software Rollout</button>
|
59
|
+
</li>
|
60
|
+
<li class="nav-item">
|
61
|
+
<button class="nav-link"
|
62
|
+
id="manual-tab"
|
63
|
+
data-bs-toggle="tab"
|
64
|
+
data-bs-target="#manual-tab-content"
|
65
|
+
type="button"
|
66
|
+
role="tab">Manual Software</button>
|
67
|
+
</li>
|
68
|
+
<li class="nav-item">
|
69
|
+
<button class="nav-link"
|
70
|
+
id="latest-tab"
|
71
|
+
data-bs-toggle="tab"
|
72
|
+
data-bs-target="#latest-tab-content"
|
73
|
+
type="button"
|
74
|
+
role="tab">Latest Software</button>
|
75
|
+
</li>
|
76
|
+
</ul>
|
77
|
+
<div class="tab-content mt-3">
|
78
|
+
<div class="tab-pane active" id="rollout-tab-content">
|
79
|
+
<form id="device-software-rollout-form" class="needs-validation" novalidate>
|
80
|
+
<div class="form-group mb-3">
|
81
|
+
<div class="input-group mb-3 has-validation">
|
82
|
+
<span class="input-group-text">Feed</span>
|
83
|
+
<input id="device-selected-feed"
|
84
|
+
class="form-control"
|
85
|
+
value="default"
|
86
|
+
required />
|
87
|
+
<div class="invalid-feedback">Feed missing. Use "default" if working with a single feed.</div>
|
88
|
+
</div>
|
89
|
+
</div>
|
90
|
+
<button type="submit" class="btn btn-outline-light w-100">Use Software Rollout</button>
|
91
|
+
</form>
|
57
92
|
</div>
|
58
|
-
<div class="
|
59
|
-
<
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
93
|
+
<div class="tab-pane" id="manual-tab-content">
|
94
|
+
<form id="device-software-manual-form" class="needs-validation" novalidate>
|
95
|
+
<div class="form-group mb-3">
|
96
|
+
<div class="input-group mb-3 has-validation">
|
97
|
+
<span class="input-group-text">Software</span>
|
98
|
+
<select class="form-control"
|
99
|
+
id="selected-sw"
|
100
|
+
data-size="5"
|
101
|
+
data-style-base="form-control"
|
102
|
+
title="Select software"
|
103
|
+
data-live-search="true"
|
104
|
+
data-live-search-normalize="true"
|
105
|
+
required></select>
|
106
|
+
<div class="invalid-feedback">Software missing.</div>
|
107
|
+
</div>
|
108
|
+
</div>
|
109
|
+
<button type="submit" class="btn btn-outline-light w-100">Use Manual Software</button>
|
110
|
+
</form>
|
111
|
+
</div>
|
112
|
+
<div class="tab-pane" id="latest-tab-content">
|
113
|
+
<form id="device-software-latest-form" class="needs-validation" novalidate>
|
114
|
+
<button type="submit" class="btn btn-outline-light w-100">Use Latest Software</button>
|
115
|
+
</form>
|
64
116
|
</div>
|
65
117
|
</div>
|
66
|
-
|
67
|
-
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
68
|
-
<button type="submit" class="btn btn-outline-light">Save changes</button>
|
69
|
-
</div>
|
70
|
-
</form>
|
118
|
+
</div>
|
71
119
|
</div>
|
72
120
|
</div>
|
73
121
|
</div>
|
@@ -10,8 +10,11 @@
|
|
10
10
|
rel="stylesheet"
|
11
11
|
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
|
12
12
|
crossorigin="anonymous" />
|
13
|
-
<script src="https://cdn.jsdelivr.net/npm/
|
14
|
-
integrity="sha384-
|
13
|
+
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"
|
14
|
+
integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"
|
15
|
+
crossorigin="anonymous"></script>
|
16
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"
|
17
|
+
integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy"
|
15
18
|
crossorigin="anonymous"></script>
|
16
19
|
<link rel="stylesheet"
|
17
20
|
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
|
@@ -27,6 +30,10 @@
|
|
27
30
|
<link href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css"
|
28
31
|
rel="stylesheet" />
|
29
32
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.all.min.js"></script>
|
33
|
+
<!-- Bootstrap select (searchable select) -->
|
34
|
+
<link rel="stylesheet"
|
35
|
+
href="https://cdn.jsdelivr.net/npm/bootstrap-select@1.14.0-beta3/dist/css/bootstrap-select.min.css">
|
36
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap-select@1.14.0-beta3/dist/js/bootstrap-select.min.js"></script>
|
30
37
|
<!--data tables alignment fix-->
|
31
38
|
<style>
|
32
39
|
th.dt-type-numeric {
|
@@ -40,6 +47,19 @@
|
|
40
47
|
background-color: transparent;
|
41
48
|
}
|
42
49
|
</style>
|
50
|
+
<!--select search fix-->
|
51
|
+
<style>
|
52
|
+
.no-results {
|
53
|
+
background-color: var(--bs-body-bg)!important;
|
54
|
+
}
|
55
|
+
.bs-searchbox input {
|
56
|
+
border: var(--bs-border-width) solid var(--bs-border-color)!important;
|
57
|
+
}
|
58
|
+
.was-validated .bs-searchbox input {
|
59
|
+
background-image: none!important;
|
60
|
+
box-shadow: none!important;
|
61
|
+
}
|
62
|
+
</style>
|
43
63
|
<script>const TABLE_UPDATE_TIME = 3000;</script>
|
44
64
|
<script src="{{ url_for('static', path='js/util.js') }}"></script>
|
45
65
|
</head>
|
@@ -36,35 +36,35 @@
|
|
36
36
|
</div>
|
37
37
|
<form id="rollout-form" class="needs-validation" novalidate>
|
38
38
|
<div class="modal-body">
|
39
|
-
<div class="
|
40
|
-
<
|
41
|
-
<input id="rollout-selected-name"
|
42
|
-
|
43
|
-
|
39
|
+
<div class="input-group mb-3 has-validation">
|
40
|
+
<span class="input-group-text">Name</span>
|
41
|
+
<input class="form-control" id="rollout-selected-name" required />
|
42
|
+
<div class="invalid-feedback">Name missing.</div>
|
43
|
+
</div>
|
44
|
+
<div class="input-group mb-3 has-validation">
|
45
|
+
<span class="input-group-text">Feed</span>
|
46
|
+
<input class="form-control" id="rollout-selected-feed" required />
|
47
|
+
<div class="invalid-feedback">Feed missing. Use "default" if working with a single feed.</div>
|
44
48
|
</div>
|
45
49
|
<div class="form-group mb-3">
|
46
|
-
<
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
50
|
+
<div class="input-group mb-3 has-validation">
|
51
|
+
<span class="input-group-text">Software</span>
|
52
|
+
<select class="form-control"
|
53
|
+
id="selected-sw"
|
54
|
+
data-size="5"
|
55
|
+
data-style-base="form-control"
|
56
|
+
title="Select software"
|
57
|
+
data-live-search="true"
|
58
|
+
data-live-search-normalize="true"
|
59
|
+
required></select>
|
60
|
+
<div class="invalid-feedback">Software missing.</div>
|
54
61
|
</div>
|
55
62
|
</div>
|
56
|
-
<div class="
|
57
|
-
<
|
58
|
-
<
|
59
|
-
<option value="" disabled selected>Select software</option>
|
60
|
-
</select>
|
61
|
-
<div class="invalid-feedback">Select software for the rollout.</div>
|
63
|
+
<div class="modal-footer">
|
64
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
65
|
+
<button type="submit" class="btn btn-outline-light">Save changes</button>
|
62
66
|
</div>
|
63
67
|
</div>
|
64
|
-
<div class="modal-footer">
|
65
|
-
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
66
|
-
<button type="submit" class="btn btn-outline-light">Save changes</button>
|
67
|
-
</div>
|
68
68
|
</form>
|
69
69
|
</div>
|
70
70
|
</div>
|
@@ -23,11 +23,14 @@ router = APIRouter(prefix="/v1")
|
|
23
23
|
|
24
24
|
@router.get("/{dev_id}")
|
25
25
|
async def polling(request: Request, dev_id: str, updater: UpdateManager = Depends(get_update_manager)):
|
26
|
-
links = {}
|
26
|
+
links: dict[str, dict[str, str]] = {}
|
27
27
|
|
28
28
|
sleep = updater.poll_time
|
29
29
|
device = await updater.get_device()
|
30
30
|
|
31
|
+
if device is None:
|
32
|
+
raise HTTPException(404)
|
33
|
+
|
31
34
|
if device.last_state == UpdateStateEnum.UNKNOWN:
|
32
35
|
# device registration
|
33
36
|
sleep = config.poll_time_registration
|
@@ -49,7 +52,7 @@ async def polling(request: Request, dev_id: str, updater: UpdateManager = Depend
|
|
49
52
|
# provide update if available. Note: this is also required while in state "running", otherwise swupdate
|
50
53
|
# won't confirm a successful testing (might be a bug/problem in swupdate)
|
51
54
|
handling_type, software = await updater.get_update()
|
52
|
-
if handling_type != HandlingType.SKIP:
|
55
|
+
if handling_type != HandlingType.SKIP and software is not None:
|
53
56
|
links["deploymentBase"] = {
|
54
57
|
"href": str(
|
55
58
|
request.url_for(
|
@@ -152,7 +155,8 @@ async def deployment_feedback(
|
|
152
155
|
|
153
156
|
try:
|
154
157
|
log = data.status.details
|
155
|
-
|
158
|
+
if log is not None:
|
159
|
+
await updater.update_log("\n".join(log))
|
156
160
|
except AttributeError:
|
157
161
|
logging.warning(f"No details to update device update log, device={updater.dev_id}")
|
158
162
|
|
@@ -41,16 +41,16 @@ class FeedbackStatusResultFinished(StrEnum):
|
|
41
41
|
|
42
42
|
class FeedbackStatusResultSchema(BaseModel):
|
43
43
|
finished: FeedbackStatusResultFinished
|
44
|
-
progress: FeedbackStatusProgressSchema = None
|
44
|
+
progress: FeedbackStatusProgressSchema | None = None
|
45
45
|
|
46
46
|
|
47
47
|
class FeedbackStatusSchema(BaseModel):
|
48
48
|
execution: FeedbackStatusExecutionState
|
49
49
|
result: FeedbackStatusResultSchema
|
50
|
-
code: int = None
|
51
|
-
details: list[str] = None
|
50
|
+
code: int | None = None
|
51
|
+
details: list[str] | None = None
|
52
52
|
|
53
53
|
|
54
54
|
class FeedbackSchema(BaseModel):
|
55
|
-
time: str = None
|
55
|
+
time: str | None = None
|
56
56
|
status: FeedbackStatusSchema
|
goosebit/updater/manager.py
CHANGED
@@ -45,7 +45,7 @@ class UpdateManager(ABC):
|
|
45
45
|
self.dev_id = dev_id
|
46
46
|
|
47
47
|
async def get_device(self) -> Device | None:
|
48
|
-
return
|
48
|
+
return None
|
49
49
|
|
50
50
|
async def update_force_update(self, force_update: bool) -> None:
|
51
51
|
return
|
@@ -86,7 +86,8 @@ class UpdateManager(ABC):
|
|
86
86
|
subscribers = self.log_subscribers
|
87
87
|
subscribers.append(callback)
|
88
88
|
self.log_subscribers = subscribers
|
89
|
-
|
89
|
+
if device is not None:
|
90
|
+
await callback(device.last_log)
|
90
91
|
try:
|
91
92
|
yield
|
92
93
|
except asyncio.CancelledError:
|
@@ -126,7 +127,7 @@ class UpdateManager(ABC):
|
|
126
127
|
await cb(log_data)
|
127
128
|
|
128
129
|
@abstractmethod
|
129
|
-
async def get_update(self) -> tuple[HandlingType, Software]: ...
|
130
|
+
async def get_update(self) -> tuple[HandlingType, Software | None]: ...
|
130
131
|
|
131
132
|
@abstractmethod
|
132
133
|
async def update_log(self, log_data: str) -> None: ...
|
@@ -137,11 +138,16 @@ class UnknownUpdateManager(UpdateManager):
|
|
137
138
|
super().__init__(dev_id)
|
138
139
|
self.poll_time = config.poll_time_updating
|
139
140
|
|
140
|
-
async def _get_software(self) -> Software:
|
141
|
-
|
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)
|
142
146
|
|
143
|
-
async def get_update(self) -> tuple[HandlingType, Software]:
|
147
|
+
async def get_update(self) -> tuple[HandlingType, Software | None]:
|
144
148
|
software = await self._get_software()
|
149
|
+
if software is None:
|
150
|
+
return HandlingType.SKIP, None
|
145
151
|
return HandlingType.FORCED, software
|
146
152
|
|
147
153
|
async def update_log(self, log_data: str) -> None:
|
@@ -161,9 +167,11 @@ class DeviceUpdateManager(UpdateManager):
|
|
161
167
|
return (await Device.get_or_create(uuid=self.dev_id, defaults={"hardware": hardware}))[0]
|
162
168
|
|
163
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
|
164
173
|
result = await caches.get("default").set(self.dev_id, device, ttl=600)
|
165
174
|
assert result, "device being cached"
|
166
|
-
await device.save(update_fields=update_fields)
|
167
175
|
|
168
176
|
async def update_force_update(self, force_update: bool) -> None:
|
169
177
|
device = await self.get_device()
|
@@ -276,7 +284,7 @@ class DeviceUpdateManager(UpdateManager):
|
|
276
284
|
assert device.update_mode == UpdateModeEnum.PINNED
|
277
285
|
return None
|
278
286
|
|
279
|
-
async def get_update(self) -> tuple[HandlingType, Software]:
|
287
|
+
async def get_update(self) -> tuple[HandlingType, Software | None]:
|
280
288
|
device = await self.get_device()
|
281
289
|
software = await self._get_software()
|
282
290
|
|
goosebit/updates/__init__.py
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
-
import
|
2
|
-
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
3
|
from urllib.parse import unquote, urlparse
|
4
4
|
from urllib.request import url2pathname
|
5
5
|
|
6
|
+
from anyio import Path
|
6
7
|
from fastapi import HTTPException
|
7
8
|
from fastapi.requests import Request
|
8
9
|
from tortoise.expressions import Q
|
@@ -10,6 +11,7 @@ from tortoise.expressions import Q
|
|
10
11
|
from goosebit.db.models import Hardware, Software
|
11
12
|
from goosebit.updater.manager import UpdateManager
|
12
13
|
|
14
|
+
from ..settings import config
|
13
15
|
from . import swdesc
|
14
16
|
|
15
17
|
|
@@ -18,6 +20,8 @@ async def create_software_update(uri: str, temp_file: Path | None) -> Software:
|
|
18
20
|
|
19
21
|
# parse swu header into update_info
|
20
22
|
if parsed_uri.scheme == "file":
|
23
|
+
if temp_file is None:
|
24
|
+
raise HTTPException(500, "Temporary file missing, cannot parse file information")
|
21
25
|
try:
|
22
26
|
update_info = await swdesc.parse_file(temp_file)
|
23
27
|
except Exception:
|
@@ -28,7 +32,6 @@ async def create_software_update(uri: str, temp_file: Path | None) -> Software:
|
|
28
32
|
update_info = await swdesc.parse_remote(uri)
|
29
33
|
except Exception:
|
30
34
|
raise HTTPException(422, "Software swu header cannot be parsed")
|
31
|
-
|
32
35
|
else:
|
33
36
|
raise HTTPException(422, "Software URI protocol unknown")
|
34
37
|
|
@@ -42,10 +45,14 @@ async def create_software_update(uri: str, temp_file: Path | None) -> Software:
|
|
42
45
|
|
43
46
|
# for local file: rename temp file to final name
|
44
47
|
if parsed_uri.scheme == "file":
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
48
|
+
if temp_file is None:
|
49
|
+
raise HTTPException(500, "Temporary file missing, cannot parse file information")
|
50
|
+
filename = Path(url2pathname(unquote(parsed_uri.path))).name
|
51
|
+
path = Path(config.artifacts_dir).joinpath(update_info["hash"], filename)
|
52
|
+
await path.parent.mkdir(parents=True, exist_ok=True)
|
53
|
+
await temp_file.replace(path)
|
54
|
+
absolute = await path.absolute()
|
55
|
+
uri = absolute.as_uri()
|
49
56
|
|
50
57
|
# create software
|
51
58
|
software = await Software.create(
|
@@ -85,20 +92,6 @@ async def _is_software_colliding(update_info):
|
|
85
92
|
return is_colliding
|
86
93
|
|
87
94
|
|
88
|
-
def _unique_path(uri):
|
89
|
-
path = Path(url2pathname(unquote(uri.path)))
|
90
|
-
if not path.exists():
|
91
|
-
return path
|
92
|
-
|
93
|
-
counter = 1
|
94
|
-
new_path = path.with_name(f"{path.stem}-{counter}{path.suffix}")
|
95
|
-
while new_path.exists():
|
96
|
-
counter += 1
|
97
|
-
new_path = path.with_name(f"{path.stem}-{counter}{path.suffix}")
|
98
|
-
|
99
|
-
return new_path
|
100
|
-
|
101
|
-
|
102
95
|
async def generate_chunk(request: Request, updater: UpdateManager) -> list:
|
103
96
|
_, software = await updater.get_update()
|
104
97
|
if software is None:
|
goosebit/updates/swdesc.py
CHANGED
@@ -1,12 +1,15 @@
|
|
1
1
|
import hashlib
|
2
2
|
import logging
|
3
|
-
|
3
|
+
import random
|
4
|
+
import string
|
4
5
|
from typing import Any
|
5
6
|
|
6
|
-
import aiofiles
|
7
7
|
import httpx
|
8
8
|
import libconf
|
9
9
|
import semver
|
10
|
+
from anyio import AsyncFile, Path, open_file
|
11
|
+
|
12
|
+
from goosebit.settings import config
|
10
13
|
|
11
14
|
logger = logging.getLogger(__name__)
|
12
15
|
|
@@ -21,7 +24,7 @@ def parse_descriptor(swdesc: libconf.AttrDict[Any, Any | None]):
|
|
21
24
|
swdesc_attrs = {}
|
22
25
|
try:
|
23
26
|
swdesc_attrs["version"] = semver.Version.parse(swdesc["software"]["version"])
|
24
|
-
compatibility = []
|
27
|
+
compatibility: list[dict[str, str]] = []
|
25
28
|
_append_compatibility("default", swdesc["software"], compatibility)
|
26
29
|
|
27
30
|
for key in swdesc["software"]:
|
@@ -41,7 +44,7 @@ def parse_descriptor(swdesc: libconf.AttrDict[Any, Any | None]):
|
|
41
44
|
|
42
45
|
|
43
46
|
async def parse_file(file: Path):
|
44
|
-
async with
|
47
|
+
async with await open_file(file, "r+b") as f:
|
45
48
|
# get file size
|
46
49
|
size = int((await f.read(110))[54:62], 16)
|
47
50
|
filename = b""
|
@@ -59,20 +62,38 @@ async def parse_file(file: Path):
|
|
59
62
|
swdesc = libconf.loads((await f.read(size)).decode("utf-8"))
|
60
63
|
|
61
64
|
swdesc_attrs = parse_descriptor(swdesc)
|
62
|
-
|
63
|
-
swdesc_attrs["
|
65
|
+
stat = await file.stat()
|
66
|
+
swdesc_attrs["size"] = stat.st_size
|
67
|
+
swdesc_attrs["hash"] = await _sha1_hash_file(f)
|
64
68
|
return swdesc_attrs
|
65
69
|
|
66
70
|
|
67
71
|
async def parse_remote(url: str):
|
68
72
|
async with httpx.AsyncClient() as c:
|
69
73
|
file = await c.get(url)
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
74
|
+
artifacts_dir = Path(config.artifacts_dir)
|
75
|
+
tmp_file_path = artifacts_dir.joinpath("tmp", ("".join(random.choices(string.ascii_lowercase, k=12)) + ".tmp"))
|
76
|
+
await tmp_file_path.parent.mkdir(parents=True, exist_ok=True)
|
77
|
+
try:
|
78
|
+
async with await open_file(tmp_file_path, "w+b") as f:
|
79
|
+
await f.write(file.content)
|
80
|
+
file_data = await parse_file(Path(str(f.name)))
|
81
|
+
finally:
|
82
|
+
await tmp_file_path.unlink()
|
83
|
+
return file_data
|
84
|
+
|
85
|
+
|
86
|
+
async def _sha1_hash_file(fileobj: AsyncFile):
|
87
|
+
last = await fileobj.tell()
|
88
|
+
await fileobj.seek(0)
|
89
|
+
sha1_hash = hashlib.sha1()
|
90
|
+
buf = bytearray(2**18)
|
91
|
+
view = memoryview(buf)
|
92
|
+
while True:
|
93
|
+
size = await fileobj.readinto(buf)
|
94
|
+
if size == 0:
|
95
|
+
break
|
96
|
+
sha1_hash.update(view[:size])
|
97
|
+
|
98
|
+
await fileobj.seek(last)
|
78
99
|
return sha1_hash.hexdigest()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: goosebit
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.4
|
4
4
|
Summary:
|
5
5
|
Author: Upstream Data
|
6
6
|
Author-email: brett@upstreamdata.ca
|
@@ -11,7 +11,6 @@ Classifier: Programming Language :: Python :: 3.12
|
|
11
11
|
Provides-Extra: postgresql
|
12
12
|
Requires-Dist: aerich (>=0.7.2,<0.8.0)
|
13
13
|
Requires-Dist: aiocache (>=0.12.2,<0.13.0)
|
14
|
-
Requires-Dist: aiofiles (>=24.1.0,<25.0.0)
|
15
14
|
Requires-Dist: argon2-cffi (>=23.1.0,<24.0.0)
|
16
15
|
Requires-Dist: asyncpg (>=0.29.0,<0.30.0) ; extra == "postgresql"
|
17
16
|
Requires-Dist: fastapi[uvicorn] (>=0.111.0,<0.112.0)
|
@@ -43,10 +42,19 @@ A simplistic, opinionated remote update server implementing hawkBit™'s [DDI AP
|
|
43
42
|
### Installation
|
44
43
|
|
45
44
|
1. Install dependencies using [Poetry](https://python-poetry.org/):
|
45
|
+
|
46
46
|
```bash
|
47
47
|
poetry install
|
48
48
|
```
|
49
|
-
|
49
|
+
|
50
|
+
2. Create the database:
|
51
|
+
|
52
|
+
```bash
|
53
|
+
poetry run aerich init -t goosebit.db.config
|
54
|
+
poetry run aerich upgrade
|
55
|
+
```
|
56
|
+
|
57
|
+
3. Launch gooseBit:
|
50
58
|
```bash
|
51
59
|
python main.py
|
52
60
|
```
|