Discovery
One workflow, one step, one function block. The interesting part is the parameter contract and where the function block actually lives.
The workflow
workflows/simple-lab-discovery.workflow.yaml is registered with the engine by the lab_bootstrap container on every make local-lab-up. It is a single-step workflow whose entire job is to pass parameters through to a function block:
# Lab discovery: SSH into the containerlab devices and register them as Device
# entities in the CMS.
label: simple_lab_discovery
name: simple_lab_discovery
package: wf.lab.neops.io
majorVersion: 1
minorVersion: 2
patchVersion: 0
seedEntity: global
type: workflow
description: "Discover the lab devices and register them in the CMS."
parameterSchema:
type: object
properties:
subnets:
type: array
description: "CIDRs to expand; a single host is a /32. A subnet can scope credentials and a platform, and the most specific prefix wins for overlaps."
default: []
items:
type: object
properties:
cidr: { type: string }
platform: { type: string }
credentials:
type: array
items:
type: object
properties:
username: { type: string }
password: { type: string }
platform: { type: string }
required: [username, password]
required: [cidr]
credentials:
type: array
description: "Logins tried in order against hosts whose subnet carries none. An entry may be scoped to a platform."
items:
type: object
properties:
username: { type: string }
password: { type: string }
platform: { type: string }
required: [username, password]
default: []
steps:
- type: functionBlock
label: discover
functionBlock: "fb.base.neops.io/global_discover_network:0.1.0"
runOn: global
parameters:
subnets: "{{ parameters.subnets }}"
credentials: "{{ parameters.credentials }}"
Its identifier is wf.lab.neops.io/simple_lab_discovery:1.2.0 — package wf.lab.neops.io, name simple_lab_discovery, and the version assembled from majorVersion/minorVersion/patchVersion. That string is what make local-lab-discover passes to run_workflow, so bumping a version field in the YAML means bumping it in the Makefile too.
seedEntity: global and runOn: global mean the step is not scoped to an entity — it runs once, globally, which is why run_workflow is called with no --ids.
How registration works
bootstrap/register.py waits for GET /health on the engine (60s budget, then tries anyway), then POSTs each workflows/*.yaml to /workflow-definition as {"workflow": <parsed yaml>}. It is idempotent: a 409, or any response whose body mentions “already exists”, counts as success. A real failure exits non-zero, and docker compose wait lab_bootstrap propagates that so make local-lab-up fails rather than continuing into a broken discovery.
The function block
The function block is not in this repo
global_discover_network lives in neops-worker-sdk-py and ships
inside the published quay.io/zebbra/neops-worker-sdk image, under
/app/neops/fb. This repo only names it — in the workflow YAML and in
the Makefile’s DISCOVER_FB variable. Renaming it upstream breaks both,
with no compile-time check anywhere.
Symptom when the pinned image does not carry it:
The worker is told where to look with a two-entry search path:
Both are relative to the worker image’s WORKDIR, /app:
lab/function_blocks→ this repo’sfunction_blocks/, bind-mounted at/app/lab. See The/app/labmount.neops/fb→ the base function blocks baked into the image.
function_blocks/ currently holds only an empty __init__.py: it is the extension point for lab-local blocks (package fb.lab.neops.io), wired up and discovered but not yet used. Drop a block in there and the worker picks it up on restart — no image rebuild, because the directory is mounted.
Parameters
The workflow’s parameterSchema declares two top-level arrays.
subnets
CIDRs to expand; a single host is a /32. Each entry may carry:
cidr
: Required. The prefix to expand.
platform
: Optional. The device platform (frr, srl). Omitting it makes discovery detect the platform, at the cost of an extra SSH probe per host.
credentials
: Optional. Logins scoped to this subnet, tried before the global list.
For overlapping ranges the most specific prefix wins (longest prefix match) — that is what makes discover-params-mixed.json meaningful: a /24 with FRR credentials, plus /32 overrides carrying the SR Linux login for the five switches.
credentials
The global fallback list, tried in order against hosts whose subnet carries none. Each entry is {username, password} plus an optional platform:
- an entry scoped to a platform is tried first for hosts of that platform, and skipped for hosts declared as another one;
- an unscoped entry is tried for anything.
So credential resolution is: subnet credentials → global credentials, and within a list, platform-matched → unscoped.
The four parameter files
| File | Shape | Generated? |
|---|---|---|
discover-params.json |
15 /32s with platform, platform-scoped credentials |
yes |
discover-params-autodetect.json |
the same /32s, no platform |
yes |
discover-params-subnet.json |
one /24, subnet-scoped credentials |
yes |
discover-params-mixed.json |
/24 + /32 overrides for the SR Linux nodes |
no — hand-maintained |
See Your first discovery for the contents of each and the DISCOVER_PARAMS override that selects them.
What discovery writes
The function block connects to every host through the connection plugin its platform selects (frr → FRRNetmikoPlugin, srl → SRLinuxNetmikoPlugin), reads facts and interfaces, then writes both Device and Interface rows to the CMS in one pass.
- Devices are keyed by IP. Re-running discovery skips devices that already exist.
- Interfaces are always recorded. Re-running against the same CMS therefore duplicates interface rows.
Discovery emits a few hundred Interface rows in a single job result, which is why the lab wants a workflow-engine image carrying the large-payload and reference-resolution fixes. If you see the run fail on result size, pin a newer engine with NEOPS_WORKFLOW_ENGINE_IMAGE.
Related
- The
/app/labmount — whyDIR_FUNCTION_BLOCKShas alab/prefix. - Troubleshooting — the three race conditions between registration, worker readiness and device boot.
- NeOps ecosystem — the contracts this repo depends on and cannot verify locally.
