backend.ai-install 23.9.6__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 (41) hide show
  1. backend.ai-install-23.9.6/MANIFEST.in +1 -0
  2. backend.ai-install-23.9.6/PKG-INFO +55 -0
  3. backend.ai-install-23.9.6/ai/backend/install/VERSION +1 -0
  4. backend.ai-install-23.9.6/ai/backend/install/__init__.py +3 -0
  5. backend.ai-install-23.9.6/ai/backend/install/app.tcss +115 -0
  6. backend.ai-install-23.9.6/ai/backend/install/cli.py +507 -0
  7. backend.ai-install-23.9.6/ai/backend/install/common.py +92 -0
  8. backend.ai-install-23.9.6/ai/backend/install/configs/__init__.py +1 -0
  9. backend.ai-install-23.9.6/ai/backend/install/configs/agent.toml +92 -0
  10. backend.ai-install-23.9.6/ai/backend/install/configs/alembic.ini +74 -0
  11. backend.ai-install-23.9.6/ai/backend/install/configs/docker-compose.yml +71 -0
  12. backend.ai-install-23.9.6/ai/backend/install/configs/manager.toml +70 -0
  13. backend.ai-install-23.9.6/ai/backend/install/configs/storage-proxy.toml +95 -0
  14. backend.ai-install-23.9.6/ai/backend/install/configs/webserver.conf +102 -0
  15. backend.ai-install-23.9.6/ai/backend/install/configure.py +0 -0
  16. backend.ai-install-23.9.6/ai/backend/install/context.py +1010 -0
  17. backend.ai-install-23.9.6/ai/backend/install/dev.py +112 -0
  18. backend.ai-install-23.9.6/ai/backend/install/docker.py +231 -0
  19. backend.ai-install-23.9.6/ai/backend/install/fixtures/__init__.py +0 -0
  20. backend.ai-install-23.9.6/ai/backend/install/fixtures/example-keypairs.json +232 -0
  21. backend.ai-install-23.9.6/ai/backend/install/fixtures/example-resource-presets.json +56 -0
  22. backend.ai-install-23.9.6/ai/backend/install/fixtures/example-session-templates.json +70 -0
  23. backend.ai-install-23.9.6/ai/backend/install/http.py +53 -0
  24. backend.ai-install-23.9.6/ai/backend/install/pkg.py +2 -0
  25. backend.ai-install-23.9.6/ai/backend/install/py.typed +0 -0
  26. backend.ai-install-23.9.6/ai/backend/install/python.py +18 -0
  27. backend.ai-install-23.9.6/ai/backend/install/tomltool.py +91 -0
  28. backend.ai-install-23.9.6/ai/backend/install/types.py +158 -0
  29. backend.ai-install-23.9.6/ai/backend/install/utils.py +5 -0
  30. backend.ai-install-23.9.6/ai/backend/install/widgets.py +157 -0
  31. backend.ai-install-23.9.6/backend.ai_install.egg-info/PKG-INFO +55 -0
  32. backend.ai-install-23.9.6/backend.ai_install.egg-info/SOURCES.txt +39 -0
  33. backend.ai-install-23.9.6/backend.ai_install.egg-info/dependency_links.txt +1 -0
  34. backend.ai-install-23.9.6/backend.ai_install.egg-info/entry_points.txt +2 -0
  35. backend.ai-install-23.9.6/backend.ai_install.egg-info/namespace_packages.txt +1 -0
  36. backend.ai-install-23.9.6/backend.ai_install.egg-info/not-zip-safe +1 -0
  37. backend.ai-install-23.9.6/backend.ai_install.egg-info/requires.txt +14 -0
  38. backend.ai-install-23.9.6/backend.ai_install.egg-info/top_level.txt +1 -0
  39. backend.ai-install-23.9.6/backend_shim.py +31 -0
  40. backend.ai-install-23.9.6/setup.cfg +4 -0
  41. backend.ai-install-23.9.6/setup.py +118 -0
@@ -0,0 +1 @@
1
+ include *.py
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.1
2
+ Name: backend.ai-install
3
+ Version: 23.9.6
4
+ Summary: Backend.AI Installer
5
+ Home-page: https://github.com/lablup/backend.ai
6
+ Author: Lablup Inc. and contributors
7
+ License: MIT
8
+ Project-URL: Documentation, https://docs.backend.ai/
9
+ Project-URL: Source, https://github.com/lablup/backend.ai
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: MacOS :: MacOS X
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Programming Language :: Python
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Environment :: No Input/Output (Daemon)
16
+ Classifier: Topic :: Scientific/Engineering
17
+ Classifier: Topic :: Software Development
18
+ Classifier: Development Status :: 5 - Production/Stable
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: License :: OSI Approved :: MIT License
21
+ Requires-Python: >=3.11,<3.12
22
+ Description-Content-Type: text/markdown
23
+
24
+ Backend.AI Installer
25
+ ====================
26
+
27
+ Package Structure
28
+ -----------------
29
+
30
+ * `ai.backend.install`: The installer package
31
+
32
+ Development
33
+ -----------
34
+
35
+ ### Using the textual debug mode
36
+
37
+ First, install the textual-dev package in the `python-default` venv.
38
+ ```shell
39
+ ./py -m pip install textual-dev
40
+ ```
41
+
42
+ Open two terminal sessions.
43
+ In the first one, run:
44
+ ```shell
45
+ dist/export/python/virtualenvs/python-default/3.11.6/bin/textual console
46
+ ```
47
+
48
+ > **Warning**
49
+ > You should use the `textual` executable created *inside the venv's `bin` directory*.
50
+ > `./py -m textual` only shows the demo instead of executing the devtool command.
51
+
52
+ In the second one, run:
53
+ ```shell
54
+ TEXTUAL=devtools,debug ./backend.ai install
55
+ ```
@@ -0,0 +1 @@
1
+ 23.09.6
@@ -0,0 +1,3 @@
1
+ from pathlib import Path
2
+
3
+ __version__ = (Path(__file__).parent / "VERSION").read_text().strip()
@@ -0,0 +1,115 @@
1
+ Screen {
2
+ align: center middle;
3
+ }
4
+ Header {
5
+ text-style: bold;
6
+ }
7
+ #logo {
8
+ width: 1fr;
9
+ text-align: center;
10
+ }
11
+
12
+ ContentSwitcher {
13
+ background: $panel;
14
+ width: 1fr;
15
+ height: 1fr;
16
+ }
17
+
18
+ ModeMenu {
19
+ width: 1fr;
20
+ margin: 2 2;
21
+ align-horizontal: center;
22
+ }
23
+ ModeMenu ListView {
24
+ layout: horizontal;
25
+ width: auto;
26
+ height: auto;
27
+ }
28
+ ModeMenu ListView > ListItem {
29
+ width: 35;
30
+ margin: 1 2;
31
+ height: 14;
32
+ border: wide $panel;
33
+ }
34
+ ModeMenu ListView > ListItem .mode-item-title {
35
+ width: 1fr;
36
+ text-style: bold;
37
+ }
38
+ ModeMenu ListView > ListItem .mode-item-desc {
39
+ width: 1fr;
40
+ color: $text-muted;
41
+ }
42
+ ModeMenu ListView > ListItem.--highlight {
43
+ border: wide $foreground;
44
+ }
45
+ ModeMenu ListView > ListItem.disabled .mode-item-title {
46
+ color: $panel-lighten-2;
47
+ }
48
+ ModeMenu ListView > ListItem.disabled .mode-item-desc {
49
+ color: $error;
50
+ }
51
+ ModeMenu ListView > ListItem.--highlight.disabled {
52
+ background: $panel;
53
+ border: wide $panel-lighten-2;
54
+ }
55
+ ModeMenu Label {
56
+ padding: 1 2;
57
+ }
58
+
59
+ .mode-title {
60
+ width: 1fr;
61
+ padding: 1 2;
62
+ background: $panel-darken-1;
63
+ text-style: bold;
64
+ }
65
+
66
+ .log {
67
+ height: 1fr;
68
+ width: 1fr;
69
+ padding: 1 2;
70
+ background: $panel-darken-3;
71
+ align: center middle;
72
+ }
73
+
74
+ #download-status {
75
+ dock: bottom;
76
+ padding: 1 2;
77
+ background: $panel;
78
+ width: 75;
79
+ height: auto;
80
+ }
81
+ #download-status ProgressBar Bar {
82
+ width: 1fr;
83
+ }
84
+
85
+ Button {
86
+ width: auto;
87
+ min-width: 16;
88
+ height: 5;
89
+ margin: 0 1;
90
+ padding: 0 2;
91
+ background: $panel-lighten-2;
92
+ border: wide $panel-lighten-1;
93
+ color: $text;
94
+ text-style: bold;
95
+ }
96
+ Button.primary {
97
+ background: $accent;
98
+ }
99
+ Button.primary:hover {
100
+ background: $accent-lighten-1;
101
+ }
102
+ Button.primary.-active:hover {
103
+ border: wide $foreground;
104
+ }
105
+ Button:hover,
106
+ Button.-active:hover {
107
+ background: $panel-lighten-3;
108
+ border: wide $foreground;
109
+ }
110
+ Button:focus,
111
+ Button.-active:focus {
112
+ border: wide $foreground;
113
+ color: $text;
114
+ text-style: bold;
115
+ }
@@ -0,0 +1,507 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import os
6
+ import sys
7
+ import textwrap
8
+ from pathlib import Path
9
+ from typing import cast
10
+ from weakref import WeakSet
11
+
12
+ import click
13
+ from rich.console import Console
14
+ from rich.text import Text
15
+ from rich.traceback import Traceback
16
+ from textual import on
17
+ from textual.app import App, ComposeResult
18
+ from textual.binding import Binding
19
+ from textual.containers import Vertical
20
+ from textual.widgets import (
21
+ ContentSwitcher,
22
+ Footer,
23
+ Header,
24
+ Label,
25
+ ListItem,
26
+ ListView,
27
+ Markdown,
28
+ Static,
29
+ TabbedContent,
30
+ TabPane,
31
+ )
32
+
33
+ from ai.backend.install.utils import shorten_path
34
+ from ai.backend.install.widgets import InputDialog, SetupLog
35
+ from ai.backend.plugin.entrypoint import find_build_root
36
+
37
+ from . import __version__
38
+ from .common import detect_os
39
+ from .context import DevContext, PackageContext, current_log
40
+ from .types import CliArgs, DistInfo, InstallInfo, InstallModes, PrerequisiteError
41
+
42
+ top_tasks: WeakSet[asyncio.Task] = WeakSet()
43
+
44
+
45
+ class DevSetup(Static):
46
+ def __init__(self, **kwargs) -> None:
47
+ super().__init__(**kwargs)
48
+ self._task = None
49
+
50
+ def compose(self) -> ComposeResult:
51
+ yield Label("Development Setup", classes="mode-title")
52
+ with TabbedContent():
53
+ with TabPane("Install Log", id="tab-dev-log"):
54
+ yield SetupLog(wrap=True, classes="log")
55
+ with TabPane("Install Report", id="tab-dev-report"):
56
+ yield Label("Installation is not complete.")
57
+
58
+ def begin_install(self, dist_info: DistInfo) -> None:
59
+ self.query_one("SetupLog.log").focus()
60
+ top_tasks.add(asyncio.create_task(self.install(dist_info)))
61
+
62
+ async def install(self, dist_info: DistInfo) -> None:
63
+ _log: SetupLog = cast(SetupLog, self.query_one(".log"))
64
+ _log_token = current_log.set(_log)
65
+ ctx = DevContext(dist_info, self.app)
66
+ try:
67
+ # prerequisites
68
+ await ctx.check_prerequisites()
69
+ # install
70
+ await ctx.install()
71
+ # configure
72
+ await ctx.configure()
73
+ # post-setup
74
+ await ctx.populate_images()
75
+ await ctx.dump_install_info()
76
+ install_report = InstallReport(ctx.install_info, id="install-report")
77
+ self.query_one("TabPane#tab-dev-report Label").remove()
78
+ self.query_one("TabPane#tab-dev-report").mount(install_report)
79
+ cast(TabbedContent, self.query_one("TabbedContent")).active = "tab-dev-report"
80
+ except asyncio.CancelledError:
81
+ _log.write(Text.from_markup("[red]Interrupted!"))
82
+ await asyncio.sleep(1)
83
+ raise
84
+ except PrerequisiteError as e:
85
+ _log.write(Text.from_markup("[red]:warning: A prerequisite check has failed."))
86
+ _log.write(e)
87
+ except Exception as e:
88
+ _log.write(Text.from_markup("[red]:warning: Unexpected error!"))
89
+ _log.write(e)
90
+ _log.write(Traceback())
91
+ finally:
92
+ _log.write("")
93
+ _log.write(Text.from_markup("[bright_cyan]All tasks finished. Press q/Q to exit."))
94
+ current_log.reset(_log_token)
95
+
96
+
97
+ class PackageSetup(Static):
98
+ def __init__(self, **kwargs) -> None:
99
+ super().__init__(**kwargs)
100
+ self._task = None
101
+
102
+ def compose(self) -> ComposeResult:
103
+ yield Label("Package Setup", classes="mode-title")
104
+ with TabbedContent():
105
+ with TabPane("Install Log", id="tab-pkg-log"):
106
+ yield SetupLog(wrap=True, classes="log")
107
+ with TabPane("Install Report", id="tab-pkg-report"):
108
+ yield Label("Installation is not complete.")
109
+
110
+ def begin_install(self, dist_info: DistInfo) -> None:
111
+ self.query_one("SetupLog.log").focus()
112
+ top_tasks.add(asyncio.create_task(self.install(dist_info)))
113
+
114
+ async def install(self, dist_info: DistInfo) -> None:
115
+ _log: SetupLog = cast(SetupLog, self.query_one(".log"))
116
+ _log_token = current_log.set(_log)
117
+ ctx = PackageContext(dist_info, self.app)
118
+ try:
119
+ # prerequisites
120
+ if dist_info.target_path.exists():
121
+ input_box = InputDialog(
122
+ f"The target path {dist_info.target_path} already exists. "
123
+ "Overwrite it or set a different target path.",
124
+ str(dist_info.target_path),
125
+ allow_cancel=False,
126
+ )
127
+ _log.mount(input_box)
128
+ value = await input_box.wait()
129
+ assert value is not None
130
+ dist_info.target_path = Path(value)
131
+ await ctx.check_prerequisites()
132
+ # install
133
+ await ctx.install()
134
+ # configure
135
+ await ctx.configure()
136
+ # post-setup
137
+ await ctx.populate_images()
138
+ await ctx.dump_install_info()
139
+ install_report = InstallReport(ctx.install_info, id="install-report")
140
+ self.query_one("TabPane#tab-pkg-report Label").remove()
141
+ self.query_one("TabPane#tab-pkg-report").mount(install_report)
142
+ cast(TabbedContent, self.query_one("TabbedContent")).active = "tab-pkg-report"
143
+ except asyncio.CancelledError:
144
+ _log.write(Text.from_markup("[red]Interrupted!"))
145
+ await asyncio.sleep(1)
146
+ raise
147
+ except PrerequisiteError as e:
148
+ _log.write(Text.from_markup("[red]:warning: A prerequisite check has failed."))
149
+ _log.write(e)
150
+ except Exception as e:
151
+ _log.write(Text.from_markup("[red]:warning: Unexpected error!"))
152
+ _log.write(e)
153
+ _log.write(Traceback())
154
+ finally:
155
+ _log.write("")
156
+ _log.write(Text.from_markup("[bright_cyan]All tasks finished. Press q/Q to exit."))
157
+ current_log.reset(_log_token)
158
+
159
+
160
+ class InstallReport(Static):
161
+ def __init__(self, install_info: InstallInfo, **kwargs) -> None:
162
+ super().__init__(**kwargs)
163
+ self.install_info = install_info
164
+
165
+ def compose(self) -> ComposeResult:
166
+ service = self.install_info.service_config
167
+ yield Markdown(textwrap.dedent(f"""
168
+ Follow each tab's instructions. Once all 5 service daemons are ready, you may connect to
169
+ `http://{service.webserver_addr.face.host}:{service.webserver_addr.face.port}`.
170
+
171
+ Use the following credentials for the admin access:
172
+ - Username: `admin@lablup.com`
173
+ - Password: `wJalrXUt`
174
+
175
+ To see this guide again, run './backendai-install-<platform> install --show-guide'.
176
+ """))
177
+ with TabbedContent():
178
+ with TabPane("Web Server", id="webserver"):
179
+ yield Markdown(textwrap.dedent(f"""
180
+ Run the following commands in a separate shell:
181
+ ```console
182
+ $ cd {self.install_info.base_path.resolve()}
183
+ $ ./backendai-webserver web start-server
184
+ ```
185
+
186
+ It works if the console output ends with something like:
187
+ ```
188
+ ...
189
+ INFO ai.backend.web.server [2215731] serving at {service.webserver_addr.bind.host}:{service.webserver_addr.bind.port}
190
+ INFO ai.backend.web.server [2215731] Using uvloop as the event loop backend
191
+ ```
192
+
193
+ To terminate, send SIGINT or press Ctrl+C in the console.
194
+ """))
195
+ with TabPane("Manager", id="manager"):
196
+ yield Markdown(textwrap.dedent(f"""
197
+ Run the following commands in a separate shell:
198
+ ```console
199
+ $ cd {self.install_info.base_path.resolve()}
200
+ $ ./backendai-manager mgr start-server
201
+ ```
202
+
203
+ It works if the console output ends with something like:
204
+ ```
205
+ ...
206
+ INFO ai.backend.manager.server [2213274] started handling API requests at {service.manager_addr.bind.host}:{service.manager_addr.bind.port}
207
+ INFO ai.backend.manager.server [2213275] started handling API requests at {service.manager_addr.bind.host}:{service.manager_addr.bind.port}
208
+ INFO ai.backend.manager.server [2213276] started handling API requests at {service.manager_addr.bind.host}:{service.manager_addr.bind.port}
209
+ ```
210
+
211
+ To terminate, send SIGINT or press Ctrl+C in the console.
212
+ """))
213
+ with TabPane("Agent", id="agent"):
214
+ yield Markdown(textwrap.dedent(f"""
215
+ Run the following commands in a separate shell:
216
+ ```console
217
+ $ cd {self.install_info.base_path.resolve()}
218
+ $ ./backendai-agent ag start-server
219
+ ```
220
+
221
+ It works if the console output ends with something like:
222
+ ```
223
+ ...
224
+ INFO ai.backend.agent.server [2214424] started handling RPC requests at {service.agent_rpc_addr.bind.host}:{service.agent_rpc_addr.bind.port}
225
+ ```
226
+
227
+ To terminate, send SIGINT or press Ctrl+C in the console.
228
+ """))
229
+ with TabPane("Storage Proxy", id="storage-proxy"):
230
+ yield Markdown(textwrap.dedent(f"""
231
+ Run the following commands in a separate shell:
232
+ ```console
233
+ $ cd {self.install_info.base_path.resolve()}
234
+ $ ./backendai-storage-proxy storage start-server
235
+ ```
236
+
237
+ It works if the console output ends with something like:
238
+ ```
239
+ ...
240
+ INFO ai.backend.storage.server [2216229] Node ID: i-storage-proxy-local
241
+ INFO ai.backend.storage.server [2216229] Using uvloop as the event loop backend
242
+ ```
243
+
244
+ To terminate, send SIGINT or press Ctrl+C in the console.
245
+ """))
246
+ with TabPane("Local Proxy", id="local-proxy"):
247
+ yield Markdown(textwrap.dedent(f"""
248
+ Run the following commands in a separate shell:
249
+ ```console
250
+ $ cd {self.install_info.base_path.resolve()}
251
+ $ ./backendai-local-proxy
252
+ ```
253
+
254
+ It works if the console output ends with something like:
255
+ ```
256
+ ...
257
+ info [manager.js]: Listening on port {service.local_proxy_addr.bind.port}!
258
+ info [local_proxy.js]: Proxy is ready: http://{service.local_proxy_addr.face.host}:{service.local_proxy_addr.face.port}/
259
+ ```
260
+
261
+ To terminate, send SIGINT or press Ctrl+C in the console.
262
+ """))
263
+
264
+
265
+ class ModeMenu(Static):
266
+ """A ListView to choose InstallModes and a description pane underneath."""
267
+
268
+ BINDINGS = [
269
+ Binding("left", "cursor_up", show=False),
270
+ Binding("right", "cursor_down", show=False),
271
+ ]
272
+
273
+ _dist_info: DistInfo
274
+ _dist_info_path: Path | None
275
+
276
+ def __init__(
277
+ self,
278
+ args: CliArgs,
279
+ *,
280
+ id: str | None = None,
281
+ ) -> None:
282
+ super().__init__(id=id)
283
+ self._build_root = None
284
+ try:
285
+ self._dist_info_path = Path.cwd() / "DIST-INFO"
286
+ self._dist_info = DistInfo(**json.loads(self._dist_info_path.read_bytes()))
287
+ except FileNotFoundError:
288
+ self._dist_info_path = None
289
+ self._dist_info = DistInfo()
290
+ self._enabled_menus = set()
291
+ self._enabled_menus.add(InstallModes.PACKAGE)
292
+ mode = args.mode
293
+ try:
294
+ self._build_root = find_build_root()
295
+ self._enabled_menus.add(InstallModes.DEVELOP)
296
+ if args.mode is None:
297
+ mode = InstallModes.DEVELOP
298
+ except ValueError:
299
+ if args.mode is None:
300
+ mode = InstallModes.PACKAGE
301
+ # TODO: implement
302
+ # if Path("INSTALL-INFO").exists():
303
+ # self._enabled_menus.add(InstallModes.MAINTAIN)
304
+ assert mode is not None
305
+ self._mode = mode
306
+
307
+ def compose(self) -> ComposeResult:
308
+ yield Label(id="heading")
309
+ if self._dist_info_path is None:
310
+ package_desc = "Install using release packages"
311
+ else:
312
+ package_desc = f"Install using release packages ({shorten_path(self._dist_info_path)})"
313
+ if self._build_root is None:
314
+ develop_desc = "Could not find the source (missing BUILD_ROOT)"
315
+ else:
316
+ develop_desc = (
317
+ f"Install from the current source checkout ({shorten_path(self._build_root)})"
318
+ )
319
+ if InstallModes.MAINTAIN in self._enabled_menus:
320
+ maintain_desc = "Maintain an existing setup"
321
+ else:
322
+ # maintain_desc = "Could not find an existing setup (missing INSTALL-INFO)"
323
+ maintain_desc = "Coming soon!"
324
+ mode_desc: dict[InstallModes, str] = {
325
+ InstallModes.DEVELOP: develop_desc,
326
+ InstallModes.PACKAGE: package_desc,
327
+ InstallModes.MAINTAIN: maintain_desc,
328
+ }
329
+ with ListView(
330
+ id="mode-list", initial_index=list(InstallModes).index(InstallModes(self._mode))
331
+ ) as lv:
332
+ self.lv = lv
333
+ for mode in InstallModes:
334
+ disabled = mode not in self._enabled_menus
335
+ yield ListItem(
336
+ Vertical(
337
+ Label(mode, classes="mode-item-title"),
338
+ Label(mode_desc[mode], classes="mode-item-desc"),
339
+ ),
340
+ classes="disabled" if disabled else "",
341
+ id=f"mode-{mode.value.lower()}",
342
+ )
343
+ yield Label(id="mode-desc")
344
+
345
+ async def on_mount(self) -> None:
346
+ os_info = await detect_os()
347
+ text = Text()
348
+ text.append("Platform: ")
349
+ text.append_text(os_info.__rich__()) # type: ignore
350
+ text.append("\n\n")
351
+ text.append("Choose the installation mode:\n(arrow keys to change, enter to select)")
352
+ cast(Static, self.query_one("#heading")).update(text)
353
+
354
+ def action_cursor_up(self) -> None:
355
+ self.lv.action_cursor_up()
356
+
357
+ def action_cursor_down(self) -> None:
358
+ self.lv.action_cursor_down()
359
+
360
+ @on(ListView.Selected, "#mode-list", item="#mode-develop")
361
+ def start_develop_mode(self) -> None:
362
+ if InstallModes.DEVELOP not in self._enabled_menus:
363
+ return
364
+ self.app.sub_title = "Development Setup"
365
+ switcher: ContentSwitcher = cast(ContentSwitcher, self.app.query_one("#top"))
366
+ switcher.current = "dev-setup"
367
+ dev_setup: DevSetup = cast(DevSetup, self.app.query_one("#dev-setup"))
368
+ switcher.call_later(dev_setup.begin_install, self._dist_info)
369
+
370
+ @on(ListView.Selected, "#mode-list", item="#mode-package")
371
+ def start_package_mode(self) -> None:
372
+ if InstallModes.PACKAGE not in self._enabled_menus:
373
+ return
374
+ self.app.sub_title = "Package Setup"
375
+ switcher: ContentSwitcher = cast(ContentSwitcher, self.app.query_one("#top"))
376
+ switcher.current = "pkg-setup"
377
+ pkg_setup: PackageSetup = cast(PackageSetup, self.app.query_one("#pkg-setup"))
378
+ switcher.call_later(pkg_setup.begin_install, self._dist_info)
379
+
380
+ @on(ListView.Selected, "#mode-list", item="#mode-maintain")
381
+ def start_maintain_mode(self) -> None:
382
+ if InstallModes.MAINTAIN not in self._enabled_menus:
383
+ return
384
+ pass
385
+
386
+
387
+ class InstallerApp(App):
388
+ BINDINGS = [
389
+ Binding("q", "shutdown", "Interrupt ongoing tasks / Quit the installer"),
390
+ Binding(
391
+ "ctrl+c",
392
+ "shutdown",
393
+ "Interrupt ongoing tasks / Quit the installer",
394
+ show=False,
395
+ priority=True,
396
+ ),
397
+ ]
398
+ CSS_PATH = "app.tcss"
399
+
400
+ _args: CliArgs
401
+
402
+ def __init__(self, args: CliArgs | None = None) -> None:
403
+ super().__init__()
404
+ if args is None: # when run as textual dev mode
405
+ args = CliArgs(
406
+ mode=None,
407
+ target_path=str(Path.home() / "backendai"),
408
+ show_guide=False,
409
+ )
410
+ self._args = args
411
+
412
+ def compose(self) -> ComposeResult:
413
+ yield Header(show_clock=True)
414
+ logo_text = textwrap.dedent(r"""
415
+ ____ _ _ _ ___
416
+ | __ ) __ _ ___| | _____ _ __ __| | / \ |_ _|
417
+ | _ \ / _` |/ __| |/ / _ \ '_ \ / _` | / _ \ | |
418
+ | |_) | (_| | (__| < __/ | | | (_| |_ / ___ \ | |
419
+ |____/ \__,_|\___|_|\_\___|_| |_|\__,_(_)_/ \_\___|
420
+ """)
421
+ yield Static(logo_text, id="logo")
422
+ if self._args.show_guide:
423
+ try:
424
+ install_info = InstallInfo(**json.loads((Path.cwd() / "INSTALL-INFO").read_bytes()))
425
+ yield InstallReport(install_info)
426
+ except IOError as e:
427
+ log = SetupLog()
428
+ log.write("Failed to read INSTALL-INFO!")
429
+ log.write(e)
430
+ yield log
431
+ else:
432
+ with ContentSwitcher(id="top", initial="mode-menu"):
433
+ yield ModeMenu(self._args, id="mode-menu")
434
+ yield DevSetup(id="dev-setup")
435
+ yield PackageSetup(id="pkg-setup")
436
+ yield Footer()
437
+
438
+ async def on_mount(self) -> None:
439
+ header: Header = cast(Header, self.query_one("Header"))
440
+ header.tall = True
441
+ self.title = "Backend.AI Installer"
442
+
443
+ async def action_shutdown(self, message: str | None = None, exit_code: int = 0) -> None:
444
+ had_cancelled_tasks = False
445
+ for t in {*top_tasks}:
446
+ if t.done():
447
+ continue
448
+ had_cancelled_tasks = True
449
+ t.cancel()
450
+ try:
451
+ await t
452
+ except asyncio.CancelledError:
453
+ pass
454
+ if not had_cancelled_tasks:
455
+ # Let the user shutdown twice if there were cancelled tasks,
456
+ # so that the user could inspect what happened.
457
+ self.exit(return_code=exit_code, message=message)
458
+
459
+
460
+ @click.command(
461
+ context_settings={
462
+ "help_option_names": ["-h", "--help"],
463
+ },
464
+ )
465
+ @click.option(
466
+ "--mode",
467
+ type=click.Choice([*InstallModes.__members__], case_sensitive=False),
468
+ default=None,
469
+ help="Override the installation mode. [default: auto-detect]",
470
+ )
471
+ @click.option(
472
+ "--target-path",
473
+ type=str,
474
+ default=str(Path.home() / "backendai"),
475
+ help="Explicitly set the target installation path. [default: ~/backendai]",
476
+ )
477
+ @click.option(
478
+ "--show-guide",
479
+ is_flag=True,
480
+ default=False,
481
+ help="Show the post-install guide using INSTALL-INFO if present.",
482
+ )
483
+ @click.version_option(version=__version__)
484
+ @click.pass_context
485
+ def main(
486
+ cli_ctx: click.Context,
487
+ mode: InstallModes | None,
488
+ target_path: str,
489
+ show_guide: bool,
490
+ ) -> None:
491
+ """The installer"""
492
+ # check sudo permission
493
+ console = Console(stderr=True)
494
+ if os.geteuid() == 0:
495
+ console.print(
496
+ "[bright_red] The script should not be run as root, while it requires"
497
+ " the passwordless sudo privilege."
498
+ )
499
+ sys.exit(1)
500
+ # start installer
501
+ args = CliArgs(
502
+ mode=mode,
503
+ target_path=target_path,
504
+ show_guide=show_guide,
505
+ )
506
+ app = InstallerApp(args)
507
+ app.run()