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.
- goosebit/__init__.py +56 -6
- goosebit/api/telemetry/metrics.py +1 -5
- goosebit/api/v1/devices/device/responses.py +1 -0
- goosebit/api/v1/devices/device/routes.py +8 -8
- goosebit/api/v1/devices/requests.py +20 -0
- goosebit/api/v1/devices/routes.py +83 -8
- goosebit/api/v1/download/routes.py +14 -3
- goosebit/api/v1/rollouts/routes.py +5 -4
- goosebit/api/v1/routes.py +2 -1
- goosebit/api/v1/settings/routes.py +14 -0
- goosebit/api/v1/settings/users/__init__.py +1 -0
- goosebit/api/v1/settings/users/requests.py +16 -0
- goosebit/api/v1/settings/users/responses.py +7 -0
- goosebit/api/v1/settings/users/routes.py +56 -0
- goosebit/api/v1/software/routes.py +18 -14
- goosebit/auth/__init__.py +54 -14
- goosebit/auth/permissions.py +80 -0
- goosebit/db/config.py +57 -1
- goosebit/db/migrations/models/1_20241109151811_update.py +11 -0
- goosebit/db/migrations/models/2_20241121113728_update.py +11 -0
- goosebit/db/migrations/models/3_20241121140210_update.py +11 -0
- goosebit/db/migrations/models/4_20250324110331_update.py +16 -0
- goosebit/db/migrations/models/4_20250402085235_rename_uuid_to_id.py +11 -0
- goosebit/db/migrations/models/5_20250619090242_null_feed.py +83 -0
- goosebit/db/models.py +22 -7
- goosebit/db/pg_ssl_context.py +51 -0
- goosebit/device_manager.py +262 -0
- goosebit/plugins/__init__.py +32 -0
- goosebit/schema/devices.py +9 -6
- goosebit/schema/plugins.py +67 -0
- goosebit/schema/updates.py +15 -0
- goosebit/schema/users.py +9 -0
- goosebit/settings/__init__.py +0 -3
- goosebit/settings/schema.py +62 -14
- goosebit/storage/__init__.py +62 -0
- goosebit/storage/base.py +14 -0
- goosebit/storage/filesystem.py +111 -0
- goosebit/storage/s3.py +104 -0
- goosebit/ui/bff/common/columns.py +50 -0
- goosebit/ui/bff/common/requests.py +3 -15
- goosebit/ui/bff/common/responses.py +17 -0
- goosebit/ui/bff/devices/device/__init__.py +1 -0
- goosebit/ui/bff/devices/device/routes.py +17 -0
- goosebit/ui/bff/devices/requests.py +1 -0
- goosebit/ui/bff/devices/responses.py +6 -2
- goosebit/ui/bff/devices/routes.py +71 -17
- goosebit/ui/bff/download/routes.py +14 -3
- goosebit/ui/bff/rollouts/responses.py +6 -2
- goosebit/ui/bff/rollouts/routes.py +32 -4
- goosebit/ui/bff/routes.py +6 -3
- goosebit/ui/bff/settings/__init__.py +1 -0
- goosebit/ui/bff/settings/routes.py +20 -0
- goosebit/ui/bff/settings/users/__init__.py +1 -0
- goosebit/ui/bff/settings/users/responses.py +33 -0
- goosebit/ui/bff/settings/users/routes.py +80 -0
- goosebit/ui/bff/software/responses.py +19 -9
- goosebit/ui/bff/software/routes.py +40 -12
- goosebit/ui/nav.py +12 -2
- goosebit/ui/routes.py +70 -26
- goosebit/ui/static/js/devices.js +72 -80
- goosebit/ui/static/js/login.js +21 -5
- goosebit/ui/static/js/logs.js +7 -22
- goosebit/ui/static/js/rollouts.js +39 -35
- goosebit/ui/static/js/settings.js +322 -0
- goosebit/ui/static/js/setup.js +28 -0
- goosebit/ui/static/js/software.js +127 -127
- goosebit/ui/static/js/util.js +45 -4
- goosebit/ui/templates/__init__.py +10 -1
- goosebit/ui/templates/devices.html.jinja +0 -20
- goosebit/ui/templates/login.html.jinja +5 -0
- goosebit/ui/templates/nav.html.jinja +26 -7
- goosebit/ui/templates/rollouts.html.jinja +4 -22
- goosebit/ui/templates/settings.html.jinja +88 -0
- goosebit/ui/templates/setup.html.jinja +71 -0
- goosebit/ui/templates/software.html.jinja +0 -11
- goosebit/updater/controller/v1/routes.py +120 -72
- goosebit/updater/routes.py +86 -7
- goosebit/updates/__init__.py +24 -31
- goosebit/updates/swdesc.py +15 -8
- goosebit/users/__init__.py +63 -0
- goosebit/util/__init__.py +0 -0
- goosebit/util/path.py +42 -0
- goosebit/util/version.py +92 -0
- goosebit-0.2.6.dist-info/METADATA +280 -0
- goosebit-0.2.6.dist-info/RECORD +133 -0
- {goosebit-0.2.4.dist-info → goosebit-0.2.6.dist-info}/WHEEL +1 -1
- goosebit-0.2.6.dist-info/entry_points.txt +3 -0
- goosebit/realtime/logs.py +0 -42
- goosebit/realtime/routes.py +0 -13
- goosebit/ui/static/js/index.js +0 -155
- goosebit/ui/templates/index.html.jinja +0 -25
- goosebit/updater/manager.py +0 -357
- goosebit-0.2.4.dist-info/METADATA +0 -181
- goosebit-0.2.4.dist-info/RECORD +0 -98
- /goosebit/{realtime → api/v1/settings}/__init__.py +0 -0
- {goosebit-0.2.4.dist-info → goosebit-0.2.6.dist-info}/LICENSE +0 -0
goosebit/ui/static/js/index.js
DELETED
@@ -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 %}
|
goosebit/updater/manager.py
DELETED
@@ -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"
|