goosebit 0.2.4__tar.gz → 0.2.6__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 (157) hide show
  1. goosebit-0.2.6/PKG-INFO +280 -0
  2. goosebit-0.2.6/README.md +246 -0
  3. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/__init__.py +56 -6
  4. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/telemetry/metrics.py +1 -5
  5. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/v1/devices/device/responses.py +1 -0
  6. goosebit-0.2.6/goosebit/api/v1/devices/device/routes.py +33 -0
  7. goosebit-0.2.6/goosebit/api/v1/devices/requests.py +27 -0
  8. goosebit-0.2.6/goosebit/api/v1/devices/routes.py +111 -0
  9. goosebit-0.2.6/goosebit/api/v1/download/routes.py +33 -0
  10. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/v1/rollouts/routes.py +5 -4
  11. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/v1/routes.py +2 -1
  12. goosebit-0.2.6/goosebit/api/v1/settings/routes.py +14 -0
  13. goosebit-0.2.6/goosebit/api/v1/settings/users/requests.py +16 -0
  14. goosebit-0.2.6/goosebit/api/v1/settings/users/responses.py +7 -0
  15. goosebit-0.2.6/goosebit/api/v1/settings/users/routes.py +56 -0
  16. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/v1/software/routes.py +18 -14
  17. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/auth/__init__.py +54 -14
  18. goosebit-0.2.6/goosebit/auth/permissions.py +80 -0
  19. goosebit-0.2.6/goosebit/db/config.py +66 -0
  20. goosebit-0.2.6/goosebit/db/migrations/models/1_20241109151811_update.py +11 -0
  21. goosebit-0.2.6/goosebit/db/migrations/models/2_20241121113728_update.py +11 -0
  22. goosebit-0.2.6/goosebit/db/migrations/models/3_20241121140210_update.py +11 -0
  23. goosebit-0.2.6/goosebit/db/migrations/models/4_20250324110331_update.py +16 -0
  24. goosebit-0.2.6/goosebit/db/migrations/models/4_20250402085235_rename_uuid_to_id.py +11 -0
  25. goosebit-0.2.6/goosebit/db/migrations/models/5_20250619090242_null_feed.py +83 -0
  26. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/db/models.py +22 -7
  27. goosebit-0.2.6/goosebit/db/pg_ssl_context.py +51 -0
  28. goosebit-0.2.6/goosebit/device_manager.py +262 -0
  29. goosebit-0.2.6/goosebit/plugins/__init__.py +32 -0
  30. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/schema/devices.py +9 -6
  31. goosebit-0.2.6/goosebit/schema/plugins.py +67 -0
  32. goosebit-0.2.6/goosebit/schema/updates.py +15 -0
  33. goosebit-0.2.6/goosebit/schema/users.py +9 -0
  34. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/settings/__init__.py +0 -3
  35. goosebit-0.2.6/goosebit/settings/schema.py +134 -0
  36. goosebit-0.2.6/goosebit/storage/__init__.py +62 -0
  37. goosebit-0.2.6/goosebit/storage/base.py +14 -0
  38. goosebit-0.2.6/goosebit/storage/filesystem.py +111 -0
  39. goosebit-0.2.6/goosebit/storage/s3.py +104 -0
  40. goosebit-0.2.6/goosebit/ui/bff/common/columns.py +50 -0
  41. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/ui/bff/common/requests.py +3 -15
  42. goosebit-0.2.6/goosebit/ui/bff/common/responses.py +17 -0
  43. goosebit-0.2.6/goosebit/ui/bff/devices/device/routes.py +17 -0
  44. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/ui/bff/devices/requests.py +1 -0
  45. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/ui/bff/devices/responses.py +6 -2
  46. goosebit-0.2.6/goosebit/ui/bff/devices/routes.py +129 -0
  47. goosebit-0.2.6/goosebit/ui/bff/download/__init__.py +1 -0
  48. goosebit-0.2.6/goosebit/ui/bff/download/routes.py +33 -0
  49. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/ui/bff/rollouts/responses.py +6 -2
  50. goosebit-0.2.6/goosebit/ui/bff/rollouts/routes.py +82 -0
  51. goosebit-0.2.6/goosebit/ui/bff/routes.py +14 -0
  52. goosebit-0.2.6/goosebit/ui/bff/settings/routes.py +20 -0
  53. {goosebit-0.2.4/goosebit/ui/bff/software → goosebit-0.2.6/goosebit/ui/bff/settings/users}/responses.py +9 -13
  54. goosebit-0.2.6/goosebit/ui/bff/settings/users/routes.py +80 -0
  55. goosebit-0.2.6/goosebit/ui/bff/software/responses.py +47 -0
  56. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/ui/bff/software/routes.py +40 -12
  57. goosebit-0.2.6/goosebit/ui/nav.py +26 -0
  58. goosebit-0.2.6/goosebit/ui/routes.py +108 -0
  59. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/ui/static/js/devices.js +72 -80
  60. goosebit-0.2.6/goosebit/ui/static/js/login.js +39 -0
  61. goosebit-0.2.6/goosebit/ui/static/js/logs.js +10 -0
  62. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/ui/static/js/rollouts.js +39 -35
  63. goosebit-0.2.6/goosebit/ui/static/js/settings.js +322 -0
  64. goosebit-0.2.6/goosebit/ui/static/js/setup.js +28 -0
  65. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/ui/static/js/software.js +127 -127
  66. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/ui/static/js/util.js +45 -4
  67. goosebit-0.2.6/goosebit/ui/templates/__init__.py +22 -0
  68. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/ui/templates/devices.html.jinja +0 -20
  69. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/ui/templates/login.html.jinja +5 -0
  70. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/ui/templates/nav.html.jinja +26 -7
  71. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/ui/templates/rollouts.html.jinja +4 -22
  72. goosebit-0.2.6/goosebit/ui/templates/settings.html.jinja +88 -0
  73. goosebit-0.2.6/goosebit/ui/templates/setup.html.jinja +71 -0
  74. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/ui/templates/software.html.jinja +0 -11
  75. goosebit-0.2.6/goosebit/updater/__init__.py +1 -0
  76. goosebit-0.2.6/goosebit/updater/controller/__init__.py +1 -0
  77. goosebit-0.2.6/goosebit/updater/controller/v1/__init__.py +1 -0
  78. goosebit-0.2.6/goosebit/updater/controller/v1/routes.py +237 -0
  79. goosebit-0.2.6/goosebit/updater/routes.py +100 -0
  80. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/updates/__init__.py +24 -31
  81. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/updates/swdesc.py +15 -8
  82. goosebit-0.2.6/goosebit/users/__init__.py +63 -0
  83. goosebit-0.2.6/goosebit/util/__init__.py +0 -0
  84. goosebit-0.2.6/goosebit/util/path.py +42 -0
  85. goosebit-0.2.6/goosebit/util/version.py +92 -0
  86. goosebit-0.2.6/pyproject.toml +84 -0
  87. goosebit-0.2.4/PKG-INFO +0 -181
  88. goosebit-0.2.4/README.md +0 -149
  89. goosebit-0.2.4/goosebit/api/v1/devices/device/routes.py +0 -33
  90. goosebit-0.2.4/goosebit/api/v1/devices/requests.py +0 -7
  91. goosebit-0.2.4/goosebit/api/v1/devices/routes.py +0 -36
  92. goosebit-0.2.4/goosebit/api/v1/download/routes.py +0 -22
  93. goosebit-0.2.4/goosebit/db/config.py +0 -10
  94. goosebit-0.2.4/goosebit/realtime/logs.py +0 -42
  95. goosebit-0.2.4/goosebit/realtime/routes.py +0 -13
  96. goosebit-0.2.4/goosebit/settings/schema.py +0 -86
  97. goosebit-0.2.4/goosebit/ui/bff/devices/routes.py +0 -75
  98. goosebit-0.2.4/goosebit/ui/bff/download/routes.py +0 -22
  99. goosebit-0.2.4/goosebit/ui/bff/rollouts/routes.py +0 -54
  100. goosebit-0.2.4/goosebit/ui/bff/routes.py +0 -11
  101. goosebit-0.2.4/goosebit/ui/nav.py +0 -16
  102. goosebit-0.2.4/goosebit/ui/routes.py +0 -64
  103. goosebit-0.2.4/goosebit/ui/static/js/index.js +0 -155
  104. goosebit-0.2.4/goosebit/ui/static/js/login.js +0 -23
  105. goosebit-0.2.4/goosebit/ui/static/js/logs.js +0 -25
  106. goosebit-0.2.4/goosebit/ui/templates/__init__.py +0 -13
  107. goosebit-0.2.4/goosebit/ui/templates/index.html.jinja +0 -25
  108. goosebit-0.2.4/goosebit/updater/controller/v1/routes.py +0 -189
  109. goosebit-0.2.4/goosebit/updater/manager.py +0 -357
  110. goosebit-0.2.4/goosebit/updater/routes.py +0 -21
  111. goosebit-0.2.4/pyproject.toml +0 -70
  112. {goosebit-0.2.4 → goosebit-0.2.6}/LICENSE +0 -0
  113. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/__main__.py +0 -0
  114. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/__init__.py +0 -0
  115. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/responses.py +0 -0
  116. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/routes.py +0 -0
  117. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/telemetry/__init__.py +0 -0
  118. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/telemetry/prometheus/__init__.py +0 -0
  119. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/telemetry/prometheus/readers.py +0 -0
  120. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/telemetry/prometheus/routes.py +0 -0
  121. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/telemetry/routes.py +0 -0
  122. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/v1/__init__.py +0 -0
  123. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/v1/devices/__init__.py +0 -0
  124. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/v1/devices/device/__init__.py +0 -0
  125. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/v1/devices/responses.py +0 -0
  126. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/v1/download/__init__.py +0 -0
  127. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/v1/rollouts/__init__.py +0 -0
  128. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/v1/rollouts/requests.py +0 -0
  129. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/v1/rollouts/responses.py +0 -0
  130. {goosebit-0.2.4/goosebit/api/v1/software → goosebit-0.2.6/goosebit/api/v1/settings}/__init__.py +0 -0
  131. {goosebit-0.2.4/goosebit/realtime → goosebit-0.2.6/goosebit/api/v1/settings/users}/__init__.py +0 -0
  132. {goosebit-0.2.4/goosebit/ui → goosebit-0.2.6/goosebit/api/v1/software}/__init__.py +0 -0
  133. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/v1/software/requests.py +0 -0
  134. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/api/v1/software/responses.py +0 -0
  135. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/db/__init__.py +0 -0
  136. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/db/migrations/models/0_20240830054046_init.py +0 -0
  137. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/schema/__init__.py +0 -0
  138. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/schema/rollouts.py +0 -0
  139. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/schema/software.py +0 -0
  140. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/settings/const.py +0 -0
  141. {goosebit-0.2.4/goosebit/ui/bff → goosebit-0.2.6/goosebit/ui}/__init__.py +0 -0
  142. {goosebit-0.2.4/goosebit/ui/bff/devices → goosebit-0.2.6/goosebit/ui/bff}/__init__.py +0 -0
  143. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/ui/bff/common/__init__.py +0 -0
  144. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/ui/bff/common/util.py +0 -0
  145. {goosebit-0.2.4/goosebit/ui/bff/rollouts → goosebit-0.2.6/goosebit/ui/bff/devices}/__init__.py +0 -0
  146. {goosebit-0.2.4/goosebit/ui/bff/download → goosebit-0.2.6/goosebit/ui/bff/devices/device}/__init__.py +0 -0
  147. {goosebit-0.2.4/goosebit/ui/bff/software → goosebit-0.2.6/goosebit/ui/bff/rollouts}/__init__.py +0 -0
  148. {goosebit-0.2.4/goosebit/updater → goosebit-0.2.6/goosebit/ui/bff/settings}/__init__.py +0 -0
  149. {goosebit-0.2.4/goosebit/updater/controller → goosebit-0.2.6/goosebit/ui/bff/settings/users}/__init__.py +0 -0
  150. {goosebit-0.2.4/goosebit/updater/controller/v1 → goosebit-0.2.6/goosebit/ui/bff/software}/__init__.py +0 -0
  151. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/ui/static/__init__.py +0 -0
  152. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/ui/static/favicon.ico +0 -0
  153. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/ui/static/favicon.svg +0 -0
  154. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/ui/static/svg/goosebit-logo.svg +0 -0
  155. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/ui/templates/logs.html.jinja +0 -0
  156. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/updater/controller/routes.py +0 -0
  157. {goosebit-0.2.4 → goosebit-0.2.6}/goosebit/updater/controller/v1/schema.py +0 -0
@@ -0,0 +1,280 @@
1
+ Metadata-Version: 2.3
2
+ Name: goosebit
3
+ Version: 0.2.6
4
+ Summary:
5
+ Author: Brett Rowan
6
+ Author-email: 121075405+b-rowan@users.noreply.github.com
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
+ Classifier: Programming Language :: Python :: 3.13
12
+ Provides-Extra: postgresql
13
+ Requires-Dist: aerich (>=0.9.1,<0.10.0)
14
+ Requires-Dist: aiocache (>=0.12.3,<0.13.0)
15
+ Requires-Dist: argon2-cffi (>=25.1.0,<26.0.0)
16
+ Requires-Dist: asyncpg (>=0.30.0,<0.31.0) ; extra == "postgresql"
17
+ Requires-Dist: boto3 (>=1.40.8,<2.0.0)
18
+ Requires-Dist: fastapi (>=0.116.1,<0.117.0)
19
+ Requires-Dist: httpx (>=0.28.1,<0.29.0)
20
+ Requires-Dist: itsdangerous (>=2.2.0,<3.0.0)
21
+ Requires-Dist: jinja2 (>=3.1.6,<4.0.0)
22
+ Requires-Dist: joserfc (>=1.2.2,<2.0.0)
23
+ Requires-Dist: libconf (>=2.0.1,<3.0.0)
24
+ Requires-Dist: opentelemetry-distro (>=0.57b0,<0.58)
25
+ Requires-Dist: opentelemetry-exporter-prometheus (>=0.57b0,<0.58)
26
+ Requires-Dist: opentelemetry-instrumentation-fastapi (>=0.57b0,<0.58)
27
+ Requires-Dist: pydantic-settings[yaml] (>=2.10.1,<3.0.0)
28
+ Requires-Dist: python-multipart (>=0.0.20,<0.0.21)
29
+ Requires-Dist: semver (>=3.0.4,<4.0.0)
30
+ Requires-Dist: tortoise-orm (>=0.25.1,<0.26.0)
31
+ Requires-Dist: uvicorn (>=0.35.0,<0.36.0)
32
+ Description-Content-Type: text/markdown
33
+
34
+ # gooseBit
35
+
36
+ <img src="https://upstreamdatainc.github.io/goosebit/img/goosebit-logo.png" style="width: 100px; height: 100px; display: block;">
37
+
38
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/UpstreamDataInc/goosebit/badge)](https://scorecard.dev/viewer/?uri=github.com/UpstreamDataInc/goosebit)
39
+
40
+ ---
41
+
42
+ A simplistic, opinionated remote update server implementing hawkBit™'s [DDI API](https://eclipse.dev/hawkbit/apis/ddi_api/).
43
+
44
+ ## Deployment
45
+
46
+ ### Docker Compose Demo
47
+
48
+ The Docker Compose demo [docker/demo/docker-compose.yml] may serve as inspiration for a containerized (cloud) deployment.
49
+ It uses PostgreSQL as the database and NGINX as a reverse proxy.
50
+
51
+ > [!WARNING]
52
+ > Do not use the demo (as-is) in production!
53
+
54
+ Make sure you have [Docker](https://www.docker.com/get-started/) (and Docker Compose) installed.
55
+ Then run:
56
+
57
+ ```txt
58
+ docker compose -f docker/demo/docker-compose.yml up
59
+ ```
60
+
61
+ Visit gooseBit at: https://localhost
62
+
63
+ [docker/demo/docker-compose.yml]: https://github.com/UpstreamDataInc/goosebit/blob/master/docker/docker-compose-dev.yml
64
+
65
+ ### Configuration
66
+
67
+ gooseBit can be configured through a configuration file (`/etc/goosebit.yaml`) or by setting environment variables.
68
+ For the available options and their defaults, see [goosebit.yaml].
69
+ The environment variable corresponding to e.g. the `poll_time` YAML setting would be `GOOSEBIT_POLL_TIME`.
70
+ Environment variables for nested settings are constructed using `__` as the separator:
71
+
72
+ ```txt
73
+ GOOSEBIT_DEVICE_AUTH__ENABLE=true
74
+ ```
75
+
76
+ Alternatively, a JSON string can be assigned:
77
+
78
+ ```txt
79
+ GOOSEBIT_DEVICE_AUTH='{"enable": true}'
80
+ ```
81
+
82
+ [goosebit.yaml]: https://github.com/UpstreamDataInc/goosebit/blob/master/goosebit.yaml
83
+
84
+ ### Database
85
+
86
+ By default, SQLite is used as the database. For more sophisticated setups, PostgreSQL is supported.
87
+ To use PostgreSQL, set `db_uri` or the `GOSSEBIT_DB_URI` environment variable to something like:
88
+
89
+ ```txt
90
+ postgres://user:password@host:5432/db_name
91
+ ```
92
+
93
+ ### Artifact Storage
94
+
95
+ The software packages managed by gooseBit are either stored on the local filesystem (`artifacts_dir` setting) or an S3-compatible object storage.
96
+
97
+ ## Assumptions
98
+
99
+ - Devices use [SWUpdate](https://swupdate.org) for managing software updates.
100
+
101
+ ## Features
102
+
103
+ ### Device Registry
104
+
105
+ 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
+
107
+ - `hw_model` and `hw_revision`: Used to match compatible software.
108
+ - `sw_version`: Indicates the currently installed software version.
109
+
110
+ The registry tracks each device's status, including the last online timestamp, installed software version, update state, and more.
111
+
112
+ ### Software Repository
113
+
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.
115
+
116
+ ### Device Update Modes
117
+
118
+ Devices can be configured with different update modes. The default mode is `Rollout`.
119
+
120
+ #### 1. Manual Update to Specified Software
121
+
122
+ Assign specific software to a device manually. Once installed, no further updates will be triggered.
123
+
124
+ #### 2. Automatic Update to Latest Software
125
+
126
+ 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.
127
+
128
+ #### 3. Software Rollout
129
+
130
+ 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.
131
+
132
+ ### Pause Updates
133
+
134
+ Devices can be pinned to their current software version, preventing any updates from being applied.
135
+
136
+ ### Real-time Update Logs
137
+
138
+ While updates are in progress, gooseBit captures real-time logs, which are accessible through the device repository.
139
+
140
+ ## Development with Poetry
141
+
142
+ ### Initial Setup
143
+
144
+ Install Poetry as described [here](https://python-poetry.org/docs/#installation).
145
+
146
+ Then, to install gooseBit's dependencies, run:
147
+
148
+ ```txt
149
+ poetry install
150
+ ```
151
+
152
+ Initialize the database:
153
+
154
+ ```txt
155
+ poetry run aerich upgrade
156
+ ```
157
+
158
+ Launch gooseBit:
159
+
160
+ ```txt
161
+ poetry run python -m goosebit
162
+ ```
163
+
164
+ The service is now available at: http://localhost:60053
165
+
166
+ ### Database
167
+
168
+ Initialize or migrate database:
169
+
170
+ ```txt
171
+ poetry run aerich upgrade
172
+ ```
173
+
174
+ After a model change create the migration:
175
+
176
+ ```txt
177
+ poetry run aerich migrate
178
+ ```
179
+
180
+ To seed some sample data (attention: drops all current data) use:
181
+
182
+ ```txt
183
+ poetry run generate-sample-data
184
+ ```
185
+
186
+ ### Code formatting and linting
187
+
188
+ Code is formatted using different tools
189
+
190
+ - black and isort for `*.py`
191
+ - biomejs for `*.js`, `*.json`
192
+ - prettier for `*.html`, `*.md`, `*.yml`, `*.yaml`
193
+
194
+ Code is linted using different tools as well
195
+
196
+ - flake8 for `*.py`
197
+ - biomejs for `*.js`
198
+
199
+ Best to have pre-commit install git hooks that run all those tools before a commit:
200
+
201
+ ```txt
202
+ poetry run pre-commit install
203
+ ```
204
+
205
+ To manually apply the hooks to all files use:
206
+
207
+ ```txt
208
+ poetry run pre-commit run --all-files
209
+ ```
210
+
211
+ ### Testing
212
+
213
+ Tests are implemented using pytest. You can run all the tests with:
214
+
215
+ ```txt
216
+ poetry run pytest
217
+ ```
218
+
219
+ To run only the unit tests:
220
+
221
+ ```txt
222
+ poetry run pytest tests/unit
223
+ ```
224
+
225
+ To run only the end-to-end tests:
226
+
227
+ ```txt
228
+ poetry run pytest tests/e2e
229
+ ```
230
+
231
+ ## Development with Docker (and PostgreSQL)
232
+
233
+ ### Running the Containers
234
+
235
+ ```txt
236
+ docker compose -f docker/docker-compose-dev.yml up --build
237
+ ```
238
+
239
+ ### Applying the Migrations
240
+
241
+ ```txt
242
+ docker exec goosebit-dev python -m aerich upgrade
243
+ ```
244
+
245
+ ### Using the Interactive Debugger
246
+
247
+ You might need [rlwrap](https://github.com/hanslub42/rlwrap) to fix readline support.
248
+
249
+ Place `breakpoint()` before the code you want to debug. The server will reload automatically.
250
+ Then, connect to remote PDB (when the breakpoint has been hit):
251
+
252
+ ```txt
253
+ rlwrap telnet localhost 4444
254
+ ```
255
+
256
+ To exit the debugger, press `Ctrl + ]` and then `q`.
257
+
258
+ ## Architecture
259
+
260
+ ### Structure
261
+
262
+ The structure of gooseBit is as follows:
263
+
264
+ - `api`: Files for the API.
265
+ - `ui`: Files for the UI.
266
+ - `bff`: Backend for frontend API.
267
+ - `static`: Static files.
268
+ - `templates`: Jinja2 formatted templates.
269
+ - `nav`: Navbar handler.
270
+ - `updater`: DDI API handler and device update manager.
271
+ - `updates`: SWUpdate file parsing.
272
+ - `auth`: Authentication functions and permission handling.
273
+ - `models`: Database models.
274
+ - `db`: Database config and initialization.
275
+ - `schema`: Pydantic models used for API type hinting.
276
+ - `settings`: Settings loader and handler.
277
+ - `storage`: Storage for software artifacts.
278
+ - `telemetry`: Telemetry data handlers.
279
+ - `routes`: Routes for a giving endpoint, including the router.
280
+
@@ -0,0 +1,246 @@
1
+ # gooseBit
2
+
3
+ <img src="https://upstreamdatainc.github.io/goosebit/img/goosebit-logo.png" style="width: 100px; height: 100px; display: block;">
4
+
5
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/UpstreamDataInc/goosebit/badge)](https://scorecard.dev/viewer/?uri=github.com/UpstreamDataInc/goosebit)
6
+
7
+ ---
8
+
9
+ A simplistic, opinionated remote update server implementing hawkBit™'s [DDI API](https://eclipse.dev/hawkbit/apis/ddi_api/).
10
+
11
+ ## Deployment
12
+
13
+ ### Docker Compose Demo
14
+
15
+ The Docker Compose demo [docker/demo/docker-compose.yml] may serve as inspiration for a containerized (cloud) deployment.
16
+ It uses PostgreSQL as the database and NGINX as a reverse proxy.
17
+
18
+ > [!WARNING]
19
+ > Do not use the demo (as-is) in production!
20
+
21
+ Make sure you have [Docker](https://www.docker.com/get-started/) (and Docker Compose) installed.
22
+ Then run:
23
+
24
+ ```txt
25
+ docker compose -f docker/demo/docker-compose.yml up
26
+ ```
27
+
28
+ Visit gooseBit at: https://localhost
29
+
30
+ [docker/demo/docker-compose.yml]: https://github.com/UpstreamDataInc/goosebit/blob/master/docker/docker-compose-dev.yml
31
+
32
+ ### Configuration
33
+
34
+ gooseBit can be configured through a configuration file (`/etc/goosebit.yaml`) or by setting environment variables.
35
+ For the available options and their defaults, see [goosebit.yaml].
36
+ The environment variable corresponding to e.g. the `poll_time` YAML setting would be `GOOSEBIT_POLL_TIME`.
37
+ Environment variables for nested settings are constructed using `__` as the separator:
38
+
39
+ ```txt
40
+ GOOSEBIT_DEVICE_AUTH__ENABLE=true
41
+ ```
42
+
43
+ Alternatively, a JSON string can be assigned:
44
+
45
+ ```txt
46
+ GOOSEBIT_DEVICE_AUTH='{"enable": true}'
47
+ ```
48
+
49
+ [goosebit.yaml]: https://github.com/UpstreamDataInc/goosebit/blob/master/goosebit.yaml
50
+
51
+ ### Database
52
+
53
+ By default, SQLite is used as the database. For more sophisticated setups, PostgreSQL is supported.
54
+ To use PostgreSQL, set `db_uri` or the `GOSSEBIT_DB_URI` environment variable to something like:
55
+
56
+ ```txt
57
+ postgres://user:password@host:5432/db_name
58
+ ```
59
+
60
+ ### Artifact Storage
61
+
62
+ The software packages managed by gooseBit are either stored on the local filesystem (`artifacts_dir` setting) or an S3-compatible object storage.
63
+
64
+ ## Assumptions
65
+
66
+ - Devices use [SWUpdate](https://swupdate.org) for managing software updates.
67
+
68
+ ## Features
69
+
70
+ ### Device Registry
71
+
72
+ 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:
73
+
74
+ - `hw_model` and `hw_revision`: Used to match compatible software.
75
+ - `sw_version`: Indicates the currently installed software version.
76
+
77
+ The registry tracks each device's status, including the last online timestamp, installed software version, update state, and more.
78
+
79
+ ### Software Repository
80
+
81
+ 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.
82
+
83
+ ### Device Update Modes
84
+
85
+ Devices can be configured with different update modes. The default mode is `Rollout`.
86
+
87
+ #### 1. Manual Update to Specified Software
88
+
89
+ Assign specific software to a device manually. Once installed, no further updates will be triggered.
90
+
91
+ #### 2. Automatic Update to Latest Software
92
+
93
+ 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.
94
+
95
+ #### 3. Software Rollout
96
+
97
+ 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.
98
+
99
+ ### Pause Updates
100
+
101
+ Devices can be pinned to their current software version, preventing any updates from being applied.
102
+
103
+ ### Real-time Update Logs
104
+
105
+ While updates are in progress, gooseBit captures real-time logs, which are accessible through the device repository.
106
+
107
+ ## Development with Poetry
108
+
109
+ ### Initial Setup
110
+
111
+ Install Poetry as described [here](https://python-poetry.org/docs/#installation).
112
+
113
+ Then, to install gooseBit's dependencies, run:
114
+
115
+ ```txt
116
+ poetry install
117
+ ```
118
+
119
+ Initialize the database:
120
+
121
+ ```txt
122
+ poetry run aerich upgrade
123
+ ```
124
+
125
+ Launch gooseBit:
126
+
127
+ ```txt
128
+ poetry run python -m goosebit
129
+ ```
130
+
131
+ The service is now available at: http://localhost:60053
132
+
133
+ ### Database
134
+
135
+ Initialize or migrate database:
136
+
137
+ ```txt
138
+ poetry run aerich upgrade
139
+ ```
140
+
141
+ After a model change create the migration:
142
+
143
+ ```txt
144
+ poetry run aerich migrate
145
+ ```
146
+
147
+ To seed some sample data (attention: drops all current data) use:
148
+
149
+ ```txt
150
+ poetry run generate-sample-data
151
+ ```
152
+
153
+ ### Code formatting and linting
154
+
155
+ Code is formatted using different tools
156
+
157
+ - black and isort for `*.py`
158
+ - biomejs for `*.js`, `*.json`
159
+ - prettier for `*.html`, `*.md`, `*.yml`, `*.yaml`
160
+
161
+ Code is linted using different tools as well
162
+
163
+ - flake8 for `*.py`
164
+ - biomejs for `*.js`
165
+
166
+ Best to have pre-commit install git hooks that run all those tools before a commit:
167
+
168
+ ```txt
169
+ poetry run pre-commit install
170
+ ```
171
+
172
+ To manually apply the hooks to all files use:
173
+
174
+ ```txt
175
+ poetry run pre-commit run --all-files
176
+ ```
177
+
178
+ ### Testing
179
+
180
+ Tests are implemented using pytest. You can run all the tests with:
181
+
182
+ ```txt
183
+ poetry run pytest
184
+ ```
185
+
186
+ To run only the unit tests:
187
+
188
+ ```txt
189
+ poetry run pytest tests/unit
190
+ ```
191
+
192
+ To run only the end-to-end tests:
193
+
194
+ ```txt
195
+ poetry run pytest tests/e2e
196
+ ```
197
+
198
+ ## Development with Docker (and PostgreSQL)
199
+
200
+ ### Running the Containers
201
+
202
+ ```txt
203
+ docker compose -f docker/docker-compose-dev.yml up --build
204
+ ```
205
+
206
+ ### Applying the Migrations
207
+
208
+ ```txt
209
+ docker exec goosebit-dev python -m aerich upgrade
210
+ ```
211
+
212
+ ### Using the Interactive Debugger
213
+
214
+ You might need [rlwrap](https://github.com/hanslub42/rlwrap) to fix readline support.
215
+
216
+ Place `breakpoint()` before the code you want to debug. The server will reload automatically.
217
+ Then, connect to remote PDB (when the breakpoint has been hit):
218
+
219
+ ```txt
220
+ rlwrap telnet localhost 4444
221
+ ```
222
+
223
+ To exit the debugger, press `Ctrl + ]` and then `q`.
224
+
225
+ ## Architecture
226
+
227
+ ### Structure
228
+
229
+ The structure of gooseBit is as follows:
230
+
231
+ - `api`: Files for the API.
232
+ - `ui`: Files for the UI.
233
+ - `bff`: Backend for frontend API.
234
+ - `static`: Static files.
235
+ - `templates`: Jinja2 formatted templates.
236
+ - `nav`: Navbar handler.
237
+ - `updater`: DDI API handler and device update manager.
238
+ - `updates`: SWUpdate file parsing.
239
+ - `auth`: Authentication functions and permission handling.
240
+ - `models`: Database models.
241
+ - `db`: Database config and initialization.
242
+ - `schema`: Pydantic models used for API type hinting.
243
+ - `settings`: Settings loader and handler.
244
+ - `storage`: Storage for software artifacts.
245
+ - `telemetry`: Telemetry data handlers.
246
+ - `routes`: Routes for a giving endpoint, including the router.
@@ -4,19 +4,23 @@ from logging import getLogger
4
4
  from typing import Annotated
5
5
 
6
6
  from fastapi import Depends, FastAPI, HTTPException
7
+ from fastapi.exception_handlers import http_exception_handler
7
8
  from fastapi.openapi.docs import get_swagger_ui_html
8
9
  from fastapi.requests import Request
9
10
  from fastapi.responses import RedirectResponse
10
11
  from fastapi.security import OAuth2PasswordRequestForm
11
12
  from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor as Instrumentor
13
+ from starlette.exceptions import HTTPException as StarletteHTTPException
12
14
  from tortoise.exceptions import ValidationError
13
15
 
14
- from goosebit import api, db, realtime, ui, updater
15
- from goosebit.api.telemetry import metrics
16
+ from goosebit import api, db, plugins, ui, updater
16
17
  from goosebit.auth import get_user_from_request, login_user, redirect_if_authenticated
18
+ from goosebit.device_manager import DeviceManager
19
+ from goosebit.settings import PWD_CXT, config
17
20
  from goosebit.ui.nav import nav
18
21
  from goosebit.ui.static import static
19
22
  from goosebit.ui.templates import templates
23
+ from goosebit.users import create_initial_user
20
24
 
21
25
  logger = getLogger(__name__)
22
26
 
@@ -26,7 +30,9 @@ async def lifespan(_: FastAPI):
26
30
  db_ready = await db.init()
27
31
  if not db_ready:
28
32
  logger.exception("DB does not exist, try running `poetry run aerich upgrade`.")
29
- await metrics.init()
33
+
34
+ logger.debug(f"Initialized storage backend: {config.storage.backend}")
35
+
30
36
  if db_ready:
31
37
  yield
32
38
  await db.close()
@@ -54,10 +60,30 @@ app = FastAPI(
54
60
  app.include_router(updater.router)
55
61
  app.include_router(ui.router)
56
62
  app.include_router(api.router)
57
- app.include_router(realtime.router)
58
63
  app.mount("/static", static, name="static")
59
64
  Instrumentor.instrument_app(app)
60
65
 
66
+ for plugin in plugins.load():
67
+ if plugin.middleware is not None:
68
+ logger.info(f"Adding middleware for plugin: {plugin.name}")
69
+ app.add_middleware(plugin.middleware)
70
+ if plugin.router is not None:
71
+ logger.info(f"Adding routing handler for plugin: {plugin.name}")
72
+ app.include_router(router=plugin.router, prefix=plugin.url_prefix)
73
+ if plugin.db_model_path is not None:
74
+ logger.info(f"Adding db handler for plugin: {plugin.name}")
75
+ db.config.add_models(plugin.db_model_path)
76
+ if plugin.static_files is not None:
77
+ logger.info(f"Adding static files handler for plugin: {plugin.name}")
78
+ app.mount(f"{plugin.url_prefix}/static", plugin.static_files, name=plugin.static_files_name)
79
+ if plugin.templates is not None:
80
+ logger.info(f"Adding template handler for plugin: {plugin.name}")
81
+ templates.add_template_handler(plugin.templates)
82
+ if plugin.update_source_hook is not None:
83
+ DeviceManager.add_update_source(plugin.update_source_hook)
84
+ if plugin.config_data_hook is not None:
85
+ DeviceManager.add_config_callback(plugin.config_data_hook)
86
+
61
87
 
62
88
  # Custom exception handler for Tortoise ValidationError
63
89
  @app.exception_handler(ValidationError)
@@ -65,6 +91,13 @@ async def tortoise_validation_exception_handler(request: Request, exc: Validatio
65
91
  raise HTTPException(422, str(exc))
66
92
 
67
93
 
94
+ # Extend default handler to do logging
95
+ @app.exception_handler(StarletteHTTPException)
96
+ async def custom_http_exception_handler(request, exc):
97
+ logger.warning(f"HTTPException, request={request.url}, status={exc.status_code}, detail={exc.detail}")
98
+ return await http_exception_handler(request, exc)
99
+
100
+
68
101
  @app.middleware("http")
69
102
  async def attach_user(request: Request, call_next):
70
103
  request.scope["user"] = await get_user_from_request(request)
@@ -77,6 +110,12 @@ async def attach_nav(request: Request, call_next):
77
110
  return await call_next(request)
78
111
 
79
112
 
113
+ @app.middleware("http")
114
+ async def attach_config(request: Request, call_next):
115
+ request.scope["config"] = config
116
+ return await call_next(request)
117
+
118
+
80
119
  @app.get("/", include_in_schema=False)
81
120
  def root_redirect(request: Request):
82
121
  return RedirectResponse(request.url_for("ui_root"))
@@ -89,7 +128,18 @@ async def login_get(request: Request):
89
128
 
90
129
  @app.post("/login", tags=["login"])
91
130
  async def login_post(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
92
- return {"access_token": login_user(form_data.username, form_data.password), "token_type": "bearer"}
131
+ return {"access_token": await login_user(form_data.username, form_data.password), "token_type": "bearer"}
132
+
133
+
134
+ @app.get("/setup", include_in_schema=False)
135
+ async def setup_get(request: Request):
136
+ return templates.TemplateResponse(request, "setup.html.jinja")
137
+
138
+
139
+ @app.post("/setup", include_in_schema=False)
140
+ async def setup_post(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
141
+ await create_initial_user(form_data.username, PWD_CXT.hash(form_data.password))
142
+ return {"access_token": await login_user(form_data.username, form_data.password), "token_type": "bearer"}
93
143
 
94
144
 
95
145
  @app.get("/logout", include_in_schema=False)
@@ -99,7 +149,7 @@ async def logout(request: Request):
99
149
  return resp
100
150
 
101
151
 
102
- @app.get("/docs")
152
+ @app.get("/docs", include_in_schema=False)
103
153
  async def swagger_docs(request: Request):
104
154
  return get_swagger_ui_html(
105
155
  title="gooseBit docs",
@@ -2,7 +2,7 @@ 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.settings import USERS, config
5
+ from goosebit.settings import config
6
6
 
7
7
  from . import prometheus
8
8
 
@@ -28,7 +28,3 @@ users_count = meter.create_gauge(
28
28
  "users.count",
29
29
  description="The number of registered users",
30
30
  )
31
-
32
-
33
- async def init():
34
- users_count.set(len(USERS))
@@ -7,6 +7,7 @@ from goosebit.schema.devices import DeviceSchema
7
7
 
8
8
  class DeviceLogResponse(BaseModel):
9
9
  log: str | None
10
+ progress: int | None
10
11
 
11
12
 
12
13
  class DeviceResponse(DeviceSchema):
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from fastapi import APIRouter, Depends, HTTPException, 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.auth.permissions import GOOSEBIT_PERMISSIONS
9
+ from goosebit.db import Device
10
+ from goosebit.device_manager import get_device
11
+
12
+ router = APIRouter(prefix="/{dev_id}")
13
+
14
+
15
+ @router.get(
16
+ "",
17
+ dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["device"]["read"]()])],
18
+ )
19
+ async def device_get(_: Request, device: Device = Depends(get_device)) -> DeviceResponse:
20
+ if device is None:
21
+ raise HTTPException(404)
22
+ await device.fetch_related("assigned_software", "hardware")
23
+ return DeviceResponse.model_validate(device)
24
+
25
+
26
+ @router.get(
27
+ "/log",
28
+ dependencies=[Security(validate_user_permissions, scopes=[GOOSEBIT_PERMISSIONS["device"]["read"]()])],
29
+ )
30
+ async def device_logs(_: Request, device: Device = Depends(get_device)) -> DeviceLogResponse:
31
+ if device is None:
32
+ raise HTTPException(404)
33
+ return DeviceLogResponse(log=device.last_log, progress=device.progress)