cyclo-manager 0.4.0.dev0__tar.gz → 1.0.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.
- {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0}/PKG-INFO +26 -20
- {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0}/README.md +25 -19
- {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0}/cyclo_host_agent/routers/repos.py +19 -5
- {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0}/cyclo_host_agent/routers/update.py +17 -14
- {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0}/cyclo_manager.egg-info/PKG-INFO +26 -20
- {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0}/cyclo_manager_cli/__init__.py +1 -1
- {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0}/cyclo_manager_cli/cli.py +157 -30
- {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0}/cyclo_manager_cli/config/config.yml +4 -1
- {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0}/cyclo_manager_cli/docker/docker-compose.yml +2 -2
- {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0}/pyproject.toml +1 -1
- {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0}/cyclo_host_agent/__init__.py +0 -0
- {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0}/cyclo_host_agent/main.py +0 -0
- {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0}/cyclo_host_agent/models.py +0 -0
- {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0}/cyclo_host_agent/routers/__init__.py +0 -0
- {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0}/cyclo_manager.egg-info/SOURCES.txt +0 -0
- {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0}/cyclo_manager.egg-info/dependency_links.txt +0 -0
- {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0}/cyclo_manager.egg-info/entry_points.txt +0 -0
- {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0}/cyclo_manager.egg-info/requires.txt +0 -0
- {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0}/cyclo_manager.egg-info/top_level.txt +0 -0
- {cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cyclo-manager
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: cyclo_manager CLI: pip-installable launcher for cyclo_manager server and UI containers. Run 'cyclo_manager up' to start Docker stack.
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -18,7 +18,7 @@ Requires-Dist: ruff; extra == "dev"
|
|
|
18
18
|
**Console commands:** `cyclo_manager` (underscore), `cyclo-manager` (hyphen)
|
|
19
19
|
**Host agent:** `cyclo_host_agent` (installed by the package, run via systemd)
|
|
20
20
|
|
|
21
|
-
This package is the **pip-installable launcher** for the cyclo_manager stack on a robot host. It:
|
|
21
|
+
This package is the **pip-installable launcher** for the cyclo_manager stack on a robot host. It is intended for production robots that run the pre-built Docker images. It:
|
|
22
22
|
|
|
23
23
|
1. Runs **`docker compose`** against a **bundled** [`cyclo_manager_cli/docker/docker-compose.yml`](cyclo_manager_cli/docker/docker-compose.yml) to start the **API** and **web UI** containers.
|
|
24
24
|
2. Installs and maintains **`cyclo_host_agent`** as a **systemd** service for host-level operations (git repo updates, `cyclo_manager` package update from the UI).
|
|
@@ -50,11 +50,12 @@ For the full monorepo (API source, UI source, dev compose), see the [repository
|
|
|
50
50
|
|
|
51
51
|
## Prerequisites
|
|
52
52
|
|
|
53
|
-
- **Docker** with **Compose v2** (`docker compose
|
|
53
|
+
- **Docker** with **Compose v2** (`docker compose`; the legacy `docker-compose` binary alone is not enough)
|
|
54
54
|
- **Python 3.10+**
|
|
55
|
-
-
|
|
55
|
+
- **systemd**: required for the managed `cyclo_host_agent.service`
|
|
56
|
+
- **`sudo`** — required on normal-user hosts to install the host agent systemd unit, socket directory permissions, and sudoers rules
|
|
56
57
|
- For **`cyclo_manager update`:** **`pip`** or **`pip3`** on `PATH`
|
|
57
|
-
- **
|
|
58
|
+
- **Root shell handling** — `cyclo_manager up` rejects direct root execution when a normal login user exists. On root-only devices, the host agent is installed as root.
|
|
58
59
|
- **Agent sockets** on the host under `/var/run/robotis/agent_sockets/` (robot containers and host agent). The bundled Compose file bind-mounts this tree into the API container as `/agents/`.
|
|
59
60
|
|
|
60
61
|
---
|
|
@@ -94,6 +95,8 @@ python3 -m cyclo_manager_cli.cli up
|
|
|
94
95
|
| `cyclo_manager update --no-pull` | Same as `update`, but skip image pull (use locally cached images). |
|
|
95
96
|
| `cyclo_manager --help` | Subcommand overview |
|
|
96
97
|
|
|
98
|
+
The CLI also has an internal `refresh-host-agent` subcommand. It is used by the sudoers rule and should normally not be invoked by users directly.
|
|
99
|
+
|
|
97
100
|
**`down` vs `update`:** `down` removes the host agent entirely. `update` only stops Docker containers, upgrades the pip package, restarts the stack, and refreshes the host agent — it does **not** call `cyclo_manager down`.
|
|
98
101
|
|
|
99
102
|
---
|
|
@@ -120,16 +123,21 @@ The pip package installs a small **FastAPI** server that listens on a **Unix dom
|
|
|
120
123
|
|------|------|
|
|
121
124
|
| Socket | `/var/run/robotis/agent_sockets/host/host_agent.sock` |
|
|
122
125
|
| systemd unit | `cyclo_host_agent.service` |
|
|
123
|
-
| sudoers | `/etc/sudoers.d/cyclo_manager` (`cp`, `udevadm` for container setup scripts) |
|
|
126
|
+
| sudoers | `/etc/sudoers.d/cyclo_manager` (`cp`, `udevadm` for container setup scripts; `refresh-host-agent` CLI subcommand for package-update refresh; skipped on root-only devices) |
|
|
124
127
|
|
|
125
128
|
The API container reaches it at `/agents/host/host_agent.sock` (see bundled `config.yml`).
|
|
126
129
|
|
|
127
130
|
**Responsibilities:**
|
|
128
131
|
|
|
129
|
-
- List
|
|
130
|
-
-
|
|
132
|
+
- List managed **ROBOTIS-GIT** git repositories in the configured workspace.
|
|
133
|
+
- Check update availability from remote tags and local `package.xml` versions.
|
|
134
|
+
- Update repos on allowed branches (`main`, `jazzy`) using stash or reset workflows.
|
|
135
|
+
- Stop/start a repo's `docker/container.sh` helper during an update when requested by the UI.
|
|
136
|
+
- Run Cyclo Manager package updates from the UI by delegating to `cyclo_manager update`.
|
|
137
|
+
|
|
138
|
+
`cyclo_manager up` installs the `refresh-host-agent` CLI subcommand into sudoers. It is registered as `sys.executable -m cyclo_manager_cli.cli refresh-host-agent` with `SETENV`, so UI-triggered updates can pass the current `PYTHONPATH` to sudo and do not depend on `PATH`. The command is idempotent and refreshes the socket directory, sudoers file, systemd unit, and service state after package upgrades.
|
|
131
139
|
|
|
132
|
-
|
|
140
|
+
Repository scanning uses `CYCLO_HOST_AGENT_WORKSPACE` when set. Otherwise it uses the service user's home directory, except root-only devices with `/data/docker`, where `/data/docker` is used automatically. Only repositories whose `origin` remote belongs to `ROBOTIS-GIT` are returned by the repo list/update endpoints.
|
|
133
141
|
|
|
134
142
|
---
|
|
135
143
|
|
|
@@ -139,8 +147,8 @@ Defined in [`cyclo_manager_cli/docker/docker-compose.yml`](cyclo_manager_cli/doc
|
|
|
139
147
|
|
|
140
148
|
| Compose service | Container name | `cyclo_manager up` | Image (example) |
|
|
141
149
|
|-----------------|----------------|--------------------|-----------------|
|
|
142
|
-
| `cyclo_manager` | `cyclo_manager` | **Started** | `robotis/cyclo-manager:0.
|
|
143
|
-
| `ui` | `cyclo_manager_ui` | **Started** | `robotis/cyclo-manager-ui:0.
|
|
150
|
+
| `cyclo_manager` | `cyclo_manager` | **Started** | `robotis/cyclo-manager:1.0.0` |
|
|
151
|
+
| `ui` | `cyclo_manager_ui` | **Started** | `robotis/cyclo-manager-ui:1.0.0` |
|
|
144
152
|
| `rmw_zenoh` | `zenoh_daemon` | **Created only** | `robotis/zenoh-daemon:latest` |
|
|
145
153
|
| `novnc-server` | `novnc-server` | **Created only** | `robotis/novnc-server:latest` |
|
|
146
154
|
|
|
@@ -160,7 +168,7 @@ The API reads it as `CONFIG_FILE=/app/config.yml` inside the container.
|
|
|
160
168
|
|
|
161
169
|
| Key | Description |
|
|
162
170
|
|-----|-------------|
|
|
163
|
-
| **`
|
|
171
|
+
| **`supported_robot_containers`** | Robot Docker container names that can open the System page (e.g. `ai_worker`, `open_manipulator`). Each must be a key in `sockets` and cannot be `host_agent`. |
|
|
164
172
|
| **`sockets`** | Map of logical name → agent **Unix socket path as seen inside the API container** (under `/agents/...`). Include robot/service containers and `host_agent`. |
|
|
165
173
|
|
|
166
174
|
s6 **service names** are not listed in config; each in-container agent reports them at runtime.
|
|
@@ -168,20 +176,17 @@ s6 **service names** are not listed in config; each in-container agent reports t
|
|
|
168
176
|
### Example (bundled default)
|
|
169
177
|
|
|
170
178
|
```yaml
|
|
171
|
-
|
|
179
|
+
supported_robot_containers:
|
|
180
|
+
- ai_worker
|
|
181
|
+
- open_manipulator
|
|
172
182
|
|
|
173
183
|
sockets:
|
|
174
184
|
ai_worker: "/agents/ai_worker/s6_agent.sock"
|
|
185
|
+
open_manipulator: "/agents/open_manipulator/s6_agent.sock"
|
|
175
186
|
cyclo_intelligence: "/agents/cyclo_intelligence/s6_agent.sock"
|
|
176
187
|
host_agent: "/agents/host/host_agent.sock"
|
|
177
188
|
```
|
|
178
189
|
|
|
179
|
-
### Validation (API startup)
|
|
180
|
-
|
|
181
|
-
- At least one socket entry besides `host_agent`
|
|
182
|
-
- `robot_container` must exist in `sockets`
|
|
183
|
-
- All socket paths must be non-empty strings
|
|
184
|
-
|
|
185
190
|
To use a different config layout, mounts, or local source builds, use the repository’s **`docker-compose.dev.yml`** — see [Custom config and development](#custom-config-and-development).
|
|
186
191
|
|
|
187
192
|
---
|
|
@@ -212,6 +217,7 @@ So `/agents/ai_worker/s6_agent.sock` in config corresponds to the host path abov
|
|
|
212
217
|
| Variable | Set by | Purpose |
|
|
213
218
|
|----------|--------|---------|
|
|
214
219
|
| **`CYCLO_MANAGER_CONFIG_FILE`** | CLI (`up`, `down`, `update`) | Absolute path to bundled `config.yml` on the host; mounted into the API container |
|
|
220
|
+
| **`CYCLO_HOST_AGENT_WORKSPACE`** | User or CLI-generated systemd unit | Workspace scanned by `cyclo_host_agent` for managed git repositories |
|
|
215
221
|
| **`HOSTNAME`** | CLI (default: machine hostname) | Passed to API as `HOST_HOSTNAME` |
|
|
216
222
|
| **`CONFIG_FILE`** | Compose (`/app/config.yml`) | Path inside the API container |
|
|
217
223
|
| **`ROS_DOMAIN_ID`** | **Not** set by CLI | Set inside robot containers (e.g. `~/.bashrc`) so DDS matches your fleet |
|
|
@@ -253,7 +259,7 @@ Python dependencies declared in [`pyproject.toml`](pyproject.toml):
|
|
|
253
259
|
|---------|---------|
|
|
254
260
|
| **fastapi** | `cyclo_host_agent` HTTP API |
|
|
255
261
|
| **uvicorn** | `cyclo_host_agent` server |
|
|
256
|
-
| **psutil** |
|
|
262
|
+
| **psutil** | Declared package dependency for host/system utility support |
|
|
257
263
|
|
|
258
264
|
The **`cyclo_manager`** CLI itself uses only the Python standard library plus **`subprocess`** calls to **`docker compose`**, **`pip`**, **`systemctl`**, and **`sudo`**.
|
|
259
265
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
**Console commands:** `cyclo_manager` (underscore), `cyclo-manager` (hyphen)
|
|
5
5
|
**Host agent:** `cyclo_host_agent` (installed by the package, run via systemd)
|
|
6
6
|
|
|
7
|
-
This package is the **pip-installable launcher** for the cyclo_manager stack on a robot host. It:
|
|
7
|
+
This package is the **pip-installable launcher** for the cyclo_manager stack on a robot host. It is intended for production robots that run the pre-built Docker images. It:
|
|
8
8
|
|
|
9
9
|
1. Runs **`docker compose`** against a **bundled** [`cyclo_manager_cli/docker/docker-compose.yml`](cyclo_manager_cli/docker/docker-compose.yml) to start the **API** and **web UI** containers.
|
|
10
10
|
2. Installs and maintains **`cyclo_host_agent`** as a **systemd** service for host-level operations (git repo updates, `cyclo_manager` package update from the UI).
|
|
@@ -36,11 +36,12 @@ For the full monorepo (API source, UI source, dev compose), see the [repository
|
|
|
36
36
|
|
|
37
37
|
## Prerequisites
|
|
38
38
|
|
|
39
|
-
- **Docker** with **Compose v2** (`docker compose
|
|
39
|
+
- **Docker** with **Compose v2** (`docker compose`; the legacy `docker-compose` binary alone is not enough)
|
|
40
40
|
- **Python 3.10+**
|
|
41
|
-
-
|
|
41
|
+
- **systemd**: required for the managed `cyclo_host_agent.service`
|
|
42
|
+
- **`sudo`** — required on normal-user hosts to install the host agent systemd unit, socket directory permissions, and sudoers rules
|
|
42
43
|
- For **`cyclo_manager update`:** **`pip`** or **`pip3`** on `PATH`
|
|
43
|
-
- **
|
|
44
|
+
- **Root shell handling** — `cyclo_manager up` rejects direct root execution when a normal login user exists. On root-only devices, the host agent is installed as root.
|
|
44
45
|
- **Agent sockets** on the host under `/var/run/robotis/agent_sockets/` (robot containers and host agent). The bundled Compose file bind-mounts this tree into the API container as `/agents/`.
|
|
45
46
|
|
|
46
47
|
---
|
|
@@ -80,6 +81,8 @@ python3 -m cyclo_manager_cli.cli up
|
|
|
80
81
|
| `cyclo_manager update --no-pull` | Same as `update`, but skip image pull (use locally cached images). |
|
|
81
82
|
| `cyclo_manager --help` | Subcommand overview |
|
|
82
83
|
|
|
84
|
+
The CLI also has an internal `refresh-host-agent` subcommand. It is used by the sudoers rule and should normally not be invoked by users directly.
|
|
85
|
+
|
|
83
86
|
**`down` vs `update`:** `down` removes the host agent entirely. `update` only stops Docker containers, upgrades the pip package, restarts the stack, and refreshes the host agent — it does **not** call `cyclo_manager down`.
|
|
84
87
|
|
|
85
88
|
---
|
|
@@ -106,16 +109,21 @@ The pip package installs a small **FastAPI** server that listens on a **Unix dom
|
|
|
106
109
|
|------|------|
|
|
107
110
|
| Socket | `/var/run/robotis/agent_sockets/host/host_agent.sock` |
|
|
108
111
|
| systemd unit | `cyclo_host_agent.service` |
|
|
109
|
-
| sudoers | `/etc/sudoers.d/cyclo_manager` (`cp`, `udevadm` for container setup scripts) |
|
|
112
|
+
| sudoers | `/etc/sudoers.d/cyclo_manager` (`cp`, `udevadm` for container setup scripts; `refresh-host-agent` CLI subcommand for package-update refresh; skipped on root-only devices) |
|
|
110
113
|
|
|
111
114
|
The API container reaches it at `/agents/host/host_agent.sock` (see bundled `config.yml`).
|
|
112
115
|
|
|
113
116
|
**Responsibilities:**
|
|
114
117
|
|
|
115
|
-
- List
|
|
116
|
-
-
|
|
118
|
+
- List managed **ROBOTIS-GIT** git repositories in the configured workspace.
|
|
119
|
+
- Check update availability from remote tags and local `package.xml` versions.
|
|
120
|
+
- Update repos on allowed branches (`main`, `jazzy`) using stash or reset workflows.
|
|
121
|
+
- Stop/start a repo's `docker/container.sh` helper during an update when requested by the UI.
|
|
122
|
+
- Run Cyclo Manager package updates from the UI by delegating to `cyclo_manager update`.
|
|
123
|
+
|
|
124
|
+
`cyclo_manager up` installs the `refresh-host-agent` CLI subcommand into sudoers. It is registered as `sys.executable -m cyclo_manager_cli.cli refresh-host-agent` with `SETENV`, so UI-triggered updates can pass the current `PYTHONPATH` to sudo and do not depend on `PATH`. The command is idempotent and refreshes the socket directory, sudoers file, systemd unit, and service state after package upgrades.
|
|
117
125
|
|
|
118
|
-
|
|
126
|
+
Repository scanning uses `CYCLO_HOST_AGENT_WORKSPACE` when set. Otherwise it uses the service user's home directory, except root-only devices with `/data/docker`, where `/data/docker` is used automatically. Only repositories whose `origin` remote belongs to `ROBOTIS-GIT` are returned by the repo list/update endpoints.
|
|
119
127
|
|
|
120
128
|
---
|
|
121
129
|
|
|
@@ -125,8 +133,8 @@ Defined in [`cyclo_manager_cli/docker/docker-compose.yml`](cyclo_manager_cli/doc
|
|
|
125
133
|
|
|
126
134
|
| Compose service | Container name | `cyclo_manager up` | Image (example) |
|
|
127
135
|
|-----------------|----------------|--------------------|-----------------|
|
|
128
|
-
| `cyclo_manager` | `cyclo_manager` | **Started** | `robotis/cyclo-manager:0.
|
|
129
|
-
| `ui` | `cyclo_manager_ui` | **Started** | `robotis/cyclo-manager-ui:0.
|
|
136
|
+
| `cyclo_manager` | `cyclo_manager` | **Started** | `robotis/cyclo-manager:1.0.0` |
|
|
137
|
+
| `ui` | `cyclo_manager_ui` | **Started** | `robotis/cyclo-manager-ui:1.0.0` |
|
|
130
138
|
| `rmw_zenoh` | `zenoh_daemon` | **Created only** | `robotis/zenoh-daemon:latest` |
|
|
131
139
|
| `novnc-server` | `novnc-server` | **Created only** | `robotis/novnc-server:latest` |
|
|
132
140
|
|
|
@@ -146,7 +154,7 @@ The API reads it as `CONFIG_FILE=/app/config.yml` inside the container.
|
|
|
146
154
|
|
|
147
155
|
| Key | Description |
|
|
148
156
|
|-----|-------------|
|
|
149
|
-
| **`
|
|
157
|
+
| **`supported_robot_containers`** | Robot Docker container names that can open the System page (e.g. `ai_worker`, `open_manipulator`). Each must be a key in `sockets` and cannot be `host_agent`. |
|
|
150
158
|
| **`sockets`** | Map of logical name → agent **Unix socket path as seen inside the API container** (under `/agents/...`). Include robot/service containers and `host_agent`. |
|
|
151
159
|
|
|
152
160
|
s6 **service names** are not listed in config; each in-container agent reports them at runtime.
|
|
@@ -154,20 +162,17 @@ s6 **service names** are not listed in config; each in-container agent reports t
|
|
|
154
162
|
### Example (bundled default)
|
|
155
163
|
|
|
156
164
|
```yaml
|
|
157
|
-
|
|
165
|
+
supported_robot_containers:
|
|
166
|
+
- ai_worker
|
|
167
|
+
- open_manipulator
|
|
158
168
|
|
|
159
169
|
sockets:
|
|
160
170
|
ai_worker: "/agents/ai_worker/s6_agent.sock"
|
|
171
|
+
open_manipulator: "/agents/open_manipulator/s6_agent.sock"
|
|
161
172
|
cyclo_intelligence: "/agents/cyclo_intelligence/s6_agent.sock"
|
|
162
173
|
host_agent: "/agents/host/host_agent.sock"
|
|
163
174
|
```
|
|
164
175
|
|
|
165
|
-
### Validation (API startup)
|
|
166
|
-
|
|
167
|
-
- At least one socket entry besides `host_agent`
|
|
168
|
-
- `robot_container` must exist in `sockets`
|
|
169
|
-
- All socket paths must be non-empty strings
|
|
170
|
-
|
|
171
176
|
To use a different config layout, mounts, or local source builds, use the repository’s **`docker-compose.dev.yml`** — see [Custom config and development](#custom-config-and-development).
|
|
172
177
|
|
|
173
178
|
---
|
|
@@ -198,6 +203,7 @@ So `/agents/ai_worker/s6_agent.sock` in config corresponds to the host path abov
|
|
|
198
203
|
| Variable | Set by | Purpose |
|
|
199
204
|
|----------|--------|---------|
|
|
200
205
|
| **`CYCLO_MANAGER_CONFIG_FILE`** | CLI (`up`, `down`, `update`) | Absolute path to bundled `config.yml` on the host; mounted into the API container |
|
|
206
|
+
| **`CYCLO_HOST_AGENT_WORKSPACE`** | User or CLI-generated systemd unit | Workspace scanned by `cyclo_host_agent` for managed git repositories |
|
|
201
207
|
| **`HOSTNAME`** | CLI (default: machine hostname) | Passed to API as `HOST_HOSTNAME` |
|
|
202
208
|
| **`CONFIG_FILE`** | Compose (`/app/config.yml`) | Path inside the API container |
|
|
203
209
|
| **`ROS_DOMAIN_ID`** | **Not** set by CLI | Set inside robot containers (e.g. `~/.bashrc`) so DDS matches your fleet |
|
|
@@ -239,7 +245,7 @@ Python dependencies declared in [`pyproject.toml`](pyproject.toml):
|
|
|
239
245
|
|---------|---------|
|
|
240
246
|
| **fastapi** | `cyclo_host_agent` HTTP API |
|
|
241
247
|
| **uvicorn** | `cyclo_host_agent` server |
|
|
242
|
-
| **psutil** |
|
|
248
|
+
| **psutil** | Declared package dependency for host/system utility support |
|
|
243
249
|
|
|
244
250
|
The **`cyclo_manager`** CLI itself uses only the Python standard library plus **`subprocess`** calls to **`docker compose`**, **`pip`**, **`systemctl`**, and **`sudo`**.
|
|
245
251
|
|
|
@@ -41,16 +41,20 @@ from fastapi import APIRouter, HTTPException, status
|
|
|
41
41
|
router = APIRouter(prefix='/repos', tags=['repos'])
|
|
42
42
|
|
|
43
43
|
GIT_TIMEOUT = 120.0
|
|
44
|
+
GIT_REMOTE_TIMEOUT = 5.0
|
|
44
45
|
|
|
45
46
|
|
|
46
|
-
def
|
|
47
|
+
def _resolve_workspace() -> Path:
|
|
48
|
+
configured_workspace = os.environ.get('CYCLO_HOST_AGENT_WORKSPACE')
|
|
49
|
+
if configured_workspace:
|
|
50
|
+
return Path(configured_workspace).expanduser()
|
|
47
51
|
sudo_user = os.environ.get('SUDO_USER')
|
|
48
52
|
if sudo_user:
|
|
49
53
|
return Path(f'/home/{sudo_user}')
|
|
50
54
|
return Path.home()
|
|
51
55
|
|
|
52
56
|
|
|
53
|
-
WORKSPACE_PATH =
|
|
57
|
+
WORKSPACE_PATH = _resolve_workspace()
|
|
54
58
|
MANAGED_GITHUB_ORG = 'ROBOTIS-GIT'
|
|
55
59
|
ALLOWED_UPDATE_BRANCHES = frozenset({'main', 'jazzy'})
|
|
56
60
|
|
|
@@ -120,7 +124,11 @@ async def _ensure_allowed_branch(repo_path: Path) -> None:
|
|
|
120
124
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=detail)
|
|
121
125
|
|
|
122
126
|
|
|
123
|
-
async def _git(
|
|
127
|
+
async def _git(
|
|
128
|
+
args: list[str],
|
|
129
|
+
cwd: Optional[Path] = None,
|
|
130
|
+
timeout: float = GIT_TIMEOUT,
|
|
131
|
+
) -> tuple[int, str, str]:
|
|
124
132
|
proc = await asyncio.create_subprocess_exec(
|
|
125
133
|
'git', *args,
|
|
126
134
|
cwd=str(cwd) if cwd else None,
|
|
@@ -128,7 +136,7 @@ async def _git(args: list[str], cwd: Optional[Path] = None) -> tuple[int, str, s
|
|
|
128
136
|
stderr=asyncio.subprocess.PIPE,
|
|
129
137
|
)
|
|
130
138
|
try:
|
|
131
|
-
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=
|
|
139
|
+
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
|
132
140
|
except asyncio.TimeoutError:
|
|
133
141
|
proc.kill()
|
|
134
142
|
await proc.wait()
|
|
@@ -192,7 +200,13 @@ def _read_package_xml_version(repo_path: Path) -> Optional[str]:
|
|
|
192
200
|
|
|
193
201
|
|
|
194
202
|
async def _fetch_latest_tag(remote_url: str) -> Optional[str]:
|
|
195
|
-
|
|
203
|
+
try:
|
|
204
|
+
rc, out, _ = await _git(
|
|
205
|
+
['ls-remote', '--tags', remote_url],
|
|
206
|
+
timeout=GIT_REMOTE_TIMEOUT,
|
|
207
|
+
)
|
|
208
|
+
except asyncio.TimeoutError:
|
|
209
|
+
return None
|
|
196
210
|
if rc != 0:
|
|
197
211
|
return None
|
|
198
212
|
tags = []
|
|
@@ -19,28 +19,33 @@
|
|
|
19
19
|
"""System-level endpoints for the host agent."""
|
|
20
20
|
|
|
21
21
|
import asyncio
|
|
22
|
-
import
|
|
23
|
-
import shutil
|
|
22
|
+
import sys
|
|
24
23
|
|
|
25
24
|
from fastapi import APIRouter, HTTPException, status
|
|
26
25
|
|
|
27
|
-
logger = logging.getLogger(__name__)
|
|
28
|
-
|
|
29
26
|
router = APIRouter(prefix='/system', tags=['system'])
|
|
30
27
|
|
|
31
28
|
_update_status: dict = {'phase': 'idle', 'output': '', 'error': ''}
|
|
32
29
|
|
|
33
30
|
|
|
34
|
-
|
|
31
|
+
@router.get('/version')
|
|
32
|
+
async def get_host_agent_version() -> dict:
|
|
33
|
+
"""Return the running cyclo_host_agent package version."""
|
|
34
|
+
from cyclo_manager_cli import __version__
|
|
35
|
+
|
|
36
|
+
return {'version': __version__}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def _run_update() -> None:
|
|
35
40
|
global _update_status
|
|
36
41
|
try:
|
|
37
42
|
proc = await asyncio.create_subprocess_exec(
|
|
38
|
-
|
|
43
|
+
sys.executable, '-m', 'cyclo_manager_cli.cli', 'update',
|
|
39
44
|
stdout=asyncio.subprocess.PIPE,
|
|
40
45
|
stderr=asyncio.subprocess.PIPE,
|
|
41
46
|
)
|
|
42
47
|
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=600)
|
|
43
|
-
out = (stdout.decode() + stderr.decode()).strip()
|
|
48
|
+
out = (stdout.decode(errors='replace') + stderr.decode(errors='replace')).strip()
|
|
44
49
|
_update_status['output'] = out
|
|
45
50
|
if proc.returncode != 0:
|
|
46
51
|
_update_status['phase'] = 'error'
|
|
@@ -50,24 +55,22 @@ async def _run_update(cyclo_exe: str) -> None:
|
|
|
50
55
|
except asyncio.TimeoutError:
|
|
51
56
|
_update_status['phase'] = 'error'
|
|
52
57
|
_update_status['error'] = 'cyclo_manager update timed out'
|
|
58
|
+
except Exception as e:
|
|
59
|
+
_update_status['phase'] = 'error'
|
|
60
|
+
_update_status['error'] = f'Failed to start cyclo_manager update: {e}'
|
|
53
61
|
|
|
54
62
|
|
|
55
63
|
@router.post('/update')
|
|
56
64
|
async def start_update() -> dict:
|
|
57
|
-
"""
|
|
65
|
+
"""Run cyclo_manager update in the background."""
|
|
58
66
|
global _update_status
|
|
59
67
|
|
|
60
|
-
cyclo_exe = shutil.which('cyclo_manager')
|
|
61
|
-
if not cyclo_exe:
|
|
62
|
-
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
63
|
-
detail='cyclo_manager command not found')
|
|
64
|
-
|
|
65
68
|
if _update_status.get('phase') == 'updating':
|
|
66
69
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT,
|
|
67
70
|
detail='Update already in progress')
|
|
68
71
|
|
|
69
72
|
_update_status = {'phase': 'updating', 'output': '', 'error': ''}
|
|
70
|
-
asyncio.create_task(_run_update(
|
|
73
|
+
asyncio.create_task(_run_update())
|
|
71
74
|
return {'status': 'update started'}
|
|
72
75
|
|
|
73
76
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cyclo-manager
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: cyclo_manager CLI: pip-installable launcher for cyclo_manager server and UI containers. Run 'cyclo_manager up' to start Docker stack.
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -18,7 +18,7 @@ Requires-Dist: ruff; extra == "dev"
|
|
|
18
18
|
**Console commands:** `cyclo_manager` (underscore), `cyclo-manager` (hyphen)
|
|
19
19
|
**Host agent:** `cyclo_host_agent` (installed by the package, run via systemd)
|
|
20
20
|
|
|
21
|
-
This package is the **pip-installable launcher** for the cyclo_manager stack on a robot host. It:
|
|
21
|
+
This package is the **pip-installable launcher** for the cyclo_manager stack on a robot host. It is intended for production robots that run the pre-built Docker images. It:
|
|
22
22
|
|
|
23
23
|
1. Runs **`docker compose`** against a **bundled** [`cyclo_manager_cli/docker/docker-compose.yml`](cyclo_manager_cli/docker/docker-compose.yml) to start the **API** and **web UI** containers.
|
|
24
24
|
2. Installs and maintains **`cyclo_host_agent`** as a **systemd** service for host-level operations (git repo updates, `cyclo_manager` package update from the UI).
|
|
@@ -50,11 +50,12 @@ For the full monorepo (API source, UI source, dev compose), see the [repository
|
|
|
50
50
|
|
|
51
51
|
## Prerequisites
|
|
52
52
|
|
|
53
|
-
- **Docker** with **Compose v2** (`docker compose
|
|
53
|
+
- **Docker** with **Compose v2** (`docker compose`; the legacy `docker-compose` binary alone is not enough)
|
|
54
54
|
- **Python 3.10+**
|
|
55
|
-
-
|
|
55
|
+
- **systemd**: required for the managed `cyclo_host_agent.service`
|
|
56
|
+
- **`sudo`** — required on normal-user hosts to install the host agent systemd unit, socket directory permissions, and sudoers rules
|
|
56
57
|
- For **`cyclo_manager update`:** **`pip`** or **`pip3`** on `PATH`
|
|
57
|
-
- **
|
|
58
|
+
- **Root shell handling** — `cyclo_manager up` rejects direct root execution when a normal login user exists. On root-only devices, the host agent is installed as root.
|
|
58
59
|
- **Agent sockets** on the host under `/var/run/robotis/agent_sockets/` (robot containers and host agent). The bundled Compose file bind-mounts this tree into the API container as `/agents/`.
|
|
59
60
|
|
|
60
61
|
---
|
|
@@ -94,6 +95,8 @@ python3 -m cyclo_manager_cli.cli up
|
|
|
94
95
|
| `cyclo_manager update --no-pull` | Same as `update`, but skip image pull (use locally cached images). |
|
|
95
96
|
| `cyclo_manager --help` | Subcommand overview |
|
|
96
97
|
|
|
98
|
+
The CLI also has an internal `refresh-host-agent` subcommand. It is used by the sudoers rule and should normally not be invoked by users directly.
|
|
99
|
+
|
|
97
100
|
**`down` vs `update`:** `down` removes the host agent entirely. `update` only stops Docker containers, upgrades the pip package, restarts the stack, and refreshes the host agent — it does **not** call `cyclo_manager down`.
|
|
98
101
|
|
|
99
102
|
---
|
|
@@ -120,16 +123,21 @@ The pip package installs a small **FastAPI** server that listens on a **Unix dom
|
|
|
120
123
|
|------|------|
|
|
121
124
|
| Socket | `/var/run/robotis/agent_sockets/host/host_agent.sock` |
|
|
122
125
|
| systemd unit | `cyclo_host_agent.service` |
|
|
123
|
-
| sudoers | `/etc/sudoers.d/cyclo_manager` (`cp`, `udevadm` for container setup scripts) |
|
|
126
|
+
| sudoers | `/etc/sudoers.d/cyclo_manager` (`cp`, `udevadm` for container setup scripts; `refresh-host-agent` CLI subcommand for package-update refresh; skipped on root-only devices) |
|
|
124
127
|
|
|
125
128
|
The API container reaches it at `/agents/host/host_agent.sock` (see bundled `config.yml`).
|
|
126
129
|
|
|
127
130
|
**Responsibilities:**
|
|
128
131
|
|
|
129
|
-
- List
|
|
130
|
-
-
|
|
132
|
+
- List managed **ROBOTIS-GIT** git repositories in the configured workspace.
|
|
133
|
+
- Check update availability from remote tags and local `package.xml` versions.
|
|
134
|
+
- Update repos on allowed branches (`main`, `jazzy`) using stash or reset workflows.
|
|
135
|
+
- Stop/start a repo's `docker/container.sh` helper during an update when requested by the UI.
|
|
136
|
+
- Run Cyclo Manager package updates from the UI by delegating to `cyclo_manager update`.
|
|
137
|
+
|
|
138
|
+
`cyclo_manager up` installs the `refresh-host-agent` CLI subcommand into sudoers. It is registered as `sys.executable -m cyclo_manager_cli.cli refresh-host-agent` with `SETENV`, so UI-triggered updates can pass the current `PYTHONPATH` to sudo and do not depend on `PATH`. The command is idempotent and refreshes the socket directory, sudoers file, systemd unit, and service state after package upgrades.
|
|
131
139
|
|
|
132
|
-
|
|
140
|
+
Repository scanning uses `CYCLO_HOST_AGENT_WORKSPACE` when set. Otherwise it uses the service user's home directory, except root-only devices with `/data/docker`, where `/data/docker` is used automatically. Only repositories whose `origin` remote belongs to `ROBOTIS-GIT` are returned by the repo list/update endpoints.
|
|
133
141
|
|
|
134
142
|
---
|
|
135
143
|
|
|
@@ -139,8 +147,8 @@ Defined in [`cyclo_manager_cli/docker/docker-compose.yml`](cyclo_manager_cli/doc
|
|
|
139
147
|
|
|
140
148
|
| Compose service | Container name | `cyclo_manager up` | Image (example) |
|
|
141
149
|
|-----------------|----------------|--------------------|-----------------|
|
|
142
|
-
| `cyclo_manager` | `cyclo_manager` | **Started** | `robotis/cyclo-manager:0.
|
|
143
|
-
| `ui` | `cyclo_manager_ui` | **Started** | `robotis/cyclo-manager-ui:0.
|
|
150
|
+
| `cyclo_manager` | `cyclo_manager` | **Started** | `robotis/cyclo-manager:1.0.0` |
|
|
151
|
+
| `ui` | `cyclo_manager_ui` | **Started** | `robotis/cyclo-manager-ui:1.0.0` |
|
|
144
152
|
| `rmw_zenoh` | `zenoh_daemon` | **Created only** | `robotis/zenoh-daemon:latest` |
|
|
145
153
|
| `novnc-server` | `novnc-server` | **Created only** | `robotis/novnc-server:latest` |
|
|
146
154
|
|
|
@@ -160,7 +168,7 @@ The API reads it as `CONFIG_FILE=/app/config.yml` inside the container.
|
|
|
160
168
|
|
|
161
169
|
| Key | Description |
|
|
162
170
|
|-----|-------------|
|
|
163
|
-
| **`
|
|
171
|
+
| **`supported_robot_containers`** | Robot Docker container names that can open the System page (e.g. `ai_worker`, `open_manipulator`). Each must be a key in `sockets` and cannot be `host_agent`. |
|
|
164
172
|
| **`sockets`** | Map of logical name → agent **Unix socket path as seen inside the API container** (under `/agents/...`). Include robot/service containers and `host_agent`. |
|
|
165
173
|
|
|
166
174
|
s6 **service names** are not listed in config; each in-container agent reports them at runtime.
|
|
@@ -168,20 +176,17 @@ s6 **service names** are not listed in config; each in-container agent reports t
|
|
|
168
176
|
### Example (bundled default)
|
|
169
177
|
|
|
170
178
|
```yaml
|
|
171
|
-
|
|
179
|
+
supported_robot_containers:
|
|
180
|
+
- ai_worker
|
|
181
|
+
- open_manipulator
|
|
172
182
|
|
|
173
183
|
sockets:
|
|
174
184
|
ai_worker: "/agents/ai_worker/s6_agent.sock"
|
|
185
|
+
open_manipulator: "/agents/open_manipulator/s6_agent.sock"
|
|
175
186
|
cyclo_intelligence: "/agents/cyclo_intelligence/s6_agent.sock"
|
|
176
187
|
host_agent: "/agents/host/host_agent.sock"
|
|
177
188
|
```
|
|
178
189
|
|
|
179
|
-
### Validation (API startup)
|
|
180
|
-
|
|
181
|
-
- At least one socket entry besides `host_agent`
|
|
182
|
-
- `robot_container` must exist in `sockets`
|
|
183
|
-
- All socket paths must be non-empty strings
|
|
184
|
-
|
|
185
190
|
To use a different config layout, mounts, or local source builds, use the repository’s **`docker-compose.dev.yml`** — see [Custom config and development](#custom-config-and-development).
|
|
186
191
|
|
|
187
192
|
---
|
|
@@ -212,6 +217,7 @@ So `/agents/ai_worker/s6_agent.sock` in config corresponds to the host path abov
|
|
|
212
217
|
| Variable | Set by | Purpose |
|
|
213
218
|
|----------|--------|---------|
|
|
214
219
|
| **`CYCLO_MANAGER_CONFIG_FILE`** | CLI (`up`, `down`, `update`) | Absolute path to bundled `config.yml` on the host; mounted into the API container |
|
|
220
|
+
| **`CYCLO_HOST_AGENT_WORKSPACE`** | User or CLI-generated systemd unit | Workspace scanned by `cyclo_host_agent` for managed git repositories |
|
|
215
221
|
| **`HOSTNAME`** | CLI (default: machine hostname) | Passed to API as `HOST_HOSTNAME` |
|
|
216
222
|
| **`CONFIG_FILE`** | Compose (`/app/config.yml`) | Path inside the API container |
|
|
217
223
|
| **`ROS_DOMAIN_ID`** | **Not** set by CLI | Set inside robot containers (e.g. `~/.bashrc`) so DDS matches your fleet |
|
|
@@ -253,7 +259,7 @@ Python dependencies declared in [`pyproject.toml`](pyproject.toml):
|
|
|
253
259
|
|---------|---------|
|
|
254
260
|
| **fastapi** | `cyclo_host_agent` HTTP API |
|
|
255
261
|
| **uvicorn** | `cyclo_host_agent` server |
|
|
256
|
-
| **psutil** |
|
|
262
|
+
| **psutil** | Declared package dependency for host/system utility support |
|
|
257
263
|
|
|
258
264
|
The **`cyclo_manager`** CLI itself uses only the Python standard library plus **`subprocess`** calls to **`docker compose`**, **`pip`**, **`systemctl`**, and **`sudo`**.
|
|
259
265
|
|
|
@@ -26,6 +26,7 @@ Launches cyclo_manager server and UI via Docker.
|
|
|
26
26
|
import argparse
|
|
27
27
|
import os
|
|
28
28
|
from pathlib import Path
|
|
29
|
+
import pwd
|
|
29
30
|
import shutil
|
|
30
31
|
import socket
|
|
31
32
|
import subprocess
|
|
@@ -36,10 +37,11 @@ HOST_AGENT_SERVICE = 'cyclo_host_agent'
|
|
|
36
37
|
HOST_AGENT_SOCKET = '/var/run/robotis/agent_sockets/host/host_agent.sock'
|
|
37
38
|
HOST_AGENT_SOCKET_DIR = '/var/run/robotis/agent_sockets/host'
|
|
38
39
|
|
|
39
|
-
# `cyclo_manager up` starts these immediately.
|
|
40
|
+
# `cyclo_manager up` starts these Containers immediately.
|
|
40
41
|
COMPOSE_SERVICES_UP = ('cyclo_manager', 'ui')
|
|
41
42
|
# These get `docker compose create` only (stopped); start from UI or `docker start`.
|
|
42
43
|
COMPOSE_SERVICES_CREATE_ONLY = ('rmw_zenoh', 'novnc-server')
|
|
44
|
+
DISABLED_LOGIN_SHELLS = {'', '/bin/false', '/sbin/nologin', '/usr/sbin/nologin'}
|
|
43
45
|
|
|
44
46
|
|
|
45
47
|
def _docker_dir() -> Path:
|
|
@@ -55,6 +57,47 @@ def _packaged_config_path() -> Path:
|
|
|
55
57
|
return _config_dir() / 'config.yml'
|
|
56
58
|
|
|
57
59
|
|
|
60
|
+
def _sudo_cmd(args: list[str]) -> list[str]:
|
|
61
|
+
"""Return args prefixed with sudo when the current process is not already root."""
|
|
62
|
+
if os.geteuid() == 0:
|
|
63
|
+
return args
|
|
64
|
+
return ['sudo', *args]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _user_home(user: str) -> Path:
|
|
68
|
+
"""Return a user's home directory from passwd, with a conservative fallback."""
|
|
69
|
+
try:
|
|
70
|
+
return Path(pwd.getpwnam(user).pw_dir)
|
|
71
|
+
except KeyError:
|
|
72
|
+
if user == 'root':
|
|
73
|
+
return Path('/root')
|
|
74
|
+
return Path(f'/home/{user}')
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _has_normal_login_user() -> bool:
|
|
78
|
+
"""Return True when the host has a non-root login user."""
|
|
79
|
+
for account in pwd.getpwall():
|
|
80
|
+
if account.pw_name == 'root':
|
|
81
|
+
continue
|
|
82
|
+
if account.pw_uid < 1000 or account.pw_uid >= 65534:
|
|
83
|
+
continue
|
|
84
|
+
if account.pw_shell in DISABLED_LOGIN_SHELLS:
|
|
85
|
+
continue
|
|
86
|
+
return True
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _host_agent_workspace(user: str, user_home: Path) -> Path:
|
|
91
|
+
"""Return the workspace path the host agent should scan for git repositories."""
|
|
92
|
+
configured_workspace = os.environ.get('CYCLO_HOST_AGENT_WORKSPACE')
|
|
93
|
+
if configured_workspace:
|
|
94
|
+
return Path(configured_workspace).expanduser()
|
|
95
|
+
data_docker = Path('/data/docker')
|
|
96
|
+
if user == 'root' and data_docker.is_dir():
|
|
97
|
+
return data_docker
|
|
98
|
+
return user_home
|
|
99
|
+
|
|
100
|
+
|
|
58
101
|
def _resolve_service_user() -> tuple[str, Path] | None:
|
|
59
102
|
"""
|
|
60
103
|
Return (username, home) for the host agent systemd service.
|
|
@@ -64,9 +107,15 @@ def _resolve_service_user() -> tuple[str, Path] | None:
|
|
|
64
107
|
"""
|
|
65
108
|
sudo_user = os.environ.get('SUDO_USER')
|
|
66
109
|
if sudo_user:
|
|
67
|
-
return sudo_user,
|
|
68
|
-
user = os.environ.get('USER') or os.
|
|
110
|
+
return sudo_user, _user_home(sudo_user)
|
|
111
|
+
user = os.environ.get('USER') or pwd.getpwuid(os.getuid()).pw_name
|
|
69
112
|
if user == 'root':
|
|
113
|
+
if not _has_normal_login_user():
|
|
114
|
+
print(
|
|
115
|
+
'Info: root-only device detected; cyclo_host_agent will run as root.',
|
|
116
|
+
file=sys.stderr,
|
|
117
|
+
)
|
|
118
|
+
return 'root', Path('/root')
|
|
70
119
|
print(
|
|
71
120
|
'Error: cyclo_manager up must run as a normal user (e.g. robotis), not root.',
|
|
72
121
|
file=sys.stderr,
|
|
@@ -85,19 +134,19 @@ def _setup_socket_dir(user: str) -> bool:
|
|
|
85
134
|
print(f' [1/3] Creating socket directory {HOST_AGENT_SOCKET_DIR}...')
|
|
86
135
|
try:
|
|
87
136
|
subprocess.run(
|
|
88
|
-
['
|
|
137
|
+
_sudo_cmd(['mkdir', '-p', HOST_AGENT_SOCKET_DIR]),
|
|
89
138
|
check=True,
|
|
90
139
|
capture_output=True,
|
|
91
140
|
)
|
|
92
141
|
print(f' [2/3] Setting ownership to {user}:{user}...')
|
|
93
142
|
subprocess.run(
|
|
94
|
-
['
|
|
143
|
+
_sudo_cmd(['chown', f'{user}:{user}', HOST_AGENT_SOCKET_DIR]),
|
|
95
144
|
check=True,
|
|
96
145
|
capture_output=True,
|
|
97
146
|
)
|
|
98
147
|
print(' [3/3] Removing stale socket file (if any)...')
|
|
99
148
|
subprocess.run(
|
|
100
|
-
['
|
|
149
|
+
_sudo_cmd(['rm', '-f', HOST_AGENT_SOCKET]),
|
|
101
150
|
check=True,
|
|
102
151
|
capture_output=True,
|
|
103
152
|
)
|
|
@@ -111,25 +160,34 @@ def _install_sudoers(user: str) -> bool:
|
|
|
111
160
|
"""
|
|
112
161
|
Write sudoers rules for cyclo_manager.
|
|
113
162
|
|
|
114
|
-
Grant NOPASSWD for udev operations
|
|
115
|
-
(cp to /etc/udev/rules.d/, udevadm control/trigger).
|
|
163
|
+
Grant NOPASSWD for udev operations and the root-only host-agent refresh command.
|
|
116
164
|
"""
|
|
117
|
-
|
|
165
|
+
if not sys.executable:
|
|
166
|
+
print(
|
|
167
|
+
'Python executable is unknown. '
|
|
168
|
+
'Run `pip install cyclo-manager` and retry.',
|
|
169
|
+
file=sys.stderr,
|
|
170
|
+
)
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
print(' Installing sudoers rules (cp, udevadm, host-agent refresh command)...')
|
|
118
174
|
sudoers_content = (
|
|
119
175
|
f'{user} ALL=(ALL) NOPASSWD: /usr/bin/cp, '
|
|
120
176
|
f'/usr/bin/udevadm control --reload-rules, '
|
|
121
|
-
f'/usr/bin/udevadm trigger
|
|
177
|
+
f'/usr/bin/udevadm trigger, '
|
|
178
|
+
'SETENV: '
|
|
179
|
+
f'{sys.executable} -m cyclo_manager_cli.cli refresh-host-agent\n'
|
|
122
180
|
)
|
|
123
181
|
sudoers_file = '/etc/sudoers.d/cyclo_manager'
|
|
124
182
|
try:
|
|
125
183
|
subprocess.run(
|
|
126
|
-
['
|
|
184
|
+
_sudo_cmd(['tee', sudoers_file]),
|
|
127
185
|
input=sudoers_content.encode(),
|
|
128
186
|
capture_output=True,
|
|
129
187
|
check=True,
|
|
130
188
|
)
|
|
131
189
|
subprocess.run(
|
|
132
|
-
['
|
|
190
|
+
_sudo_cmd(['chmod', '440', sudoers_file]),
|
|
133
191
|
check=True,
|
|
134
192
|
capture_output=True,
|
|
135
193
|
)
|
|
@@ -139,7 +197,7 @@ def _install_sudoers(user: str) -> bool:
|
|
|
139
197
|
return False
|
|
140
198
|
|
|
141
199
|
|
|
142
|
-
def _ensure_host_agent() -> int:
|
|
200
|
+
def _ensure_host_agent(restart_no_block: bool = False) -> int:
|
|
143
201
|
"""
|
|
144
202
|
Install or refresh cyclo_host_agent: systemd unit, socket dir, and service start.
|
|
145
203
|
|
|
@@ -151,10 +209,11 @@ def _ensure_host_agent() -> int:
|
|
|
151
209
|
return 1
|
|
152
210
|
|
|
153
211
|
user, user_home = account
|
|
154
|
-
|
|
155
|
-
if
|
|
212
|
+
workspace_path = _host_agent_workspace(user, user_home)
|
|
213
|
+
agent_exec_start = f'{sys.executable} -m cyclo_host_agent.main' if sys.executable else None
|
|
214
|
+
if not agent_exec_start:
|
|
156
215
|
print(
|
|
157
|
-
'Warning:
|
|
216
|
+
'Warning: Python executable is unknown. '
|
|
158
217
|
'Run `pip install cyclo-manager` and retry.',
|
|
159
218
|
file=sys.stderr,
|
|
160
219
|
)
|
|
@@ -163,7 +222,7 @@ def _ensure_host_agent() -> int:
|
|
|
163
222
|
if not _setup_socket_dir(user):
|
|
164
223
|
return 1
|
|
165
224
|
|
|
166
|
-
if not _install_sudoers(user):
|
|
225
|
+
if user != 'root' and not _install_sudoers(user):
|
|
167
226
|
return 1
|
|
168
227
|
|
|
169
228
|
service_content = f"""\
|
|
@@ -177,8 +236,9 @@ User={user}
|
|
|
177
236
|
Group={user}
|
|
178
237
|
RuntimeDirectory=robotis/agent_sockets/host
|
|
179
238
|
RuntimeDirectoryMode=0755
|
|
180
|
-
ExecStart={
|
|
239
|
+
ExecStart={agent_exec_start}
|
|
181
240
|
Environment=HOME={user_home}
|
|
241
|
+
Environment=CYCLO_HOST_AGENT_WORKSPACE={workspace_path}
|
|
182
242
|
Restart=always
|
|
183
243
|
RestartSec=3
|
|
184
244
|
|
|
@@ -192,22 +252,29 @@ WantedBy=multi-user.target
|
|
|
192
252
|
try:
|
|
193
253
|
print(f' Writing unit file to {service_file}...')
|
|
194
254
|
subprocess.run(
|
|
195
|
-
['
|
|
255
|
+
_sudo_cmd(['tee', service_file]),
|
|
196
256
|
input=service_content.encode(),
|
|
197
257
|
capture_output=True,
|
|
198
258
|
check=True,
|
|
199
259
|
)
|
|
200
260
|
print(' Running systemctl daemon-reload...')
|
|
201
|
-
subprocess.run(
|
|
261
|
+
subprocess.run(
|
|
262
|
+
_sudo_cmd(['systemctl', 'daemon-reload']),
|
|
263
|
+
check=True,
|
|
264
|
+
capture_output=True,
|
|
265
|
+
)
|
|
202
266
|
print(f' Enabling {HOST_AGENT_SERVICE}...')
|
|
203
267
|
subprocess.run(
|
|
204
|
-
['
|
|
268
|
+
_sudo_cmd(['systemctl', 'enable', f'{HOST_AGENT_SERVICE}.service']),
|
|
205
269
|
check=True,
|
|
206
270
|
capture_output=True,
|
|
207
271
|
)
|
|
208
272
|
print(f' Starting {HOST_AGENT_SERVICE}...')
|
|
273
|
+
restart_cmd = ['systemctl', 'restart', f'{HOST_AGENT_SERVICE}.service']
|
|
274
|
+
if restart_no_block:
|
|
275
|
+
restart_cmd.insert(1, '--no-block')
|
|
209
276
|
subprocess.run(
|
|
210
|
-
|
|
277
|
+
_sudo_cmd(restart_cmd),
|
|
211
278
|
check=True,
|
|
212
279
|
capture_output=True,
|
|
213
280
|
)
|
|
@@ -217,10 +284,54 @@ WantedBy=multi-user.target
|
|
|
217
284
|
print(f'Failed to install host agent service: {e}', file=sys.stderr)
|
|
218
285
|
return 1
|
|
219
286
|
except FileNotFoundError:
|
|
220
|
-
print(
|
|
287
|
+
print(
|
|
288
|
+
'Required command not found. Install the host agent service manually.',
|
|
289
|
+
file=sys.stderr,
|
|
290
|
+
)
|
|
221
291
|
return 1
|
|
222
292
|
|
|
223
293
|
|
|
294
|
+
def _refresh_host_agent_after_update() -> int:
|
|
295
|
+
"""Refresh host agent with the sudoers-registered cyclo_manager command when possible."""
|
|
296
|
+
if os.geteuid() == 0:
|
|
297
|
+
return _ensure_host_agent(restart_no_block=True)
|
|
298
|
+
if not sys.executable:
|
|
299
|
+
print(
|
|
300
|
+
'Python executable is unknown; falling back to setup.',
|
|
301
|
+
file=sys.stderr,
|
|
302
|
+
)
|
|
303
|
+
return _ensure_host_agent() if sys.stdin.isatty() else 1
|
|
304
|
+
|
|
305
|
+
refresh_command = [sys.executable, '-m', 'cyclo_manager_cli.cli', 'refresh-host-agent']
|
|
306
|
+
print(f'Refreshing host agent with sudo command: {" ".join(refresh_command)}')
|
|
307
|
+
pythonpath = os.pathsep.join(dict.fromkeys(path for path in sys.path if path))
|
|
308
|
+
try:
|
|
309
|
+
subprocess.run(['sudo', '-n', f'PYTHONPATH={pythonpath}', *refresh_command], check=True)
|
|
310
|
+
return 0
|
|
311
|
+
except subprocess.CalledProcessError as e:
|
|
312
|
+
if sys.stdin.isatty():
|
|
313
|
+
print(
|
|
314
|
+
'Warning: sudo refresh command failed; falling back to interactive setup.',
|
|
315
|
+
file=sys.stderr,
|
|
316
|
+
)
|
|
317
|
+
return _ensure_host_agent()
|
|
318
|
+
print(f'Host-agent refresh command failed: {e}', file=sys.stderr)
|
|
319
|
+
return e.returncode
|
|
320
|
+
except FileNotFoundError:
|
|
321
|
+
if sys.stdin.isatty():
|
|
322
|
+
return _ensure_host_agent()
|
|
323
|
+
print('sudo not found; cannot refresh host agent service.', file=sys.stderr)
|
|
324
|
+
return 1
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def cmd_refresh_host_agent(args: argparse.Namespace) -> int:
|
|
328
|
+
"""Refresh the host agent from sudoers after a package update."""
|
|
329
|
+
if os.geteuid() != 0:
|
|
330
|
+
print('cyclo_manager refresh-host-agent must run as root.', file=sys.stderr)
|
|
331
|
+
return 1
|
|
332
|
+
return _ensure_host_agent(restart_no_block=True)
|
|
333
|
+
|
|
334
|
+
|
|
224
335
|
def cmd_up(args: argparse.Namespace) -> int:
|
|
225
336
|
"""Start API + UI; create zenoh and noVNC containers without starting them."""
|
|
226
337
|
print('--- cyclo_manager up ---')
|
|
@@ -272,15 +383,21 @@ def _teardown_host_agent() -> None:
|
|
|
272
383
|
"""Stop and remove the host agent service, unit file, sudoers, and socket dir."""
|
|
273
384
|
print(f'Tearing down {HOST_AGENT_SERVICE}...')
|
|
274
385
|
steps = [
|
|
275
|
-
(['sudo', 'systemctl', 'stop', f'{HOST_AGENT_SERVICE}.service'], 'stop service'),
|
|
276
|
-
(['sudo', 'systemctl', 'disable', f'{HOST_AGENT_SERVICE}.service'], 'disable service'),
|
|
277
386
|
(
|
|
278
|
-
['
|
|
387
|
+
_sudo_cmd(['systemctl', 'stop', f'{HOST_AGENT_SERVICE}.service']),
|
|
388
|
+
'stop service',
|
|
389
|
+
),
|
|
390
|
+
(
|
|
391
|
+
_sudo_cmd(['systemctl', 'disable', f'{HOST_AGENT_SERVICE}.service']),
|
|
392
|
+
'disable service',
|
|
393
|
+
),
|
|
394
|
+
(
|
|
395
|
+
_sudo_cmd(['rm', '-f', f'/etc/systemd/system/{HOST_AGENT_SERVICE}.service']),
|
|
279
396
|
'remove unit file',
|
|
280
397
|
),
|
|
281
|
-
(['
|
|
282
|
-
(['
|
|
283
|
-
(['
|
|
398
|
+
(_sudo_cmd(['systemctl', 'daemon-reload']), 'daemon-reload'),
|
|
399
|
+
(_sudo_cmd(['rm', '-f', '/etc/sudoers.d/cyclo_manager']), 'remove sudoers'),
|
|
400
|
+
(_sudo_cmd(['rm', '-rf', HOST_AGENT_SOCKET_DIR]), 'remove socket dir'),
|
|
284
401
|
]
|
|
285
402
|
for cmd, desc in steps:
|
|
286
403
|
print(f' {desc}...')
|
|
@@ -384,7 +501,7 @@ def cmd_update(args: argparse.Namespace) -> int:
|
|
|
384
501
|
return e.returncode
|
|
385
502
|
|
|
386
503
|
print('Refreshing host agent (unit, socket, sudoers)...')
|
|
387
|
-
if
|
|
504
|
+
if _refresh_host_agent_after_update() != 0:
|
|
388
505
|
print('Warning: Failed to refresh host agent service.', file=sys.stderr)
|
|
389
506
|
return 1
|
|
390
507
|
|
|
@@ -425,8 +542,18 @@ def main() -> int:
|
|
|
425
542
|
)
|
|
426
543
|
update_parser.set_defaults(func=cmd_update)
|
|
427
544
|
|
|
545
|
+
refresh_parser = sub.add_parser(
|
|
546
|
+
'refresh-host-agent',
|
|
547
|
+
help='Refresh cyclo_host_agent service after package update',
|
|
548
|
+
)
|
|
549
|
+
refresh_parser.set_defaults(func=cmd_refresh_host_agent)
|
|
550
|
+
|
|
428
551
|
args = parser.parse_args()
|
|
429
552
|
if not args.command:
|
|
430
553
|
parser.print_help()
|
|
431
554
|
return 0
|
|
432
555
|
return args.func(args)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
if __name__ == '__main__':
|
|
559
|
+
raise SystemExit(main())
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# cyclo_manager Configuration
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
supported_robot_containers:
|
|
4
|
+
- ai_worker
|
|
5
|
+
- open_manipulator
|
|
4
6
|
|
|
5
7
|
sockets:
|
|
6
8
|
ai_worker: "/agents/ai_worker/s6_agent.sock"
|
|
9
|
+
open_manipulator: "/agents/open_manipulator/s6_agent.sock"
|
|
7
10
|
cyclo_intelligence: "/agents/cyclo_intelligence/s6_agent.sock"
|
|
8
11
|
host_agent: "/agents/host/host_agent.sock"
|
{cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0}/cyclo_manager_cli/docker/docker-compose.yml
RENAMED
|
@@ -11,7 +11,7 @@ services:
|
|
|
11
11
|
|
|
12
12
|
cyclo_manager:
|
|
13
13
|
container_name: cyclo_manager
|
|
14
|
-
image: robotis/cyclo-manager:0.
|
|
14
|
+
image: robotis/cyclo-manager:1.0.0
|
|
15
15
|
restart: always
|
|
16
16
|
network_mode: host
|
|
17
17
|
volumes:
|
|
@@ -27,7 +27,7 @@ services:
|
|
|
27
27
|
|
|
28
28
|
ui:
|
|
29
29
|
container_name: cyclo_manager_ui
|
|
30
|
-
image: robotis/cyclo-manager-ui:0.
|
|
30
|
+
image: robotis/cyclo-manager-ui:1.0.0
|
|
31
31
|
restart: always
|
|
32
32
|
network_mode: host
|
|
33
33
|
environment:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cyclo-manager"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "1.0.0"
|
|
8
8
|
description = "cyclo_manager CLI: pip-installable launcher for cyclo_manager server and UI containers. Run 'cyclo_manager up' to start Docker stack."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cyclo_manager-0.4.0.dev0 → cyclo_manager-1.0.0}/cyclo_manager.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|