cyclo-manager 0.2.0.dev11__tar.gz → 0.2.0.dev12__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.2.0.dev12/PKG-INFO +275 -0
- cyclo_manager-0.2.0.dev12/README.md +261 -0
- {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_host_agent/routers/repos.py +44 -1
- cyclo_manager-0.2.0.dev12/cyclo_manager.egg-info/PKG-INFO +275 -0
- {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_manager_cli/cli.py +4 -12
- {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/pyproject.toml +1 -1
- {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/setup.cfg +1 -1
- cyclo_manager-0.2.0.dev11/PKG-INFO +0 -141
- cyclo_manager-0.2.0.dev11/README.md +0 -127
- cyclo_manager-0.2.0.dev11/cyclo_manager.egg-info/PKG-INFO +0 -141
- {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_host_agent/__init__.py +0 -0
- {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_host_agent/main.py +0 -0
- {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_host_agent/models.py +0 -0
- {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_host_agent/routers/__init__.py +0 -0
- {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_host_agent/routers/update.py +0 -0
- {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_manager.egg-info/SOURCES.txt +0 -0
- {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_manager.egg-info/dependency_links.txt +0 -0
- {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_manager.egg-info/entry_points.txt +0 -0
- {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_manager.egg-info/requires.txt +0 -0
- {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_manager.egg-info/top_level.txt +0 -0
- {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_manager_cli/__init__.py +0 -0
- {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_manager_cli/config/config.yml +0 -0
- {cyclo_manager-0.2.0.dev11 → cyclo_manager-0.2.0.dev12}/cyclo_manager_cli/docker/docker-compose.yml +0 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cyclo-manager
|
|
3
|
+
Version: 0.2.0.dev12
|
|
4
|
+
Summary: cyclo_manager CLI: pip-installable launcher for cyclo_manager server and UI containers. Run 'cyclo_manager up' to start Docker stack.
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: fastapi>=0.100.0
|
|
9
|
+
Requires-Dist: uvicorn[standard]>=0.23.0
|
|
10
|
+
Requires-Dist: psutil>=5.9.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest; extra == "dev"
|
|
13
|
+
Requires-Dist: ruff; extra == "dev"
|
|
14
|
+
|
|
15
|
+
# cyclo_manager CLI
|
|
16
|
+
|
|
17
|
+
**PyPI package:** `cyclo-manager`
|
|
18
|
+
**Console commands:** `cyclo_manager` (underscore), `cyclo-manager` (hyphen)
|
|
19
|
+
**Host agent:** `cyclo_host_agent` (installed by the package, run via systemd)
|
|
20
|
+
|
|
21
|
+
This package is the **pip-installable launcher** for the cyclo_manager stack on a robot host. It:
|
|
22
|
+
|
|
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
|
+
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).
|
|
25
|
+
3. Can **`pip install -U`** itself and bring the Docker stack back up.
|
|
26
|
+
|
|
27
|
+
Implementation: [`cyclo_manager_cli/cli.py`](cyclo_manager_cli/cli.py), [`cyclo_host_agent/`](cyclo_host_agent/).
|
|
28
|
+
|
|
29
|
+
For the full monorepo (API source, UI source, dev compose), see the [repository root README](../README.md).
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Table of contents
|
|
34
|
+
|
|
35
|
+
- [Prerequisites](#prerequisites)
|
|
36
|
+
- [Install](#install)
|
|
37
|
+
- [Commands](#commands)
|
|
38
|
+
- [What `cyclo_manager up` does](#what-cyclo_manager-up-does)
|
|
39
|
+
- [Host agent (`cyclo_host_agent`)](#host-agent-cyclo_host_agent)
|
|
40
|
+
- [Compose services](#compose-services)
|
|
41
|
+
- [Configuration](#configuration)
|
|
42
|
+
- [Agent sockets on the host](#agent-sockets-on-the-host)
|
|
43
|
+
- [Environment variables](#environment-variables)
|
|
44
|
+
- [URLs (packaged stack)](#urls-packaged-stack)
|
|
45
|
+
- [Custom config and development](#custom-config-and-development)
|
|
46
|
+
- [Dependencies](#dependencies)
|
|
47
|
+
- [License](#license)
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Prerequisites
|
|
52
|
+
|
|
53
|
+
- **Docker** with **Compose v2** (`docker compose` — the legacy `docker-compose` binary alone is not enough)
|
|
54
|
+
- **Python 3.10+**
|
|
55
|
+
- **`sudo`** — required to install the host agent systemd unit, socket directory permissions, and sudoers rules
|
|
56
|
+
- For **`cyclo_manager update`:** **`pip`** or **`pip3`** on `PATH`
|
|
57
|
+
- **Do not run as root** — the host agent runs as the invoking user (or `SUDO_USER` when using `sudo cyclo_manager up`). Running as root is rejected.
|
|
58
|
+
- **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
|
+
---
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
From **PyPI**:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install cyclo-manager
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Pre-release versions:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install --pre cyclo-manager==0.2.0.dev0
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
If `cyclo_manager` is not on `PATH`, add the user script directory:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
80
|
+
cyclo_manager up
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
If the entry point is missing:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
python3 -m cyclo_manager_cli.cli --help
|
|
87
|
+
python3 -m cyclo_manager_cli.cli up
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Commands
|
|
93
|
+
|
|
94
|
+
| Command | Behavior |
|
|
95
|
+
|--------|----------|
|
|
96
|
+
| `cyclo_manager up` | Install/refresh **cyclo_host_agent** (systemd unit, socket dir, sudoers), then `docker compose up -d` for **API + UI**, then `docker compose create --no-recreate` for **Zenoh** and **noVNC** (created but **not started**). Sets **`CYCLO_MANAGER_CONFIG_FILE`** to the bundled config. |
|
|
97
|
+
| `cyclo_manager up --pull` | `docker compose pull` first, then the same as `up`. |
|
|
98
|
+
| `cyclo_manager down` | **Tear down host agent** (stop/disable service, remove unit, sudoers, socket dir), then `docker compose down` for all services in the bundled compose file. |
|
|
99
|
+
| `cyclo_manager update` | `docker compose down` (containers only — **host agent stays installed**), `pip install -U cyclo-manager`, `docker compose up` + create optional services, then **refresh host agent** (unit, socket, sudoers). Optional `--pull` pulls images before the final `up`. |
|
|
100
|
+
| `cyclo_manager --help` | Subcommand overview |
|
|
101
|
+
|
|
102
|
+
**`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`.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## What `cyclo_manager up` does
|
|
107
|
+
|
|
108
|
+
Three phases:
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
[1/3] Setting up host agent → systemd unit, socket ownership, sudoers
|
|
112
|
+
[2/3] Starting containers → cyclo_manager (API) + ui
|
|
113
|
+
[3/3] Creating (not starting) → zenoh_daemon + novnc-server
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Open **http://127.0.0.1:3000** (UI) and **http://127.0.0.1:8081/docs** (API).
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Host agent (`cyclo_host_agent`)
|
|
121
|
+
|
|
122
|
+
The pip package installs a small **FastAPI** server that listens on a **Unix domain socket** on the host:
|
|
123
|
+
|
|
124
|
+
| Item | Path |
|
|
125
|
+
|------|------|
|
|
126
|
+
| Socket | `/var/run/robotis/agent_sockets/host/host_agent.sock` |
|
|
127
|
+
| systemd unit | `cyclo_host_agent.service` |
|
|
128
|
+
| sudoers | `/etc/sudoers.d/cyclo_manager` (`cp`, `udevadm` for container setup scripts) |
|
|
129
|
+
|
|
130
|
+
The API container reaches it at `/agents/host/host_agent.sock` (see bundled `config.yml`).
|
|
131
|
+
|
|
132
|
+
**Responsibilities:**
|
|
133
|
+
|
|
134
|
+
- List and update **`~/ROBOTIS-GIT/*`** git repositories (Version Management in the UI)
|
|
135
|
+
- Run **`cyclo_manager update`** when triggered from the UI (`POST /host/update`)
|
|
136
|
+
|
|
137
|
+
`cyclo_manager up` and `cyclo_manager update` both call `_ensure_host_agent()`, which is idempotent — safe to run after upgrades or user changes.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Compose services
|
|
142
|
+
|
|
143
|
+
Defined in [`cyclo_manager_cli/docker/docker-compose.yml`](cyclo_manager_cli/docker/docker-compose.yml). All services use **`network_mode: host`**.
|
|
144
|
+
|
|
145
|
+
| Compose service | Container name | `cyclo_manager up` | Image (example) |
|
|
146
|
+
|-----------------|----------------|--------------------|-----------------|
|
|
147
|
+
| `cyclo_manager` | `cyclo_manager` | **Started** | `robotis/cyclo-manager:0.2.0` |
|
|
148
|
+
| `ui` | `cyclo_manager_ui` | **Started** | `robotis/cyclo-manager-ui:0.2.0` |
|
|
149
|
+
| `rmw_zenoh` | `zenoh_daemon` | **Created only** | `robotis/zenoh-daemon:latest` |
|
|
150
|
+
| `novnc-server` | `novnc-server` | **Created only** | `robotis/novnc-server:latest` |
|
|
151
|
+
|
|
152
|
+
Start optional containers from the UI or manually, e.g. `docker start zenoh_daemon`.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Configuration
|
|
157
|
+
|
|
158
|
+
The CLI always mounts the **bundled** config into the API container. There is **no** `-c` / `--config` flag on the pip CLI.
|
|
159
|
+
|
|
160
|
+
**Bundled path:** [`cyclo_manager_cli/config/config.yml`](cyclo_manager_cli/config/config.yml)
|
|
161
|
+
|
|
162
|
+
The API reads it as `CONFIG_FILE=/app/config.yml` inside the container.
|
|
163
|
+
|
|
164
|
+
### Schema
|
|
165
|
+
|
|
166
|
+
| Key | Description |
|
|
167
|
+
|-----|-------------|
|
|
168
|
+
| **`robot_container`** | Primary robot Docker container name (e.g. `ai_worker`). Used by the UI System page and service bringup. Must be a key in `sockets` and cannot be `host_agent`. |
|
|
169
|
+
| **`sockets`** | Map of logical name → agent **Unix socket path as seen inside the API container** (under `/agents/...`). Include robot/service containers and `host_agent`. |
|
|
170
|
+
|
|
171
|
+
s6 **service names** are not listed in config; each in-container agent reports them at runtime.
|
|
172
|
+
|
|
173
|
+
### Example (bundled default)
|
|
174
|
+
|
|
175
|
+
```yaml
|
|
176
|
+
robot_container: ai_worker
|
|
177
|
+
|
|
178
|
+
sockets:
|
|
179
|
+
ai_worker: "/agents/ai_worker/s6_agent.sock"
|
|
180
|
+
physical_ai_server: "/agents/physical_ai_server/s6_agent.sock"
|
|
181
|
+
host_agent: "/agents/host/host_agent.sock"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Validation (API startup)
|
|
185
|
+
|
|
186
|
+
- At least one socket entry besides `host_agent`
|
|
187
|
+
- `robot_container` must exist in `sockets`
|
|
188
|
+
- All socket paths must be non-empty strings
|
|
189
|
+
|
|
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).
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Agent sockets on the host
|
|
195
|
+
|
|
196
|
+
On the host, sockets typically live under:
|
|
197
|
+
|
|
198
|
+
```text
|
|
199
|
+
/var/run/robotis/agent_sockets/
|
|
200
|
+
├── ai_worker/s6_agent.sock
|
|
201
|
+
├── physical_ai_server/s6_agent.sock
|
|
202
|
+
└── host/host_agent.sock ← created by cyclo_manager up
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The bundled compose file mounts the parent directory:
|
|
206
|
+
|
|
207
|
+
```yaml
|
|
208
|
+
- /var/run/robotis/agent_sockets:/agents
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
So `/agents/ai_worker/s6_agent.sock` in config corresponds to the host path above.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Environment variables
|
|
216
|
+
|
|
217
|
+
| Variable | Set by | Purpose |
|
|
218
|
+
|----------|--------|---------|
|
|
219
|
+
| **`CYCLO_MANAGER_CONFIG_FILE`** | CLI (`up`, `down`, `update`) | Absolute path to bundled `config.yml` on the host; mounted into the API container |
|
|
220
|
+
| **`HOSTNAME`** | CLI (default: machine hostname) | Passed to API as `HOST_HOSTNAME` |
|
|
221
|
+
| **`CONFIG_FILE`** | Compose (`/app/config.yml`) | Path inside the API container |
|
|
222
|
+
| **`ROS_DOMAIN_ID`** | **Not** set by CLI | Set inside robot containers (e.g. `~/.bashrc`) so DDS matches your fleet |
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## URLs (packaged stack)
|
|
227
|
+
|
|
228
|
+
With `network_mode: host` and default ports:
|
|
229
|
+
|
|
230
|
+
| What | URL |
|
|
231
|
+
|------|-----|
|
|
232
|
+
| Web UI | http://127.0.0.1:3000 |
|
|
233
|
+
| cyclo_manager API | http://127.0.0.1:8081 |
|
|
234
|
+
| OpenAPI (Swagger) | http://127.0.0.1:8081/docs |
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Custom config and development
|
|
239
|
+
|
|
240
|
+
The pip workflow is intended for **production robots** with pre-built images.
|
|
241
|
+
|
|
242
|
+
For development (local API/UI source, custom `config.yml`, hot reload):
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
# From repository root
|
|
246
|
+
docker compose -f docker-compose.dev.yml up
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
See the [root README](../README.md#development).
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Dependencies
|
|
254
|
+
|
|
255
|
+
Python dependencies declared in [`pyproject.toml`](pyproject.toml):
|
|
256
|
+
|
|
257
|
+
| Package | Used by |
|
|
258
|
+
|---------|---------|
|
|
259
|
+
| **fastapi** | `cyclo_host_agent` HTTP API |
|
|
260
|
+
| **uvicorn** | `cyclo_host_agent` server |
|
|
261
|
+
| **psutil** | Host agent / system utilities |
|
|
262
|
+
|
|
263
|
+
The **`cyclo_manager`** CLI itself uses only the Python standard library plus **`subprocess`** calls to **`docker compose`**, **`pip`**, **`systemctl`**, and **`sudo`**.
|
|
264
|
+
|
|
265
|
+
Runtime **outside** Python:
|
|
266
|
+
|
|
267
|
+
- **Docker** + **Compose v2**
|
|
268
|
+
- **systemd** (host agent service)
|
|
269
|
+
- Pre-built **Docker images** referenced in the bundled compose file
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## License
|
|
274
|
+
|
|
275
|
+
SPDX: **Apache-2.0** (see [pyproject.toml](pyproject.toml) and the repository [LICENSE](../LICENSE)).
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# cyclo_manager CLI
|
|
2
|
+
|
|
3
|
+
**PyPI package:** `cyclo-manager`
|
|
4
|
+
**Console commands:** `cyclo_manager` (underscore), `cyclo-manager` (hyphen)
|
|
5
|
+
**Host agent:** `cyclo_host_agent` (installed by the package, run via systemd)
|
|
6
|
+
|
|
7
|
+
This package is the **pip-installable launcher** for the cyclo_manager stack on a robot host. It:
|
|
8
|
+
|
|
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
|
+
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).
|
|
11
|
+
3. Can **`pip install -U`** itself and bring the Docker stack back up.
|
|
12
|
+
|
|
13
|
+
Implementation: [`cyclo_manager_cli/cli.py`](cyclo_manager_cli/cli.py), [`cyclo_host_agent/`](cyclo_host_agent/).
|
|
14
|
+
|
|
15
|
+
For the full monorepo (API source, UI source, dev compose), see the [repository root README](../README.md).
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Table of contents
|
|
20
|
+
|
|
21
|
+
- [Prerequisites](#prerequisites)
|
|
22
|
+
- [Install](#install)
|
|
23
|
+
- [Commands](#commands)
|
|
24
|
+
- [What `cyclo_manager up` does](#what-cyclo_manager-up-does)
|
|
25
|
+
- [Host agent (`cyclo_host_agent`)](#host-agent-cyclo_host_agent)
|
|
26
|
+
- [Compose services](#compose-services)
|
|
27
|
+
- [Configuration](#configuration)
|
|
28
|
+
- [Agent sockets on the host](#agent-sockets-on-the-host)
|
|
29
|
+
- [Environment variables](#environment-variables)
|
|
30
|
+
- [URLs (packaged stack)](#urls-packaged-stack)
|
|
31
|
+
- [Custom config and development](#custom-config-and-development)
|
|
32
|
+
- [Dependencies](#dependencies)
|
|
33
|
+
- [License](#license)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Prerequisites
|
|
38
|
+
|
|
39
|
+
- **Docker** with **Compose v2** (`docker compose` — the legacy `docker-compose` binary alone is not enough)
|
|
40
|
+
- **Python 3.10+**
|
|
41
|
+
- **`sudo`** — required to install the host agent systemd unit, socket directory permissions, and sudoers rules
|
|
42
|
+
- For **`cyclo_manager update`:** **`pip`** or **`pip3`** on `PATH`
|
|
43
|
+
- **Do not run as root** — the host agent runs as the invoking user (or `SUDO_USER` when using `sudo cyclo_manager up`). Running as root is rejected.
|
|
44
|
+
- **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
|
+
---
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
From **PyPI**:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install cyclo-manager
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Pre-release versions:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install --pre cyclo-manager==0.2.0.dev0
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
If `cyclo_manager` is not on `PATH`, add the user script directory:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
66
|
+
cyclo_manager up
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
If the entry point is missing:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
python3 -m cyclo_manager_cli.cli --help
|
|
73
|
+
python3 -m cyclo_manager_cli.cli up
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Commands
|
|
79
|
+
|
|
80
|
+
| Command | Behavior |
|
|
81
|
+
|--------|----------|
|
|
82
|
+
| `cyclo_manager up` | Install/refresh **cyclo_host_agent** (systemd unit, socket dir, sudoers), then `docker compose up -d` for **API + UI**, then `docker compose create --no-recreate` for **Zenoh** and **noVNC** (created but **not started**). Sets **`CYCLO_MANAGER_CONFIG_FILE`** to the bundled config. |
|
|
83
|
+
| `cyclo_manager up --pull` | `docker compose pull` first, then the same as `up`. |
|
|
84
|
+
| `cyclo_manager down` | **Tear down host agent** (stop/disable service, remove unit, sudoers, socket dir), then `docker compose down` for all services in the bundled compose file. |
|
|
85
|
+
| `cyclo_manager update` | `docker compose down` (containers only — **host agent stays installed**), `pip install -U cyclo-manager`, `docker compose up` + create optional services, then **refresh host agent** (unit, socket, sudoers). Optional `--pull` pulls images before the final `up`. |
|
|
86
|
+
| `cyclo_manager --help` | Subcommand overview |
|
|
87
|
+
|
|
88
|
+
**`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`.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## What `cyclo_manager up` does
|
|
93
|
+
|
|
94
|
+
Three phases:
|
|
95
|
+
|
|
96
|
+
```text
|
|
97
|
+
[1/3] Setting up host agent → systemd unit, socket ownership, sudoers
|
|
98
|
+
[2/3] Starting containers → cyclo_manager (API) + ui
|
|
99
|
+
[3/3] Creating (not starting) → zenoh_daemon + novnc-server
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Open **http://127.0.0.1:3000** (UI) and **http://127.0.0.1:8081/docs** (API).
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Host agent (`cyclo_host_agent`)
|
|
107
|
+
|
|
108
|
+
The pip package installs a small **FastAPI** server that listens on a **Unix domain socket** on the host:
|
|
109
|
+
|
|
110
|
+
| Item | Path |
|
|
111
|
+
|------|------|
|
|
112
|
+
| Socket | `/var/run/robotis/agent_sockets/host/host_agent.sock` |
|
|
113
|
+
| systemd unit | `cyclo_host_agent.service` |
|
|
114
|
+
| sudoers | `/etc/sudoers.d/cyclo_manager` (`cp`, `udevadm` for container setup scripts) |
|
|
115
|
+
|
|
116
|
+
The API container reaches it at `/agents/host/host_agent.sock` (see bundled `config.yml`).
|
|
117
|
+
|
|
118
|
+
**Responsibilities:**
|
|
119
|
+
|
|
120
|
+
- List and update **`~/ROBOTIS-GIT/*`** git repositories (Version Management in the UI)
|
|
121
|
+
- Run **`cyclo_manager update`** when triggered from the UI (`POST /host/update`)
|
|
122
|
+
|
|
123
|
+
`cyclo_manager up` and `cyclo_manager update` both call `_ensure_host_agent()`, which is idempotent — safe to run after upgrades or user changes.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Compose services
|
|
128
|
+
|
|
129
|
+
Defined in [`cyclo_manager_cli/docker/docker-compose.yml`](cyclo_manager_cli/docker/docker-compose.yml). All services use **`network_mode: host`**.
|
|
130
|
+
|
|
131
|
+
| Compose service | Container name | `cyclo_manager up` | Image (example) |
|
|
132
|
+
|-----------------|----------------|--------------------|-----------------|
|
|
133
|
+
| `cyclo_manager` | `cyclo_manager` | **Started** | `robotis/cyclo-manager:0.2.0` |
|
|
134
|
+
| `ui` | `cyclo_manager_ui` | **Started** | `robotis/cyclo-manager-ui:0.2.0` |
|
|
135
|
+
| `rmw_zenoh` | `zenoh_daemon` | **Created only** | `robotis/zenoh-daemon:latest` |
|
|
136
|
+
| `novnc-server` | `novnc-server` | **Created only** | `robotis/novnc-server:latest` |
|
|
137
|
+
|
|
138
|
+
Start optional containers from the UI or manually, e.g. `docker start zenoh_daemon`.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Configuration
|
|
143
|
+
|
|
144
|
+
The CLI always mounts the **bundled** config into the API container. There is **no** `-c` / `--config` flag on the pip CLI.
|
|
145
|
+
|
|
146
|
+
**Bundled path:** [`cyclo_manager_cli/config/config.yml`](cyclo_manager_cli/config/config.yml)
|
|
147
|
+
|
|
148
|
+
The API reads it as `CONFIG_FILE=/app/config.yml` inside the container.
|
|
149
|
+
|
|
150
|
+
### Schema
|
|
151
|
+
|
|
152
|
+
| Key | Description |
|
|
153
|
+
|-----|-------------|
|
|
154
|
+
| **`robot_container`** | Primary robot Docker container name (e.g. `ai_worker`). Used by the UI System page and service bringup. Must be a key in `sockets` and cannot be `host_agent`. |
|
|
155
|
+
| **`sockets`** | Map of logical name → agent **Unix socket path as seen inside the API container** (under `/agents/...`). Include robot/service containers and `host_agent`. |
|
|
156
|
+
|
|
157
|
+
s6 **service names** are not listed in config; each in-container agent reports them at runtime.
|
|
158
|
+
|
|
159
|
+
### Example (bundled default)
|
|
160
|
+
|
|
161
|
+
```yaml
|
|
162
|
+
robot_container: ai_worker
|
|
163
|
+
|
|
164
|
+
sockets:
|
|
165
|
+
ai_worker: "/agents/ai_worker/s6_agent.sock"
|
|
166
|
+
physical_ai_server: "/agents/physical_ai_server/s6_agent.sock"
|
|
167
|
+
host_agent: "/agents/host/host_agent.sock"
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Validation (API startup)
|
|
171
|
+
|
|
172
|
+
- At least one socket entry besides `host_agent`
|
|
173
|
+
- `robot_container` must exist in `sockets`
|
|
174
|
+
- All socket paths must be non-empty strings
|
|
175
|
+
|
|
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).
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Agent sockets on the host
|
|
181
|
+
|
|
182
|
+
On the host, sockets typically live under:
|
|
183
|
+
|
|
184
|
+
```text
|
|
185
|
+
/var/run/robotis/agent_sockets/
|
|
186
|
+
├── ai_worker/s6_agent.sock
|
|
187
|
+
├── physical_ai_server/s6_agent.sock
|
|
188
|
+
└── host/host_agent.sock ← created by cyclo_manager up
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The bundled compose file mounts the parent directory:
|
|
192
|
+
|
|
193
|
+
```yaml
|
|
194
|
+
- /var/run/robotis/agent_sockets:/agents
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
So `/agents/ai_worker/s6_agent.sock` in config corresponds to the host path above.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Environment variables
|
|
202
|
+
|
|
203
|
+
| Variable | Set by | Purpose |
|
|
204
|
+
|----------|--------|---------|
|
|
205
|
+
| **`CYCLO_MANAGER_CONFIG_FILE`** | CLI (`up`, `down`, `update`) | Absolute path to bundled `config.yml` on the host; mounted into the API container |
|
|
206
|
+
| **`HOSTNAME`** | CLI (default: machine hostname) | Passed to API as `HOST_HOSTNAME` |
|
|
207
|
+
| **`CONFIG_FILE`** | Compose (`/app/config.yml`) | Path inside the API container |
|
|
208
|
+
| **`ROS_DOMAIN_ID`** | **Not** set by CLI | Set inside robot containers (e.g. `~/.bashrc`) so DDS matches your fleet |
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## URLs (packaged stack)
|
|
213
|
+
|
|
214
|
+
With `network_mode: host` and default ports:
|
|
215
|
+
|
|
216
|
+
| What | URL |
|
|
217
|
+
|------|-----|
|
|
218
|
+
| Web UI | http://127.0.0.1:3000 |
|
|
219
|
+
| cyclo_manager API | http://127.0.0.1:8081 |
|
|
220
|
+
| OpenAPI (Swagger) | http://127.0.0.1:8081/docs |
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Custom config and development
|
|
225
|
+
|
|
226
|
+
The pip workflow is intended for **production robots** with pre-built images.
|
|
227
|
+
|
|
228
|
+
For development (local API/UI source, custom `config.yml`, hot reload):
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
# From repository root
|
|
232
|
+
docker compose -f docker-compose.dev.yml up
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
See the [root README](../README.md#development).
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Dependencies
|
|
240
|
+
|
|
241
|
+
Python dependencies declared in [`pyproject.toml`](pyproject.toml):
|
|
242
|
+
|
|
243
|
+
| Package | Used by |
|
|
244
|
+
|---------|---------|
|
|
245
|
+
| **fastapi** | `cyclo_host_agent` HTTP API |
|
|
246
|
+
| **uvicorn** | `cyclo_host_agent` server |
|
|
247
|
+
| **psutil** | Host agent / system utilities |
|
|
248
|
+
|
|
249
|
+
The **`cyclo_manager`** CLI itself uses only the Python standard library plus **`subprocess`** calls to **`docker compose`**, **`pip`**, **`systemctl`**, and **`sudo`**.
|
|
250
|
+
|
|
251
|
+
Runtime **outside** Python:
|
|
252
|
+
|
|
253
|
+
- **Docker** + **Compose v2**
|
|
254
|
+
- **systemd** (host agent service)
|
|
255
|
+
- Pre-built **Docker images** referenced in the bundled compose file
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## License
|
|
260
|
+
|
|
261
|
+
SPDX: **Apache-2.0** (see [pyproject.toml](pyproject.toml) and the repository [LICENSE](../LICENSE)).
|
|
@@ -52,6 +52,7 @@ def _resolve_home() -> Path:
|
|
|
52
52
|
|
|
53
53
|
WORKSPACE_PATH = _resolve_home()
|
|
54
54
|
MANAGED_GITHUB_ORG = 'ROBOTIS-GIT'
|
|
55
|
+
ALLOWED_UPDATE_BRANCHES = frozenset({'main', 'jazzy'})
|
|
55
56
|
|
|
56
57
|
|
|
57
58
|
# ── shared helpers ─────────────────────────────────────────────────────────────
|
|
@@ -84,6 +85,41 @@ def _fmt_cmd(cmd: str, output: str) -> str:
|
|
|
84
85
|
|
|
85
86
|
# ── git helpers ────────────────────────────────────────────────────────────────
|
|
86
87
|
|
|
88
|
+
async def _stash_count(cwd: Path) -> int:
|
|
89
|
+
rc, out, _ = await _git(['stash', 'list'], cwd=cwd)
|
|
90
|
+
if rc != 0:
|
|
91
|
+
return 0
|
|
92
|
+
return sum(1 for line in out.splitlines() if line.strip())
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def _current_branch(repo_path: Path) -> Optional[str]:
|
|
96
|
+
rc, out, _ = await _git(['rev-parse', '--abbrev-ref', 'HEAD'], cwd=repo_path)
|
|
97
|
+
if rc != 0:
|
|
98
|
+
return None
|
|
99
|
+
branch = out.strip()
|
|
100
|
+
if not branch or branch == 'HEAD':
|
|
101
|
+
return None
|
|
102
|
+
return branch
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def _ensure_allowed_branch(repo_path: Path) -> None:
|
|
106
|
+
branch = await _current_branch(repo_path)
|
|
107
|
+
if branch in ALLOWED_UPDATE_BRANCHES:
|
|
108
|
+
return
|
|
109
|
+
allowed = ', '.join(sorted(ALLOWED_UPDATE_BRANCHES))
|
|
110
|
+
if branch:
|
|
111
|
+
detail = (
|
|
112
|
+
f"Updates are only allowed on branches: {allowed}. "
|
|
113
|
+
f"Current branch is '{branch}'."
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
detail = (
|
|
117
|
+
f"Updates are only allowed on branches: {allowed}. "
|
|
118
|
+
'Could not determine the current branch (detached HEAD?).'
|
|
119
|
+
)
|
|
120
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=detail)
|
|
121
|
+
|
|
122
|
+
|
|
87
123
|
async def _git(args: list[str], cwd: Optional[Path] = None) -> tuple[int, str, str]:
|
|
88
124
|
proc = await asyncio.create_subprocess_exec(
|
|
89
125
|
'git', *args,
|
|
@@ -202,17 +238,23 @@ async def _update_stash(repo_path: Path) -> UpdateResponse:
|
|
|
202
238
|
name = repo_path.name
|
|
203
239
|
lines: list[str] = []
|
|
204
240
|
|
|
241
|
+
stash_before = await _stash_count(repo_path)
|
|
205
242
|
rc, out, err = await _git(['stash', '-u'], cwd=repo_path)
|
|
206
243
|
lines.append(_fmt_cmd('git stash -u', out + err))
|
|
207
244
|
if rc != 0:
|
|
208
245
|
return UpdateResponse(name=name, success=False, output='\n'.join(lines))
|
|
246
|
+
stashed = await _stash_count(repo_path) > stash_before
|
|
209
247
|
|
|
210
248
|
rc, out, err = await _git(['pull'], cwd=repo_path)
|
|
211
249
|
lines.append(_fmt_cmd('git pull', out + err))
|
|
212
250
|
if rc != 0:
|
|
213
|
-
|
|
251
|
+
if stashed:
|
|
252
|
+
await _git(['stash', 'pop'], cwd=repo_path)
|
|
214
253
|
return UpdateResponse(name=name, success=False, output='\n'.join(lines))
|
|
215
254
|
|
|
255
|
+
if not stashed:
|
|
256
|
+
return UpdateResponse(name=name, success=True, output='\n'.join(lines))
|
|
257
|
+
|
|
216
258
|
rc, out, err = await _git(['stash', 'pop'], cwd=repo_path)
|
|
217
259
|
lines.append(_fmt_cmd('git stash pop', out + err))
|
|
218
260
|
|
|
@@ -347,6 +389,7 @@ async def start_repo_container(name: str) -> ContainerScriptResponse:
|
|
|
347
389
|
@router.post('/{name}/update', response_model=UpdateResponse)
|
|
348
390
|
async def update_repo(name: str, req: UpdateRequest) -> UpdateResponse:
|
|
349
391
|
repo_path = _get_repo(name)
|
|
392
|
+
await _ensure_allowed_branch(repo_path)
|
|
350
393
|
try:
|
|
351
394
|
if req.strategy == 'reset':
|
|
352
395
|
return await _update_reset(repo_path, req.preserve_files)
|