cyclo-manager 0.2.0.dev4__tar.gz → 0.2.0.dev6__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.dev4 → cyclo_manager-0.2.0.dev6}/PKG-INFO +1 -1
- cyclo_manager-0.2.0.dev6/cyclo_host_agent/routers/update.py +96 -0
- {cyclo_manager-0.2.0.dev4 → cyclo_manager-0.2.0.dev6}/cyclo_manager.egg-info/PKG-INFO +1 -1
- {cyclo_manager-0.2.0.dev4 → cyclo_manager-0.2.0.dev6}/cyclo_manager_cli/cli.py +84 -17
- {cyclo_manager-0.2.0.dev4 → cyclo_manager-0.2.0.dev6}/pyproject.toml +1 -1
- cyclo_manager-0.2.0.dev4/cyclo_host_agent/routers/update.py +0 -175
- {cyclo_manager-0.2.0.dev4 → cyclo_manager-0.2.0.dev6}/README.md +0 -0
- {cyclo_manager-0.2.0.dev4 → cyclo_manager-0.2.0.dev6}/cyclo_host_agent/__init__.py +0 -0
- {cyclo_manager-0.2.0.dev4 → cyclo_manager-0.2.0.dev6}/cyclo_host_agent/main.py +0 -0
- {cyclo_manager-0.2.0.dev4 → cyclo_manager-0.2.0.dev6}/cyclo_host_agent/models.py +0 -0
- {cyclo_manager-0.2.0.dev4 → cyclo_manager-0.2.0.dev6}/cyclo_host_agent/routers/__init__.py +0 -0
- {cyclo_manager-0.2.0.dev4 → cyclo_manager-0.2.0.dev6}/cyclo_host_agent/routers/repos.py +0 -0
- {cyclo_manager-0.2.0.dev4 → cyclo_manager-0.2.0.dev6}/cyclo_manager.egg-info/SOURCES.txt +0 -0
- {cyclo_manager-0.2.0.dev4 → cyclo_manager-0.2.0.dev6}/cyclo_manager.egg-info/dependency_links.txt +0 -0
- {cyclo_manager-0.2.0.dev4 → cyclo_manager-0.2.0.dev6}/cyclo_manager.egg-info/entry_points.txt +0 -0
- {cyclo_manager-0.2.0.dev4 → cyclo_manager-0.2.0.dev6}/cyclo_manager.egg-info/requires.txt +0 -0
- {cyclo_manager-0.2.0.dev4 → cyclo_manager-0.2.0.dev6}/cyclo_manager.egg-info/top_level.txt +0 -0
- {cyclo_manager-0.2.0.dev4 → cyclo_manager-0.2.0.dev6}/cyclo_manager_cli/__init__.py +0 -0
- {cyclo_manager-0.2.0.dev4 → cyclo_manager-0.2.0.dev6}/cyclo_manager_cli/config/config.yml +0 -0
- {cyclo_manager-0.2.0.dev4 → cyclo_manager-0.2.0.dev6}/cyclo_manager_cli/docker/docker-compose.yml +0 -0
- {cyclo_manager-0.2.0.dev4 → cyclo_manager-0.2.0.dev6}/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.dev6
|
|
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
|
|
@@ -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.dev6
|
|
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
|
|
@@ -103,6 +103,35 @@ def _setup_socket_dir(user: str) -> bool:
|
|
|
103
103
|
return False
|
|
104
104
|
|
|
105
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
|
+
|
|
106
135
|
def _ensure_host_agent() -> int:
|
|
107
136
|
"""
|
|
108
137
|
Install or refresh cyclo_host_agent: systemd unit, socket dir, and service start.
|
|
@@ -127,6 +156,9 @@ def _ensure_host_agent() -> int:
|
|
|
127
156
|
if not _setup_socket_dir(user):
|
|
128
157
|
return 1
|
|
129
158
|
|
|
159
|
+
if not _install_sudoers(user):
|
|
160
|
+
return 1
|
|
161
|
+
|
|
130
162
|
service_content = f"""\
|
|
131
163
|
[Unit]
|
|
132
164
|
Description=Cyclo Host Agent
|
|
@@ -208,8 +240,27 @@ def cmd_up(args: argparse.Namespace) -> int:
|
|
|
208
240
|
return 0
|
|
209
241
|
|
|
210
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
|
+
|
|
211
260
|
def cmd_down(args: argparse.Namespace) -> int:
|
|
212
261
|
"""Stop cyclo_manager server, cyclo_manager_ui, and zenoh daemon (docker compose down)."""
|
|
262
|
+
_teardown_host_agent()
|
|
263
|
+
|
|
213
264
|
compose_path = _docker_dir() / 'docker-compose.yml'
|
|
214
265
|
if not compose_path.is_file():
|
|
215
266
|
print(f'Compose file not found: {compose_path}', file=sys.stderr)
|
|
@@ -239,29 +290,32 @@ def cmd_down(args: argparse.Namespace) -> int:
|
|
|
239
290
|
|
|
240
291
|
|
|
241
292
|
def cmd_update(args: argparse.Namespace) -> int:
|
|
242
|
-
"""
|
|
243
|
-
cyclo_manager_exe = shutil.which('cyclo_manager')
|
|
244
|
-
if not cyclo_manager_exe:
|
|
245
|
-
print('cyclo_manager command not found in PATH.', file=sys.stderr)
|
|
246
|
-
return 1
|
|
293
|
+
"""Bring containers down, pip install -U, bring containers back up, restart host agent."""
|
|
247
294
|
pip_exe = shutil.which('pip3') or shutil.which('pip')
|
|
248
295
|
if not pip_exe:
|
|
249
296
|
print('pip not found; cannot update package.', file=sys.stderr)
|
|
250
297
|
return 1
|
|
251
298
|
|
|
252
|
-
|
|
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...')
|
|
253
308
|
try:
|
|
254
|
-
subprocess.run([
|
|
309
|
+
subprocess.run([*base, 'down'], env=env, check=True)
|
|
255
310
|
except subprocess.CalledProcessError as e:
|
|
256
311
|
return e.returncode
|
|
312
|
+
except FileNotFoundError:
|
|
313
|
+
print('Docker not found.', file=sys.stderr)
|
|
314
|
+
return 1
|
|
257
315
|
|
|
258
316
|
print(f'Updating {PYPI_PACKAGE} (pip install -U)...')
|
|
259
317
|
try:
|
|
260
|
-
subprocess.run(
|
|
261
|
-
[pip_exe, 'install', '-U', PYPI_PACKAGE],
|
|
262
|
-
check=True,
|
|
263
|
-
timeout=120,
|
|
264
|
-
)
|
|
318
|
+
subprocess.run([pip_exe, 'install', '-U', PYPI_PACKAGE], check=True, timeout=120)
|
|
265
319
|
except subprocess.CalledProcessError as e:
|
|
266
320
|
print(f'pip install -U {PYPI_PACKAGE} failed.', file=sys.stderr)
|
|
267
321
|
return e.returncode
|
|
@@ -269,15 +323,28 @@ def cmd_update(args: argparse.Namespace) -> int:
|
|
|
269
323
|
print('pip install timed out.', file=sys.stderr)
|
|
270
324
|
return 1
|
|
271
325
|
|
|
272
|
-
print('Starting containers
|
|
273
|
-
up_args = [cyclo_manager_exe, 'up']
|
|
274
|
-
if getattr(args, 'pull', False):
|
|
275
|
-
up_args.append('--pull')
|
|
326
|
+
print('Starting containers...')
|
|
276
327
|
try:
|
|
277
|
-
|
|
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
|
+
)
|
|
278
336
|
except subprocess.CalledProcessError as e:
|
|
279
337
|
return e.returncode
|
|
280
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
|
+
|
|
281
348
|
print('cyclo_manager update completed.')
|
|
282
349
|
return 0
|
|
283
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.dev6"
|
|
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'}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cyclo_manager-0.2.0.dev4 → cyclo_manager-0.2.0.dev6}/cyclo_manager.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{cyclo_manager-0.2.0.dev4 → cyclo_manager-0.2.0.dev6}/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.dev4 → cyclo_manager-0.2.0.dev6}/cyclo_manager_cli/docker/docker-compose.yml
RENAMED
|
File without changes
|
|
File without changes
|