python-netgear-switch-library 0.0.post154__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- netgear_switch/__init__.py +132 -0
- netgear_switch/_dispatch.py +178 -0
- netgear_switch/_version.py +24 -0
- netgear_switch/aio_api.py +529 -0
- netgear_switch/cli/__init__.py +1 -0
- netgear_switch/cli/capture.py +131 -0
- netgear_switch/cli/context.py +39 -0
- netgear_switch/cli/format.py +201 -0
- netgear_switch/cli/main.py +484 -0
- netgear_switch/cli/resolve.py +108 -0
- netgear_switch/cli/safety.py +71 -0
- netgear_switch/config.py +184 -0
- netgear_switch/errors.py +52 -0
- netgear_switch/http_read.py +174 -0
- netgear_switch/http_write.py +420 -0
- netgear_switch/models.py +156 -0
- netgear_switch/nsdp_read.py +221 -0
- netgear_switch/nsdp_write.py +315 -0
- netgear_switch/protocols/__init__.py +1 -0
- netgear_switch/protocols/http/__init__.py +1 -0
- netgear_switch/protocols/http/crypt.py +29 -0
- netgear_switch/protocols/http/endpoints.py +165 -0
- netgear_switch/protocols/http/forms.py +77 -0
- netgear_switch/protocols/http/parse.py +238 -0
- netgear_switch/protocols/http/session.py +29 -0
- netgear_switch/protocols/nsdp/__init__.py +7 -0
- netgear_switch/protocols/nsdp/auth.py +33 -0
- netgear_switch/protocols/nsdp/client.py +67 -0
- netgear_switch/protocols/nsdp/parsers.py +209 -0
- netgear_switch/protocols/nsdp/protocol.py +201 -0
- netgear_switch/protocols/nsdp/types.py +137 -0
- netgear_switch/protocols/nsdp/write.py +98 -0
- netgear_switch/protocols/snmp/__init__.py +1 -0
- netgear_switch/protocols/snmp/client.py +88 -0
- netgear_switch/protocols/snmp/oids.py +125 -0
- netgear_switch/protocols/snmp/parse.py +777 -0
- netgear_switch/protocols/snmp/write.py +112 -0
- netgear_switch/py.typed +0 -0
- netgear_switch/registry.py +227 -0
- netgear_switch/snmp_read.py +226 -0
- netgear_switch/snmp_write.py +625 -0
- netgear_switch/sync_api.py +557 -0
- netgear_switch/transport/__init__.py +1 -0
- netgear_switch/transport/aio/__init__.py +1 -0
- netgear_switch/transport/aio/nsdp_udp.py +152 -0
- netgear_switch/transport/aio/snmp_pysnmp.py +247 -0
- netgear_switch/transport/http/__init__.py +1 -0
- netgear_switch/transport/http/client.py +217 -0
- netgear_switch/transport/sync/__init__.py +1 -0
- netgear_switch/transport/sync/nsdp_udp.py +109 -0
- netgear_switch/transport/sync/snmp_netsnmp_cli.py +257 -0
- netgear_switch/virtual/__init__.py +8 -0
- netgear_switch/virtual/faces/__init__.py +2 -0
- netgear_switch/virtual/faces/http.py +164 -0
- netgear_switch/virtual/faces/mibview.py +92 -0
- netgear_switch/virtual/faces/nsdp.py +124 -0
- netgear_switch/virtual/faces/snmp.py +412 -0
- netgear_switch/virtual/seed.py +220 -0
- netgear_switch/virtual/server.py +106 -0
- netgear_switch/virtual/state.py +615 -0
- netgear_switch/virtual/web.py +210 -0
- python_netgear_switch_library-0.0.post154.dist-info/METADATA +85 -0
- python_netgear_switch_library-0.0.post154.dist-info/RECORD +66 -0
- python_netgear_switch_library-0.0.post154.dist-info/WHEEL +4 -0
- python_netgear_switch_library-0.0.post154.dist-info/entry_points.txt +2 -0
- python_netgear_switch_library-0.0.post154.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Pure output formatting for ngsw: JSON and human-readable tables.
|
|
2
|
+
|
|
3
|
+
Every function is a pure ``model object(s) -> str`` map (except ``emit``, which
|
|
4
|
+
prints), so the whole module is unit-testable without a switch or network.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import dataclasses
|
|
10
|
+
import enum
|
|
11
|
+
import json
|
|
12
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Callable, Sequence
|
|
16
|
+
|
|
17
|
+
from netgear_switch.models import (
|
|
18
|
+
LLDPNeighbor,
|
|
19
|
+
MacEntry,
|
|
20
|
+
MgmtIpConfig,
|
|
21
|
+
PoEStatus,
|
|
22
|
+
PortStats,
|
|
23
|
+
PortStatus,
|
|
24
|
+
Sensor,
|
|
25
|
+
SwitchData,
|
|
26
|
+
VLANInfo,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
from .context import CliContext
|
|
30
|
+
|
|
31
|
+
T = TypeVar("T")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def jsonify(obj: object) -> object:
|
|
35
|
+
"""Recursively convert dataclasses / enums / sets into JSON-native values."""
|
|
36
|
+
if dataclasses.is_dataclass(obj) and not isinstance(obj, type):
|
|
37
|
+
return {f.name: jsonify(getattr(obj, f.name)) for f in dataclasses.fields(obj)}
|
|
38
|
+
if isinstance(obj, enum.Enum):
|
|
39
|
+
return obj.value
|
|
40
|
+
if isinstance(obj, set | frozenset):
|
|
41
|
+
return sorted(obj)
|
|
42
|
+
if isinstance(obj, list | tuple):
|
|
43
|
+
return [jsonify(x) for x in obj]
|
|
44
|
+
return obj
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def to_json(obj: object) -> str:
|
|
48
|
+
return json.dumps(jsonify(obj), indent=2)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def emit(ctx: CliContext, obj: T, table_fn: Callable[[T], str]) -> None:
|
|
52
|
+
"""Print ``obj`` as JSON (when ``ctx.as_json``) or via ``table_fn``."""
|
|
53
|
+
print(to_json(obj) if ctx.as_json else table_fn(obj), file=ctx.out)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _table(headers: Sequence[str], rows: Sequence[Sequence[str]]) -> str:
|
|
57
|
+
widths = [len(h) for h in headers]
|
|
58
|
+
for row in rows:
|
|
59
|
+
for i, cell in enumerate(row):
|
|
60
|
+
widths[i] = max(widths[i], len(cell))
|
|
61
|
+
|
|
62
|
+
def render(cells: Sequence[str]) -> str:
|
|
63
|
+
return " ".join(cell.ljust(widths[i]) for i, cell in enumerate(cells))
|
|
64
|
+
|
|
65
|
+
return "\n".join([render(headers), *(render(r) for r in rows)])
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _ports(port_set: frozenset[int]) -> str:
|
|
69
|
+
return ",".join(str(p) for p in sorted(port_set)) or "-"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def ports_table(ports: Sequence[PortStatus]) -> str:
|
|
73
|
+
rows = [
|
|
74
|
+
[
|
|
75
|
+
str(p.port),
|
|
76
|
+
p.name or "-",
|
|
77
|
+
"up" if p.link_up else "down",
|
|
78
|
+
"enabled" if p.admin_enabled else "disabled",
|
|
79
|
+
"-" if p.speed_mbps is None else str(p.speed_mbps),
|
|
80
|
+
p.description or "-",
|
|
81
|
+
]
|
|
82
|
+
for p in ports
|
|
83
|
+
]
|
|
84
|
+
return _table(("Port", "Name", "Link", "Admin", "Speed", "Description"), rows)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def poe_table(entries: Sequence[PoEStatus]) -> str:
|
|
88
|
+
rows = [
|
|
89
|
+
[
|
|
90
|
+
str(e.port),
|
|
91
|
+
"enabled" if e.admin_enabled else "disabled",
|
|
92
|
+
e.detect.value,
|
|
93
|
+
"-" if e.power_mw is None else str(e.power_mw),
|
|
94
|
+
]
|
|
95
|
+
for e in entries
|
|
96
|
+
]
|
|
97
|
+
return _table(("Port", "Admin", "Detect", "Power(mW)"), rows)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def vlans_table(vlans: Sequence[VLANInfo]) -> str:
|
|
101
|
+
rows = [
|
|
102
|
+
[
|
|
103
|
+
str(v.vlan_id),
|
|
104
|
+
v.name or "-",
|
|
105
|
+
_ports(v.untagged_ports),
|
|
106
|
+
_ports(v.tagged_ports),
|
|
107
|
+
]
|
|
108
|
+
for v in vlans
|
|
109
|
+
]
|
|
110
|
+
return _table(("VLAN", "Name", "Untagged", "Tagged"), rows)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def pvids_table(pvids: Sequence[tuple[int, int]]) -> str:
|
|
114
|
+
rows = [[str(port), str(vlan)] for port, vlan in pvids]
|
|
115
|
+
return _table(("Port", "PVID"), rows)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def lldp_table(neighbors: Sequence[LLDPNeighbor]) -> str:
|
|
119
|
+
rows = [
|
|
120
|
+
[
|
|
121
|
+
str(n.local_port),
|
|
122
|
+
n.remote_sys_name or "-",
|
|
123
|
+
n.remote_port_id or "-",
|
|
124
|
+
n.remote_port_desc or "-",
|
|
125
|
+
n.remote_chassis_id or "-",
|
|
126
|
+
]
|
|
127
|
+
for n in neighbors
|
|
128
|
+
]
|
|
129
|
+
return _table(
|
|
130
|
+
("Port", "Neighbor", "RemotePortId", "RemotePortDesc", "ChassisID"), rows
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def macs_table(entries: Sequence[MacEntry]) -> str:
|
|
135
|
+
rows = [
|
|
136
|
+
[e.mac, str(e.port), "-" if e.vlan_id is None else str(e.vlan_id)]
|
|
137
|
+
for e in entries
|
|
138
|
+
]
|
|
139
|
+
return _table(("MAC", "Port", "VLAN"), rows)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def stats_table(stats: Sequence[PortStats]) -> str:
|
|
143
|
+
def cell(value: int | None) -> str:
|
|
144
|
+
return "-" if value is None else str(value)
|
|
145
|
+
|
|
146
|
+
rows = [
|
|
147
|
+
[
|
|
148
|
+
str(s.port),
|
|
149
|
+
cell(s.rx_bytes),
|
|
150
|
+
cell(s.tx_bytes),
|
|
151
|
+
cell(s.rx_packets),
|
|
152
|
+
cell(s.tx_packets),
|
|
153
|
+
cell(s.rx_errors),
|
|
154
|
+
cell(s.tx_errors),
|
|
155
|
+
]
|
|
156
|
+
for s in stats
|
|
157
|
+
]
|
|
158
|
+
headers = (
|
|
159
|
+
"Port", "RxBytes", "TxBytes", "RxPackets", "TxPackets", "RxErrors", "TxErrors",
|
|
160
|
+
)
|
|
161
|
+
return _table(headers, rows)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def sensors_table(sensors: Sequence[Sensor]) -> str:
|
|
165
|
+
rows = [[s.name, s.kind, f"{s.value:g}", s.unit] for s in sensors]
|
|
166
|
+
return _table(("Sensor", "Kind", "Value", "Unit"), rows)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def mgmt_ip_text(cfg: MgmtIpConfig) -> str:
|
|
170
|
+
return "\n".join(
|
|
171
|
+
[
|
|
172
|
+
f"mode: {cfg.mode.value}",
|
|
173
|
+
f"address: {cfg.address or '-'}",
|
|
174
|
+
f"netmask: {cfg.netmask or '-'}",
|
|
175
|
+
f"gateway: {cfg.gateway or '-'}",
|
|
176
|
+
f"mac: {cfg.base_mac or '-'}",
|
|
177
|
+
]
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def snapshot_text(data: SwitchData) -> str:
|
|
182
|
+
sections = [
|
|
183
|
+
f"# {data.model} @ {data.host}",
|
|
184
|
+
"## Ports",
|
|
185
|
+
ports_table(data.ports),
|
|
186
|
+
"## PoE",
|
|
187
|
+
poe_table(data.poe),
|
|
188
|
+
"## VLANs",
|
|
189
|
+
vlans_table(data.vlans),
|
|
190
|
+
"## PVIDs",
|
|
191
|
+
pvids_table(data.pvids),
|
|
192
|
+
"## LLDP",
|
|
193
|
+
lldp_table(data.lldp),
|
|
194
|
+
"## MACs",
|
|
195
|
+
macs_table(data.macs),
|
|
196
|
+
"## Sensors",
|
|
197
|
+
sensors_table(data.sensors),
|
|
198
|
+
]
|
|
199
|
+
if data.mgmt_ip is not None:
|
|
200
|
+
sections += ["## Mgmt IP", mgmt_ip_text(data.mgmt_ip)]
|
|
201
|
+
return "\n".join(sections)
|
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
"""``ngsw`` entry point: argparse wiring, dispatch, and error handling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import getpass
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
import traceback
|
|
10
|
+
from typing import TYPE_CHECKING, TypedDict
|
|
11
|
+
|
|
12
|
+
from netgear_switch.errors import NetgearSwitchError
|
|
13
|
+
from netgear_switch.models import VlanMode
|
|
14
|
+
from netgear_switch.registry import MODELS
|
|
15
|
+
|
|
16
|
+
from . import capture, safety
|
|
17
|
+
from . import format as fmt
|
|
18
|
+
from .context import EXIT_OK, EXIT_USAGE, CliContext, exit_code_for
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from collections.abc import Callable, Mapping
|
|
22
|
+
from typing import TextIO
|
|
23
|
+
|
|
24
|
+
from netgear_switch.sync_api import SyncSwitch
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _global_parser(*, suppress_defaults: bool = False) -> argparse.ArgumentParser:
|
|
28
|
+
"""Options shared by the top-level parser and every subparser.
|
|
29
|
+
|
|
30
|
+
``argparse`` subparsers parse into a *fresh* namespace and then copy
|
|
31
|
+
every attribute (including unset defaults) back onto the parent
|
|
32
|
+
namespace, so a global flag given before the subcommand (e.g.
|
|
33
|
+
``ngsw --json models``) would otherwise be clobbered back to its
|
|
34
|
+
default by the subparser's own copy of the same option. When this
|
|
35
|
+
parser is used as a parent for a *subparser* (``suppress_defaults``),
|
|
36
|
+
each option's default becomes ``argparse.SUPPRESS`` so an option left
|
|
37
|
+
unset at the subcommand level doesn't overwrite a value already set
|
|
38
|
+
at the top level, while explicitly repeating the flag after the
|
|
39
|
+
subcommand still takes effect normally.
|
|
40
|
+
"""
|
|
41
|
+
default = argparse.SUPPRESS if suppress_defaults else None
|
|
42
|
+
gp = argparse.ArgumentParser(add_help=False)
|
|
43
|
+
gp.add_argument(
|
|
44
|
+
"--config",
|
|
45
|
+
metavar="INVENTORY.toml",
|
|
46
|
+
help="TOML inventory file",
|
|
47
|
+
default=default,
|
|
48
|
+
)
|
|
49
|
+
gp.add_argument(
|
|
50
|
+
"--switch",
|
|
51
|
+
metavar="NAME",
|
|
52
|
+
help="switch name from the inventory",
|
|
53
|
+
default=default,
|
|
54
|
+
)
|
|
55
|
+
gp.add_argument(
|
|
56
|
+
"--host", metavar="HOST", help="switch host (with --model)", default=default
|
|
57
|
+
)
|
|
58
|
+
gp.add_argument(
|
|
59
|
+
"--model", metavar="KEY", help="model key (with --host)", default=default
|
|
60
|
+
)
|
|
61
|
+
gp.add_argument(
|
|
62
|
+
"--community",
|
|
63
|
+
metavar="STR",
|
|
64
|
+
help="SNMP read community override",
|
|
65
|
+
default=default,
|
|
66
|
+
)
|
|
67
|
+
gp.add_argument(
|
|
68
|
+
"--write-community",
|
|
69
|
+
metavar="STR",
|
|
70
|
+
help="SNMP write community override",
|
|
71
|
+
default=default,
|
|
72
|
+
)
|
|
73
|
+
gp.add_argument(
|
|
74
|
+
"--json",
|
|
75
|
+
action="store_true",
|
|
76
|
+
help="emit machine-readable JSON output",
|
|
77
|
+
default=argparse.SUPPRESS if suppress_defaults else False,
|
|
78
|
+
)
|
|
79
|
+
gp.add_argument(
|
|
80
|
+
"-v",
|
|
81
|
+
"--verbose",
|
|
82
|
+
action="store_true",
|
|
83
|
+
help="print tracebacks on error",
|
|
84
|
+
default=argparse.SUPPRESS if suppress_defaults else False,
|
|
85
|
+
)
|
|
86
|
+
return gp
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
_ModelRow = TypedDict(
|
|
90
|
+
"_ModelRow",
|
|
91
|
+
{
|
|
92
|
+
"key": str,
|
|
93
|
+
"display_name": str,
|
|
94
|
+
"class": str,
|
|
95
|
+
"ports": int,
|
|
96
|
+
"backends": list[str],
|
|
97
|
+
"verified": bool,
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _cmd_models(
|
|
103
|
+
args: argparse.Namespace,
|
|
104
|
+
ctx: CliContext,
|
|
105
|
+
get_switch: Callable[[], SyncSwitch],
|
|
106
|
+
) -> int:
|
|
107
|
+
del args, get_switch # unused: this handler needs neither
|
|
108
|
+
rows: list[_ModelRow] = [
|
|
109
|
+
{
|
|
110
|
+
"key": m.key,
|
|
111
|
+
"display_name": m.display_name,
|
|
112
|
+
"class": m.switch_class.value,
|
|
113
|
+
"ports": m.port_count,
|
|
114
|
+
"backends": sorted(b.value for b in m.backends),
|
|
115
|
+
"verified": m.verified,
|
|
116
|
+
}
|
|
117
|
+
for m in MODELS.values()
|
|
118
|
+
]
|
|
119
|
+
if ctx.as_json:
|
|
120
|
+
print(json.dumps(rows, indent=2), file=ctx.out)
|
|
121
|
+
else:
|
|
122
|
+
for row in rows:
|
|
123
|
+
# UNVERIFIED-pending-capture models (registry.py's `verified`
|
|
124
|
+
# flag) are marked so `ngsw models` never implies these are
|
|
125
|
+
# capture-confirmed like the rest of the registry.
|
|
126
|
+
suffix = "" if row["verified"] else " [UNVERIFIED]"
|
|
127
|
+
print(
|
|
128
|
+
f"{row['key']:<12} {row['display_name']:<24} "
|
|
129
|
+
f"{'+'.join(row['backends'])}{suffix}",
|
|
130
|
+
file=ctx.out,
|
|
131
|
+
)
|
|
132
|
+
return EXIT_OK
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _cmd_ports(
|
|
136
|
+
args: argparse.Namespace, ctx: CliContext, get_switch: Callable[[], SyncSwitch]
|
|
137
|
+
) -> int:
|
|
138
|
+
fmt.emit(ctx, get_switch().get_ports(), fmt.ports_table)
|
|
139
|
+
return EXIT_OK
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _cmd_stats(
|
|
143
|
+
args: argparse.Namespace, ctx: CliContext, get_switch: Callable[[], SyncSwitch]
|
|
144
|
+
) -> int:
|
|
145
|
+
fmt.emit(ctx, get_switch().get_stats(), fmt.stats_table)
|
|
146
|
+
return EXIT_OK
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _cmd_vlans(
|
|
150
|
+
args: argparse.Namespace, ctx: CliContext, get_switch: Callable[[], SyncSwitch]
|
|
151
|
+
) -> int:
|
|
152
|
+
fmt.emit(ctx, get_switch().get_vlans(), fmt.vlans_table)
|
|
153
|
+
return EXIT_OK
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _cmd_pvids(
|
|
157
|
+
args: argparse.Namespace, ctx: CliContext, get_switch: Callable[[], SyncSwitch]
|
|
158
|
+
) -> int:
|
|
159
|
+
fmt.emit(ctx, get_switch().get_pvids(), fmt.pvids_table)
|
|
160
|
+
return EXIT_OK
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _cmd_lldp(
|
|
164
|
+
args: argparse.Namespace, ctx: CliContext, get_switch: Callable[[], SyncSwitch]
|
|
165
|
+
) -> int:
|
|
166
|
+
fmt.emit(ctx, get_switch().get_lldp(), fmt.lldp_table)
|
|
167
|
+
return EXIT_OK
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _cmd_macs(
|
|
171
|
+
args: argparse.Namespace, ctx: CliContext, get_switch: Callable[[], SyncSwitch]
|
|
172
|
+
) -> int:
|
|
173
|
+
fmt.emit(ctx, get_switch().get_macs(), fmt.macs_table)
|
|
174
|
+
return EXIT_OK
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _cmd_sensors(
|
|
178
|
+
args: argparse.Namespace, ctx: CliContext, get_switch: Callable[[], SyncSwitch]
|
|
179
|
+
) -> int:
|
|
180
|
+
fmt.emit(ctx, get_switch().get_sensors(), fmt.sensors_table)
|
|
181
|
+
return EXIT_OK
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _cmd_show(
|
|
185
|
+
args: argparse.Namespace, ctx: CliContext, get_switch: Callable[[], SyncSwitch]
|
|
186
|
+
) -> int:
|
|
187
|
+
fmt.emit(ctx, get_switch().snapshot(), fmt.snapshot_text)
|
|
188
|
+
return EXIT_OK
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _cmd_poe(
|
|
192
|
+
args: argparse.Namespace, ctx: CliContext, get_switch: Callable[[], SyncSwitch]
|
|
193
|
+
) -> int:
|
|
194
|
+
switch = get_switch()
|
|
195
|
+
if args.port is None:
|
|
196
|
+
fmt.emit(ctx, switch.get_poe(), fmt.poe_table)
|
|
197
|
+
return EXIT_OK
|
|
198
|
+
if args.action is None:
|
|
199
|
+
print(
|
|
200
|
+
"error: an action (on|off|cycle|clear-fault) is required with a port",
|
|
201
|
+
file=ctx.err,
|
|
202
|
+
)
|
|
203
|
+
return EXIT_USAGE
|
|
204
|
+
actions: dict[str, Callable[[], None]] = {
|
|
205
|
+
"on": lambda: switch.set_poe(args.port, True, force=args.force),
|
|
206
|
+
"off": lambda: switch.set_poe(args.port, False, force=args.force),
|
|
207
|
+
"cycle": lambda: switch.cycle_poe(args.port, force=args.force),
|
|
208
|
+
"clear-fault": lambda: switch.clear_poe_fault(args.port, force=args.force),
|
|
209
|
+
}
|
|
210
|
+
return safety.do_write(
|
|
211
|
+
ctx,
|
|
212
|
+
dry_run=args.dry_run,
|
|
213
|
+
assume_yes=args.yes,
|
|
214
|
+
host=switch.host,
|
|
215
|
+
description=f"set PoE port {args.port} -> {args.action}",
|
|
216
|
+
action=actions[args.action],
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _cmd_port(
|
|
221
|
+
args: argparse.Namespace, ctx: CliContext, get_switch: Callable[[], SyncSwitch]
|
|
222
|
+
) -> int:
|
|
223
|
+
switch = get_switch()
|
|
224
|
+
enabled = args.state == "up"
|
|
225
|
+
return safety.do_write(
|
|
226
|
+
ctx,
|
|
227
|
+
dry_run=args.dry_run,
|
|
228
|
+
assume_yes=args.yes,
|
|
229
|
+
host=switch.host,
|
|
230
|
+
description=f"set port {args.port} {'up' if enabled else 'down'}",
|
|
231
|
+
action=lambda: switch.set_port_enabled(args.port, enabled, force=args.force),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _cmd_pvid(
|
|
236
|
+
args: argparse.Namespace, ctx: CliContext, get_switch: Callable[[], SyncSwitch]
|
|
237
|
+
) -> int:
|
|
238
|
+
switch = get_switch()
|
|
239
|
+
return safety.do_write(
|
|
240
|
+
ctx,
|
|
241
|
+
dry_run=args.dry_run,
|
|
242
|
+
assume_yes=args.yes,
|
|
243
|
+
host=switch.host,
|
|
244
|
+
description=f"set PVID port {args.port} -> VLAN {args.vlan}",
|
|
245
|
+
action=lambda: switch.set_pvid(args.port, args.vlan, force=args.force),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _cmd_vlan_set(
|
|
250
|
+
args: argparse.Namespace, ctx: CliContext, get_switch: Callable[[], SyncSwitch]
|
|
251
|
+
) -> int:
|
|
252
|
+
switch = get_switch()
|
|
253
|
+
mode = VlanMode(args.mode)
|
|
254
|
+
return safety.do_write(
|
|
255
|
+
ctx,
|
|
256
|
+
dry_run=args.dry_run,
|
|
257
|
+
assume_yes=args.yes,
|
|
258
|
+
host=switch.host,
|
|
259
|
+
description=f"set VLAN {args.vlan} port {args.port} -> {args.mode}",
|
|
260
|
+
action=lambda: switch.set_vlan_membership(
|
|
261
|
+
args.vlan, args.port, mode, force=args.force
|
|
262
|
+
),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _cmd_vlan_create(
|
|
267
|
+
args: argparse.Namespace, ctx: CliContext, get_switch: Callable[[], SyncSwitch]
|
|
268
|
+
) -> int:
|
|
269
|
+
switch = get_switch()
|
|
270
|
+
return safety.do_write(
|
|
271
|
+
ctx,
|
|
272
|
+
dry_run=args.dry_run,
|
|
273
|
+
assume_yes=args.yes,
|
|
274
|
+
host=switch.host,
|
|
275
|
+
description=f"create VLAN {args.vlan} named {args.name!r}",
|
|
276
|
+
action=lambda: switch.create_vlan(args.vlan, args.name, force=args.force),
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _cmd_vlan_delete(
|
|
281
|
+
args: argparse.Namespace, ctx: CliContext, get_switch: Callable[[], SyncSwitch]
|
|
282
|
+
) -> int:
|
|
283
|
+
switch = get_switch()
|
|
284
|
+
return safety.do_write(
|
|
285
|
+
ctx,
|
|
286
|
+
dry_run=args.dry_run,
|
|
287
|
+
assume_yes=args.yes,
|
|
288
|
+
host=switch.host,
|
|
289
|
+
description=f"delete VLAN {args.vlan}",
|
|
290
|
+
action=lambda: switch.delete_vlan(args.vlan, force=args.force),
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _cmd_ip(
|
|
295
|
+
args: argparse.Namespace, ctx: CliContext, get_switch: Callable[[], SyncSwitch]
|
|
296
|
+
) -> int:
|
|
297
|
+
del args
|
|
298
|
+
fmt.emit(ctx, get_switch().get_mgmt_ip(), fmt.mgmt_ip_text)
|
|
299
|
+
return EXIT_OK
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _cmd_ip_set(
|
|
303
|
+
args: argparse.Namespace, ctx: CliContext, get_switch: Callable[[], SyncSwitch]
|
|
304
|
+
) -> int:
|
|
305
|
+
switch = get_switch()
|
|
306
|
+
return safety.do_write(
|
|
307
|
+
ctx,
|
|
308
|
+
dry_run=args.dry_run,
|
|
309
|
+
assume_yes=args.yes,
|
|
310
|
+
host=switch.host,
|
|
311
|
+
description=(
|
|
312
|
+
f"set mgmt IP {args.address} netmask {args.netmask} gw {args.gateway}"
|
|
313
|
+
),
|
|
314
|
+
action=lambda: switch.set_mgmt_ip(
|
|
315
|
+
args.address, args.netmask, args.gateway, force=args.force
|
|
316
|
+
),
|
|
317
|
+
warning="WARNING: a wrong management-IP change can strand the switch.",
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _cmd_capture(
|
|
322
|
+
args: argparse.Namespace, ctx: CliContext, get_switch: Callable[[], SyncSwitch]
|
|
323
|
+
) -> int:
|
|
324
|
+
from pathlib import Path
|
|
325
|
+
|
|
326
|
+
switch = get_switch()
|
|
327
|
+
record = capture.run_capture(
|
|
328
|
+
switch,
|
|
329
|
+
Path(args.output),
|
|
330
|
+
snapshot_only=args.snapshot_only,
|
|
331
|
+
raw_walk=None if args.snapshot_only else capture.default_raw_walk,
|
|
332
|
+
)
|
|
333
|
+
print(f"wrote capture for {record.model} to {args.output}", file=ctx.out)
|
|
334
|
+
for note in record.notes:
|
|
335
|
+
print(f"note: {note}", file=ctx.err)
|
|
336
|
+
return EXIT_OK
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
340
|
+
gp = _global_parser()
|
|
341
|
+
parser = argparse.ArgumentParser(
|
|
342
|
+
prog="ngsw",
|
|
343
|
+
parents=[gp],
|
|
344
|
+
description="Query and control Netgear switches over the SyncSwitch facade.",
|
|
345
|
+
)
|
|
346
|
+
parser.set_defaults(func=None)
|
|
347
|
+
sub = parser.add_subparsers(dest="command")
|
|
348
|
+
child_gp = _global_parser(suppress_defaults=True)
|
|
349
|
+
models = sub.add_parser(
|
|
350
|
+
"models", parents=[child_gp], help="list the known switch models"
|
|
351
|
+
)
|
|
352
|
+
models.set_defaults(func=_cmd_models)
|
|
353
|
+
|
|
354
|
+
def read_cmd(name: str, handler: object, help_text: str) -> None:
|
|
355
|
+
parser_ = sub.add_parser(name, parents=[child_gp], help=help_text)
|
|
356
|
+
parser_.set_defaults(func=handler)
|
|
357
|
+
|
|
358
|
+
read_cmd("ports", _cmd_ports, "show port status")
|
|
359
|
+
read_cmd("stats", _cmd_stats, "show port RX/TX counters")
|
|
360
|
+
read_cmd("vlans", _cmd_vlans, "show VLANs")
|
|
361
|
+
read_cmd("pvids", _cmd_pvids, "show per-port PVIDs")
|
|
362
|
+
read_cmd("lldp", _cmd_lldp, "show LLDP neighbours")
|
|
363
|
+
read_cmd("macs", _cmd_macs, "show the MAC/FDB table")
|
|
364
|
+
read_cmd("sensors", _cmd_sensors, "show sensors")
|
|
365
|
+
read_cmd("show", _cmd_show, "show a full switch snapshot")
|
|
366
|
+
|
|
367
|
+
poe = sub.add_parser(
|
|
368
|
+
"poe", parents=[child_gp], help="show PoE status, or control a port's PoE"
|
|
369
|
+
)
|
|
370
|
+
poe.add_argument("port", type=int, nargs="?", help="port number to control")
|
|
371
|
+
poe.add_argument(
|
|
372
|
+
"action",
|
|
373
|
+
nargs="?",
|
|
374
|
+
choices=("on", "off", "cycle", "clear-fault"),
|
|
375
|
+
help="PoE action for the given port",
|
|
376
|
+
)
|
|
377
|
+
safety.add_write_args(poe)
|
|
378
|
+
poe.set_defaults(func=_cmd_poe)
|
|
379
|
+
|
|
380
|
+
port = sub.add_parser("port", parents=[child_gp], help="bring a port up or down")
|
|
381
|
+
port.add_argument("port", type=int, help="port number")
|
|
382
|
+
port.add_argument("state", choices=("up", "down"), help="admin state")
|
|
383
|
+
safety.add_write_args(port)
|
|
384
|
+
port.set_defaults(func=_cmd_port)
|
|
385
|
+
|
|
386
|
+
pvid = sub.add_parser("pvid", parents=[child_gp], help="set a port's PVID")
|
|
387
|
+
pvid.add_argument("port", type=int, help="port number")
|
|
388
|
+
pvid.add_argument("vlan", type=int, help="VLAN id")
|
|
389
|
+
safety.add_write_args(pvid)
|
|
390
|
+
pvid.set_defaults(func=_cmd_pvid)
|
|
391
|
+
|
|
392
|
+
vlan = sub.add_parser(
|
|
393
|
+
"vlan", parents=[child_gp], help="create/delete VLANs or set membership"
|
|
394
|
+
)
|
|
395
|
+
vlan_sub = vlan.add_subparsers(dest="vlan_cmd", required=True)
|
|
396
|
+
|
|
397
|
+
vlan_set = vlan_sub.add_parser(
|
|
398
|
+
"set", parents=[child_gp], help="set port VLAN membership"
|
|
399
|
+
)
|
|
400
|
+
vlan_set.add_argument("vlan", type=int)
|
|
401
|
+
vlan_set.add_argument("port", type=int)
|
|
402
|
+
vlan_set.add_argument("mode", choices=("untagged", "tagged", "excluded"))
|
|
403
|
+
safety.add_write_args(vlan_set)
|
|
404
|
+
vlan_set.set_defaults(func=_cmd_vlan_set)
|
|
405
|
+
|
|
406
|
+
vlan_create = vlan_sub.add_parser(
|
|
407
|
+
"create", parents=[child_gp], help="create a VLAN"
|
|
408
|
+
)
|
|
409
|
+
vlan_create.add_argument("vlan", type=int)
|
|
410
|
+
vlan_create.add_argument("name")
|
|
411
|
+
safety.add_write_args(vlan_create)
|
|
412
|
+
vlan_create.set_defaults(func=_cmd_vlan_create)
|
|
413
|
+
|
|
414
|
+
vlan_delete = vlan_sub.add_parser(
|
|
415
|
+
"delete", parents=[child_gp], help="delete a VLAN"
|
|
416
|
+
)
|
|
417
|
+
vlan_delete.add_argument("vlan", type=int)
|
|
418
|
+
safety.add_write_args(vlan_delete)
|
|
419
|
+
vlan_delete.set_defaults(func=_cmd_vlan_delete)
|
|
420
|
+
|
|
421
|
+
ip = sub.add_parser("ip", parents=[child_gp], help="show or set the management IP")
|
|
422
|
+
ip.set_defaults(func=_cmd_ip)
|
|
423
|
+
ip_sub = ip.add_subparsers(dest="ip_cmd")
|
|
424
|
+
ip_set = ip_sub.add_parser(
|
|
425
|
+
"set", parents=[child_gp], help="set the management IP"
|
|
426
|
+
)
|
|
427
|
+
ip_set.add_argument("address")
|
|
428
|
+
ip_set.add_argument("netmask")
|
|
429
|
+
ip_set.add_argument("gateway")
|
|
430
|
+
safety.add_write_args(ip_set)
|
|
431
|
+
ip_set.set_defaults(func=_cmd_ip_set)
|
|
432
|
+
|
|
433
|
+
cap = sub.add_parser(
|
|
434
|
+
"capture",
|
|
435
|
+
parents=[child_gp],
|
|
436
|
+
help="record a real switch's state + protocol exchanges (opt-in, live)",
|
|
437
|
+
)
|
|
438
|
+
cap.add_argument("output", help="output JSON file path")
|
|
439
|
+
cap.add_argument(
|
|
440
|
+
"--snapshot-only",
|
|
441
|
+
action="store_true",
|
|
442
|
+
help="record only the state snapshot (skip the live raw protocol walk)",
|
|
443
|
+
)
|
|
444
|
+
cap.set_defaults(func=_cmd_capture)
|
|
445
|
+
|
|
446
|
+
return parser
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def main(
|
|
450
|
+
argv: list[str] | None = None,
|
|
451
|
+
*,
|
|
452
|
+
switch_factory: Callable[[argparse.Namespace, CliContext], SyncSwitch]
|
|
453
|
+
| None = None,
|
|
454
|
+
stdin: TextIO | None = None,
|
|
455
|
+
stdout: TextIO | None = None,
|
|
456
|
+
stderr: TextIO | None = None,
|
|
457
|
+
env: Mapping[str, str] | None = None,
|
|
458
|
+
prompt: Callable[[str], str] = getpass.getpass,
|
|
459
|
+
) -> int:
|
|
460
|
+
parser = build_parser()
|
|
461
|
+
args = parser.parse_args(argv)
|
|
462
|
+
out = sys.stdout if stdout is None else stdout
|
|
463
|
+
err = sys.stderr if stderr is None else stderr
|
|
464
|
+
inp = sys.stdin if stdin is None else stdin
|
|
465
|
+
ctx = CliContext(out=out, err=err, inp=inp, as_json=args.json, verbose=args.verbose)
|
|
466
|
+
if args.func is None:
|
|
467
|
+
parser.print_help(err)
|
|
468
|
+
return EXIT_USAGE
|
|
469
|
+
|
|
470
|
+
def get_switch() -> SyncSwitch:
|
|
471
|
+
if switch_factory is not None:
|
|
472
|
+
return switch_factory(args, ctx)
|
|
473
|
+
from .resolve import resolve_switch
|
|
474
|
+
|
|
475
|
+
return resolve_switch(args, env=env, prompt=prompt)
|
|
476
|
+
|
|
477
|
+
try:
|
|
478
|
+
result: int = args.func(args, ctx, get_switch)
|
|
479
|
+
return result
|
|
480
|
+
except NetgearSwitchError as exc:
|
|
481
|
+
if ctx.verbose:
|
|
482
|
+
traceback.print_exc(file=err)
|
|
483
|
+
print(f"error: {exc}", file=err)
|
|
484
|
+
return exit_code_for(exc)
|