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.
Files changed (105) hide show
  1. goosebit/__init__.py +50 -19
  2. goosebit/__main__.py +7 -0
  3. goosebit/api/responses.py +5 -0
  4. goosebit/api/routes.py +5 -15
  5. goosebit/api/telemetry/__init__.py +1 -0
  6. goosebit/{telemetry/__init__.py → api/telemetry/metrics.py} +9 -3
  7. goosebit/api/telemetry/prometheus/__init__.py +2 -0
  8. goosebit/api/telemetry/prometheus/readers.py +3 -0
  9. goosebit/api/telemetry/prometheus/routes.py +18 -0
  10. goosebit/api/telemetry/routes.py +9 -0
  11. goosebit/api/v1/__init__.py +1 -0
  12. goosebit/api/v1/devices/__init__.py +1 -0
  13. goosebit/api/v1/devices/device/__init__.py +1 -0
  14. goosebit/api/v1/devices/device/responses.py +13 -0
  15. goosebit/api/v1/devices/device/routes.py +27 -0
  16. goosebit/api/v1/devices/requests.py +7 -0
  17. goosebit/api/v1/devices/responses.py +16 -0
  18. goosebit/api/v1/devices/routes.py +35 -0
  19. goosebit/api/v1/download/__init__.py +1 -0
  20. goosebit/api/v1/download/routes.py +22 -0
  21. goosebit/api/v1/rollouts/__init__.py +1 -0
  22. goosebit/api/v1/rollouts/requests.py +16 -0
  23. goosebit/api/v1/rollouts/responses.py +19 -0
  24. goosebit/api/v1/rollouts/routes.py +50 -0
  25. goosebit/api/v1/routes.py +9 -0
  26. goosebit/api/v1/software/__init__.py +1 -0
  27. goosebit/api/v1/software/requests.py +5 -0
  28. goosebit/api/v1/software/responses.py +16 -0
  29. goosebit/api/v1/software/routes.py +77 -0
  30. goosebit/auth/__init__.py +101 -101
  31. goosebit/db/__init__.py +11 -0
  32. goosebit/db/config.py +10 -0
  33. goosebit/db/migrations/models/0_20240830054046_init.py +136 -0
  34. goosebit/{models.py → db/models.py} +17 -10
  35. goosebit/realtime/logs.py +4 -3
  36. goosebit/realtime/routes.py +2 -2
  37. goosebit/schema/__init__.py +0 -0
  38. goosebit/schema/devices.py +73 -0
  39. goosebit/schema/rollouts.py +31 -0
  40. goosebit/schema/software.py +37 -0
  41. goosebit/settings/__init__.py +17 -0
  42. goosebit/settings/const.py +21 -0
  43. goosebit/settings/schema.py +86 -0
  44. goosebit/ui/bff/__init__.py +1 -0
  45. goosebit/ui/bff/devices/__init__.py +1 -0
  46. goosebit/ui/bff/devices/requests.py +12 -0
  47. goosebit/ui/bff/devices/responses.py +39 -0
  48. goosebit/ui/bff/devices/routes.py +72 -0
  49. goosebit/ui/bff/download/__init__.py +1 -0
  50. goosebit/ui/bff/download/routes.py +22 -0
  51. goosebit/ui/bff/rollouts/__init__.py +1 -0
  52. goosebit/ui/bff/rollouts/responses.py +37 -0
  53. goosebit/ui/bff/rollouts/routes.py +52 -0
  54. goosebit/ui/bff/routes.py +11 -0
  55. goosebit/ui/bff/software/__init__.py +1 -0
  56. goosebit/ui/bff/software/responses.py +37 -0
  57. goosebit/ui/bff/software/routes.py +83 -0
  58. goosebit/ui/nav.py +16 -0
  59. goosebit/ui/routes.py +29 -66
  60. goosebit/ui/static/favicon.ico +0 -0
  61. goosebit/ui/static/favicon.svg +1 -1
  62. goosebit/ui/static/js/devices.js +47 -71
  63. goosebit/ui/static/js/index.js +4 -9
  64. goosebit/ui/static/js/login.js +23 -0
  65. goosebit/ui/static/js/logs.js +1 -1
  66. goosebit/ui/static/js/rollouts.js +33 -19
  67. goosebit/ui/static/js/{firmware.js → software.js} +87 -86
  68. goosebit/ui/static/js/util.js +60 -6
  69. goosebit/ui/static/svg/goosebit-logo.svg +1 -1
  70. goosebit/ui/templates/__init__.py +9 -1
  71. goosebit/ui/templates/devices.html.jinja +75 -0
  72. goosebit/ui/templates/index.html.jinja +25 -0
  73. goosebit/ui/templates/login.html.jinja +57 -0
  74. goosebit/ui/templates/logs.html.jinja +31 -0
  75. goosebit/ui/templates/nav.html.jinja +84 -0
  76. goosebit/ui/templates/rollouts.html.jinja +93 -0
  77. goosebit/ui/templates/software.html.jinja +139 -0
  78. goosebit/updater/controller/v1/routes.py +101 -96
  79. goosebit/updater/controller/v1/schema.py +56 -0
  80. goosebit/updater/manager.py +65 -65
  81. goosebit/updater/routes.py +3 -11
  82. goosebit/updates/__init__.py +91 -32
  83. goosebit/updates/swdesc.py +2 -7
  84. goosebit-0.2.1.dist-info/METADATA +173 -0
  85. goosebit-0.2.1.dist-info/RECORD +95 -0
  86. goosebit/api/devices.py +0 -136
  87. goosebit/api/download.py +0 -34
  88. goosebit/api/firmware.py +0 -57
  89. goosebit/api/helper.py +0 -30
  90. goosebit/api/rollouts.py +0 -87
  91. goosebit/db.py +0 -37
  92. goosebit/permissions.py +0 -75
  93. goosebit/settings.py +0 -64
  94. goosebit/telemetry/prometheus.py +0 -10
  95. goosebit/ui/templates/devices.html +0 -115
  96. goosebit/ui/templates/firmware.html +0 -163
  97. goosebit/ui/templates/index.html +0 -23
  98. goosebit/ui/templates/login.html +0 -65
  99. goosebit/ui/templates/logs.html +0 -36
  100. goosebit/ui/templates/nav.html +0 -117
  101. goosebit/ui/templates/rollouts.html +0 -76
  102. goosebit-0.1.2.dist-info/METADATA +0 -123
  103. goosebit-0.1.2.dist-info/RECORD +0 -51
  104. {goosebit-0.1.2.dist-info → goosebit-0.2.1.dist-info}/LICENSE +0 -0
  105. {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 Firmware, UpdateStateEnum
8
- from goosebit.settings import POLL_TIME_REGISTRATION
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 = POLL_TIME_REGISTRATION
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, firmware = await updater.get_update()
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=firmware.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
- request: Request,
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, firmware = await updater.get_update()
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": f"{action_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, firmware),
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
- request: Request,
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
- try:
114
- data = await request.json()
115
- except json.JSONDecodeError as e:
116
- logging.warning(f"Parsing deployment feedback failed, error={e}, device={updater.dev_id}")
117
- return
118
- try:
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}")
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["status"]["details"]
154
+ log = data.status.details
176
155
  await updater.update_log("\n".join(log))
177
- except KeyError:
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