Refactor access card handling and update CLI output for tunnel operations
- Integrated `print_access_card` functionality into the `up` command for both tunneled and non-tunneled scenarios. - Removed the deprecated `print_mcp_snippet` function from the session management flow. - Updated the `tunnel_forwards` function to streamline port handling for LLM runtimes. - Enhanced test cases to reflect changes in access card printing and MCP snippet logging.
This commit is contained in:
@@ -0,0 +1,196 @@
|
|||||||
|
"""Pretty end-of-launch access card: all local URLs and API endpoints."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from rich.console import Console, Group
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.table import Table
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
|
from gpu_rent.config import Config
|
||||||
|
from gpu_rent.llm_runtime import normalize_runtime
|
||||||
|
from gpu_rent.state import load_state
|
||||||
|
|
||||||
|
Log = Callable[[str], None]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AccessLink:
|
||||||
|
label: str
|
||||||
|
url: str
|
||||||
|
note: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_llm_runtime(cfg: Config) -> str:
|
||||||
|
runtime = normalize_runtime(cfg.llm_runtime)
|
||||||
|
try:
|
||||||
|
noted = (load_state().notes or {}).get("llm_runtime")
|
||||||
|
if noted:
|
||||||
|
runtime = normalize_runtime(str(noted))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return runtime
|
||||||
|
|
||||||
|
|
||||||
|
def collect_access_links(cfg: Config, *, tunneled: bool) -> list[AccessLink]:
|
||||||
|
"""Build the list of user-facing endpoints (unit-tested)."""
|
||||||
|
port = cfg.swarmui_local_port
|
||||||
|
base = f"http://127.0.0.1:{port}"
|
||||||
|
links: list[AccessLink] = []
|
||||||
|
if tunneled:
|
||||||
|
links.extend(
|
||||||
|
[
|
||||||
|
AccessLink("SwarmUI UI", base, "браузер"),
|
||||||
|
AccessLink("SwarmUI API", f"{base}/API/", "HTTP JSON"),
|
||||||
|
AccessLink("SwarmUI MCP", f"{base}/mcp", "Cursor mcp.json"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
runtime = resolve_llm_runtime(cfg)
|
||||||
|
if runtime == "ollama":
|
||||||
|
o = cfg.ollama_local_port
|
||||||
|
links.extend(
|
||||||
|
[
|
||||||
|
AccessLink(
|
||||||
|
"Ollama API",
|
||||||
|
f"http://127.0.0.1:{o}",
|
||||||
|
f"OLLAMA_HOST=http://127.0.0.1:{o}",
|
||||||
|
),
|
||||||
|
AccessLink("Ollama tags", f"http://127.0.0.1:{o}/api/tags", "список моделей"),
|
||||||
|
AccessLink(
|
||||||
|
"Ollama chat",
|
||||||
|
f"http://127.0.0.1:{o}/api/chat",
|
||||||
|
"POST generate",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
elif runtime == "llamacpp":
|
||||||
|
p = cfg.llamacpp_local_port
|
||||||
|
links.extend(
|
||||||
|
[
|
||||||
|
AccessLink("llama.cpp", f"http://127.0.0.1:{p}", "OpenAI-compatible"),
|
||||||
|
AccessLink(
|
||||||
|
"OpenAI /v1",
|
||||||
|
f"http://127.0.0.1:{p}/v1/chat/completions",
|
||||||
|
"chat completions",
|
||||||
|
),
|
||||||
|
AccessLink("Models", f"http://127.0.0.1:{p}/v1/models", "list"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
links.append(
|
||||||
|
AccessLink(
|
||||||
|
"Туннель",
|
||||||
|
"gpu-rent tunnel --open",
|
||||||
|
"локальные URL появятся после tunnel",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return links
|
||||||
|
|
||||||
|
|
||||||
|
def mcp_snippet_lines(cfg: Config) -> list[str]:
|
||||||
|
port = cfg.swarmui_local_port
|
||||||
|
return [
|
||||||
|
"{",
|
||||||
|
' "mcpServers": {',
|
||||||
|
' "swarmui": {',
|
||||||
|
f' "url": "http://127.0.0.1:{port}/mcp"',
|
||||||
|
" }",
|
||||||
|
" }",
|
||||||
|
"}",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def render_access_panel(
|
||||||
|
cfg: Config,
|
||||||
|
*,
|
||||||
|
tunneled: bool = True,
|
||||||
|
host: str | None = None,
|
||||||
|
title: str = "gpu-rent · доступы",
|
||||||
|
) -> Panel:
|
||||||
|
links = collect_access_links(cfg, tunneled=tunneled)
|
||||||
|
table = Table(show_header=True, header_style="bold", box=None, pad_edge=False)
|
||||||
|
table.add_column("сервис", style="cyan", no_wrap=True)
|
||||||
|
table.add_column("URL / команда", style="bold white")
|
||||||
|
table.add_column("заметка", style="dim")
|
||||||
|
|
||||||
|
for link in links:
|
||||||
|
table.add_row(link.label, link.url, link.note)
|
||||||
|
|
||||||
|
cmds = Table(show_header=False, box=None, pad_edge=False)
|
||||||
|
cmds.add_column(style="dim", no_wrap=True)
|
||||||
|
cmds.add_column()
|
||||||
|
cmds.add_row("открыть UI", "gpu-rent open")
|
||||||
|
if resolve_llm_runtime(cfg) in {"ollama", "llamacpp"}:
|
||||||
|
cmds.add_row("открыть LLM", "gpu-rent open --llm")
|
||||||
|
cmds.add_row("hold killer", "gpu-rent hold")
|
||||||
|
cmds.add_row("стоп GPU", "gpu-rent stop")
|
||||||
|
if not tunneled:
|
||||||
|
cmds.add_row("туннель", "gpu-rent tunnel --open")
|
||||||
|
|
||||||
|
mcp = Text("\n".join(mcp_snippet_lines(cfg)), style="green")
|
||||||
|
subtitle = Text()
|
||||||
|
if tunneled:
|
||||||
|
subtitle.append("туннель слушает localhost", style="green")
|
||||||
|
if host:
|
||||||
|
subtitle.append(f" · VM {host}", style="dim")
|
||||||
|
else:
|
||||||
|
subtitle.append("облако готово, туннеля нет", style="yellow")
|
||||||
|
if host:
|
||||||
|
subtitle.append(f" · FIP {host}", style="dim")
|
||||||
|
|
||||||
|
body = Group(
|
||||||
|
subtitle,
|
||||||
|
Text(""),
|
||||||
|
table,
|
||||||
|
Text(""),
|
||||||
|
Text("команды", style="bold"),
|
||||||
|
cmds,
|
||||||
|
Text(""),
|
||||||
|
Text("Cursor MCP (вставь в mcp.json — файл сам не трогаем)", style="bold"),
|
||||||
|
mcp,
|
||||||
|
)
|
||||||
|
return Panel(
|
||||||
|
body,
|
||||||
|
title=f"[bold]{title}[/bold]",
|
||||||
|
border_style="bright_blue",
|
||||||
|
padding=(1, 2),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def print_access_card(
|
||||||
|
cfg: Config,
|
||||||
|
*,
|
||||||
|
tunneled: bool = True,
|
||||||
|
host: str | None = None,
|
||||||
|
console: Console | None = None,
|
||||||
|
log: Log | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Print Rich panel; fall back to plain lines if needed."""
|
||||||
|
panel = render_access_panel(cfg, tunneled=tunneled, host=host)
|
||||||
|
if console is not None:
|
||||||
|
console.print()
|
||||||
|
console.print(panel)
|
||||||
|
console.print()
|
||||||
|
return
|
||||||
|
if log is not None:
|
||||||
|
# Plain fallback for non-Rich loggers
|
||||||
|
log("")
|
||||||
|
log("══ gpu-rent · доступы ══")
|
||||||
|
for link in collect_access_links(cfg, tunneled=tunneled):
|
||||||
|
extra = f" ({link.note})" if link.note else ""
|
||||||
|
log(f" {link.label:14} {link.url}{extra}")
|
||||||
|
log("— Cursor MCP —")
|
||||||
|
for line in mcp_snippet_lines(cfg):
|
||||||
|
log(line)
|
||||||
|
log("hold: gpu-rent hold | stop: gpu-rent stop")
|
||||||
|
log("")
|
||||||
|
return
|
||||||
|
Console(highlight=False, legacy_windows=False).print(panel)
|
||||||
|
|
||||||
|
|
||||||
|
def print_mcp_snippet(cfg: Config, log: Log) -> None:
|
||||||
|
"""Back-compat: plain MCP + links via log."""
|
||||||
|
print_access_card(cfg, tunneled=True, log=log)
|
||||||
+8
-4
@@ -456,20 +456,24 @@ def up(
|
|||||||
log=lambda m: console.print(m),
|
log=lambda m: console.print(m),
|
||||||
)
|
)
|
||||||
if no_tunnel:
|
if no_tunnel:
|
||||||
|
from gpu_rent.access_card import print_access_card
|
||||||
|
|
||||||
console.print(
|
console.print(
|
||||||
f"[bold]готово[/bold] (без туннеля). "
|
f"[bold]готово[/bold] (без туннеля). "
|
||||||
f"UI: gpu-rent tunnel --open | stop: gpu-rent stop"
|
f"UI: gpu-rent tunnel --open | stop: gpu-rent stop"
|
||||||
)
|
)
|
||||||
if state.floating_ip:
|
if state.floating_ip:
|
||||||
console.print(f"FIP {state.floating_ip}")
|
console.print(f"FIP {state.floating_ip}")
|
||||||
|
print_access_card(
|
||||||
|
cfg,
|
||||||
|
tunneled=False,
|
||||||
|
host=state.floating_ip,
|
||||||
|
console=console,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not state.floating_ip:
|
if not state.floating_ip:
|
||||||
raise GpuRentError("нет floating IP после up — туннель не открыть")
|
raise GpuRentError("нет floating IP после up — туннель не открыть")
|
||||||
console.print(
|
|
||||||
f"[bold]панель[/bold] http://127.0.0.1:{cfg.swarmui_local_port} "
|
|
||||||
"(Ctrl+C = закрыть туннель, GPU жив → gpu-rent stop)"
|
|
||||||
)
|
|
||||||
run_tunnel(
|
run_tunnel(
|
||||||
cfg,
|
cfg,
|
||||||
state.floating_ip,
|
state.floating_ip,
|
||||||
|
|||||||
+3
-22
@@ -5,10 +5,13 @@ from __future__ import annotations
|
|||||||
import sys
|
import sys
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
from gpu_rent.access_card import print_access_card, print_mcp_snippet
|
||||||
from gpu_rent.config import Config
|
from gpu_rent.config import Config
|
||||||
|
|
||||||
Log = Callable[[str], None]
|
Log = Callable[[str], None]
|
||||||
|
|
||||||
|
__all__ = ["notify_ready", "print_mcp_snippet", "print_access_card"]
|
||||||
|
|
||||||
|
|
||||||
def notify_ready(cfg: Config, log: Log) -> None:
|
def notify_ready(cfg: Config, log: Log) -> None:
|
||||||
if not cfg.notify_ready:
|
if not cfg.notify_ready:
|
||||||
@@ -36,7 +39,6 @@ def _sound() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _windows_toast(log: Log) -> None:
|
def _windows_toast(log: Log) -> None:
|
||||||
# PowerShell WinRT toast — no admin; fail soft.
|
|
||||||
script = (
|
script = (
|
||||||
"[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, "
|
"[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, "
|
||||||
"ContentType = WindowsRuntime] > $null; "
|
"ContentType = WindowsRuntime] > $null; "
|
||||||
@@ -59,24 +61,3 @@ def _windows_toast(log: Log) -> None:
|
|||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log(f"toast недоступен ({exc}) — только звук/лог")
|
log(f"toast недоступен ({exc}) — только звук/лог")
|
||||||
|
|
||||||
|
|
||||||
def print_mcp_snippet(cfg: Config, log: Log) -> None:
|
|
||||||
port = cfg.swarmui_local_port
|
|
||||||
log("")
|
|
||||||
log("— Cursor MCP (вставь в mcp.json, файл сам не трогаем) —")
|
|
||||||
log("{")
|
|
||||||
log(' "mcpServers": {')
|
|
||||||
log(' "swarmui": {')
|
|
||||||
log(f' "url": "http://127.0.0.1:{port}/mcp"')
|
|
||||||
log(" }")
|
|
||||||
log(" }")
|
|
||||||
log("}")
|
|
||||||
log("")
|
|
||||||
log(f"SwarmUI на VM: 127.0.0.1:7801 (только через туннель)")
|
|
||||||
log(f"Локально: уже в up (или gpu-rent tunnel)")
|
|
||||||
log(f"Браузер: gpu-rent open → http://127.0.0.1:{port}")
|
|
||||||
log(f"API: http://127.0.0.1:{port}/API/")
|
|
||||||
log(f"MCP: http://127.0.0.1:{port}/mcp")
|
|
||||||
log("Hold killer: gpu-rent hold")
|
|
||||||
log("Стоп GPU: gpu-rent stop")
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from gpu_rent.bootstrap import run_bootstrap
|
|||||||
from gpu_rent.provision import provision_vm
|
from gpu_rent.provision import provision_vm
|
||||||
from gpu_rent.ready import wait_backend_idle
|
from gpu_rent.ready import wait_backend_idle
|
||||||
from gpu_rent.snapshot import ensure_boot_snapshot
|
from gpu_rent.snapshot import ensure_boot_snapshot
|
||||||
from gpu_rent.notify import notify_ready, print_mcp_snippet
|
from gpu_rent.notify import notify_ready
|
||||||
from gpu_rent.config import Config
|
from gpu_rent.config import Config
|
||||||
from gpu_rent.errors import CloudError, GpuRentError
|
from gpu_rent.errors import CloudError, GpuRentError
|
||||||
from gpu_rent.inventory import (
|
from gpu_rent.inventory import (
|
||||||
@@ -125,7 +125,6 @@ def _bind_access(
|
|||||||
except CloudError as exc:
|
except CloudError as exc:
|
||||||
log(f"snapshot: {exc}")
|
log(f"snapshot: {exc}")
|
||||||
notify_ready(cfg, log)
|
notify_ready(cfg, log)
|
||||||
print_mcp_snippet(cfg, log)
|
|
||||||
state.bootstrapped = True
|
state.bootstrapped = True
|
||||||
state.phase = "ready_cloud"
|
state.phase = "ready_cloud"
|
||||||
save_state(state)
|
save_state(state)
|
||||||
|
|||||||
+9
-25
@@ -15,7 +15,7 @@ from gpu_rent.cloud import (
|
|||||||
)
|
)
|
||||||
from gpu_rent.config import Config
|
from gpu_rent.config import Config
|
||||||
from gpu_rent.errors import CloudError, GpuRentError
|
from gpu_rent.errors import CloudError, GpuRentError
|
||||||
from gpu_rent.llm_runtime import llm_local_port, llm_remote_port, normalize_runtime
|
from gpu_rent.llm_runtime import normalize_runtime
|
||||||
from gpu_rent.os_client import connect
|
from gpu_rent.os_client import connect
|
||||||
from gpu_rent.ssh_ops import wait_ssh
|
from gpu_rent.ssh_ops import wait_ssh
|
||||||
from gpu_rent.state import load_state, save_state, utc_now
|
from gpu_rent.state import load_state, save_state, utc_now
|
||||||
@@ -75,7 +75,6 @@ def tunnel_forwards(cfg: Config) -> list[tuple[int, int]]:
|
|||||||
"""List of (local_port, remote_port). SwarmUI always; LLM if configured."""
|
"""List of (local_port, remote_port). SwarmUI always; LLM if configured."""
|
||||||
pairs = [(cfg.swarmui_local_port, 7801)]
|
pairs = [(cfg.swarmui_local_port, 7801)]
|
||||||
runtime = normalize_runtime(cfg.llm_runtime)
|
runtime = normalize_runtime(cfg.llm_runtime)
|
||||||
# Prefer state notes if provision recorded a different runtime this session.
|
|
||||||
state = load_state()
|
state = load_state()
|
||||||
noted = (state.notes or {}).get("llm_runtime")
|
noted = (state.notes or {}).get("llm_runtime")
|
||||||
if noted:
|
if noted:
|
||||||
@@ -83,17 +82,10 @@ def tunnel_forwards(cfg: Config) -> list[tuple[int, int]]:
|
|||||||
runtime = normalize_runtime(str(noted))
|
runtime = normalize_runtime(str(noted))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
remote = llm_remote_port(runtime)
|
|
||||||
local = llm_local_port(cfg) if runtime != "none" else None
|
|
||||||
# llm_local_port uses cfg.llm_runtime — override by mutating check:
|
|
||||||
if runtime == "ollama":
|
if runtime == "ollama":
|
||||||
local = cfg.ollama_local_port
|
pairs.append((cfg.ollama_local_port, 11434))
|
||||||
remote = 11434
|
|
||||||
elif runtime == "llamacpp":
|
elif runtime == "llamacpp":
|
||||||
local = cfg.llamacpp_local_port
|
pairs.append((cfg.llamacpp_local_port, 8080))
|
||||||
remote = 8080
|
|
||||||
if local and remote:
|
|
||||||
pairs.append((local, remote))
|
|
||||||
return pairs
|
return pairs
|
||||||
|
|
||||||
|
|
||||||
@@ -194,23 +186,15 @@ def run_tunnel(
|
|||||||
|
|
||||||
server = _start_forwarder(cfg, current_host, forwards)
|
server = _start_forwarder(cfg, current_host, forwards)
|
||||||
swarm_url = f"http://127.0.0.1:{cfg.swarmui_local_port}"
|
swarm_url = f"http://127.0.0.1:{cfg.swarmui_local_port}"
|
||||||
log(f"UI {swarm_url}")
|
|
||||||
log(f"API {swarm_url}/API/")
|
from gpu_rent.access_card import print_access_card
|
||||||
log(f"MCP {swarm_url}/mcp")
|
|
||||||
runtime = normalize_runtime(cfg.llm_runtime)
|
print_access_card(cfg, tunneled=True, host=current_host)
|
||||||
state = load_state()
|
|
||||||
if (state.notes or {}).get("llm_runtime"):
|
|
||||||
try:
|
|
||||||
runtime = normalize_runtime(str(state.notes["llm_runtime"]))
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
if runtime == "ollama":
|
|
||||||
log(f"Ollama API http://127.0.0.1:{cfg.ollama_local_port} (OLLAMA_HOST=…)")
|
|
||||||
elif runtime == "llamacpp":
|
|
||||||
log(f"llama.cpp http://127.0.0.1:{cfg.llamacpp_local_port}")
|
|
||||||
if open_browser:
|
if open_browser:
|
||||||
webbrowser.open(swarm_url)
|
webbrowser.open(swarm_url)
|
||||||
|
|
||||||
|
state = load_state()
|
||||||
state.phase = "ready_tunneled"
|
state.phase = "ready_tunneled"
|
||||||
save_state(state)
|
save_state(state)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
from gpu_rent.access_card import collect_access_links, mcp_snippet_lines
|
||||||
|
|
||||||
|
|
||||||
|
class _Cfg:
|
||||||
|
swarmui_local_port = 17801
|
||||||
|
llm_runtime = "ollama"
|
||||||
|
ollama_local_port = 17811
|
||||||
|
llamacpp_local_port = 17812
|
||||||
|
|
||||||
|
|
||||||
|
def test_collect_links_swarm_and_ollama(monkeypatch):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"gpu_rent.access_card.load_state",
|
||||||
|
lambda: type("S", (), {"notes": {"llm_runtime": "ollama"}})(),
|
||||||
|
)
|
||||||
|
links = collect_access_links(_Cfg(), tunneled=True)
|
||||||
|
labels = [x.label for x in links]
|
||||||
|
assert "SwarmUI UI" in labels
|
||||||
|
assert "SwarmUI MCP" in labels
|
||||||
|
assert "Ollama API" in labels
|
||||||
|
assert any("17811" in x.url for x in links)
|
||||||
|
|
||||||
|
|
||||||
|
def test_collect_links_no_tunnel():
|
||||||
|
links = collect_access_links(_Cfg(), tunneled=False)
|
||||||
|
assert links[0].url.startswith("gpu-rent tunnel")
|
||||||
|
|
||||||
|
|
||||||
|
def test_mcp_snippet_json():
|
||||||
|
lines = mcp_snippet_lines(_Cfg())
|
||||||
|
assert any("17801/mcp" in line for line in lines)
|
||||||
@@ -6,6 +6,9 @@ class _Cfg:
|
|||||||
boot_snapshot_name = "gpu-rent-boot-ok"
|
boot_snapshot_name = "gpu-rent-boot-ok"
|
||||||
swarmui_local_port = 17801
|
swarmui_local_port = 17801
|
||||||
notify_ready = True
|
notify_ready = True
|
||||||
|
llm_runtime = "none"
|
||||||
|
ollama_local_port = 17811
|
||||||
|
llamacpp_local_port = 17812
|
||||||
|
|
||||||
|
|
||||||
def test_ensure_boot_snapshot_skips_existing():
|
def test_ensure_boot_snapshot_skips_existing():
|
||||||
@@ -66,10 +69,15 @@ def test_ensure_boot_snapshot_creates(monkeypatch):
|
|||||||
assert got.status == "available"
|
assert got.status == "available"
|
||||||
|
|
||||||
|
|
||||||
def test_mcp_snippet(capsys):
|
def test_mcp_snippet(monkeypatch):
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"gpu_rent.access_card.load_state",
|
||||||
|
lambda: type("S", (), {"notes": {}})(),
|
||||||
|
)
|
||||||
logs = []
|
logs = []
|
||||||
print_mcp_snippet(_Cfg(), logs.append)
|
print_mcp_snippet(_Cfg(), logs.append)
|
||||||
text = "\n".join(logs)
|
text = "\n".join(logs)
|
||||||
assert "17801/mcp" in text
|
assert "17801/mcp" in text
|
||||||
assert "gpu-rent tunnel" in text
|
|
||||||
assert "mcp.json" in text
|
assert "mcp.json" in text
|
||||||
|
assert "SwarmUI UI" in text
|
||||||
|
assert "gpu-rent stop" in text
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ def _mock_bind(monkeypatch):
|
|||||||
lambda conn, boot_volume_id, cfg, log: None,
|
lambda conn, boot_volume_id, cfg, log: None,
|
||||||
)
|
)
|
||||||
monkeypatch.setattr("gpu_rent.session.notify_ready", lambda cfg, log: None)
|
monkeypatch.setattr("gpu_rent.session.notify_ready", lambda cfg, log: None)
|
||||||
monkeypatch.setattr("gpu_rent.session.print_mcp_snippet", lambda cfg, log: None)
|
|
||||||
|
|
||||||
|
|
||||||
def test_cmd_up_refuses_zero_gpu_quota(monkeypatch):
|
def test_cmd_up_refuses_zero_gpu_quota(monkeypatch):
|
||||||
|
|||||||
Reference in New Issue
Block a user