docs(l4d2): finalize v1 CLI contracts and web-facing read APIs
This commit is contained in:
parent
a6c4a6c50f
commit
466abe66ee
3 changed files with 77 additions and 13 deletions
|
|
@ -1 +1,31 @@
|
|||
# l4d2-host-lib
|
||||
|
||||
Python host library and CLI for managing L4D2 instances.
|
||||
|
||||
## CLI
|
||||
|
||||
`l4d2ctl` exposes exactly these commands in v1:
|
||||
|
||||
- `install`
|
||||
- `initialize <name> -f <spec.yaml>`
|
||||
- `start <name>`
|
||||
- `stop <name>`
|
||||
- `delete <name>`
|
||||
|
||||
Subprocess failures are fail-fast. Raw stderr is written to stderr and the command exits with the same subprocess return code.
|
||||
|
||||
## Runtime Paths
|
||||
|
||||
The host library uses hard-coded runtime paths under `/opt/l4d2`:
|
||||
|
||||
- `/opt/l4d2/installation`
|
||||
- `/opt/l4d2/overlays/<overlay>`
|
||||
- `/opt/l4d2/instances/<name>`
|
||||
- `/opt/l4d2/runtime/<name>/{upper,work,merged}`
|
||||
|
||||
## Web App Read APIs
|
||||
|
||||
These read APIs are provided for web app integration:
|
||||
|
||||
- `get_instance_status(name)`
|
||||
- `stream_instance_logs(name, lines=200, follow=True)`
|
||||
|
|
|
|||
|
|
@ -1,38 +1,56 @@
|
|||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
import typer
|
||||
|
||||
from l4d2host.instances import delete_instance, initialize_instance, start_instance, stop_instance
|
||||
from l4d2host.steam_install import SteamInstaller
|
||||
|
||||
|
||||
app = typer.Typer(no_args_is_help=True)
|
||||
|
||||
|
||||
def _todo() -> None:
|
||||
raise typer.Exit(code=1)
|
||||
def _exit_from_subprocess_error(exc: subprocess.CalledProcessError) -> None:
|
||||
if exc.stderr:
|
||||
typer.echo(exc.stderr, err=True)
|
||||
raise typer.Exit(code=exc.returncode)
|
||||
|
||||
|
||||
@app.command()
|
||||
def install() -> None:
|
||||
_todo()
|
||||
try:
|
||||
SteamInstaller().install_or_update(passthrough=True)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
_exit_from_subprocess_error(exc)
|
||||
|
||||
|
||||
@app.command()
|
||||
def initialize(name: str, spec: str = typer.Option(..., "--spec", "-f")) -> None:
|
||||
del name
|
||||
del spec
|
||||
_todo()
|
||||
def initialize(name: str, spec: Path = typer.Option(..., "-f")) -> None:
|
||||
try:
|
||||
initialize_instance(name, spec, passthrough=True)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
_exit_from_subprocess_error(exc)
|
||||
|
||||
|
||||
@app.command()
|
||||
def start(name: str) -> None:
|
||||
del name
|
||||
_todo()
|
||||
try:
|
||||
start_instance(name, passthrough=True)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
_exit_from_subprocess_error(exc)
|
||||
|
||||
|
||||
@app.command()
|
||||
def stop(name: str) -> None:
|
||||
del name
|
||||
_todo()
|
||||
try:
|
||||
stop_instance(name, passthrough=True)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
_exit_from_subprocess_error(exc)
|
||||
|
||||
|
||||
@app.command()
|
||||
def delete(name: str) -> None:
|
||||
del name
|
||||
_todo()
|
||||
try:
|
||||
delete_instance(name, passthrough=True)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
_exit_from_subprocess_error(exc)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import subprocess
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from l4d2host.cli import app
|
||||
|
|
@ -8,3 +10,17 @@ def test_help_lists_v1_commands() -> None:
|
|||
assert result.exit_code == 0
|
||||
for command in ["install", "initialize", "start", "stop", "delete"]:
|
||||
assert command in result.output
|
||||
|
||||
|
||||
def test_cli_propagates_subprocess_return_code(monkeypatch) -> None:
|
||||
def fail(*args, **kwargs):
|
||||
del args
|
||||
del kwargs
|
||||
raise subprocess.CalledProcessError(returncode=9, cmd=["x"], stderr="boom")
|
||||
|
||||
monkeypatch.setattr("l4d2host.cli.start_instance", fail)
|
||||
|
||||
result = CliRunner().invoke(app, ["start", "alpha"])
|
||||
|
||||
assert result.exit_code == 9
|
||||
assert "boom" in result.stderr
|
||||
|
|
|
|||
Loading…
Reference in a new issue