Skip to content

Commit 907d4ab

Browse files
authored
Huge revamp (#19)
* netlab-ui rounds 3–6: traffic, faults, captures, exercises, providers and showcase - Multi-node shell, Ctrl+P context actions and smart run-on-nodes - Traffic lens: live load, down links, errors/drops, tiles and sparklines - Fault injection: link down/up and netem presets - Packet capture with pcap downloads and live streaming - Search by address, prefix, AS, VLAN, VRF or module - Running-config snapshots, drift detection and diffs - Lab time limits and per-user quotas; fork, publish and duplicate labs into workspaces - Guided exercises with tasks, validation checks and saved progress - libvirt and external providers; expanded netlab tools, setup, reports and custom configs - Generated showcase gallery, feature previews and captioned tour video - Animated loading intro and README logo outro with community credits - Background music with seamless looping, automatic fades and attribution - Keep video outputs and recording metadata local; commit gallery images and renderer sources - Remove repository gallery links to ignored video files - Move assistant MCP server/client to MCP 2 - Preserve netlab's short group form when saving * Add CI jobs for hygiene checks and secrets scanning; update CONTRIBUTING.md for local commit checks * Refactor README and GALLERY.md for improved layout; enhance error handling in router and config snapshot services; add tests for symlink validation * Update badge colors in README for consistency and improved visibility
1 parent b524c56 commit 907d4ab

294 files changed

Lines changed: 18818 additions & 9576 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,55 @@ concurrency:
1212
cancel-in-progress: true
1313

1414
jobs:
15+
hygiene:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
with:
20+
fetch-depth: 0
21+
- uses: actions/setup-python@v5
22+
with:
23+
python-version: "3.12"
24+
- name: Install pre-commit
25+
run: pip install pre-commit==4.3.0
26+
- name: Check changed files
27+
env:
28+
SKIP: gitleaks
29+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
30+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
31+
run: |
32+
set -euo pipefail
33+
if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
34+
base=$(git merge-base "$BASE_SHA" "$HEAD_SHA")
35+
pre-commit run --from-ref "$base" --to-ref "$HEAD_SHA" --show-diff-on-failure
36+
else
37+
pre-commit run --all-files --show-diff-on-failure
38+
fi
39+
40+
secrets:
41+
runs-on: ubuntu-latest
42+
steps:
43+
- uses: actions/checkout@v4
44+
with:
45+
fetch-depth: 0
46+
- uses: actions/setup-go@v5
47+
with:
48+
go-version: '1.25'
49+
cache: false
50+
- name: Install Gitleaks
51+
run: go install github.com/zricethezav/gitleaks/v8@v8.30.1
52+
- name: Scan every PR commit for secrets
53+
env:
54+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
55+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
56+
run: |
57+
set -euo pipefail
58+
if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
59+
gitleaks git --redact --no-banner --log-opts="$BASE_SHA..$HEAD_SHA"
60+
else
61+
gitleaks git --redact --no-banner
62+
fi
63+
1564
# Decide which halves of the repo the PR actually touches, so a
1665
# frontend-only PR doesn't pay for a backend run (and vice versa).
1766
changes:
@@ -34,9 +83,11 @@ jobs:
3483
3584
if [ "${{ github.event_name }}" != "pull_request" ]; then
3685
# Manual runs have no diff to inspect — exercise everything.
37-
echo "backend=true" >> "$GITHUB_OUTPUT"
38-
echo "frontend=true" >> "$GITHUB_OUTPUT"
39-
echo "docker=true" >> "$GITHUB_OUTPUT"
86+
{
87+
echo "backend=true"
88+
echo "frontend=true"
89+
echo "docker=true"
90+
} >> "$GITHUB_OUTPUT"
4091
exit 0
4192
fi
4293
@@ -83,7 +134,7 @@ jobs:
83134
cache-dependency-path: backend/pyproject.toml
84135

85136
- name: Install dependencies
86-
run: pip install -e ".[dev,netlab,assistant]"
137+
run: pip install -e ".[dev,netlab]"
87138

88139
- name: Lint (ruff check)
89140
run: ruff check
@@ -127,6 +178,10 @@ jobs:
127178
needs: changes
128179
if: needs.changes.outputs.docker == 'true'
129180
runs-on: ubuntu-latest
181+
strategy:
182+
matrix:
183+
# Both published images: the UI-only default and the bundled toolchain.
184+
target: [ui, full]
130185
steps:
131186
- uses: actions/checkout@v4
132187

@@ -135,11 +190,12 @@ jobs:
135190
- uses: docker/build-push-action@v6
136191
with:
137192
context: .
193+
target: ${{ matrix.target }}
138194
push: false
139195
# Single arch on PRs: this is a "does it build" gate, and emulating
140196
# arm64 here would roughly double the run for no extra signal.
141197
platforms: linux/amd64
142-
# Shares the cache the release build populates, so tagging later is
143-
# mostly cache hits.
144-
cache-from: type=gha
145-
cache-to: type=gha,mode=max
198+
# Shares the per-target cache the release build uses (scope ui/full),
199+
# so tagging later is mostly cache hits.
200+
cache-from: type=gha,scope=${{ matrix.target }}
201+
cache-to: type=gha,mode=max,scope=${{ matrix.target }}

‎.github/workflows/container-release.yml‎

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,54 @@ jobs:
4040
type=semver,pattern={{major}}.{{minor}}
4141
type=semver,pattern={{major}}
4242
43+
# Same tags with a -full suffix (latest → latest-full) for the image that
44+
# bundles netlab, Ansible and containerlab (Dockerfile target `full`).
45+
- id: metadata-full
46+
uses: docker/metadata-action@v5
47+
with:
48+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
49+
flavor: |
50+
suffix=-full,onlatest=true
51+
tags: |
52+
type=semver,pattern={{version}}
53+
type=semver,pattern={{major}}.{{minor}}
54+
type=semver,pattern={{major}}
55+
4356
- uses: docker/setup-qemu-action@v3
4457
- uses: docker/setup-buildx-action@v3
4558

4659
- uses: docker/build-push-action@v6
4760
with:
4861
context: .
62+
target: ui
4963
# arm64 matters for Apple Silicon and arm-based lab hosts. Only the
5064
# Python stage is emulated — the frontend stage builds natively.
5165
platforms: linux/amd64,linux/arm64
66+
build-args: |
67+
NETLAB_GUI_VERSION=${{ github.ref_name }}
5268
push: true
5369
tags: ${{ steps.metadata.outputs.tags }}
5470
labels: ${{ steps.metadata.outputs.labels }}
5571
provenance: mode=max
5672
sbom: true
57-
cache-from: type=gha
58-
cache-to: type=gha,mode=max
73+
# One cache scope per target, or the second build overwrites the first's.
74+
cache-from: type=gha,scope=ui
75+
cache-to: type=gha,mode=max,scope=ui
76+
77+
- uses: docker/build-push-action@v6
78+
with:
79+
context: .
80+
target: full
81+
platforms: linux/amd64,linux/arm64
82+
build-args: |
83+
NETLAB_GUI_VERSION=${{ github.ref_name }}
84+
push: true
85+
tags: ${{ steps.metadata-full.outputs.tags }}
86+
labels: ${{ steps.metadata-full.outputs.labels }}
87+
provenance: mode=max
88+
sbom: true
89+
cache-from: type=gha,scope=full
90+
cache-to: type=gha,mode=max,scope=full
5991

6092
release:
6193
needs: publish
@@ -65,6 +97,11 @@ jobs:
6597
with:
6698
fetch-depth: 0
6799

100+
# Image tags come from docker/metadata-action's semver patterns, which
101+
# drop the leading "v" (tag v1.2.3 → image 1.2.3).
102+
- id: version
103+
run: echo "tag=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
104+
68105
# The GitHub Release is the landing page people reach from the tag, so
69106
# it leads with the pull command rather than only a source zip.
70107
- uses: softprops/action-gh-release@v2
@@ -76,27 +113,44 @@ jobs:
76113
body: |
77114
## Run it
78115
116+
Everything included (netlab, Ansible, containerlab) — the host only
117+
needs Docker. Download `docker-compose.yml` from this release, then:
118+
79119
```bash
120+
docker compose up -d # → http://localhost:8000
121+
```
122+
123+
or the equivalent `docker run`:
124+
125+
```bash
126+
mkdir -p "$PWD/labs"
80127
docker run -d --name netlab-ui \
81-
-p 8000:8000 \
128+
--privileged --network host --pid host \
82129
-v /var/run/docker.sock:/var/run/docker.sock \
83-
-v "$PWD/labs:/work" \
84-
ghcr.io/${{ github.repository }}:${{ github.ref_name }}
130+
-v /var/run/netns:/var/run/netns \
131+
-v "$PWD/labs:$PWD/labs" -e NETLAB_WORKSPACE="$PWD/labs" \
132+
-v "$HOME/.netlab:/root/.netlab" \
133+
-e UVICORN_HOST=127.0.0.1 \
134+
ghcr.io/muddyblack/netlab-ui:${{ steps.version.outputs.tag }}-full
85135
```
86136
87-
Then open <http://localhost:8000>.
137+
The labs directory must be mounted at the **same path** as on the
138+
host (containerlab bind-mounts node files through the host Docker
139+
daemon), and `~/.netlab` keeps netlab's running-lab registry shared
140+
with the host. The UI checks all of this itself: see
141+
**Settings → Environment → Container Setup**.
88142
89-
Images: `linux/amd64`, `linux/arm64`. Also tagged `latest`.
143+
### UI-only image
90144
91-
### Scope
145+
`ghcr.io/muddyblack/netlab-ui:${{ steps.version.outputs.tag }}` (no `-full`)
146+
leaves netlab, Ansible and containerlab on your host. Use the same
147+
flags, plus mount your netlab install at its own path and point the
148+
UI at it:
92149
93-
This image is the **UI only**. `netlab`, Ansible and containerlab
94-
stay on your host — run it on a machine that already has netlab
95-
set up, and point Settings → Environment at that install (or set
96-
`NETLAB_BIN`).
150+
```bash
151+
-v /opt/netlab-venv:/opt/netlab-venv:ro \
152+
-e NETLAB_BIN=/opt/netlab-venv/bin/netlab \
153+
-v /usr/bin/containerlab:/usr/bin/containerlab:ro
154+
```
97155
98-
Features that call containerlab directly — link impairment,
99-
orphaned-instance cleanup, post-start link reconcile — need the
100-
binary visible inside the container. It is a static Go binary, so
101-
add `-v /usr/bin/containerlab:/usr/bin/containerlab:ro` to enable
102-
them. Everything else works without it.
156+
Images: `linux/amd64`, `linux/arm64`. Also tagged `latest` / `latest-full`.

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
frontend/.npmrc
22
.npmrc
3+
notes.md
34

45
.vscode/
56

‎.pre-commit-config.yaml‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
minimum_pre_commit_version: '3.2.0'
2+
repos:
3+
- repo: https://github.com/pre-commit/pre-commit-hooks
4+
rev: v6.0.0
5+
hooks:
6+
- id: trailing-whitespace
7+
args: [--markdown-linebreak-ext=md]
8+
# Preserve third-party patch context and generated SVG artwork.
9+
exclude: '(^frontend/patches/|\.svg$)'
10+
- id: check-merge-conflict
11+
args: [--assume-in-merge]
12+
- id: check-yaml
13+
args: [--allow-multiple-documents]
14+
- id: check-json
15+
- id: detect-private-key
16+
- repo: https://github.com/gitleaks/gitleaks
17+
rev: v8.30.1
18+
hooks:
19+
- id: gitleaks

‎AGENTS.md‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,16 @@ The netlab YAML stays coordinate-free. Positions live in `<topology>.netlab-ui.j
120120
- **Running-lab matching is by topology file, then the file's own directory** (`host/runningMatch.ts`). A directory *prefix* or bare name match marks unrelated labs as running. netlab allows one lab per directory — new labs are created as `<workspace>/<name>/topology.yml`.
121121
- **`host.createSession` does not make a session active.** Explorer actions on a lab that isn't the open tab get their own background session (`getOrCreateSession` in `useTabManager`); only `activateSession` changes what canvas callbacks target. Resolving an action through "the current session" ran Deploy/Destroy on the wrong lab before.
122122
- **The YAML serializer merges into the loaded document** (`serialize.to_yaml` + `Topology.source`) so comments and flow style survive UI edits. Rebuilding the document from the model dropped users' comments.
123+
- **`netlab exec` and `netlab connect --show` always exit 0.** `app/lab/broadcast.py` gets a real status for shell commands on containers from an exit-code trailer, and flags CLI error replies (`% Unknown command`) as failures. netlab joins the words after the node name and hands them to `bash -c`, so pass commands as whitespace-split words: `shlex` would strip the quotes, and an extra `sh -c` gets its `$?` expanded by the outer shell.
124+
- **clab-ui keeps only the traffic-rate keys of interface stats** (`EDGE_STATS_KEYS`). Anything else the runtime sample carries (error/drop counters, netem state) reaches our own UI through `host/runtimeStore.ts`, which the controller's runtime poll publishes to. Don't add a second poll.
125+
- **Dialogs and overlays opened from the canvas or explorer use small module stores** (`host/captureStore.ts`, `configsDialogStore.ts`, `copyLabStore.ts`, `canvasSpotlight.ts`), mounted once in `components/dialogs/LabToolDialogs.tsx` / `components/app/CanvasLabOverlays.tsx`. This avoids threading props through `App.tsx`; follow the same pattern for new ones.
126+
- **Explorer action ids choose their clab-ui menu group by substring** (`ACTION_GROUP_RULES`: `.graph.`, `copy`, `inspect`, `shell`, `addtoworkspace`, …). An id matching nothing lands in "Other".
127+
- **netlab's transformed topology stores `validate:` as a list** of tests with a `name` key, not the YAML's mapping. The same goes for other normalized blocks: read both forms.
128+
- **netlab's short group form (`core: [r1, r2]`) is a member list**, and `Group.short_form` keeps it that way on save.
129+
- **Providers are per node** (`node.provider`, else the lab's; netlab reports unmanaged devices as `unmanaged`). A lab with any non-clab node gets its canvas from `services/netlab/projection.py` (source `"transform"`), because `clab.yml` lists containers only. Gate runtime actions on the node's provider in the backend (`lifecycle._require_clab_node` / `_provider_limit`) with a message the user can act on, and keep `frontend/src/netlabProviders.ts` (the support table) and the README table in step.
130+
- **libvirt NICs map to netlab interfaces by order** (`services/netlab/libvirt.py`): `virsh domiflist` lists the management NIC first, then one NIC per non-virtual, non-loopback interface. p2p links are UDP tunnels (no host tap, target `-`); only LAN links have a `vnetN` tap. Tap counters are the host's view, so rx/tx are swapped. No KVM in CI: test with the fake `virsh` in `tests/test_libvirt.py`.
131+
- **netlab's Ansible playbooks find the lab through `$PWD`**, not the process cwd (`lookup('env','PWD')` in collect-configs, initial-config: node_files). `runner._child_env(cwd)` sets `PWD`. Any new way of spawning netlab must do the same, or configs land in the backend's own directory.
132+
- **Things netlab only knows from the deployed snapshot run in netlab's own Python** (`services/netlab/_tools_bridge.py`, the catalog script in `services/netlab/setup.py`, run with `location.target_python()`), never imported into the backend. `netlab test` exits 0 even when the test failed, and waits for RETURN before cleaning up: `setup.stream` answers the prompt, checks the output for failure, and tears the test lab down in a shielded task.
123133

124134
## Key rules
125135

@@ -129,12 +139,16 @@ Never put netlab text, branding, or logic into clab-ui — it's a third-party de
129139

130140
### Installing / upgrading clab-ui
131141

132-
`@containerlab/clab-ui` is published on the public npm registry (source: the [srl-labs/containerlab-app](https://github.com/srl-labs/containerlab-app) monorepo), pinned in `frontend/package.json`. No auth is needed to install. To upgrade, bump the version, port the sequenced patches in `frontend/patches/` (`@containerlab+clab-ui+<version>+NNN+<name>.patch`: 001 initial fixes, 002 lifecycle-modal lab name + actions, 003 node-editor `hostFields`), and run `npm install` — there's no build step on our side. Keep each patch generic (no netlab text) and small; add new ones with `npx patch-package @containerlab/clab-ui --append <name>`. After changing a patch, restart Vite with `--force` (its dependency pre-bundle cache keeps the old code).
142+
`@containerlab/clab-ui` is published on the public npm registry (source: the [srl-labs/containerlab-app](https://github.com/srl-labs/containerlab-app) monorepo), pinned in `frontend/package.json`. No auth is needed to install. To upgrade, bump the version, port the sequenced patches in `frontend/patches/` (`@containerlab+clab-ui+<version>+NNN+<name>.patch`: 001 initial fixes, 002 lifecycle-modal lab name + actions, 003 node-editor `hostFields`, 004 link down/up menu item), and run `npm install` — there's no build step on our side. Keep each patch generic (no netlab text) and small; add new ones with `npx patch-package @containerlab/clab-ui --append <name>`. After changing a patch, restart Vite with `--force` (its dependency pre-bundle cache keeps the old code).
133143

134144
### Public API only
135145

136146
Import from `@containerlab/clab-ui` (the package root or its declared sub-entries like `@containerlab/clab-ui/host`). Never use `@containerlab/clab-ui/src/...` or `@containerlab/clab-ui/dist/...` deep paths.
137147

148+
### Screenshots and videos are generated
149+
150+
`docs/showcase/` drives the real app with Playwright and produces the README gallery, per-feature stills (light and dark), clips and one shareable video (`docs/showcase/run.sh`). A new user-facing feature gets a file in `docs/showcase/features/` (see its README); after UI changes, rerun `run.sh` (or `--only <feature>`) rather than hand-editing images. Commit the gallery screenshots and animated WebP previews. MP4s, intro/outro renders, recording manifests, caption timings, `.work/` and `.cache/` stay local and are ignored. Publish videos with their manifest and timings as release assets or external artifacts; do not add repository links to ignored videos.
151+
138152
### API types are generated
139153

140154
`frontend/src/api/generated.ts` is auto-generated from the backend's OpenAPI schema. Never hand-edit it. After changing a Pydantic response model, run `npm run gen:api` (backend must be running) then `npm run typecheck`.

0 commit comments

Comments
 (0)