goosebit 0.1.2__tar.gz → 0.2.1__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.
- goosebit-0.2.1/PKG-INFO +173 -0
- goosebit-0.2.1/README.md +140 -0
- goosebit-0.2.1/goosebit/__init__.py +96 -0
- goosebit-0.2.1/goosebit/__main__.py +7 -0
- goosebit-0.2.1/goosebit/api/responses.py +5 -0
- goosebit-0.2.1/goosebit/api/routes.py +9 -0
- goosebit-0.1.2/goosebit/telemetry/__init__.py → goosebit-0.2.1/goosebit/api/telemetry/metrics.py +9 -3
- goosebit-0.2.1/goosebit/api/telemetry/prometheus/__init__.py +2 -0
- goosebit-0.2.1/goosebit/api/telemetry/prometheus/readers.py +3 -0
- goosebit-0.2.1/goosebit/api/telemetry/prometheus/routes.py +18 -0
- goosebit-0.2.1/goosebit/api/telemetry/routes.py +9 -0
- goosebit-0.2.1/goosebit/api/v1/devices/__init__.py +1 -0
- goosebit-0.2.1/goosebit/api/v1/devices/device/__init__.py +1 -0
- goosebit-0.2.1/goosebit/api/v1/devices/device/responses.py +13 -0
- goosebit-0.2.1/goosebit/api/v1/devices/device/routes.py +27 -0
- goosebit-0.2.1/goosebit/api/v1/devices/requests.py +7 -0
- goosebit-0.2.1/goosebit/api/v1/devices/responses.py +16 -0
- goosebit-0.2.1/goosebit/api/v1/devices/routes.py +35 -0
- goosebit-0.2.1/goosebit/api/v1/download/__init__.py +1 -0
- goosebit-0.2.1/goosebit/api/v1/download/routes.py +22 -0
- goosebit-0.2.1/goosebit/api/v1/rollouts/requests.py +16 -0
- goosebit-0.2.1/goosebit/api/v1/rollouts/responses.py +19 -0
- goosebit-0.2.1/goosebit/api/v1/rollouts/routes.py +50 -0
- goosebit-0.2.1/goosebit/api/v1/routes.py +9 -0
- goosebit-0.2.1/goosebit/api/v1/software/requests.py +5 -0
- goosebit-0.2.1/goosebit/api/v1/software/responses.py +16 -0
- goosebit-0.2.1/goosebit/api/v1/software/routes.py +77 -0
- goosebit-0.2.1/goosebit/auth/__init__.py +139 -0
- goosebit-0.2.1/goosebit/db/__init__.py +11 -0
- goosebit-0.2.1/goosebit/db/config.py +10 -0
- goosebit-0.2.1/goosebit/db/migrations/models/0_20240830054046_init.py +136 -0
- {goosebit-0.1.2/goosebit → goosebit-0.2.1/goosebit/db}/models.py +17 -10
- {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/realtime/logs.py +4 -3
- {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/realtime/routes.py +2 -2
- goosebit-0.2.1/goosebit/schema/__init__.py +0 -0
- goosebit-0.2.1/goosebit/schema/devices.py +73 -0
- goosebit-0.2.1/goosebit/schema/rollouts.py +31 -0
- goosebit-0.2.1/goosebit/schema/software.py +37 -0
- goosebit-0.2.1/goosebit/settings/__init__.py +17 -0
- goosebit-0.2.1/goosebit/settings/const.py +21 -0
- goosebit-0.2.1/goosebit/settings/schema.py +86 -0
- goosebit-0.2.1/goosebit/ui/__init__.py +1 -0
- goosebit-0.2.1/goosebit/ui/bff/__init__.py +1 -0
- goosebit-0.2.1/goosebit/ui/bff/devices/__init__.py +1 -0
- goosebit-0.2.1/goosebit/ui/bff/devices/requests.py +12 -0
- goosebit-0.2.1/goosebit/ui/bff/devices/responses.py +39 -0
- goosebit-0.2.1/goosebit/ui/bff/devices/routes.py +72 -0
- goosebit-0.2.1/goosebit/ui/bff/download/__init__.py +1 -0
- goosebit-0.2.1/goosebit/ui/bff/download/routes.py +22 -0
- goosebit-0.2.1/goosebit/ui/bff/rollouts/__init__.py +1 -0
- goosebit-0.2.1/goosebit/ui/bff/rollouts/responses.py +37 -0
- goosebit-0.2.1/goosebit/ui/bff/rollouts/routes.py +52 -0
- goosebit-0.2.1/goosebit/ui/bff/routes.py +11 -0
- goosebit-0.2.1/goosebit/ui/bff/software/__init__.py +1 -0
- goosebit-0.2.1/goosebit/ui/bff/software/responses.py +37 -0
- goosebit-0.2.1/goosebit/ui/bff/software/routes.py +83 -0
- goosebit-0.2.1/goosebit/ui/nav.py +16 -0
- goosebit-0.2.1/goosebit/ui/routes.py +64 -0
- goosebit-0.2.1/goosebit/ui/static/favicon.ico +0 -0
- goosebit-0.2.1/goosebit/ui/static/favicon.svg +1 -0
- {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/ui/static/js/devices.js +47 -71
- {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/ui/static/js/index.js +4 -9
- goosebit-0.2.1/goosebit/ui/static/js/login.js +23 -0
- {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/ui/static/js/logs.js +1 -1
- {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/ui/static/js/rollouts.js +33 -19
- goosebit-0.1.2/goosebit/ui/static/js/firmware.js → goosebit-0.2.1/goosebit/ui/static/js/software.js +87 -86
- goosebit-0.2.1/goosebit/ui/static/js/util.js +120 -0
- goosebit-0.2.1/goosebit/ui/static/svg/goosebit-logo.svg +1 -0
- goosebit-0.2.1/goosebit/ui/templates/__init__.py +13 -0
- goosebit-0.2.1/goosebit/ui/templates/devices.html.jinja +75 -0
- goosebit-0.2.1/goosebit/ui/templates/index.html.jinja +25 -0
- goosebit-0.2.1/goosebit/ui/templates/login.html.jinja +57 -0
- goosebit-0.2.1/goosebit/ui/templates/logs.html.jinja +31 -0
- goosebit-0.2.1/goosebit/ui/templates/nav.html.jinja +84 -0
- goosebit-0.2.1/goosebit/ui/templates/rollouts.html.jinja +93 -0
- goosebit-0.2.1/goosebit/ui/templates/software.html.jinja +139 -0
- goosebit-0.2.1/goosebit/updater/__init__.py +1 -0
- goosebit-0.2.1/goosebit/updater/controller/__init__.py +1 -0
- goosebit-0.2.1/goosebit/updater/controller/v1/__init__.py +1 -0
- goosebit-0.2.1/goosebit/updater/controller/v1/routes.py +185 -0
- goosebit-0.2.1/goosebit/updater/controller/v1/schema.py +56 -0
- {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/updater/manager.py +65 -65
- {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/updater/routes.py +3 -11
- goosebit-0.2.1/goosebit/updates/__init__.py +129 -0
- {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/updates/swdesc.py +2 -7
- {goosebit-0.1.2 → goosebit-0.2.1}/pyproject.toml +17 -6
- goosebit-0.1.2/PKG-INFO +0 -123
- goosebit-0.1.2/README.md +0 -94
- goosebit-0.1.2/goosebit/__init__.py +0 -65
- goosebit-0.1.2/goosebit/api/devices.py +0 -136
- goosebit-0.1.2/goosebit/api/download.py +0 -34
- goosebit-0.1.2/goosebit/api/firmware.py +0 -57
- goosebit-0.1.2/goosebit/api/helper.py +0 -30
- goosebit-0.1.2/goosebit/api/rollouts.py +0 -87
- goosebit-0.1.2/goosebit/api/routes.py +0 -19
- goosebit-0.1.2/goosebit/auth/__init__.py +0 -139
- goosebit-0.1.2/goosebit/db.py +0 -37
- goosebit-0.1.2/goosebit/permissions.py +0 -75
- goosebit-0.1.2/goosebit/settings.py +0 -64
- goosebit-0.1.2/goosebit/telemetry/prometheus.py +0 -10
- goosebit-0.1.2/goosebit/ui/routes.py +0 -101
- goosebit-0.1.2/goosebit/ui/static/favicon.ico +0 -0
- goosebit-0.1.2/goosebit/ui/static/favicon.svg +0 -1
- goosebit-0.1.2/goosebit/ui/static/js/util.js +0 -66
- goosebit-0.1.2/goosebit/ui/static/svg/goosebit-logo.svg +0 -1
- goosebit-0.1.2/goosebit/ui/templates/__init__.py +0 -5
- goosebit-0.1.2/goosebit/ui/templates/devices.html +0 -115
- goosebit-0.1.2/goosebit/ui/templates/firmware.html +0 -163
- goosebit-0.1.2/goosebit/ui/templates/index.html +0 -23
- goosebit-0.1.2/goosebit/ui/templates/login.html +0 -65
- goosebit-0.1.2/goosebit/ui/templates/logs.html +0 -36
- goosebit-0.1.2/goosebit/ui/templates/nav.html +0 -117
- goosebit-0.1.2/goosebit/ui/templates/rollouts.html +0 -76
- goosebit-0.1.2/goosebit/updater/controller/v1/routes.py +0 -180
- goosebit-0.1.2/goosebit/updates/__init__.py +0 -70
- {goosebit-0.1.2 → goosebit-0.2.1}/LICENSE +0 -0
- {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/api/__init__.py +0 -0
- {goosebit-0.1.2/goosebit/realtime → goosebit-0.2.1/goosebit/api/telemetry}/__init__.py +0 -0
- {goosebit-0.1.2/goosebit/ui → goosebit-0.2.1/goosebit/api/v1}/__init__.py +0 -0
- {goosebit-0.1.2/goosebit/updater → goosebit-0.2.1/goosebit/api/v1/rollouts}/__init__.py +0 -0
- {goosebit-0.1.2/goosebit/updater/controller → goosebit-0.2.1/goosebit/api/v1/software}/__init__.py +0 -0
- {goosebit-0.1.2/goosebit/updater/controller/v1 → goosebit-0.2.1/goosebit/realtime}/__init__.py +0 -0
- {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/ui/static/__init__.py +0 -0
- {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/updater/controller/routes.py +0 -0
goosebit-0.2.1/PKG-INFO
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: goosebit
|
3
|
+
Version: 0.2.1
|
4
|
+
Summary:
|
5
|
+
Author: Upstream Data
|
6
|
+
Author-email: brett@upstreamdata.ca
|
7
|
+
Requires-Python: >=3.11,<4.0
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
11
|
+
Provides-Extra: postgresql
|
12
|
+
Requires-Dist: aerich (>=0.7.2,<0.8.0)
|
13
|
+
Requires-Dist: aiocache (>=0.12.2,<0.13.0)
|
14
|
+
Requires-Dist: aiofiles (>=24.1.0,<25.0.0)
|
15
|
+
Requires-Dist: argon2-cffi (>=23.1.0,<24.0.0)
|
16
|
+
Requires-Dist: asyncpg (>=0.29.0,<0.30.0) ; extra == "postgresql"
|
17
|
+
Requires-Dist: fastapi[uvicorn] (>=0.111.0,<0.112.0)
|
18
|
+
Requires-Dist: httpx (>=0.27.0,<0.28.0)
|
19
|
+
Requires-Dist: itsdangerous (>=2.2.0,<3.0.0)
|
20
|
+
Requires-Dist: jinja2 (>=3.1.4,<4.0.0)
|
21
|
+
Requires-Dist: joserfc (>=1.0.0,<2.0.0)
|
22
|
+
Requires-Dist: libconf (>=2.0.1,<3.0.0)
|
23
|
+
Requires-Dist: opentelemetry-distro (>=0.47b0,<0.48)
|
24
|
+
Requires-Dist: opentelemetry-exporter-prometheus (>=0.47b0,<0.48)
|
25
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi (>=0.47b0,<0.48)
|
26
|
+
Requires-Dist: pydantic-settings (>=2.4.0,<3.0.0)
|
27
|
+
Requires-Dist: python-multipart (>=0.0.9,<0.0.10)
|
28
|
+
Requires-Dist: semver (>=3.0.2,<4.0.0)
|
29
|
+
Requires-Dist: tortoise-orm (>=0.21.4,<0.22.0)
|
30
|
+
Requires-Dist: websockets (>=12.0,<13.0)
|
31
|
+
Description-Content-Type: text/markdown
|
32
|
+
|
33
|
+
# gooseBit
|
34
|
+
|
35
|
+
<img src="docs/img/goosebit-logo.png" style="width: 100px; height: 100px; display: block;">
|
36
|
+
|
37
|
+
---
|
38
|
+
|
39
|
+
A simplistic, opinionated remote update server implementing hawkBit™'s [DDI API](https://eclipse.dev/hawkbit/apis/ddi_api/).
|
40
|
+
|
41
|
+
## Quick Start
|
42
|
+
|
43
|
+
### Installation
|
44
|
+
|
45
|
+
1. Install dependencies using [Poetry](https://python-poetry.org/):
|
46
|
+
```bash
|
47
|
+
poetry install
|
48
|
+
```
|
49
|
+
2. Launch gooseBit:
|
50
|
+
```bash
|
51
|
+
python main.py
|
52
|
+
```
|
53
|
+
|
54
|
+
### Initial Configuration
|
55
|
+
|
56
|
+
Before running gooseBit for the first time, update the default credentials in `settings.yaml`. The default login for testing purposes is:
|
57
|
+
|
58
|
+
- **Username:** `admin@goosebit.local`
|
59
|
+
- **Password:** `admin`
|
60
|
+
|
61
|
+
## Assumptions
|
62
|
+
|
63
|
+
- Devices use [SWUpdate](https://swupdate.org) for managing software updates.
|
64
|
+
|
65
|
+
## Features
|
66
|
+
|
67
|
+
### Device Registry
|
68
|
+
|
69
|
+
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:
|
70
|
+
|
71
|
+
- `hw_model` and `hw_revision`: Used to match compatible software.
|
72
|
+
- `sw_version`: Indicates the currently installed software version.
|
73
|
+
|
74
|
+
The registry tracks each device's status, including the last online timestamp, installed software version, update state, and more.
|
75
|
+
|
76
|
+
### Software Repository
|
77
|
+
|
78
|
+
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.
|
79
|
+
|
80
|
+
### Device Update Modes
|
81
|
+
|
82
|
+
Devices can be configured with different update modes. The default mode is `Rollout`.
|
83
|
+
|
84
|
+
#### 1. Manual Update to Specified Software
|
85
|
+
|
86
|
+
Assign specific software to a device manually. Once installed, no further updates will be triggered.
|
87
|
+
|
88
|
+
#### 2. Automatic Update to Latest Software
|
89
|
+
|
90
|
+
Automatically updates the device to the latest compatible software, based on the reported `hw_model` and `hw_revision`. Note: versions are interpreted as [SemVer](https://semver.org) versions.
|
91
|
+
|
92
|
+
#### 3. Software Rollout
|
93
|
+
|
94
|
+
Rollouts target all devices with a specified "feed" value, ensuring that the assigned software is installed on all matching devices. Rollouts also track success and error rates, with future plans for automatic aborts. If multiple rollouts exist for the same feed, the most recent rollout takes precedence.
|
95
|
+
|
96
|
+
### Pause Updates
|
97
|
+
|
98
|
+
Devices can be pinned to their current software version, preventing any updates from being applied.
|
99
|
+
|
100
|
+
### Real-time Update Logs
|
101
|
+
|
102
|
+
While updates are in progress, gooseBit captures real-time logs, which are accessible through the device repository.
|
103
|
+
|
104
|
+
## Development
|
105
|
+
|
106
|
+
### Database
|
107
|
+
|
108
|
+
Create or upgrade database
|
109
|
+
|
110
|
+
```bash
|
111
|
+
poetry run aerich upgrade
|
112
|
+
```
|
113
|
+
|
114
|
+
After a model change create the migration
|
115
|
+
|
116
|
+
```bash
|
117
|
+
poetry run aerich migrate
|
118
|
+
```
|
119
|
+
|
120
|
+
### Code formatting and linting
|
121
|
+
|
122
|
+
Code is formatted using different tools
|
123
|
+
|
124
|
+
- black and isort for `*.py`
|
125
|
+
- biomejs for `*.js`, `*.json`
|
126
|
+
- prettier for `*.html`, `*.md`, `*.yml`, `*.yaml`
|
127
|
+
|
128
|
+
Code is linted using different tools as well
|
129
|
+
|
130
|
+
- flake8 for `*.py`
|
131
|
+
- biomejs for `*.js`
|
132
|
+
|
133
|
+
Best to have pre-commit install git hooks that run all those tools before a commit:
|
134
|
+
|
135
|
+
```bash
|
136
|
+
poetry run pre-commit install
|
137
|
+
```
|
138
|
+
|
139
|
+
To manually apply the hooks to all files use:
|
140
|
+
|
141
|
+
```bash
|
142
|
+
pre-commit run --all-files
|
143
|
+
```
|
144
|
+
|
145
|
+
### Testing
|
146
|
+
|
147
|
+
Tests are implemented using pytest. To run all tests
|
148
|
+
|
149
|
+
```bash
|
150
|
+
poetry run pytest
|
151
|
+
```
|
152
|
+
|
153
|
+
### Structure
|
154
|
+
|
155
|
+
The structure of gooseBit is as follows:
|
156
|
+
|
157
|
+
- `api`: Files for the API.
|
158
|
+
- `ui`: Files for the UI.
|
159
|
+
- `bff`: Backend for frontend API.
|
160
|
+
- `static`: Static files.
|
161
|
+
- `templates`: Jinja2 formatted templates.
|
162
|
+
- `nav`: Navbar handler.
|
163
|
+
- `updater`: DDI API handler and device update manager.
|
164
|
+
- `updates`: SWUpdate file parsing.
|
165
|
+
- `realtime`: Realtime API functionality with websockets.
|
166
|
+
- `auth`: Authentication functions and permission handling.
|
167
|
+
- `models`: Database models.
|
168
|
+
- `db`: Database config and initialization.
|
169
|
+
- `schema`: Pydantic models used for API type hinting.
|
170
|
+
- `settings`: Settings loader and handler.
|
171
|
+
- `telemetry`: Telemetry data handlers.
|
172
|
+
- `routes`: Routes for a giving endpoint, including the router.
|
173
|
+
|
goosebit-0.2.1/README.md
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
# gooseBit
|
2
|
+
|
3
|
+
<img src="docs/img/goosebit-logo.png" style="width: 100px; height: 100px; display: block;">
|
4
|
+
|
5
|
+
---
|
6
|
+
|
7
|
+
A simplistic, opinionated remote update server implementing hawkBit™'s [DDI API](https://eclipse.dev/hawkbit/apis/ddi_api/).
|
8
|
+
|
9
|
+
## Quick Start
|
10
|
+
|
11
|
+
### Installation
|
12
|
+
|
13
|
+
1. Install dependencies using [Poetry](https://python-poetry.org/):
|
14
|
+
```bash
|
15
|
+
poetry install
|
16
|
+
```
|
17
|
+
2. Launch gooseBit:
|
18
|
+
```bash
|
19
|
+
python main.py
|
20
|
+
```
|
21
|
+
|
22
|
+
### Initial Configuration
|
23
|
+
|
24
|
+
Before running gooseBit for the first time, update the default credentials in `settings.yaml`. The default login for testing purposes is:
|
25
|
+
|
26
|
+
- **Username:** `admin@goosebit.local`
|
27
|
+
- **Password:** `admin`
|
28
|
+
|
29
|
+
## Assumptions
|
30
|
+
|
31
|
+
- Devices use [SWUpdate](https://swupdate.org) for managing software updates.
|
32
|
+
|
33
|
+
## Features
|
34
|
+
|
35
|
+
### Device Registry
|
36
|
+
|
37
|
+
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:
|
38
|
+
|
39
|
+
- `hw_model` and `hw_revision`: Used to match compatible software.
|
40
|
+
- `sw_version`: Indicates the currently installed software version.
|
41
|
+
|
42
|
+
The registry tracks each device's status, including the last online timestamp, installed software version, update state, and more.
|
43
|
+
|
44
|
+
### Software Repository
|
45
|
+
|
46
|
+
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.
|
47
|
+
|
48
|
+
### Device Update Modes
|
49
|
+
|
50
|
+
Devices can be configured with different update modes. The default mode is `Rollout`.
|
51
|
+
|
52
|
+
#### 1. Manual Update to Specified Software
|
53
|
+
|
54
|
+
Assign specific software to a device manually. Once installed, no further updates will be triggered.
|
55
|
+
|
56
|
+
#### 2. Automatic Update to Latest Software
|
57
|
+
|
58
|
+
Automatically updates the device to the latest compatible software, based on the reported `hw_model` and `hw_revision`. Note: versions are interpreted as [SemVer](https://semver.org) versions.
|
59
|
+
|
60
|
+
#### 3. Software Rollout
|
61
|
+
|
62
|
+
Rollouts target all devices with a specified "feed" value, ensuring that the assigned software is installed on all matching devices. Rollouts also track success and error rates, with future plans for automatic aborts. If multiple rollouts exist for the same feed, the most recent rollout takes precedence.
|
63
|
+
|
64
|
+
### Pause Updates
|
65
|
+
|
66
|
+
Devices can be pinned to their current software version, preventing any updates from being applied.
|
67
|
+
|
68
|
+
### Real-time Update Logs
|
69
|
+
|
70
|
+
While updates are in progress, gooseBit captures real-time logs, which are accessible through the device repository.
|
71
|
+
|
72
|
+
## Development
|
73
|
+
|
74
|
+
### Database
|
75
|
+
|
76
|
+
Create or upgrade database
|
77
|
+
|
78
|
+
```bash
|
79
|
+
poetry run aerich upgrade
|
80
|
+
```
|
81
|
+
|
82
|
+
After a model change create the migration
|
83
|
+
|
84
|
+
```bash
|
85
|
+
poetry run aerich migrate
|
86
|
+
```
|
87
|
+
|
88
|
+
### Code formatting and linting
|
89
|
+
|
90
|
+
Code is formatted using different tools
|
91
|
+
|
92
|
+
- black and isort for `*.py`
|
93
|
+
- biomejs for `*.js`, `*.json`
|
94
|
+
- prettier for `*.html`, `*.md`, `*.yml`, `*.yaml`
|
95
|
+
|
96
|
+
Code is linted using different tools as well
|
97
|
+
|
98
|
+
- flake8 for `*.py`
|
99
|
+
- biomejs for `*.js`
|
100
|
+
|
101
|
+
Best to have pre-commit install git hooks that run all those tools before a commit:
|
102
|
+
|
103
|
+
```bash
|
104
|
+
poetry run pre-commit install
|
105
|
+
```
|
106
|
+
|
107
|
+
To manually apply the hooks to all files use:
|
108
|
+
|
109
|
+
```bash
|
110
|
+
pre-commit run --all-files
|
111
|
+
```
|
112
|
+
|
113
|
+
### Testing
|
114
|
+
|
115
|
+
Tests are implemented using pytest. To run all tests
|
116
|
+
|
117
|
+
```bash
|
118
|
+
poetry run pytest
|
119
|
+
```
|
120
|
+
|
121
|
+
### Structure
|
122
|
+
|
123
|
+
The structure of gooseBit is as follows:
|
124
|
+
|
125
|
+
- `api`: Files for the API.
|
126
|
+
- `ui`: Files for the UI.
|
127
|
+
- `bff`: Backend for frontend API.
|
128
|
+
- `static`: Static files.
|
129
|
+
- `templates`: Jinja2 formatted templates.
|
130
|
+
- `nav`: Navbar handler.
|
131
|
+
- `updater`: DDI API handler and device update manager.
|
132
|
+
- `updates`: SWUpdate file parsing.
|
133
|
+
- `realtime`: Realtime API functionality with websockets.
|
134
|
+
- `auth`: Authentication functions and permission handling.
|
135
|
+
- `models`: Database models.
|
136
|
+
- `db`: Database config and initialization.
|
137
|
+
- `schema`: Pydantic models used for API type hinting.
|
138
|
+
- `settings`: Settings loader and handler.
|
139
|
+
- `telemetry`: Telemetry data handlers.
|
140
|
+
- `routes`: Routes for a giving endpoint, including the router.
|
@@ -0,0 +1,96 @@
|
|
1
|
+
import importlib.metadata
|
2
|
+
from contextlib import asynccontextmanager
|
3
|
+
from typing import Annotated
|
4
|
+
|
5
|
+
from fastapi import Depends, FastAPI
|
6
|
+
from fastapi.openapi.docs import get_swagger_ui_html
|
7
|
+
from fastapi.requests import Request
|
8
|
+
from fastapi.responses import RedirectResponse
|
9
|
+
from fastapi.security import OAuth2PasswordRequestForm
|
10
|
+
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor as Instrumentor
|
11
|
+
|
12
|
+
from goosebit import api, db, realtime, ui, updater
|
13
|
+
from goosebit.api.telemetry import metrics
|
14
|
+
from goosebit.auth import get_user_from_request, login_user, redirect_if_authenticated
|
15
|
+
from goosebit.ui.nav import nav
|
16
|
+
from goosebit.ui.static import static
|
17
|
+
from goosebit.ui.templates import templates
|
18
|
+
|
19
|
+
|
20
|
+
@asynccontextmanager
|
21
|
+
async def lifespan(_: FastAPI):
|
22
|
+
await db.init()
|
23
|
+
await metrics.init()
|
24
|
+
yield
|
25
|
+
await db.close()
|
26
|
+
|
27
|
+
|
28
|
+
app = FastAPI(
|
29
|
+
title="gooseBit",
|
30
|
+
summary="A simplistic, opinionated remote update server implementing hawkBit™'s DDI API.",
|
31
|
+
version=importlib.metadata.version("goosebit"),
|
32
|
+
lifespan=lifespan,
|
33
|
+
license_info={
|
34
|
+
"name": "Apache 2.0",
|
35
|
+
"identifier": "Apache-2.0",
|
36
|
+
},
|
37
|
+
redoc_url=None,
|
38
|
+
docs_url=None,
|
39
|
+
openapi_tags=[
|
40
|
+
{
|
41
|
+
"name": "login",
|
42
|
+
"description": "API authentication. "
|
43
|
+
"Can be used in the `authorization` header, in the format `{token_type} {access_token}`.",
|
44
|
+
}
|
45
|
+
],
|
46
|
+
)
|
47
|
+
app.include_router(updater.router)
|
48
|
+
app.include_router(ui.router)
|
49
|
+
app.include_router(api.router)
|
50
|
+
app.include_router(realtime.router)
|
51
|
+
app.mount("/static", static, name="static")
|
52
|
+
Instrumentor.instrument_app(app)
|
53
|
+
|
54
|
+
|
55
|
+
@app.middleware("http")
|
56
|
+
async def attach_user(request: Request, call_next):
|
57
|
+
request.scope["user"] = await get_user_from_request(request)
|
58
|
+
return await call_next(request)
|
59
|
+
|
60
|
+
|
61
|
+
@app.middleware("http")
|
62
|
+
async def attach_nav(request: Request, call_next):
|
63
|
+
request.scope["nav"] = nav.get()
|
64
|
+
return await call_next(request)
|
65
|
+
|
66
|
+
|
67
|
+
@app.get("/", include_in_schema=False)
|
68
|
+
def root_redirect(request: Request):
|
69
|
+
return RedirectResponse(request.url_for("ui_root"))
|
70
|
+
|
71
|
+
|
72
|
+
@app.get("/login", include_in_schema=False, dependencies=[Depends(redirect_if_authenticated)])
|
73
|
+
async def login_get(request: Request):
|
74
|
+
return templates.TemplateResponse(request, "login.html.jinja")
|
75
|
+
|
76
|
+
|
77
|
+
@app.post("/login", tags=["login"])
|
78
|
+
async def login_post(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
|
79
|
+
return {"access_token": login_user(form_data.username, form_data.password), "token_type": "bearer"}
|
80
|
+
|
81
|
+
|
82
|
+
@app.get("/logout", include_in_schema=False)
|
83
|
+
async def logout(request: Request):
|
84
|
+
resp = RedirectResponse(request.url_for("login_get"), status_code=302)
|
85
|
+
resp.delete_cookie(key="session_id")
|
86
|
+
return resp
|
87
|
+
|
88
|
+
|
89
|
+
@app.get("/docs")
|
90
|
+
async def swagger_docs(request: Request):
|
91
|
+
return get_swagger_ui_html(
|
92
|
+
title="gooseBit docs",
|
93
|
+
openapi_url="/openapi.json",
|
94
|
+
swagger_favicon_url=str(request.url_for("static", path="/favicon.svg")),
|
95
|
+
swagger_ui_parameters={"operationsSorter": "alpha"},
|
96
|
+
)
|
@@ -0,0 +1,9 @@
|
|
1
|
+
from fastapi import APIRouter, Depends
|
2
|
+
|
3
|
+
from goosebit.auth import validate_current_user
|
4
|
+
|
5
|
+
from . import telemetry, v1
|
6
|
+
|
7
|
+
router = APIRouter(prefix="/api", dependencies=[Depends(validate_current_user)])
|
8
|
+
router.include_router(telemetry.router)
|
9
|
+
router.include_router(v1.router)
|
goosebit-0.1.2/goosebit/telemetry/__init__.py → goosebit-0.2.1/goosebit/api/telemetry/metrics.py
RENAMED
@@ -2,13 +2,19 @@ from opentelemetry import metrics
|
|
2
2
|
from opentelemetry.sdk.metrics import MeterProvider
|
3
3
|
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
|
4
4
|
|
5
|
-
from goosebit import
|
5
|
+
from goosebit.settings import USERS, config
|
6
6
|
|
7
7
|
from . import prometheus
|
8
8
|
|
9
|
+
readers = []
|
10
|
+
|
11
|
+
if config.metrics.prometheus.enable:
|
12
|
+
readers.append(prometheus.reader)
|
13
|
+
|
14
|
+
|
9
15
|
resource = Resource(attributes={SERVICE_NAME: "goosebit"})
|
10
16
|
|
11
|
-
provider = MeterProvider(resource=resource, metric_readers=
|
17
|
+
provider = MeterProvider(resource=resource, metric_readers=readers)
|
12
18
|
metrics.set_meter_provider(provider)
|
13
19
|
|
14
20
|
meter = metrics.get_meter("goosebit.meter")
|
@@ -25,4 +31,4 @@ users_count = meter.create_gauge(
|
|
25
31
|
|
26
32
|
|
27
33
|
async def init():
|
28
|
-
users_count.set(len(
|
34
|
+
users_count.set(len(USERS))
|
@@ -0,0 +1,18 @@
|
|
1
|
+
from fastapi import APIRouter, Header
|
2
|
+
from fastapi.requests import Request
|
3
|
+
from fastapi.responses import Response
|
4
|
+
from prometheus_client import REGISTRY
|
5
|
+
from prometheus_client.exposition import _bake_output
|
6
|
+
|
7
|
+
router = APIRouter(prefix="/prometheus", tags=["prometheus"])
|
8
|
+
|
9
|
+
|
10
|
+
@router.get("/metrics")
|
11
|
+
async def metrics(
|
12
|
+
request: Request, accept: list[str] = Header(None), accept_encoding: list[str] = Header(None)
|
13
|
+
) -> Response:
|
14
|
+
status, http_headers, output = _bake_output(
|
15
|
+
REGISTRY, ",".join(accept), ",".join(accept_encoding), request.query_params, False
|
16
|
+
)
|
17
|
+
headers = {h[0]: h[1] for h in http_headers}
|
18
|
+
return Response(content=output, headers=headers)
|
@@ -0,0 +1 @@
|
|
1
|
+
from .routes import router # noqa : F401
|
@@ -0,0 +1 @@
|
|
1
|
+
from .routes import router # noqa : F401
|
@@ -0,0 +1,27 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from fastapi import APIRouter, Depends, Security
|
4
|
+
from fastapi.requests import Request
|
5
|
+
|
6
|
+
from goosebit.api.v1.devices.device.responses import DeviceLogResponse, DeviceResponse
|
7
|
+
from goosebit.auth import validate_user_permissions
|
8
|
+
from goosebit.updater.manager import UpdateManager, get_update_manager
|
9
|
+
|
10
|
+
router = APIRouter(prefix="/{dev_id}")
|
11
|
+
|
12
|
+
|
13
|
+
@router.get(
|
14
|
+
"",
|
15
|
+
dependencies=[Security(validate_user_permissions, scopes=["home.read"])],
|
16
|
+
)
|
17
|
+
async def device_get(_: Request, updater: UpdateManager = Depends(get_update_manager)) -> DeviceResponse:
|
18
|
+
return await DeviceResponse.convert(await updater.get_device())
|
19
|
+
|
20
|
+
|
21
|
+
@router.get(
|
22
|
+
"/log",
|
23
|
+
dependencies=[Security(validate_user_permissions, scopes=["home.read"])],
|
24
|
+
)
|
25
|
+
async def device_logs(_: Request, updater: UpdateManager = Depends(get_update_manager)) -> DeviceLogResponse:
|
26
|
+
device = await updater.get_device()
|
27
|
+
return DeviceLogResponse(log=device.last_log)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
|
5
|
+
from pydantic import BaseModel
|
6
|
+
|
7
|
+
from goosebit.db.models import Device
|
8
|
+
from goosebit.schema.devices import DeviceSchema
|
9
|
+
|
10
|
+
|
11
|
+
class DevicesResponse(BaseModel):
|
12
|
+
devices: list[DeviceSchema]
|
13
|
+
|
14
|
+
@classmethod
|
15
|
+
async def convert(cls, devices: list[Device]):
|
16
|
+
return cls(devices=await asyncio.gather(*[DeviceSchema.convert(d) for d in devices]))
|
@@ -0,0 +1,35 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from fastapi import APIRouter, Security
|
4
|
+
from fastapi.requests import Request
|
5
|
+
|
6
|
+
from goosebit.api.responses import StatusResponse
|
7
|
+
from goosebit.auth import validate_user_permissions
|
8
|
+
from goosebit.db.models import Device
|
9
|
+
from goosebit.updater.manager import delete_devices
|
10
|
+
|
11
|
+
from . import device
|
12
|
+
from .requests import DevicesDeleteRequest
|
13
|
+
from .responses import DevicesResponse
|
14
|
+
|
15
|
+
router = APIRouter(prefix="/devices", tags=["devices"])
|
16
|
+
|
17
|
+
|
18
|
+
@router.get(
|
19
|
+
"",
|
20
|
+
dependencies=[Security(validate_user_permissions, scopes=["home.read"])],
|
21
|
+
)
|
22
|
+
async def devices_get(_: Request) -> DevicesResponse:
|
23
|
+
return await DevicesResponse.convert(await Device.all().prefetch_related("assigned_software", "hardware"))
|
24
|
+
|
25
|
+
|
26
|
+
@router.delete(
|
27
|
+
"",
|
28
|
+
dependencies=[Security(validate_user_permissions, scopes=["device.delete"])],
|
29
|
+
)
|
30
|
+
async def devices_delete(_: Request, config: DevicesDeleteRequest) -> StatusResponse:
|
31
|
+
await delete_devices(config.devices)
|
32
|
+
return StatusResponse(success=True)
|
33
|
+
|
34
|
+
|
35
|
+
router.include_router(device.router)
|
@@ -0,0 +1 @@
|
|
1
|
+
from .routes import router # noqa : F401
|
@@ -0,0 +1,22 @@
|
|
1
|
+
from fastapi import APIRouter, HTTPException
|
2
|
+
from fastapi.requests import Request
|
3
|
+
from fastapi.responses import FileResponse, RedirectResponse
|
4
|
+
|
5
|
+
from goosebit.db.models import Software
|
6
|
+
|
7
|
+
router = APIRouter(prefix="/download", tags=["download"])
|
8
|
+
|
9
|
+
|
10
|
+
@router.get("/{file_id}")
|
11
|
+
async def download_file(_: Request, file_id: int):
|
12
|
+
software = await Software.get_or_none(id=file_id)
|
13
|
+
if software is None:
|
14
|
+
raise HTTPException(404)
|
15
|
+
if software.local:
|
16
|
+
return FileResponse(
|
17
|
+
software.path,
|
18
|
+
media_type="application/octet-stream",
|
19
|
+
filename=software.path.name,
|
20
|
+
)
|
21
|
+
else:
|
22
|
+
return RedirectResponse(url=software.uri)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
from pydantic import BaseModel
|
2
|
+
|
3
|
+
|
4
|
+
class RolloutsPutRequest(BaseModel):
|
5
|
+
name: str
|
6
|
+
feed: str
|
7
|
+
software_id: int
|
8
|
+
|
9
|
+
|
10
|
+
class RolloutsPatchRequest(BaseModel):
|
11
|
+
ids: list[int]
|
12
|
+
paused: bool
|
13
|
+
|
14
|
+
|
15
|
+
class RolloutsDeleteRequest(BaseModel):
|
16
|
+
ids: list[int]
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import asyncio
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
|
5
|
+
from goosebit.api.responses import StatusResponse
|
6
|
+
from goosebit.db.models import Rollout
|
7
|
+
from goosebit.schema.rollouts import RolloutSchema
|
8
|
+
|
9
|
+
|
10
|
+
class RolloutsPutResponse(StatusResponse):
|
11
|
+
id: int
|
12
|
+
|
13
|
+
|
14
|
+
class RolloutsResponse(BaseModel):
|
15
|
+
rollouts: list[RolloutSchema]
|
16
|
+
|
17
|
+
@classmethod
|
18
|
+
async def convert(cls, devices: list[Rollout]):
|
19
|
+
return cls(rollouts=await asyncio.gather(*[RolloutSchema.convert(d) for d in devices]))
|