goosebit 0.2.5__py3-none-any.whl → 0.2.7__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 +41 -7
- 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 +68 -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 +49 -13
- goosebit/auth/permissions.py +80 -0
- goosebit/db/config.py +57 -1
- 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 +19 -8
- goosebit/db/pg_ssl_context.py +51 -0
- goosebit/device_manager.py +262 -0
- goosebit/plugins/__init__.py +32 -0
- goosebit/schema/devices.py +8 -5
- 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 +60 -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/responses.py +1 -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/routes.py +49 -46
- goosebit/ui/bff/download/routes.py +14 -3
- goosebit/ui/bff/rollouts/routes.py +32 -4
- goosebit/ui/bff/routes.py +2 -1
- 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/routes.py +40 -12
- goosebit/ui/nav.py +12 -2
- goosebit/ui/routes.py +66 -13
- goosebit/ui/static/js/devices.js +32 -24
- goosebit/ui/static/js/login.js +21 -5
- goosebit/ui/static/js/logs.js +7 -22
- goosebit/ui/static/js/rollouts.js +31 -30
- goosebit/ui/static/js/settings.js +322 -0
- goosebit/ui/static/js/setup.js +28 -0
- goosebit/ui/static/js/software.js +127 -121
- goosebit/ui/static/js/util.js +25 -4
- goosebit/ui/templates/__init__.py +10 -1
- goosebit/ui/templates/login.html.jinja +5 -0
- goosebit/ui/templates/nav.html.jinja +13 -5
- 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 +119 -77
- goosebit/updater/routes.py +83 -8
- 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.7.dist-info/METADATA +280 -0
- goosebit-0.2.7.dist-info/RECORD +133 -0
- {goosebit-0.2.5.dist-info → goosebit-0.2.7.dist-info}/WHEEL +1 -1
- goosebit/realtime/logs.py +0 -42
- goosebit/realtime/routes.py +0 -13
- goosebit/updater/manager.py +0 -325
- goosebit-0.2.5.dist-info/METADATA +0 -189
- goosebit-0.2.5.dist-info/RECORD +0 -99
- /goosebit/{realtime → api/v1/settings}/__init__.py +0 -0
- {goosebit-0.2.5.dist-info → goosebit-0.2.7.dist-info}/LICENSE +0 -0
- {goosebit-0.2.5.dist-info → goosebit-0.2.7.dist-info}/entry_points.txt +0 -0
@@ -31,11 +31,15 @@
|
|
31
31
|
alt="gooseBit Logo" />
|
32
32
|
</div>
|
33
33
|
</div>
|
34
|
+
<div id="login_error" class="alert alert-danger d-none" role="alert">
|
35
|
+
<!-- Error message will be inserted here -->
|
36
|
+
</div>
|
34
37
|
<form method="post" id="login_form">
|
35
38
|
<div class="form-outline form-white mb-4">
|
36
39
|
<input type="email"
|
37
40
|
id="username"
|
38
41
|
name="username"
|
42
|
+
autocomplete="username"
|
39
43
|
placeholder="Email"
|
40
44
|
class="form-control form-control-lg" />
|
41
45
|
</div>
|
@@ -43,6 +47,7 @@
|
|
43
47
|
<input type="password"
|
44
48
|
id="password"
|
45
49
|
name="password"
|
50
|
+
autocomplete="current-password"
|
46
51
|
placeholder="Password"
|
47
52
|
class="form-control form-control-lg" />
|
48
53
|
</div>
|
@@ -1,7 +1,7 @@
|
|
1
1
|
<!DOCTYPE html>
|
2
2
|
<html lang="en">
|
3
3
|
<head>
|
4
|
-
<script>const PERMISSIONS = {{request.user.
|
4
|
+
<script>const PERMISSIONS = {{request.user.permissions | safe}};</script>
|
5
5
|
<meta charset="utf-8" />
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
7
7
|
<title>{{ title }}</title>
|
@@ -84,6 +84,7 @@
|
|
84
84
|
width="30px"
|
85
85
|
alt="gooseBit logo" />
|
86
86
|
gooseBit
|
87
|
+
<small class="text-muted">v{{ request.app.version }}</small>
|
87
88
|
</a>
|
88
89
|
<button class="navbar-toggler"
|
89
90
|
type="button"
|
@@ -97,14 +98,21 @@
|
|
97
98
|
<div class="collapse navbar-collapse" id="navbar">
|
98
99
|
<div class="navbar-nav">
|
99
100
|
{% for nav_item in request.nav %}
|
100
|
-
{% if
|
101
|
-
|
102
|
-
|
101
|
+
{% if nav_item.show %}
|
102
|
+
{% if compare_permissions(nav_item.permissions, request.user.permissions) %}
|
103
|
+
<a class="nav-link{% if request.url == request.url_for(nav_item.function) %} active{% endif %}"
|
104
|
+
href="{{ request.url_for(nav_item.function) }}">{{ nav_item.text }}</a>
|
105
|
+
{% endif %}
|
103
106
|
{% endif %}
|
104
107
|
{% endfor %}
|
105
108
|
</div>
|
106
109
|
<div class="navbar-nav d-flex flex-fill justify-content-end">
|
107
|
-
|
110
|
+
{% if compare_permissions("goosebit.settings", request.user.permissions) %}
|
111
|
+
<a class="nav-link ps-3 me-2 border-start"
|
112
|
+
href="{{ request.url_for('settings_ui') }}">Settings<i class="bi bi-gear ps-2"></i></a>
|
113
|
+
{% endif %}
|
114
|
+
<a class="nav-link ps-3 border-start"
|
115
|
+
href="{{ request.url_for('logout') }}">Logout<i class="bi bi-box-arrow-right ps-2"></i></a>
|
108
116
|
</div>
|
109
117
|
</div>
|
110
118
|
</div>
|
@@ -4,28 +4,13 @@
|
|
4
4
|
<div class="row p-2 d-flex justify-content-center">
|
5
5
|
<div class="col">
|
6
6
|
<table id="rollout-table" class="table table-hover">
|
7
|
-
<thead>
|
8
|
-
<tr>
|
9
|
-
<th>Id</th>
|
10
|
-
<th>Created</th>
|
11
|
-
<th>Name</th>
|
12
|
-
<th>Feed</th>
|
13
|
-
<th>Software File</th>
|
14
|
-
<th>Software Version</th>
|
15
|
-
<th>Paused</th>
|
16
|
-
<th>Success Count</th>
|
17
|
-
<th>Failure Count</th>
|
18
|
-
</tr>
|
19
|
-
</thead>
|
20
|
-
<tbody id="rollouts-list">
|
21
|
-
</tbody>
|
22
7
|
</table>
|
23
8
|
</div>
|
24
9
|
</div>
|
25
10
|
</div>
|
26
11
|
{% if compare_permissions(["rollout.write"], request.user.permissions) %}
|
27
|
-
<div class="modal" id="rollout-create-modal">
|
28
|
-
<div class="modal-dialog modal-
|
12
|
+
<div class="modal modal-lg fade" id="rollout-create-modal">
|
13
|
+
<div class="modal-dialog modal-dialog-centered modal-xl">
|
29
14
|
<div class="modal-content">
|
30
15
|
<div class="modal-header">
|
31
16
|
<h5 class="modal-title">Create Rollout</h5>
|
@@ -46,7 +31,7 @@
|
|
46
31
|
<input class="form-control" id="rollout-selected-feed" required />
|
47
32
|
<div class="invalid-feedback">Feed missing. Use "default" if working with a single feed.</div>
|
48
33
|
</div>
|
49
|
-
<div class="form-group
|
34
|
+
<div class="form-group">
|
50
35
|
<div class="input-group mb-3 has-validation">
|
51
36
|
<span class="input-group-text">Software</span>
|
52
37
|
<select class="form-control"
|
@@ -59,10 +44,7 @@
|
|
59
44
|
required></select>
|
60
45
|
<div class="invalid-feedback">Software missing.</div>
|
61
46
|
</div>
|
62
|
-
|
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>
|
47
|
+
<button type="submit" class="btn btn-outline-light w-100">Save changes</button>
|
66
48
|
</div>
|
67
49
|
</div>
|
68
50
|
</form>
|
@@ -0,0 +1,88 @@
|
|
1
|
+
{% extends "nav.html.jinja" %}
|
2
|
+
{% block content %}
|
3
|
+
<style>
|
4
|
+
input[type="checkbox"].ignore-validation:valid,
|
5
|
+
input[type="checkbox"].ignore-validation:invalid {
|
6
|
+
border-color: var(--bs-border-color);
|
7
|
+
}
|
8
|
+
</style>
|
9
|
+
<div class="container-fluid">
|
10
|
+
<ul class="nav nav-underline nav-fill my-2">
|
11
|
+
<li class="nav-item" role="presentation">
|
12
|
+
<button class="nav-link active"
|
13
|
+
id="settings-users-tab"
|
14
|
+
data-bs-toggle="tab"
|
15
|
+
data-bs-target="#settings-users-panel"
|
16
|
+
type="button"
|
17
|
+
role="tab">Users</button>
|
18
|
+
</li>
|
19
|
+
</ul>
|
20
|
+
<div class="tab-content mb-2">
|
21
|
+
<div class="tab-pane fade show active"
|
22
|
+
id="settings-users-panel"
|
23
|
+
role="tabpanel">
|
24
|
+
<table id="users-table" class="table table-hover">
|
25
|
+
</table>
|
26
|
+
</div>
|
27
|
+
</div>
|
28
|
+
</div>
|
29
|
+
{% if compare_permissions(["settings.write"], request.user.permissions) %}
|
30
|
+
<div class="modal modal-lg fade" id="create-user-modal">
|
31
|
+
<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered modal-xl">
|
32
|
+
<div class="modal-content">
|
33
|
+
<div class="modal-header">
|
34
|
+
<h5 class="modal-title">Create User</h5>
|
35
|
+
<button type="button"
|
36
|
+
class="btn-close"
|
37
|
+
data-bs-dismiss="modal"
|
38
|
+
aria-label="Close"></button>
|
39
|
+
</div>
|
40
|
+
<form id="create-user-form" class="needs-validation" novalidate>
|
41
|
+
<div class="modal-body">
|
42
|
+
<div class="input-group mb-3 has-validation">
|
43
|
+
<span class="input-group-text">Username</span>
|
44
|
+
<input class="form-control" type="text" id="create-user-username" required />
|
45
|
+
<div class="invalid-feedback">Username missing.</div>
|
46
|
+
</div>
|
47
|
+
<div class="input-group mb-3 has-validation">
|
48
|
+
<span class="input-group-text">Password</span>
|
49
|
+
<input class="form-control" type="text" id="create-user-password" required />
|
50
|
+
<div class="invalid-feedback">Password missing.</div>
|
51
|
+
</div>
|
52
|
+
<div class="input-group mb-3 has-validation">
|
53
|
+
<input type="checkbox"
|
54
|
+
id="create-user-permissions-validator"
|
55
|
+
required
|
56
|
+
style="display:none">
|
57
|
+
<div class="col" id="create-user-permissions"></div>
|
58
|
+
<div class="invalid-feedback">Please set at least one permission.</div>
|
59
|
+
</div>
|
60
|
+
<input class="btn btn-outline-light w-100"
|
61
|
+
id="create-user-submit"
|
62
|
+
type="submit"
|
63
|
+
value="Create User" />
|
64
|
+
</div>
|
65
|
+
</form>
|
66
|
+
</div>
|
67
|
+
</div>
|
68
|
+
</div>
|
69
|
+
{% else %}
|
70
|
+
<div class="modal modal-lg fade" id="upload-modal">
|
71
|
+
<div class="modal-dialog modal-dialog-centered modal-xl">
|
72
|
+
<div class="modal-content">
|
73
|
+
<div class="modal-header">
|
74
|
+
Unavailable
|
75
|
+
<button type="button"
|
76
|
+
class="btn-close"
|
77
|
+
data-bs-dismiss="modal"
|
78
|
+
aria-label="Close"></button>
|
79
|
+
</div>
|
80
|
+
<div class="modal-body">
|
81
|
+
<div class="alert alert-warning m-0" role="alert">You do not have permission to add users.</div>
|
82
|
+
</div>
|
83
|
+
</div>
|
84
|
+
</div>
|
85
|
+
</div>
|
86
|
+
{% endif %}
|
87
|
+
<script src="{{ url_for('static', path='js/settings.js') }}"></script>
|
88
|
+
{% endblock content %}
|
@@ -0,0 +1,71 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8" />
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
6
|
+
<title>Login</title>
|
7
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
8
|
+
rel="stylesheet"
|
9
|
+
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
|
10
|
+
crossorigin="anonymous" />
|
11
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
12
|
+
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
|
13
|
+
crossorigin="anonymous"></script>
|
14
|
+
<link rel="stylesheet"
|
15
|
+
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
|
16
|
+
<link rel="icon" href="{{ url_for('static', path='favicon.svg') }}" />
|
17
|
+
</head>
|
18
|
+
<body data-bs-theme="dark">
|
19
|
+
<div class="container py-2">
|
20
|
+
<div class="row d-flex justify-content-center align-items-center h-100">
|
21
|
+
<div class="col-12 col-md-8 col-lg-6 col-xl-5">
|
22
|
+
<div class="mb-md-5 mt-md-4 pb-5">
|
23
|
+
<div class="row mb-2 p-2">
|
24
|
+
<div class="col d-flex align-items-center">
|
25
|
+
<h2 class="fw-bold m-0 text-uppercase">gooseBit</h2>
|
26
|
+
</div>
|
27
|
+
<div class="col d-flex justify-content-end px-3">
|
28
|
+
<img src="{{ request.url_for('static', path='svg/goosebit-logo.svg') }}"
|
29
|
+
height="35px"
|
30
|
+
width="35px"
|
31
|
+
alt="gooseBit Logo" />
|
32
|
+
</div>
|
33
|
+
</div>
|
34
|
+
<div class="row mb-2 p-2">
|
35
|
+
<div class="col d-flex align-items-center">
|
36
|
+
<p class="m-0">Welcome to gooseBit. Please create an admin user.</p>
|
37
|
+
</div>
|
38
|
+
</div>
|
39
|
+
<form method="post" id="setup_form">
|
40
|
+
<div class="form-outline form-white mb-4">
|
41
|
+
<input type="email"
|
42
|
+
id="username"
|
43
|
+
name="username"
|
44
|
+
placeholder="Email"
|
45
|
+
class="form-control form-control-lg" />
|
46
|
+
</div>
|
47
|
+
<div class="form-outline form-white mb-4">
|
48
|
+
<input type="password"
|
49
|
+
id="password"
|
50
|
+
name="password"
|
51
|
+
placeholder="Password"
|
52
|
+
autocomplete="new-password"
|
53
|
+
class="form-control form-control-lg" />
|
54
|
+
</div>
|
55
|
+
<div class="form-outline form-white mb-4">
|
56
|
+
<input type="password"
|
57
|
+
id="password_confirm"
|
58
|
+
name="password_confirm"
|
59
|
+
placeholder="Confirm Password"
|
60
|
+
autocomplete="new-password"
|
61
|
+
class="form-control form-control-lg" />
|
62
|
+
</div>
|
63
|
+
<button class="btn btn-outline-light btn-lg px-5 w-100" type="submit">Setup</button>
|
64
|
+
</form>
|
65
|
+
</div>
|
66
|
+
</div>
|
67
|
+
</div>
|
68
|
+
</div>
|
69
|
+
<script src="{{ url_for('static', path='js/setup.js') }}"></script>
|
70
|
+
</body>
|
71
|
+
</html>
|
@@ -4,17 +4,6 @@
|
|
4
4
|
<div class="row p-2 pt-4 g-4 d-flex justify-content-center">
|
5
5
|
<div class="col">
|
6
6
|
<table id="software-table" class="table table-hover">
|
7
|
-
<thead>
|
8
|
-
<tr>
|
9
|
-
<th>ID</th>
|
10
|
-
<th>Name</th>
|
11
|
-
<th>Version</th>
|
12
|
-
<th>Compatibility</th>
|
13
|
-
<th>Size</th>
|
14
|
-
</tr>
|
15
|
-
</thead>
|
16
|
-
<tbody id="software-list">
|
17
|
-
</tbody>
|
18
7
|
</table>
|
19
8
|
</div>
|
20
9
|
</div>
|
@@ -2,11 +2,17 @@ import logging
|
|
2
2
|
|
3
3
|
from fastapi import APIRouter, Depends, HTTPException
|
4
4
|
from fastapi.requests import Request
|
5
|
-
from fastapi.responses import
|
5
|
+
from fastapi.responses import (
|
6
|
+
FileResponse,
|
7
|
+
RedirectResponse,
|
8
|
+
Response,
|
9
|
+
StreamingResponse,
|
10
|
+
)
|
6
11
|
|
7
|
-
from goosebit.db.models import Software, UpdateStateEnum
|
12
|
+
from goosebit.db.models import Device, Software, UpdateStateEnum
|
13
|
+
from goosebit.device_manager import DeviceManager, HandlingType, get_device
|
8
14
|
from goosebit.settings import config
|
9
|
-
from goosebit.
|
15
|
+
from goosebit.storage import storage
|
10
16
|
from goosebit.updates import generate_chunk
|
11
17
|
|
12
18
|
from .schema import (
|
@@ -22,53 +28,64 @@ router = APIRouter(prefix="/v1")
|
|
22
28
|
|
23
29
|
|
24
30
|
@router.get("/{dev_id}")
|
25
|
-
async def polling(request: Request,
|
31
|
+
async def polling(request: Request, device: Device = Depends(get_device)):
|
26
32
|
links: dict[str, dict[str, str]] = {}
|
27
33
|
|
28
|
-
device = await updater.get_device()
|
29
|
-
|
30
34
|
if device is None:
|
31
35
|
raise HTTPException(404)
|
32
36
|
|
37
|
+
sleep = config.poll_time
|
38
|
+
|
33
39
|
if device.last_state == UpdateStateEnum.UNKNOWN:
|
34
|
-
# device registration
|
35
|
-
sleep =
|
40
|
+
# device registration: force device to poll again in 10s. After registration, an update might be available
|
41
|
+
sleep = "00:00:10"
|
36
42
|
links["configData"] = {
|
37
43
|
"href": str(
|
38
44
|
request.url_for(
|
39
45
|
"config_data",
|
40
|
-
dev_id=
|
46
|
+
dev_id=device.id,
|
41
47
|
)
|
42
48
|
)
|
43
49
|
}
|
44
|
-
logger.info(f"Skip: registration required, device={
|
50
|
+
logger.info(f"Skip: registration required, device={device.id}")
|
45
51
|
|
46
52
|
elif device.last_state == UpdateStateEnum.ERROR and not device.force_update:
|
47
|
-
|
48
|
-
logger.info(f"Skip: device in error state, device={updater.dev_id}")
|
53
|
+
logger.info(f"Skip: device in error state, device={device.id}")
|
49
54
|
|
50
55
|
else:
|
51
56
|
# provide update if available. Note: this is also required while in state "running", otherwise swupdate
|
52
57
|
# won't confirm a successful testing (might be a bug/problem in swupdate)
|
53
|
-
handling_type, software = await
|
58
|
+
handling_type, software = await DeviceManager.get_update(device)
|
54
59
|
if handling_type != HandlingType.SKIP and software is not None:
|
55
|
-
|
56
|
-
|
57
|
-
"
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
60
|
+
number_of_running = await Device.filter(last_state=UpdateStateEnum.RUNNING).count()
|
61
|
+
if number_of_running < config.max_concurrent_updates or device.last_state == UpdateStateEnum.RUNNING:
|
62
|
+
links["deploymentBase"] = {
|
63
|
+
"href": str(
|
64
|
+
request.url_for(
|
65
|
+
"deployment_base",
|
66
|
+
dev_id=device.id,
|
67
|
+
action_id=software.id,
|
68
|
+
)
|
62
69
|
)
|
63
|
-
|
64
|
-
|
65
|
-
logger.info(f"Forced: update available, device={updater.dev_id}")
|
70
|
+
}
|
71
|
+
logger.info(f"Forced: update available, device={device.id}")
|
66
72
|
else:
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
73
|
+
number_of_running = await Device.filter(last_state=UpdateStateEnum.RUNNING).count()
|
74
|
+
if number_of_running < config.max_concurrent_updates or device.last_state == UpdateStateEnum.RUNNING:
|
75
|
+
plugin_sources = await DeviceManager.get_alt_src_updates(request, device)
|
76
|
+
for handling_type, _ in plugin_sources:
|
77
|
+
if handling_type == HandlingType.SKIP:
|
78
|
+
continue
|
79
|
+
links["deploymentBase"] = {
|
80
|
+
"href": str(
|
81
|
+
request.url_for(
|
82
|
+
"deployment_base",
|
83
|
+
dev_id=device.id,
|
84
|
+
action_id=-1, # custom plugin
|
85
|
+
)
|
86
|
+
)
|
87
|
+
}
|
88
|
+
break
|
72
89
|
return {
|
73
90
|
"config": {"polling": {"sleep": sleep}},
|
74
91
|
"_links": links,
|
@@ -76,9 +93,9 @@ async def polling(request: Request, dev_id: str, updater: UpdateManager = Depend
|
|
76
93
|
|
77
94
|
|
78
95
|
@router.put("/{dev_id}/configData")
|
79
|
-
async def config_data(_: Request, cfg: ConfigDataSchema,
|
80
|
-
await
|
81
|
-
logger.info(f"Updating config data, device={
|
96
|
+
async def config_data(_: Request, cfg: ConfigDataSchema, device: Device = Depends(get_device)):
|
97
|
+
await DeviceManager.update_config_data(device, **cfg.data)
|
98
|
+
logger.info(f"Updating config data, device={device.id}")
|
82
99
|
return {"success": True, "message": "Updated swupdate data."}
|
83
100
|
|
84
101
|
|
@@ -86,46 +103,56 @@ async def config_data(_: Request, cfg: ConfigDataSchema, updater: UpdateManager
|
|
86
103
|
async def deployment_base(
|
87
104
|
request: Request,
|
88
105
|
action_id: int,
|
89
|
-
|
106
|
+
device: Device = Depends(get_device),
|
90
107
|
):
|
91
|
-
handling_type, software = await
|
92
|
-
|
93
|
-
logger.info(f"Request deployment base, device={
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
108
|
+
handling_type, software = await DeviceManager.get_update(device)
|
109
|
+
|
110
|
+
logger.info(f"Request deployment base, device={device.id}")
|
111
|
+
if not handling_type == HandlingType.SKIP:
|
112
|
+
return {
|
113
|
+
"id": str(action_id),
|
114
|
+
"deployment": {
|
115
|
+
"download": str(handling_type),
|
116
|
+
"update": str(handling_type),
|
117
|
+
"chunks": [chunk.model_dump(by_alias=True) for chunk in (await generate_chunk(request, device))],
|
118
|
+
},
|
119
|
+
}
|
120
|
+
else:
|
121
|
+
plugin_sources = await DeviceManager.get_alt_src_updates(request, device)
|
122
|
+
for handling_type, chunk in plugin_sources:
|
123
|
+
if handling_type == HandlingType.SKIP or chunk is None:
|
124
|
+
continue
|
125
|
+
return {
|
126
|
+
"id": str(action_id),
|
127
|
+
"deployment": {
|
128
|
+
"download": str(handling_type),
|
129
|
+
"update": str(handling_type),
|
130
|
+
"chunks": [chunk.model_dump(by_alias=True)],
|
131
|
+
},
|
132
|
+
}
|
103
133
|
|
104
134
|
|
105
135
|
@router.post("/{dev_id}/deploymentBase/{action_id}/feedback")
|
106
|
-
async def deployment_feedback(
|
107
|
-
_: Request, data: FeedbackSchema, action_id: int, updater: UpdateManager = Depends(get_update_manager)
|
108
|
-
):
|
136
|
+
async def deployment_feedback(_: Request, data: FeedbackSchema, action_id: int, device: Device = Depends(get_device)):
|
109
137
|
if data.status.execution == FeedbackStatusExecutionState.PROCEEDING:
|
110
|
-
device = await updater.get_device()
|
111
138
|
if device and device.last_state != UpdateStateEnum.RUNNING:
|
112
|
-
await
|
113
|
-
await
|
139
|
+
await DeviceManager.deployment_action_start(device)
|
140
|
+
await DeviceManager.update_device_state(device, UpdateStateEnum.RUNNING)
|
114
141
|
|
115
|
-
logger.debug(f"Installation in progress, device={
|
142
|
+
logger.debug(f"Installation in progress, device={device.id}")
|
116
143
|
|
117
144
|
elif data.status.execution == FeedbackStatusExecutionState.CLOSED:
|
118
|
-
await
|
145
|
+
await DeviceManager.update_force_update(device, False)
|
119
146
|
|
120
147
|
reported_software = await Software.get_or_none(id=action_id)
|
121
148
|
|
122
149
|
# From hawkBit docu: DDI defines also a status NONE which will not be interpreted by the update server
|
123
150
|
# and handled like SUCCESS.
|
124
151
|
if data.status.result.finished in [FeedbackStatusResultFinished.SUCCESS, FeedbackStatusResultFinished.NONE]:
|
125
|
-
await
|
126
|
-
await
|
152
|
+
await DeviceManager.deployment_action_success(device)
|
153
|
+
await DeviceManager.update_device_state(device, UpdateStateEnum.FINISHED)
|
127
154
|
|
128
|
-
rollout = await
|
155
|
+
rollout = await DeviceManager.get_rollout(device)
|
129
156
|
if rollout:
|
130
157
|
if rollout.software == reported_software:
|
131
158
|
rollout.success_count += 1
|
@@ -133,16 +160,20 @@ async def deployment_feedback(
|
|
133
160
|
else:
|
134
161
|
# edge case where device update mode got changed while update was running
|
135
162
|
logging.warning(
|
136
|
-
f"Updating rollout success stats failed,
|
163
|
+
f"Updating rollout success stats failed, action_id={action_id}, device={device.id}"
|
164
|
+
# noqa: E501
|
137
165
|
)
|
138
166
|
|
139
|
-
|
140
|
-
|
167
|
+
if reported_software:
|
168
|
+
await DeviceManager.update_sw_version(device, reported_software.version)
|
169
|
+
|
170
|
+
software_version = reported_software.version if reported_software else None
|
171
|
+
logger.debug(f"Installation successful, software={software_version}, device={device.id}")
|
141
172
|
|
142
173
|
elif data.status.result.finished == FeedbackStatusResultFinished.FAILURE:
|
143
|
-
await
|
174
|
+
await DeviceManager.update_device_state(device, UpdateStateEnum.ERROR)
|
144
175
|
|
145
|
-
rollout = await
|
176
|
+
rollout = await DeviceManager.get_rollout(device)
|
146
177
|
if rollout:
|
147
178
|
if rollout.software == reported_software:
|
148
179
|
rollout.failure_count += 1
|
@@ -150,28 +181,28 @@ async def deployment_feedback(
|
|
150
181
|
else:
|
151
182
|
# edge case where device update mode got changed while update was running
|
152
183
|
logging.warning(
|
153
|
-
f"Updating rollout failure stats failed,
|
184
|
+
f"Updating rollout failure stats failed, action_id={action_id}, device={device.id}"
|
185
|
+
# noqa: E501
|
154
186
|
)
|
155
187
|
|
156
|
-
|
188
|
+
software_version = reported_software.version if reported_software else None
|
189
|
+
logger.debug(f"Installation failed, software={software_version}, device={device.id}")
|
157
190
|
else:
|
158
|
-
logging.error(
|
159
|
-
f"Device reported unhandled execution state, state={data.status.execution}, device={updater.dev_id}"
|
160
|
-
)
|
191
|
+
logging.error(f"Device reported unhandled execution state, state={data.status.execution}, device={device.id}")
|
161
192
|
|
162
193
|
try:
|
163
194
|
log = data.status.details
|
164
195
|
if log is not None:
|
165
|
-
await
|
196
|
+
await DeviceManager.update_log(device, "\n".join(log))
|
166
197
|
except AttributeError:
|
167
|
-
logging.warning(f"No details to update device update log, device={
|
198
|
+
logging.warning(f"No details to update device update log, device={device.id}")
|
168
199
|
|
169
200
|
return {"id": str(action_id)}
|
170
201
|
|
171
202
|
|
172
203
|
@router.head("/{dev_id}/download")
|
173
|
-
async def download_artifact_head(_: Request,
|
174
|
-
_, software = await
|
204
|
+
async def download_artifact_head(_: Request, device: Device = Depends(get_device)):
|
205
|
+
_, software = await DeviceManager.get_update(device)
|
175
206
|
if software is None:
|
176
207
|
raise HTTPException(404)
|
177
208
|
|
@@ -181,15 +212,26 @@ async def download_artifact_head(_: Request, updater: UpdateManager = Depends(ge
|
|
181
212
|
|
182
213
|
|
183
214
|
@router.get("/{dev_id}/download")
|
184
|
-
async def download_artifact(_: Request,
|
185
|
-
_, software = await
|
215
|
+
async def download_artifact(_: Request, device: Device = Depends(get_device)):
|
216
|
+
_, software = await DeviceManager.get_update(device)
|
186
217
|
if software is None:
|
187
218
|
raise HTTPException(404)
|
188
219
|
|
189
|
-
|
220
|
+
if software.local:
|
221
|
+
return FileResponse(
|
222
|
+
software.path,
|
223
|
+
media_type="application/octet-stream",
|
224
|
+
filename=software.path.name,
|
225
|
+
)
|
190
226
|
|
191
|
-
|
192
|
-
software.
|
193
|
-
|
194
|
-
|
195
|
-
|
227
|
+
try:
|
228
|
+
url = await storage.get_download_url(software.uri)
|
229
|
+
return RedirectResponse(url=url)
|
230
|
+
except ValueError:
|
231
|
+
# Fallback to streaming if redirect fails.
|
232
|
+
file_stream = storage.get_file_stream(software.uri)
|
233
|
+
return StreamingResponse(
|
234
|
+
file_stream,
|
235
|
+
media_type="application/octet-stream",
|
236
|
+
headers={"Content-Disposition": f"attachment; filename={software.path.name}"},
|
237
|
+
)
|