cyclo-manager 0.2.0.dev2__tar.gz → 0.2.0.dev3__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.dev2 → cyclo_manager-0.2.0.dev3}/PKG-INFO +1 -1
- {cyclo_manager-0.2.0.dev2 → cyclo_manager-0.2.0.dev3}/cyclo_host_agent/routers/repos.py +26 -31
- {cyclo_manager-0.2.0.dev2 → cyclo_manager-0.2.0.dev3}/cyclo_host_agent/routers/update.py +56 -18
- {cyclo_manager-0.2.0.dev2 → cyclo_manager-0.2.0.dev3}/cyclo_manager.egg-info/PKG-INFO +1 -1
- {cyclo_manager-0.2.0.dev2 → cyclo_manager-0.2.0.dev3}/cyclo_manager_cli/cli.py +65 -33
- {cyclo_manager-0.2.0.dev2 → cyclo_manager-0.2.0.dev3}/pyproject.toml +1 -1
- {cyclo_manager-0.2.0.dev2 → cyclo_manager-0.2.0.dev3}/README.md +0 -0
- {cyclo_manager-0.2.0.dev2 → cyclo_manager-0.2.0.dev3}/cyclo_host_agent/__init__.py +0 -0
- {cyclo_manager-0.2.0.dev2 → cyclo_manager-0.2.0.dev3}/cyclo_host_agent/main.py +0 -0
- {cyclo_manager-0.2.0.dev2 → cyclo_manager-0.2.0.dev3}/cyclo_host_agent/models.py +0 -0
- {cyclo_manager-0.2.0.dev2 → cyclo_manager-0.2.0.dev3}/cyclo_host_agent/routers/__init__.py +0 -0
- {cyclo_manager-0.2.0.dev2 → cyclo_manager-0.2.0.dev3}/cyclo_manager.egg-info/SOURCES.txt +0 -0
- {cyclo_manager-0.2.0.dev2 → cyclo_manager-0.2.0.dev3}/cyclo_manager.egg-info/dependency_links.txt +0 -0
- {cyclo_manager-0.2.0.dev2 → cyclo_manager-0.2.0.dev3}/cyclo_manager.egg-info/entry_points.txt +0 -0
- {cyclo_manager-0.2.0.dev2 → cyclo_manager-0.2.0.dev3}/cyclo_manager.egg-info/requires.txt +0 -0
- {cyclo_manager-0.2.0.dev2 → cyclo_manager-0.2.0.dev3}/cyclo_manager.egg-info/top_level.txt +0 -0
- {cyclo_manager-0.2.0.dev2 → cyclo_manager-0.2.0.dev3}/cyclo_manager_cli/__init__.py +0 -0
- {cyclo_manager-0.2.0.dev2 → cyclo_manager-0.2.0.dev3}/cyclo_manager_cli/config/config.yml +0 -0
- {cyclo_manager-0.2.0.dev2 → cyclo_manager-0.2.0.dev3}/cyclo_manager_cli/docker/docker-compose.yml +0 -0
- {cyclo_manager-0.2.0.dev2 → cyclo_manager-0.2.0.dev3}/setup.cfg +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.dev3
|
|
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
|
|
@@ -22,7 +22,6 @@ import asyncio
|
|
|
22
22
|
import json
|
|
23
23
|
import os
|
|
24
24
|
import re
|
|
25
|
-
import subprocess
|
|
26
25
|
import urllib.request
|
|
27
26
|
from pathlib import Path
|
|
28
27
|
from typing import Optional
|
|
@@ -87,23 +86,20 @@ def _fmt_cmd(cmd: str, output: str) -> str:
|
|
|
87
86
|
|
|
88
87
|
# ── git helpers ────────────────────────────────────────────────────────────────
|
|
89
88
|
|
|
90
|
-
def _git_sync(args: list[str], cwd: Optional[Path] = None) -> tuple[int, str, str]:
|
|
91
|
-
try:
|
|
92
|
-
result = subprocess.run(
|
|
93
|
-
['git', *args],
|
|
94
|
-
cwd=str(cwd) if cwd else None,
|
|
95
|
-
capture_output=True,
|
|
96
|
-
timeout=GIT_TIMEOUT,
|
|
97
|
-
)
|
|
98
|
-
return result.returncode, result.stdout.decode(), result.stderr.decode()
|
|
99
|
-
except subprocess.TimeoutExpired:
|
|
100
|
-
return 1, '', 'git timed out'
|
|
101
|
-
except Exception as e:
|
|
102
|
-
return 1, '', str(e)
|
|
103
|
-
|
|
104
|
-
|
|
105
89
|
async def _git(args: list[str], cwd: Optional[Path] = None) -> tuple[int, str, str]:
|
|
106
|
-
|
|
90
|
+
proc = await asyncio.create_subprocess_exec(
|
|
91
|
+
'git', *args,
|
|
92
|
+
cwd=str(cwd) if cwd else None,
|
|
93
|
+
stdout=asyncio.subprocess.PIPE,
|
|
94
|
+
stderr=asyncio.subprocess.PIPE,
|
|
95
|
+
)
|
|
96
|
+
try:
|
|
97
|
+
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=GIT_TIMEOUT)
|
|
98
|
+
except asyncio.TimeoutError:
|
|
99
|
+
proc.kill()
|
|
100
|
+
await proc.wait()
|
|
101
|
+
raise
|
|
102
|
+
return proc.returncode, stdout.decode(), stderr.decode()
|
|
107
103
|
|
|
108
104
|
|
|
109
105
|
async def _repo_info(repo_path: Path) -> RepoInfo:
|
|
@@ -283,25 +279,24 @@ async def _update_reset(repo_path: Path, preserve_files: list[str]) -> UpdateRes
|
|
|
283
279
|
|
|
284
280
|
# ── container helper ───────────────────────────────────────────────────────────
|
|
285
281
|
|
|
286
|
-
def
|
|
282
|
+
async def _run_container_sh(repo_path: Path, action: str) -> tuple[bool, str]:
|
|
287
283
|
script = repo_path / 'docker' / 'container.sh'
|
|
288
284
|
if not script.exists():
|
|
289
285
|
return False, f'container.sh not found at {script}'
|
|
286
|
+
proc = await asyncio.create_subprocess_exec(
|
|
287
|
+
'bash', str(script), action,
|
|
288
|
+
cwd=str(repo_path / 'docker'),
|
|
289
|
+
stdin=asyncio.subprocess.PIPE,
|
|
290
|
+
stdout=asyncio.subprocess.PIPE,
|
|
291
|
+
stderr=asyncio.subprocess.PIPE,
|
|
292
|
+
)
|
|
290
293
|
try:
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
capture_output=True,
|
|
296
|
-
timeout=300.0,
|
|
297
|
-
)
|
|
298
|
-
return result.returncode == 0, (result.stdout.decode() + result.stderr.decode()).strip()
|
|
299
|
-
except subprocess.TimeoutExpired:
|
|
294
|
+
stdout, stderr = await asyncio.wait_for(proc.communicate(input=b'y\n'), timeout=300.0)
|
|
295
|
+
except asyncio.TimeoutError:
|
|
296
|
+
proc.kill()
|
|
297
|
+
await proc.wait()
|
|
300
298
|
return False, 'Timeout waiting for container.sh'
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
async def _run_container_sh(repo_path: Path, action: str) -> tuple[bool, str]:
|
|
304
|
-
return await asyncio.to_thread(_run_container_sh_sync, repo_path, action)
|
|
299
|
+
return proc.returncode == 0, (stdout.decode() + stderr.decode()).strip()
|
|
305
300
|
|
|
306
301
|
|
|
307
302
|
# ── endpoints ──────────────────────────────────────────────────────────────────
|
|
@@ -42,15 +42,16 @@ def _fmt(cmd: str, out: str) -> str:
|
|
|
42
42
|
return f'$ {cmd}\n{out.strip()}' if out.strip() else f'$ {cmd}'
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
def _restart_self() -> None:
|
|
46
|
+
"""Restart the host agent after a short delay so new binary takes effect."""
|
|
47
|
+
time.sleep(2)
|
|
47
48
|
try:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
subprocess.run(
|
|
50
|
+
['sudo', 'systemctl', 'restart', f'{HOST_AGENT_SERVICE}.service'],
|
|
51
|
+
check=True,
|
|
52
|
+
)
|
|
52
53
|
except Exception as e:
|
|
53
|
-
|
|
54
|
+
logger.error('Failed to restart host agent: %s', e)
|
|
54
55
|
|
|
55
56
|
|
|
56
57
|
async def _run_install_and_up(cyclo_exe: str, pip_exe: str) -> None:
|
|
@@ -59,24 +60,50 @@ async def _run_install_and_up(cyclo_exe: str, pip_exe: str) -> None:
|
|
|
59
60
|
|
|
60
61
|
# pip install -U
|
|
61
62
|
_update_status['phase'] = 'installing'
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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:
|
|
65
77
|
_update_status['phase'] = 'error'
|
|
66
|
-
_update_status['error'] =
|
|
78
|
+
_update_status['error'] = 'pip install timed out'
|
|
67
79
|
return
|
|
68
80
|
|
|
69
81
|
# cyclo up
|
|
70
82
|
_update_status['phase'] = 'starting'
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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:
|
|
74
97
|
_update_status['phase'] = 'error'
|
|
75
|
-
_update_status['error'] =
|
|
98
|
+
_update_status['error'] = 'cyclo_manager up timed out'
|
|
76
99
|
return
|
|
77
100
|
|
|
78
101
|
_update_status['phase'] = 'done'
|
|
79
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
|
+
|
|
80
107
|
|
|
81
108
|
@router.post('/update')
|
|
82
109
|
async def start_update() -> dict:
|
|
@@ -100,9 +127,20 @@ async def start_update() -> dict:
|
|
|
100
127
|
|
|
101
128
|
_update_status = {'phase': 'stopping', 'install_output': '', 'up_output': '', 'error': ''}
|
|
102
129
|
|
|
103
|
-
# cyclo down
|
|
104
|
-
|
|
105
|
-
|
|
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})'
|
|
106
144
|
|
|
107
145
|
# Continue install + up in background
|
|
108
146
|
asyncio.create_task(_run_install_and_up(cyclo_exe, pip_exe))
|
|
@@ -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.dev3
|
|
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
|
|
@@ -33,6 +33,7 @@ import sys
|
|
|
33
33
|
PYPI_PACKAGE = 'cyclo-manager'
|
|
34
34
|
HOST_AGENT_SERVICE = 'cyclo_host_agent'
|
|
35
35
|
HOST_AGENT_SOCKET = '/var/run/robotis/agent_sockets/host/host_agent.sock'
|
|
36
|
+
HOST_AGENT_SOCKET_DIR = '/var/run/robotis/agent_sockets/host'
|
|
36
37
|
|
|
37
38
|
# `cyclo_manager up` starts these immediately.
|
|
38
39
|
COMPOSE_SERVICES_UP = ('cyclo_manager', 'ui')
|
|
@@ -53,24 +54,67 @@ def _packaged_config_path() -> Path:
|
|
|
53
54
|
return _config_dir() / 'config.yml'
|
|
54
55
|
|
|
55
56
|
|
|
56
|
-
def
|
|
57
|
-
"""
|
|
58
|
-
|
|
59
|
-
['systemctl', 'is-active', f'{HOST_AGENT_SERVICE}.service'],
|
|
60
|
-
capture_output=True,
|
|
61
|
-
text=True,
|
|
62
|
-
)
|
|
63
|
-
return result.returncode == 0
|
|
57
|
+
def _resolve_service_user() -> tuple[str, Path] | None:
|
|
58
|
+
"""
|
|
59
|
+
Return (username, home) for the host agent systemd service.
|
|
64
60
|
|
|
61
|
+
Uses SUDO_USER when cyclo_manager up is invoked via sudo so the agent runs
|
|
62
|
+
as the login user (required for git repo scans under ~/...).
|
|
63
|
+
"""
|
|
64
|
+
sudo_user = os.environ.get('SUDO_USER')
|
|
65
|
+
if sudo_user:
|
|
66
|
+
return sudo_user, Path(f'/home/{sudo_user}')
|
|
67
|
+
user = os.environ.get('USER') or os.getlogin()
|
|
68
|
+
if user == 'root':
|
|
69
|
+
print(
|
|
70
|
+
'Error: cyclo_manager up must run as a normal user (e.g. robotis), not root.',
|
|
71
|
+
file=sys.stderr,
|
|
72
|
+
)
|
|
73
|
+
return None
|
|
74
|
+
return user, Path.home()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _setup_socket_dir(user: str) -> bool:
|
|
78
|
+
"""
|
|
79
|
+
Ensure the host agent UDS directory exists and is owned by the service user.
|
|
80
|
+
|
|
81
|
+
Only host/ is chowned so other agent sockets under agent_sockets/ are untouched.
|
|
82
|
+
Removes a stale socket file (e.g. left by a previous root-owned service).
|
|
83
|
+
"""
|
|
84
|
+
try:
|
|
85
|
+
subprocess.run(
|
|
86
|
+
['sudo', 'mkdir', '-p', HOST_AGENT_SOCKET_DIR],
|
|
87
|
+
check=True,
|
|
88
|
+
capture_output=True,
|
|
89
|
+
)
|
|
90
|
+
subprocess.run(
|
|
91
|
+
['sudo', 'chown', f'{user}:{user}', HOST_AGENT_SOCKET_DIR],
|
|
92
|
+
check=True,
|
|
93
|
+
capture_output=True,
|
|
94
|
+
)
|
|
95
|
+
subprocess.run(
|
|
96
|
+
['sudo', 'rm', '-f', HOST_AGENT_SOCKET],
|
|
97
|
+
check=True,
|
|
98
|
+
capture_output=True,
|
|
99
|
+
)
|
|
100
|
+
return True
|
|
101
|
+
except subprocess.CalledProcessError as e:
|
|
102
|
+
print(f'Failed to set up host agent socket directory: {e}', file=sys.stderr)
|
|
103
|
+
return False
|
|
65
104
|
|
|
66
105
|
|
|
67
|
-
def
|
|
106
|
+
def _ensure_host_agent() -> int:
|
|
68
107
|
"""
|
|
69
|
-
|
|
108
|
+
Install or refresh cyclo_host_agent: systemd unit, socket dir, and service start.
|
|
70
109
|
|
|
71
|
-
|
|
72
|
-
|
|
110
|
+
Idempotent — safe to run on every `cyclo_manager up` (fixes upgraded units and
|
|
111
|
+
socket ownership after switching away from a root-owned service).
|
|
73
112
|
"""
|
|
113
|
+
account = _resolve_service_user()
|
|
114
|
+
if account is None:
|
|
115
|
+
return 1
|
|
116
|
+
|
|
117
|
+
user, user_home = account
|
|
74
118
|
agent_exe = shutil.which('cyclo_host_agent')
|
|
75
119
|
if not agent_exe:
|
|
76
120
|
print(
|
|
@@ -80,8 +124,9 @@ def _create_host_agent() -> int:
|
|
|
80
124
|
)
|
|
81
125
|
return 1
|
|
82
126
|
|
|
83
|
-
|
|
84
|
-
|
|
127
|
+
if not _setup_socket_dir(user):
|
|
128
|
+
return 1
|
|
129
|
+
|
|
85
130
|
service_content = f"""\
|
|
86
131
|
[Unit]
|
|
87
132
|
Description=Cyclo Host Agent
|
|
@@ -89,7 +134,8 @@ After=network.target
|
|
|
89
134
|
|
|
90
135
|
[Service]
|
|
91
136
|
Type=simple
|
|
92
|
-
User={
|
|
137
|
+
User={user}
|
|
138
|
+
Group={user}
|
|
93
139
|
ExecStart={agent_exe}
|
|
94
140
|
Environment=HOME={user_home}
|
|
95
141
|
Restart=always
|
|
@@ -110,7 +156,8 @@ WantedBy=multi-user.target
|
|
|
110
156
|
check=True,
|
|
111
157
|
)
|
|
112
158
|
subprocess.run(['sudo', 'systemctl', 'daemon-reload'], check=True)
|
|
113
|
-
subprocess.run(['sudo', 'systemctl', 'enable',
|
|
159
|
+
subprocess.run(['sudo', 'systemctl', 'enable', f'{HOST_AGENT_SERVICE}.service'], check=True)
|
|
160
|
+
subprocess.run(['sudo', 'systemctl', 'restart', f'{HOST_AGENT_SERVICE}.service'], check=True)
|
|
114
161
|
print('Host agent service installed and started.')
|
|
115
162
|
return 0
|
|
116
163
|
except subprocess.CalledProcessError as e:
|
|
@@ -123,14 +170,8 @@ WantedBy=multi-user.target
|
|
|
123
170
|
|
|
124
171
|
def cmd_up(args: argparse.Namespace) -> int:
|
|
125
172
|
"""Start API + UI; create zenoh and noVNC containers without starting them."""
|
|
126
|
-
if
|
|
127
|
-
|
|
128
|
-
subprocess.run(['sudo', 'systemctl', 'restart', f'{HOST_AGENT_SERVICE}.service'], check=True)
|
|
129
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
130
|
-
print('Warning: Failed to restart host agent service.', file=sys.stderr)
|
|
131
|
-
else:
|
|
132
|
-
if _create_host_agent() != 0:
|
|
133
|
-
print('Warning: Failed to install host agent service.')
|
|
173
|
+
if _ensure_host_agent() != 0:
|
|
174
|
+
print('Warning: Failed to install host agent service.', file=sys.stderr)
|
|
134
175
|
|
|
135
176
|
config_path = _packaged_config_path()
|
|
136
177
|
if not config_path.is_file():
|
|
@@ -237,15 +278,6 @@ def cmd_update(args: argparse.Namespace) -> int:
|
|
|
237
278
|
except subprocess.CalledProcessError as e:
|
|
238
279
|
return e.returncode
|
|
239
280
|
|
|
240
|
-
print('Restarting host agent...')
|
|
241
|
-
try:
|
|
242
|
-
subprocess.run(
|
|
243
|
-
['sudo', 'systemctl', 'restart', f'{HOST_AGENT_SERVICE}.service'],
|
|
244
|
-
check=True,
|
|
245
|
-
)
|
|
246
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
247
|
-
print('Warning: Failed to restart host agent service.', file=sys.stderr)
|
|
248
|
-
|
|
249
281
|
print('cyclo_manager update completed.')
|
|
250
282
|
return 0
|
|
251
283
|
|
|
@@ -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.dev3"
|
|
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
|
|
File without changes
|
{cyclo_manager-0.2.0.dev2 → cyclo_manager-0.2.0.dev3}/cyclo_manager.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{cyclo_manager-0.2.0.dev2 → cyclo_manager-0.2.0.dev3}/cyclo_manager.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cyclo_manager-0.2.0.dev2 → cyclo_manager-0.2.0.dev3}/cyclo_manager_cli/docker/docker-compose.yml
RENAMED
|
File without changes
|
|
File without changes
|