goosebit 0.1.2__py3-none-any.whl → 0.2.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 +50 -19
- goosebit/__main__.py +7 -0
- goosebit/api/responses.py +5 -0
- goosebit/api/routes.py +5 -15
- goosebit/api/telemetry/__init__.py +1 -0
- goosebit/{telemetry/__init__.py → api/telemetry/metrics.py} +9 -3
- goosebit/api/telemetry/prometheus/__init__.py +2 -0
- goosebit/api/telemetry/prometheus/readers.py +3 -0
- goosebit/api/telemetry/prometheus/routes.py +18 -0
- goosebit/api/telemetry/routes.py +9 -0
- goosebit/api/v1/__init__.py +1 -0
- goosebit/api/v1/devices/__init__.py +1 -0
- goosebit/api/v1/devices/device/__init__.py +1 -0
- goosebit/api/v1/devices/device/responses.py +13 -0
- goosebit/api/v1/devices/device/routes.py +27 -0
- goosebit/api/v1/devices/requests.py +7 -0
- goosebit/api/v1/devices/responses.py +16 -0
- goosebit/api/v1/devices/routes.py +35 -0
- goosebit/api/v1/download/__init__.py +1 -0
- goosebit/api/v1/download/routes.py +22 -0
- goosebit/api/v1/rollouts/__init__.py +1 -0
- goosebit/api/v1/rollouts/requests.py +16 -0
- goosebit/api/v1/rollouts/responses.py +19 -0
- goosebit/api/v1/rollouts/routes.py +50 -0
- goosebit/api/v1/routes.py +9 -0
- goosebit/api/v1/software/__init__.py +1 -0
- goosebit/api/v1/software/requests.py +5 -0
- goosebit/api/v1/software/responses.py +16 -0
- goosebit/api/v1/software/routes.py +77 -0
- goosebit/auth/__init__.py +101 -101
- goosebit/db/__init__.py +11 -0
- goosebit/db/config.py +10 -0
- goosebit/db/migrations/models/0_20240830054046_init.py +136 -0
- goosebit/{models.py → db/models.py} +17 -10
- goosebit/realtime/logs.py +4 -3
- goosebit/realtime/routes.py +2 -2
- goosebit/schema/__init__.py +0 -0
- goosebit/schema/devices.py +73 -0
- goosebit/schema/rollouts.py +31 -0
- goosebit/schema/software.py +37 -0
- goosebit/settings/__init__.py +17 -0
- goosebit/settings/const.py +21 -0
- goosebit/settings/schema.py +86 -0
- goosebit/ui/bff/__init__.py +1 -0
- goosebit/ui/bff/devices/__init__.py +1 -0
- goosebit/ui/bff/devices/requests.py +12 -0
- goosebit/ui/bff/devices/responses.py +39 -0
- goosebit/ui/bff/devices/routes.py +72 -0
- goosebit/ui/bff/download/__init__.py +1 -0
- goosebit/ui/bff/download/routes.py +22 -0
- goosebit/ui/bff/rollouts/__init__.py +1 -0
- goosebit/ui/bff/rollouts/responses.py +37 -0
- goosebit/ui/bff/rollouts/routes.py +52 -0
- goosebit/ui/bff/routes.py +11 -0
- goosebit/ui/bff/software/__init__.py +1 -0
- goosebit/ui/bff/software/responses.py +37 -0
- goosebit/ui/bff/software/routes.py +83 -0
- goosebit/ui/nav.py +16 -0
- goosebit/ui/routes.py +29 -66
- goosebit/ui/static/favicon.ico +0 -0
- goosebit/ui/static/favicon.svg +1 -1
- goosebit/ui/static/js/devices.js +47 -71
- goosebit/ui/static/js/index.js +4 -9
- goosebit/ui/static/js/login.js +23 -0
- goosebit/ui/static/js/logs.js +1 -1
- goosebit/ui/static/js/rollouts.js +33 -19
- goosebit/ui/static/js/{firmware.js → software.js} +87 -86
- goosebit/ui/static/js/util.js +60 -6
- goosebit/ui/static/svg/goosebit-logo.svg +1 -1
- goosebit/ui/templates/__init__.py +9 -1
- goosebit/ui/templates/devices.html.jinja +75 -0
- goosebit/ui/templates/index.html.jinja +25 -0
- goosebit/ui/templates/login.html.jinja +57 -0
- goosebit/ui/templates/logs.html.jinja +31 -0
- goosebit/ui/templates/nav.html.jinja +84 -0
- goosebit/ui/templates/rollouts.html.jinja +93 -0
- goosebit/ui/templates/software.html.jinja +139 -0
- goosebit/updater/controller/v1/routes.py +101 -96
- goosebit/updater/controller/v1/schema.py +56 -0
- goosebit/updater/manager.py +65 -65
- goosebit/updater/routes.py +3 -11
- goosebit/updates/__init__.py +91 -32
- goosebit/updates/swdesc.py +2 -7
- goosebit-0.2.1.dist-info/METADATA +173 -0
- goosebit-0.2.1.dist-info/RECORD +95 -0
- goosebit/api/devices.py +0 -136
- goosebit/api/download.py +0 -34
- goosebit/api/firmware.py +0 -57
- goosebit/api/helper.py +0 -30
- goosebit/api/rollouts.py +0 -87
- goosebit/db.py +0 -37
- goosebit/permissions.py +0 -75
- goosebit/settings.py +0 -64
- goosebit/telemetry/prometheus.py +0 -10
- goosebit/ui/templates/devices.html +0 -115
- goosebit/ui/templates/firmware.html +0 -163
- goosebit/ui/templates/index.html +0 -23
- goosebit/ui/templates/login.html +0 -65
- goosebit/ui/templates/logs.html +0 -36
- goosebit/ui/templates/nav.html +0 -117
- goosebit/ui/templates/rollouts.html +0 -76
- goosebit-0.1.2.dist-info/METADATA +0 -123
- goosebit-0.1.2.dist-info/RECORD +0 -51
- {goosebit-0.1.2.dist-info → goosebit-0.2.1.dist-info}/LICENSE +0 -0
- {goosebit-0.1.2.dist-info → goosebit-0.2.1.dist-info}/WHEEL +0 -0
@@ -0,0 +1,84 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<script>const PERMISSIONS = {{request.user.get_json_permissions() | safe}};</script>
|
5
|
+
<meta charset="utf-8" />
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
7
|
+
<title>{{ title }}</title>
|
8
|
+
<!--bootstrap-->
|
9
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
10
|
+
rel="stylesheet"
|
11
|
+
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
|
12
|
+
crossorigin="anonymous" />
|
13
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
14
|
+
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
|
15
|
+
crossorigin="anonymous"></script>
|
16
|
+
<link rel="stylesheet"
|
17
|
+
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
|
18
|
+
<!--data tables-->
|
19
|
+
<link href="https://cdn.datatables.net/v/bs5/jq-3.7.0/dt-2.0.1/b-3.0.0/r-3.0.0/sl-2.0.0/datatables.min.css"
|
20
|
+
rel="stylesheet" />
|
21
|
+
<script src="https://cdn.datatables.net/v/bs5/jq-3.7.0/dt-2.0.1/b-3.0.0/r-3.0.0/sl-2.0.0/datatables.min.js"></script>
|
22
|
+
<!--favicon-->
|
23
|
+
<link rel="icon" href="{{ url_for('static', path='favicon.svg') }}" />
|
24
|
+
<!-- SweetAlert2 CSS -->
|
25
|
+
<link href="https://cdn.jsdelivr.net/npm/@sweetalert2/theme-bootstrap-4/bootstrap-4.css"
|
26
|
+
rel="stylesheet" />
|
27
|
+
<link href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css"
|
28
|
+
rel="stylesheet" />
|
29
|
+
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.all.min.js"></script>
|
30
|
+
<!--data tables alignment fix-->
|
31
|
+
<style>
|
32
|
+
th.dt-type-numeric {
|
33
|
+
text-align: left !important;
|
34
|
+
}
|
35
|
+
td.dt-type-numeric {
|
36
|
+
text-align: left !important;
|
37
|
+
}
|
38
|
+
.active {
|
39
|
+
color: var(--bs-nav-pills-link-active-color);
|
40
|
+
background-color: transparent;
|
41
|
+
}
|
42
|
+
</style>
|
43
|
+
<script>const TABLE_UPDATE_TIME = 3000;</script>
|
44
|
+
<script src="{{ url_for('static', path='js/util.js') }}"></script>
|
45
|
+
</head>
|
46
|
+
<body data-bs-theme="dark">
|
47
|
+
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
48
|
+
<div class="container-fluid">
|
49
|
+
<a class="navbar-brand" href="{{ request.url_for('home_ui') }}">
|
50
|
+
<img src="{{ request.url_for('static', path='svg/goosebit-logo.svg') }}"
|
51
|
+
class="me-2"
|
52
|
+
height="30px"
|
53
|
+
width="30px"
|
54
|
+
alt="gooseBit logo" />
|
55
|
+
gooseBit
|
56
|
+
</a>
|
57
|
+
<button class="navbar-toggler"
|
58
|
+
type="button"
|
59
|
+
data-bs-toggle="collapse"
|
60
|
+
data-bs-target="#navbar"
|
61
|
+
aria-controls="navbar"
|
62
|
+
aria-expanded="false"
|
63
|
+
aria-label="Toggle navigation">
|
64
|
+
<span class="navbar-toggler-icon"></span>
|
65
|
+
</button>
|
66
|
+
<div class="collapse navbar-collapse" id="navbar">
|
67
|
+
<div class="navbar-nav">
|
68
|
+
{% for nav_item in request.nav %}
|
69
|
+
{% if compare_permissions(nav_item.permissions, request.user.permissions) %}
|
70
|
+
<a class="nav-link{% if request.url == request.url_for(nav_item.function) %} active{% endif %}"
|
71
|
+
href="{{ request.url_for(nav_item.function) }}">{{ nav_item.text }}</a>
|
72
|
+
{% endif %}
|
73
|
+
{% endfor %}
|
74
|
+
</div>
|
75
|
+
<div class="navbar-nav d-flex flex-fill justify-content-end">
|
76
|
+
<a class="nav-link" href="{{ request.url_for('logout') }}">Logout<i class="bi bi-box-arrow-right ps-2"></i></a>
|
77
|
+
</div>
|
78
|
+
</div>
|
79
|
+
</div>
|
80
|
+
</nav>
|
81
|
+
{% block content %}
|
82
|
+
{% endblock content %}
|
83
|
+
</body>
|
84
|
+
</html>
|
@@ -0,0 +1,93 @@
|
|
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="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
|
+
</table>
|
23
|
+
</div>
|
24
|
+
</div>
|
25
|
+
</div>
|
26
|
+
{% if compare_permissions(["rollout.write"], request.user.permissions) %}
|
27
|
+
<div class="modal" id="rollout-create-modal">
|
28
|
+
<div class="modal-dialog modal-lg">
|
29
|
+
<div class="modal-content">
|
30
|
+
<div class="modal-header">
|
31
|
+
<h5 class="modal-title">Create Rollout</h5>
|
32
|
+
<button type="button"
|
33
|
+
class="btn-close"
|
34
|
+
data-bs-dismiss="modal"
|
35
|
+
aria-label="Close"></button>
|
36
|
+
</div>
|
37
|
+
<form id="rollout-form" class="needs-validation" novalidate>
|
38
|
+
<div class="modal-body">
|
39
|
+
<div class="form-group mb-3">
|
40
|
+
<label for="rollout-selected-name">Name</label>
|
41
|
+
<input id="rollout-selected-name"
|
42
|
+
class="form-control"
|
43
|
+
placeholder="Release 1" />
|
44
|
+
</div>
|
45
|
+
<div class="form-group mb-3">
|
46
|
+
<label for="rollout-selected-feed">Feed</label>
|
47
|
+
<input id="rollout-selected-feed"
|
48
|
+
class="form-control"
|
49
|
+
placeholder="qa"
|
50
|
+
required />
|
51
|
+
<div class="invalid-feedback">
|
52
|
+
Feed missing. Use "default" if working with a single
|
53
|
+
feed.
|
54
|
+
</div>
|
55
|
+
</div>
|
56
|
+
<div class="form-group mb-3">
|
57
|
+
<label for="selected-sw">Software</label>
|
58
|
+
<select class="form-select" id="selected-sw" required>
|
59
|
+
<option value="" disabled selected>Select software</option>
|
60
|
+
</select>
|
61
|
+
<div class="invalid-feedback">Select software for the rollout.</div>
|
62
|
+
</div>
|
63
|
+
</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
|
+
</form>
|
69
|
+
</div>
|
70
|
+
</div>
|
71
|
+
</div>
|
72
|
+
{% else %}
|
73
|
+
<div class="modal modal-lg fade" id="rollout-create-modal">
|
74
|
+
<div class="modal-dialog modal-dialog-centered modal-xl">
|
75
|
+
<div class="modal-content">
|
76
|
+
<div class="modal-header">
|
77
|
+
Unavailable
|
78
|
+
<button type="button"
|
79
|
+
class="btn-close"
|
80
|
+
data-bs-dismiss="modal"
|
81
|
+
aria-label="Close"></button>
|
82
|
+
</div>
|
83
|
+
<div class="modal-body">
|
84
|
+
<div class="alert alert-warning m-0" role="alert">You do not have permission to add rollouts.</div>
|
85
|
+
<form id="rollout-form">
|
86
|
+
</form>
|
87
|
+
</div>
|
88
|
+
</div>
|
89
|
+
</div>
|
90
|
+
</div>
|
91
|
+
{% endif %}
|
92
|
+
<script src="{{ url_for('static', path='js/rollouts.js') }}"></script>
|
93
|
+
{% endblock content %}
|
@@ -0,0 +1,139 @@
|
|
1
|
+
{% extends "nav.html.jinja" %}
|
2
|
+
{% block content %}
|
3
|
+
<div class="container-fluid">
|
4
|
+
<div class="row p-2 pt-4 g-4 d-flex justify-content-center">
|
5
|
+
<div class="col">
|
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
|
+
</table>
|
19
|
+
</div>
|
20
|
+
</div>
|
21
|
+
</div>
|
22
|
+
{% if compare_permissions(["software.write"], request.user.permissions) %}
|
23
|
+
<div class="modal modal-lg fade" id="upload-modal">
|
24
|
+
<div class="modal-dialog modal-dialog-centered modal-xl">
|
25
|
+
<div class="modal-content">
|
26
|
+
<div class="modal-header">
|
27
|
+
<ul class="nav nav-underline nav-justified w-100" role="tablist">
|
28
|
+
<li class="nav-item">
|
29
|
+
<button class="nav-link active"
|
30
|
+
aria-current="page"
|
31
|
+
id="upload-tab"
|
32
|
+
data-bs-toggle="tab"
|
33
|
+
data-bs-target="#upload-tab-content"
|
34
|
+
type="button"
|
35
|
+
role="tab">Upload File</button>
|
36
|
+
</li>
|
37
|
+
<li class="nav-item">
|
38
|
+
<button class="nav-link"
|
39
|
+
id="url-tab"
|
40
|
+
data-bs-toggle="tab"
|
41
|
+
data-bs-target="#url-tab-content"
|
42
|
+
type="button"
|
43
|
+
role="tab">Remote URL</button>
|
44
|
+
</li>
|
45
|
+
</ul>
|
46
|
+
</div>
|
47
|
+
<div class="tab-content">
|
48
|
+
<div class="tab-pane active" id="upload-tab-content">
|
49
|
+
<div class="modal-body">
|
50
|
+
<form id="upload-form">
|
51
|
+
<div class="row g-3 row-cols-1">
|
52
|
+
<div id="upload-alerts"></div>
|
53
|
+
<div class="col">
|
54
|
+
<input class="form-control"
|
55
|
+
type="file"
|
56
|
+
accept=".swu"
|
57
|
+
id="file-upload"
|
58
|
+
name="file" />
|
59
|
+
</div>
|
60
|
+
<div class="col">
|
61
|
+
<input class="btn btn-outline-light w-100"
|
62
|
+
id="file-upload-submit"
|
63
|
+
type="submit"
|
64
|
+
value="Upload" />
|
65
|
+
<div class="progress border border-light bg-dark d-none"
|
66
|
+
style="height: 36px"
|
67
|
+
role="progressbar"
|
68
|
+
aria-valuenow="0"
|
69
|
+
aria-valuemin="0"
|
70
|
+
aria-valuemax="100">
|
71
|
+
<div class="progress-bar progress-bar-striped bg-success progress-bar-animated"
|
72
|
+
id="upload-progress"
|
73
|
+
style="width: 0%">0%</div>
|
74
|
+
</div>
|
75
|
+
</div>
|
76
|
+
</div>
|
77
|
+
</form>
|
78
|
+
</div>
|
79
|
+
</div>
|
80
|
+
<div class="tab-pane" id="url-tab-content">
|
81
|
+
<div class="modal-body">
|
82
|
+
<form id="url-form">
|
83
|
+
<div class="row g-3 row-cols-1">
|
84
|
+
<div id="url-alerts"></div>
|
85
|
+
<div class="col">
|
86
|
+
<div class="input-group">
|
87
|
+
<span class="input-group-text">File URL</span>
|
88
|
+
<input class="form-control" type="url" id="file-url" />
|
89
|
+
</div>
|
90
|
+
</div>
|
91
|
+
<div class="col">
|
92
|
+
<input class="btn btn-outline-light w-100"
|
93
|
+
id="url-submit"
|
94
|
+
type="submit"
|
95
|
+
value="Upload" />
|
96
|
+
<div class="progress border border-light bg-dark d-none"
|
97
|
+
style="height: 36px"
|
98
|
+
role="progressbar"
|
99
|
+
aria-valuenow="0"
|
100
|
+
aria-valuemin="0"
|
101
|
+
aria-valuemax="100">
|
102
|
+
<div class="progress-bar progress-bar-striped bg-success progress-bar-animated"
|
103
|
+
id="url-progress"
|
104
|
+
style="width: 0%">0%</div>
|
105
|
+
</div>
|
106
|
+
</div>
|
107
|
+
</div>
|
108
|
+
</form>
|
109
|
+
</div>
|
110
|
+
</div>
|
111
|
+
</div>
|
112
|
+
</div>
|
113
|
+
</div>
|
114
|
+
</div>
|
115
|
+
{% else %}
|
116
|
+
<div class="modal modal-lg fade" id="upload-modal">
|
117
|
+
<div class="modal-dialog modal-dialog-centered modal-xl">
|
118
|
+
<div class="modal-content">
|
119
|
+
<div class="modal-header">
|
120
|
+
Unavailable
|
121
|
+
<button type="button"
|
122
|
+
class="btn-close"
|
123
|
+
data-bs-dismiss="modal"
|
124
|
+
aria-label="Close"></button>
|
125
|
+
</div>
|
126
|
+
<div class="modal-body">
|
127
|
+
<div class="alert alert-warning m-0" role="alert">You do not have permission to add software files.</div>
|
128
|
+
<form id="upload-form">
|
129
|
+
</form>
|
130
|
+
<form id="url-form">
|
131
|
+
</form>
|
132
|
+
</div>
|
133
|
+
</div>
|
134
|
+
</div>
|
135
|
+
</div>
|
136
|
+
{% endif %}
|
137
|
+
<script src="{{ url_for('static', path='js/util.js') }}"></script>
|
138
|
+
<script src="{{ url_for('static', path='js/software.js') }}"></script>
|
139
|
+
{% endblock content %}
|
@@ -1,26 +1,28 @@
|
|
1
|
-
import json
|
2
1
|
import logging
|
3
2
|
|
4
|
-
from fastapi import APIRouter, Depends
|
3
|
+
from fastapi import APIRouter, Depends, HTTPException
|
5
4
|
from fastapi.requests import Request
|
5
|
+
from fastapi.responses import FileResponse, RedirectResponse, Response
|
6
6
|
|
7
|
-
from goosebit.models import
|
8
|
-
from goosebit.settings import
|
7
|
+
from goosebit.db.models import Software, UpdateStateEnum
|
8
|
+
from goosebit.settings import config
|
9
9
|
from goosebit.updater.manager import HandlingType, UpdateManager, get_update_manager
|
10
10
|
from goosebit.updates import generate_chunk
|
11
11
|
|
12
|
+
from .schema import (
|
13
|
+
ConfigDataSchema,
|
14
|
+
FeedbackSchema,
|
15
|
+
FeedbackStatusExecutionState,
|
16
|
+
FeedbackStatusResultFinished,
|
17
|
+
)
|
18
|
+
|
12
19
|
logger = logging.getLogger("DDI API")
|
13
20
|
|
14
21
|
router = APIRouter(prefix="/v1")
|
15
22
|
|
16
23
|
|
17
24
|
@router.get("/{dev_id}")
|
18
|
-
async def polling(
|
19
|
-
request: Request,
|
20
|
-
tenant: str,
|
21
|
-
dev_id: str,
|
22
|
-
updater: UpdateManager = Depends(get_update_manager),
|
23
|
-
):
|
25
|
+
async def polling(request: Request, dev_id: str, updater: UpdateManager = Depends(get_update_manager)):
|
24
26
|
links = {}
|
25
27
|
|
26
28
|
sleep = updater.poll_time
|
@@ -28,12 +30,11 @@ async def polling(
|
|
28
30
|
|
29
31
|
if device.last_state == UpdateStateEnum.UNKNOWN:
|
30
32
|
# device registration
|
31
|
-
sleep =
|
33
|
+
sleep = config.poll_time_registration
|
32
34
|
links["configData"] = {
|
33
35
|
"href": str(
|
34
36
|
request.url_for(
|
35
37
|
"config_data",
|
36
|
-
tenant=tenant,
|
37
38
|
dev_id=dev_id,
|
38
39
|
)
|
39
40
|
)
|
@@ -47,15 +48,14 @@ async def polling(
|
|
47
48
|
else:
|
48
49
|
# provide update if available. Note: this is also required while in state "running", otherwise swupdate
|
49
50
|
# won't confirm a successful testing (might be a bug/problem in swupdate)
|
50
|
-
handling_type,
|
51
|
+
handling_type, software = await updater.get_update()
|
51
52
|
if handling_type != HandlingType.SKIP:
|
52
53
|
links["deploymentBase"] = {
|
53
54
|
"href": str(
|
54
55
|
request.url_for(
|
55
56
|
"deployment_base",
|
56
|
-
tenant=tenant,
|
57
57
|
dev_id=dev_id,
|
58
|
-
action_id=
|
58
|
+
action_id=software.id,
|
59
59
|
)
|
60
60
|
)
|
61
61
|
}
|
@@ -68,15 +68,8 @@ async def polling(
|
|
68
68
|
|
69
69
|
|
70
70
|
@router.put("/{dev_id}/configData")
|
71
|
-
async def config_data(
|
72
|
-
|
73
|
-
dev_id: str,
|
74
|
-
tenant: str,
|
75
|
-
updater: UpdateManager = Depends(get_update_manager),
|
76
|
-
):
|
77
|
-
data = await request.json()
|
78
|
-
# TODO: make standard schema to deal with this
|
79
|
-
await updater.update_config_data(**data["data"])
|
71
|
+
async def config_data(_: Request, cfg: ConfigDataSchema, updater: UpdateManager = Depends(get_update_manager)):
|
72
|
+
await updater.update_config_data(**cfg.data)
|
80
73
|
logger.info(f"Updating config data, device={updater.dev_id}")
|
81
74
|
return {"success": True, "message": "Updated swupdate data."}
|
82
75
|
|
@@ -84,97 +77,109 @@ async def config_data(
|
|
84
77
|
@router.get("/{dev_id}/deploymentBase/{action_id}")
|
85
78
|
async def deployment_base(
|
86
79
|
request: Request,
|
87
|
-
tenant: str,
|
88
|
-
dev_id: str,
|
89
80
|
action_id: int,
|
90
81
|
updater: UpdateManager = Depends(get_update_manager),
|
91
82
|
):
|
92
|
-
handling_type,
|
83
|
+
handling_type, software = await updater.get_update()
|
93
84
|
|
94
85
|
logger.info(f"Request deployment base, device={updater.dev_id}")
|
95
86
|
|
96
87
|
return {
|
97
|
-
"id":
|
88
|
+
"id": str(action_id),
|
98
89
|
"deployment": {
|
99
90
|
"download": str(handling_type),
|
100
91
|
"update": str(handling_type),
|
101
|
-
"chunks": generate_chunk(request,
|
92
|
+
"chunks": await generate_chunk(request, updater),
|
102
93
|
},
|
103
94
|
}
|
104
95
|
|
105
96
|
|
106
97
|
@router.post("/{dev_id}/deploymentBase/{action_id}/feedback")
|
107
98
|
async def deployment_feedback(
|
108
|
-
|
109
|
-
tenant: str,
|
110
|
-
action_id: int,
|
111
|
-
updater: UpdateManager = Depends(get_update_manager),
|
99
|
+
_: Request, data: FeedbackSchema, action_id: int, updater: UpdateManager = Depends(get_update_manager)
|
112
100
|
):
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
await updater.
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
else:
|
165
|
-
logging.warning(
|
166
|
-
f"Updating rollout failure stats failed, firmware={reported_firmware.id}, device={updater.dev_id}" # noqa: E501
|
167
|
-
)
|
168
|
-
|
169
|
-
logger.debug(f"Installation failed, firmware={reported_firmware.version}, device={updater.dev_id}")
|
170
|
-
|
171
|
-
except KeyError as e:
|
172
|
-
logging.warning(f"Processing deployment feedback failed, error={e}, device={updater.dev_id}")
|
101
|
+
if data.status.execution == FeedbackStatusExecutionState.PROCEEDING:
|
102
|
+
await updater.update_device_state(UpdateStateEnum.RUNNING)
|
103
|
+
logger.debug(f"Installation in progress, device={updater.dev_id}")
|
104
|
+
|
105
|
+
elif data.status.execution == FeedbackStatusExecutionState.CLOSED:
|
106
|
+
await updater.update_force_update(False)
|
107
|
+
await updater.update_log_complete(True)
|
108
|
+
|
109
|
+
reported_software = await Software.get_or_none(id=action_id)
|
110
|
+
|
111
|
+
# From hawkBit docu: DDI defines also a status NONE which will not be interpreted by the update server
|
112
|
+
# and handled like SUCCESS.
|
113
|
+
if data.status.result.finished in [FeedbackStatusResultFinished.SUCCESS, FeedbackStatusResultFinished.NONE]:
|
114
|
+
await updater.update_device_state(UpdateStateEnum.FINISHED)
|
115
|
+
|
116
|
+
# not guaranteed to be the correct rollout - see next comment.
|
117
|
+
rollout = await updater.get_rollout()
|
118
|
+
if rollout:
|
119
|
+
if rollout.software == reported_software:
|
120
|
+
rollout.success_count += 1
|
121
|
+
await rollout.save()
|
122
|
+
else:
|
123
|
+
logging.warning(
|
124
|
+
f"Updating rollout success stats failed, software={reported_software.id}, device={updater.dev_id}" # noqa: E501
|
125
|
+
)
|
126
|
+
|
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
|
+
await updater.update_sw_version(reported_software.version)
|
131
|
+
logger.debug(f"Installation successful, software={reported_software.version}, device={updater.dev_id}")
|
132
|
+
|
133
|
+
elif data.status.result.finished == FeedbackStatusResultFinished.FAILURE:
|
134
|
+
await updater.update_device_state(UpdateStateEnum.ERROR)
|
135
|
+
|
136
|
+
# not guaranteed to be the correct rollout - see comment above.
|
137
|
+
rollout = await updater.get_rollout()
|
138
|
+
if rollout:
|
139
|
+
if rollout.software == reported_software:
|
140
|
+
rollout.failure_count += 1
|
141
|
+
await rollout.save()
|
142
|
+
else:
|
143
|
+
logging.warning(
|
144
|
+
f"Updating rollout failure stats failed, software={reported_software.id}, device={updater.dev_id}" # noqa: E501
|
145
|
+
)
|
146
|
+
|
147
|
+
logger.debug(f"Installation failed, software={reported_software.version}, device={updater.dev_id}")
|
148
|
+
else:
|
149
|
+
logging.warning(
|
150
|
+
f"Device reported unhandled execution state, state={data.status.execution}, device={updater.dev_id}"
|
151
|
+
)
|
173
152
|
|
174
153
|
try:
|
175
|
-
log = data
|
154
|
+
log = data.status.details
|
176
155
|
await updater.update_log("\n".join(log))
|
177
|
-
except
|
178
|
-
logging.warning(f"No details to update update log, device={updater.dev_id}")
|
156
|
+
except AttributeError:
|
157
|
+
logging.warning(f"No details to update device update log, device={updater.dev_id}")
|
179
158
|
|
180
159
|
return {"id": str(action_id)}
|
160
|
+
|
161
|
+
|
162
|
+
@router.head("/{dev_id}/download")
|
163
|
+
async def download_artifact_head(_: Request, updater: UpdateManager = Depends(get_update_manager)):
|
164
|
+
_, software = await updater.get_update()
|
165
|
+
if software is None:
|
166
|
+
raise HTTPException(404)
|
167
|
+
|
168
|
+
response = Response()
|
169
|
+
response.headers["Content-Length"] = str(software.size)
|
170
|
+
return response
|
171
|
+
|
172
|
+
|
173
|
+
@router.get("/{dev_id}/download")
|
174
|
+
async def download_artifact(_: Request, updater: UpdateManager = Depends(get_update_manager)):
|
175
|
+
_, software = await updater.get_update()
|
176
|
+
if software is None:
|
177
|
+
raise HTTPException(404)
|
178
|
+
if software.local:
|
179
|
+
return FileResponse(
|
180
|
+
software.path,
|
181
|
+
media_type="application/octet-stream",
|
182
|
+
filename=software.path.name,
|
183
|
+
)
|
184
|
+
else:
|
185
|
+
return RedirectResponse(url=software.uri)
|
@@ -0,0 +1,56 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from enum import StrEnum
|
4
|
+
from typing import Any
|
5
|
+
|
6
|
+
from pydantic import BaseModel
|
7
|
+
|
8
|
+
|
9
|
+
class ConfigDataUpdateMode(StrEnum):
|
10
|
+
MERGE = "merge"
|
11
|
+
REPLACE = "replace"
|
12
|
+
REMOVE = "remove"
|
13
|
+
|
14
|
+
|
15
|
+
class ConfigDataSchema(BaseModel):
|
16
|
+
data: dict[str, Any]
|
17
|
+
mode: ConfigDataUpdateMode = ConfigDataUpdateMode.MERGE
|
18
|
+
|
19
|
+
|
20
|
+
class FeedbackStatusExecutionState(StrEnum):
|
21
|
+
CLOSED = "closed"
|
22
|
+
PROCEEDING = "proceeding"
|
23
|
+
CANCELED = "canceled"
|
24
|
+
SCHEDULED = "scheduled"
|
25
|
+
REJECTED = "rejected"
|
26
|
+
RESUMED = "resumed"
|
27
|
+
DOWNLOADED = "downloaded"
|
28
|
+
DOWNLOAD = "download"
|
29
|
+
|
30
|
+
|
31
|
+
class FeedbackStatusProgressSchema(BaseModel):
|
32
|
+
cnt: int
|
33
|
+
of: int | None
|
34
|
+
|
35
|
+
|
36
|
+
class FeedbackStatusResultFinished(StrEnum):
|
37
|
+
SUCCESS = "success"
|
38
|
+
FAILURE = "failure"
|
39
|
+
NONE = "none"
|
40
|
+
|
41
|
+
|
42
|
+
class FeedbackStatusResultSchema(BaseModel):
|
43
|
+
finished: FeedbackStatusResultFinished
|
44
|
+
progress: FeedbackStatusProgressSchema = None
|
45
|
+
|
46
|
+
|
47
|
+
class FeedbackStatusSchema(BaseModel):
|
48
|
+
execution: FeedbackStatusExecutionState
|
49
|
+
result: FeedbackStatusResultSchema
|
50
|
+
code: int = None
|
51
|
+
details: list[str] = None
|
52
|
+
|
53
|
+
|
54
|
+
class FeedbackSchema(BaseModel):
|
55
|
+
time: str = None
|
56
|
+
status: FeedbackStatusSchema
|