goosebit 0.2.7__py3-none-any.whl → 0.2.9__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.
@@ -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";"""
goosebit/db/models.py CHANGED
@@ -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,7 +1,7 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: goosebit
3
- Version: 0.2.7
4
- Summary:
3
+ Version: 0.2.9
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
7
7
  Requires-Python: >=3.11,<4.0
@@ -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.
@@ -45,7 +45,8 @@ goosebit/db/migrations/models/3_20241121140210_update.py,sha256=VB_zhZmu7_dw4blQ
45
45
  goosebit/db/migrations/models/4_20250324110331_update.py,sha256=GnXlb37X2l2VEHrN6wiNyG0iUd5-KLJmJiOu85rHkDE,425
46
46
  goosebit/db/migrations/models/4_20250402085235_rename_uuid_to_id.py,sha256=HnE7MebCxMVPG9JLcSqRGLGiwkL4PVahd6YU0Hklfp0,297
47
47
  goosebit/db/migrations/models/5_20250619090242_null_feed.py,sha256=jNU0mjUF4veuQZdjiFBNI5K8O2L7hBoqZF1H4znChuQ,2436
48
- goosebit/db/models.py,sha256=Vau-LAo14cghXhb4dFhlyThTwVvt3BxavQfB9BgJNqk,5663
48
+ goosebit/db/migrations/models/6_20250904081506_add_image_format.py,sha256=Ywo7PHr65KwUv5H18_Vce0L73HPW02_b1nEYagaPBXo,340
49
+ goosebit/db/models.py,sha256=BQTa-mMiLIMQ6iTfktsqKgNPtJUnMPFjpdutP6XIxdQ,6025
49
50
  goosebit/db/pg_ssl_context.py,sha256=OyNJBYPLb_yRpA3AaesYr3bro9R3_20fzcAFUUTl00c,2344
50
51
  goosebit/device_manager.py,sha256=UwAslhSk6FEt2cJOMujj6enfmSR-KPrNoAeSCa56Zpg,9284
51
52
  goosebit/plugins/__init__.py,sha256=9VLjGc2F72teF9dxGGkwDXTbL0Q1wbjLTheb4hY1qEg,1185
@@ -112,7 +113,7 @@ goosebit/ui/templates/nav.html.jinja,sha256=qgjxA-hxTr6UG600fju-OiPrrRCQV87-K_22
112
113
  goosebit/ui/templates/rollouts.html.jinja,sha256=cprN5d8lqpGgUdRVC7WrCY9iNlDICXoQoSJAlp85xpE,3944
113
114
  goosebit/ui/templates/settings.html.jinja,sha256=EWGErVNp0Q-0AXb4EhGzaGt_sBPeXpJ3N27M-pFVwf0,4325
114
115
  goosebit/ui/templates/setup.html.jinja,sha256=5h-02XXpBltmQ7sK1xu_yF3SO5VhCeepcY13frRcfK8,3908
115
- goosebit/ui/templates/software.html.jinja,sha256=zFtM-UGq-7jHPgoObD15pl3pwqO5FIAR9kp4Ee-5WmE,7139
116
+ goosebit/ui/templates/software.html.jinja,sha256=fQGZMLbX_g8DU5CUTVqgzKWARieJiVBX_dgA9sv0hzI,7146
116
117
  goosebit/updater/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH4KROTGd2S4AoI,41
117
118
  goosebit/updater/controller/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH4KROTGd2S4AoI,41
118
119
  goosebit/updater/controller/routes.py,sha256=8CnLb-kDuO-yFeWdu4apIyctCf9OzvJ011Az3QDGemU,123
@@ -120,14 +121,17 @@ goosebit/updater/controller/v1/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH
120
121
  goosebit/updater/controller/v1/routes.py,sha256=qFD6NqqyEfk4xZqq5JvCLkfiG3laoRJYgPepC2-NveQ,9542
121
122
  goosebit/updater/controller/v1/schema.py,sha256=NwSyPx3A38iFabfOfzaVtxPozJQNacikP5WOhxMHqdg,1228
122
123
  goosebit/updater/routes.py,sha256=09FVP3vwc5Up926Ll_GzaAQevt3o3ODkSONr7Fupgj4,4271
123
- goosebit/updates/__init__.py,sha256=ErUsH7eoiM0kVrBgKWNkB1E_p3dRL7F4cNBAiWDW9qg,4207
124
- goosebit/updates/swdesc.py,sha256=XDKToV9raYe11q9Qws-AvK_fQqq_c0UM9KgPGBiwhAQ,3407
124
+ goosebit/updates/__init__.py,sha256=9neUBUO0pfPHi5qe0WsV0jg7mwrqAElJfHoLgfPvVuQ,4265
125
+ goosebit/updates/swdesc/__init__.py,sha256=-DXcEcBXt_Os2jxOi0UAUBFb0ViofDowxXdTvCzOho4,1432
126
+ goosebit/updates/swdesc/func.py,sha256=Bhs7PFmFdL338HT8hI7elFH3PuAOJUOnDhNKNjyFpDw,435
127
+ goosebit/updates/swdesc/rauc.py,sha256=bsF2--N4AvSSy4sRRx7X01duUxrV-ePwcz_svmtHpTE,1588
128
+ goosebit/updates/swdesc/swu.py,sha256=_bmxKMOMb8ToBLEA1AAqj__5S8wwLXqmM7v2At7zDOc,2343
125
129
  goosebit/users/__init__.py,sha256=fmoq3LtDFk0nKUaJyvOFunRMSMiUvEgRQYu56BcGlo8,2187
126
130
  goosebit/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
127
131
  goosebit/util/path.py,sha256=Ir6h3C_kfsVDbzt0icHhH3zDhLnEdW7AeKN0BkjeIJY,1584
128
132
  goosebit/util/version.py,sha256=dLBOn8Pb3CeIHhTXYxRfDE3fWGoAl0LPjCTF_xHGFzo,3137
129
- goosebit-0.2.7.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
130
- goosebit-0.2.7.dist-info/METADATA,sha256=ZT_HV_lwYX5eE6RrqKeZt4XwfKmQ856UWU_x1lhtYwg,8208
131
- goosebit-0.2.7.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
132
- goosebit-0.2.7.dist-info/entry_points.txt,sha256=5p3wNB9_WEljksEBgZmOxO0DrBVhRxP20JD9JJ_lpb4,57
133
- goosebit-0.2.7.dist-info/RECORD,,
133
+ goosebit-0.2.9.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
134
+ goosebit-0.2.9.dist-info/METADATA,sha256=Y9jzIsB4sOahUb2HV6sCFuTJeBR3uAtLrlcDGXw6PFk,8754
135
+ goosebit-0.2.9.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
136
+ goosebit-0.2.9.dist-info/entry_points.txt,sha256=5p3wNB9_WEljksEBgZmOxO0DrBVhRxP20JD9JJ_lpb4,57
137
+ goosebit-0.2.9.dist-info/RECORD,,