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
|
# 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
|
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)
|
app = typer.Typer(no_args_is_help=True)
|
||||||
|
|
||||||
|
|
||||||
def _todo() -> None:
|
def _exit_from_subprocess_error(exc: subprocess.CalledProcessError) -> None:
|
||||||
raise typer.Exit(code=1)
|
if exc.stderr:
|
||||||
|
typer.echo(exc.stderr, err=True)
|
||||||
|
raise typer.Exit(code=exc.returncode)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def install() -> None:
|
def install() -> None:
|
||||||
_todo()
|
try:
|
||||||
|
SteamInstaller().install_or_update(passthrough=True)
|
||||||
|
except subprocess.CalledProcessError as exc:
|
||||||
|
_exit_from_subprocess_error(exc)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def initialize(name: str, spec: str = typer.Option(..., "--spec", "-f")) -> None:
|
def initialize(name: str, spec: Path = typer.Option(..., "-f")) -> None:
|
||||||
del name
|
try:
|
||||||
del spec
|
initialize_instance(name, spec, passthrough=True)
|
||||||
_todo()
|
except subprocess.CalledProcessError as exc:
|
||||||
|
_exit_from_subprocess_error(exc)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def start(name: str) -> None:
|
def start(name: str) -> None:
|
||||||
del name
|
try:
|
||||||
_todo()
|
start_instance(name, passthrough=True)
|
||||||
|
except subprocess.CalledProcessError as exc:
|
||||||
|
_exit_from_subprocess_error(exc)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def stop(name: str) -> None:
|
def stop(name: str) -> None:
|
||||||
del name
|
try:
|
||||||
_todo()
|
stop_instance(name, passthrough=True)
|
||||||
|
except subprocess.CalledProcessError as exc:
|
||||||
|
_exit_from_subprocess_error(exc)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def delete(name: str) -> None:
|
def delete(name: str) -> None:
|
||||||
del name
|
try:
|
||||||
_todo()
|
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 typer.testing import CliRunner
|
||||||
|
|
||||||
from l4d2host.cli import app
|
from l4d2host.cli import app
|
||||||
|
|
@ -8,3 +10,17 @@ def test_help_lists_v1_commands() -> None:
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
for command in ["install", "initialize", "start", "stop", "delete"]:
|
for command in ["install", "initialize", "start", "stop", "delete"]:
|
||||||
assert command in result.output
|
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