gpu-server-setup 0.4.0__py3-none-any.whl

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.
@@ -0,0 +1,248 @@
1
+ Metadata-Version: 2.4
2
+ Name: gpu-server-setup
3
+ Version: 0.4.0
4
+ Summary: Один клик — удалённый GPU-сервер с Ollama + VS Code Remote
5
+ Author: The Fool
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: typer[all]>=0.12
11
+ Requires-Dist: rich>=13.7
12
+ Requires-Dist: paramiko>=3.4
13
+ Requires-Dist: pydantic-settings>=2.5
14
+ Requires-Dist: tomli>=2.0; python_version >= "3.9"
15
+ Dynamic: license-file
16
+
17
+ ```markdown
18
+ # Gpu-setup — один клик до удалённого GPU-сервера с Ollama и VS Code Remote
19
+
20
+ Автоматическая настройка SSH, установка [Ollama](https://ollama.com/), загрузка LLM-моделей и открытие VS Code Remote на удалённом GPU-сервере — всё одной командой.
21
+
22
+ ---
23
+
24
+ ## Возможности
25
+
26
+ - **Генерация SSH-ключей** (ed25519 / RSA) и автоматическое копирование публичного ключа на сервер.
27
+ - **Настройка `~/.ssh/config`** с алиасом для удобного подключения.
28
+ - **Установка Ollama** (через официальный скрипт) с запуском сервиса.
29
+ - **Загрузка моделей** (например, `llama3.2`, `gemma2`) с живым выводом прогресса.
30
+ - **Автоматический запуск VS Code Remote** (если установлен `code` CLI).
31
+ - **Режим `--dry-run`** для безопасного просмотра плана действий.
32
+ - **Красивый вывод** с помощью `rich`.
33
+
34
+ ---
35
+
36
+ ## Предварительные требования
37
+
38
+ - Python 3.8+ (рекомендуется 3.10)
39
+ - Удалённый сервер с Linux (Ubuntu/Debian/CentOS) и предустановленным Python
40
+ - Доступ по SSH с паролем (для первого копирования ключа)
41
+ - На локальной машине:
42
+ - `ssh-keygen` (обычно уже есть)
43
+ - `ssh` клиент
44
+ - (опционально) `code` для VS Code Remote (если планируете открывать редактор)
45
+
46
+ ---
47
+
48
+ ## Установка
49
+
50
+ ```bash
51
+ # Клонируем репозиторий
52
+ git clone https://github.com/your-username/gpu-setup.git
53
+ cd gpu-setup
54
+
55
+ # Устанавливаем в виртуальное окружение (рекомендуется)
56
+ python -m venv venv
57
+ source venv/bin/activate # или venv\Scripts\activate на Windows
58
+
59
+ # Устанавливаем зависимости
60
+ pip install -r requirements.txt
61
+
62
+ # Устанавливаем пакет в режиме разработки (опционально)
63
+ pip install -e .
64
+ ```
65
+
66
+ После этого команда `gpu-setup` будет доступна в терминале.
67
+
68
+ ---
69
+
70
+ ## Использование
71
+
72
+ Общий синтаксис:
73
+
74
+ ```bash
75
+ gpu-setup [команда] [опции]
76
+ ```
77
+
78
+ ### Доступные команды
79
+
80
+ | Команда | Описание |
81
+ |-------------------|----------|
82
+ | `setup` | **Полная настройка:** SSH, Ollama, модель, VS Code |
83
+ | `ssh` | Только настройка SSH-ключей и алиаса |
84
+ | `ollama-install` | Установка Ollama на сервер |
85
+ | `model-pull` | Загрузка указанной модели (после установки Ollama) |
86
+ | `vscode` | Открытие VS Code Remote (если `code` установлен) |
87
+ | `plan` | Показать план действий без выполнения (dry-run) |
88
+
89
+ ---
90
+
91
+ ## Подробное описание команд
92
+
93
+ ### `gpu-setup setup`
94
+
95
+ Выполняет все шаги: генерация ключа, копирование, настройка SSH-конфига, установка Ollama, загрузка модели, открытие VS Code.
96
+
97
+ **Опции:**
98
+
99
+ - `--ip` (обязательный) — IP сервера
100
+ - `--user` (обязательный) — имя пользователя
101
+ - `--password` (обязательный для первого подключения) — пароль (можно ввести интерактивно)
102
+ - `--model` — модель для скачивания (например `llama3.2`)
103
+ - `--install-ollama` / `--no-install` — устанавливать Ollama (по умолчанию `--install-ollama`)
104
+ - `--no-vscode` — не открывать VS Code (по умолчанию открывает)
105
+ - `--dry-run` — только показать план
106
+ - `--key-type` — тип ключа: `ed25519` (по умолчанию) или `rsa`
107
+
108
+ ---
109
+
110
+ ### `gpu-setup ssh`
111
+
112
+ Только настройка SSH-доступа по ключу.
113
+
114
+ **Опции:**
115
+
116
+ - `--ip` (обязательный)
117
+ - `--user` (обязательный)
118
+ - `--password` (обязательный для первого копирования)
119
+ - `--dry-run`
120
+ - `--key-type`
121
+
122
+ ---
123
+
124
+ ### `gpu-setup ollama-install`
125
+
126
+ Устанавливает Ollama на сервер (требуется пароль для sudo).
127
+
128
+ **Опции:**
129
+
130
+ - `--ip` (обязательный)
131
+ - `--user` (обязательный)
132
+ - `--password` (обязательный, т.к. нужен sudo)
133
+ - `--dry-run`
134
+
135
+ ---
136
+
137
+ ### `gpu-setup model-pull MODEL`
138
+
139
+ Скачивает указанную модель через Ollama. Если сервис Ollama не запущен, пытается запустить его автоматически.
140
+
141
+ **Аргументы:**
142
+
143
+ - `MODEL` — имя модели (например `llama3.2`)
144
+
145
+ **Опции:**
146
+
147
+ - `--ip` (обязательный)
148
+ - `--user` (обязательный)
149
+ - `--password` (необязателен, если SSH уже настроен; нужен только для запуска сервиса)
150
+ - `--dry-run`
151
+
152
+ ---
153
+
154
+ ### `gpu-setup vscode`
155
+
156
+ Открывает текущую сессию в VS Code Remote.
157
+
158
+ **Опции:**
159
+
160
+ - `--ip` (обязательный)
161
+ - `--user` (обязательный)
162
+ - `--password` (не требуется, если ключ настроен)
163
+ - `--dry-run`
164
+
165
+ ---
166
+
167
+ ### `gpu-setup plan`
168
+
169
+ Показывает, какие шаги будут выполнены при вызове `setup` с указанными параметрами.
170
+
171
+ **Опции:**
172
+
173
+ - `--ip` (обязательный)
174
+ - `--user` (обязательный)
175
+ - `--model`
176
+ - `--install-ollama` / `--no-install`
177
+ - `--no-vscode`
178
+
179
+ ---
180
+
181
+ ## Примеры использования
182
+
183
+ ### 1. Полная настройка (самый частый вариант)
184
+
185
+ ```bash
186
+ gpu-setup setup --ip 195.208.16.2 --user user --password sWDoAVym --model llama3.2
187
+ ```
188
+
189
+ ### 2. Только SSH-ключи + config
190
+
191
+ ```bash
192
+ gpu-setup ssh --ip 195.208.16.2 --user user --password sWDoAVym
193
+ ```
194
+
195
+ ### 3. Только установить Ollama
196
+
197
+ ```bash
198
+ gpu-setup ollama-install --ip 195.208.16.2 --user user --password sWDoAVym
199
+ ```
200
+
201
+ ### 4. Только скачать модель (пароль уже не нужен, если SSH настроен)
202
+
203
+ ```bash
204
+ gpu-setup model-pull llama3.2 --ip 195.208.16.2 --user user
205
+ ```
206
+
207
+ ### 5. Только открыть VS Code Remote
208
+
209
+ ```bash
210
+ gpu-setup vscode --ip 195.208.16.2 --user user
211
+ ```
212
+
213
+ ### 6. Посмотреть план перед запуском
214
+
215
+ ```bash
216
+ gpu-setup plan --ip 195.208.16.2 --user user --model llama3.2
217
+ ```
218
+
219
+ ---
220
+
221
+ ## Dry-run
222
+
223
+ Любая команда поддерживает флаг `--dry-run`.
224
+ В этом режиме ничего не изменяется, а только выводится, что было бы сделано.
225
+
226
+ ```bash
227
+ gpu-setup setup --ip 195.208.16.2 --user user --dry-run
228
+ ```
229
+ ---
230
+
231
+ ## Устранение неполадок
232
+
233
+ - **`ssh: Could not resolve hostname`** — проверьте IP и доступность сервера.
234
+ - **Ошибка при копировании ключа** — убедитесь, что пароль верный и на сервере разрешена аутентификация по паролю (`PasswordAuthentication yes` в `/etc/ssh/sshd_config`).
235
+ - **Ollama не устанавливается** — возможно, требуется вручную разрешить `sudo` без пароля или передать пароль в опции.
236
+ - **VS Code не открывается** — установите CLI-команду `code` (в VS Code: `Shift+Ctrl+P` → `Install 'code' command in PATH`).
237
+
238
+ ---
239
+
240
+ ## Лицензия
241
+
242
+ MIT License. См. файл [LICENSE](LICENSE).
243
+
244
+ ---
245
+
246
+ ## Вклад
247
+
248
+ PR и issues приветствуются!
@@ -0,0 +1,13 @@
1
+ gpu_server_setup-0.4.0.dist-info/licenses/LICENSE,sha256=ZWAZzQVUVtlMdFelTjFhMd-K8y2T9tFK4EyLxMkU9Ao,201
2
+ gpu_setup/__init__.py,sha256=avsbCKip30Rpzp1Xah_6nKWzTTTDXCoqG1vbJXEEE3Q,39
3
+ gpu_setup/__version__.py,sha256=pVYhOkVS5PHJd_cIEx6PTaY6VggXFTC3pqWdf0I8zao,21
4
+ gpu_setup/cli.py,sha256=Gc27DVPWoXkwPWpwAhrs4fQeMT69raWotldJOJPSldg,4548
5
+ gpu_setup/config.py,sha256=sYh7ZrKciweVoG_KcRZhepNTD9llPIhsUvkF6LRsp6U,405
6
+ gpu_setup/core.py,sha256=VdRztX4hOz8T05cikhRspC41-zlfdyyGWO6sHpb02Hw,12718
7
+ gpu_setup/rich_console.py,sha256=UgTfBq3w-okFmC0sfMKrOikHE5SbhQ6cc-xjoaw0-xY,1135
8
+ gpu_setup/utils.py,sha256=_d4frRgb1oWSFFKp4tjjfyfCWjx1JiPgdtlyOI_4fXo,609
9
+ gpu_server_setup-0.4.0.dist-info/METADATA,sha256=PZl4NIx0QszbnyQgk6ZNd5V5z9RyG4ycC4JdhOncq7c,8686
10
+ gpu_server_setup-0.4.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
+ gpu_server_setup-0.4.0.dist-info/entry_points.txt,sha256=CfPLHuqN1W6kaaKXckZAxW4KGfM_yKb1_v4uQ8ps8is,55
12
+ gpu_server_setup-0.4.0.dist-info/top_level.txt,sha256=hEATDPSHJ8aD3B1_Txg94aJsKavZXMFQbaJHEst5WqY,10
13
+ gpu_server_setup-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gpu-server-setup = gpu_setup.cli:app
@@ -0,0 +1,9 @@
1
+
2
+ ### 3. `LICENSE`
3
+ ```text
4
+ MIT License
5
+
6
+ Copyright (c) 2025 Твой Ник
7
+
8
+ Permission is hereby granted, free of charge...
9
+ (стандартный MIT — можешь оставить как есть)
@@ -0,0 +1 @@
1
+ gpu_setup
gpu_setup/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import app
2
+
3
+ __all__ = ["app"]
@@ -0,0 +1 @@
1
+ __version__ = "0.5.0"
gpu_setup/cli.py ADDED
@@ -0,0 +1,138 @@
1
+ import typer
2
+ from typing import Optional
3
+
4
+ from .core import GPUServer, get_plan
5
+ from .rich_console import header, plan_table, success, error
6
+
7
+ app = typer.Typer(
8
+ name="gpu-setup",
9
+ help="Один клик — удалённый GPU-сервер с Ollama + VS Code Remote",
10
+ rich_markup_mode="rich",
11
+ add_completion=True,
12
+ )
13
+
14
+
15
+ @app.command()
16
+ def plan(
17
+ ip: str = typer.Option(..., "--ip", help="IP-адрес сервера"),
18
+ user: str = typer.Option(..., "--user", help="Имя пользователя"),
19
+ model: Optional[str] = typer.Option(None, "--model", help="Модель Ollama"),
20
+ install_ollama: bool = typer.Option(True, "--install-ollama/--no-install"),
21
+ no_vscode: bool = typer.Option(False, "--no-vscode"),
22
+ ):
23
+ """Показать план выполнения (dry-run)"""
24
+ server = GPUServer(ip=ip, user=user, dry_run=True)
25
+ plan_list = get_plan(server, install_ollama, model, no_vscode)
26
+ header(f"План для сервера {ip}")
27
+ plan_table(plan_list)
28
+
29
+
30
+ @app.command()
31
+ def setup(
32
+ ip: str = typer.Option(..., "--ip", help="IP-адрес сервера"),
33
+ user: str = typer.Option(..., "--user", help="Имя пользователя"),
34
+ password: str = typer.Option(
35
+ None,
36
+ "--password",
37
+ prompt=True,
38
+ hide_input=True,
39
+ help="Пароль (только для первого подключения и sudo)",
40
+ ),
41
+ model: Optional[str] = typer.Option(None, "--model", help="Модель Ollama (например llama3.2)"),
42
+ install_ollama: bool = typer.Option(True, "--install-ollama/--no-install"),
43
+ no_vscode: bool = typer.Option(False, "--no-vscode"),
44
+ dry_run: bool = typer.Option(False, "--dry-run"),
45
+ key_type: str = typer.Option("ed25519", "--key-type"),
46
+ ):
47
+ if dry_run:
48
+ plan(ip=ip, user=user, model=model, install_ollama=install_ollama, no_vscode=no_vscode)
49
+ return
50
+
51
+ server = GPUServer(
52
+ ip=ip, user=user, password=password, key_type=key_type, dry_run=False
53
+ )
54
+
55
+ server.generate_key()
56
+ server.copy_pubkey()
57
+ server.setup_ssh_config()
58
+ server.test_connection()
59
+
60
+ if install_ollama:
61
+ server.install_ollama()
62
+
63
+ if model:
64
+ server.pull_model(model)
65
+
66
+ if not no_vscode:
67
+ server.open_vscode()
68
+
69
+ success("Готово! 🎉")
70
+ if model:
71
+ typer.echo(f"\nЗапуск модели: [bold]ollama run {model}[/bold]")
72
+
73
+
74
+
75
+ @app.command()
76
+ def ssh(
77
+ ip: str = typer.Option(..., "--ip", help="IP-адрес сервера"),
78
+ user: str = typer.Option(..., "--user", help="Имя пользователя"),
79
+ password: str = typer.Option(
80
+ None,
81
+ "--password",
82
+ prompt=True,
83
+ hide_input=True,
84
+ help="Пароль для первого копирования ключа",
85
+ ),
86
+ dry_run: bool = typer.Option(False, "--dry-run"),
87
+ key_type: str = typer.Option("ed25519", "--key-type"),
88
+ ):
89
+ server = GPUServer(ip=ip, user=user, password=password, key_type=key_type, dry_run=dry_run)
90
+ server.generate_key()
91
+ server.copy_pubkey()
92
+ server.setup_ssh_config()
93
+ server.test_connection()
94
+ success("SSH полностью настроен!")
95
+
96
+
97
+ @app.command("ollama-install")
98
+ def ollama_install(
99
+ ip: str = typer.Option(..., "--ip"),
100
+ user: str = typer.Option(..., "--user"),
101
+ password: str = typer.Option(
102
+ None,
103
+ "--password",
104
+ prompt=True,
105
+ hide_input=True,
106
+ help="Пароль для sudo",
107
+ ),
108
+ dry_run: bool = typer.Option(False, "--dry-run"),
109
+ ):
110
+ server = GPUServer(ip=ip, user=user, password=password, dry_run=dry_run)
111
+ server.install_ollama()
112
+
113
+
114
+ @app.command("model-pull")
115
+ def model_pull(
116
+ model: str = typer.Argument(..., help="Название модели (llama3.2, gemma2 и т.д.)"),
117
+ ip: str = typer.Option(..., "--ip"),
118
+ user: str = typer.Option(..., "--user"),
119
+ password: Optional[str] = typer.Option(None, "--password", hide_input=True),
120
+ dry_run: bool = typer.Option(False, "--dry-run"),
121
+ ):
122
+ server = GPUServer(ip=ip, user=user, password=password, dry_run=dry_run)
123
+ server.pull_model(model)
124
+
125
+
126
+ @app.command()
127
+ def vscode(
128
+ ip: str = typer.Option(..., "--ip"),
129
+ user: str = typer.Option(..., "--user"),
130
+ password: Optional[str] = typer.Option(None, "--password", hide_input=True),
131
+ dry_run: bool = typer.Option(False, "--dry-run"),
132
+ ):
133
+ server = GPUServer(ip=ip, user=user, password=password, dry_run=dry_run)
134
+ server.open_vscode()
135
+
136
+
137
+ if __name__ == "__main__":
138
+ app()
gpu_setup/config.py ADDED
@@ -0,0 +1,17 @@
1
+ from pathlib import Path
2
+ from pydantic_settings import BaseSettings, SettingsConfigDict
3
+
4
+
5
+ class Settings(BaseSettings):
6
+ model_config = SettingsConfigDict(
7
+ env_prefix="GPU_SETUP_",
8
+ env_file=".env",
9
+ env_file_encoding="utf-8",
10
+ extra="ignore",
11
+ )
12
+
13
+ default_key_type: str = "ed25519"
14
+ config_dir: Path = Path.home() / ".config" / "gpu-setup"
15
+
16
+
17
+ settings = Settings()
gpu_setup/core.py ADDED
@@ -0,0 +1,331 @@
1
+ from pathlib import Path
2
+ import sys
3
+ from typing import Optional
4
+
5
+ import paramiko
6
+ from paramiko import SSHClient, AutoAddPolicy, SSHException
7
+
8
+ from .utils import run_local, console
9
+ from .rich_console import header, success, error, warning
10
+ from .config import settings
11
+
12
+ import subprocess
13
+ import shutil
14
+ import time
15
+
16
+
17
+ class GPUServer:
18
+ def __init__(
19
+ self,
20
+ ip: str,
21
+ user: str,
22
+ password: Optional[str] = None,
23
+ key_type: str = "ed25519",
24
+ dry_run: bool = False,
25
+ ):
26
+ self.ip = ip
27
+ self.user = user
28
+ self.password = password
29
+ self.dry_run = dry_run
30
+ self.key_type = key_type
31
+ self.alias = f"gpu-server-{ip.replace('.', '-')}"
32
+ self.key_path = Path.home() / f".ssh/id_{key_type}_gpu"
33
+ self.pub_path = self.key_path.with_suffix(".pub")
34
+ self.config_file = Path.home() / ".ssh/config"
35
+
36
+ def generate_key(self) -> None:
37
+ header("Генерация SSH-ключа")
38
+ if self.key_path.exists():
39
+ success("SSH-ключ уже существует")
40
+ return
41
+
42
+ if self.dry_run:
43
+ console.print("[dim]DRY-RUN: ssh-keygen[/dim]")
44
+ return
45
+
46
+ cmd = (
47
+ ["ssh-keygen", "-t", self.key_type, "-f", str(self.key_path), "-N", "", "-q"]
48
+ if self.key_type == "ed25519"
49
+ else ["ssh-keygen", "-t", "rsa", "-b", "4096", "-f", str(self.key_path), "-N", "", "-q"]
50
+ )
51
+ run_local(cmd)
52
+ success(f"Ключ {self.key_type} создан")
53
+
54
+ def copy_pubkey(self) -> None:
55
+ header("Копирование публичного ключа на сервер")
56
+ if self.dry_run:
57
+ console.print("[dim]DRY-RUN: копирование ключа через paramiko[/dim]")
58
+ return
59
+
60
+ if not self.password:
61
+ error("Для первого подключения нужен пароль")
62
+ sys.exit(1)
63
+
64
+ try:
65
+ client = SSHClient()
66
+ client.set_missing_host_key_policy(AutoAddPolicy())
67
+ client.connect(
68
+ hostname=self.ip,
69
+ username=self.user,
70
+ password=self.password,
71
+ timeout=15,
72
+ look_for_keys=False,
73
+ allow_agent=False,
74
+ )
75
+
76
+ pubkey = self.pub_path.read_text().strip()
77
+ cmd = (
78
+ f"mkdir -p ~/.ssh && chmod 700 ~/.ssh && "
79
+ f"echo '{pubkey}' >> ~/.ssh/authorized_keys && "
80
+ f"chmod 600 ~/.ssh/authorized_keys"
81
+ )
82
+
83
+ _, stdout, stderr = client.exec_command(cmd)
84
+ stdout.read()
85
+ stderr.read()
86
+ client.close()
87
+ success("Публичный ключ добавлен")
88
+ except SSHException as e:
89
+ error(f"Не удалось скопировать ключ: {e}")
90
+ sys.exit(1)
91
+
92
+ def setup_ssh_config(self) -> None:
93
+ header("Настройка ~/.ssh/config")
94
+ if self.dry_run:
95
+ console.print("[dim]DRY-RUN: запись алиаса[/dim]")
96
+ return
97
+
98
+ entry = f"""
99
+ Host {self.alias}
100
+ HostName {self.ip}
101
+ User {self.user}
102
+ IdentityFile {self.key_path}
103
+ StrictHostKeyChecking no
104
+ """
105
+
106
+ self.config_file.parent.mkdir(exist_ok=True)
107
+ self.config_file.touch(exist_ok=True)
108
+
109
+ content = self.config_file.read_text()
110
+ if f"Host {self.alias}" not in content:
111
+ with open(self.config_file, "a", encoding="utf-8") as f:
112
+ f.write(entry)
113
+ success(f"Алиас {self.alias} добавлен")
114
+ else:
115
+ success("Алиас уже существует")
116
+
117
+ def test_connection(self) -> None:
118
+ header("Тест SSH-подключения по ключу")
119
+ if self.dry_run:
120
+ console.print("[dim]DRY-RUN: тест SSH[/dim]")
121
+ return
122
+
123
+ try:
124
+ client = SSHClient()
125
+ client.set_missing_host_key_policy(AutoAddPolicy())
126
+ client.connect(
127
+ hostname=self.ip,
128
+ username=self.user,
129
+ key_filename=str(self.key_path),
130
+ timeout=10,
131
+ look_for_keys=False,
132
+ allow_agent=False,
133
+ )
134
+ client.exec_command("exit")
135
+ client.close()
136
+ success("SSH по ключу работает")
137
+ except Exception as e:
138
+ error(f"Тест SSH провалился: {e}")
139
+ sys.exit(1)
140
+
141
+ def install_ollama(self) -> None:
142
+ header("Установка Ollama")
143
+ if self.dry_run:
144
+ console.print("[dim]DRY-RUN: установка Ollama[/dim]")
145
+ return
146
+
147
+ client = SSHClient()
148
+ client.set_missing_host_key_policy(AutoAddPolicy())
149
+ client.connect(hostname=self.ip, username=self.user, key_filename=str(self.key_path))
150
+
151
+ _, stdout, _ = client.exec_command("command -v ollama")
152
+ if stdout.read().decode().strip():
153
+ success("Ollama уже установлен")
154
+ client.close()
155
+ return
156
+
157
+ if not self.password:
158
+ error("Для установки Ollama нужен пароль (sudo)")
159
+ sys.exit(1)
160
+
161
+ sudo_client = SSHClient()
162
+ sudo_client.set_missing_host_key_policy(AutoAddPolicy())
163
+ sudo_client.connect(
164
+ hostname=self.ip,
165
+ username=self.user,
166
+ password=self.password,
167
+ timeout=15,
168
+ )
169
+
170
+ install_cmd = "bash -c 'curl -fsSL https://ollama.com/install.sh | sh'"
171
+ _, stdout, stderr = sudo_client.exec_command(f"sudo -S {install_cmd}")
172
+ stdout.channel.send(self.password + "\n")
173
+ stdout.channel.flush()
174
+
175
+ output = stdout.read().decode()
176
+ err_output = stderr.read().decode()
177
+ if err_output:
178
+ console.print(err_output, end="")
179
+
180
+ console.print("[bold blue]Запускаем Ollama сервис...[/bold blue]")
181
+ start_cmd = "sudo systemctl enable ollama && sudo systemctl start ollama"
182
+ _, stdout, stderr = sudo_client.exec_command(start_cmd)
183
+
184
+ stdout.read()
185
+ err_output = stderr.read().decode()
186
+
187
+ if err_output:
188
+ console.print(err_output, end="")
189
+
190
+ success("Ollama сервис запущен")
191
+
192
+ sudo_client.close()
193
+ client.close()
194
+ success("Ollama успешно установлен")
195
+
196
+ def pull_model(self, model: str) -> None:
197
+ header(f"Скачивание модели {model}")
198
+ if self.dry_run:
199
+ console.print(f"[dim]DRY-RUN: ollama pull {model}[/dim]")
200
+ return
201
+
202
+ client = SSHClient()
203
+ client.set_missing_host_key_policy(AutoAddPolicy())
204
+ client.connect(hostname=self.ip, username=self.user, key_filename=str(self.key_path))
205
+
206
+ _, stdout, _ = client.exec_command("command -v ollama")
207
+ if not stdout.read().decode().strip():
208
+ error("Ollama не установлен. Добавь --install-ollama")
209
+ sys.exit(1)
210
+
211
+ def check_ollama_service():
212
+ _, stdout, stderr = client.exec_command("ollama list")
213
+ exit_code = stdout.channel.recv_exit_status()
214
+ if exit_code != 0:
215
+ err_output = stderr.read().decode()
216
+ if "could not connect to ollama server" in err_output:
217
+ return False, err_output
218
+ return True, None
219
+
220
+ service_ok, err_msg = check_ollama_service()
221
+ if not service_ok:
222
+ warning("Ollama сервис не отвечает. Пытаемся запустить...")
223
+
224
+ if self.password:
225
+ sudo_client = SSHClient()
226
+ sudo_client.set_missing_host_key_policy(AutoAddPolicy())
227
+ sudo_client.connect(
228
+ hostname=self.ip,
229
+ username=self.user,
230
+ password=self.password,
231
+ timeout=15,
232
+ )
233
+
234
+ console.print("[dim]Запуск через systemctl...[/dim]")
235
+ stdin, stdout, stderr = sudo_client.exec_command("sudo -S systemctl start ollama")
236
+ stdin.write(self.password + "\n")
237
+ stdin.flush()
238
+ stdout.read()
239
+ stderr.read()
240
+ sudo_client.close()
241
+
242
+ console.print("[dim]Даём 5 секунд на запуск...[/dim]")
243
+ time.sleep(5)
244
+
245
+ service_ok, err_msg = check_ollama_service()
246
+ if not service_ok:
247
+ warning(f"systemctl не помог: {err_msg}")
248
+ console.print("[dim]Пробуем запустить ollama serve в фоне...[/dim]")
249
+ sudo_client2 = SSHClient()
250
+ sudo_client2.set_missing_host_key_policy(AutoAddPolicy())
251
+ sudo_client2.connect(
252
+ hostname=self.ip,
253
+ username=self.user,
254
+ password=self.password,
255
+ timeout=15,
256
+ )
257
+ start_cmd = f"nohup ollama serve > /tmp/ollama.log 2>&1 &"
258
+ stdin, stdout, stderr = sudo_client2.exec_command(f"sudo -S -u {self.user} bash -c '{start_cmd}'")
259
+ stdin.write(self.password + "\n")
260
+ stdin.flush()
261
+ stdout.read()
262
+ stderr.read()
263
+ sudo_client2.close()
264
+
265
+ console.print("[dim]Даём 10 секунд на запуск...[/dim]")
266
+ time.sleep(10)
267
+
268
+ # Последняя проверка
269
+ service_ok, err_msg = check_ollama_service()
270
+ if not service_ok:
271
+ error(f"Не удалось запустить сервис: {err_msg}")
272
+ sys.exit(1)
273
+ else:
274
+ success("Сервис Ollama успешно запущен (через nohup)")
275
+ else:
276
+ success("Сервис Ollama успешно запущен (через systemctl)")
277
+ else:
278
+ error("Пароль не задан, невозможно автоматически запустить сервис.")
279
+ console.print("Запустите вручную: ssh {} 'sudo systemctl start ollama' или 'ollama serve'".format(self.alias))
280
+ sys.exit(1)
281
+
282
+ _, stdout, _ = client.exec_command(f"ollama list | grep -q '{model}'")
283
+ if stdout.channel.recv_exit_status() == 0:
284
+ success(f"Модель {model} уже есть")
285
+ client.close()
286
+ return
287
+
288
+ console.print(f"[bold blue]Скачиваем {model}...[/bold blue]")
289
+ stdin, stdout, stderr = client.exec_command(f"ollama pull {model}", get_pty=True)
290
+ channel = stdout.channel
291
+
292
+ while not channel.exit_status_ready():
293
+ if channel.recv_ready():
294
+ print(channel.recv(1024).decode("utf-8", errors="replace"), end="", flush=True)
295
+ if channel.recv_stderr_ready():
296
+ print(channel.recv_stderr(1024).decode("utf-8", errors="replace"), end="", flush=True)
297
+
298
+ print(stdout.read().decode(), end="")
299
+ err_out = stderr.read().decode()
300
+ if err_out:
301
+ print(err_out, end="")
302
+ client.close()
303
+ success(f"Модель {model} готова")
304
+
305
+ def open_vscode(self) -> None:
306
+ header("Открываем VS Code Remote")
307
+ if self.dry_run:
308
+ console.print(f"[dim]DRY-RUN: code --remote ssh-remote+{self.alias}[/dim]")
309
+ return
310
+
311
+ if shutil.which("code"):
312
+ subprocess.run(["code", "--remote", f"ssh-remote+{self.alias}"])
313
+ success("VS Code Remote открыт")
314
+ else:
315
+ warning("Команда 'code' не найдена. Установи VS Code CLI")
316
+
317
+
318
+ def get_plan(server: GPUServer, install_ollama: bool, model: Optional[str], no_vscode: bool) -> list:
319
+ plan = [
320
+ ("Генерация ключа", "ed25519 (если нет)"),
321
+ ("Копирование ключа", "на сервер"),
322
+ ("SSH config", f"алиас {server.alias}"),
323
+ ("Тест подключения", "по ключу"),
324
+ ]
325
+ if install_ollama:
326
+ plan.append(("Ollama", "установка"))
327
+ if model:
328
+ plan.append(("Модель", f"ollama pull {model}"))
329
+ if not no_vscode:
330
+ plan.append(("VS Code", "открытие Remote"))
331
+ return plan
@@ -0,0 +1,39 @@
1
+ from rich.console import Console
2
+ from rich.panel import Panel
3
+ from rich.table import Table
4
+ from rich.progress import Progress, SpinnerColumn, TextColumn
5
+
6
+ console = Console()
7
+
8
+ def header(text: str) -> None:
9
+ console.print(Panel(f"[bold green]{text}[/bold green]", expand=False))
10
+
11
+
12
+ def success(text: str) -> None:
13
+ console.print(f"[bold green]{text}[/bold green]")
14
+
15
+
16
+ def warning(text: str) -> None:
17
+ console.print(f"[bold yellow]{text}[/bold yellow]")
18
+
19
+
20
+ def error(text: str) -> None:
21
+ console.print(f"[bold red]{text}[/bold red]")
22
+
23
+
24
+ def plan_table(plan: list[tuple[str, str]]) -> None:
25
+ table = Table(title="План выполнения", show_header=True, header_style="bold magenta")
26
+ table.add_column("Шаг", style="cyan")
27
+ table.add_column("Описание", style="dim")
28
+ for step, desc in plan:
29
+ table.add_row(step, desc)
30
+ console.print(table)
31
+
32
+
33
+ def live_ollama_output() -> None:
34
+ with Progress(
35
+ SpinnerColumn(),
36
+ TextColumn("[bold blue]Скачиваем модель...[/bold blue]"),
37
+ transient=True,
38
+ ) as progress:
39
+ progress.add_task("pull", total=None)
gpu_setup/utils.py ADDED
@@ -0,0 +1,22 @@
1
+ import subprocess
2
+ import sys
3
+ from pathlib import Path
4
+ from typing import List
5
+
6
+ from rich.console import Console
7
+
8
+ console = Console()
9
+
10
+ def run_local(cmd: List[str], dry_run: bool = False) -> None:
11
+ if dry_run:
12
+ console.print(f"[dim]DRY-RUN → {' '.join(cmd)}[/dim]")
13
+ return
14
+ try:
15
+ subprocess.run(cmd, check=True, capture_output=True)
16
+ except subprocess.CalledProcessError as e:
17
+ console.print(f"[red]Локальная команда провалилась:[/red] {e}")
18
+ sys.exit(1)
19
+
20
+
21
+ def ensure_dir(path: Path) -> None:
22
+ path.mkdir(parents=True, exist_ok=True)