csp-lab 0.1.0__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.
- csp_lab-0.1.0/.gitignore +12 -0
- csp_lab-0.1.0/LICENSE +21 -0
- csp_lab-0.1.0/PKG-INFO +84 -0
- csp_lab-0.1.0/README.md +72 -0
- csp_lab-0.1.0/THIRD_PARTY_NOTICES.md +14 -0
- csp_lab-0.1.0/docs/versioning.md +28 -0
- csp_lab-0.1.0/pyproject.toml +47 -0
- csp_lab-0.1.0/src/csp_lab/__init__.py +6 -0
- csp_lab-0.1.0/src/csp_lab/cli.py +191 -0
- csp_lab-0.1.0/src/csp_lab/data/compose.build.yaml +11 -0
- csp_lab-0.1.0/src/csp_lab/data/compose.yaml +16 -0
- csp_lab-0.1.0/src/csp_lab/docker.py +270 -0
- csp_lab-0.1.0/src/csp_lab/mcp_server.py +42 -0
- csp_lab-0.1.0/src/csp_lab/models.py +50 -0
csp_lab-0.1.0/.gitignore
ADDED
csp_lab-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lackim
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
csp_lab-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: csp-lab
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A local CubeSat Space Protocol lab powered by libcsp
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: mcp<2,>=1.20
|
|
9
|
+
Requires-Dist: pydantic<3,>=2.10
|
|
10
|
+
Requires-Dist: typer<1,>=0.15
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# csp-lab
|
|
14
|
+
|
|
15
|
+
A local CubeSat Space Protocol lab powered by [libcsp](https://github.com/libcsp/libcsp). It runs two simulated nodes and a ZMQ hub in Docker. The Python CLI and MCP server provide the same diagnostic operations.
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- Python 3.10 or newer and [uv](https://docs.astral.sh/uv/)
|
|
20
|
+
- Docker with Compose
|
|
21
|
+
|
|
22
|
+
The native code is built against the pinned `libcsp` v2.1 submodule. That library supports CSP protocol v1 and v2 at runtime. The Docker image uses Linux because recent libcsp releases do not provide maintained native macOS support. See [versioning policy](docs/versioning.md) for the supported combinations.
|
|
23
|
+
|
|
24
|
+
## Local development
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
git submodule update --init --recursive
|
|
28
|
+
uv sync --locked
|
|
29
|
+
uv run --locked csp-lab up --protocol 2 --build
|
|
30
|
+
uv run --locked csp-lab ping 2 --json
|
|
31
|
+
uv run --locked csp-lab doctor --json
|
|
32
|
+
uv run --locked csp-lab down
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`--build` compiles the native image from this checkout, so it needs the libcsp submodule. Without `--build`, the CLI uses the image matching its own version from GHCR. That image becomes available after the first release.
|
|
36
|
+
|
|
37
|
+
Once released, the package can run without a clone or local compilation:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
uvx csp-lab up --protocol 2
|
|
41
|
+
uvx csp-lab ping 2
|
|
42
|
+
uvx csp-lab down
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
You can also install it with `uv tool install csp-lab` and then use `csp-lab` directly. Lab state is stored in `~/.local/state/csp-lab` or under `XDG_STATE_HOME` when set, so commands work from any directory.
|
|
46
|
+
|
|
47
|
+
If a lab was started by the earlier checkout-based CLI, run `csp-lab down` from that checkout once before switching to `uvx` from another directory. The CLI recognizes and removes the earlier path-based Docker project there.
|
|
48
|
+
|
|
49
|
+
To test CSP v1, start a fresh lab with `up --protocol 1`. `up` refuses to change the protocol of an existing lab; run `down` first. The two versions never share a running network.
|
|
50
|
+
|
|
51
|
+
The CLI commands are `up`, `down`, `status`, `topology`, `ping <address>`, and `doctor`. `status` checks whether the hub and both nodes are running in Docker. Use `--json` for machine-readable output. `ping` and `doctor` return a nonzero exit code when a node does not respond. Addresses must be 1–31 for CSP v1 or 1–16383 for CSP v2. The demo nodes use addresses 2 and 3; the probe uses address 4.
|
|
52
|
+
|
|
53
|
+
## MCP
|
|
54
|
+
|
|
55
|
+
Start the lab first, then configure an MCP client to launch `csp-lab mcp` over stdio. After the package is published, the entry is:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"mcpServers": {
|
|
60
|
+
"csp-lab": {
|
|
61
|
+
"command": "uvx",
|
|
62
|
+
"args": ["csp-lab", "mcp"]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
With `uv tool install`, set the command to `csp-lab` and the argument to `mcp`. GUI clients may not see the shell's `PATH`; use the full path to `uvx` or the installed `csp-lab` executable when needed. For a local clone after `uv sync`, use `/absolute/path/to/csp-lab/.venv/bin/csp-lab` as the command. Keep the lab running in a separate terminal while using the MCP tools.
|
|
69
|
+
|
|
70
|
+
The stdio server provides `csp_topology`, `csp_ping`, and `csp_diagnose`. It does not expose commands that reboot, shut down, or read or write node memory. The simulated server binds only the CSP ping service port.
|
|
71
|
+
|
|
72
|
+
## What is tested
|
|
73
|
+
|
|
74
|
+
`uv run --locked pytest` checks protocol validation, unavailable Docker, cleanup after a failed start, and unresponsive nodes without requiring Docker. Run `uv run --locked ruff check src/csp_lab tests` and `uv run --locked mypy src/csp_lab` for lint and types. The Docker integration flow exercises real libcsp, ZMQ, CLI diagnostics, and MCP for both protocol versions:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
bash test/integration.sh
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The GitHub Actions workflow is configured to run both checks on Ubuntu with Docker Compose. The local integration script stops the lab on exit, including after a failure.
|
|
81
|
+
|
|
82
|
+
## Scope and license
|
|
83
|
+
|
|
84
|
+
This lab uses the pinned libcsp release as a build dependency and selects CSP v1 or v2 as a runtime protocol. It does not build against historical libcsp 1.x releases or operate mixed-version networks. ZMQ is the only transport in this lab. The project is licensed under [MIT](LICENSE); the libcsp submodule retains its own license. Image component notices are listed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
csp_lab-0.1.0/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# csp-lab
|
|
2
|
+
|
|
3
|
+
A local CubeSat Space Protocol lab powered by [libcsp](https://github.com/libcsp/libcsp). It runs two simulated nodes and a ZMQ hub in Docker. The Python CLI and MCP server provide the same diagnostic operations.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Python 3.10 or newer and [uv](https://docs.astral.sh/uv/)
|
|
8
|
+
- Docker with Compose
|
|
9
|
+
|
|
10
|
+
The native code is built against the pinned `libcsp` v2.1 submodule. That library supports CSP protocol v1 and v2 at runtime. The Docker image uses Linux because recent libcsp releases do not provide maintained native macOS support. See [versioning policy](docs/versioning.md) for the supported combinations.
|
|
11
|
+
|
|
12
|
+
## Local development
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
git submodule update --init --recursive
|
|
16
|
+
uv sync --locked
|
|
17
|
+
uv run --locked csp-lab up --protocol 2 --build
|
|
18
|
+
uv run --locked csp-lab ping 2 --json
|
|
19
|
+
uv run --locked csp-lab doctor --json
|
|
20
|
+
uv run --locked csp-lab down
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`--build` compiles the native image from this checkout, so it needs the libcsp submodule. Without `--build`, the CLI uses the image matching its own version from GHCR. That image becomes available after the first release.
|
|
24
|
+
|
|
25
|
+
Once released, the package can run without a clone or local compilation:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
uvx csp-lab up --protocol 2
|
|
29
|
+
uvx csp-lab ping 2
|
|
30
|
+
uvx csp-lab down
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
You can also install it with `uv tool install csp-lab` and then use `csp-lab` directly. Lab state is stored in `~/.local/state/csp-lab` or under `XDG_STATE_HOME` when set, so commands work from any directory.
|
|
34
|
+
|
|
35
|
+
If a lab was started by the earlier checkout-based CLI, run `csp-lab down` from that checkout once before switching to `uvx` from another directory. The CLI recognizes and removes the earlier path-based Docker project there.
|
|
36
|
+
|
|
37
|
+
To test CSP v1, start a fresh lab with `up --protocol 1`. `up` refuses to change the protocol of an existing lab; run `down` first. The two versions never share a running network.
|
|
38
|
+
|
|
39
|
+
The CLI commands are `up`, `down`, `status`, `topology`, `ping <address>`, and `doctor`. `status` checks whether the hub and both nodes are running in Docker. Use `--json` for machine-readable output. `ping` and `doctor` return a nonzero exit code when a node does not respond. Addresses must be 1–31 for CSP v1 or 1–16383 for CSP v2. The demo nodes use addresses 2 and 3; the probe uses address 4.
|
|
40
|
+
|
|
41
|
+
## MCP
|
|
42
|
+
|
|
43
|
+
Start the lab first, then configure an MCP client to launch `csp-lab mcp` over stdio. After the package is published, the entry is:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"csp-lab": {
|
|
49
|
+
"command": "uvx",
|
|
50
|
+
"args": ["csp-lab", "mcp"]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
With `uv tool install`, set the command to `csp-lab` and the argument to `mcp`. GUI clients may not see the shell's `PATH`; use the full path to `uvx` or the installed `csp-lab` executable when needed. For a local clone after `uv sync`, use `/absolute/path/to/csp-lab/.venv/bin/csp-lab` as the command. Keep the lab running in a separate terminal while using the MCP tools.
|
|
57
|
+
|
|
58
|
+
The stdio server provides `csp_topology`, `csp_ping`, and `csp_diagnose`. It does not expose commands that reboot, shut down, or read or write node memory. The simulated server binds only the CSP ping service port.
|
|
59
|
+
|
|
60
|
+
## What is tested
|
|
61
|
+
|
|
62
|
+
`uv run --locked pytest` checks protocol validation, unavailable Docker, cleanup after a failed start, and unresponsive nodes without requiring Docker. Run `uv run --locked ruff check src/csp_lab tests` and `uv run --locked mypy src/csp_lab` for lint and types. The Docker integration flow exercises real libcsp, ZMQ, CLI diagnostics, and MCP for both protocol versions:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
bash test/integration.sh
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The GitHub Actions workflow is configured to run both checks on Ubuntu with Docker Compose. The local integration script stops the lab on exit, including after a failure.
|
|
69
|
+
|
|
70
|
+
## Scope and license
|
|
71
|
+
|
|
72
|
+
This lab uses the pinned libcsp release as a build dependency and selects CSP v1 or v2 as a runtime protocol. It does not build against historical libcsp 1.x releases or operate mixed-version networks. ZMQ is the only transport in this lab. The project is licensed under [MIT](LICENSE); the libcsp submodule retains its own license. Image component notices are listed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Third party notices
|
|
2
|
+
|
|
3
|
+
The container image includes these components. Their original notices and license
|
|
4
|
+
terms remain in the image.
|
|
5
|
+
|
|
6
|
+
| Component | Source | License and notice location in the image |
|
|
7
|
+
| --- | --- | --- |
|
|
8
|
+
| libcsp v2.1 | [libcsp](https://github.com/libcsp/libcsp/tree/v2.1) | MIT; `/usr/share/licenses/libcsp/LICENSE` and `AUTHORS` |
|
|
9
|
+
| ZeroMQ (`libzmq5`, Debian bookworm package) | [Debian package](https://packages.debian.org/bookworm/libzmq5) | LGPL-3.0+ with a special exception and package-specific terms; `/usr/share/doc/libzmq5/copyright` |
|
|
10
|
+
| Debian base image and runtime packages | [Debian](https://www.debian.org/) | Individual package notices under `/usr/share/doc/` |
|
|
11
|
+
|
|
12
|
+
The csp-lab source and its own MIT license are at
|
|
13
|
+
`/usr/share/licenses/csp-lab/LICENSE` in the image. The installed package versions
|
|
14
|
+
can be inspected with `dpkg-query -W` inside a container.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Versioning policy
|
|
2
|
+
|
|
3
|
+
Three version numbers matter here and they have different meanings:
|
|
4
|
+
|
|
5
|
+
| Layer | Current support | How it is selected |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| libcsp library and API | v2.1, pinned as a Git submodule commit | Build time |
|
|
8
|
+
| CSP wire protocol | v1 or v2 | `csp-lab up --protocol 1` or `--protocol 2` |
|
|
9
|
+
| csp-lab CLI and MCP API | 0.1.x prototype | Project release |
|
|
10
|
+
|
|
11
|
+
The current image always builds against libcsp v2.1. Selecting `--protocol 1` changes the wire format used by that library; it does **not** build libcsp 1.x. The two simulated nodes and the probe always use the same protocol setting. A running lab must be stopped before switching protocols.
|
|
12
|
+
|
|
13
|
+
## Updating libcsp 2.x
|
|
14
|
+
|
|
15
|
+
Update the pinned submodule in a dedicated change. Check the C API and ZMQ build, then run `uv run --locked pytest` and `bash test/integration.sh` for both wire protocols. Review the generated Docker image and record the new libcsp commit in the change. Keep the CLI and MCP output compatible within a csp-lab release unless a breaking change is intentional.
|
|
16
|
+
|
|
17
|
+
## Historical libcsp 1.x
|
|
18
|
+
|
|
19
|
+
libcsp 1.x has materially different build and API surfaces. If support becomes useful, add a separate legacy native build and adapter, with its own pinned revision and CI job. Do not switch native library generations implicitly based on `--protocol`; that option is reserved for the wire protocol. A mixed v1/v2 network is outside this lab's scope.
|
|
20
|
+
|
|
21
|
+
## Support matrix
|
|
22
|
+
|
|
23
|
+
| Native library | Wire protocol | Status |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| libcsp v2.1 | CSP v1 | Tested locally; covered by the CI workflow |
|
|
26
|
+
| libcsp v2.1 | CSP v2 | Tested locally; covered by the CI workflow |
|
|
27
|
+
| libcsp 1.x | CSP v1 | Unsupported |
|
|
28
|
+
| Any library | Mixed CSP v1/v2 in one lab | Unsupported |
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "csp-lab"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A local CubeSat Space Protocol lab powered by libcsp"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"mcp>=1.20,<2",
|
|
14
|
+
"pydantic>=2.10,<3",
|
|
15
|
+
"typer>=0.15,<1",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
csp-lab = "csp_lab.cli:run_cli"
|
|
20
|
+
|
|
21
|
+
[dependency-groups]
|
|
22
|
+
dev = [
|
|
23
|
+
"mypy>=1.15,<2",
|
|
24
|
+
"pytest>=8,<10",
|
|
25
|
+
"ruff>=0.11,<1",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["src/csp_lab"]
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.sdist]
|
|
32
|
+
only-include = ["src/csp_lab", "THIRD_PARTY_NOTICES.md", "docs/versioning.md"]
|
|
33
|
+
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
testpaths = ["tests"]
|
|
36
|
+
|
|
37
|
+
[tool.ruff]
|
|
38
|
+
target-version = "py310"
|
|
39
|
+
line-length = 100
|
|
40
|
+
|
|
41
|
+
[tool.ruff.lint]
|
|
42
|
+
select = ["E", "F", "I", "S602", "S605"]
|
|
43
|
+
|
|
44
|
+
[tool.mypy]
|
|
45
|
+
python_version = "3.10"
|
|
46
|
+
strict = true
|
|
47
|
+
files = ["src/csp_lab"]
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Public models and lab operations for the local CSP lab."""
|
|
2
|
+
|
|
3
|
+
from csp_lab.docker import diagnose, down, ping, status, topology, up
|
|
4
|
+
from csp_lab.models import LabState, PingResult
|
|
5
|
+
|
|
6
|
+
__all__ = ["LabState", "PingResult", "diagnose", "down", "ping", "status", "topology", "up"]
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Command-line interface for the local CSP lab."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from importlib.metadata import version
|
|
7
|
+
from typing import TypeVar
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from csp_lab import docker
|
|
13
|
+
from csp_lab.models import PingResult
|
|
14
|
+
|
|
15
|
+
ResultT = TypeVar("ResultT")
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(
|
|
18
|
+
name="csp-lab",
|
|
19
|
+
help="Local CubeSat Space Protocol lab",
|
|
20
|
+
add_completion=False,
|
|
21
|
+
no_args_is_help=True,
|
|
22
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _show_version(value: bool) -> bool:
|
|
27
|
+
if value:
|
|
28
|
+
print(version("csp-lab"))
|
|
29
|
+
raise typer.Exit()
|
|
30
|
+
return value
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@app.callback()
|
|
34
|
+
def main(
|
|
35
|
+
ctx: typer.Context,
|
|
36
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
37
|
+
show_version: bool = typer.Option(
|
|
38
|
+
False, "-V", "--version", callback=_show_version, is_eager=True
|
|
39
|
+
),
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Global output options."""
|
|
42
|
+
ctx.obj = json_output
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _json(ctx: typer.Context, local: bool) -> bool:
|
|
46
|
+
return bool(ctx.obj) or local
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _emit(value: BaseModel | dict[str, object], json_output: bool) -> None:
|
|
50
|
+
data = value.model_dump(by_alias=True) if isinstance(value, BaseModel) else value
|
|
51
|
+
print(
|
|
52
|
+
json.dumps(
|
|
53
|
+
data,
|
|
54
|
+
indent=None if json_output else 2,
|
|
55
|
+
separators=(",", ":") if json_output else None,
|
|
56
|
+
ensure_ascii=False,
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _action(operation: Callable[[], ResultT], json_output: bool) -> ResultT:
|
|
62
|
+
try:
|
|
63
|
+
return operation()
|
|
64
|
+
except Exception as exc:
|
|
65
|
+
message = str(exc)
|
|
66
|
+
if json_output:
|
|
67
|
+
print(json.dumps({"error": message}, separators=(",", ":"), ensure_ascii=False))
|
|
68
|
+
else:
|
|
69
|
+
print(f"csp-lab: {message}", file=sys.stderr)
|
|
70
|
+
raise typer.Exit(code=1) from exc
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.command()
|
|
74
|
+
def up(
|
|
75
|
+
ctx: typer.Context,
|
|
76
|
+
protocol: str = typer.Option("2", "--protocol", help="CSP protocol version (1 or 2)"),
|
|
77
|
+
build: bool = typer.Option(False, "--build", help="Build the native image from this checkout"),
|
|
78
|
+
json_output: bool = typer.Option(False, "--json", help="Print JSON output"),
|
|
79
|
+
show_version: bool = typer.Option(
|
|
80
|
+
False, "-V", "--version", callback=_show_version, is_eager=True, hidden=True
|
|
81
|
+
),
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Start two libcsp nodes and a ZMQ hub."""
|
|
84
|
+
json_output = _json(ctx, json_output)
|
|
85
|
+
state = _action(lambda: docker.up(docker.parse_protocol(protocol), build), json_output)
|
|
86
|
+
data: dict[str, object] = {"started": True}
|
|
87
|
+
data.update(state.as_json())
|
|
88
|
+
_emit(data, json_output)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@app.command()
|
|
92
|
+
def down(
|
|
93
|
+
ctx: typer.Context,
|
|
94
|
+
json_output: bool = typer.Option(False, "--json", help="Print JSON output"),
|
|
95
|
+
show_version: bool = typer.Option(
|
|
96
|
+
False, "-V", "--version", callback=_show_version, is_eager=True, hidden=True
|
|
97
|
+
),
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Stop the lab."""
|
|
100
|
+
json_output = _json(ctx, json_output)
|
|
101
|
+
_action(docker.down, json_output)
|
|
102
|
+
_emit({"stopped": True}, json_output)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@app.command()
|
|
106
|
+
def ping(
|
|
107
|
+
ctx: typer.Context,
|
|
108
|
+
address: str = typer.Argument(..., help="CSP address to ping"),
|
|
109
|
+
json_output: bool = typer.Option(False, "--json", help="Print JSON output"),
|
|
110
|
+
show_version: bool = typer.Option(
|
|
111
|
+
False, "-V", "--version", callback=_show_version, is_eager=True, hidden=True
|
|
112
|
+
),
|
|
113
|
+
) -> None:
|
|
114
|
+
"""Ping a node using libcsp."""
|
|
115
|
+
json_output = _json(ctx, json_output)
|
|
116
|
+
|
|
117
|
+
def probe() -> PingResult:
|
|
118
|
+
state = docker.require_state()
|
|
119
|
+
return docker.ping(docker.parse_address(address, state.protocol))
|
|
120
|
+
|
|
121
|
+
result = _action(probe, json_output)
|
|
122
|
+
_emit(result, json_output)
|
|
123
|
+
if not result.reachable:
|
|
124
|
+
raise typer.Exit(code=1)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@app.command()
|
|
128
|
+
def topology(
|
|
129
|
+
ctx: typer.Context,
|
|
130
|
+
json_output: bool = typer.Option(False, "--json", help="Print JSON output"),
|
|
131
|
+
show_version: bool = typer.Option(
|
|
132
|
+
False, "-V", "--version", callback=_show_version, is_eager=True, hidden=True
|
|
133
|
+
),
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Show the configured network."""
|
|
136
|
+
json_output = _json(ctx, json_output)
|
|
137
|
+
_emit(_action(docker.topology, json_output), json_output)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.command()
|
|
141
|
+
def doctor(
|
|
142
|
+
ctx: typer.Context,
|
|
143
|
+
json_output: bool = typer.Option(False, "--json", help="Print JSON output"),
|
|
144
|
+
show_version: bool = typer.Option(
|
|
145
|
+
False, "-V", "--version", callback=_show_version, is_eager=True, hidden=True
|
|
146
|
+
),
|
|
147
|
+
) -> None:
|
|
148
|
+
"""Test both nodes."""
|
|
149
|
+
json_output = _json(ctx, json_output)
|
|
150
|
+
result = _action(docker.diagnose, json_output)
|
|
151
|
+
_emit(result, json_output)
|
|
152
|
+
if not result.healthy:
|
|
153
|
+
raise typer.Exit(code=1)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@app.command()
|
|
157
|
+
def status(
|
|
158
|
+
ctx: typer.Context,
|
|
159
|
+
json_output: bool = typer.Option(False, "--json", help="Print JSON output"),
|
|
160
|
+
show_version: bool = typer.Option(
|
|
161
|
+
False, "-V", "--version", callback=_show_version, is_eager=True, hidden=True
|
|
162
|
+
),
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Show local lab state."""
|
|
165
|
+
json_output = _json(ctx, json_output)
|
|
166
|
+
_emit(_action(docker.status, json_output), json_output)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@app.command()
|
|
170
|
+
def mcp() -> None:
|
|
171
|
+
"""Serve read-only CSP diagnostics over MCP stdio."""
|
|
172
|
+
from csp_lab.mcp_server import server
|
|
173
|
+
|
|
174
|
+
server.run(transport="stdio")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def run_cli() -> None:
|
|
178
|
+
"""Map legacy help syntax and parser exit codes to the original CLI."""
|
|
179
|
+
args = sys.argv[1:]
|
|
180
|
+
if args and args[0] == "help":
|
|
181
|
+
args = [*args[1:], "--help"]
|
|
182
|
+
try:
|
|
183
|
+
app(args=args)
|
|
184
|
+
except SystemExit as exc:
|
|
185
|
+
if exc.code == 2:
|
|
186
|
+
raise SystemExit(1) from exc
|
|
187
|
+
raise
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
run_cli()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
services:
|
|
2
|
+
hub:
|
|
3
|
+
image: ${CSP_LAB_IMAGE:?}
|
|
4
|
+
entrypoint: ["/usr/local/bin/zmqproxy"]
|
|
5
|
+
command: ["-v", "${CSP_VERSION:-2}"]
|
|
6
|
+
node2:
|
|
7
|
+
image: ${CSP_LAB_IMAGE:?}
|
|
8
|
+
depends_on: [hub]
|
|
9
|
+
command: ["server", "${CSP_VERSION:-2}", "2", "hub"]
|
|
10
|
+
node3:
|
|
11
|
+
image: ${CSP_LAB_IMAGE:?}
|
|
12
|
+
depends_on: [hub]
|
|
13
|
+
command: ["server", "${CSP_VERSION:-2}", "3", "hub"]
|
|
14
|
+
probe:
|
|
15
|
+
image: ${CSP_LAB_IMAGE:?}
|
|
16
|
+
profiles: ["tools"]
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Docker Compose orchestration and CSP probe operations."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
from contextlib import ExitStack
|
|
10
|
+
from importlib.metadata import version
|
|
11
|
+
from importlib.resources import as_file, files
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from pydantic import ValidationError
|
|
15
|
+
|
|
16
|
+
from csp_lab.models import (
|
|
17
|
+
DiagnoseResult,
|
|
18
|
+
LabState,
|
|
19
|
+
PingResult,
|
|
20
|
+
Protocol,
|
|
21
|
+
StatusResult,
|
|
22
|
+
TopologyResult,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
26
|
+
STATE_HOME = Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local" / "state"))
|
|
27
|
+
STATE_DIR = STATE_HOME / "csp-lab"
|
|
28
|
+
STATE_FILE = STATE_DIR / "state.json"
|
|
29
|
+
PROJECT = "csp-lab"
|
|
30
|
+
IMAGE = "ghcr.io/lackim/csp-lab"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def selected_image(build: bool) -> str:
|
|
34
|
+
return "csp-lab-native:local" if build else f"{IMAGE}:v{version('csp-lab')}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def state_image(state: LabState) -> str:
|
|
38
|
+
return state.image or selected_image(state.build)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def state_project(state: LabState) -> str:
|
|
42
|
+
return state.project or PROJECT
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def parse_protocol(value: str) -> Protocol:
|
|
46
|
+
if value == "1":
|
|
47
|
+
return 1
|
|
48
|
+
if value == "2":
|
|
49
|
+
return 2
|
|
50
|
+
raise ValueError("Protocol must be 1 or 2.")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def parse_address(value: str, protocol: Protocol) -> int:
|
|
54
|
+
if re.fullmatch(r"[0-9]+", value) is None:
|
|
55
|
+
raise ValueError("Address must be a decimal integer.")
|
|
56
|
+
address = int(value)
|
|
57
|
+
maximum = 31 if protocol == 1 else 16383
|
|
58
|
+
if address < 1 or address > maximum:
|
|
59
|
+
raise ValueError(f"CSP v{protocol} address must be between 1 and {maximum}.")
|
|
60
|
+
return address
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _read_state(path: Path) -> LabState | None:
|
|
64
|
+
try:
|
|
65
|
+
content = path.read_text(encoding="utf-8")
|
|
66
|
+
except FileNotFoundError:
|
|
67
|
+
return None
|
|
68
|
+
try:
|
|
69
|
+
return LabState.model_validate_json(content)
|
|
70
|
+
except ValidationError as exc:
|
|
71
|
+
raise ValueError("Unsupported lab state. Run 'csp-lab down' and start again.") from exc
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_state() -> LabState | None:
|
|
75
|
+
state = _read_state(STATE_FILE)
|
|
76
|
+
if state is not None:
|
|
77
|
+
return state
|
|
78
|
+
for root in dict.fromkeys((ROOT, Path.cwd().resolve())):
|
|
79
|
+
legacy = _read_state(root / ".csp-lab" / "state.json")
|
|
80
|
+
if legacy is not None:
|
|
81
|
+
project = "csp-lab-" + hashlib.sha256(str(root).encode()).hexdigest()[:8]
|
|
82
|
+
return legacy.model_copy(
|
|
83
|
+
update={
|
|
84
|
+
"build": True,
|
|
85
|
+
"image": "csp-lab-native:local",
|
|
86
|
+
"project": project,
|
|
87
|
+
"legacy_root": root,
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def require_state() -> LabState:
|
|
94
|
+
state = get_state()
|
|
95
|
+
if state is None:
|
|
96
|
+
raise RuntimeError("Lab is not started. Run 'csp-lab up --protocol 2' first.")
|
|
97
|
+
return state
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def run(
|
|
101
|
+
command: str, args: list[str], env: dict[str, str] | None = None, timeout: int = 120
|
|
102
|
+
) -> str:
|
|
103
|
+
try:
|
|
104
|
+
result = subprocess.run(
|
|
105
|
+
[command, *args],
|
|
106
|
+
env={**os.environ, **(env or {})},
|
|
107
|
+
capture_output=True,
|
|
108
|
+
text=True,
|
|
109
|
+
timeout=timeout,
|
|
110
|
+
check=False,
|
|
111
|
+
)
|
|
112
|
+
except FileNotFoundError as exc:
|
|
113
|
+
raise RuntimeError(f"{command} ENOENT: {exc}") from exc
|
|
114
|
+
if result.returncode != 0:
|
|
115
|
+
detail = (result.stderr or result.stdout).strip()
|
|
116
|
+
raise RuntimeError(f"{command} exited {result.returncode}: {detail}")
|
|
117
|
+
return result.stdout
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def compose(
|
|
121
|
+
args: list[str],
|
|
122
|
+
protocol: Protocol,
|
|
123
|
+
image: str | None = None,
|
|
124
|
+
timeout: int = 120,
|
|
125
|
+
build_overlay: bool = False,
|
|
126
|
+
project: str = PROJECT,
|
|
127
|
+
) -> str:
|
|
128
|
+
env = {"CSP_VERSION": str(protocol), "CSP_LAB_IMAGE": image or selected_image(False)}
|
|
129
|
+
with ExitStack() as stack:
|
|
130
|
+
compose_file = stack.enter_context(as_file(files("csp_lab") / "data" / "compose.yaml"))
|
|
131
|
+
file_args = ["-f", str(compose_file)]
|
|
132
|
+
if build_overlay:
|
|
133
|
+
if not (ROOT / "Dockerfile").is_file() or not (ROOT / "vendor" / "libcsp").is_dir():
|
|
134
|
+
raise RuntimeError(
|
|
135
|
+
"Local build requires a source checkout with the libcsp submodule."
|
|
136
|
+
)
|
|
137
|
+
build_file = stack.enter_context(
|
|
138
|
+
as_file(files("csp_lab") / "data" / "compose.build.yaml")
|
|
139
|
+
)
|
|
140
|
+
file_args.extend(["-f", str(build_file)])
|
|
141
|
+
env["CSP_LAB_BUILD_CONTEXT"] = str(ROOT)
|
|
142
|
+
return run("docker", ["compose", "-p", project, *file_args, *args], env, timeout)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def up(protocol: Protocol, build: bool = False) -> LabState:
|
|
146
|
+
if get_state() is not None:
|
|
147
|
+
raise RuntimeError("Lab is already started. Run 'csp-lab down' before changing protocol.")
|
|
148
|
+
if build and (
|
|
149
|
+
not (ROOT / "Dockerfile").is_file() or not (ROOT / "vendor" / "libcsp").is_dir()
|
|
150
|
+
):
|
|
151
|
+
raise RuntimeError("Local build requires a source checkout with the libcsp submodule.")
|
|
152
|
+
image = selected_image(build)
|
|
153
|
+
try:
|
|
154
|
+
pull = ["--build"] if build else ["--pull", "always"]
|
|
155
|
+
compose(
|
|
156
|
+
["up", "-d", *pull, "hub", "node2", "node3"],
|
|
157
|
+
protocol,
|
|
158
|
+
image,
|
|
159
|
+
600,
|
|
160
|
+
build_overlay=build,
|
|
161
|
+
)
|
|
162
|
+
for target in (2, 3):
|
|
163
|
+
if not ping_with_protocol(target, protocol, image).reachable:
|
|
164
|
+
raise RuntimeError(f"Node {target} did not answer a CSP v{protocol} ping.")
|
|
165
|
+
state = LabState(
|
|
166
|
+
protocol=protocol,
|
|
167
|
+
libcspVersion="2.1",
|
|
168
|
+
build=build,
|
|
169
|
+
image=image,
|
|
170
|
+
project=PROJECT,
|
|
171
|
+
)
|
|
172
|
+
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
173
|
+
STATE_FILE.write_text(
|
|
174
|
+
json.dumps(
|
|
175
|
+
{**state.as_json(), "build": build, "image": image, "project": PROJECT},
|
|
176
|
+
indent=2,
|
|
177
|
+
)
|
|
178
|
+
+ "\n",
|
|
179
|
+
encoding="utf-8",
|
|
180
|
+
)
|
|
181
|
+
return state
|
|
182
|
+
except Exception:
|
|
183
|
+
try:
|
|
184
|
+
compose(["down", "--remove-orphans"], protocol, image)
|
|
185
|
+
except Exception:
|
|
186
|
+
pass
|
|
187
|
+
shutil.rmtree(STATE_DIR, ignore_errors=True)
|
|
188
|
+
raise
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def down() -> None:
|
|
192
|
+
state = get_state()
|
|
193
|
+
compose(
|
|
194
|
+
["down", "--remove-orphans"],
|
|
195
|
+
state.protocol if state else 2,
|
|
196
|
+
state_image(state) if state else None,
|
|
197
|
+
project=state_project(state) if state else PROJECT,
|
|
198
|
+
)
|
|
199
|
+
state_dir = state.legacy_root / ".csp-lab" if state and state.legacy_root else STATE_DIR
|
|
200
|
+
shutil.rmtree(state_dir, ignore_errors=True)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def ping_with_protocol(
|
|
204
|
+
target: int, protocol: Protocol, image: str | None = None, project: str = PROJECT
|
|
205
|
+
) -> PingResult:
|
|
206
|
+
parse_address(str(target), protocol)
|
|
207
|
+
output = compose(
|
|
208
|
+
["run", "--rm", "--no-deps", "probe", "probe", str(protocol), "4", str(target)],
|
|
209
|
+
protocol,
|
|
210
|
+
image,
|
|
211
|
+
30,
|
|
212
|
+
project=project,
|
|
213
|
+
)
|
|
214
|
+
line = next(
|
|
215
|
+
(part for part in reversed(output.splitlines()) if part.startswith('{"target":')),
|
|
216
|
+
None,
|
|
217
|
+
)
|
|
218
|
+
if line is None:
|
|
219
|
+
raise ValueError(f"Probe returned no JSON result: {output.strip()}")
|
|
220
|
+
try:
|
|
221
|
+
result = PingResult.model_validate_json(line)
|
|
222
|
+
except ValidationError as exc:
|
|
223
|
+
raise ValueError("Probe returned an invalid result.") from exc
|
|
224
|
+
if result.target != target:
|
|
225
|
+
raise ValueError("Probe returned an invalid result.")
|
|
226
|
+
return result
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def ping(target: int) -> PingResult:
|
|
230
|
+
state = require_state()
|
|
231
|
+
return ping_with_protocol(target, state.protocol, state_image(state), state_project(state))
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def topology() -> TopologyResult:
|
|
235
|
+
state = require_state()
|
|
236
|
+
return TopologyResult(
|
|
237
|
+
protocol=state.protocol,
|
|
238
|
+
libcspVersion=state.libcsp_version,
|
|
239
|
+
nodes=[2, 3],
|
|
240
|
+
probeAddress=4,
|
|
241
|
+
transport="ZMQ",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def diagnose() -> DiagnoseResult:
|
|
246
|
+
state = require_state()
|
|
247
|
+
nodes = [
|
|
248
|
+
ping_with_protocol(target, state.protocol, state_image(state), state_project(state))
|
|
249
|
+
for target in (2, 3)
|
|
250
|
+
]
|
|
251
|
+
return DiagnoseResult(
|
|
252
|
+
protocol=state.protocol,
|
|
253
|
+
libcspVersion=state.libcsp_version,
|
|
254
|
+
healthy=all(node.reachable for node in nodes),
|
|
255
|
+
nodes=nodes,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def status() -> StatusResult:
|
|
260
|
+
state = get_state()
|
|
261
|
+
if state is None:
|
|
262
|
+
return StatusResult(running=False, state=None)
|
|
263
|
+
services = compose(
|
|
264
|
+
["ps", "--services", "--status", "running"],
|
|
265
|
+
state.protocol,
|
|
266
|
+
state_image(state),
|
|
267
|
+
project=state_project(state),
|
|
268
|
+
).splitlines()
|
|
269
|
+
running = all(name in services for name in ("hub", "node2", "node3"))
|
|
270
|
+
return StatusResult(running=running, state=state)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Read-only MCP tools for inspecting a running CSP lab."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
from mcp.server.fastmcp import FastMCP
|
|
7
|
+
from mcp.types import ToolAnnotations
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
|
|
10
|
+
from csp_lab import docker
|
|
11
|
+
|
|
12
|
+
server = FastMCP("csp-lab")
|
|
13
|
+
READ_ONLY = ToolAnnotations(readOnlyHint=True, openWorldHint=False)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@server.tool(
|
|
17
|
+
name="csp_topology",
|
|
18
|
+
description="Read the configured CSP protocol version and simulated nodes",
|
|
19
|
+
annotations=READ_ONLY,
|
|
20
|
+
)
|
|
21
|
+
def csp_topology() -> str:
|
|
22
|
+
return json.dumps(docker.topology().as_json())
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@server.tool(
|
|
26
|
+
name="csp_ping",
|
|
27
|
+
description="Ping a simulated node through libcsp",
|
|
28
|
+
annotations=READ_ONLY,
|
|
29
|
+
)
|
|
30
|
+
def csp_ping(address: Annotated[int, Field(strict=True, gt=0)]) -> str:
|
|
31
|
+
state = docker.require_state()
|
|
32
|
+
target = docker.parse_address(str(address), state.protocol)
|
|
33
|
+
return json.dumps(docker.ping(target).as_json())
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@server.tool(
|
|
37
|
+
name="csp_diagnose",
|
|
38
|
+
description="Ping both nodes and summarize lab health",
|
|
39
|
+
annotations=READ_ONLY,
|
|
40
|
+
)
|
|
41
|
+
def csp_diagnose() -> str:
|
|
42
|
+
return json.dumps(docker.diagnose().as_json())
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Validated data returned by the CLI and MCP server."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field, StrictBool
|
|
7
|
+
|
|
8
|
+
Protocol = Literal[1, 2]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LabModel(BaseModel):
|
|
12
|
+
model_config = ConfigDict(extra="forbid", strict=True, populate_by_name=True)
|
|
13
|
+
|
|
14
|
+
def as_json(self) -> dict[str, object]:
|
|
15
|
+
return self.model_dump(by_alias=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LabState(LabModel):
|
|
19
|
+
protocol: Protocol
|
|
20
|
+
libcsp_version: Literal["2.1"] = Field(alias="libcspVersion")
|
|
21
|
+
build: bool = Field(default=False, exclude=True)
|
|
22
|
+
image: str | None = Field(default=None, exclude=True)
|
|
23
|
+
project: str | None = Field(default=None, exclude=True)
|
|
24
|
+
legacy_root: Path | None = Field(default=None, exclude=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PingResult(LabModel):
|
|
28
|
+
target: int
|
|
29
|
+
reachable: StrictBool
|
|
30
|
+
rtt_ms: int = Field(alias="rttMs")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class StatusResult(LabModel):
|
|
34
|
+
running: bool
|
|
35
|
+
state: LabState | None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TopologyResult(LabModel):
|
|
39
|
+
protocol: Protocol
|
|
40
|
+
libcsp_version: Literal["2.1"] = Field(alias="libcspVersion")
|
|
41
|
+
nodes: list[int]
|
|
42
|
+
probe_address: int = Field(alias="probeAddress")
|
|
43
|
+
transport: Literal["ZMQ"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DiagnoseResult(LabModel):
|
|
47
|
+
protocol: Protocol
|
|
48
|
+
libcsp_version: Literal["2.1"] = Field(alias="libcspVersion")
|
|
49
|
+
healthy: bool
|
|
50
|
+
nodes: list[PingResult]
|