fiofleet 0.5.0__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.
fiofleet-0.5.0/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 fiofleet contributors
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.4
2
+ Name: fiofleet
3
+ Version: 0.5.0
4
+ Summary: Bulk fleet operations for Foundries.io devices
5
+ Author: fiofleet contributors
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/gabbuman/fiofleet
8
+ Project-URL: Repository, https://github.com/gabbuman/fiofleet
9
+ Project-URL: Issues, https://github.com/gabbuman/fiofleet/issues
10
+ Keywords: foundries,fioctl,fleet,iot,wireguard,ota
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: click>=8.1
15
+ Requires-Dist: requests>=2.31
16
+ Requires-Dist: paramiko>=3
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=7; extra == "dev"
19
+ Requires-Dist: responses>=0.25; extra == "dev"
20
+ Dynamic: license-file
21
+
22
+ # fiofleet
23
+
24
+ Bulk fleet operations for [Foundries.io](https://foundries.io) devices.
25
+
26
+ `fioctl` is great for single-device work. **fiofleet** is designed for when you have a large fleet of
27
+ devices, and want to enable/disable wireguard vpn and run ssh commands remotely en masse.
28
+ It's a thin, scriptable layer over the Foundries OTA API (and, optionally, `fioctl`).
29
+
30
+ ## Features
31
+
32
+ - **Device inventory** — list/show devices, filter by tag or group, with
33
+ online/offline detection.
34
+ - **OTA update reports** — for a tag/group, show each device's last update
35
+ broken down by stage (download → install), exactly *which stage failed* and
36
+ the error the device reported, plus a fleet-level pass/fail summary. Drill
37
+ into a single device's full update timeline with `ota stages`.
38
+ - **WireGuard fleet management** — enable/disable/status across many devices at
39
+ once, and *wait* until the platform confirms each device is a live VPN peer.
40
+ Works through the config API directly, so `fioctl` is **not required**.
41
+ - **Fan-out SSH/exec** — run a command (or open a shell) across a tag/group in
42
+ parallel, and collect the results as JSON (`--json`) so you can drive scripts
43
+ off them. Runs from *any* machine: fiofleet hops through your Factory
44
+ WireGuard server (a bastion) and SSHes to the devices from there, so the
45
+ operator doesn't need to be on the VPN.
46
+
47
+ ## Install
48
+
49
+ ```
50
+ pip install fiofleet
51
+ ```
52
+
53
+ Requires Python 3.9+. `fioctl` is optional — only needed if you pass
54
+ `--via-fioctl` to the WireGuard commands.
55
+
56
+ ## Setup
57
+
58
+ ```
59
+ fiofleet config set
60
+ # prompts for API token, factory name (and optionally an API base URL)
61
+ ```
62
+
63
+ Or via env vars (these override the saved config):
64
+
65
+ ```
66
+ export FOUNDRIES_API_TOKEN=...
67
+ export FOUNDRIES_FACTORY=my-factory
68
+ ```
69
+
70
+ Get your API token at https://app.foundries.io/settings/tokens/.
71
+
72
+ To use `ssh`/`exec` from a machine that isn't on the device VPN, point fiofleet
73
+ at your Factory WireGuard server (the bastion it hops through):
74
+
75
+ ```
76
+ fiofleet config set-server --server vpn.example.com --server-user ops
77
+ # add --device-password ... if your devices use password (sshpass) auth
78
+ ```
79
+
80
+ **Connecting to the server with an OpenSSH key** (e.g. an Azure VM running the
81
+ Factory WireGuard server — the same `.pem`/OpenSSH key you'd use for `ssh -i`):
82
+
83
+ ```
84
+ fiofleet config set-server \
85
+ --server my-fio-vpn.eastus.cloudapp.azure.com \
86
+ --server-user azureuser \
87
+ --server-key ~/.ssh/azure-fio-vpn.pem \
88
+ --device-user fio
89
+ # device auth happens on the server: add --device-password ... if devices need
90
+ # sshpass, otherwise omit (the server's own key reaches the devices).
91
+ ```
92
+
93
+ `--server-key` is passed through to paramiko — anything `ssh -i` would accept
94
+ works (`.pem`, `~/.ssh/id_ed25519`, …). Omit it to fall back to your SSH agent
95
+ / default keys, then password.
96
+
97
+ ## Commands
98
+
99
+ ```
100
+ # Factories your token can see
101
+ fiofleet factories
102
+
103
+ # Devices
104
+ fiofleet devices list
105
+ fiofleet devices list --tag prod-eu --online-only
106
+ fiofleet devices show my-device-01
107
+
108
+ # OTA update reports
109
+ fiofleet ota report --tag prod-eu # last update per device + fleet summary
110
+ fiofleet ota report --tag prod-eu --failed-only # just the devices that failed
111
+ fiofleet ota report --tag prod-eu --json # structured, for dashboards/CI
112
+ fiofleet ota report --tag prod-eu --target lmp-124 # every device that attempted target lmp-124
113
+ fiofleet ota report --tag prod-eu --target lmp-124 --failed-only # …and which of them failed
114
+ fiofleet ota stages my-device-01 # full stage timeline for one device
115
+
116
+ # WireGuard
117
+ fiofleet wg enable my-device-01
118
+ fiofleet wg enable --tag prod-eu --parallel 20 # enable + wait until applied
119
+ fiofleet wg status --tag prod-eu
120
+ fiofleet wg disable --tag prod-eu
121
+ fiofleet wg enable my-device-01 --via-fioctl # delegate to fioctl instead
122
+
123
+ # SSH / exec (hops through the configured WireGuard-server bastion by default)
124
+ fiofleet ssh my-device-01
125
+ fiofleet exec "uptime" --tag prod-eu
126
+ fiofleet exec "systemctl is-active aktualizr-lite" --tag prod-eu
127
+ fiofleet exec "fiotest" --tag prod-eu --json # collect results as JSON
128
+ fiofleet exec "reboot" --tag prod-eu --strict # non-zero exit if any device fails
129
+ fiofleet exec "uptime" --tag prod-eu --server vpn.example.com # ad-hoc bastion
130
+ fiofleet exec "uptime" --name dev-01 --direct # already on the VPN; skip the hop
131
+ ```
132
+
133
+ A typical `ota report` looks like:
134
+
135
+ ```
136
+ DEVICE RESULT FAILED@ TARGET WHEN
137
+ dev-us-01 FAILED install raspberrypi4-64-lmp-124 2026-05-21T08:14:02Z
138
+ -> install: Installation failed: ostree pull error: Server returned HTTP 500
139
+ dev-eu-02 IN_PROGRESS - raspberrypi4-64-lmp-124 2026-05-21T08:13:55Z
140
+ dev-eu-01 SUCCESS - raspberrypi4-64-lmp-124 2026-05-20T22:01:10Z
141
+
142
+ Fleet summary (3 device(s)):
143
+ FAILED 1 (install: 1)
144
+ IN_PROGRESS 1
145
+ SUCCESS 1
146
+ ```
147
+
148
+ ## How OTA reporting works
149
+
150
+ Each device posts a stream of [libaktualizr](https://docs.foundries.io/latest/reference-manual/ota/ota.html)
151
+ report events to the device-gateway as it updates
152
+ (`EcuDownloadStarted`/`Completed`, `EcuInstallationStarted`/`Applied`/`Completed`).
153
+ fiofleet reads that stream from the OTA API's per-device `updates` view — the
154
+ same history `fioctl` shows — and collapses it into two operator-facing stages,
155
+ **download** and **install**. A stage that reports `success=false` marks the
156
+ update `FAILED` at that stage and surfaces the `details` the device attached;
157
+ an update that reached `EcuInstallationApplied` but not `…Completed` is
158
+ `IN_PROGRESS` (applied, awaiting the post-reboot confirmation). No agent on the
159
+ device is required — it's all read from the API.
160
+
161
+ Pass `--target X` to scope the report to one rollout: each device's update
162
+ history is searched (newest first) for an update whose target/version contains
163
+ `X`, and only devices that *actually attempted* it appear in the output —
164
+ their most recent attempt, with the same SUCCESS/FAILED/IN_PROGRESS verdict
165
+ and failing-stage detail.
166
+
167
+ ## How WireGuard works here
168
+
169
+ Enabling WireGuard on a device writes a `wireguard-client` config entry (the same
170
+ one `fioctl devices config wireguard enable` writes). The platform assigns the
171
+ device a `10.42.42.x` address; the device applies the change on its next check-in.
172
+
173
+ `fiofleet wg status` / `--wait` poll the Foundries
174
+ [`wireguard-ips`](https://docs.foundries.io/latest/reference-manual/remote-access/wireguard.html)
175
+ view — the same one the [Factory WireGuard server](https://github.com/foundriesio/factory-wireguard-server)
176
+ reads to learn its peers — so "applied" means the platform actually considers the
177
+ device a live VPN peer, not just that a config was queued.
178
+
179
+ ## How ssh/exec reach a device (the jump-host model)
180
+
181
+ A route to a device only exists on the Factory WireGuard server — it's peered
182
+ into the VPN and keeps `/etc/hosts` in sync with device VPN IPs. Rather than
183
+ require you to be on that box, fiofleet treats it as a **bastion**: it opens an
184
+ SSH connection to the server (via [paramiko](https://www.paramiko.org/)) and
185
+ runs the device `ssh` *there*. So:
186
+
187
+ ```
188
+ your laptop ──SSH──► WireGuard server ──SSH──► device (10.42.42.x)
189
+ (anywhere) (on the VPN) (fio@…)
190
+ ```
191
+
192
+ Device authentication therefore happens on the server — using the server's key,
193
+ or a password via `sshpass` (`--device-password`) — exactly as an admin SSHing
194
+ into the box by hand would. Configure the bastion once with
195
+ `fiofleet config set-server` (or pass `--server` ad hoc); pass `--direct` to
196
+ skip it when you're already on the VPN. fiofleet runs `ssh`; it doesn't manage
197
+ the tunnel itself.
198
+
199
+ ## Development
200
+
201
+ ```
202
+ pip install -e ".[dev]"
203
+ pytest
204
+ ```
205
+
206
+ A local end-to-end harness (real Pi WireGuard server + containerised devices) lives
207
+ in [`harness/`](harness/README.md).
208
+
209
+ ## License
210
+
211
+ Apache 2.0
@@ -0,0 +1,190 @@
1
+ # fiofleet
2
+
3
+ Bulk fleet operations for [Foundries.io](https://foundries.io) devices.
4
+
5
+ `fioctl` is great for single-device work. **fiofleet** is designed for when you have a large fleet of
6
+ devices, and want to enable/disable wireguard vpn and run ssh commands remotely en masse.
7
+ It's a thin, scriptable layer over the Foundries OTA API (and, optionally, `fioctl`).
8
+
9
+ ## Features
10
+
11
+ - **Device inventory** — list/show devices, filter by tag or group, with
12
+ online/offline detection.
13
+ - **OTA update reports** — for a tag/group, show each device's last update
14
+ broken down by stage (download → install), exactly *which stage failed* and
15
+ the error the device reported, plus a fleet-level pass/fail summary. Drill
16
+ into a single device's full update timeline with `ota stages`.
17
+ - **WireGuard fleet management** — enable/disable/status across many devices at
18
+ once, and *wait* until the platform confirms each device is a live VPN peer.
19
+ Works through the config API directly, so `fioctl` is **not required**.
20
+ - **Fan-out SSH/exec** — run a command (or open a shell) across a tag/group in
21
+ parallel, and collect the results as JSON (`--json`) so you can drive scripts
22
+ off them. Runs from *any* machine: fiofleet hops through your Factory
23
+ WireGuard server (a bastion) and SSHes to the devices from there, so the
24
+ operator doesn't need to be on the VPN.
25
+
26
+ ## Install
27
+
28
+ ```
29
+ pip install fiofleet
30
+ ```
31
+
32
+ Requires Python 3.9+. `fioctl` is optional — only needed if you pass
33
+ `--via-fioctl` to the WireGuard commands.
34
+
35
+ ## Setup
36
+
37
+ ```
38
+ fiofleet config set
39
+ # prompts for API token, factory name (and optionally an API base URL)
40
+ ```
41
+
42
+ Or via env vars (these override the saved config):
43
+
44
+ ```
45
+ export FOUNDRIES_API_TOKEN=...
46
+ export FOUNDRIES_FACTORY=my-factory
47
+ ```
48
+
49
+ Get your API token at https://app.foundries.io/settings/tokens/.
50
+
51
+ To use `ssh`/`exec` from a machine that isn't on the device VPN, point fiofleet
52
+ at your Factory WireGuard server (the bastion it hops through):
53
+
54
+ ```
55
+ fiofleet config set-server --server vpn.example.com --server-user ops
56
+ # add --device-password ... if your devices use password (sshpass) auth
57
+ ```
58
+
59
+ **Connecting to the server with an OpenSSH key** (e.g. an Azure VM running the
60
+ Factory WireGuard server — the same `.pem`/OpenSSH key you'd use for `ssh -i`):
61
+
62
+ ```
63
+ fiofleet config set-server \
64
+ --server my-fio-vpn.eastus.cloudapp.azure.com \
65
+ --server-user azureuser \
66
+ --server-key ~/.ssh/azure-fio-vpn.pem \
67
+ --device-user fio
68
+ # device auth happens on the server: add --device-password ... if devices need
69
+ # sshpass, otherwise omit (the server's own key reaches the devices).
70
+ ```
71
+
72
+ `--server-key` is passed through to paramiko — anything `ssh -i` would accept
73
+ works (`.pem`, `~/.ssh/id_ed25519`, …). Omit it to fall back to your SSH agent
74
+ / default keys, then password.
75
+
76
+ ## Commands
77
+
78
+ ```
79
+ # Factories your token can see
80
+ fiofleet factories
81
+
82
+ # Devices
83
+ fiofleet devices list
84
+ fiofleet devices list --tag prod-eu --online-only
85
+ fiofleet devices show my-device-01
86
+
87
+ # OTA update reports
88
+ fiofleet ota report --tag prod-eu # last update per device + fleet summary
89
+ fiofleet ota report --tag prod-eu --failed-only # just the devices that failed
90
+ fiofleet ota report --tag prod-eu --json # structured, for dashboards/CI
91
+ fiofleet ota report --tag prod-eu --target lmp-124 # every device that attempted target lmp-124
92
+ fiofleet ota report --tag prod-eu --target lmp-124 --failed-only # …and which of them failed
93
+ fiofleet ota stages my-device-01 # full stage timeline for one device
94
+
95
+ # WireGuard
96
+ fiofleet wg enable my-device-01
97
+ fiofleet wg enable --tag prod-eu --parallel 20 # enable + wait until applied
98
+ fiofleet wg status --tag prod-eu
99
+ fiofleet wg disable --tag prod-eu
100
+ fiofleet wg enable my-device-01 --via-fioctl # delegate to fioctl instead
101
+
102
+ # SSH / exec (hops through the configured WireGuard-server bastion by default)
103
+ fiofleet ssh my-device-01
104
+ fiofleet exec "uptime" --tag prod-eu
105
+ fiofleet exec "systemctl is-active aktualizr-lite" --tag prod-eu
106
+ fiofleet exec "fiotest" --tag prod-eu --json # collect results as JSON
107
+ fiofleet exec "reboot" --tag prod-eu --strict # non-zero exit if any device fails
108
+ fiofleet exec "uptime" --tag prod-eu --server vpn.example.com # ad-hoc bastion
109
+ fiofleet exec "uptime" --name dev-01 --direct # already on the VPN; skip the hop
110
+ ```
111
+
112
+ A typical `ota report` looks like:
113
+
114
+ ```
115
+ DEVICE RESULT FAILED@ TARGET WHEN
116
+ dev-us-01 FAILED install raspberrypi4-64-lmp-124 2026-05-21T08:14:02Z
117
+ -> install: Installation failed: ostree pull error: Server returned HTTP 500
118
+ dev-eu-02 IN_PROGRESS - raspberrypi4-64-lmp-124 2026-05-21T08:13:55Z
119
+ dev-eu-01 SUCCESS - raspberrypi4-64-lmp-124 2026-05-20T22:01:10Z
120
+
121
+ Fleet summary (3 device(s)):
122
+ FAILED 1 (install: 1)
123
+ IN_PROGRESS 1
124
+ SUCCESS 1
125
+ ```
126
+
127
+ ## How OTA reporting works
128
+
129
+ Each device posts a stream of [libaktualizr](https://docs.foundries.io/latest/reference-manual/ota/ota.html)
130
+ report events to the device-gateway as it updates
131
+ (`EcuDownloadStarted`/`Completed`, `EcuInstallationStarted`/`Applied`/`Completed`).
132
+ fiofleet reads that stream from the OTA API's per-device `updates` view — the
133
+ same history `fioctl` shows — and collapses it into two operator-facing stages,
134
+ **download** and **install**. A stage that reports `success=false` marks the
135
+ update `FAILED` at that stage and surfaces the `details` the device attached;
136
+ an update that reached `EcuInstallationApplied` but not `…Completed` is
137
+ `IN_PROGRESS` (applied, awaiting the post-reboot confirmation). No agent on the
138
+ device is required — it's all read from the API.
139
+
140
+ Pass `--target X` to scope the report to one rollout: each device's update
141
+ history is searched (newest first) for an update whose target/version contains
142
+ `X`, and only devices that *actually attempted* it appear in the output —
143
+ their most recent attempt, with the same SUCCESS/FAILED/IN_PROGRESS verdict
144
+ and failing-stage detail.
145
+
146
+ ## How WireGuard works here
147
+
148
+ Enabling WireGuard on a device writes a `wireguard-client` config entry (the same
149
+ one `fioctl devices config wireguard enable` writes). The platform assigns the
150
+ device a `10.42.42.x` address; the device applies the change on its next check-in.
151
+
152
+ `fiofleet wg status` / `--wait` poll the Foundries
153
+ [`wireguard-ips`](https://docs.foundries.io/latest/reference-manual/remote-access/wireguard.html)
154
+ view — the same one the [Factory WireGuard server](https://github.com/foundriesio/factory-wireguard-server)
155
+ reads to learn its peers — so "applied" means the platform actually considers the
156
+ device a live VPN peer, not just that a config was queued.
157
+
158
+ ## How ssh/exec reach a device (the jump-host model)
159
+
160
+ A route to a device only exists on the Factory WireGuard server — it's peered
161
+ into the VPN and keeps `/etc/hosts` in sync with device VPN IPs. Rather than
162
+ require you to be on that box, fiofleet treats it as a **bastion**: it opens an
163
+ SSH connection to the server (via [paramiko](https://www.paramiko.org/)) and
164
+ runs the device `ssh` *there*. So:
165
+
166
+ ```
167
+ your laptop ──SSH──► WireGuard server ──SSH──► device (10.42.42.x)
168
+ (anywhere) (on the VPN) (fio@…)
169
+ ```
170
+
171
+ Device authentication therefore happens on the server — using the server's key,
172
+ or a password via `sshpass` (`--device-password`) — exactly as an admin SSHing
173
+ into the box by hand would. Configure the bastion once with
174
+ `fiofleet config set-server` (or pass `--server` ad hoc); pass `--direct` to
175
+ skip it when you're already on the VPN. fiofleet runs `ssh`; it doesn't manage
176
+ the tunnel itself.
177
+
178
+ ## Development
179
+
180
+ ```
181
+ pip install -e ".[dev]"
182
+ pytest
183
+ ```
184
+
185
+ A local end-to-end harness (real Pi WireGuard server + containerised devices) lives
186
+ in [`harness/`](harness/README.md).
187
+
188
+ ## License
189
+
190
+ Apache 2.0
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "fiofleet"
7
+ version = "0.5.0"
8
+ description = "Bulk fleet operations for Foundries.io devices"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [{ name = "fiofleet contributors" }]
13
+ keywords = ["foundries", "fioctl", "fleet", "iot", "wireguard", "ota"]
14
+ dependencies = [
15
+ "click>=8.1",
16
+ "requests>=2.31",
17
+ "paramiko>=3",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/gabbuman/fiofleet"
22
+ Repository = "https://github.com/gabbuman/fiofleet"
23
+ Issues = "https://github.com/gabbuman/fiofleet/issues"
24
+
25
+ [project.optional-dependencies]
26
+ dev = ["pytest>=7", "responses>=0.25"]
27
+
28
+ [project.scripts]
29
+ fiofleet = "fiofleet.cli:cli"
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["src"]
33
+
34
+ [tool.pytest.ini_options]
35
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """fiofleet — bulk fleet operations for Foundries.io devices."""
2
+
3
+ __version__ = "0.5.0"