cyclo-manager 0.2.0.dev5__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.
Files changed (21) hide show
  1. {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev6}/PKG-INFO +1 -1
  2. {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev6}/cyclo_host_agent/routers/repos.py +4 -6
  3. cyclo_manager-0.2.0.dev6/cyclo_host_agent/routers/update.py +96 -0
  4. {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev6}/cyclo_manager.egg-info/PKG-INFO +1 -1
  5. {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev6}/cyclo_manager_cli/cli.py +92 -19
  6. {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev6}/pyproject.toml +1 -1
  7. cyclo_manager-0.2.0.dev5/cyclo_host_agent/routers/update.py +0 -175
  8. {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev6}/README.md +0 -0
  9. {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev6}/cyclo_host_agent/__init__.py +0 -0
  10. {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev6}/cyclo_host_agent/main.py +0 -0
  11. {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev6}/cyclo_host_agent/models.py +0 -0
  12. {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev6}/cyclo_host_agent/routers/__init__.py +0 -0
  13. {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev6}/cyclo_manager.egg-info/SOURCES.txt +0 -0
  14. {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev6}/cyclo_manager.egg-info/dependency_links.txt +0 -0
  15. {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev6}/cyclo_manager.egg-info/entry_points.txt +0 -0
  16. {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev6}/cyclo_manager.egg-info/requires.txt +0 -0
  17. {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev6}/cyclo_manager.egg-info/top_level.txt +0 -0
  18. {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev6}/cyclo_manager_cli/__init__.py +0 -0
  19. {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev6}/cyclo_manager_cli/config/config.yml +0 -0
  20. {cyclo_manager-0.2.0.dev5 → cyclo_manager-0.2.0.dev6}/cyclo_manager_cli/docker/docker-compose.yml +0 -0
  21. {cyclo_manager-0.2.0.dev5 → 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.dev5
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
@@ -44,9 +44,9 @@ GIT_TIMEOUT = 120.0
44
44
 
45
45
 
46
46
  def _resolve_home() -> Path:
47
- user_home = os.environ.get('CYCLO_MANAGER_USER_HOME')
48
- if user_home:
49
- return Path(user_home)
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
- *cmd,
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.dev5
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
@@ -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=CYCLO_MANAGER_USER={user}
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
- """Down containers, pip install -U cyclo-manager, then up again."""
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
- print('Stopping containers (cyclo_manager down)...')
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([cyclo_manager_exe, 'down'], check=True)
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 (cyclo_manager up)...')
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
- subprocess.run(up_args, check=True)
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.dev5"
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'}