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.
Files changed (124) hide show
  1. goosebit-0.2.1/PKG-INFO +173 -0
  2. goosebit-0.2.1/README.md +140 -0
  3. goosebit-0.2.1/goosebit/__init__.py +96 -0
  4. goosebit-0.2.1/goosebit/__main__.py +7 -0
  5. goosebit-0.2.1/goosebit/api/responses.py +5 -0
  6. goosebit-0.2.1/goosebit/api/routes.py +9 -0
  7. goosebit-0.1.2/goosebit/telemetry/__init__.py → goosebit-0.2.1/goosebit/api/telemetry/metrics.py +9 -3
  8. goosebit-0.2.1/goosebit/api/telemetry/prometheus/__init__.py +2 -0
  9. goosebit-0.2.1/goosebit/api/telemetry/prometheus/readers.py +3 -0
  10. goosebit-0.2.1/goosebit/api/telemetry/prometheus/routes.py +18 -0
  11. goosebit-0.2.1/goosebit/api/telemetry/routes.py +9 -0
  12. goosebit-0.2.1/goosebit/api/v1/devices/__init__.py +1 -0
  13. goosebit-0.2.1/goosebit/api/v1/devices/device/__init__.py +1 -0
  14. goosebit-0.2.1/goosebit/api/v1/devices/device/responses.py +13 -0
  15. goosebit-0.2.1/goosebit/api/v1/devices/device/routes.py +27 -0
  16. goosebit-0.2.1/goosebit/api/v1/devices/requests.py +7 -0
  17. goosebit-0.2.1/goosebit/api/v1/devices/responses.py +16 -0
  18. goosebit-0.2.1/goosebit/api/v1/devices/routes.py +35 -0
  19. goosebit-0.2.1/goosebit/api/v1/download/__init__.py +1 -0
  20. goosebit-0.2.1/goosebit/api/v1/download/routes.py +22 -0
  21. goosebit-0.2.1/goosebit/api/v1/rollouts/requests.py +16 -0
  22. goosebit-0.2.1/goosebit/api/v1/rollouts/responses.py +19 -0
  23. goosebit-0.2.1/goosebit/api/v1/rollouts/routes.py +50 -0
  24. goosebit-0.2.1/goosebit/api/v1/routes.py +9 -0
  25. goosebit-0.2.1/goosebit/api/v1/software/requests.py +5 -0
  26. goosebit-0.2.1/goosebit/api/v1/software/responses.py +16 -0
  27. goosebit-0.2.1/goosebit/api/v1/software/routes.py +77 -0
  28. goosebit-0.2.1/goosebit/auth/__init__.py +139 -0
  29. goosebit-0.2.1/goosebit/db/__init__.py +11 -0
  30. goosebit-0.2.1/goosebit/db/config.py +10 -0
  31. goosebit-0.2.1/goosebit/db/migrations/models/0_20240830054046_init.py +136 -0
  32. {goosebit-0.1.2/goosebit → goosebit-0.2.1/goosebit/db}/models.py +17 -10
  33. {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/realtime/logs.py +4 -3
  34. {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/realtime/routes.py +2 -2
  35. goosebit-0.2.1/goosebit/schema/__init__.py +0 -0
  36. goosebit-0.2.1/goosebit/schema/devices.py +73 -0
  37. goosebit-0.2.1/goosebit/schema/rollouts.py +31 -0
  38. goosebit-0.2.1/goosebit/schema/software.py +37 -0
  39. goosebit-0.2.1/goosebit/settings/__init__.py +17 -0
  40. goosebit-0.2.1/goosebit/settings/const.py +21 -0
  41. goosebit-0.2.1/goosebit/settings/schema.py +86 -0
  42. goosebit-0.2.1/goosebit/ui/__init__.py +1 -0
  43. goosebit-0.2.1/goosebit/ui/bff/__init__.py +1 -0
  44. goosebit-0.2.1/goosebit/ui/bff/devices/__init__.py +1 -0
  45. goosebit-0.2.1/goosebit/ui/bff/devices/requests.py +12 -0
  46. goosebit-0.2.1/goosebit/ui/bff/devices/responses.py +39 -0
  47. goosebit-0.2.1/goosebit/ui/bff/devices/routes.py +72 -0
  48. goosebit-0.2.1/goosebit/ui/bff/download/__init__.py +1 -0
  49. goosebit-0.2.1/goosebit/ui/bff/download/routes.py +22 -0
  50. goosebit-0.2.1/goosebit/ui/bff/rollouts/__init__.py +1 -0
  51. goosebit-0.2.1/goosebit/ui/bff/rollouts/responses.py +37 -0
  52. goosebit-0.2.1/goosebit/ui/bff/rollouts/routes.py +52 -0
  53. goosebit-0.2.1/goosebit/ui/bff/routes.py +11 -0
  54. goosebit-0.2.1/goosebit/ui/bff/software/__init__.py +1 -0
  55. goosebit-0.2.1/goosebit/ui/bff/software/responses.py +37 -0
  56. goosebit-0.2.1/goosebit/ui/bff/software/routes.py +83 -0
  57. goosebit-0.2.1/goosebit/ui/nav.py +16 -0
  58. goosebit-0.2.1/goosebit/ui/routes.py +64 -0
  59. goosebit-0.2.1/goosebit/ui/static/favicon.ico +0 -0
  60. goosebit-0.2.1/goosebit/ui/static/favicon.svg +1 -0
  61. {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/ui/static/js/devices.js +47 -71
  62. {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/ui/static/js/index.js +4 -9
  63. goosebit-0.2.1/goosebit/ui/static/js/login.js +23 -0
  64. {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/ui/static/js/logs.js +1 -1
  65. {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/ui/static/js/rollouts.js +33 -19
  66. goosebit-0.1.2/goosebit/ui/static/js/firmware.js → goosebit-0.2.1/goosebit/ui/static/js/software.js +87 -86
  67. goosebit-0.2.1/goosebit/ui/static/js/util.js +120 -0
  68. goosebit-0.2.1/goosebit/ui/static/svg/goosebit-logo.svg +1 -0
  69. goosebit-0.2.1/goosebit/ui/templates/__init__.py +13 -0
  70. goosebit-0.2.1/goosebit/ui/templates/devices.html.jinja +75 -0
  71. goosebit-0.2.1/goosebit/ui/templates/index.html.jinja +25 -0
  72. goosebit-0.2.1/goosebit/ui/templates/login.html.jinja +57 -0
  73. goosebit-0.2.1/goosebit/ui/templates/logs.html.jinja +31 -0
  74. goosebit-0.2.1/goosebit/ui/templates/nav.html.jinja +84 -0
  75. goosebit-0.2.1/goosebit/ui/templates/rollouts.html.jinja +93 -0
  76. goosebit-0.2.1/goosebit/ui/templates/software.html.jinja +139 -0
  77. goosebit-0.2.1/goosebit/updater/__init__.py +1 -0
  78. goosebit-0.2.1/goosebit/updater/controller/__init__.py +1 -0
  79. goosebit-0.2.1/goosebit/updater/controller/v1/__init__.py +1 -0
  80. goosebit-0.2.1/goosebit/updater/controller/v1/routes.py +185 -0
  81. goosebit-0.2.1/goosebit/updater/controller/v1/schema.py +56 -0
  82. {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/updater/manager.py +65 -65
  83. {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/updater/routes.py +3 -11
  84. goosebit-0.2.1/goosebit/updates/__init__.py +129 -0
  85. {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/updates/swdesc.py +2 -7
  86. {goosebit-0.1.2 → goosebit-0.2.1}/pyproject.toml +17 -6
  87. goosebit-0.1.2/PKG-INFO +0 -123
  88. goosebit-0.1.2/README.md +0 -94
  89. goosebit-0.1.2/goosebit/__init__.py +0 -65
  90. goosebit-0.1.2/goosebit/api/devices.py +0 -136
  91. goosebit-0.1.2/goosebit/api/download.py +0 -34
  92. goosebit-0.1.2/goosebit/api/firmware.py +0 -57
  93. goosebit-0.1.2/goosebit/api/helper.py +0 -30
  94. goosebit-0.1.2/goosebit/api/rollouts.py +0 -87
  95. goosebit-0.1.2/goosebit/api/routes.py +0 -19
  96. goosebit-0.1.2/goosebit/auth/__init__.py +0 -139
  97. goosebit-0.1.2/goosebit/db.py +0 -37
  98. goosebit-0.1.2/goosebit/permissions.py +0 -75
  99. goosebit-0.1.2/goosebit/settings.py +0 -64
  100. goosebit-0.1.2/goosebit/telemetry/prometheus.py +0 -10
  101. goosebit-0.1.2/goosebit/ui/routes.py +0 -101
  102. goosebit-0.1.2/goosebit/ui/static/favicon.ico +0 -0
  103. goosebit-0.1.2/goosebit/ui/static/favicon.svg +0 -1
  104. goosebit-0.1.2/goosebit/ui/static/js/util.js +0 -66
  105. goosebit-0.1.2/goosebit/ui/static/svg/goosebit-logo.svg +0 -1
  106. goosebit-0.1.2/goosebit/ui/templates/__init__.py +0 -5
  107. goosebit-0.1.2/goosebit/ui/templates/devices.html +0 -115
  108. goosebit-0.1.2/goosebit/ui/templates/firmware.html +0 -163
  109. goosebit-0.1.2/goosebit/ui/templates/index.html +0 -23
  110. goosebit-0.1.2/goosebit/ui/templates/login.html +0 -65
  111. goosebit-0.1.2/goosebit/ui/templates/logs.html +0 -36
  112. goosebit-0.1.2/goosebit/ui/templates/nav.html +0 -117
  113. goosebit-0.1.2/goosebit/ui/templates/rollouts.html +0 -76
  114. goosebit-0.1.2/goosebit/updater/controller/v1/routes.py +0 -180
  115. goosebit-0.1.2/goosebit/updates/__init__.py +0 -70
  116. {goosebit-0.1.2 → goosebit-0.2.1}/LICENSE +0 -0
  117. {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/api/__init__.py +0 -0
  118. {goosebit-0.1.2/goosebit/realtime → goosebit-0.2.1/goosebit/api/telemetry}/__init__.py +0 -0
  119. {goosebit-0.1.2/goosebit/ui → goosebit-0.2.1/goosebit/api/v1}/__init__.py +0 -0
  120. {goosebit-0.1.2/goosebit/updater → goosebit-0.2.1/goosebit/api/v1/rollouts}/__init__.py +0 -0
  121. {goosebit-0.1.2/goosebit/updater/controller → goosebit-0.2.1/goosebit/api/v1/software}/__init__.py +0 -0
  122. {goosebit-0.1.2/goosebit/updater/controller/v1 → goosebit-0.2.1/goosebit/realtime}/__init__.py +0 -0
  123. {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/ui/static/__init__.py +0 -0
  124. {goosebit-0.1.2 → goosebit-0.2.1}/goosebit/updater/controller/routes.py +0 -0
@@ -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
+
@@ -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,7 @@
1
+ import uvicorn
2
+
3
+ from goosebit import app
4
+ from goosebit.settings import config
5
+
6
+ if __name__ == "__main__":
7
+ uvicorn.run(app, host="0.0.0.0", port=config.port)
@@ -0,0 +1,5 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class StatusResponse(BaseModel):
5
+ success: bool
@@ -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)
@@ -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 settings
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=[prometheus.reader])
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(settings.USERS))
34
+ users_count.set(len(USERS))
@@ -0,0 +1,2 @@
1
+ from .readers import reader # noqa: F401
2
+ from .routes import router # noqa: F401
@@ -0,0 +1,3 @@
1
+ from opentelemetry.exporter.prometheus import PrometheusMetricReader
2
+
3
+ reader = PrometheusMetricReader()
@@ -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,9 @@
1
+ from fastapi import APIRouter
2
+
3
+ from goosebit.settings import config
4
+
5
+ from . import prometheus
6
+
7
+ router = APIRouter(prefix="/telemetry")
8
+ if config.metrics.prometheus.enable:
9
+ router.include_router(prometheus.router)
@@ -0,0 +1 @@
1
+ from .routes import router # noqa : F401
@@ -0,0 +1 @@
1
+ from .routes import router # noqa : F401
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from goosebit.schema.devices import DeviceSchema
6
+
7
+
8
+ class DeviceLogResponse(BaseModel):
9
+ log: str | None
10
+
11
+
12
+ class DeviceResponse(DeviceSchema):
13
+ pass
@@ -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,7 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class DevicesDeleteRequest(BaseModel):
7
+ devices: list[str]
@@ -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]))