goosebit 0.2.3__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 +32 -3
- goosebit/api/v1/devices/device/routes.py +10 -4
- goosebit/api/v1/devices/responses.py +0 -7
- goosebit/api/v1/devices/routes.py +19 -3
- 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 +12 -8
- goosebit/db/__init__.py +12 -1
- goosebit/db/migrations/models/1_20241109151811_update.py +11 -0
- goosebit/db/models.py +19 -4
- goosebit/realtime/logs.py +1 -1
- goosebit/schema/devices.py +42 -38
- goosebit/schema/rollouts.py +21 -18
- goosebit/schema/software.py +24 -19
- goosebit/settings/schema.py +2 -0
- goosebit/ui/bff/common/__init__.py +0 -0
- goosebit/ui/bff/common/requests.py +44 -0
- goosebit/ui/bff/common/responses.py +16 -0
- goosebit/ui/bff/common/util.py +32 -0
- goosebit/ui/bff/devices/responses.py +15 -19
- goosebit/ui/bff/devices/routes.py +61 -7
- goosebit/ui/bff/rollouts/responses.py +15 -19
- goosebit/ui/bff/rollouts/routes.py +8 -6
- goosebit/ui/bff/routes.py +4 -2
- goosebit/ui/bff/software/responses.py +29 -19
- goosebit/ui/bff/software/routes.py +29 -16
- goosebit/ui/nav.py +1 -1
- goosebit/ui/routes.py +10 -19
- goosebit/ui/static/js/devices.js +188 -94
- goosebit/ui/static/js/rollouts.js +20 -13
- goosebit/ui/static/js/software.js +5 -11
- goosebit/ui/static/js/util.js +43 -14
- goosebit/ui/templates/devices.html.jinja +77 -49
- goosebit/ui/templates/nav.html.jinja +35 -4
- goosebit/ui/templates/rollouts.html.jinja +23 -23
- goosebit/updater/controller/v1/routes.py +33 -23
- goosebit/updater/controller/v1/schema.py +4 -4
- goosebit/updater/manager.py +28 -52
- goosebit/updater/routes.py +6 -2
- goosebit/updates/__init__.py +14 -21
- goosebit/updates/swdesc.py +36 -15
- {goosebit-0.2.3.dist-info → goosebit-0.2.5.dist-info}/METADATA +23 -7
- {goosebit-0.2.3.dist-info → goosebit-0.2.5.dist-info}/RECORD +48 -44
- {goosebit-0.2.3.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.3.dist-info → goosebit-0.2.5.dist-info}/LICENSE +0 -0
@@ -4,70 +4,98 @@
|
|
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>
|
30
10
|
</div>
|
31
|
-
<div class="modal" id="device-config-modal">
|
32
|
-
<div class="modal-dialog modal-
|
11
|
+
<div class="modal modal-lg fade" id="device-config-modal">
|
12
|
+
<div class="modal-dialog modal-dialog-centered modal-xl">
|
33
13
|
<div class="modal-content">
|
34
14
|
<div class="modal-header">
|
35
|
-
<h5 class="modal-title">
|
15
|
+
<h5 class="modal-title">Edit Devices</h5>
|
36
16
|
<button type="button"
|
37
17
|
class="btn-close"
|
38
18
|
data-bs-dismiss="modal"
|
39
19
|
aria-label="Close"></button>
|
40
20
|
</div>
|
41
|
-
<
|
42
|
-
<
|
43
|
-
<div class="
|
44
|
-
<
|
45
|
-
<input id="device-
|
21
|
+
<div class="modal-body">
|
22
|
+
<form id="device-name-form">
|
23
|
+
<div class="input-group mb-3">
|
24
|
+
<span class="input-group-text">Name</span>
|
25
|
+
<input id="device-name" class="form-control" />
|
26
|
+
<button type="submit" class="btn btn-outline-light">Apply</button>
|
46
27
|
</div>
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
28
|
+
</form>
|
29
|
+
<hr>
|
30
|
+
<ul class="nav nav-underline nav-justified w-100" role="tablist">
|
31
|
+
<li class="nav-item">
|
32
|
+
<button class="nav-link active"
|
33
|
+
aria-current="page"
|
34
|
+
id="rollout-tab"
|
35
|
+
data-bs-toggle="tab"
|
36
|
+
data-bs-target="#rollout-tab-content"
|
37
|
+
type="button"
|
38
|
+
role="tab">Software Rollout</button>
|
39
|
+
</li>
|
40
|
+
<li class="nav-item">
|
41
|
+
<button class="nav-link"
|
42
|
+
id="manual-tab"
|
43
|
+
data-bs-toggle="tab"
|
44
|
+
data-bs-target="#manual-tab-content"
|
45
|
+
type="button"
|
46
|
+
role="tab">Manual Software</button>
|
47
|
+
</li>
|
48
|
+
<li class="nav-item">
|
49
|
+
<button class="nav-link"
|
50
|
+
id="latest-tab"
|
51
|
+
data-bs-toggle="tab"
|
52
|
+
data-bs-target="#latest-tab-content"
|
53
|
+
type="button"
|
54
|
+
role="tab">Latest Software</button>
|
55
|
+
</li>
|
56
|
+
</ul>
|
57
|
+
<div class="tab-content mt-3">
|
58
|
+
<div class="tab-pane active" id="rollout-tab-content">
|
59
|
+
<form id="device-software-rollout-form" class="needs-validation" novalidate>
|
60
|
+
<div class="form-group mb-3">
|
61
|
+
<div class="input-group mb-3 has-validation">
|
62
|
+
<span class="input-group-text">Feed</span>
|
63
|
+
<input id="device-selected-feed"
|
64
|
+
class="form-control"
|
65
|
+
value="default"
|
66
|
+
required />
|
67
|
+
<div class="invalid-feedback">Feed missing. Use "default" if working with a single feed.</div>
|
68
|
+
</div>
|
69
|
+
</div>
|
70
|
+
<button type="submit" class="btn btn-outline-light w-100">Use Software Rollout</button>
|
71
|
+
</form>
|
57
72
|
</div>
|
58
|
-
<div class="
|
59
|
-
<
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
73
|
+
<div class="tab-pane" id="manual-tab-content">
|
74
|
+
<form id="device-software-manual-form" class="needs-validation" novalidate>
|
75
|
+
<div class="form-group mb-3">
|
76
|
+
<div class="input-group mb-3 has-validation">
|
77
|
+
<span class="input-group-text">Software</span>
|
78
|
+
<select class="form-control"
|
79
|
+
id="selected-sw"
|
80
|
+
data-size="5"
|
81
|
+
data-style-base="form-control"
|
82
|
+
title="Select software"
|
83
|
+
data-live-search="true"
|
84
|
+
data-live-search-normalize="true"
|
85
|
+
required></select>
|
86
|
+
<div class="invalid-feedback">Software missing.</div>
|
87
|
+
</div>
|
88
|
+
</div>
|
89
|
+
<button type="submit" class="btn btn-outline-light w-100">Use Manual Software</button>
|
90
|
+
</form>
|
91
|
+
</div>
|
92
|
+
<div class="tab-pane" id="latest-tab-content">
|
93
|
+
<form id="device-software-latest-form" class="needs-validation" novalidate>
|
94
|
+
<button type="submit" class="btn btn-outline-light w-100">Use Latest Software</button>
|
95
|
+
</form>
|
64
96
|
</div>
|
65
97
|
</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>
|
98
|
+
</div>
|
71
99
|
</div>
|
72
100
|
</div>
|
73
101
|
</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,13 +47,37 @@
|
|
40
47
|
background-color: transparent;
|
41
48
|
}
|
42
49
|
</style>
|
43
|
-
|
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>
|
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>
|
44
75
|
<script src="{{ url_for('static', path='js/util.js') }}"></script>
|
45
76
|
</head>
|
46
77
|
<body data-bs-theme="dark">
|
47
78
|
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
48
79
|
<div class="container-fluid">
|
49
|
-
<a class="navbar-brand" href="{{ request.url_for('
|
80
|
+
<a class="navbar-brand" href="{{ request.url_for('devices_ui') }}">
|
50
81
|
<img src="{{ request.url_for('static', path='svg/goosebit-logo.svg') }}"
|
51
82
|
class="me-2"
|
52
83
|
height="30px"
|
@@ -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>
|
@@ -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
|
@@ -23,11 +23,13 @@ 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
|
-
sleep = updater.poll_time
|
29
28
|
device = await updater.get_device()
|
30
29
|
|
30
|
+
if device is None:
|
31
|
+
raise HTTPException(404)
|
32
|
+
|
31
33
|
if device.last_state == UpdateStateEnum.UNKNOWN:
|
32
34
|
# device registration
|
33
35
|
sleep = config.poll_time_registration
|
@@ -42,14 +44,15 @@ async def polling(request: Request, dev_id: str, updater: UpdateManager = Depend
|
|
42
44
|
logger.info(f"Skip: registration required, device={updater.dev_id}")
|
43
45
|
|
44
46
|
elif device.last_state == UpdateStateEnum.ERROR and not device.force_update:
|
45
|
-
|
46
|
-
|
47
|
+
sleep = config.poll_time_default
|
48
|
+
logger.info(f"Skip: device in error state, device={updater.dev_id}")
|
47
49
|
|
48
50
|
else:
|
49
51
|
# provide update if available. Note: this is also required while in state "running", otherwise swupdate
|
50
52
|
# won't confirm a successful testing (might be a bug/problem in swupdate)
|
51
53
|
handling_type, software = await updater.get_update()
|
52
|
-
if handling_type != HandlingType.SKIP:
|
54
|
+
if handling_type != HandlingType.SKIP and software is not None:
|
55
|
+
sleep = config.poll_time_updating
|
53
56
|
links["deploymentBase"] = {
|
54
57
|
"href": str(
|
55
58
|
request.url_for(
|
@@ -60,6 +63,11 @@ async def polling(request: Request, dev_id: str, updater: UpdateManager = Depend
|
|
60
63
|
)
|
61
64
|
}
|
62
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
|
63
71
|
|
64
72
|
return {
|
65
73
|
"config": {"polling": {"sleep": sleep}},
|
@@ -99,60 +107,62 @@ async def deployment_feedback(
|
|
99
107
|
_: Request, data: FeedbackSchema, action_id: int, updater: UpdateManager = Depends(get_update_manager)
|
100
108
|
):
|
101
109
|
if data.status.execution == FeedbackStatusExecutionState.PROCEEDING:
|
102
|
-
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
|
+
|
103
115
|
logger.debug(f"Installation in progress, device={updater.dev_id}")
|
104
116
|
|
105
117
|
elif data.status.execution == FeedbackStatusExecutionState.CLOSED:
|
106
118
|
await updater.update_force_update(False)
|
107
|
-
await updater.update_log_complete(True)
|
108
119
|
|
109
120
|
reported_software = await Software.get_or_none(id=action_id)
|
110
121
|
|
111
122
|
# From hawkBit docu: DDI defines also a status NONE which will not be interpreted by the update server
|
112
123
|
# and handled like SUCCESS.
|
113
124
|
if data.status.result.finished in [FeedbackStatusResultFinished.SUCCESS, FeedbackStatusResultFinished.NONE]:
|
125
|
+
await updater.deployment_action_success()
|
114
126
|
await updater.update_device_state(UpdateStateEnum.FINISHED)
|
115
127
|
|
116
|
-
# not guaranteed to be the correct rollout - see next comment.
|
117
128
|
rollout = await updater.get_rollout()
|
118
129
|
if rollout:
|
119
130
|
if rollout.software == reported_software:
|
120
131
|
rollout.success_count += 1
|
121
132
|
await rollout.save()
|
122
133
|
else:
|
134
|
+
# edge case where device update mode got changed while update was running
|
123
135
|
logging.warning(
|
124
136
|
f"Updating rollout success stats failed, software={reported_software.id}, device={updater.dev_id}" # noqa: E501
|
125
137
|
)
|
126
138
|
|
127
|
-
# setting the currently installed version based on the current assigned software / existing rollouts
|
128
|
-
# is problematic. Better to assign custom action_id for each update (rollout id? software id? new id?).
|
129
|
-
# Alternatively - but requires customization on the gateway side - use version reported by the gateway.
|
130
139
|
await updater.update_sw_version(reported_software.version)
|
131
140
|
logger.debug(f"Installation successful, software={reported_software.version}, device={updater.dev_id}")
|
132
141
|
|
133
142
|
elif data.status.result.finished == FeedbackStatusResultFinished.FAILURE:
|
134
143
|
await updater.update_device_state(UpdateStateEnum.ERROR)
|
135
144
|
|
136
|
-
# not guaranteed to be the correct rollout - see comment above.
|
137
145
|
rollout = await updater.get_rollout()
|
138
146
|
if rollout:
|
139
147
|
if rollout.software == reported_software:
|
140
148
|
rollout.failure_count += 1
|
141
149
|
await rollout.save()
|
142
150
|
else:
|
151
|
+
# edge case where device update mode got changed while update was running
|
143
152
|
logging.warning(
|
144
153
|
f"Updating rollout failure stats failed, software={reported_software.id}, device={updater.dev_id}" # noqa: E501
|
145
154
|
)
|
146
155
|
|
147
156
|
logger.debug(f"Installation failed, software={reported_software.version}, device={updater.dev_id}")
|
148
157
|
else:
|
149
|
-
logging.
|
158
|
+
logging.error(
|
150
159
|
f"Device reported unhandled execution state, state={data.status.execution}, device={updater.dev_id}"
|
151
160
|
)
|
152
161
|
|
153
162
|
try:
|
154
163
|
log = data.status.details
|
155
|
-
|
164
|
+
if log is not None:
|
165
|
+
await updater.update_log("\n".join(log))
|
156
166
|
except AttributeError:
|
157
167
|
logging.warning(f"No details to update device update log, device={updater.dev_id}")
|
158
168
|
|
@@ -175,11 +185,11 @@ async def download_artifact(_: Request, updater: UpdateManager = Depends(get_upd
|
|
175
185
|
_, software = await updater.get_update()
|
176
186
|
if software is None:
|
177
187
|
raise HTTPException(404)
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
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
|
+
)
|
@@ -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
|
@@ -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,15 +86,19 @@ 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
|
89
|
-
|
93
|
+
|
94
|
+
if device is not None:
|
95
|
+
await callback(device.last_log)
|
90
96
|
try:
|
91
97
|
yield
|
92
98
|
except asyncio.CancelledError:
|
93
99
|
pass
|
94
100
|
finally:
|
101
|
+
# do not modify, breaks when combined
|
95
102
|
subscribers = self.log_subscribers
|
96
103
|
subscribers.remove(callback)
|
97
104
|
self.log_subscribers = subscribers
|
@@ -126,28 +133,12 @@ class UpdateManager(ABC):
|
|
126
133
|
await cb(log_data)
|
127
134
|
|
128
135
|
@abstractmethod
|
129
|
-
async def get_update(self) -> tuple[HandlingType, Software]: ...
|
136
|
+
async def get_update(self) -> tuple[HandlingType, Software | None]: ...
|
130
137
|
|
131
138
|
@abstractmethod
|
132
139
|
async def update_log(self, log_data: str) -> None: ...
|
133
140
|
|
134
141
|
|
135
|
-
class UnknownUpdateManager(UpdateManager):
|
136
|
-
def __init__(self, dev_id: str):
|
137
|
-
super().__init__(dev_id)
|
138
|
-
self.poll_time = config.poll_time_updating
|
139
|
-
|
140
|
-
async def _get_software(self) -> Software:
|
141
|
-
return await Software.latest(await self.get_device())
|
142
|
-
|
143
|
-
async def get_update(self) -> tuple[HandlingType, Software]:
|
144
|
-
software = await self._get_software()
|
145
|
-
return HandlingType.FORCED, software
|
146
|
-
|
147
|
-
async def update_log(self, log_data: str) -> None:
|
148
|
-
return
|
149
|
-
|
150
|
-
|
151
142
|
class DeviceUpdateManager(UpdateManager):
|
152
143
|
hardware_default = None
|
153
144
|
|
@@ -161,9 +152,11 @@ class DeviceUpdateManager(UpdateManager):
|
|
161
152
|
return (await Device.get_or_create(uuid=self.dev_id, defaults={"hardware": hardware}))[0]
|
162
153
|
|
163
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
|
164
158
|
result = await caches.get("default").set(self.dev_id, device, ttl=600)
|
165
159
|
assert result, "device being cached"
|
166
|
-
await device.save(update_fields=update_fields)
|
167
160
|
|
168
161
|
async def update_force_update(self, force_update: bool) -> None:
|
169
162
|
device = await self.get_device()
|
@@ -185,10 +178,12 @@ class DeviceUpdateManager(UpdateManager):
|
|
185
178
|
device.last_state = state
|
186
179
|
await self.save_device(device, update_fields=["last_state"])
|
187
180
|
|
188
|
-
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:
|
189
182
|
device = await self.get_device()
|
190
183
|
device.last_seen = last_seen
|
191
|
-
if
|
184
|
+
if last_ip is None:
|
185
|
+
await self.save_device(device, update_fields=["last_seen"])
|
186
|
+
elif ":" in last_ip:
|
192
187
|
device.last_ipv6 = last_ip
|
193
188
|
await self.save_device(device, update_fields=["last_seen", "last_ipv6"])
|
194
189
|
else:
|
@@ -235,10 +230,10 @@ class DeviceUpdateManager(UpdateManager):
|
|
235
230
|
if modified:
|
236
231
|
await self.save_device(device, update_fields=["hardware_id", "last_state", "sw_version"])
|
237
232
|
|
238
|
-
async def
|
233
|
+
async def deployment_action_success(self):
|
239
234
|
device = await self.get_device()
|
240
|
-
device.
|
241
|
-
await self.save_device(device, update_fields=["
|
235
|
+
device.progress = 100
|
236
|
+
await self.save_device(device, update_fields=["progress"])
|
242
237
|
|
243
238
|
async def get_rollout(self) -> Rollout | None:
|
244
239
|
device = await self.get_device()
|
@@ -276,29 +271,21 @@ class DeviceUpdateManager(UpdateManager):
|
|
276
271
|
assert device.update_mode == UpdateModeEnum.PINNED
|
277
272
|
return None
|
278
273
|
|
279
|
-
async def get_update(self) -> tuple[HandlingType, Software]:
|
274
|
+
async def get_update(self) -> tuple[HandlingType, Software | None]:
|
280
275
|
device = await self.get_device()
|
281
276
|
software = await self._get_software()
|
282
277
|
|
283
278
|
if software is None:
|
284
279
|
handling_type = HandlingType.SKIP
|
285
|
-
self.poll_time = config.poll_time_default
|
286
280
|
|
287
281
|
elif software.version == device.sw_version and not device.force_update:
|
288
282
|
handling_type = HandlingType.SKIP
|
289
|
-
self.poll_time = config.poll_time_default
|
290
283
|
|
291
284
|
elif device.last_state == UpdateStateEnum.ERROR and not device.force_update:
|
292
285
|
handling_type = HandlingType.SKIP
|
293
|
-
self.poll_time = config.poll_time_default
|
294
286
|
|
295
287
|
else:
|
296
288
|
handling_type = HandlingType.FORCED
|
297
|
-
self.poll_time = config.poll_time_updating
|
298
|
-
|
299
|
-
if device.log_complete:
|
300
|
-
await self.update_log_complete(False)
|
301
|
-
await self.clear_log()
|
302
289
|
|
303
290
|
return handling_type, software
|
304
291
|
|
@@ -310,23 +297,15 @@ class DeviceUpdateManager(UpdateManager):
|
|
310
297
|
if device.last_log is None:
|
311
298
|
device.last_log = ""
|
312
299
|
|
300
|
+
# SWUpdate-specific log parsing to report progress
|
313
301
|
matches = re.findall(r"Downloaded (\d+)%", log_data)
|
314
302
|
if matches:
|
315
303
|
device.progress = matches[-1]
|
316
304
|
|
317
|
-
|
318
|
-
|
319
|
-
device.last_log = ""
|
320
|
-
await self.publish_log(None)
|
321
|
-
|
322
|
-
if not log_data == "Skipped Update.":
|
323
|
-
device.last_log += f"{log_data}\n"
|
324
|
-
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")
|
325
307
|
|
326
|
-
await self.save_device(
|
327
|
-
device,
|
328
|
-
update_fields=["progress", "last_log"],
|
329
|
-
)
|
308
|
+
await self.save_device(device, update_fields=["progress", "last_log"])
|
330
309
|
|
331
310
|
async def clear_log(self) -> None:
|
332
311
|
device = await self.get_device()
|
@@ -336,10 +315,7 @@ class DeviceUpdateManager(UpdateManager):
|
|
336
315
|
|
337
316
|
|
338
317
|
async def get_update_manager(dev_id: str) -> UpdateManager:
|
339
|
-
|
340
|
-
return UnknownUpdateManager("unknown")
|
341
|
-
else:
|
342
|
-
return DeviceUpdateManager(dev_id)
|
318
|
+
return DeviceUpdateManager(dev_id)
|
343
319
|
|
344
320
|
|
345
321
|
async def delete_devices(ids: list[str]):
|