Skip to content

Discovery

One workflow, one step, one function block. The interesting part is the parameter contract and where the function block actually lives.

The workflow

scenarios/_base/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:

scenarios/_base/workflows/simple-lab-discovery.workflow.yaml
# 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 publishes each *.yaml, *.yml and *.json it finds in /workflows — the selected scenario’s resolved workflows/ directory, bind-mounted there — through POST /workflow-definition/publish as {"workflow": <parsed document>}. JSON is a subset of YAML, so one parser reads both and the engine is handed the same structure either way; anything else in that directory (a README, say) is skipped rather than posted. It is idempotent: 201 means the version was written, 200 means this version already held exactly this document. Published content is immutable, so a 409 is a real failure — it means the version exists with different content, and the fix is to bump majorVersion/minorVersion/patchVersion in the document rather than edit in place. A 422 means the engine computed a higher version floor than the document declares. Either exits non-zero, and docker compose wait lab_bootstrap propagates that so make local-lab-up fails rather than continuing into a broken discovery. Against an engine from before the publish route the script falls back to the legacy POST /workflow-definition on a 404.

The function block

fb.base.neops.io/global_discover_network:0.1.0

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:

failed_safe_ack — Function block with id fb.base.neops.io/global_discover_network:0.1.0 not found

The worker is told where to look with a two-entry search path:

DIR_FUNCTION_BLOCKS: lab/generated/${SCENARIO}/scenario/function_blocks,neops/fb

Both are relative to the worker image’s WORKDIR, /app:

  • lab/generated/<scenario>/scenario/function_blocks → the selected scenario’s resolved function blocks, reached through the whole-repo bind mount at /app/lab. See The /app/lab mount.
  • neops/fb → the base function blocks baked into the image.

scenarios/_base/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 — or in a single scenario’s own function_blocks/, which overrides the base file by file — and the worker picks it up on restart. No image rebuild, because the directory is mounted; but make scenario-resolve has to run first, since the worker reads the resolved copy rather than the source. See Scenarios.

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

They live in scenarios/<scenario>/workflow-execution-parameters/; the counts below are for wan-and-fabric.

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

frr-only carries the three generated files only: mixing per-subnet platforms and credentials has nothing to express when every device runs the same NOS.

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.

  • Scenarios — how the workflow, the parameters and the function blocks are selected per lab.
  • The /app/lab mount — why DIR_FUNCTION_BLOCKS has a lab/ 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.