goosebit 0.1.0__py3-none-any.whl → 0.1.2__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.
Files changed (52) hide show
  1. goosebit/__init__.py +8 -5
  2. goosebit/api/__init__.py +1 -1
  3. goosebit/api/devices.py +60 -36
  4. goosebit/api/download.py +28 -14
  5. goosebit/api/firmware.py +37 -44
  6. goosebit/api/helper.py +30 -0
  7. goosebit/api/rollouts.py +87 -0
  8. goosebit/api/routes.py +15 -7
  9. goosebit/auth/__init__.py +37 -21
  10. goosebit/db.py +5 -0
  11. goosebit/models.py +125 -6
  12. goosebit/permissions.py +33 -13
  13. goosebit/realtime/__init__.py +1 -1
  14. goosebit/realtime/logs.py +4 -6
  15. goosebit/settings.py +38 -29
  16. goosebit/telemetry/__init__.py +28 -0
  17. goosebit/telemetry/prometheus.py +10 -0
  18. goosebit/ui/__init__.py +1 -1
  19. goosebit/ui/routes.py +36 -39
  20. goosebit/ui/static/js/devices.js +191 -239
  21. goosebit/ui/static/js/firmware.js +234 -88
  22. goosebit/ui/static/js/index.js +83 -84
  23. goosebit/ui/static/js/logs.js +17 -10
  24. goosebit/ui/static/js/rollouts.js +198 -0
  25. goosebit/ui/static/js/util.js +66 -0
  26. goosebit/ui/templates/devices.html +75 -42
  27. goosebit/ui/templates/firmware.html +150 -34
  28. goosebit/ui/templates/index.html +9 -23
  29. goosebit/ui/templates/login.html +58 -27
  30. goosebit/ui/templates/logs.html +18 -3
  31. goosebit/ui/templates/nav.html +78 -25
  32. goosebit/ui/templates/rollouts.html +76 -0
  33. goosebit/updater/__init__.py +1 -1
  34. goosebit/updater/controller/__init__.py +1 -1
  35. goosebit/updater/controller/v1/__init__.py +1 -1
  36. goosebit/updater/controller/v1/routes.py +112 -24
  37. goosebit/updater/manager.py +237 -94
  38. goosebit/updater/routes.py +7 -8
  39. goosebit/updates/__init__.py +70 -0
  40. goosebit/updates/swdesc.py +83 -0
  41. goosebit-0.1.2.dist-info/METADATA +123 -0
  42. goosebit-0.1.2.dist-info/RECORD +51 -0
  43. goosebit/updater/download/__init__.py +0 -1
  44. goosebit/updater/download/routes.py +0 -6
  45. goosebit/updater/download/v1/__init__.py +0 -1
  46. goosebit/updater/download/v1/routes.py +0 -26
  47. goosebit/updater/misc.py +0 -69
  48. goosebit/updater/updates.py +0 -93
  49. goosebit-0.1.0.dist-info/METADATA +0 -37
  50. goosebit-0.1.0.dist-info/RECORD +0 -48
  51. {goosebit-0.1.0.dist-info → goosebit-0.1.2.dist-info}/LICENSE +0 -0
  52. {goosebit-0.1.0.dist-info → goosebit-0.1.2.dist-info}/WHEEL +0 -0
@@ -1,34 +1,65 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
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" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
8
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
9
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
10
- <link rel="icon" href="{{ url_for('static', path='favicon.svg') }}">
11
- </head>
12
- <body data-bs-theme="dark">
13
- <div class="container py-5 h-100">
14
- <div class="row d-flex justify-content-center align-items-center h-100">
15
- <div class="col-12 col-md-8 col-lg-6 col-xl-5">
16
- <div class="mb-md-5 mt-md-4 pb-5">
17
- <h2 class="fw-bold mb-3 text-uppercase">Login</h2>
18
- <form method="post">
19
- <div class="form-outline form-white mb-4">
20
- <input type="email" id="username" name="username" placeholder="Email" class="form-control form-control-lg" />
21
- </div>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Login</title>
7
+ <link
8
+ href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
9
+ rel="stylesheet"
10
+ integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
11
+ crossorigin="anonymous"
12
+ />
13
+ <script
14
+ src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
15
+ integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
16
+ crossorigin="anonymous"
17
+ ></script>
18
+ <link
19
+ rel="stylesheet"
20
+ href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
21
+ />
22
+ <link rel="icon" href="{{ url_for('static', path='favicon.svg') }}" />
23
+ </head>
24
+ <body data-bs-theme="dark">
25
+ <div class="container py-5 h-100">
26
+ <div
27
+ class="row d-flex justify-content-center align-items-center h-100"
28
+ >
29
+ <div class="col-12 col-md-8 col-lg-6 col-xl-5">
30
+ <div class="mb-md-5 mt-md-4 pb-5">
31
+ <h2 class="fw-bold mb-3 text-uppercase">Login</h2>
32
+ <form method="post">
33
+ <div class="form-outline form-white mb-4">
34
+ <input
35
+ type="email"
36
+ id="username"
37
+ name="username"
38
+ placeholder="Email"
39
+ class="form-control form-control-lg"
40
+ />
41
+ </div>
22
42
 
23
- <div class="form-outline form-white mb-4">
24
- <input type="password" id="password" name="password" placeholder="Password" class="form-control form-control-lg" />
25
- </div>
43
+ <div class="form-outline form-white mb-4">
44
+ <input
45
+ type="password"
46
+ id="password"
47
+ name="password"
48
+ placeholder="Password"
49
+ class="form-control form-control-lg"
50
+ />
51
+ </div>
26
52
 
27
- <button class="btn btn-outline-light btn-lg px-5 w-100" type="submit">Login</button>
28
- </form>
53
+ <button
54
+ class="btn btn-outline-light btn-lg px-5 w-100"
55
+ type="submit"
56
+ >
57
+ Login
58
+ </button>
59
+ </form>
60
+ </div>
29
61
  </div>
30
62
  </div>
31
63
  </div>
32
- </div>
33
- </body>
64
+ </body>
34
65
  </html>
@@ -1,5 +1,4 @@
1
- {% extends "nav.html" %}
2
- {% block content %}
1
+ {% extends "nav.html" %} {% block content %}
3
2
  <div class="container-fluid">
4
3
  <div class="row p-2 d-flex justify-content-center">
5
4
  <div class="col col-12 col-lg-9">
@@ -7,6 +6,22 @@
7
6
  <div class="card-header">
8
7
  <h3>Logs - {{ device }}</h3>
9
8
  </div>
9
+ <div class="card-header">
10
+ <div
11
+ class="progress m-2"
12
+ role="progressbar"
13
+ aria-label="Basic example"
14
+ aria-valuenow="0"
15
+ aria-valuemin="0"
16
+ aria-valuemax="100"
17
+ >
18
+ <div
19
+ class="progress-bar progress-bar-striped progress-bar-animated"
20
+ id="install-progress"
21
+ style="width: 0%"
22
+ ></div>
23
+ </div>
24
+ </div>
10
25
  <div class="card-body">
11
26
  <pre id="device-log"></pre>
12
27
  </div>
@@ -15,7 +30,7 @@
15
30
  </div>
16
31
  </div>
17
32
  <script>
18
- device = "{{ device }}"
33
+ device = "{{ device }}";
19
34
  </script>
20
35
  <script src="{{ url_for('static', path='js/logs.js') }}"></script>
21
36
  {% endblock content %}
@@ -1,64 +1,117 @@
1
- <!DOCTYPE html>
1
+ <!doctype html>
2
2
  <html lang="en">
3
3
  <head>
4
4
  <script>
5
5
  const PERMISSIONS = {{request.user.get_json_permissions() | safe}};
6
6
  </script>
7
- <meta charset="utf-8">
8
- <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <meta charset="utf-8" />
8
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
9
9
  <title>{{title}}</title>
10
10
  <!--bootstrap-->
11
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
12
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
13
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
11
+ <link
12
+ href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
13
+ rel="stylesheet"
14
+ integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
15
+ crossorigin="anonymous"
16
+ />
17
+ <script
18
+ src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
19
+ integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
20
+ crossorigin="anonymous"
21
+ ></script>
22
+ <link
23
+ rel="stylesheet"
24
+ href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
25
+ />
14
26
  <!--data tables-->
15
- <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" rel="stylesheet">
27
+ <link
28
+ 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"
29
+ rel="stylesheet"
30
+ />
16
31
  <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>
17
32
  <!--favicon-->
18
- <link rel="icon" href="{{ url_for('static', path='favicon.svg') }}">
33
+ <link rel="icon" href="{{ url_for('static', path='favicon.svg') }}" />
19
34
  <!--data tables alignment fix-->
20
35
  <style>
21
36
  th.dt-type-numeric {
22
- text-align: left!important;
37
+ text-align: left !important;
23
38
  }
24
39
  td.dt-type-numeric {
25
- text-align: left!important;
40
+ text-align: left !important;
41
+ }
42
+ .active {
43
+ color: var(--bs-nav-pills-link-active-color);
44
+ background-color: transparent;
26
45
  }
27
46
  </style>
28
47
  <script>
29
48
  const TABLE_UPDATE_TIME = 3000;
30
49
  </script>
50
+ <script src="{{ url_for('static', path='js/util.js') }}"></script>
31
51
  </head>
32
52
  <body data-bs-theme="dark">
33
53
  <nav class="navbar navbar-expand-lg bg-body-tertiary">
34
54
  <div class="container-fluid">
35
55
  <a class="navbar-brand" href="/ui/home">
36
- <img src="{{ request.url_for('static', path='svg/goosebit-logo.svg') }}" class="me-2" style="height:30px; width: 30px;"/>
37
- GooseBit
56
+ <img
57
+ src="{{ request.url_for('static', path='svg/goosebit-logo.svg') }}"
58
+ class="me-2"
59
+ style="height: 30px; width: 30px"
60
+ />
61
+ gooseBit
38
62
  </a>
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">
63
+ <button
64
+ class="navbar-toggler"
65
+ type="button"
66
+ data-bs-toggle="collapse"
67
+ data-bs-target="#navbar"
68
+ aria-controls="navbar"
69
+ aria-expanded="false"
70
+ aria-label="Toggle navigation"
71
+ >
40
72
  <span class="navbar-toggler-icon"></span>
41
73
  </button>
42
74
  <div class="collapse navbar-collapse" id="navbar">
43
75
  <div class="navbar-nav">
44
76
  {% if "home.read" in request.user.permissions %}
45
- <a class="nav-link{% if request.url.path.endswith('home') %} active{% endif %}" href="/ui/home">Home</a>
46
- {% endif %}
47
- {% if "firmware.read" in request.user.permissions %}
48
- <a class="nav-link{% if request.url.path.endswith('firmware') %} active{% endif %}" href="/ui/firmware">Firmware</a>
49
- {% endif %}
50
- {% if "devices.read" in request.user.permissions %}
51
- <a class="nav-link{% if request.url.path.endswith('devices') %} active{% endif %}" href="/ui/devices">Devices</a>
77
+ <a
78
+ class="nav-link{% if request.url.path.endswith('home') %} active{% endif %}"
79
+ href="/ui/home"
80
+ >Home</a
81
+ >
82
+ {% endif %} {% if "firmware.read" in
83
+ request.user.permissions %}
84
+ <a
85
+ class="nav-link{% if request.url.path.endswith('firmware') %} active{% endif %}"
86
+ href="/ui/firmware"
87
+ >Firmware</a
88
+ >
89
+ {% endif %} {% if "device.read" in
90
+ request.user.permissions %}
91
+ <a
92
+ class="nav-link{% if request.url.path.endswith('devices') %} active{% endif %}"
93
+ href="/ui/devices"
94
+ >Devices</a
95
+ >
96
+ {% endif %} {% if "rollout.read" in
97
+ request.user.permissions %}
98
+ <a
99
+ class="nav-link{% if request.url.path.endswith('rollouts') %} active{% endif %}"
100
+ href="/ui/rollouts"
101
+ >Rollouts</a
102
+ >
52
103
  {% endif %}
53
-
54
104
  </div>
55
- <div class="navbar-nav d-flex flex-fill justify-content-end">
56
- <a class="nav-link" href="/logout">Logout<i class="bi bi-box-arrow-right ps-2"></i></a>
105
+ <div
106
+ class="navbar-nav d-flex flex-fill justify-content-end"
107
+ >
108
+ <a class="nav-link" href="/logout"
109
+ >Logout<i class="bi bi-box-arrow-right ps-2"></i
110
+ ></a>
57
111
  </div>
58
112
  </div>
59
113
  </div>
60
114
  </nav>
61
- {% block content %}
62
- {% endblock content %}
115
+ {% block content %} {% endblock content %}
63
116
  </body>
64
117
  </html>
@@ -0,0 +1,76 @@
1
+ {% extends "nav.html" %} {% block content %}
2
+ <div class="container-fluid">
3
+ <div class="row p-2 d-flex justify-content-center">
4
+ <div class="col">
5
+ <table id="rollout-table" class="table table-hover">
6
+ <thead>
7
+ <tr>
8
+ <th>Id</th>
9
+ <th>Created</th>
10
+ <th>Name</th>
11
+ <th>Feed</th>
12
+ <th>Flavour</th>
13
+ <th>Firmware File</th>
14
+ <th>Firmware 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"></tbody>
21
+ </table>
22
+ </div>
23
+ </div>
24
+ </div>
25
+ <div class="modal" id="rollout-create-modal">
26
+ <div class="modal-dialog">
27
+ <div class="modal-content">
28
+ <div class="modal-header">
29
+ <h5 class="modal-title">Create Rollout</h5>
30
+ <button
31
+ type="button"
32
+ class="btn-close"
33
+ data-bs-dismiss="modal"
34
+ aria-label="Close"
35
+ ></button>
36
+ </div>
37
+ <div class="modal-body">
38
+ <input
39
+ id="rollout-selected-name"
40
+ class="form-control mb-3"
41
+ placeholder="Name"
42
+ />
43
+ <input
44
+ id="rollout-selected-feed"
45
+ class="form-control mb-3"
46
+ placeholder="Feed"
47
+ />
48
+ <input
49
+ id="rollout-selected-flavor"
50
+ class="form-control mb-3"
51
+ placeholder="Flavor"
52
+ />
53
+ <select class="form-select" id="selected-fw"></select>
54
+ </div>
55
+ <div class="modal-footer">
56
+ <button
57
+ type="button"
58
+ class="btn btn-secondary"
59
+ data-bs-dismiss="modal"
60
+ >
61
+ Close
62
+ </button>
63
+ <button
64
+ type="button"
65
+ class="btn btn-outline-light"
66
+ data-bs-dismiss="modal"
67
+ onclick="createRollout()"
68
+ >
69
+ Save changes
70
+ </button>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ </div>
75
+ <script src="{{ url_for('static', path='js/rollouts.js') }}"></script>
76
+ {% endblock content %}
@@ -1 +1 @@
1
- from .routes import router
1
+ from .routes import router # noqa: F401
@@ -1 +1 @@
1
- from .routes import router
1
+ from .routes import router # noqa: F401
@@ -1 +1 @@
1
- from .routes import router
1
+ from .routes import router # noqa: F401
@@ -1,11 +1,16 @@
1
1
  import json
2
+ import logging
2
3
 
3
4
  from fastapi import APIRouter, Depends
4
5
  from fastapi.requests import Request
5
6
 
6
- from goosebit.updater.manager import UpdateManager, get_update_manager
7
+ from goosebit.models import Firmware, UpdateStateEnum
8
+ from goosebit.settings import POLL_TIME_REGISTRATION
9
+ from goosebit.updater.manager import HandlingType, UpdateManager, get_update_manager
10
+ from goosebit.updates import generate_chunk
11
+
12
+ logger = logging.getLogger("DDI API")
7
13
 
8
- # v1 is hardware revision
9
14
  router = APIRouter(prefix="/v1")
10
15
 
11
16
 
@@ -16,17 +21,49 @@ async def polling(
16
21
  dev_id: str,
17
22
  updater: UpdateManager = Depends(get_update_manager),
18
23
  ):
19
- return {
20
- "config": {"polling": {"sleep": updater.poll_time}},
21
- "_links": {
22
- "deploymentBase": {
24
+ links = {}
25
+
26
+ sleep = updater.poll_time
27
+ device = await updater.get_device()
28
+
29
+ if device.last_state == UpdateStateEnum.UNKNOWN:
30
+ # device registration
31
+ sleep = POLL_TIME_REGISTRATION
32
+ links["configData"] = {
33
+ "href": str(
34
+ request.url_for(
35
+ "config_data",
36
+ tenant=tenant,
37
+ dev_id=dev_id,
38
+ )
39
+ )
40
+ }
41
+ logger.info(f"Skip: registration required, device={updater.dev_id}")
42
+
43
+ elif device.last_state == UpdateStateEnum.ERROR and not device.force_update:
44
+ logger.warning(f"Skip: device in error state, device={updater.dev_id}")
45
+ pass
46
+
47
+ else:
48
+ # provide update if available. Note: this is also required while in state "running", otherwise swupdate
49
+ # won't confirm a successful testing (might be a bug/problem in swupdate)
50
+ handling_type, firmware = await updater.get_update()
51
+ if handling_type != HandlingType.SKIP:
52
+ links["deploymentBase"] = {
23
53
  "href": str(
24
54
  request.url_for(
25
- "deployment_base", tenant=tenant, dev_id=dev_id, action_id=1
55
+ "deployment_base",
56
+ tenant=tenant,
57
+ dev_id=dev_id,
58
+ action_id=firmware.id,
26
59
  )
27
60
  )
28
- },
29
- },
61
+ }
62
+ logger.info(f"Forced: update available, device={updater.dev_id}")
63
+
64
+ return {
65
+ "config": {"polling": {"sleep": sleep}},
66
+ "_links": links,
30
67
  }
31
68
 
32
69
 
@@ -34,11 +71,13 @@ async def polling(
34
71
  async def config_data(
35
72
  request: Request,
36
73
  dev_id: str,
74
+ tenant: str,
37
75
  updater: UpdateManager = Depends(get_update_manager),
38
76
  ):
39
77
  data = await request.json()
40
78
  # TODO: make standard schema to deal with this
41
- print(data)
79
+ await updater.update_config_data(**data["data"])
80
+ logger.info(f"Updating config data, device={updater.dev_id}")
42
81
  return {"success": True, "message": "Updated swupdate data."}
43
82
 
44
83
 
@@ -50,16 +89,16 @@ async def deployment_base(
50
89
  action_id: int,
51
90
  updater: UpdateManager = Depends(get_update_manager),
52
91
  ):
53
- artifact = await updater.get_update_file()
54
- update = await updater.get_update_mode()
55
- await updater.save()
92
+ handling_type, firmware = await updater.get_update()
93
+
94
+ logger.info(f"Request deployment base, device={updater.dev_id}")
56
95
 
57
96
  return {
58
97
  "id": f"{action_id}",
59
98
  "deployment": {
60
- "download": update,
61
- "update": update,
62
- "chunks": artifact.generate_chunk(request, tenant=tenant, dev_id=dev_id),
99
+ "download": str(handling_type),
100
+ "update": str(handling_type),
101
+ "chunks": generate_chunk(request, firmware),
63
102
  },
64
103
  }
65
104
 
@@ -68,25 +107,74 @@ async def deployment_base(
68
107
  async def deployment_feedback(
69
108
  request: Request,
70
109
  tenant: str,
71
- dev_id: str,
72
110
  action_id: int,
73
111
  updater: UpdateManager = Depends(get_update_manager),
74
112
  ):
75
113
  try:
76
114
  data = await request.json()
77
- except json.JSONDecodeError:
115
+ except json.JSONDecodeError as e:
116
+ logging.warning(f"Parsing deployment feedback failed, error={e}, device={updater.dev_id}")
78
117
  return
79
118
  try:
80
- state = data["status"]["result"]["finished"]
81
- await updater.update_device_state(state)
82
- except KeyError:
83
- pass
119
+ execution = data["status"]["execution"]
120
+
121
+ if execution == "proceeding":
122
+ await updater.update_device_state(UpdateStateEnum.RUNNING)
123
+ logger.debug(f"Installation in progress, device={updater.dev_id}")
124
+
125
+ elif execution == "closed":
126
+ state = data["status"]["result"]["finished"]
127
+
128
+ await updater.update_force_update(False)
129
+ await updater.update_log_complete(True)
130
+
131
+ reported_firmware = await Firmware.get_or_none(id=data["id"])
132
+
133
+ # From hawkBit docu: DDI defines also a status NONE which will not be interpreted by the update server
134
+ # and handled like SUCCESS.
135
+ if state == "success" or state == "none":
136
+ await updater.update_device_state(UpdateStateEnum.FINISHED)
137
+
138
+ # not guaranteed to be the correct rollout - see next comment.
139
+ rollout = await updater.get_rollout()
140
+ if rollout:
141
+ if rollout.firmware == reported_firmware:
142
+ rollout.success_count += 1
143
+ await rollout.save()
144
+ else:
145
+ logging.warning(
146
+ f"Updating rollout success stats failed, firmware={reported_firmware.id}, device={updater.dev_id}" # noqa: E501
147
+ )
148
+
149
+ # setting the currently installed version based on the current assigned firmware / existing rollouts
150
+ # is problematic. Better to assign custom action_id for each update (rollout id? firmware id? new id?).
151
+ # Alternatively - but requires customization on the gateway side - use version reported by the gateway.
152
+ await updater.update_fw_version(reported_firmware.version)
153
+ logger.debug(f"Installation successful, firmware={reported_firmware.version}, device={updater.dev_id}")
154
+
155
+ elif state == "failure":
156
+ await updater.update_device_state(UpdateStateEnum.ERROR)
157
+
158
+ # not guaranteed to be the correct rollout - see comment above.
159
+ rollout = await updater.get_rollout()
160
+ if rollout:
161
+ if rollout.firmware == reported_firmware:
162
+ rollout.failure_count += 1
163
+ await rollout.save()
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}")
84
173
 
85
174
  try:
86
175
  log = data["status"]["details"]
87
176
  await updater.update_log("\n".join(log))
88
177
  except KeyError:
89
- pass
178
+ logging.warning(f"No details to update update log, device={updater.dev_id}")
90
179
 
91
- await updater.save()
92
180
  return {"id": str(action_id)}