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.
- backend.ai-install-23.9.6/MANIFEST.in +1 -0
- backend.ai-install-23.9.6/PKG-INFO +55 -0
- backend.ai-install-23.9.6/ai/backend/install/VERSION +1 -0
- backend.ai-install-23.9.6/ai/backend/install/__init__.py +3 -0
- backend.ai-install-23.9.6/ai/backend/install/app.tcss +115 -0
- backend.ai-install-23.9.6/ai/backend/install/cli.py +507 -0
- backend.ai-install-23.9.6/ai/backend/install/common.py +92 -0
- backend.ai-install-23.9.6/ai/backend/install/configs/__init__.py +1 -0
- backend.ai-install-23.9.6/ai/backend/install/configs/agent.toml +92 -0
- backend.ai-install-23.9.6/ai/backend/install/configs/alembic.ini +74 -0
- backend.ai-install-23.9.6/ai/backend/install/configs/docker-compose.yml +71 -0
- backend.ai-install-23.9.6/ai/backend/install/configs/manager.toml +70 -0
- backend.ai-install-23.9.6/ai/backend/install/configs/storage-proxy.toml +95 -0
- backend.ai-install-23.9.6/ai/backend/install/configs/webserver.conf +102 -0
- backend.ai-install-23.9.6/ai/backend/install/configure.py +0 -0
- backend.ai-install-23.9.6/ai/backend/install/context.py +1010 -0
- backend.ai-install-23.9.6/ai/backend/install/dev.py +112 -0
- backend.ai-install-23.9.6/ai/backend/install/docker.py +231 -0
- backend.ai-install-23.9.6/ai/backend/install/fixtures/__init__.py +0 -0
- backend.ai-install-23.9.6/ai/backend/install/fixtures/example-keypairs.json +232 -0
- backend.ai-install-23.9.6/ai/backend/install/fixtures/example-resource-presets.json +56 -0
- backend.ai-install-23.9.6/ai/backend/install/fixtures/example-session-templates.json +70 -0
- backend.ai-install-23.9.6/ai/backend/install/http.py +53 -0
- backend.ai-install-23.9.6/ai/backend/install/pkg.py +2 -0
- backend.ai-install-23.9.6/ai/backend/install/py.typed +0 -0
- backend.ai-install-23.9.6/ai/backend/install/python.py +18 -0
- backend.ai-install-23.9.6/ai/backend/install/tomltool.py +91 -0
- backend.ai-install-23.9.6/ai/backend/install/types.py +158 -0
- backend.ai-install-23.9.6/ai/backend/install/utils.py +5 -0
- backend.ai-install-23.9.6/ai/backend/install/widgets.py +157 -0
- backend.ai-install-23.9.6/backend.ai_install.egg-info/PKG-INFO +55 -0
- backend.ai-install-23.9.6/backend.ai_install.egg-info/SOURCES.txt +39 -0
- backend.ai-install-23.9.6/backend.ai_install.egg-info/dependency_links.txt +1 -0
- backend.ai-install-23.9.6/backend.ai_install.egg-info/entry_points.txt +2 -0
- backend.ai-install-23.9.6/backend.ai_install.egg-info/namespace_packages.txt +1 -0
- backend.ai-install-23.9.6/backend.ai_install.egg-info/not-zip-safe +1 -0
- backend.ai-install-23.9.6/backend.ai_install.egg-info/requires.txt +14 -0
- backend.ai-install-23.9.6/backend.ai_install.egg-info/top_level.txt +1 -0
- backend.ai-install-23.9.6/backend_shim.py +31 -0
- backend.ai-install-23.9.6/setup.cfg +4 -0
- 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,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()
|