goosebit 0.2.5__tar.gz → 0.2.7__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 (154) hide show
  1. goosebit-0.2.7/PKG-INFO +280 -0
  2. goosebit-0.2.7/README.md +246 -0
  3. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/__init__.py +41 -7
  4. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/telemetry/metrics.py +1 -5
  5. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/v1/devices/device/responses.py +1 -0
  6. goosebit-0.2.7/goosebit/api/v1/devices/device/routes.py +33 -0
  7. goosebit-0.2.7/goosebit/api/v1/devices/requests.py +27 -0
  8. goosebit-0.2.7/goosebit/api/v1/devices/routes.py +111 -0
  9. goosebit-0.2.7/goosebit/api/v1/download/routes.py +33 -0
  10. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/v1/rollouts/routes.py +5 -4
  11. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/v1/routes.py +2 -1
  12. goosebit-0.2.7/goosebit/api/v1/settings/routes.py +14 -0
  13. goosebit-0.2.7/goosebit/api/v1/settings/users/requests.py +16 -0
  14. goosebit-0.2.7/goosebit/api/v1/settings/users/responses.py +7 -0
  15. goosebit-0.2.7/goosebit/api/v1/settings/users/routes.py +56 -0
  16. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/v1/software/routes.py +18 -14
  17. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/auth/__init__.py +49 -13
  18. goosebit-0.2.7/goosebit/auth/permissions.py +80 -0
  19. goosebit-0.2.7/goosebit/db/config.py +66 -0
  20. goosebit-0.2.7/goosebit/db/migrations/models/2_20241121113728_update.py +11 -0
  21. goosebit-0.2.7/goosebit/db/migrations/models/3_20241121140210_update.py +11 -0
  22. goosebit-0.2.7/goosebit/db/migrations/models/4_20250324110331_update.py +16 -0
  23. goosebit-0.2.7/goosebit/db/migrations/models/4_20250402085235_rename_uuid_to_id.py +11 -0
  24. goosebit-0.2.7/goosebit/db/migrations/models/5_20250619090242_null_feed.py +83 -0
  25. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/db/models.py +19 -8
  26. goosebit-0.2.7/goosebit/db/pg_ssl_context.py +51 -0
  27. goosebit-0.2.7/goosebit/device_manager.py +262 -0
  28. goosebit-0.2.7/goosebit/plugins/__init__.py +32 -0
  29. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/schema/devices.py +8 -5
  30. goosebit-0.2.7/goosebit/schema/plugins.py +67 -0
  31. goosebit-0.2.7/goosebit/schema/updates.py +15 -0
  32. goosebit-0.2.7/goosebit/schema/users.py +9 -0
  33. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/settings/__init__.py +0 -3
  34. goosebit-0.2.7/goosebit/settings/schema.py +134 -0
  35. goosebit-0.2.7/goosebit/storage/__init__.py +62 -0
  36. goosebit-0.2.7/goosebit/storage/base.py +14 -0
  37. goosebit-0.2.7/goosebit/storage/filesystem.py +111 -0
  38. goosebit-0.2.7/goosebit/storage/s3.py +104 -0
  39. goosebit-0.2.7/goosebit/ui/bff/common/columns.py +50 -0
  40. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/bff/common/responses.py +1 -0
  41. goosebit-0.2.7/goosebit/ui/bff/devices/device/routes.py +17 -0
  42. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/bff/devices/requests.py +1 -0
  43. goosebit-0.2.7/goosebit/ui/bff/devices/routes.py +129 -0
  44. goosebit-0.2.7/goosebit/ui/bff/download/__init__.py +1 -0
  45. goosebit-0.2.7/goosebit/ui/bff/download/routes.py +33 -0
  46. goosebit-0.2.7/goosebit/ui/bff/rollouts/routes.py +82 -0
  47. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/bff/routes.py +2 -1
  48. goosebit-0.2.7/goosebit/ui/bff/settings/routes.py +20 -0
  49. goosebit-0.2.7/goosebit/ui/bff/settings/users/responses.py +33 -0
  50. goosebit-0.2.7/goosebit/ui/bff/settings/users/routes.py +80 -0
  51. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/bff/software/routes.py +40 -12
  52. goosebit-0.2.7/goosebit/ui/nav.py +26 -0
  53. goosebit-0.2.7/goosebit/ui/routes.py +108 -0
  54. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/static/js/devices.js +32 -24
  55. goosebit-0.2.7/goosebit/ui/static/js/login.js +39 -0
  56. goosebit-0.2.7/goosebit/ui/static/js/logs.js +10 -0
  57. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/static/js/rollouts.js +31 -30
  58. goosebit-0.2.7/goosebit/ui/static/js/settings.js +322 -0
  59. goosebit-0.2.7/goosebit/ui/static/js/setup.js +28 -0
  60. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/static/js/software.js +127 -121
  61. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/static/js/util.js +25 -4
  62. goosebit-0.2.7/goosebit/ui/templates/__init__.py +22 -0
  63. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/templates/login.html.jinja +5 -0
  64. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/templates/nav.html.jinja +13 -5
  65. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/templates/rollouts.html.jinja +4 -22
  66. goosebit-0.2.7/goosebit/ui/templates/settings.html.jinja +88 -0
  67. goosebit-0.2.7/goosebit/ui/templates/setup.html.jinja +71 -0
  68. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/templates/software.html.jinja +0 -11
  69. goosebit-0.2.7/goosebit/updater/__init__.py +1 -0
  70. goosebit-0.2.7/goosebit/updater/controller/__init__.py +1 -0
  71. goosebit-0.2.7/goosebit/updater/controller/v1/__init__.py +1 -0
  72. goosebit-0.2.7/goosebit/updater/controller/v1/routes.py +237 -0
  73. goosebit-0.2.7/goosebit/updater/routes.py +100 -0
  74. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/updates/__init__.py +24 -31
  75. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/updates/swdesc.py +15 -8
  76. goosebit-0.2.7/goosebit/users/__init__.py +63 -0
  77. goosebit-0.2.7/goosebit/util/__init__.py +0 -0
  78. goosebit-0.2.7/goosebit/util/path.py +42 -0
  79. goosebit-0.2.7/goosebit/util/version.py +92 -0
  80. goosebit-0.2.7/pyproject.toml +84 -0
  81. goosebit-0.2.5/PKG-INFO +0 -189
  82. goosebit-0.2.5/README.md +0 -156
  83. goosebit-0.2.5/goosebit/api/v1/devices/device/routes.py +0 -33
  84. goosebit-0.2.5/goosebit/api/v1/devices/requests.py +0 -7
  85. goosebit-0.2.5/goosebit/api/v1/devices/routes.py +0 -51
  86. goosebit-0.2.5/goosebit/api/v1/download/routes.py +0 -22
  87. goosebit-0.2.5/goosebit/db/config.py +0 -10
  88. goosebit-0.2.5/goosebit/realtime/logs.py +0 -42
  89. goosebit-0.2.5/goosebit/realtime/routes.py +0 -13
  90. goosebit-0.2.5/goosebit/settings/schema.py +0 -88
  91. goosebit-0.2.5/goosebit/ui/bff/devices/routes.py +0 -126
  92. goosebit-0.2.5/goosebit/ui/bff/download/routes.py +0 -22
  93. goosebit-0.2.5/goosebit/ui/bff/rollouts/routes.py +0 -54
  94. goosebit-0.2.5/goosebit/ui/nav.py +0 -16
  95. goosebit-0.2.5/goosebit/ui/routes.py +0 -55
  96. goosebit-0.2.5/goosebit/ui/static/js/login.js +0 -23
  97. goosebit-0.2.5/goosebit/ui/static/js/logs.js +0 -25
  98. goosebit-0.2.5/goosebit/ui/templates/__init__.py +0 -13
  99. goosebit-0.2.5/goosebit/updater/controller/v1/routes.py +0 -195
  100. goosebit-0.2.5/goosebit/updater/manager.py +0 -325
  101. goosebit-0.2.5/goosebit/updater/routes.py +0 -25
  102. goosebit-0.2.5/pyproject.toml +0 -74
  103. {goosebit-0.2.5 → goosebit-0.2.7}/LICENSE +0 -0
  104. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/__main__.py +0 -0
  105. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/__init__.py +0 -0
  106. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/responses.py +0 -0
  107. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/routes.py +0 -0
  108. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/telemetry/__init__.py +0 -0
  109. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/telemetry/prometheus/__init__.py +0 -0
  110. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/telemetry/prometheus/readers.py +0 -0
  111. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/telemetry/prometheus/routes.py +0 -0
  112. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/telemetry/routes.py +0 -0
  113. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/v1/__init__.py +0 -0
  114. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/v1/devices/__init__.py +0 -0
  115. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/v1/devices/device/__init__.py +0 -0
  116. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/v1/devices/responses.py +0 -0
  117. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/v1/download/__init__.py +0 -0
  118. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/v1/rollouts/__init__.py +0 -0
  119. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/v1/rollouts/requests.py +0 -0
  120. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/v1/rollouts/responses.py +0 -0
  121. {goosebit-0.2.5/goosebit/api/v1/software → goosebit-0.2.7/goosebit/api/v1/settings}/__init__.py +0 -0
  122. {goosebit-0.2.5/goosebit/realtime → goosebit-0.2.7/goosebit/api/v1/settings/users}/__init__.py +0 -0
  123. {goosebit-0.2.5/goosebit/ui → goosebit-0.2.7/goosebit/api/v1/software}/__init__.py +0 -0
  124. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/v1/software/requests.py +0 -0
  125. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/api/v1/software/responses.py +0 -0
  126. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/db/__init__.py +0 -0
  127. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/db/migrations/models/0_20240830054046_init.py +0 -0
  128. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/db/migrations/models/1_20241109151811_update.py +0 -0
  129. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/schema/__init__.py +0 -0
  130. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/schema/rollouts.py +0 -0
  131. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/schema/software.py +0 -0
  132. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/settings/const.py +0 -0
  133. {goosebit-0.2.5/goosebit/ui/bff → goosebit-0.2.7/goosebit/ui}/__init__.py +0 -0
  134. {goosebit-0.2.5/goosebit/ui/bff/devices → goosebit-0.2.7/goosebit/ui/bff}/__init__.py +0 -0
  135. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/bff/common/__init__.py +0 -0
  136. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/bff/common/requests.py +0 -0
  137. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/bff/common/util.py +0 -0
  138. {goosebit-0.2.5/goosebit/ui/bff/rollouts → goosebit-0.2.7/goosebit/ui/bff/devices}/__init__.py +0 -0
  139. {goosebit-0.2.5/goosebit/ui/bff/download → goosebit-0.2.7/goosebit/ui/bff/devices/device}/__init__.py +0 -0
  140. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/bff/devices/responses.py +0 -0
  141. {goosebit-0.2.5/goosebit/ui/bff/software → goosebit-0.2.7/goosebit/ui/bff/rollouts}/__init__.py +0 -0
  142. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/bff/rollouts/responses.py +0 -0
  143. {goosebit-0.2.5/goosebit/updater → goosebit-0.2.7/goosebit/ui/bff/settings}/__init__.py +0 -0
  144. {goosebit-0.2.5/goosebit/updater/controller → goosebit-0.2.7/goosebit/ui/bff/settings/users}/__init__.py +0 -0
  145. {goosebit-0.2.5/goosebit/updater/controller/v1 → goosebit-0.2.7/goosebit/ui/bff/software}/__init__.py +0 -0
  146. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/bff/software/responses.py +0 -0
  147. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/static/__init__.py +0 -0
  148. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/static/favicon.ico +0 -0
  149. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/static/favicon.svg +0 -0
  150. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/static/svg/goosebit-logo.svg +0 -0
  151. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/templates/devices.html.jinja +0 -0
  152. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/ui/templates/logs.html.jinja +0 -0
  153. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/updater/controller/routes.py +0 -0
  154. {goosebit-0.2.5 → goosebit-0.2.7}/goosebit/updater/controller/v1/schema.py +0 -0
@@ -0,0 +1,280 @@
1
+ Metadata-Version: 2.3
2
+ Name: goosebit
3
+ Version: 0.2.7
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](https://localhost)
62
+
63
+ [docker/demo/docker-compose.yml]: https://github.com/UpstreamDataInc/goosebit/blob/master/docker/demo/docker-compose.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](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](https://localhost)
29
+
30
+ [docker/demo/docker-compose.yml]: https://github.com/UpstreamDataInc/goosebit/blob/master/docker/demo/docker-compose.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](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.
@@ -13,13 +13,14 @@ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor as Instrum
13
13
  from starlette.exceptions import HTTPException as StarletteHTTPException
14
14
  from tortoise.exceptions import ValidationError
15
15
 
16
- from goosebit import api, db, realtime, ui, updater
17
- from goosebit.api.telemetry import metrics
16
+ from goosebit import api, db, plugins, ui, updater
18
17
  from goosebit.auth import get_user_from_request, login_user, redirect_if_authenticated
19
- from goosebit.settings import config
18
+ from goosebit.device_manager import DeviceManager
19
+ from goosebit.settings import PWD_CXT, config
20
20
  from goosebit.ui.nav import nav
21
21
  from goosebit.ui.static import static
22
22
  from goosebit.ui.templates import templates
23
+ from goosebit.users import create_initial_user
23
24
 
24
25
  logger = getLogger(__name__)
25
26
 
@@ -29,7 +30,9 @@ async def lifespan(_: FastAPI):
29
30
  db_ready = await db.init()
30
31
  if not db_ready:
31
32
  logger.exception("DB does not exist, try running `poetry run aerich upgrade`.")
32
- await metrics.init()
33
+
34
+ logger.debug(f"Initialized storage backend: {config.storage.backend}")
35
+
33
36
  if db_ready:
34
37
  yield
35
38
  await db.close()
@@ -57,10 +60,30 @@ app = FastAPI(
57
60
  app.include_router(updater.router)
58
61
  app.include_router(ui.router)
59
62
  app.include_router(api.router)
60
- app.include_router(realtime.router)
61
63
  app.mount("/static", static, name="static")
62
64
  Instrumentor.instrument_app(app)
63
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
+
64
87
 
65
88
  # Custom exception handler for Tortoise ValidationError
66
89
  @app.exception_handler(ValidationError)
@@ -105,7 +128,18 @@ async def login_get(request: Request):
105
128
 
106
129
  @app.post("/login", tags=["login"])
107
130
  async def login_post(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
108
- 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"}
109
143
 
110
144
 
111
145
  @app.get("/logout", include_in_schema=False)
@@ -115,7 +149,7 @@ async def logout(request: Request):
115
149
  return resp
116
150
 
117
151
 
118
- @app.get("/docs")
152
+ @app.get("/docs", include_in_schema=False)
119
153
  async def swagger_docs(request: Request):
120
154
  return get_swagger_ui_html(
121
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)
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class DevicesDeleteRequest(BaseModel):
7
+ devices: list[str]
8
+
9
+
10
+ class DevicesPatchRequest(BaseModel):
11
+ devices: list[str]
12
+ software: str | None = None
13
+ name: str | None = None
14
+ pinned: bool | None = None
15
+ feed: str | None = None
16
+ force_update: bool | None = None
17
+ auth_token: str | None = None
18
+
19
+
20
+ class DevicesPutRequest(BaseModel):
21
+ devices: list[str]
22
+ software: str | None = None
23
+ name: str | None = None
24
+ pinned: bool | None = None
25
+ feed: str | None = None
26
+ force_update: bool | None = None
27
+ auth_token: str | None = None