Inventories and credentials¶
Hosts, models and credentials belong in an inventory file, not in your code or
your shell history. load_inventory reads a TOML file into
{name: SwitchConfig}; the ngsw CLI and the MCP server both use it, via
the same resolver.
The inventory file¶
[switches.core]
model = "gsm7252ps"
host = "10.1.5.22"
protected_ports = [1, 2, 49, 50, 51, 52]
[switches.core.snmp]
community = "public"
write_community = "!pass show netgear/core"
[switches.core.http]
password = "${CORE_WEB_PASSWORD}"
[switches.edge]
model = "gs305ep"
host = "10.1.5.28"
[switches.edge.nsdp]
interface = "eth0"
[switches.edge.http]
password = "${EDGE_PASSWORD}"
Per switch:
Key |
Required |
Meaning |
|---|---|---|
|
yes |
A registry key or alias; see |
|
yes |
Address or hostname of the switch. |
no |
Ports a write refuses to touch without |
|
|
no |
SNMP read community. |
|
no |
SNMP write community — a secret spec (below). |
|
no |
Web-UI admin password — a secret spec. On Plus switches this same secret is also the NSDP v1 admin password. |
|
no |
Interface to send NSDP from, e.g. |
Secret specs¶
Any value marked “secret spec” above is resolved by resolve_secret, which
accepts three forms:
Form |
Behaviour |
|---|---|
|
Read environment variable |
|
Run the command (split with |
anything else |
The literal secret. |
Prefer the first two. If any secret in the file is a literal, the file’s
permissions are checked and load_inventory raises ConfigError unless the
file is unreadable by group and other:
ConfigError: inventory.toml has insecure permissions 0o644; chmod 600 it
(contains a literal secret)
That check is ensure_secure_file, and it only fires when a literal is present
— a file containing only ${VAR} and !command specs needs no special
mode.
Secrets are resolved lazily, at the moment an operation needs them. Reading port status over SNMP never runs your password command.
Using an inventory¶
from netgear_switch import SyncSwitch, load_inventory
inventory = load_inventory("/etc/ngsw/inventory.toml")
switch = SyncSwitch.from_config(inventory["core"])
print(switch.get_ports())
from netgear_switch import AsyncSwitch, load_inventory
# load_inventory is plain file I/O — the same call either way.
inventory = load_inventory("/etc/ngsw/inventory.toml")
switch = AsyncSwitch.from_config(inventory["core"])
try:
print(await switch.get_ports())
finally:
await switch.aclose()
snmp_write_community and http_password resolve
their specs on demand and take an explicit env= mapping, which makes them
straightforward to test.
From the CLI¶
ngsw --config /etc/ngsw/inventory.toml --switch core ports
--switch requires --config; the CLI has no environment variable for the
inventory path. (The MCP server does — see MCP server.)
Credential precedence is command-line flag → environment variable → inventory
→ interactive prompt, implemented in src/netgear_switch/cli/resolve.py:
Credential |
Flag |
Environment variable |
|---|---|---|
SNMP read community |
|
|
SNMP write community |
|
|
Web UI / NSDP password |
|
— |
The prompt is only reached for a backend that actually needs the secret: a Plus switch reached over NSDP is never asked for an SNMP community.
Protected ports¶
protected_ports is enforced by the library, not only the CLI. Every write
that names a port checks it and raises ProtectedPortError unless
force=True:
switch = SyncSwitch(
get_model("gsm7252ps"), host="10.1.5.22",
protected_ports=frozenset({49, 50, 51, 52}),
)
switch.set_port_enabled(49, False) # ProtectedPortError
switch.set_port_enabled(49, False, force=True) # proceeds
switch = AsyncSwitch(
get_model("gsm7252ps"), host="10.1.5.22",
protected_ports=frozenset({49, 50, 51, 52}),
)
await switch.set_port_enabled(49, False) # ProtectedPortError
await switch.set_port_enabled(49, False, force=True) # proceeds
That one list is the cheapest guard against disabling the uplink you are connected through.