goosebit 0.2.8__py3-none-any.whl → 0.2.10__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",
@@ -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)
@@ -2,49 +2,206 @@ from .responses import DTColumnDescription
2
2
 
3
3
 
4
4
  class DeviceColumns:
5
- id = DTColumnDescription(title="ID", data="id", name="id", searchable=True, orderable=True)
6
- name = DTColumnDescription(title="Name", data="name", name="name", searchable=True, orderable=True)
7
- hw_model = DTColumnDescription(title="Model", data="hw_model")
8
- hw_revision = DTColumnDescription(title="Revision", data="hw_revision")
9
- feed = DTColumnDescription(title="Feed", data="feed", name="feed", searchable=True, orderable=True)
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
+ )
10
40
  sw_version = DTColumnDescription(
11
- title="Installed Software", data="sw_version", name="sw_version", searchable=True, orderable=True
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,
12
53
  )
13
- sw_target_version = DTColumnDescription(title="Target Software", data="sw_target_version")
14
54
  update_mode = DTColumnDescription(
15
- title="Update Mode", data="update_mode", name="update_mode", searchable=True, orderable=True
55
+ title="Update Mode",
56
+ data="update_mode",
57
+ name="update_mode",
58
+ searchable=True,
59
+ orderable=True,
16
60
  )
17
61
  last_state = DTColumnDescription(
18
- title="State", data="last_state", name="last_state", searchable=True, orderable=True
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,
19
96
  )
20
- force_update = DTColumnDescription(title="Force Update", data="force_update")
21
- progress = DTColumnDescription(title="Progress", data="progress")
22
- last_ip = DTColumnDescription(title="Last IP", data="last_ip")
23
- polling = DTColumnDescription(title="Polling", data="polling")
24
- last_seen = DTColumnDescription(title="Last Seen", data="last_seen")
25
97
 
26
98
 
27
99
  class RolloutColumns:
28
- id = DTColumnDescription(title="ID", data="id", visible=False)
29
- created_at = DTColumnDescription(title="Created", data="created_at", name="created_at", orderable=True)
30
- name = DTColumnDescription(title="Name", data="name", name="name", searchable=True, orderable=True)
31
- feed = DTColumnDescription(title="Feed", data="feed", name="feed", searchable=True, orderable=True)
32
- sw_file = DTColumnDescription(title="Software File", data="sw_file", name="sw_file")
33
- sw_version = DTColumnDescription(title="Software Version", data="sw_version", name="sw_version")
34
- paused = DTColumnDescription(title="Paused", name="paused", data="paused")
35
- success_count = DTColumnDescription(title="Success Count", data="success_count", name="success_count")
36
- failure_count = DTColumnDescription(title="Failure Count", data="failure_count", name="failure_count")
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
+ )
37
157
 
38
158
 
39
159
  class SoftwareColumns:
40
- id = DTColumnDescription(title="ID", data="id", visible=False)
41
- name = DTColumnDescription(title="Name", data="name", name="name")
42
- version = DTColumnDescription(title="Version", data="version", name="version", searchable=True, orderable=True)
43
- compatibility = DTColumnDescription(title="Compatibility", name="compatibility", data="compatibility")
44
- size = DTColumnDescription(title="Size", name="size", data="size")
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
+ )
45
190
 
46
191
 
47
192
  class SettingsUsersColumns:
48
- username = DTColumnDescription(title="Username", data="username", searchable=True, orderable=True)
49
- enabled = DTColumnDescription(title="Enabled", data="enabled")
50
- permissions = DTColumnDescription(title="Permissions", data="permissions")
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
@@ -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
  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.
@@ -45,14 +45,15 @@ 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
52
53
  goosebit/schema/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
- goosebit/schema/devices.py,sha256=4LBYGnceK2IWQFhcIEMYC7bLlHPoTYO0ciKcnUnwSkA,2787
54
+ goosebit/schema/devices.py,sha256=pfhUlsd4SaluBBWrZrHRerEfEw4SVXWP2TqdAIazDag,2007
54
55
  goosebit/schema/plugins.py,sha256=jJFQ-o6QY04YQPz-ovCpnMf-yKlEmoZqq3gJlHUJJ1I,2281
55
- goosebit/schema/rollouts.py,sha256=Kp--XRC39SDs1bKf59q8fBHzitoW88ZMN1GVlbCQotQ,882
56
+ goosebit/schema/rollouts.py,sha256=uRRuLalello11Qe7qzeXlVz-9kEjqMK0UFS650TDQrU,584
56
57
  goosebit/schema/software.py,sha256=W02rA0guQ7zeeVMTdwxt6EjCnTdf-9JewVJRqdN_GK0,951
57
58
  goosebit/schema/updates.py,sha256=aZdNAfXZote6JxgH1Ut0YZMCLlOmtBz3Ia3nXRtxfQA,343
58
59
  goosebit/schema/users.py,sha256=ZKp4eHh4EH0XRZcmpw7exZO-ypcDmzAtJXJ7zVSbans,190
@@ -66,7 +67,7 @@ goosebit/storage/s3.py,sha256=buvLjn10AHRvI48-OfYbppcRVyjq3z2C-PvZvt94XX8,3627
66
67
  goosebit/ui/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH4KROTGd2S4AoI,41
67
68
  goosebit/ui/bff/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH4KROTGd2S4AoI,41
68
69
  goosebit/ui/bff/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
69
- goosebit/ui/bff/common/columns.py,sha256=NiHPjStNz4eZH2RlMvI6fPYZXd5oLTqP8NXG84iOAd4,3026
70
+ goosebit/ui/bff/common/columns.py,sha256=Q7c8FQ_z5bMAfoIlNwmUZAJ-vWQXqS7geG0H7HGf0UE,4972
70
71
  goosebit/ui/bff/common/requests.py,sha256=Db8AohkRfsjUbx3pbdKVva7ZkLhAfBiDwAA221OXd5M,1183
71
72
  goosebit/ui/bff/common/responses.py,sha256=DKEvkpOsFcOkUmYaF1YDqElEOFIqV-uH9yUd5e4e2ak,335
72
73
  goosebit/ui/bff/common/util.py,sha256=_hz89EFLAL1UvbIvJJJSF80PL4T1mz1Vk7354wHBzOE,1144
@@ -75,12 +76,12 @@ goosebit/ui/bff/devices/device/__init__.py,sha256=kzt5TBA-bJmDZFXYPohggbMOW7Ghkl
75
76
  goosebit/ui/bff/devices/device/routes.py,sha256=d3ZwswAPR5J8CvbjmJ8gH9KtXTHfZmSFR5txufLtCEU,495
76
77
  goosebit/ui/bff/devices/requests.py,sha256=DRt8gz8TFpgCsaevDc_s-V2XKfDwHHirXE-LOuiMPZU,320
77
78
  goosebit/ui/bff/devices/responses.py,sha256=4279xeu-soqgyOc4ncKZpai6juBQ3dl096p2LgtIu2s,1208
78
- goosebit/ui/bff/devices/routes.py,sha256=ljvIgN2MGJv_j59YPNoeSIC5w5kBc6hvfDagF0GbLNQ,5093
79
+ goosebit/ui/bff/devices/routes.py,sha256=aFZvu55GdgZHCWrJ0SFY0-LWFKY_2Bord7OrvWieOi0,5327
79
80
  goosebit/ui/bff/download/__init__.py,sha256=kzt5TBA-bJmDZFXYPohggbMOW7GhklyflaRKZNC0yDY,42
80
81
  goosebit/ui/bff/download/routes.py,sha256=zaGZmHBUGRwxKTjDfpr151Yt1fpC803b1rj25PvxE-Y,1126
81
82
  goosebit/ui/bff/rollouts/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH4KROTGd2S4AoI,41
82
83
  goosebit/ui/bff/rollouts/responses.py,sha256=gFEzWkFUBIka98lrA0ivdXBiTqaU5AugfJm2F3p33uk,1180
83
- goosebit/ui/bff/rollouts/routes.py,sha256=kxuV_wW1xIS4UzWAw47ibJ7XNcRK5c90eA2AyVftSNU,2533
84
+ goosebit/ui/bff/rollouts/routes.py,sha256=lLOSK33FKsi5H3rue4thKCuA0oCaB6Pl58YiJbbT6AA,2683
84
85
  goosebit/ui/bff/routes.py,sha256=V-C-WS92PvhT6SJJzezF1t6M7w8e_lWsvZE-g5n7w1Y,477
85
86
  goosebit/ui/bff/settings/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH4KROTGd2S4AoI,41
86
87
  goosebit/ui/bff/settings/routes.py,sha256=5WOlsPg552pb9tSrKlAw3FbEW734hGCfT5zGMxkatCg,594
@@ -88,7 +89,7 @@ goosebit/ui/bff/settings/users/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH
88
89
  goosebit/ui/bff/settings/users/responses.py,sha256=_y-qVOQlZjiY-Ru4ejh_LxoR6u8tkR572CyNbe2wOmw,1173
89
90
  goosebit/ui/bff/settings/users/routes.py,sha256=YlAWOcRmmeeaUGzu4ZNyu1yELbyFQkvY8fJiUD79WAQ,2548
90
91
  goosebit/ui/bff/software/__init__.py,sha256=4RRIzqC6KbCESIC9Vc6G4iAa28IWQH4KROTGd2S4AoI,41
91
- goosebit/ui/bff/software/responses.py,sha256=e7zMP8q2uWgKRy1q5Jp0HTNx-lggljOy2BFQRJglDSo,1920
92
+ goosebit/ui/bff/software/responses.py,sha256=Xc16AqB6W8NQzfSI7DBsOHPMohpva7QOfm9zIht4n7g,1906
92
93
  goosebit/ui/bff/software/routes.py,sha256=rBD7rDFr4QsnOHRhrY_92b2vjoBcTcCciHFMJV090Q0,4123
93
94
  goosebit/ui/nav.py,sha256=4s-SoIIYhJxOeTSsxnkbzErSzIkgwIUO-1ubc4ayemI,561
94
95
  goosebit/ui/routes.py,sha256=DuRh-RAahamL4E6p_NaXW2nDWGGIyLL611KCRhhf6O8,3720
@@ -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.8.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
130
- goosebit-0.2.8.dist-info/METADATA,sha256=lok9b0VFvgV3vjtpYQESdWNO90ole2cm7CMxFbxhtsA,8288
131
- goosebit-0.2.8.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
132
- goosebit-0.2.8.dist-info/entry_points.txt,sha256=5p3wNB9_WEljksEBgZmOxO0DrBVhRxP20JD9JJ_lpb4,57
133
- goosebit-0.2.8.dist-info/RECORD,,
133
+ goosebit-0.2.10.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
134
+ goosebit-0.2.10.dist-info/METADATA,sha256=f0d8bVWfxoCPI5aC_nJ4jAq7MPb1EnMv5cEvimev8dI,8749
135
+ goosebit-0.2.10.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
136
+ goosebit-0.2.10.dist-info/entry_points.txt,sha256=5p3wNB9_WEljksEBgZmOxO0DrBVhRxP20JD9JJ_lpb4,57
137
+ goosebit-0.2.10.dist-info/RECORD,,