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.
- goosebit/db/migrations/models/6_20250904081506_add_image_format.py +11 -0
- goosebit/db/models.py +16 -0
- goosebit/schema/devices.py +3 -23
- goosebit/schema/rollouts.py +2 -12
- goosebit/ui/bff/common/columns.py +188 -31
- goosebit/ui/bff/devices/routes.py +4 -0
- goosebit/ui/bff/rollouts/routes.py +6 -1
- goosebit/ui/bff/software/responses.py +2 -2
- goosebit/ui/templates/software.html.jinja +1 -1
- goosebit/updates/__init__.py +5 -4
- goosebit/updates/swdesc/__init__.py +45 -0
- goosebit/updates/swdesc/func.py +19 -0
- goosebit/updates/swdesc/rauc.py +49 -0
- goosebit/updates/{swdesc.py → swdesc/swu.py} +8 -40
- {goosebit-0.2.8.dist-info → goosebit-0.2.10.dist-info}/METADATA +10 -5
- {goosebit-0.2.8.dist-info → goosebit-0.2.10.dist-info}/RECORD +19 -15
- {goosebit-0.2.8.dist-info → goosebit-0.2.10.dist-info}/LICENSE +0 -0
- {goosebit-0.2.8.dist-info → goosebit-0.2.10.dist-info}/WHEEL +0 -0
- {goosebit-0.2.8.dist-info → goosebit-0.2.10.dist-info}/entry_points.txt +0 -0
@@ -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",
|
goosebit/schema/devices.py
CHANGED
@@ -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,
|
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
|
38
|
-
hardware: HardwareSchema | None
|
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:
|
goosebit/schema/rollouts.py
CHANGED
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
from datetime import datetime
|
4
4
|
|
5
|
-
from pydantic import BaseModel, ConfigDict,
|
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
|
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(
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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",
|
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",
|
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",
|
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(
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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(
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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(
|
49
|
-
|
50
|
-
|
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
|
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
|
-
|
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)
|
goosebit/updates/__init__.py
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
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
|
-
|
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
|
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.
|
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
|
-
- `
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
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=
|
124
|
-
goosebit/updates/swdesc.py,sha256
|
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.
|
130
|
-
goosebit-0.2.
|
131
|
-
goosebit-0.2.
|
132
|
-
goosebit-0.2.
|
133
|
-
goosebit-0.2.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|