goosebit 0.2.8__tar.gz → 0.2.10__tar.gz

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 (137) hide show
  1. {goosebit-0.2.8 → goosebit-0.2.10}/PKG-INFO +10 -5
  2. {goosebit-0.2.8 → goosebit-0.2.10}/README.md +7 -4
  3. goosebit-0.2.10/goosebit/db/migrations/models/6_20250904081506_add_image_format.py +11 -0
  4. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/db/models.py +16 -0
  5. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/schema/devices.py +3 -23
  6. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/schema/rollouts.py +2 -12
  7. goosebit-0.2.10/goosebit/ui/bff/common/columns.py +207 -0
  8. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/devices/routes.py +4 -0
  9. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/rollouts/routes.py +6 -1
  10. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/software/responses.py +2 -2
  11. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/templates/software.html.jinja +1 -1
  12. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/updates/__init__.py +5 -4
  13. goosebit-0.2.10/goosebit/updates/swdesc/__init__.py +45 -0
  14. goosebit-0.2.10/goosebit/updates/swdesc/func.py +19 -0
  15. goosebit-0.2.10/goosebit/updates/swdesc/rauc.py +49 -0
  16. goosebit-0.2.8/goosebit/updates/swdesc.py → goosebit-0.2.10/goosebit/updates/swdesc/swu.py +8 -40
  17. {goosebit-0.2.8 → goosebit-0.2.10}/pyproject.toml +4 -2
  18. goosebit-0.2.8/goosebit/ui/bff/common/columns.py +0 -50
  19. {goosebit-0.2.8 → goosebit-0.2.10}/LICENSE +0 -0
  20. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/__init__.py +0 -0
  21. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/__main__.py +0 -0
  22. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/__init__.py +0 -0
  23. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/responses.py +0 -0
  24. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/routes.py +0 -0
  25. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/telemetry/__init__.py +0 -0
  26. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/telemetry/metrics.py +0 -0
  27. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/telemetry/prometheus/__init__.py +0 -0
  28. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/telemetry/prometheus/readers.py +0 -0
  29. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/telemetry/prometheus/routes.py +0 -0
  30. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/telemetry/routes.py +0 -0
  31. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/__init__.py +0 -0
  32. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/devices/__init__.py +0 -0
  33. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/devices/device/__init__.py +0 -0
  34. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/devices/device/responses.py +0 -0
  35. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/devices/device/routes.py +0 -0
  36. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/devices/requests.py +0 -0
  37. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/devices/responses.py +0 -0
  38. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/devices/routes.py +0 -0
  39. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/download/__init__.py +0 -0
  40. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/download/routes.py +0 -0
  41. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/rollouts/__init__.py +0 -0
  42. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/rollouts/requests.py +0 -0
  43. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/rollouts/responses.py +0 -0
  44. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/rollouts/routes.py +0 -0
  45. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/routes.py +0 -0
  46. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/settings/__init__.py +0 -0
  47. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/settings/routes.py +0 -0
  48. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/settings/users/__init__.py +0 -0
  49. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/settings/users/requests.py +0 -0
  50. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/settings/users/responses.py +0 -0
  51. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/settings/users/routes.py +0 -0
  52. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/software/__init__.py +0 -0
  53. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/software/requests.py +0 -0
  54. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/software/responses.py +0 -0
  55. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/api/v1/software/routes.py +0 -0
  56. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/auth/__init__.py +0 -0
  57. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/auth/permissions.py +0 -0
  58. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/db/__init__.py +0 -0
  59. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/db/config.py +0 -0
  60. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/db/migrations/models/0_20240830054046_init.py +0 -0
  61. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/db/migrations/models/1_20241109151811_update.py +0 -0
  62. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/db/migrations/models/2_20241121113728_update.py +0 -0
  63. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/db/migrations/models/3_20241121140210_update.py +0 -0
  64. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/db/migrations/models/4_20250324110331_update.py +0 -0
  65. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/db/migrations/models/4_20250402085235_rename_uuid_to_id.py +0 -0
  66. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/db/migrations/models/5_20250619090242_null_feed.py +0 -0
  67. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/db/pg_ssl_context.py +0 -0
  68. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/device_manager.py +0 -0
  69. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/plugins/__init__.py +0 -0
  70. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/schema/__init__.py +0 -0
  71. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/schema/plugins.py +0 -0
  72. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/schema/software.py +0 -0
  73. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/schema/updates.py +0 -0
  74. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/schema/users.py +0 -0
  75. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/settings/__init__.py +0 -0
  76. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/settings/const.py +0 -0
  77. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/settings/schema.py +0 -0
  78. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/storage/__init__.py +0 -0
  79. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/storage/base.py +0 -0
  80. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/storage/filesystem.py +0 -0
  81. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/storage/s3.py +0 -0
  82. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/__init__.py +0 -0
  83. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/__init__.py +0 -0
  84. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/common/__init__.py +0 -0
  85. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/common/requests.py +0 -0
  86. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/common/responses.py +0 -0
  87. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/common/util.py +0 -0
  88. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/devices/__init__.py +0 -0
  89. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/devices/device/__init__.py +0 -0
  90. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/devices/device/routes.py +0 -0
  91. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/devices/requests.py +0 -0
  92. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/devices/responses.py +0 -0
  93. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/download/__init__.py +0 -0
  94. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/download/routes.py +0 -0
  95. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/rollouts/__init__.py +0 -0
  96. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/rollouts/responses.py +0 -0
  97. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/routes.py +0 -0
  98. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/settings/__init__.py +0 -0
  99. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/settings/routes.py +0 -0
  100. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/settings/users/__init__.py +0 -0
  101. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/settings/users/responses.py +0 -0
  102. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/settings/users/routes.py +0 -0
  103. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/software/__init__.py +0 -0
  104. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/bff/software/routes.py +0 -0
  105. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/nav.py +0 -0
  106. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/routes.py +0 -0
  107. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/static/__init__.py +0 -0
  108. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/static/favicon.ico +0 -0
  109. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/static/favicon.svg +0 -0
  110. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/static/js/devices.js +0 -0
  111. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/static/js/login.js +0 -0
  112. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/static/js/logs.js +0 -0
  113. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/static/js/rollouts.js +0 -0
  114. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/static/js/settings.js +0 -0
  115. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/static/js/setup.js +0 -0
  116. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/static/js/software.js +0 -0
  117. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/static/js/util.js +0 -0
  118. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/static/svg/goosebit-logo.svg +0 -0
  119. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/templates/__init__.py +0 -0
  120. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/templates/devices.html.jinja +0 -0
  121. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/templates/login.html.jinja +0 -0
  122. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/templates/logs.html.jinja +0 -0
  123. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/templates/nav.html.jinja +0 -0
  124. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/templates/rollouts.html.jinja +0 -0
  125. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/templates/settings.html.jinja +0 -0
  126. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/ui/templates/setup.html.jinja +0 -0
  127. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/updater/__init__.py +0 -0
  128. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/updater/controller/__init__.py +0 -0
  129. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/updater/controller/routes.py +0 -0
  130. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/updater/controller/v1/__init__.py +0 -0
  131. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/updater/controller/v1/routes.py +0 -0
  132. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/updater/controller/v1/schema.py +0 -0
  133. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/updater/routes.py +0 -0
  134. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/users/__init__.py +0 -0
  135. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/util/__init__.py +0 -0
  136. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/util/path.py +0 -0
  137. {goosebit-0.2.8 → goosebit-0.2.10}/goosebit/util/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: goosebit
3
- Version: 0.2.8
3
+ Version: 0.2.10
4
4
  Summary: A simplistic, opinionated remote update server implementing hawkBit™'s DDI API
5
5
  Author: Brett Rowan
6
6
  Author-email: 121075405+b-rowan@users.noreply.github.com
@@ -25,10 +25,12 @@ Requires-Dist: opentelemetry-distro (>=0.57b0,<0.58)
25
25
  Requires-Dist: opentelemetry-exporter-prometheus (>=0.57b0,<0.58)
26
26
  Requires-Dist: opentelemetry-instrumentation-fastapi (>=0.57b0,<0.58)
27
27
  Requires-Dist: pydantic-settings[yaml] (>=2.10.1,<3.0.0)
28
+ Requires-Dist: pysquashfsimage (>=0.9.0,<1.0.0)
28
29
  Requires-Dist: python-multipart (>=0.0.20,<0.0.21)
29
30
  Requires-Dist: semver (>=3.0.4,<4.0.0)
30
31
  Requires-Dist: tortoise-orm (>=0.25.1,<0.26.0)
31
32
  Requires-Dist: uvicorn (>=0.35.0,<0.36.0)
33
+ Requires-Dist: zstandard (>=0.24.0,<0.25.0)
32
34
  Description-Content-Type: text/markdown
33
35
 
34
36
  # gooseBit
@@ -96,7 +98,10 @@ The software packages managed by gooseBit are either stored on the local filesys
96
98
 
97
99
  ## Assumptions
98
100
 
99
- - Devices use [SWUpdate](https://swupdate.org) for managing software updates.
101
+ - Devices use [SWUpdate](https://swupdate.org) or [RAUC](https://rauc.io) + [RAUC hawkBit Updater](https://rauc-hawkbit-updater.readthedocs.io) for managing software updates.
102
+ - Devices send certain attributes (`sw_version`, `hw_boardname`, `hw_revision`).
103
+ - Semantic versions are used.
104
+ - With RAUC and multiple hardware revisions, `compatible` in `manifest.raucm` is set to something like `my-board-rev4.2` or `Some Board 2b`.
100
105
 
101
106
  ## Features
102
107
 
@@ -104,14 +109,14 @@ The software packages managed by gooseBit are either stored on the local filesys
104
109
 
105
110
  When a device connects to gooseBit for the first time, it is automatically added to the device registry. The server will then request the device's configuration data, including:
106
111
 
107
- - `hw_model` and `hw_revision`: Used to match compatible software.
112
+ - `hw_boardname` and `hw_revision`: Used to match compatible software.
108
113
  - `sw_version`: Indicates the currently installed software version.
109
114
 
110
115
  The registry tracks each device's status, including the last online timestamp, installed software version, update state, and more.
111
116
 
112
117
  ### Software Repository
113
118
 
114
- Software packages (`*.swu` files) can be hosted directly on the gooseBit server or on an external server. gooseBit parses the software metadata to determine compatibility with specific hardware models and revisions.
119
+ Software packages (`*.swu`/`*.raucb` files) can be hosted directly on the gooseBit server or on an external server. gooseBit parses the software metadata to determine compatibility with specific hardware models and revisions.
115
120
 
116
121
  ### Device Update Modes
117
122
 
@@ -268,7 +273,7 @@ The structure of gooseBit is as follows:
268
273
  - `templates`: Jinja2 formatted templates.
269
274
  - `nav`: Navbar handler.
270
275
  - `updater`: DDI API handler and device update manager.
271
- - `updates`: SWUpdate file parsing.
276
+ - `updates`: SWUpdate/RAUC file parsing.
272
277
  - `auth`: Authentication functions and permission handling.
273
278
  - `models`: Database models.
274
279
  - `db`: Database config and initialization.
@@ -63,7 +63,10 @@ The software packages managed by gooseBit are either stored on the local filesys
63
63
 
64
64
  ## Assumptions
65
65
 
66
- - Devices use [SWUpdate](https://swupdate.org) for managing software updates.
66
+ - Devices use [SWUpdate](https://swupdate.org) or [RAUC](https://rauc.io) + [RAUC hawkBit Updater](https://rauc-hawkbit-updater.readthedocs.io) for managing software updates.
67
+ - Devices send certain attributes (`sw_version`, `hw_boardname`, `hw_revision`).
68
+ - Semantic versions are used.
69
+ - With RAUC and multiple hardware revisions, `compatible` in `manifest.raucm` is set to something like `my-board-rev4.2` or `Some Board 2b`.
67
70
 
68
71
  ## Features
69
72
 
@@ -71,14 +74,14 @@ The software packages managed by gooseBit are either stored on the local filesys
71
74
 
72
75
  When a device connects to gooseBit for the first time, it is automatically added to the device registry. The server will then request the device's configuration data, including:
73
76
 
74
- - `hw_model` and `hw_revision`: Used to match compatible software.
77
+ - `hw_boardname` and `hw_revision`: Used to match compatible software.
75
78
  - `sw_version`: Indicates the currently installed software version.
76
79
 
77
80
  The registry tracks each device's status, including the last online timestamp, installed software version, update state, and more.
78
81
 
79
82
  ### Software Repository
80
83
 
81
- Software packages (`*.swu` files) can be hosted directly on the gooseBit server or on an external server. gooseBit parses the software metadata to determine compatibility with specific hardware models and revisions.
84
+ Software packages (`*.swu`/`*.raucb` files) can be hosted directly on the gooseBit server or on an external server. gooseBit parses the software metadata to determine compatibility with specific hardware models and revisions.
82
85
 
83
86
  ### Device Update Modes
84
87
 
@@ -235,7 +238,7 @@ The structure of gooseBit is as follows:
235
238
  - `templates`: Jinja2 formatted templates.
236
239
  - `nav`: Navbar handler.
237
240
  - `updater`: DDI API handler and device update manager.
238
- - `updates`: SWUpdate file parsing.
241
+ - `updates`: SWUpdate/RAUC file parsing.
239
242
  - `auth`: Authentication functions and permission handling.
240
243
  - `models`: Database models.
241
244
  - `db`: Database config and initialization.
@@ -0,0 +1,11 @@
1
+ from tortoise import BaseDBAsyncClient
2
+
3
+
4
+ async def upgrade(db: BaseDBAsyncClient) -> str:
5
+ return """
6
+ ALTER TABLE "software" ADD "image_format" SMALLINT NOT NULL DEFAULT 0 /* SWU: 0\nRAUC: 1 */;"""
7
+
8
+
9
+ async def downgrade(db: BaseDBAsyncClient) -> str:
10
+ return """
11
+ ALTER TABLE "software" DROP COLUMN "image_format";"""
@@ -123,12 +123,28 @@ class Hardware(Model):
123
123
  revision = fields.CharField(max_length=255)
124
124
 
125
125
 
126
+ class SoftwareImageFormat(IntEnum):
127
+ SWU = 0
128
+ RAUC = 1
129
+
130
+ def __str__(self):
131
+ return self.name.upper()
132
+
133
+ @classmethod
134
+ def from_str(cls, name):
135
+ try:
136
+ return cls[name.upper()]
137
+ except KeyError:
138
+ return cls.SWU
139
+
140
+
126
141
  class Software(Model):
127
142
  id = fields.IntField(primary_key=True)
128
143
  uri = fields.CharField(max_length=255)
129
144
  size = fields.BigIntField()
130
145
  hash = fields.CharField(max_length=255)
131
146
  version = fields.CharField(max_length=255)
147
+ image_format = fields.IntEnumField(SoftwareImageFormat, default=SoftwareImageFormat.SWU)
132
148
  compatibility = fields.ManyToManyField(
133
149
  "models.Hardware",
134
150
  related_name="softwares",
@@ -5,7 +5,7 @@ from datetime import datetime
5
5
  from enum import Enum, IntEnum, StrEnum
6
6
  from typing import Annotated
7
7
 
8
- from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, computed_field
8
+ from pydantic import BaseModel, BeforeValidator, ConfigDict, computed_field
9
9
 
10
10
  from goosebit.db.models import UpdateModeEnum, UpdateStateEnum
11
11
  from goosebit.schema.software import HardwareSchema, SoftwareSchema
@@ -34,8 +34,8 @@ class DeviceSchema(BaseModel):
34
34
  name: str | None
35
35
  sw_version: str | None
36
36
 
37
- assigned_software: SoftwareSchema | None = Field(exclude=True)
38
- hardware: HardwareSchema | None = Field(exclude=True)
37
+ assigned_software: SoftwareSchema | None
38
+ hardware: HardwareSchema | None
39
39
 
40
40
  feed: str | None
41
41
  progress: int | None
@@ -53,26 +53,6 @@ class DeviceSchema(BaseModel):
53
53
  def polling(self) -> bool | None:
54
54
  return self.last_seen < (self.poll_seconds + 10) if self.last_seen is not None else None
55
55
 
56
- @computed_field # type: ignore[misc]
57
- @property
58
- def sw_target_version(self) -> str | None:
59
- return self.assigned_software.version if self.assigned_software is not None else None
60
-
61
- @computed_field # type: ignore[misc]
62
- @property
63
- def sw_assigned(self) -> int | None:
64
- return self.assigned_software.id if self.assigned_software is not None else None
65
-
66
- @computed_field # type: ignore[misc]
67
- @property
68
- def hw_model(self) -> str | None:
69
- return self.hardware.model if self.hardware is not None else None
70
-
71
- @computed_field # type: ignore[misc]
72
- @property
73
- def hw_revision(self) -> str | None:
74
- return self.hardware.revision if self.hardware is not None else None
75
-
76
56
  @computed_field # type: ignore[misc]
77
57
  @property
78
58
  def poll_seconds(self) -> int:
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from datetime import datetime
4
4
 
5
- from pydantic import BaseModel, ConfigDict, Field, computed_field, field_serializer
5
+ from pydantic import BaseModel, ConfigDict, field_serializer
6
6
 
7
7
  from goosebit.schema.software import SoftwareSchema
8
8
 
@@ -14,21 +14,11 @@ class RolloutSchema(BaseModel):
14
14
  created_at: datetime
15
15
  name: str | None
16
16
  feed: str
17
- software: SoftwareSchema = Field(exclude=True)
17
+ software: SoftwareSchema
18
18
  paused: bool
19
19
  success_count: int
20
20
  failure_count: int
21
21
 
22
- @computed_field # type: ignore[misc]
23
- @property
24
- def sw_version(self) -> str:
25
- return self.software.version
26
-
27
- @computed_field # type: ignore[misc]
28
- @property
29
- def sw_file(self) -> str:
30
- return self.software.path.name
31
-
32
22
  @field_serializer("created_at")
33
23
  def serialize_created_at(self, created_at: datetime, _info):
34
24
  return int(created_at.timestamp() * 1000)
@@ -0,0 +1,207 @@
1
+ from .responses import DTColumnDescription
2
+
3
+
4
+ class DeviceColumns:
5
+ id = DTColumnDescription(
6
+ title="ID",
7
+ data="id",
8
+ name="id",
9
+ searchable=True,
10
+ orderable=True,
11
+ )
12
+ name = DTColumnDescription(
13
+ title="Name",
14
+ data="name",
15
+ name="name",
16
+ searchable=True,
17
+ orderable=True,
18
+ )
19
+ hw_model = DTColumnDescription(
20
+ title="Model",
21
+ data="hardware.model",
22
+ name="hardware__model",
23
+ searchable=True,
24
+ orderable=True,
25
+ )
26
+ hw_revision = DTColumnDescription(
27
+ title="Revision",
28
+ data="hardware.revision",
29
+ name="hardware__revision",
30
+ searchable=True,
31
+ orderable=True,
32
+ )
33
+ feed = DTColumnDescription(
34
+ title="Feed",
35
+ data="feed",
36
+ name="feed",
37
+ searchable=True,
38
+ orderable=True,
39
+ )
40
+ sw_version = DTColumnDescription(
41
+ title="Installed Software",
42
+ data="sw_version",
43
+ name="sw_version",
44
+ searchable=True,
45
+ orderable=True,
46
+ )
47
+ sw_target_version = DTColumnDescription(
48
+ title="Target Software",
49
+ data="assigned_software.version",
50
+ name="assigned_software__version",
51
+ searchable=True,
52
+ orderable=True,
53
+ )
54
+ update_mode = DTColumnDescription(
55
+ title="Update Mode",
56
+ data="update_mode",
57
+ name="update_mode",
58
+ searchable=True,
59
+ orderable=True,
60
+ )
61
+ last_state = DTColumnDescription(
62
+ title="State",
63
+ data="last_state",
64
+ name="last_state",
65
+ searchable=True,
66
+ orderable=True,
67
+ )
68
+ force_update = DTColumnDescription(
69
+ title="Force Update",
70
+ data="force_update",
71
+ name="force_update",
72
+ orderable=True,
73
+ )
74
+ progress = DTColumnDescription(
75
+ title="Progress",
76
+ data="progress",
77
+ name="progress",
78
+ orderable=True,
79
+ )
80
+ last_ip = DTColumnDescription(
81
+ title="Last IP",
82
+ data="last_ip",
83
+ name="last_ip",
84
+ searchable=True,
85
+ orderable=True,
86
+ )
87
+ polling = DTColumnDescription(
88
+ title="Polling",
89
+ data="polling",
90
+ )
91
+ last_seen = DTColumnDescription(
92
+ title="Last Seen",
93
+ data="last_seen",
94
+ name="last_seen",
95
+ orderable=True,
96
+ )
97
+
98
+
99
+ class RolloutColumns:
100
+ id = DTColumnDescription(
101
+ title="ID",
102
+ data="id",
103
+ visible=False,
104
+ )
105
+ created_at = DTColumnDescription(
106
+ title="Created",
107
+ data="created_at",
108
+ name="created_at",
109
+ orderable=True,
110
+ )
111
+ name = DTColumnDescription(
112
+ title="Name",
113
+ data="name",
114
+ name="name",
115
+ searchable=True,
116
+ orderable=True,
117
+ )
118
+ feed = DTColumnDescription(
119
+ title="Feed",
120
+ data="feed",
121
+ name="feed",
122
+ searchable=True,
123
+ orderable=True,
124
+ )
125
+ sw_file = DTColumnDescription(
126
+ title="Software File",
127
+ data="software.name",
128
+ name="software__uri", # May cause strange orderings sorting by uri instead of the end of the path
129
+ searchable=True,
130
+ orderable=True,
131
+ )
132
+ sw_version = DTColumnDescription(
133
+ title="Software Version",
134
+ data="software.version",
135
+ name="software__version",
136
+ searchable=True,
137
+ orderable=True,
138
+ )
139
+ paused = DTColumnDescription(
140
+ title="Paused",
141
+ name="paused",
142
+ data="paused",
143
+ orderable=True,
144
+ )
145
+ success_count = DTColumnDescription(
146
+ title="Success Count",
147
+ data="success_count",
148
+ name="success_count",
149
+ orderable=True,
150
+ )
151
+ failure_count = DTColumnDescription(
152
+ title="Failure Count",
153
+ data="failure_count",
154
+ name="failure_count",
155
+ orderable=True,
156
+ )
157
+
158
+
159
+ class SoftwareColumns:
160
+ id = DTColumnDescription(
161
+ title="ID",
162
+ data="id",
163
+ visible=False,
164
+ )
165
+ name = DTColumnDescription(
166
+ title="Name",
167
+ data="name",
168
+ name="uri", # May cause strange orderings sorting by uri instead of the end of the path
169
+ searchable=True,
170
+ orderable=True,
171
+ )
172
+ version = DTColumnDescription(
173
+ title="Version",
174
+ data="version",
175
+ name="version",
176
+ searchable=True,
177
+ orderable=True,
178
+ )
179
+ compatibility = DTColumnDescription(
180
+ title="Compatibility",
181
+ name="compatibility",
182
+ data="compatibility",
183
+ )
184
+ size = DTColumnDescription(
185
+ title="Size",
186
+ name="size",
187
+ data="size",
188
+ orderable=True,
189
+ )
190
+
191
+
192
+ class SettingsUsersColumns:
193
+ username = DTColumnDescription(
194
+ title="Username",
195
+ data="username",
196
+ searchable=True,
197
+ orderable=True,
198
+ )
199
+ enabled = DTColumnDescription(
200
+ title="Enabled",
201
+ data="enabled",
202
+ orderable=True,
203
+ )
204
+ permissions = DTColumnDescription(
205
+ title="Permissions",
206
+ data="permissions",
207
+ )
@@ -37,10 +37,14 @@ async def devices_get(dt_query: Annotated[DataTableRequest, Depends(parse_datata
37
37
  return (
38
38
  Q(id__icontains=search_value)
39
39
  | Q(name__icontains=search_value)
40
+ | Q(hardware__model__icontains=search_value)
41
+ | Q(hardware__revision__icontains=search_value)
40
42
  | Q(feed__icontains=search_value)
41
43
  | Q(sw_version__icontains=search_value)
44
+ | Q(assigned_software__version__icontains=search_value)
42
45
  | Q(update_mode=int(UpdateModeEnum.from_str(search_value)))
43
46
  | Q(last_state=int(UpdateStateEnum.from_str(search_value)))
47
+ | Q(last_ip__icontains=search_value)
44
48
  )
45
49
 
46
50
  query = Device.all().prefetch_related("assigned_software", "hardware", "assigned_software__compatibility")
@@ -23,7 +23,12 @@ router = APIRouter(prefix="/rollouts")
23
23
  )
24
24
  async def rollouts_get(dt_query: Annotated[DataTableRequest, Depends(parse_datatables_query)]) -> BFFRolloutsResponse:
25
25
  def search_filter(search_value):
26
- return Q(name__icontains=search_value) | Q(feed__icontains=search_value)
26
+ return (
27
+ Q(name__icontains=search_value)
28
+ | Q(feed__icontains=search_value)
29
+ | Q(software__uri__icontains=search_value)
30
+ | Q(software__version__icontains=search_value)
31
+ )
27
32
 
28
33
  query = Rollout.all().prefetch_related("software", "software__compatibility")
29
34
 
@@ -5,7 +5,8 @@ from tortoise.expressions import Q
5
5
  from tortoise.queryset import QuerySet
6
6
 
7
7
  from goosebit.schema.software import SoftwareSchema
8
- from goosebit.ui.bff.common.requests import DataTableOrderDirection, DataTableRequest
8
+
9
+ from ..common.requests import DataTableOrderDirection, DataTableRequest
9
10
 
10
11
 
11
12
  class BFFSoftwareResponse(BaseModel):
@@ -41,7 +42,6 @@ class BFFSoftwareResponse(BaseModel):
41
42
  query = query.limit(dt_query.length)
42
43
 
43
44
  software = await query.offset(dt_query.start).all()
44
-
45
45
  data = [SoftwareSchema.model_validate(s) for s in software]
46
46
 
47
47
  return cls(data=data, draw=dt_query.draw, records_total=total_records, records_filtered=filtered_records)
@@ -42,7 +42,7 @@
42
42
  <div class="col">
43
43
  <input class="form-control"
44
44
  type="file"
45
- accept=".swu"
45
+ accept=".swu,.raucb"
46
46
  id="file-upload"
47
47
  name="file" />
48
48
  </div>
@@ -19,25 +19,25 @@ from . import swdesc
19
19
  async def create_software_update(uri: str, temp_file: Path | None) -> Software:
20
20
  parsed_uri = urlparse(uri)
21
21
 
22
- # parse swu header into update_info
22
+ # parse image header into update_info
23
23
  if parsed_uri.scheme == "file":
24
24
  if temp_file is None:
25
25
  raise HTTPException(500, "Temporary file missing, cannot parse file information")
26
26
  try:
27
27
  update_info = await swdesc.parse_file(temp_file)
28
28
  except Exception:
29
- raise HTTPException(422, "Software swu header cannot be parsed")
29
+ raise HTTPException(422, "Software image header cannot be parsed")
30
30
 
31
31
  elif parsed_uri.scheme.startswith("http"):
32
32
  try:
33
33
  update_info = await swdesc.parse_remote(uri)
34
34
  except Exception:
35
- raise HTTPException(422, "Software swu header cannot be parsed")
35
+ raise HTTPException(422, "Software image header cannot be parsed")
36
36
  else:
37
37
  raise HTTPException(422, "Software URI protocol unknown")
38
38
 
39
39
  if update_info is None:
40
- raise HTTPException(422, "Software swu header contains invalid data")
40
+ raise HTTPException(422, "Software image header contains invalid data")
41
41
 
42
42
  # check for collisions
43
43
  is_colliding = await _is_software_colliding(update_info)
@@ -59,6 +59,7 @@ async def create_software_update(uri: str, temp_file: Path | None) -> Software:
59
59
  version=str(update_info["version"]),
60
60
  size=update_info["size"],
61
61
  hash=update_info["hash"],
62
+ image_format=update_info["image_format"],
62
63
  )
63
64
 
64
65
  # create compatibility information
@@ -0,0 +1,45 @@
1
+ import logging
2
+ import random
3
+ import string
4
+
5
+ import httpx
6
+ from anyio import Path, open_file
7
+
8
+ from goosebit.db.models import SoftwareImageFormat
9
+ from goosebit.storage import storage
10
+
11
+ from . import rauc, swu
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ async def parse_remote(url: str):
17
+ async with httpx.AsyncClient() as c:
18
+ file = await c.get(url)
19
+ temp_dir = Path(storage.get_temp_dir())
20
+ tmp_file_path = temp_dir.joinpath("".join(random.choices(string.ascii_lowercase, k=12)) + ".tmp")
21
+ try:
22
+ async with await open_file(tmp_file_path, "w+b") as f:
23
+ await f.write(file.content)
24
+ file_data = await parse_file(tmp_file_path) # Use anyio.Path for parse_file
25
+ except Exception:
26
+ raise
27
+ finally:
28
+ await tmp_file_path.unlink(missing_ok=True)
29
+ return file_data
30
+
31
+
32
+ async def parse_file(file: Path):
33
+ async with await open_file(file, "r+b") as f:
34
+ magic = await f.read(4)
35
+ if magic == swu.MAGIC:
36
+ image_format = SoftwareImageFormat.SWU
37
+ attributes = await swu.parse_file(file)
38
+ elif magic == rauc.MAGIC:
39
+ image_format = SoftwareImageFormat.RAUC
40
+ attributes = await rauc.parse_file(file)
41
+ else:
42
+ logger.warning(f"Unknown file format, magic={magic}")
43
+ raise ValueError(f"Unknown file format, magic={magic}")
44
+ attributes["image_format"] = image_format
45
+ return attributes
@@ -0,0 +1,19 @@
1
+ import hashlib
2
+
3
+ from anyio import AsyncFile
4
+
5
+
6
+ async def sha1_hash_file(fileobj: AsyncFile):
7
+ last = await fileobj.tell()
8
+ await fileobj.seek(0)
9
+ sha1_hash = hashlib.sha1()
10
+ buf = bytearray(2**18)
11
+ view = memoryview(buf)
12
+ while True:
13
+ size = await fileobj.readinto(buf)
14
+ if size == 0:
15
+ break
16
+ sha1_hash.update(view[:size])
17
+
18
+ await fileobj.seek(last)
19
+ return sha1_hash.hexdigest()
@@ -0,0 +1,49 @@
1
+ import configparser
2
+ import logging
3
+ import re
4
+
5
+ import semver
6
+ from anyio import Path, open_file
7
+ from PySquashfsImage import SquashFsImage
8
+
9
+ from .func import sha1_hash_file
10
+
11
+ MAGIC = b"hsqs"
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ async def parse_file(file: Path):
17
+ async with await open_file(file, "r+b") as f:
18
+ image_data = await f.read()
19
+
20
+ image = SquashFsImage.from_bytes(image_data)
21
+ manifest = image.select("manifest.raucm")
22
+ manifest_str = manifest.read_bytes().decode("utf-8")
23
+ config = configparser.ConfigParser()
24
+ config.read_string(manifest_str)
25
+ swdesc_attrs = parse_descriptor(config)
26
+
27
+ stat = await file.stat()
28
+ swdesc_attrs["size"] = stat.st_size
29
+ swdesc_attrs["hash"] = await sha1_hash_file(f)
30
+ return swdesc_attrs
31
+
32
+
33
+ def parse_descriptor(manifest: configparser.ConfigParser):
34
+ swdesc_attrs = {}
35
+ try:
36
+ swdesc_attrs["version"] = semver.Version.parse(manifest["update"].get("version"))
37
+ pattern = re.compile(r"^(?P<hw_model>.+?)[- ]?(?P<hw_revision>\w*[\d.]+\w*)?$")
38
+ hw_model = "default"
39
+ hw_revision = "default"
40
+ m = pattern.match(manifest["update"]["compatible"])
41
+ if m:
42
+ hw_model = m.group("hw_model")
43
+ hw_revision = m.group("hw_revision") or "default"
44
+ swdesc_attrs["compatibility"] = [{"hw_model": hw_model, "hw_revision": hw_revision}]
45
+ except KeyError as e:
46
+ logger.warning(f"Parsing RAUC descriptor failed, error={e}")
47
+ raise ValueError("Parsing RAUC descriptor failed", e)
48
+
49
+ return swdesc_attrs