goosebit 0.1.0__py3-none-any.whl → 0.1.1__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 +3 -3
- goosebit/api/devices.py +6 -2
- goosebit/api/firmware.py +2 -15
- goosebit/api/rollouts.py +36 -0
- goosebit/api/routes.py +2 -1
- goosebit/auth/__init__.py +28 -20
- goosebit/models.py +22 -3
- goosebit/permissions.py +20 -6
- goosebit/realtime/logs.py +2 -1
- goosebit/settings.py +40 -29
- goosebit/ui/routes.py +13 -9
- goosebit/ui/static/js/devices.js +17 -2
- goosebit/ui/static/js/firmware.js +10 -1
- goosebit/ui/static/js/index.js +11 -1
- goosebit/ui/static/js/logs.js +4 -0
- goosebit/ui/static/js/rollouts.js +56 -0
- goosebit/ui/templates/devices.html +9 -0
- goosebit/ui/templates/firmware.html +2 -0
- goosebit/ui/templates/index.html +3 -0
- goosebit/ui/templates/logs.html +5 -0
- goosebit/ui/templates/nav.html +4 -2
- goosebit/ui/templates/rollouts.html +53 -0
- goosebit/updater/controller/v1/routes.py +80 -10
- goosebit/updater/download/v1/routes.py +2 -15
- goosebit/updater/manager.py +76 -35
- goosebit/updater/misc.py +23 -35
- goosebit/updater/routes.py +3 -1
- goosebit/updates/__init__.py +0 -0
- goosebit/{updater/updates.py → updates/artifacts.py} +10 -14
- goosebit/updates/version.py +38 -0
- goosebit-0.1.1.dist-info/METADATA +73 -0
- goosebit-0.1.1.dist-info/RECORD +53 -0
- goosebit-0.1.0.dist-info/METADATA +0 -37
- goosebit-0.1.0.dist-info/RECORD +0 -48
- {goosebit-0.1.0.dist-info → goosebit-0.1.1.dist-info}/LICENSE +0 -0
- {goosebit-0.1.0.dist-info → goosebit-0.1.1.dist-info}/WHEEL +0 -0
@@ -19,6 +19,8 @@
|
|
19
19
|
</div>
|
20
20
|
<h3>Upload Progress</h3>
|
21
21
|
<div class="col">
|
22
|
+
<div id="upload-alerts">
|
23
|
+
</div>
|
22
24
|
<div class="progress w-100" style=" height: 40px;" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
|
23
25
|
<div class="progress-bar progress-bar-striped progress-bar-animated" id="upload-progress" style="width: 0%;">0%</div>
|
24
26
|
</div>
|
goosebit/ui/templates/index.html
CHANGED
goosebit/ui/templates/logs.html
CHANGED
@@ -7,6 +7,11 @@
|
|
7
7
|
<div class="card-header">
|
8
8
|
<h3>Logs - {{ device }}</h3>
|
9
9
|
</div>
|
10
|
+
<div class="card-header">
|
11
|
+
<div class="progress m-2" role="progressbar" aria-label="Basic example" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
|
12
|
+
<div class="progress-bar progress-bar-striped progress-bar-animated" id="install-progress" style="width: 0%"></div>
|
13
|
+
</div>
|
14
|
+
</div>
|
10
15
|
<div class="card-body">
|
11
16
|
<pre id="device-log"></pre>
|
12
17
|
</div>
|
goosebit/ui/templates/nav.html
CHANGED
@@ -34,7 +34,7 @@
|
|
34
34
|
<div class="container-fluid">
|
35
35
|
<a class="navbar-brand" href="/ui/home">
|
36
36
|
<img src="{{ request.url_for('static', path='svg/goosebit-logo.svg') }}" class="me-2" style="height:30px; width: 30px;"/>
|
37
|
-
|
37
|
+
gooseBit
|
38
38
|
</a>
|
39
39
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
|
40
40
|
<span class="navbar-toggler-icon"></span>
|
@@ -50,7 +50,9 @@
|
|
50
50
|
{% if "devices.read" in request.user.permissions %}
|
51
51
|
<a class="nav-link{% if request.url.path.endswith('devices') %} active{% endif %}" href="/ui/devices">Devices</a>
|
52
52
|
{% endif %}
|
53
|
-
|
53
|
+
{% if "rollouts.read" in request.user.permissions %}
|
54
|
+
<a class="nav-link{% if request.url.path.endswith('rollouts') %} active{% endif %}" href="/ui/rollouts">Rollouts</a>
|
55
|
+
{% endif %}
|
54
56
|
</div>
|
55
57
|
<div class="navbar-nav d-flex flex-fill justify-content-end">
|
56
58
|
<a class="nav-link" href="/logout">Logout<i class="bi bi-box-arrow-right ps-2"></i></a>
|
@@ -0,0 +1,53 @@
|
|
1
|
+
{% extends "nav.html" %}
|
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="rollout-table" class="table table-hover">
|
7
|
+
<thead>
|
8
|
+
<tr>
|
9
|
+
<th>
|
10
|
+
Id
|
11
|
+
</th>
|
12
|
+
<th>
|
13
|
+
Created
|
14
|
+
</th>
|
15
|
+
<th>
|
16
|
+
Name
|
17
|
+
</th>
|
18
|
+
<th>
|
19
|
+
Model
|
20
|
+
</th>
|
21
|
+
<th>
|
22
|
+
Revision
|
23
|
+
</th>
|
24
|
+
<th>
|
25
|
+
Feed
|
26
|
+
</th>
|
27
|
+
<th>
|
28
|
+
Flavour
|
29
|
+
</th>
|
30
|
+
<th>
|
31
|
+
Update File
|
32
|
+
</th>
|
33
|
+
<th>
|
34
|
+
Paused
|
35
|
+
</th>
|
36
|
+
<th>
|
37
|
+
Success Count
|
38
|
+
</th>
|
39
|
+
<th>
|
40
|
+
Failure Count
|
41
|
+
</th>
|
42
|
+
</tr>
|
43
|
+
</thead>
|
44
|
+
<tbody id="rollouts-list">
|
45
|
+
|
46
|
+
</tbody>
|
47
|
+
</table>
|
48
|
+
</div>
|
49
|
+
</div>
|
50
|
+
</div>
|
51
|
+
|
52
|
+
<script src="{{ url_for('static', path='js/rollouts.js') }}"></script>
|
53
|
+
{% endblock content %}
|
@@ -3,6 +3,7 @@ import json
|
|
3
3
|
from fastapi import APIRouter, Depends
|
4
4
|
from fastapi.requests import Request
|
5
5
|
|
6
|
+
from goosebit.settings import POLL_TIME_REGISTRATION
|
6
7
|
from goosebit.updater.manager import UpdateManager, get_update_manager
|
7
8
|
|
8
9
|
# v1 is hardware revision
|
@@ -16,17 +17,47 @@ async def polling(
|
|
16
17
|
dev_id: str,
|
17
18
|
updater: UpdateManager = Depends(get_update_manager),
|
18
19
|
):
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
20
|
+
links = {}
|
21
|
+
|
22
|
+
sleep = updater.poll_time
|
23
|
+
last_state = updater.device.last_state
|
24
|
+
|
25
|
+
if last_state == "unknown":
|
26
|
+
# device registration
|
27
|
+
sleep = POLL_TIME_REGISTRATION
|
28
|
+
links["configData"] = {
|
29
|
+
"href": str(
|
30
|
+
request.url_for(
|
31
|
+
"config_data",
|
32
|
+
tenant=tenant,
|
33
|
+
dev_id=dev_id,
|
34
|
+
)
|
35
|
+
)
|
36
|
+
}
|
37
|
+
|
38
|
+
elif last_state == "error" and not updater.force_update:
|
39
|
+
# nothing to do
|
40
|
+
pass
|
41
|
+
|
42
|
+
else:
|
43
|
+
# provide update if available. Note: this is also required while in state "running", otherwise swupdate
|
44
|
+
# won't confirm a successful testing (might be a bug/problem in swupdate)
|
45
|
+
update = await updater.get_update_mode()
|
46
|
+
if update != "skip":
|
47
|
+
links["deploymentBase"] = {
|
23
48
|
"href": str(
|
24
49
|
request.url_for(
|
25
|
-
"deployment_base",
|
50
|
+
"deployment_base",
|
51
|
+
tenant=tenant,
|
52
|
+
dev_id=dev_id,
|
53
|
+
action_id=1,
|
26
54
|
)
|
27
55
|
)
|
28
|
-
}
|
29
|
-
|
56
|
+
}
|
57
|
+
|
58
|
+
return {
|
59
|
+
"config": {"polling": {"sleep": sleep}},
|
60
|
+
"_links": links,
|
30
61
|
}
|
31
62
|
|
32
63
|
|
@@ -34,11 +65,12 @@ async def polling(
|
|
34
65
|
async def config_data(
|
35
66
|
request: Request,
|
36
67
|
dev_id: str,
|
68
|
+
tenant: str,
|
37
69
|
updater: UpdateManager = Depends(get_update_manager),
|
38
70
|
):
|
39
71
|
data = await request.json()
|
40
72
|
# TODO: make standard schema to deal with this
|
41
|
-
|
73
|
+
await updater.update_config_data(**data["data"])
|
42
74
|
return {"success": True, "message": "Updated swupdate data."}
|
43
75
|
|
44
76
|
|
@@ -77,8 +109,46 @@ async def deployment_feedback(
|
|
77
109
|
except json.JSONDecodeError:
|
78
110
|
return
|
79
111
|
try:
|
80
|
-
|
81
|
-
|
112
|
+
execution = data["status"]["execution"]
|
113
|
+
|
114
|
+
if execution == "proceeding":
|
115
|
+
await updater.update_device_state("running")
|
116
|
+
|
117
|
+
elif execution == "closed":
|
118
|
+
state = data["status"]["result"]["finished"]
|
119
|
+
|
120
|
+
updater.force_update = False
|
121
|
+
updater.update_complete = True
|
122
|
+
|
123
|
+
# From hawkBit docu: DDI defines also a status NONE which will not be interpreted by the update server
|
124
|
+
# and handled like SUCCESS.
|
125
|
+
if state == "success" or state == "none":
|
126
|
+
await updater.update_device_state("finished")
|
127
|
+
|
128
|
+
# not guaranteed to be the correct rollout - see next comment.
|
129
|
+
rollout = await updater.get_rollout()
|
130
|
+
if rollout:
|
131
|
+
file = rollout.fw_file
|
132
|
+
rollout.success_count += 1
|
133
|
+
await rollout.save()
|
134
|
+
else:
|
135
|
+
device = await updater.get_device()
|
136
|
+
file = device.fw_file
|
137
|
+
|
138
|
+
# setting the currently installed version based on the current assigned firmware / existing rollouts
|
139
|
+
# is problematic. Better to assign custom action_id for each update (rollout id? firmware id? new id?).
|
140
|
+
# Alternatively - but requires customization on the gateway side - use version reported by the gateway.
|
141
|
+
await updater.update_fw_version(file)
|
142
|
+
|
143
|
+
elif state == "failure":
|
144
|
+
await updater.update_device_state("error")
|
145
|
+
|
146
|
+
# not guaranteed to be the correct rollout - see comment above.
|
147
|
+
rollout = await updater.get_rollout()
|
148
|
+
if rollout:
|
149
|
+
rollout.failure_count += 1
|
150
|
+
await rollout.save()
|
151
|
+
|
82
152
|
except KeyError:
|
83
153
|
pass
|
84
154
|
|
@@ -1,9 +1,8 @@
|
|
1
|
-
from fastapi import APIRouter
|
1
|
+
from fastapi import APIRouter
|
2
2
|
from fastapi.requests import Request
|
3
3
|
from fastapi.responses import FileResponse
|
4
4
|
|
5
|
-
from goosebit.settings import
|
6
|
-
from goosebit.updater.manager import UpdateManager, get_update_manager
|
5
|
+
from goosebit.settings import UPDATES_DIR
|
7
6
|
|
8
7
|
router = APIRouter(prefix="/v1")
|
9
8
|
|
@@ -12,15 +11,3 @@ router = APIRouter(prefix="/v1")
|
|
12
11
|
async def download_file(request: Request, tenant: str, dev_id: str, file: str):
|
13
12
|
filename = UPDATES_DIR.joinpath(file)
|
14
13
|
return FileResponse(filename, media_type="application/octet-stream")
|
15
|
-
|
16
|
-
|
17
|
-
@router.get("/{dev_id}/cfd_conf/{file}")
|
18
|
-
async def download_cfd_conf(
|
19
|
-
request: Request,
|
20
|
-
tenant: str,
|
21
|
-
dev_id: str,
|
22
|
-
file: str,
|
23
|
-
updater: UpdateManager = Depends(get_update_manager),
|
24
|
-
):
|
25
|
-
filename = TOKEN_SWU_DIR.joinpath(dev_id, file)
|
26
|
-
return FileResponse(filename, media_type="application/octet-stream")
|
goosebit/updater/manager.py
CHANGED
@@ -1,21 +1,21 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import asyncio
|
4
|
+
import re
|
4
5
|
from abc import ABC, abstractmethod
|
5
6
|
from contextlib import asynccontextmanager
|
6
|
-
from
|
7
|
+
from datetime import datetime
|
8
|
+
from typing import Callable, Optional
|
7
9
|
|
8
|
-
import
|
9
|
-
|
10
|
-
from goosebit.
|
11
|
-
from goosebit.settings import POLL_TIME, SWUPDATE_FILES_DIR, TOKEN_SWU_DIR
|
12
|
-
from goosebit.updater.misc import get_newest_fw
|
13
|
-
from goosebit.updater.updates import FirmwareArtifact
|
10
|
+
from goosebit.models import Device, Rollout
|
11
|
+
from goosebit.settings import POLL_TIME, POLL_TIME_UPDATING
|
12
|
+
from goosebit.updates.artifacts import FirmwareArtifact
|
14
13
|
|
15
14
|
|
16
15
|
class UpdateManager(ABC):
|
17
16
|
def __init__(self, dev_id: str):
|
18
17
|
self.dev_id = dev_id
|
18
|
+
self.config_data = {}
|
19
19
|
self.device = None
|
20
20
|
self.force_update = False
|
21
21
|
self.update_complete = False
|
@@ -31,26 +31,34 @@ class UpdateManager(ABC):
|
|
31
31
|
async def update_fw_version(self, version: str) -> None:
|
32
32
|
return
|
33
33
|
|
34
|
-
async def
|
34
|
+
async def update_hw_model(self, hw_model: str) -> None:
|
35
35
|
return
|
36
36
|
|
37
|
-
async def
|
37
|
+
async def update_hw_revision(self, hw_revision: str) -> None:
|
38
38
|
return
|
39
39
|
|
40
|
-
async def
|
40
|
+
async def update_device_state(self, state: str) -> None:
|
41
41
|
return
|
42
42
|
|
43
|
-
async def
|
43
|
+
async def update_last_seen(self, last_seen: int) -> None:
|
44
44
|
return
|
45
45
|
|
46
|
-
async def
|
46
|
+
async def update_last_ip(self, last_ip: str) -> None:
|
47
47
|
return
|
48
48
|
|
49
|
-
async def
|
50
|
-
return
|
49
|
+
async def get_rollout(self) -> Optional[Rollout]:
|
50
|
+
return None
|
51
51
|
|
52
|
-
async def
|
53
|
-
|
52
|
+
async def update_config_data(self, **kwargs):
|
53
|
+
await self.update_hw_model(kwargs.get("hw_model") or "default")
|
54
|
+
await self.update_hw_revision(kwargs.get("hw_revision") or "default")
|
55
|
+
|
56
|
+
device = await self.get_device()
|
57
|
+
if device.last_state == "unknown":
|
58
|
+
await self.update_device_state("registered")
|
59
|
+
await self.save()
|
60
|
+
|
61
|
+
self.config_data.update(kwargs)
|
54
62
|
|
55
63
|
@asynccontextmanager
|
56
64
|
async def subscribe_log(self, callback: Callable):
|
@@ -64,14 +72,15 @@ class UpdateManager(ABC):
|
|
64
72
|
finally:
|
65
73
|
self.log_subscribers.remove(callback)
|
66
74
|
|
75
|
+
@property
|
76
|
+
def poll_seconds(self):
|
77
|
+
time_obj = datetime.strptime(self.poll_time, "%H:%M:%S")
|
78
|
+
return time_obj.hour * 3600 + time_obj.minute * 60 + time_obj.second
|
79
|
+
|
67
80
|
async def publish_log(self, log_data: str | None):
|
68
81
|
for cb in self.log_subscribers:
|
69
82
|
await cb(log_data)
|
70
83
|
|
71
|
-
@property
|
72
|
-
def cfd_provisioned(self) -> bool:
|
73
|
-
return False
|
74
|
-
|
75
84
|
@abstractmethod
|
76
85
|
async def get_update_file(self) -> FirmwareArtifact: ...
|
77
86
|
|
@@ -85,10 +94,10 @@ class UpdateManager(ABC):
|
|
85
94
|
class UnknownUpdateManager(UpdateManager):
|
86
95
|
def __init__(self, dev_id: str):
|
87
96
|
super().__init__(dev_id)
|
88
|
-
self.poll_time =
|
97
|
+
self.poll_time = POLL_TIME_UPDATING
|
89
98
|
|
90
99
|
async def get_update_file(self) -> FirmwareArtifact:
|
91
|
-
return FirmwareArtifact(
|
100
|
+
return FirmwareArtifact("latest")
|
92
101
|
|
93
102
|
async def get_update_mode(self) -> str:
|
94
103
|
return "forced"
|
@@ -111,6 +120,14 @@ class DeviceUpdateManager(UpdateManager):
|
|
111
120
|
device = await self.get_device()
|
112
121
|
device.fw_version = version
|
113
122
|
|
123
|
+
async def update_hw_model(self, hw_model: str) -> None:
|
124
|
+
device = await self.get_device()
|
125
|
+
device.hw_model = hw_model
|
126
|
+
|
127
|
+
async def update_hw_revision(self, hw_revision: str) -> None:
|
128
|
+
device = await self.get_device()
|
129
|
+
device.hw_revision = hw_revision
|
130
|
+
|
114
131
|
async def update_device_state(self, state: str) -> None:
|
115
132
|
device = await self.get_device()
|
116
133
|
device.last_state = state
|
@@ -126,13 +143,33 @@ class DeviceUpdateManager(UpdateManager):
|
|
126
143
|
else:
|
127
144
|
device.last_ip = last_ip
|
128
145
|
|
146
|
+
async def get_rollout(self) -> Optional[Rollout]:
|
147
|
+
device = await self.get_device()
|
148
|
+
|
149
|
+
if device.fw_file == "none":
|
150
|
+
return (
|
151
|
+
await Rollout.filter(
|
152
|
+
hw_model=device.hw_model,
|
153
|
+
hw_revision=device.hw_revision,
|
154
|
+
feed=device.feed,
|
155
|
+
flavor=device.flavor,
|
156
|
+
)
|
157
|
+
.order_by("-created_at")
|
158
|
+
.first()
|
159
|
+
)
|
160
|
+
|
161
|
+
return None
|
162
|
+
|
129
163
|
async def get_update_file(self) -> FirmwareArtifact:
|
130
164
|
device = await self.get_device()
|
131
|
-
file =
|
165
|
+
file = device.fw_file
|
166
|
+
|
167
|
+
if file == "none":
|
168
|
+
rollout = await self.get_rollout()
|
169
|
+
if rollout and not rollout.paused:
|
170
|
+
file = rollout.fw_file
|
132
171
|
|
133
|
-
|
134
|
-
return file
|
135
|
-
return file
|
172
|
+
return FirmwareArtifact(file, device.hw_model, device.hw_revision)
|
136
173
|
|
137
174
|
async def get_update_mode(self) -> str:
|
138
175
|
device = await self.get_device()
|
@@ -141,20 +178,19 @@ class DeviceUpdateManager(UpdateManager):
|
|
141
178
|
if file.is_empty():
|
142
179
|
mode = "skip"
|
143
180
|
self.poll_time = POLL_TIME
|
144
|
-
elif file.name == device.fw_version:
|
181
|
+
elif file.name == device.fw_version and not self.force_update:
|
182
|
+
mode = "skip"
|
183
|
+
self.poll_time = POLL_TIME
|
184
|
+
elif device.last_state == "error" and not self.force_update:
|
145
185
|
mode = "skip"
|
146
186
|
self.poll_time = POLL_TIME
|
147
187
|
else:
|
148
188
|
mode = "forced"
|
149
|
-
self.poll_time =
|
189
|
+
self.poll_time = POLL_TIME_UPDATING
|
150
190
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
if mode == "forced" and self.update_complete:
|
156
|
-
self.update_complete = False
|
157
|
-
await self.clear_log()
|
191
|
+
if self.update_complete:
|
192
|
+
self.update_complete = False
|
193
|
+
await self.clear_log()
|
158
194
|
|
159
195
|
return mode
|
160
196
|
|
@@ -162,6 +198,9 @@ class DeviceUpdateManager(UpdateManager):
|
|
162
198
|
if log_data is None:
|
163
199
|
return
|
164
200
|
device = await self.get_device()
|
201
|
+
matches = re.findall(r"Downloaded (\d+)%", log_data)
|
202
|
+
if matches:
|
203
|
+
device.progress = matches[-1]
|
165
204
|
if device.last_log is None:
|
166
205
|
device.last_log = ""
|
167
206
|
if log_data.startswith("Installing Update Chunk Artifacts."):
|
@@ -172,10 +211,12 @@ class DeviceUpdateManager(UpdateManager):
|
|
172
211
|
if not log_data == "Skipped Update.":
|
173
212
|
device.last_log += f"{log_data}\n"
|
174
213
|
await self.publish_log(f"{log_data}\n")
|
214
|
+
await device.save()
|
175
215
|
|
176
216
|
async def clear_log(self) -> None:
|
177
217
|
device = await self.get_device()
|
178
218
|
device.last_log = ""
|
219
|
+
await device.save()
|
179
220
|
await self.publish_log(None)
|
180
221
|
|
181
222
|
|
goosebit/updater/misc.py
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import datetime
|
4
3
|
import hashlib
|
5
4
|
from pathlib import Path
|
5
|
+
from typing import Optional
|
6
6
|
|
7
7
|
from goosebit.models import Device
|
8
|
-
from goosebit.settings import UPDATES_DIR
|
8
|
+
from goosebit.settings import UPDATE_VERSION_PARSER, UPDATES_DIR
|
9
9
|
|
10
10
|
|
11
11
|
def sha1_hash_file(file_path: Path):
|
@@ -14,46 +14,34 @@ def sha1_hash_file(file_path: Path):
|
|
14
14
|
return sha1_hash.hexdigest()
|
15
15
|
|
16
16
|
|
17
|
-
def get_newest_fw() -> str:
|
18
|
-
|
17
|
+
def get_newest_fw(hw_model: str, hw_revision: str) -> Optional[str]:
|
18
|
+
def filter_filename(filename, hw_model, hw_revision) -> bool:
|
19
|
+
image_data = filename.split("_")
|
20
|
+
assert len(image_data) == 3
|
21
|
+
model, revision, _ = image_data
|
22
|
+
return model == hw_model and revision == hw_revision
|
23
|
+
|
24
|
+
fw_files = [
|
25
|
+
f
|
26
|
+
for f in UPDATES_DIR.iterdir()
|
27
|
+
if f.suffix == ".swu" and filter_filename(f.name, hw_model, hw_revision)
|
28
|
+
]
|
19
29
|
if len(fw_files) == 0:
|
20
|
-
return
|
30
|
+
return None
|
21
31
|
|
22
32
|
return str(sorted(fw_files, key=lambda x: fw_sort_key(x), reverse=True)[0].name)
|
23
33
|
|
24
34
|
|
25
|
-
def
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
else:
|
32
|
-
return datetime.datetime.now()
|
33
|
-
|
34
|
-
return datetime.datetime.strptime(f"{date}_{time}", "%Y%m%d_%H%M%S")
|
35
|
+
def validate_filename(filename: str) -> bool:
|
36
|
+
try:
|
37
|
+
fw_sort_key(Path(filename))
|
38
|
+
return True
|
39
|
+
except ValueError:
|
40
|
+
return False
|
35
41
|
|
36
42
|
|
37
|
-
def
|
38
|
-
|
39
|
-
if len(image_data) == 3:
|
40
|
-
tenant, date, time = image_data
|
41
|
-
return {
|
42
|
-
"date": datetime.datetime.strptime(f"{date}_{time}", "%Y%m%d_%H%M%S"),
|
43
|
-
"day": date,
|
44
|
-
"time": time,
|
45
|
-
"tenant": tenant,
|
46
|
-
"hw_version": 0,
|
47
|
-
}
|
48
|
-
elif len(image_data) == 4:
|
49
|
-
tenant, hw_version, date, time = image_data
|
50
|
-
return {
|
51
|
-
"date": datetime.datetime.strptime(f"{date}_{time}", "%Y%m%d_%H%M%S"),
|
52
|
-
"day": date,
|
53
|
-
"time": time,
|
54
|
-
"tenant": tenant,
|
55
|
-
"hw_version": int(hw_version.upper().replace("V", "")),
|
56
|
-
}
|
43
|
+
def fw_sort_key(filename: Path):
|
44
|
+
return UPDATE_VERSION_PARSER.parse(filename)
|
57
45
|
|
58
46
|
|
59
47
|
async def get_device_by_uuid(dev_id: str) -> Device:
|
goosebit/updater/routes.py
CHANGED
@@ -3,12 +3,14 @@ import time
|
|
3
3
|
from fastapi import APIRouter, Depends, HTTPException
|
4
4
|
from fastapi.requests import Request
|
5
5
|
|
6
|
+
from goosebit.settings import TENANT
|
7
|
+
|
6
8
|
from . import controller, download
|
7
9
|
from .manager import get_update_manager_sync
|
8
10
|
|
9
11
|
|
10
12
|
async def verify_tenant(tenant: str):
|
11
|
-
if not tenant ==
|
13
|
+
if not tenant == TENANT:
|
12
14
|
raise HTTPException(404)
|
13
15
|
return tenant
|
14
16
|
|
File without changes
|
@@ -4,19 +4,20 @@ from typing import Optional
|
|
4
4
|
|
5
5
|
from fastapi.requests import Request
|
6
6
|
|
7
|
-
from goosebit.settings import
|
7
|
+
from goosebit.settings import UPDATES_DIR
|
8
8
|
from goosebit.updater.misc import get_newest_fw, sha1_hash_file
|
9
9
|
|
10
10
|
|
11
11
|
class FirmwareArtifact:
|
12
|
-
def __init__(self, file: str = None,
|
12
|
+
def __init__(self, file: str = None, hw_model: str = None, hw_revision: str = None):
|
13
13
|
if file == "latest":
|
14
|
-
self.file = get_newest_fw()
|
14
|
+
self.file = get_newest_fw(hw_model, hw_revision)
|
15
15
|
elif file == "pinned":
|
16
16
|
self.file = None
|
17
|
+
elif file == "none":
|
18
|
+
self.file = None
|
17
19
|
else:
|
18
20
|
self.file = file
|
19
|
-
self.dev_id = dev_id
|
20
21
|
|
21
22
|
def __eq__(self, other):
|
22
23
|
if isinstance(other, str):
|
@@ -31,19 +32,18 @@ class FirmwareArtifact:
|
|
31
32
|
def file_exists(self) -> bool:
|
32
33
|
if self.is_empty():
|
33
34
|
return False
|
34
|
-
if self.file == "cloudflared.swu":
|
35
|
-
return True
|
36
35
|
return self.path.exists()
|
37
36
|
|
38
37
|
@property
|
39
|
-
def name(self):
|
40
|
-
|
41
|
-
return self.file.split(".")[0]
|
38
|
+
def name(self) -> Optional[str]:
|
39
|
+
return self.file
|
42
40
|
|
43
41
|
@property
|
44
42
|
def version(self):
|
45
43
|
if not self.is_empty():
|
46
|
-
|
44
|
+
image_data = self.name.split("_")
|
45
|
+
assert len(image_data) == 3
|
46
|
+
return "_".join(image_data[1:])
|
47
47
|
|
48
48
|
@property
|
49
49
|
def timestamp(self):
|
@@ -52,14 +52,10 @@ class FirmwareArtifact:
|
|
52
52
|
@property
|
53
53
|
def path(self) -> Optional[Path]:
|
54
54
|
if not self.is_empty():
|
55
|
-
if self.file == "cloudflared.swu":
|
56
|
-
return TOKEN_SWU_DIR.joinpath(self.dev_id, self.file)
|
57
55
|
return UPDATES_DIR.joinpath(self.file)
|
58
56
|
|
59
57
|
@property
|
60
58
|
def dl_endpoint(self):
|
61
|
-
if self.file == "cloudflared.swu":
|
62
|
-
return "download_cfd_conf"
|
63
59
|
return "download_file"
|
64
60
|
|
65
61
|
def generate_chunk(self, request: Request, tenant: str, dev_id: str) -> list:
|