cyclo-manager 0.2.0.dev5__tar.gz → 0.2.0.dev7__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.dev5 → cyclo_manager-0.2.0.dev7}/PKG-INFO +1 -1
- {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev7}/cyclo_host_agent/routers/repos.py +4 -6
- cyclo_manager-0.2.0.dev7/cyclo_host_agent/routers/update.py +96 -0
- {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev7}/cyclo_manager.egg-info/PKG-INFO +1 -1
- {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev7}/cyclo_manager_cli/cli.py +92 -19
- cyclo_manager-0.2.0.dev7/cyclo_manager_cli/config/config.yml +8 -0
- {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev7}/pyproject.toml +1 -1
- {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev7}/setup.cfg +1 -1
- cyclo_manager-0.2.0.dev5/cyclo_host_agent/routers/update.py +0 -175
- cyclo_manager-0.2.0.dev5/cyclo_manager_cli/config/config.yml +0 -10
- {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev7}/README.md +0 -0
- {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev7}/cyclo_host_agent/__init__.py +0 -0
- {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev7}/cyclo_host_agent/main.py +0 -0
- {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev7}/cyclo_host_agent/models.py +0 -0
- {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev7}/cyclo_host_agent/routers/__init__.py +0 -0
- {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev7}/cyclo_manager.egg-info/SOURCES.txt +0 -0
- {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev7}/cyclo_manager.egg-info/dependency_links.txt +0 -0
- {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev7}/cyclo_manager.egg-info/entry_points.txt +0 -0
- {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev7}/cyclo_manager.egg-info/requires.txt +0 -0
- {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev7}/cyclo_manager.egg-info/top_level.txt +0 -0
- {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev7}/cyclo_manager_cli/__init__.py +0 -0
- {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev7}/cyclo_manager_cli/docker/docker-compose.yml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cyclo-manager
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev7
|
|
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
|
|
@@ -44,9 +44,9 @@ GIT_TIMEOUT = 120.0
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
def _resolve_home() -> Path:
|
|
47
|
-
|
|
48
|
-
if
|
|
49
|
-
return Path(
|
|
47
|
+
sudo_user = os.environ.get('SUDO_USER')
|
|
48
|
+
if sudo_user:
|
|
49
|
+
return Path(f'/home/{sudo_user}')
|
|
50
50
|
return Path.home()
|
|
51
51
|
|
|
52
52
|
|
|
@@ -85,10 +85,8 @@ def _fmt_cmd(cmd: str, output: str) -> str:
|
|
|
85
85
|
# ── git helpers ────────────────────────────────────────────────────────────────
|
|
86
86
|
|
|
87
87
|
async def _git(args: list[str], cwd: Optional[Path] = None) -> tuple[int, str, str]:
|
|
88
|
-
user = os.environ.get('CYCLO_MANAGER_USER')
|
|
89
|
-
cmd = ['runuser', '-u', user, '--', 'git', *args] if user else ['git', *args]
|
|
90
88
|
proc = await asyncio.create_subprocess_exec(
|
|
91
|
-
*
|
|
89
|
+
'git', *args,
|
|
92
90
|
cwd=str(cwd) if cwd else None,
|
|
93
91
|
stdout=asyncio.subprocess.PIPE,
|
|
94
92
|
stderr=asyncio.subprocess.PIPE,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
#
|
|
3
|
+
# Copyright 2026 ROBOTIS CO., LTD.
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
#
|
|
17
|
+
# Author: Hyungyu Kim
|
|
18
|
+
|
|
19
|
+
"""System-level endpoints for the host agent."""
|
|
20
|
+
|
|
21
|
+
import asyncio
|
|
22
|
+
import logging
|
|
23
|
+
import shutil
|
|
24
|
+
import time
|
|
25
|
+
import subprocess
|
|
26
|
+
|
|
27
|
+
from fastapi import APIRouter, HTTPException, status
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
router = APIRouter(prefix='/system', tags=['system'])
|
|
32
|
+
|
|
33
|
+
_update_status: dict = {'phase': 'idle', 'output': '', 'error': ''}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def _run_update(cyclo_exe: str) -> None:
|
|
37
|
+
global _update_status
|
|
38
|
+
try:
|
|
39
|
+
proc = await asyncio.create_subprocess_exec(
|
|
40
|
+
cyclo_exe, 'update',
|
|
41
|
+
stdout=asyncio.subprocess.PIPE,
|
|
42
|
+
stderr=asyncio.subprocess.PIPE,
|
|
43
|
+
)
|
|
44
|
+
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=600)
|
|
45
|
+
out = (stdout.decode() + stderr.decode()).strip()
|
|
46
|
+
_update_status['output'] = out
|
|
47
|
+
if proc.returncode != 0:
|
|
48
|
+
_update_status['phase'] = 'error'
|
|
49
|
+
_update_status['error'] = f'cyclo_manager update failed (exit {proc.returncode})'
|
|
50
|
+
else:
|
|
51
|
+
_update_status['phase'] = 'done'
|
|
52
|
+
except asyncio.TimeoutError:
|
|
53
|
+
_update_status['phase'] = 'error'
|
|
54
|
+
_update_status['error'] = 'cyclo_manager update timed out'
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@router.post('/update')
|
|
58
|
+
async def start_update() -> dict:
|
|
59
|
+
"""Delegate update to cyclo_manager update, which handles containers and host agent restart."""
|
|
60
|
+
global _update_status
|
|
61
|
+
|
|
62
|
+
cyclo_exe = shutil.which('cyclo_manager')
|
|
63
|
+
if not cyclo_exe:
|
|
64
|
+
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
65
|
+
detail='cyclo_manager command not found')
|
|
66
|
+
|
|
67
|
+
_update_status = {'phase': 'updating', 'output': '', 'error': ''}
|
|
68
|
+
asyncio.create_task(_run_update(cyclo_exe))
|
|
69
|
+
return {'status': 'update started'}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@router.get('/update/status')
|
|
73
|
+
async def get_update_status() -> dict:
|
|
74
|
+
"""Return the current update phase and stored outputs."""
|
|
75
|
+
return _update_status
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _delayed_exec(cmd: list[str], delay: float = 2.0) -> None:
|
|
79
|
+
time.sleep(delay)
|
|
80
|
+
subprocess.run(cmd, check=False)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@router.post('/reboot')
|
|
84
|
+
async def reboot_host() -> dict:
|
|
85
|
+
"""Reboot the host machine after returning a response."""
|
|
86
|
+
loop = asyncio.get_event_loop()
|
|
87
|
+
loop.run_in_executor(None, _delayed_exec, ['sudo', 'reboot'])
|
|
88
|
+
return {'status': 'rebooting'}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@router.post('/shutdown')
|
|
92
|
+
async def shutdown_host() -> dict:
|
|
93
|
+
"""Shut down the host machine after returning a response."""
|
|
94
|
+
loop = asyncio.get_event_loop()
|
|
95
|
+
loop.run_in_executor(None, _delayed_exec, ['sudo', 'poweroff'])
|
|
96
|
+
return {'status': 'shutting_down'}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cyclo-manager
|
|
3
|
-
Version: 0.2.0.
|
|
3
|
+
Version: 0.2.0.dev7
|
|
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
|
|
@@ -87,6 +87,11 @@ def _setup_socket_dir(user: str) -> bool:
|
|
|
87
87
|
check=True,
|
|
88
88
|
capture_output=True,
|
|
89
89
|
)
|
|
90
|
+
subprocess.run(
|
|
91
|
+
['sudo', 'chown', f'{user}:{user}', HOST_AGENT_SOCKET_DIR],
|
|
92
|
+
check=True,
|
|
93
|
+
capture_output=True,
|
|
94
|
+
)
|
|
90
95
|
subprocess.run(
|
|
91
96
|
['sudo', 'rm', '-f', HOST_AGENT_SOCKET],
|
|
92
97
|
check=True,
|
|
@@ -98,6 +103,35 @@ def _setup_socket_dir(user: str) -> bool:
|
|
|
98
103
|
return False
|
|
99
104
|
|
|
100
105
|
|
|
106
|
+
def _install_sudoers(user: str) -> bool:
|
|
107
|
+
"""
|
|
108
|
+
Write /etc/sudoers.d/cyclo_manager granting NOPASSWD for udev operations
|
|
109
|
+
needed by container.sh (cp to /etc/udev/rules.d/, udevadm control/trigger).
|
|
110
|
+
"""
|
|
111
|
+
sudoers_content = (
|
|
112
|
+
f'{user} ALL=(ALL) NOPASSWD: /usr/bin/cp, '
|
|
113
|
+
f'/usr/bin/udevadm control --reload-rules, '
|
|
114
|
+
f'/usr/bin/udevadm trigger\n'
|
|
115
|
+
)
|
|
116
|
+
sudoers_file = '/etc/sudoers.d/cyclo_manager'
|
|
117
|
+
try:
|
|
118
|
+
subprocess.run(
|
|
119
|
+
['sudo', 'tee', sudoers_file],
|
|
120
|
+
input=sudoers_content.encode(),
|
|
121
|
+
capture_output=True,
|
|
122
|
+
check=True,
|
|
123
|
+
)
|
|
124
|
+
subprocess.run(
|
|
125
|
+
['sudo', 'chmod', '440', sudoers_file],
|
|
126
|
+
check=True,
|
|
127
|
+
capture_output=True,
|
|
128
|
+
)
|
|
129
|
+
return True
|
|
130
|
+
except subprocess.CalledProcessError as e:
|
|
131
|
+
print(f'Failed to install sudoers file: {e}', file=sys.stderr)
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
|
|
101
135
|
def _ensure_host_agent() -> int:
|
|
102
136
|
"""
|
|
103
137
|
Install or refresh cyclo_host_agent: systemd unit, socket dir, and service start.
|
|
@@ -122,6 +156,9 @@ def _ensure_host_agent() -> int:
|
|
|
122
156
|
if not _setup_socket_dir(user):
|
|
123
157
|
return 1
|
|
124
158
|
|
|
159
|
+
if not _install_sudoers(user):
|
|
160
|
+
return 1
|
|
161
|
+
|
|
125
162
|
service_content = f"""\
|
|
126
163
|
[Unit]
|
|
127
164
|
Description=Cyclo Host Agent
|
|
@@ -129,9 +166,10 @@ After=network.target
|
|
|
129
166
|
|
|
130
167
|
[Service]
|
|
131
168
|
Type=simple
|
|
169
|
+
User={user}
|
|
170
|
+
Group={user}
|
|
132
171
|
ExecStart={agent_exe}
|
|
133
|
-
Environment=
|
|
134
|
-
Environment=CYCLO_MANAGER_USER_HOME={user_home}
|
|
172
|
+
Environment=HOME={user_home}
|
|
135
173
|
Restart=always
|
|
136
174
|
RestartSec=3
|
|
137
175
|
|
|
@@ -202,8 +240,27 @@ def cmd_up(args: argparse.Namespace) -> int:
|
|
|
202
240
|
return 0
|
|
203
241
|
|
|
204
242
|
|
|
243
|
+
def _teardown_host_agent() -> None:
|
|
244
|
+
"""Stop and remove the host agent service, unit file, sudoers, and socket dir."""
|
|
245
|
+
steps = [
|
|
246
|
+
(['sudo', 'systemctl', 'stop', f'{HOST_AGENT_SERVICE}.service'], 'stop service'),
|
|
247
|
+
(['sudo', 'systemctl', 'disable', f'{HOST_AGENT_SERVICE}.service'], 'disable service'),
|
|
248
|
+
(['sudo', 'rm', '-f', f'/etc/systemd/system/{HOST_AGENT_SERVICE}.service'], 'remove unit file'),
|
|
249
|
+
(['sudo', 'systemctl', 'daemon-reload'], 'reload systemd'),
|
|
250
|
+
(['sudo', 'rm', '-f', '/etc/sudoers.d/cyclo_manager'], 'remove sudoers'),
|
|
251
|
+
(['sudo', 'rm', '-rf', HOST_AGENT_SOCKET_DIR], 'remove socket dir'),
|
|
252
|
+
]
|
|
253
|
+
for cmd, desc in steps:
|
|
254
|
+
try:
|
|
255
|
+
subprocess.run(cmd, check=True, capture_output=True)
|
|
256
|
+
except subprocess.CalledProcessError:
|
|
257
|
+
print(f'Warning: failed to {desc}.', file=sys.stderr)
|
|
258
|
+
|
|
259
|
+
|
|
205
260
|
def cmd_down(args: argparse.Namespace) -> int:
|
|
206
261
|
"""Stop cyclo_manager server, cyclo_manager_ui, and zenoh daemon (docker compose down)."""
|
|
262
|
+
_teardown_host_agent()
|
|
263
|
+
|
|
207
264
|
compose_path = _docker_dir() / 'docker-compose.yml'
|
|
208
265
|
if not compose_path.is_file():
|
|
209
266
|
print(f'Compose file not found: {compose_path}', file=sys.stderr)
|
|
@@ -233,29 +290,32 @@ def cmd_down(args: argparse.Namespace) -> int:
|
|
|
233
290
|
|
|
234
291
|
|
|
235
292
|
def cmd_update(args: argparse.Namespace) -> int:
|
|
236
|
-
"""
|
|
237
|
-
cyclo_manager_exe = shutil.which('cyclo_manager')
|
|
238
|
-
if not cyclo_manager_exe:
|
|
239
|
-
print('cyclo_manager command not found in PATH.', file=sys.stderr)
|
|
240
|
-
return 1
|
|
293
|
+
"""Bring containers down, pip install -U, bring containers back up, restart host agent."""
|
|
241
294
|
pip_exe = shutil.which('pip3') or shutil.which('pip')
|
|
242
295
|
if not pip_exe:
|
|
243
296
|
print('pip not found; cannot update package.', file=sys.stderr)
|
|
244
297
|
return 1
|
|
245
298
|
|
|
246
|
-
|
|
299
|
+
compose_path = _docker_dir() / 'docker-compose.yml'
|
|
300
|
+
if not compose_path.is_file():
|
|
301
|
+
print(f'Compose file not found: {compose_path}', file=sys.stderr)
|
|
302
|
+
return 1
|
|
303
|
+
env = os.environ.copy()
|
|
304
|
+
env['CYCLO_MANAGER_CONFIG_FILE'] = str(_packaged_config_path())
|
|
305
|
+
base = ['docker', 'compose', '-f', str(compose_path)]
|
|
306
|
+
|
|
307
|
+
print('Stopping containers...')
|
|
247
308
|
try:
|
|
248
|
-
subprocess.run([
|
|
309
|
+
subprocess.run([*base, 'down'], env=env, check=True)
|
|
249
310
|
except subprocess.CalledProcessError as e:
|
|
250
311
|
return e.returncode
|
|
312
|
+
except FileNotFoundError:
|
|
313
|
+
print('Docker not found.', file=sys.stderr)
|
|
314
|
+
return 1
|
|
251
315
|
|
|
252
316
|
print(f'Updating {PYPI_PACKAGE} (pip install -U)...')
|
|
253
317
|
try:
|
|
254
|
-
subprocess.run(
|
|
255
|
-
[pip_exe, 'install', '-U', PYPI_PACKAGE],
|
|
256
|
-
check=True,
|
|
257
|
-
timeout=120,
|
|
258
|
-
)
|
|
318
|
+
subprocess.run([pip_exe, 'install', '-U', PYPI_PACKAGE], check=True, timeout=120)
|
|
259
319
|
except subprocess.CalledProcessError as e:
|
|
260
320
|
print(f'pip install -U {PYPI_PACKAGE} failed.', file=sys.stderr)
|
|
261
321
|
return e.returncode
|
|
@@ -263,15 +323,28 @@ def cmd_update(args: argparse.Namespace) -> int:
|
|
|
263
323
|
print('pip install timed out.', file=sys.stderr)
|
|
264
324
|
return 1
|
|
265
325
|
|
|
266
|
-
print('Starting containers
|
|
267
|
-
up_args = [cyclo_manager_exe, 'up']
|
|
268
|
-
if getattr(args, 'pull', False):
|
|
269
|
-
up_args.append('--pull')
|
|
326
|
+
print('Starting containers...')
|
|
270
327
|
try:
|
|
271
|
-
|
|
328
|
+
if getattr(args, 'pull', False):
|
|
329
|
+
subprocess.run([*base, 'pull'], env=env, check=True)
|
|
330
|
+
subprocess.run([*base, 'up', '-d', *COMPOSE_SERVICES_UP], env=env, check=True)
|
|
331
|
+
subprocess.run(
|
|
332
|
+
[*base, 'create', '--no-recreate', *COMPOSE_SERVICES_CREATE_ONLY],
|
|
333
|
+
env=env,
|
|
334
|
+
check=True,
|
|
335
|
+
)
|
|
272
336
|
except subprocess.CalledProcessError as e:
|
|
273
337
|
return e.returncode
|
|
274
338
|
|
|
339
|
+
print('Restarting host agent...')
|
|
340
|
+
try:
|
|
341
|
+
subprocess.run(
|
|
342
|
+
['sudo', 'systemctl', 'restart', f'{HOST_AGENT_SERVICE}.service'],
|
|
343
|
+
check=True,
|
|
344
|
+
)
|
|
345
|
+
except subprocess.CalledProcessError as e:
|
|
346
|
+
print(f'Warning: failed to restart host agent: {e}', file=sys.stderr)
|
|
347
|
+
|
|
275
348
|
print('cyclo_manager update completed.')
|
|
276
349
|
return 0
|
|
277
350
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cyclo-manager"
|
|
7
|
-
version = "0.2.0.
|
|
7
|
+
version = "0.2.0.dev7"
|
|
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"
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
#
|
|
3
|
-
# Copyright 2026 ROBOTIS CO., LTD.
|
|
4
|
-
#
|
|
5
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
-
# you may not use this file except in compliance with the License.
|
|
7
|
-
# You may obtain a copy of the License at
|
|
8
|
-
#
|
|
9
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
-
#
|
|
11
|
-
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
-
# See the License for the specific language governing permissions and
|
|
15
|
-
# limitations under the License.
|
|
16
|
-
#
|
|
17
|
-
# Author: Hyungyu Kim
|
|
18
|
-
|
|
19
|
-
"""System-level endpoints for the host agent."""
|
|
20
|
-
|
|
21
|
-
import asyncio
|
|
22
|
-
import logging
|
|
23
|
-
import shutil
|
|
24
|
-
import subprocess
|
|
25
|
-
import time
|
|
26
|
-
from typing import Optional
|
|
27
|
-
|
|
28
|
-
from fastapi import APIRouter, HTTPException, status
|
|
29
|
-
|
|
30
|
-
logger = logging.getLogger(__name__)
|
|
31
|
-
|
|
32
|
-
router = APIRouter(prefix='/system', tags=['system'])
|
|
33
|
-
|
|
34
|
-
PYPI_PACKAGE = 'cyclo-manager'
|
|
35
|
-
HOST_AGENT_SERVICE = 'cyclo_host_agent'
|
|
36
|
-
|
|
37
|
-
# Stored result of the last update (survives across the server restart window)
|
|
38
|
-
_update_status: dict = {'phase': 'idle', 'install_output': '', 'up_output': '', 'error': ''}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _fmt(cmd: str, out: str) -> str:
|
|
42
|
-
return f'$ {cmd}\n{out.strip()}' if out.strip() else f'$ {cmd}'
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _restart_self() -> None:
|
|
46
|
-
"""Restart the host agent after a short delay so new binary takes effect."""
|
|
47
|
-
time.sleep(2)
|
|
48
|
-
try:
|
|
49
|
-
subprocess.run(
|
|
50
|
-
['sudo', 'systemctl', 'restart', f'{HOST_AGENT_SERVICE}.service'],
|
|
51
|
-
check=True,
|
|
52
|
-
)
|
|
53
|
-
except Exception as e:
|
|
54
|
-
logger.error('Failed to restart host agent: %s', e)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
async def _run_install_and_up(cyclo_exe: str, pip_exe: str) -> None:
|
|
58
|
-
"""Run pip install -U and cyclo up in background; store results in _update_status."""
|
|
59
|
-
global _update_status
|
|
60
|
-
|
|
61
|
-
# pip install -U
|
|
62
|
-
_update_status['phase'] = 'installing'
|
|
63
|
-
try:
|
|
64
|
-
proc = await asyncio.create_subprocess_exec(
|
|
65
|
-
pip_exe, 'install', '-U', PYPI_PACKAGE,
|
|
66
|
-
stdout=asyncio.subprocess.PIPE,
|
|
67
|
-
stderr=asyncio.subprocess.PIPE,
|
|
68
|
-
)
|
|
69
|
-
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=120)
|
|
70
|
-
out = (stdout.decode() + stderr.decode()).strip()
|
|
71
|
-
_update_status['install_output'] = _fmt(f'pip install -U {PYPI_PACKAGE}', out)
|
|
72
|
-
if proc.returncode != 0:
|
|
73
|
-
_update_status['phase'] = 'error'
|
|
74
|
-
_update_status['error'] = f'pip install failed (exit {proc.returncode})'
|
|
75
|
-
return
|
|
76
|
-
except asyncio.TimeoutError:
|
|
77
|
-
_update_status['phase'] = 'error'
|
|
78
|
-
_update_status['error'] = 'pip install timed out'
|
|
79
|
-
return
|
|
80
|
-
|
|
81
|
-
# cyclo up
|
|
82
|
-
_update_status['phase'] = 'starting'
|
|
83
|
-
try:
|
|
84
|
-
proc = await asyncio.create_subprocess_exec(
|
|
85
|
-
cyclo_exe, 'up',
|
|
86
|
-
stdout=asyncio.subprocess.PIPE,
|
|
87
|
-
stderr=asyncio.subprocess.PIPE,
|
|
88
|
-
)
|
|
89
|
-
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=300)
|
|
90
|
-
out = (stdout.decode() + stderr.decode()).strip()
|
|
91
|
-
_update_status['up_output'] = _fmt('cyclo_manager up', out)
|
|
92
|
-
if proc.returncode != 0:
|
|
93
|
-
_update_status['phase'] = 'error'
|
|
94
|
-
_update_status['error'] = f'cyclo_manager up failed (exit {proc.returncode})'
|
|
95
|
-
return
|
|
96
|
-
except asyncio.TimeoutError:
|
|
97
|
-
_update_status['phase'] = 'error'
|
|
98
|
-
_update_status['error'] = 'cyclo_manager up timed out'
|
|
99
|
-
return
|
|
100
|
-
|
|
101
|
-
_update_status['phase'] = 'done'
|
|
102
|
-
|
|
103
|
-
# Restart self so new binary takes effect
|
|
104
|
-
loop = asyncio.get_event_loop()
|
|
105
|
-
await loop.run_in_executor(None, _restart_self)
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
@router.post('/update')
|
|
109
|
-
async def start_update() -> dict:
|
|
110
|
-
"""
|
|
111
|
-
Run cyclo down synchronously, then pip install + cyclo up in background.
|
|
112
|
-
|
|
113
|
-
Returns cyclo down output immediately. The server goes down during this call;
|
|
114
|
-
the host agent continues the remaining steps and stores results for /system/update/status.
|
|
115
|
-
"""
|
|
116
|
-
global _update_status
|
|
117
|
-
|
|
118
|
-
cyclo_exe = shutil.which('cyclo_manager')
|
|
119
|
-
pip_exe = shutil.which('pip3') or shutil.which('pip')
|
|
120
|
-
|
|
121
|
-
if not cyclo_exe:
|
|
122
|
-
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
123
|
-
detail='cyclo_manager command not found')
|
|
124
|
-
if not pip_exe:
|
|
125
|
-
raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
126
|
-
detail='pip not found')
|
|
127
|
-
|
|
128
|
-
_update_status = {'phase': 'stopping', 'install_output': '', 'up_output': '', 'error': ''}
|
|
129
|
-
|
|
130
|
-
# cyclo down (synchronous — server goes down here)
|
|
131
|
-
down_output = ''
|
|
132
|
-
try:
|
|
133
|
-
proc = await asyncio.create_subprocess_exec(
|
|
134
|
-
cyclo_exe, 'down',
|
|
135
|
-
stdout=asyncio.subprocess.PIPE,
|
|
136
|
-
stderr=asyncio.subprocess.PIPE,
|
|
137
|
-
)
|
|
138
|
-
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=30)
|
|
139
|
-
down_output = _fmt('cyclo_manager down', (stdout.decode() + stderr.decode()))
|
|
140
|
-
except asyncio.TimeoutError:
|
|
141
|
-
down_output = '$ cyclo_manager down\n(timed out)'
|
|
142
|
-
except Exception as e:
|
|
143
|
-
down_output = f'$ cyclo_manager down\n(error: {e})'
|
|
144
|
-
|
|
145
|
-
# Continue install + up in background
|
|
146
|
-
asyncio.create_task(_run_install_and_up(cyclo_exe, pip_exe))
|
|
147
|
-
|
|
148
|
-
return {'down_output': down_output}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
@router.get('/update/status')
|
|
152
|
-
async def get_update_status() -> dict:
|
|
153
|
-
"""Return the current update phase and stored outputs."""
|
|
154
|
-
return _update_status
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def _delayed_exec(cmd: list[str], delay: float = 2.0) -> None:
|
|
158
|
-
time.sleep(delay)
|
|
159
|
-
subprocess.run(cmd, check=False)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
@router.post('/reboot')
|
|
163
|
-
async def reboot_host() -> dict:
|
|
164
|
-
"""Reboot the host machine after returning a response."""
|
|
165
|
-
loop = asyncio.get_event_loop()
|
|
166
|
-
loop.run_in_executor(None, _delayed_exec, ['sudo', 'reboot'])
|
|
167
|
-
return {'status': 'rebooting'}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
@router.post('/shutdown')
|
|
171
|
-
async def shutdown_host() -> dict:
|
|
172
|
-
"""Shut down the host machine after returning a response."""
|
|
173
|
-
loop = asyncio.get_event_loop()
|
|
174
|
-
loop.run_in_executor(None, _delayed_exec, ['sudo', 'poweroff'])
|
|
175
|
-
return {'status': 'shutting_down'}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
# cyclo_manager Configuration
|
|
2
|
-
# This file defines the containers that cyclo_manager can control.
|
|
3
|
-
#
|
|
4
|
-
# Services are discovered dynamically from each agent.
|
|
5
|
-
|
|
6
|
-
containers:
|
|
7
|
-
ai_worker:
|
|
8
|
-
socket_path: "/agents/ai_worker/s6_agent.sock"
|
|
9
|
-
physical_ai_server:
|
|
10
|
-
socket_path: "/agents/physical_ai_server/s6_agent.sock"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev7}/cyclo_manager.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev7}/cyclo_manager.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev7}/cyclo_manager_cli/docker/docker-compose.yml
RENAMED
|
File without changes
|