goosebit 0.2.8__tar.gz → 0.2.9__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 (136) hide show
  1. {goosebit-0.2.8 → goosebit-0.2.9}/PKG-INFO +9 -4
  2. {goosebit-0.2.8 → goosebit-0.2.9}/README.md +6 -3
  3. goosebit-0.2.9/goosebit/db/migrations/models/6_20250904081506_add_image_format.py +11 -0
  4. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/db/models.py +16 -0
  5. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/templates/software.html.jinja +1 -1
  6. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/updates/__init__.py +5 -4
  7. goosebit-0.2.9/goosebit/updates/swdesc/__init__.py +45 -0
  8. goosebit-0.2.9/goosebit/updates/swdesc/func.py +19 -0
  9. goosebit-0.2.9/goosebit/updates/swdesc/rauc.py +49 -0
  10. goosebit-0.2.8/goosebit/updates/swdesc.py → goosebit-0.2.9/goosebit/updates/swdesc/swu.py +8 -40
  11. {goosebit-0.2.8 → goosebit-0.2.9}/pyproject.toml +4 -2
  12. {goosebit-0.2.8 → goosebit-0.2.9}/LICENSE +0 -0
  13. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/__init__.py +0 -0
  14. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/__main__.py +0 -0
  15. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/__init__.py +0 -0
  16. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/responses.py +0 -0
  17. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/routes.py +0 -0
  18. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/telemetry/__init__.py +0 -0
  19. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/telemetry/metrics.py +0 -0
  20. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/telemetry/prometheus/__init__.py +0 -0
  21. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/telemetry/prometheus/readers.py +0 -0
  22. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/telemetry/prometheus/routes.py +0 -0
  23. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/telemetry/routes.py +0 -0
  24. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/__init__.py +0 -0
  25. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/devices/__init__.py +0 -0
  26. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/devices/device/__init__.py +0 -0
  27. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/devices/device/responses.py +0 -0
  28. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/devices/device/routes.py +0 -0
  29. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/devices/requests.py +0 -0
  30. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/devices/responses.py +0 -0
  31. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/devices/routes.py +0 -0
  32. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/download/__init__.py +0 -0
  33. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/download/routes.py +0 -0
  34. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/rollouts/__init__.py +0 -0
  35. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/rollouts/requests.py +0 -0
  36. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/rollouts/responses.py +0 -0
  37. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/rollouts/routes.py +0 -0
  38. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/routes.py +0 -0
  39. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/settings/__init__.py +0 -0
  40. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/settings/routes.py +0 -0
  41. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/settings/users/__init__.py +0 -0
  42. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/settings/users/requests.py +0 -0
  43. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/settings/users/responses.py +0 -0
  44. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/settings/users/routes.py +0 -0
  45. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/software/__init__.py +0 -0
  46. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/software/requests.py +0 -0
  47. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/software/responses.py +0 -0
  48. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/api/v1/software/routes.py +0 -0
  49. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/auth/__init__.py +0 -0
  50. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/auth/permissions.py +0 -0
  51. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/db/__init__.py +0 -0
  52. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/db/config.py +0 -0
  53. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/db/migrations/models/0_20240830054046_init.py +0 -0
  54. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/db/migrations/models/1_20241109151811_update.py +0 -0
  55. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/db/migrations/models/2_20241121113728_update.py +0 -0
  56. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/db/migrations/models/3_20241121140210_update.py +0 -0
  57. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/db/migrations/models/4_20250324110331_update.py +0 -0
  58. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/db/migrations/models/4_20250402085235_rename_uuid_to_id.py +0 -0
  59. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/db/migrations/models/5_20250619090242_null_feed.py +0 -0
  60. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/db/pg_ssl_context.py +0 -0
  61. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/device_manager.py +0 -0
  62. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/plugins/__init__.py +0 -0
  63. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/schema/__init__.py +0 -0
  64. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/schema/devices.py +0 -0
  65. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/schema/plugins.py +0 -0
  66. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/schema/rollouts.py +0 -0
  67. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/schema/software.py +0 -0
  68. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/schema/updates.py +0 -0
  69. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/schema/users.py +0 -0
  70. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/settings/__init__.py +0 -0
  71. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/settings/const.py +0 -0
  72. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/settings/schema.py +0 -0
  73. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/storage/__init__.py +0 -0
  74. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/storage/base.py +0 -0
  75. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/storage/filesystem.py +0 -0
  76. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/storage/s3.py +0 -0
  77. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/__init__.py +0 -0
  78. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/__init__.py +0 -0
  79. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/common/__init__.py +0 -0
  80. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/common/columns.py +0 -0
  81. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/common/requests.py +0 -0
  82. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/common/responses.py +0 -0
  83. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/common/util.py +0 -0
  84. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/devices/__init__.py +0 -0
  85. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/devices/device/__init__.py +0 -0
  86. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/devices/device/routes.py +0 -0
  87. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/devices/requests.py +0 -0
  88. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/devices/responses.py +0 -0
  89. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/devices/routes.py +0 -0
  90. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/download/__init__.py +0 -0
  91. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/download/routes.py +0 -0
  92. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/rollouts/__init__.py +0 -0
  93. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/rollouts/responses.py +0 -0
  94. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/rollouts/routes.py +0 -0
  95. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/routes.py +0 -0
  96. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/settings/__init__.py +0 -0
  97. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/settings/routes.py +0 -0
  98. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/settings/users/__init__.py +0 -0
  99. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/settings/users/responses.py +0 -0
  100. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/settings/users/routes.py +0 -0
  101. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/software/__init__.py +0 -0
  102. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/software/responses.py +0 -0
  103. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/bff/software/routes.py +0 -0
  104. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/nav.py +0 -0
  105. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/routes.py +0 -0
  106. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/static/__init__.py +0 -0
  107. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/static/favicon.ico +0 -0
  108. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/static/favicon.svg +0 -0
  109. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/static/js/devices.js +0 -0
  110. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/static/js/login.js +0 -0
  111. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/static/js/logs.js +0 -0
  112. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/static/js/rollouts.js +0 -0
  113. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/static/js/settings.js +0 -0
  114. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/static/js/setup.js +0 -0
  115. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/static/js/software.js +0 -0
  116. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/static/js/util.js +0 -0
  117. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/static/svg/goosebit-logo.svg +0 -0
  118. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/templates/__init__.py +0 -0
  119. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/templates/devices.html.jinja +0 -0
  120. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/templates/login.html.jinja +0 -0
  121. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/templates/logs.html.jinja +0 -0
  122. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/templates/nav.html.jinja +0 -0
  123. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/templates/rollouts.html.jinja +0 -0
  124. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/templates/settings.html.jinja +0 -0
  125. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/ui/templates/setup.html.jinja +0 -0
  126. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/updater/__init__.py +0 -0
  127. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/updater/controller/__init__.py +0 -0
  128. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/updater/controller/routes.py +0 -0
  129. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/updater/controller/v1/__init__.py +0 -0
  130. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/updater/controller/v1/routes.py +0 -0
  131. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/updater/controller/v1/schema.py +0 -0
  132. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/updater/routes.py +0 -0
  133. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/users/__init__.py +0 -0
  134. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/util/__init__.py +0 -0
  135. {goosebit-0.2.8 → goosebit-0.2.9}/goosebit/util/path.py +0 -0
  136. {goosebit-0.2.8 → goosebit-0.2.9}/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.9
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://github.com/rauc/rauc) + [RAUC hawkBit Updater](https://github.com/rauc/rauc-hawkbit-updater) for managing software updates.
102
+ - Devices send certain attributes (`sw_version`, `hw_model`, `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
 
@@ -111,7 +116,7 @@ The registry tracks each device's status, including the last online timestamp, i
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://github.com/rauc/rauc) + [RAUC hawkBit Updater](https://github.com/rauc/rauc-hawkbit-updater) for managing software updates.
67
+ - Devices send certain attributes (`sw_version`, `hw_model`, `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
 
@@ -78,7 +81,7 @@ The registry tracks each device's status, including the last online timestamp, i
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",
@@ -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
@@ -1,18 +1,17 @@
1
- import hashlib
2
1
  import logging
3
- import random
4
- import string
5
2
  from typing import Any
6
3
 
7
- import httpx
8
4
  import libconf
9
- from anyio import AsyncFile, Path, open_file
5
+ from anyio import Path, open_file
10
6
 
11
- from goosebit.storage import storage
12
7
  from goosebit.util.version import Version
13
8
 
9
+ from .func import sha1_hash_file
10
+
14
11
  logger = logging.getLogger(__name__)
15
12
 
13
+ MAGIC = b"0707"
14
+
16
15
 
17
16
  def _append_compatibility(boardname, value, compatibility):
18
17
  if not isinstance(value, dict):
@@ -43,7 +42,7 @@ def parse_descriptor(swdesc: libconf.AttrDict[Any, Any | None]):
43
42
 
44
43
  swdesc_attrs["compatibility"] = compatibility
45
44
  except KeyError as e:
46
- logging.warning(f"Parsing swu descriptor failed, error={e}")
45
+ logger.warning(f"Parsing swu descriptor failed, error={e}")
47
46
  raise ValueError("Parsing swu descriptor failed", e)
48
47
 
49
48
  return swdesc_attrs
@@ -70,37 +69,6 @@ async def parse_file(file: Path):
70
69
  swdesc_attrs = parse_descriptor(swdesc)
71
70
  stat = await file.stat()
72
71
  swdesc_attrs["size"] = stat.st_size
73
- swdesc_attrs["hash"] = await _sha1_hash_file(f)
74
- return swdesc_attrs
72
+ swdesc_attrs["hash"] = await sha1_hash_file(f)
75
73
 
76
-
77
- async def parse_remote(url: str):
78
- async with httpx.AsyncClient() as c:
79
- file = await c.get(url)
80
- temp_dir = Path(storage.get_temp_dir())
81
- tmp_file_path = temp_dir.joinpath("".join(random.choices(string.ascii_lowercase, k=12)) + ".tmp")
82
- try:
83
- async with await open_file(tmp_file_path, "w+b") as f:
84
- await f.write(file.content)
85
- file_data = await parse_file(tmp_file_path) # Use anyio.Path for parse_file
86
- except Exception:
87
- raise
88
- finally:
89
- await tmp_file_path.unlink(missing_ok=True)
90
- return file_data
91
-
92
-
93
- async def _sha1_hash_file(fileobj: AsyncFile):
94
- last = await fileobj.tell()
95
- await fileobj.seek(0)
96
- sha1_hash = hashlib.sha1()
97
- buf = bytearray(2**18)
98
- view = memoryview(buf)
99
- while True:
100
- size = await fileobj.readinto(buf)
101
- if size == 0:
102
- break
103
- sha1_hash.update(view[:size])
104
-
105
- await fileobj.seek(last)
106
- return sha1_hash.hexdigest()
74
+ return swdesc_attrs
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "goosebit"
3
- version = "0.2.8"
3
+ version = "0.2.9"
4
4
  description = "A simplistic, opinionated remote update server implementing hawkBit™'s DDI API"
5
5
  authors = [
6
6
  {name = "Brett Rowan", email = "121075405+b-rowan@users.noreply.github.com"}
@@ -25,7 +25,9 @@ dependencies = [
25
25
  "httpx (>=0.28.1,<0.29.0)",
26
26
  "pydantic-settings[yaml] (>=2.10.1,<3.0.0)",
27
27
  "uvicorn (>=0.35.0,<0.36.0)",
28
- "boto3 (>=1.40.8,<2.0.0)"
28
+ "boto3 (>=1.40.8,<2.0.0)",
29
+ "pysquashfsimage (>=0.9.0,<1.0.0)",
30
+ "zstandard (>=0.24.0,<0.25.0)"
29
31
  ]
30
32
 
31
33
  [project.optional-dependencies]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes