AGENTS.md · git:20261024.74ac86b · 2026-10-24 · sha256 915762416bf3a754

AGENTS.md git:20261024.74ac86bA

Immutable. This exact content never changes and is served at /api/v1/blob/915762416bf3a754.

# Agent instructions for SNMP::Info

SNMP::Info is a Perl library that presents network devices behind one
method API over SNMP. Netdisco is its main consumer; other applications use
it directly, so keep code, comments and PR text library-neutral.

Before writing or changing a device class, read
`.claude/skills/device-class-authoring/SKILL.md`. Before reviewing a branch
or pull request, including your own before you open it, read
`.claude/skills/device-class-review/SKILL.md`.

## How the library works

The developer reference is the `EXTENDING SNMP::INFO` section of
`lib/SNMP/Info.pm`, with a sample subclass. In short:

- **Methods are generated, not written.** `AUTOLOAD` in `SNMP/Info.pm`
  makes a method from any leaf a loaded MIB defines: a GET for a scalar,
  a walk returning a hashref keyed by instance id (iid) for a table
  column. Four package hashes name and shape these methods:
  - `%GLOBALS`: `method => 'scalarLeaf'`, read with GET.
  - `%FUNCS`: `method => 'tableColumn'`, walked.
  - `%MIBS`: `'MIB-NAME' => 'probeLeaf'`, MIBs that must load.
  - `%MUNGE`: `method => \&formatter`, post-processing.
- **List what the class reads.** By convention each leaf a class reads
  has a `%GLOBALS` or `%FUNCS` entry, so the top of the file shows its
  sources. A sub may call a leaf by its MIB name instead
  (`jnxContentsDescr` in `Layer3/Juniper.pm`), or to bypass an override
  of the friendly method (`e_index` in `Entity.pm`). Comment which MIB
  the leaf comes from.
- **Leaf names come from MIBs.** A `%FUNCS` or `%GLOBALS` value may also be
  a numeric OID. Since 3.981002, table columns walk correctly whether or
  not a loaded MIB defines them; earlier releases return undef for a
  column no loaded MIB defines. Prefix a leaf with its MIB to resolve name
  collisions (`ALTEON_TIGON_SWITCH_MIB__agSoftwareVersion`).
- **Subclasses merge their parents' hashes, and the last key wins:**

  ```perl
  %MIBS    = (%SNMP::Info::Layer3::MIBS, 'VENDOR-MIB' => 'probeLeaf');
  %GLOBALS = (%SNMP::Info::Layer3::GLOBALS, ...);
  %FUNCS   = (%SNMP::Info::Layer3::FUNCS, %SNMP::Info::LLDP::FUNCS, ...);
  %MUNGE   = (%SNMP::Info::Layer3::MUNGE, ...);
  ```

  - A key two parents define takes the later parent's value. Put the
    class's own entries last so they override both.
  - Subs resolve the other way: the first `@ISA` parent that defines one
    wins. List parents in the hashes in reverse `@ISA` order, so a
    collision resolves to the same parent either way (`Layer3/Cisco.pm`).
    A mismatch passes every test until two parents share a key.
  - A sub anywhere in the inheritance beats a hash entry, even the class's
    own: `can()` looks for real subs first. Override a parent's sub with a
    sub.
  - `SUPER::x` follows `@ISA` only to real subs. For a generated method it
    reads the object's own merged hash, so a class that remaps `x` gets
    its own leaf back, not the parent's.
  - A sub that wraps its own hash entry calls `orig_x`, which skips every
    sub named `x` (`i_name` in `Layer3.pm`). To read a specific leaf
    whatever the hashes say, call it by MIB name; the comment in
    `Layer3/Enterasys.pm` `interfaces` weighs all three.

- **Inheritance is a mixin web.** Feature roles (`Bridge`, `LLDP`, `CDP`,
  `Entity`, `PowerEthernet`, `IEEE802dot11`, ...) mix into the `Layer1` to
  `Layer7` bases; device classes live in `lib/SNMP/Info/Layer{1,2,3,7}/`.
- **Detection.** `device_type()` in `SNMP/Info.pm` picks the class from
  `sysServices` (layers), `sysDescr` and `sysObjectID` enterprise maps.
- **Method prefixes** keep areas apart: `i_*` interfaces, `c_*` topology,
  `bp_*`/`fw_*` bridge and forwarding, `e_*` entity, `peth_*` PoE,
  `qb_*` Q-BRIDGE. Use the prefix of the area you touch.

## Environment

- Build with Module::Build: `perl Build.PL && ./Build`.
- Needs the Net-SNMP Perl module (`SNMP`) and a copy of
  [netdisco-mibs](https://github.com/netdisco/netdisco-mibs) at
  `~/netdisco-mibs`. The test session loads every MIB directory there and
  ignores `MIBDIRS`.
- CI uses the latest netdisco-mibs **release**, not its master branch.
  `init()` croaks when a `%MIBS` entry does not load, so a class naming a
  MIB that is not yet released fails CI. Until the release ships, keep the
  class PR as a draft, or read the objects by numeric OID with a comment
  naming the pending netdisco-mibs PR. Adding a MIB follows that
  repository's own `AGENTS.md`.
- Style: `.perltidyrc` (78 columns, 4-space indent, no tabs).

## Tests

CI runs:

```sh
perl Build.PL && ./Build test --test_files t/ --test_files xt/
```

One class:

```sh
prove -Ilib -Ixt/lib xt/21_run.t :: Test::SNMP::Info::Layer3::Vendor
```

| Test | Fails when |
|---|---|
| `t/00_load.t` | a module does not compile |
| `xt/21_run.t` | a device test class fails, including `method_coverage` |
| `xt/00_local_docininfo.t` | a class is missing from the SUBCLASSES POD in `Info.pm` |
| `xt/00_local_pod-coverage.t` | a public method has no POD |
| `xt/00_local_pod.t` | POD is malformed |
| `xt/00_local_perlcritic.t` | perlcritic severity 5 is violated |
| `xt/00_local_versionsync.t` | module `$VERSION`s differ |

- `t/` holds only `00_load.t`. Device tests are Test::Class classes at
  `xt/lib/Test/SNMP/Info/Layer{N}/Vendor.pm`, inheriting
  `My::Test::Class`, mirroring the class path.
- `method_coverage` needs a test method named after **every** public sub
  the class defines. `Extra: 'model', 'vendor'` means those subs have no
  test method of the same name.
- Detection cases go in the class's own test class, in a `device_type`
  test method (`xt/lib/Test/SNMP/Info/Layer3/Siemens.pm`).
  `xt/lib/Test/SNMP/Info.pm` tests only the generic layer logic.
- `_layers` in cached data is the raw `sysServices` integer (`6`); the
  `layers` munge turns it into the bit string.
- Tests prime the mock session with `$test->{info}->cache({...})`. Build
  that data synthetically from real walks: same OIDs, index shapes and
  value forms. Never commit a raw walk.

## Before you open a pull request

- [ ] The CI command above passes locally.
- [ ] New class: test class in `xt/`, `=item` entry in the `Info.pm`
      SUBCLASSES POD, MANIFEST entries for both files.
- [ ] Class POD follows the outline in the authoring skill.
- [ ] `Changes`: one entry under the top, undated `Version` heading, in the
      form `* #PR Vendor: what changed (#issue)`. Add it once the PR number
      exists.
- [ ] Not edited: `$VERSION`, `README`, `META.json`, `META.yml`. Maintainers
      regenerate them at release.
- [ ] No raw walks, screenshots or other assets committed; attach them to
      the PR conversation instead.
- [ ] Any MIB the class names is in a netdisco-mibs release, or a
      netdisco-mibs PR adding it is linked.
- [ ] The PR body says what changed, why, and how it was verified, and
      closes the issue it fixes (`Closes #N`).

## Device data

Ask for, or attach, a numeric walk of the whole device:

```sh
snmpwalk -v2c -c <community> -ObentU <host> .1
```

Numeric output keeps objects no loaded MIB defines, and it loads into
SNMP simulators. Before anyone shares a walk, sanitize it as described in
"Removing sensitive data" under `EXTENDING SNMP::INFO` in
`lib/SNMP/Info.pm`: replace each sensitive value with one consistent
stand-in, in values and OID indexes alike, and keep OIDs, types and index
shapes intact. When helping someone capture a walk, walk them through
that list and have them read the result before posting.