Inital commit
This commit is contained in:
+44
@@ -0,0 +1,44 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
*.so
|
||||||
|
*.egg
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.eggs/
|
||||||
|
wheels/
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Packaging
|
||||||
|
*.tar.gz
|
||||||
|
*.whl
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
htmlcov/
|
||||||
|
coverage.xml
|
||||||
|
|
||||||
|
# Type checking
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# IDEs
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# napalm-tplink-jetstream
|
||||||
|
|
||||||
|
NAPALM community driver for **TP-Link Jetstream** managed switches (T1500G, T1600G, T2600G, T3700G and compatible series).
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
| Dependency | Minimum version |
|
||||||
|
|---|---|
|
||||||
|
| Python | 3.8 |
|
||||||
|
| NAPALM | 4.0 |
|
||||||
|
| Netmiko | 4.0 |
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install napalm-tplink-jetstream
|
||||||
|
```
|
||||||
|
|
||||||
|
Or from source:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/napalm-automation-community/napalm-tplink-jetstream
|
||||||
|
cd napalm-tplink-jetstream
|
||||||
|
pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```python
|
||||||
|
from napalm import get_network_driver
|
||||||
|
|
||||||
|
driver = get_network_driver("tplink_jetstream")
|
||||||
|
with driver("192.168.0.1", "admin", "admin") as device:
|
||||||
|
facts = device.get_facts()
|
||||||
|
print(facts)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implemented getters
|
||||||
|
|
||||||
|
| Getter | Status |
|
||||||
|
|---|---|
|
||||||
|
| `get_facts` | ✅ |
|
||||||
|
| `get_interfaces` | ✅ |
|
||||||
|
| `get_interfaces_ip` | ✅ |
|
||||||
|
| `get_config` | ✅ |
|
||||||
|
| `get_arp_table` | ✅ |
|
||||||
|
| `get_mac_address_table` | ✅ |
|
||||||
|
| `get_lldp_neighbors` | ✅ |
|
||||||
|
| `get_lldp_neighbors_detail` | ✅ |
|
||||||
|
| `get_vlans` | ✅ |
|
||||||
|
| `cli` | ✅ |
|
||||||
|
| `is_alive` | ✅ |
|
||||||
|
| `get_bgp_neighbors` | ❌ Not applicable |
|
||||||
|
| `load_replace_candidate` | ❌ Not applicable |
|
||||||
|
| `commit_config` | ❌ Not applicable |
|
||||||
|
|
||||||
|
## Optional arguments
|
||||||
|
|
||||||
|
| Argument | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `port` | `22` | SSH port |
|
||||||
|
| `force_no_enable` | `False` | Skip `enable` after login |
|
||||||
|
| `canonical_int_fmt` | `False` | Use canonical interface names |
|
||||||
|
|
||||||
|
Any additional keyword arguments are forwarded to Netmiko.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
pytest tests/
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache 2.0 — see [LICENSE](LICENSE).
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""NAPALM driver for TP-Link Jetstream managed switches."""
|
||||||
|
|
||||||
|
from napalm_tplink_jetstream.tplink_jetstream import TPLinkJetstreamDriver
|
||||||
|
|
||||||
|
__all__ = ["TPLinkJetstreamDriver"]
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,53 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=64", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "napalm-tplink-jetstream"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "NAPALM driver for TP-Link Jetstream managed switches"
|
||||||
|
readme = "README.md"
|
||||||
|
license = { text = "Apache-2.0" }
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
authors = [
|
||||||
|
{ name = "Christian Manivong" },
|
||||||
|
]
|
||||||
|
classifiers = [
|
||||||
|
"Topic :: Utilities",
|
||||||
|
"License :: OSI Approved :: Apache Software License",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Operating System :: POSIX :: Linux",
|
||||||
|
"Operating System :: MacOS",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"napalm>=4.0.0",
|
||||||
|
"netmiko>=4.0.0",
|
||||||
|
"netaddr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest",
|
||||||
|
"pytest-cov",
|
||||||
|
"black",
|
||||||
|
"ruff",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.entry-points."napalm.drivers"]
|
||||||
|
tplink_jetstream = "napalm_tplink_jetstream:TPLinkJetstreamDriver"
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Repository = "https://github.com/napalm-automation-community/napalm-tplink-jetstream"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["."]
|
||||||
|
include = ["napalm_tplink_jetstream*"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
target-version = "py38"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
napalm>=4.0.0
|
||||||
|
netmiko>=4.0.0
|
||||||
|
netaddr
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
"""Unit tests for TPLinkJetstreamDriver — no real device required."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from napalm_tplink_jetstream.tplink_jetstream import TPLinkJetstreamDriver
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Fixtures
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def driver():
|
||||||
|
"""Return a driver instance with a mocked Netmiko connection."""
|
||||||
|
with patch("napalm_tplink_jetstream.tplink_jetstream.ConnectHandler"):
|
||||||
|
drv = TPLinkJetstreamDriver(
|
||||||
|
hostname="192.168.0.1",
|
||||||
|
username="admin",
|
||||||
|
password="admin",
|
||||||
|
)
|
||||||
|
drv.device = MagicMock()
|
||||||
|
yield drv
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
SHOW_SYSTEM_INFO = """\
|
||||||
|
System Name : T2600G-28TS
|
||||||
|
Hardware Version : V2.0
|
||||||
|
Firmware Version : 2.0.7 Build 20200929
|
||||||
|
MAC Address : 00-0A-EB-13-07-82
|
||||||
|
IP Address : 192.168.0.1
|
||||||
|
Subnet Mask : 255.255.255.0
|
||||||
|
Default Gateway : 192.168.0.254
|
||||||
|
System Time : 2024-03-01 08:10:22
|
||||||
|
Run Time : 5 day(s) 2 hour(s) 35 min(s) 16 sec(s)
|
||||||
|
"""
|
||||||
|
|
||||||
|
SHOW_INTERFACE_STATUS = """\
|
||||||
|
Port Status Speed Duplex FlowCtrl Active-Medium LAG Linkdown-Status Description
|
||||||
|
Gi1/0/1 LinkUp 1000M Full Disable Copper N/A N/A uplink
|
||||||
|
Gi1/0/2 LinkDown 100M Full Disable Copper N/A N/A
|
||||||
|
"""
|
||||||
|
|
||||||
|
SHOW_INTERFACE_CONFIG = """\
|
||||||
|
Port State Speed Duplex FlowCtrl Description
|
||||||
|
Gi1/0/1 Enable Auto Auto Disable uplink
|
||||||
|
Gi1/0/2 Enable Auto Auto Disable
|
||||||
|
"""
|
||||||
|
|
||||||
|
SHOW_JUMBO = "Jumbo frames are disabled. Jumbo frame size is 1518 bytes."
|
||||||
|
|
||||||
|
SHOW_ARP = """\
|
||||||
|
Interface Address Hardware Addr Type
|
||||||
|
VLAN8 192.168.0.100 00:50:56:a1:b2:c3 DYNAMIC
|
||||||
|
VLAN8 192.168.0.254 00:0c:29:d4:e5:f6 DYNAMIC
|
||||||
|
"""
|
||||||
|
|
||||||
|
SHOW_MAC = """\
|
||||||
|
MAC Address Table
|
||||||
|
------------------------------------------------------------
|
||||||
|
MAC VLAN Port Type Aging
|
||||||
|
--- ---- ---- ---- -----
|
||||||
|
00:50:56:a1:b2:c3 8 Gi1/0/2 dynamic aging
|
||||||
|
ff:ff:ff:ff:ff:ff 1 CPU static noaging
|
||||||
|
"""
|
||||||
|
|
||||||
|
SHOW_VLAN = """\
|
||||||
|
UT: Untagged; TG: Tagged
|
||||||
|
VLAN Name Status Ports
|
||||||
|
---- --------------- -------- ----------------
|
||||||
|
1 Default active UT: Gi1/0/1-2
|
||||||
|
10 Management active UT: Gi1/0/3
|
||||||
|
"""
|
||||||
|
|
||||||
|
SHOW_LLDP_NEIGHBOR_INFO = """\
|
||||||
|
Port Device ID Port ID management address Port Description System Name
|
||||||
|
---- ------------ -------- ------------------ ---------------- -----------
|
||||||
|
Gi1/0/1 00-AA-BB-CC-DD-EE Gi0/1 10.0.0.1 uplink core-sw-01
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tests
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestGetFacts:
|
||||||
|
def test_returns_required_keys(self, driver):
|
||||||
|
driver.device.send_command.return_value = SHOW_SYSTEM_INFO
|
||||||
|
driver._send_command = lambda cmd, **kw: SHOW_SYSTEM_INFO
|
||||||
|
|
||||||
|
# _get_interface_list needs show interface
|
||||||
|
driver.device.send_command.side_effect = lambda cmd, **kw: (
|
||||||
|
SHOW_SYSTEM_INFO if "system-info" in cmd else SHOW_INTERFACE
|
||||||
|
)
|
||||||
|
|
||||||
|
facts = driver.get_facts()
|
||||||
|
for key in ("vendor", "model", "hostname", "os_version", "serial_number",
|
||||||
|
"uptime", "interface_list", "fqdn"):
|
||||||
|
assert key in facts
|
||||||
|
|
||||||
|
def test_vendor(self, driver):
|
||||||
|
driver._send_command = lambda cmd: SHOW_SYSTEM_INFO
|
||||||
|
driver._get_interface_list = lambda: []
|
||||||
|
facts = driver.get_facts()
|
||||||
|
assert facts["vendor"] == "TP-Link"
|
||||||
|
|
||||||
|
def test_uptime_parsing(self, driver):
|
||||||
|
driver._send_command = lambda cmd: SHOW_SYSTEM_INFO
|
||||||
|
driver._get_interface_list = lambda: []
|
||||||
|
facts = driver.get_facts()
|
||||||
|
expected = 5 * 86400 + 2 * 3600 + 35 * 60 + 16
|
||||||
|
assert facts["uptime"] == float(expected)
|
||||||
|
|
||||||
|
def test_os_version(self, driver):
|
||||||
|
driver._send_command = lambda cmd: SHOW_SYSTEM_INFO
|
||||||
|
driver._get_interface_list = lambda: []
|
||||||
|
facts = driver.get_facts()
|
||||||
|
assert "2.0.7" in facts["os_version"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetInterfaces:
|
||||||
|
def _mock_send(self, cmd):
|
||||||
|
if "jumbo" in cmd:
|
||||||
|
return SHOW_JUMBO
|
||||||
|
if "configuration" in cmd:
|
||||||
|
return SHOW_INTERFACE_CONFIG
|
||||||
|
return SHOW_INTERFACE_STATUS
|
||||||
|
|
||||||
|
def test_interface_count(self, driver):
|
||||||
|
driver._send_command = self._mock_send
|
||||||
|
ifaces = driver.get_interfaces()
|
||||||
|
assert len(ifaces) == 2
|
||||||
|
|
||||||
|
def test_is_up_and_enabled(self, driver):
|
||||||
|
driver._send_command = self._mock_send
|
||||||
|
ifaces = driver.get_interfaces()
|
||||||
|
gi1 = ifaces["Gi1/0/1"]
|
||||||
|
assert gi1["is_up"] is True
|
||||||
|
assert gi1["is_enabled"] is True
|
||||||
|
gi2 = ifaces["Gi1/0/2"]
|
||||||
|
assert gi2["is_up"] is False
|
||||||
|
|
||||||
|
def test_speed(self, driver):
|
||||||
|
driver._send_command = self._mock_send
|
||||||
|
ifaces = driver.get_interfaces()
|
||||||
|
assert ifaces["Gi1/0/1"]["speed"] == 1000.0
|
||||||
|
|
||||||
|
def test_description(self, driver):
|
||||||
|
driver._send_command = self._mock_send
|
||||||
|
ifaces = driver.get_interfaces()
|
||||||
|
assert ifaces["Gi1/0/1"]["description"] == "uplink"
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetArpTable:
|
||||||
|
def test_entry_count(self, driver):
|
||||||
|
driver._send_command = lambda cmd: SHOW_ARP
|
||||||
|
table = driver.get_arp_table()
|
||||||
|
assert len(table) == 2
|
||||||
|
|
||||||
|
def test_entry_structure(self, driver):
|
||||||
|
driver._send_command = lambda cmd: SHOW_ARP
|
||||||
|
entry = driver.get_arp_table()[0]
|
||||||
|
for key in ("interface", "mac", "ip", "age"):
|
||||||
|
assert key in entry
|
||||||
|
|
||||||
|
def test_ip_value(self, driver):
|
||||||
|
driver._send_command = lambda cmd: SHOW_ARP
|
||||||
|
ips = {e["ip"] for e in driver.get_arp_table()}
|
||||||
|
assert "192.168.0.100" in ips
|
||||||
|
assert "192.168.0.254" in ips
|
||||||
|
|
||||||
|
def test_interface_value(self, driver):
|
||||||
|
driver._send_command = lambda cmd: SHOW_ARP
|
||||||
|
interfaces = {e["interface"] for e in driver.get_arp_table()}
|
||||||
|
assert "VLAN8" in interfaces
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetMacAddressTable:
|
||||||
|
def test_entry_count(self, driver):
|
||||||
|
driver._send_command = lambda cmd: SHOW_MAC
|
||||||
|
table = driver.get_mac_address_table()
|
||||||
|
assert len(table) == 2
|
||||||
|
|
||||||
|
def test_static_flag(self, driver):
|
||||||
|
driver._send_command = lambda cmd: SHOW_MAC
|
||||||
|
entries = {e["mac"]: e for e in driver.get_mac_address_table()}
|
||||||
|
assert entries["FF:FF:FF:FF:FF:FF"]["static"] is True
|
||||||
|
assert entries["00:50:56:A1:B2:C3"]["static"] is False
|
||||||
|
|
||||||
|
def test_vlan_value(self, driver):
|
||||||
|
driver._send_command = lambda cmd: SHOW_MAC
|
||||||
|
entries = {e["mac"]: e for e in driver.get_mac_address_table()}
|
||||||
|
assert entries["00:50:56:A1:B2:C3"]["vlan"] == 8
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetVlans:
|
||||||
|
def test_vlan_count(self, driver):
|
||||||
|
driver._send_command = lambda cmd: SHOW_VLAN
|
||||||
|
vlans = driver.get_vlans()
|
||||||
|
assert len(vlans) == 2
|
||||||
|
|
||||||
|
def test_vlan_name(self, driver):
|
||||||
|
driver._send_command = lambda cmd: SHOW_VLAN
|
||||||
|
vlans = driver.get_vlans()
|
||||||
|
assert vlans["1"]["name"] == "Default"
|
||||||
|
assert vlans["10"]["name"] == "Management"
|
||||||
|
|
||||||
|
def test_port_expansion(self, driver):
|
||||||
|
driver._send_command = lambda cmd: SHOW_VLAN
|
||||||
|
vlans = driver.get_vlans()
|
||||||
|
assert "Gi1/0/1" in vlans["1"]["interfaces"]
|
||||||
|
assert "Gi1/0/2" in vlans["1"]["interfaces"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetLldpNeighbors:
|
||||||
|
def test_returns_neighbor(self, driver):
|
||||||
|
driver._send_command = lambda cmd: SHOW_LLDP_NEIGHBOR_INFO
|
||||||
|
neighbors = driver.get_lldp_neighbors()
|
||||||
|
assert "Gi1/0/1" in neighbors
|
||||||
|
assert neighbors["Gi1/0/1"][0]["hostname"] == "core-sw-01"
|
||||||
|
assert neighbors["Gi1/0/1"][0]["port"] == "Gi0/1"
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetLldpNeighborsDetail:
|
||||||
|
def test_returns_detail_keys(self, driver):
|
||||||
|
driver._send_command = lambda cmd: SHOW_LLDP_NEIGHBOR_INFO
|
||||||
|
details = driver.get_lldp_neighbors_detail()
|
||||||
|
entry = details["Gi1/0/1"][0]
|
||||||
|
for key in ("remote_chassis_id", "remote_system_name", "remote_port"):
|
||||||
|
assert key in entry
|
||||||
|
|
||||||
|
def test_system_name_parsed(self, driver):
|
||||||
|
driver._send_command = lambda cmd: SHOW_LLDP_NEIGHBOR_INFO
|
||||||
|
details = driver.get_lldp_neighbors_detail()
|
||||||
|
assert details["Gi1/0/1"][0]["remote_system_name"] == "core-sw-01"
|
||||||
|
|
||||||
|
def test_chassis_id_parsed(self, driver):
|
||||||
|
driver._send_command = lambda cmd: SHOW_LLDP_NEIGHBOR_INFO
|
||||||
|
details = driver.get_lldp_neighbors_detail()
|
||||||
|
assert details["Gi1/0/1"][0]["remote_chassis_id"] == "00-AA-BB-CC-DD-EE"
|
||||||
|
|
||||||
|
|
||||||
|
class TestIsAlive:
|
||||||
|
def test_returns_false_when_device_is_none(self, driver):
|
||||||
|
driver.device = None
|
||||||
|
assert driver.is_alive() == {"is_alive": False}
|
||||||
|
|
||||||
|
def test_returns_true_when_transport_active(self, driver):
|
||||||
|
driver.device.remote_conn.transport.is_active.return_value = True
|
||||||
|
assert driver.is_alive() == {"is_alive": True}
|
||||||
|
|
||||||
|
|
||||||
|
class TestUptimeParser:
|
||||||
|
@pytest.mark.parametrize("uptime_str,expected", [
|
||||||
|
("0 day(s) 0 hour(s) 0 min(s) 0 sec(s)", 0),
|
||||||
|
("1 day(s) 0 hour(s) 0 min(s) 0 sec(s)", 86400),
|
||||||
|
("5 day(s) 2 hour(s) 35 min(s) 16 sec(s)", 5 * 86400 + 2 * 3600 + 35 * 60 + 16),
|
||||||
|
])
|
||||||
|
def test_parse_uptime_seconds(self, uptime_str, expected):
|
||||||
|
result = TPLinkJetstreamDriver._parse_uptime_seconds(uptime_str)
|
||||||
|
assert result == float(expected)
|
||||||
|
|
||||||
|
|
||||||
|
class TestParseVlanPorts:
|
||||||
|
def test_single_port(self):
|
||||||
|
assert TPLinkJetstreamDriver._parse_vlan_ports("Gi1/0/1") == ["Gi1/0/1"]
|
||||||
|
|
||||||
|
def test_range(self):
|
||||||
|
result = TPLinkJetstreamDriver._parse_vlan_ports("Gi1/0/1-4")
|
||||||
|
assert result == ["Gi1/0/1", "Gi1/0/2", "Gi1/0/3", "Gi1/0/4"]
|
||||||
|
|
||||||
|
def test_comma_separated(self):
|
||||||
|
result = TPLinkJetstreamDriver._parse_vlan_ports("Gi1/0/1, Gi1/0/5")
|
||||||
|
assert result == ["Gi1/0/1", "Gi1/0/5"]
|
||||||
|
|
||||||
|
def test_tg_ut_prefixes(self):
|
||||||
|
result = TPLinkJetstreamDriver._parse_vlan_ports("UT: Gi1/0/1-2 TG: Gi1/0/9")
|
||||||
|
assert "Gi1/0/1" in result
|
||||||
|
assert "Gi1/0/2" in result
|
||||||
|
assert "Gi1/0/9" in result
|
||||||
Reference in New Issue
Block a user