chore: track extracted mixin modules
Split from monolithic openwrt.py into separate mixin files for interfaces, VLANs, packages, routing, wireless, LLDP, and config. Already in use by the main driver; just missing from git tracking. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
b7e4831b26
commit
2c0a5e94bf
@@ -0,0 +1,309 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import netaddr
|
||||
|
||||
|
||||
class OpenWrtRoutingMixin:
|
||||
"""Mixin providing routing-related NAPALM methods (ping, traceroute, route)."""
|
||||
|
||||
def ping(
|
||||
self,
|
||||
destination: str,
|
||||
source: str = "",
|
||||
ttl: int = 255,
|
||||
timeout: int = 2,
|
||||
size: int = 56,
|
||||
count: int = 5,
|
||||
vrf: str = "",
|
||||
source_interface: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""Execute ping on the device and return statistics.
|
||||
|
||||
Builds a ``ping`` command with standard BusyBox/iputils flags::
|
||||
|
||||
ping -c <count> -W <timeout> -s <size> [-t <ttl>] [-I <iface>] <dest>
|
||||
|
||||
Returns ``{'success': {...}}`` or ``{'error': '<message>'}``.
|
||||
"""
|
||||
cmd_parts = ["ping", "-c", str(count), "-W", str(timeout), "-s", str(size)]
|
||||
if ttl != 255:
|
||||
cmd_parts += ["-t", str(ttl)]
|
||||
if source_interface:
|
||||
cmd_parts += ["-I", source_interface]
|
||||
elif source:
|
||||
cmd_parts += ["-I", source]
|
||||
cmd_parts.append(destination)
|
||||
|
||||
output = self._send_command(" ".join(cmd_parts))
|
||||
|
||||
# Check for hard failure before parsing
|
||||
if re.search(r"unknown host|bad address|Network unreachable|not reachable", output, re.I):
|
||||
m = re.search(r"(unknown host.*|bad address.*|Network unreachable)", output, re.I)
|
||||
return {"error": m.group(0) if m else output.strip()}
|
||||
|
||||
return self._parse_ping_output(output, destination)
|
||||
|
||||
@staticmethod
|
||||
def _parse_ping_output(output: str, destination: str) -> dict[str, Any]:
|
||||
"""Parse BusyBox/iputils ping output into NAPALM format."""
|
||||
# "2 packets transmitted, 2 packets received, 0% packet loss"
|
||||
summary_m = re.search(
|
||||
r"(\d+)\s+packets?\s+transmitted.*?(\d+)\s+(?:packets?\s+)?received.*?(\d+)%\s+packet\s+loss",
|
||||
output,
|
||||
re.S | re.I,
|
||||
)
|
||||
if not summary_m:
|
||||
return {"error": output.strip() or f"No response from {destination}"}
|
||||
|
||||
sent = int(summary_m.group(1))
|
||||
received = int(summary_m.group(2))
|
||||
loss = sent - received
|
||||
|
||||
# "round-trip min/avg/max = 6.987/7.055/7.123 ms" (BusyBox)
|
||||
# "rtt min/avg/max/mdev = 6.987/7.055/7.123/0.094 ms" (iputils)
|
||||
rtt_m = re.search(
|
||||
r"(?:round-trip|rtt)\s+min/avg/max(?:/(?:mdev|stddev))?\s*=\s*([\d.]+)/([\d.]+)/([\d.]+)(?:/([\d.]+))?",
|
||||
output,
|
||||
re.I,
|
||||
)
|
||||
rtt_min = rtt_avg = rtt_max = rtt_stddev = 0.0
|
||||
if rtt_m:
|
||||
rtt_min = float(rtt_m.group(1))
|
||||
rtt_avg = float(rtt_m.group(2))
|
||||
rtt_max = float(rtt_m.group(3))
|
||||
rtt_stddev = float(rtt_m.group(4)) if rtt_m.group(4) else 0.0
|
||||
|
||||
# Individual probe results
|
||||
results = []
|
||||
for m in re.finditer(
|
||||
r"(\d+)\s+bytes\s+from\s+(\S+?):\s+(?:icmp_seq|seq)=\d+\s+.*?time=([\d.]+)\s*ms",
|
||||
output,
|
||||
re.I,
|
||||
):
|
||||
ip = m.group(2).rstrip(":")
|
||||
results.append({"ip_address": ip, "rtt": float(m.group(3))})
|
||||
|
||||
return {
|
||||
"success": {
|
||||
"probes_sent": sent,
|
||||
"packet_loss": loss,
|
||||
"rtt_min": rtt_min,
|
||||
"rtt_avg": rtt_avg,
|
||||
"rtt_max": rtt_max,
|
||||
"rtt_stddev": rtt_stddev,
|
||||
"results": results,
|
||||
}
|
||||
}
|
||||
|
||||
def get_route_to(
|
||||
self, destination: str = "", protocol: str = "", longer: bool = False
|
||||
) -> dict[str, list[dict[str, Any]]]:
|
||||
"""Return routes to *destination* from the kernel routing table.
|
||||
|
||||
Uses ``ip route show`` (optionally filtered by prefix/match) and
|
||||
``ip route get <dest>`` for the best-path lookup.
|
||||
|
||||
Protocol filter is applied post-parse (kernel proto names:
|
||||
``kernel``, ``static``, ``dhcp``, ``bird``, ``zebra``, …).
|
||||
|
||||
Example ``ip route show`` output::
|
||||
|
||||
default via 192.168.1.1 dev br-wan proto dhcp src 203.0.113.1 metric 100
|
||||
192.168.1.0/24 dev br-lan proto kernel scope link src 192.168.1.1
|
||||
"""
|
||||
if destination:
|
||||
cmd = f"ip route show {'match ' if longer else ''}{destination}"
|
||||
else:
|
||||
cmd = "ip route show"
|
||||
|
||||
output = self._send_command(cmd)
|
||||
routes: dict[str, list[dict[str, Any]]] = {}
|
||||
|
||||
for line in output.splitlines():
|
||||
line_s = line.strip()
|
||||
if not line_s:
|
||||
continue
|
||||
|
||||
# Determine the prefix
|
||||
# "default via ..." → prefix = "0.0.0.0/0"
|
||||
# "192.168.1.0/24 dev ..." → prefix as-is
|
||||
if line_s.startswith("default"):
|
||||
prefix = "0.0.0.0/0"
|
||||
rest = line_s[len("default"):].strip()
|
||||
else:
|
||||
parts = line_s.split()
|
||||
prefix = parts[0]
|
||||
rest = " ".join(parts[1:])
|
||||
|
||||
# Extract fields
|
||||
next_hop = ""
|
||||
outgoing_iface = ""
|
||||
proto_raw = "kernel"
|
||||
metric = 0
|
||||
|
||||
m = re.search(r"\bvia\s+(\S+)", rest)
|
||||
if m:
|
||||
next_hop = m.group(1)
|
||||
|
||||
m = re.search(r"\bdev\s+(\S+)", rest)
|
||||
if m:
|
||||
outgoing_iface = m.group(1)
|
||||
|
||||
m = re.search(r"\bproto\s+(\S+)", rest)
|
||||
if m:
|
||||
proto_raw = m.group(1)
|
||||
|
||||
m = re.search(r"\bmetric\s+(\d+)", rest)
|
||||
if m:
|
||||
metric = int(m.group(1))
|
||||
|
||||
# Map proto to NAPALM-style name
|
||||
proto_map = {
|
||||
"kernel": "connected",
|
||||
"static": "static",
|
||||
"dhcp": "static",
|
||||
"bird": "bgp",
|
||||
"zebra": "ospf",
|
||||
}
|
||||
napalm_proto = proto_map.get(proto_raw.lower(), proto_raw)
|
||||
|
||||
if protocol and napalm_proto.lower() != protocol.lower():
|
||||
continue
|
||||
|
||||
entry = {
|
||||
"protocol": napalm_proto,
|
||||
"current_active": True,
|
||||
"last_active": True,
|
||||
"age": 0,
|
||||
"next_hop": next_hop,
|
||||
"outgoing_interface": outgoing_iface,
|
||||
"selected_next_hop": True,
|
||||
"preference": metric,
|
||||
"inactive_reason": "",
|
||||
"routing_table": "default",
|
||||
"protocol_attributes": {},
|
||||
}
|
||||
routes.setdefault(prefix, []).append(entry)
|
||||
|
||||
return routes
|
||||
|
||||
def traceroute(
|
||||
self,
|
||||
destination: str,
|
||||
source: str = "",
|
||||
ttl: int = 30,
|
||||
timeout: int = 3,
|
||||
vrf: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""Execute traceroute on the device.
|
||||
|
||||
Uses ``traceroute -m <ttl> -w <timeout> <dest>`` (BusyBox-compatible).
|
||||
Falls back to ``traceroute6`` for IPv6 destinations.
|
||||
|
||||
Returns ``{'success': {hop: {'probes': {probe: {rtt, ip_address, host_name}}}}}``
|
||||
or ``{'error': '<message>'}``.
|
||||
"""
|
||||
# Detect IPv6 destination
|
||||
try:
|
||||
is_ipv6 = netaddr.IPAddress(destination).version == 6
|
||||
except (netaddr.AddrFormatError, ValueError):
|
||||
is_ipv6 = ":" in destination
|
||||
|
||||
cmd_base = "traceroute6" if is_ipv6 else "traceroute"
|
||||
cmd_parts = [cmd_base, "-m", str(ttl), "-w", str(timeout)]
|
||||
if source:
|
||||
cmd_parts += ["-s", source]
|
||||
cmd_parts.append(destination)
|
||||
|
||||
output = self._send_command(" ".join(cmd_parts))
|
||||
|
||||
if re.search(r"unknown host|bad address|not reachable|cannot resolve", output, re.I):
|
||||
m = re.search(r"(unknown host.*|bad address.*|cannot resolve.*)", output, re.I)
|
||||
return {"error": m.group(0) if m else output.strip()}
|
||||
|
||||
return self._parse_traceroute_output(output)
|
||||
|
||||
@staticmethod
|
||||
def _parse_traceroute_output(output: str) -> dict[str, Any]:
|
||||
"""Parse BusyBox traceroute output into NAPALM format.
|
||||
|
||||
Example lines::
|
||||
|
||||
1 192.168.1.1 (192.168.1.1) 1.123 ms 1.456 ms 1.789 ms
|
||||
2 * * *
|
||||
"""
|
||||
hops: dict[int, dict[str, Any]] = {}
|
||||
|
||||
for line in output.splitlines():
|
||||
line_s = line.strip()
|
||||
# Hop line starts with an integer
|
||||
m = re.match(r"^(\d+)\s+(.*)", line_s)
|
||||
if not m:
|
||||
continue
|
||||
|
||||
hop_id = int(m.group(1))
|
||||
rest = m.group(2).strip()
|
||||
|
||||
# All-star line: no response
|
||||
if re.match(r"^\*[\s*]*$", rest):
|
||||
hops[hop_id] = {
|
||||
"probes": {
|
||||
1: {"rtt": -1.0, "ip_address": "*", "host_name": "*"},
|
||||
2: {"rtt": -1.0, "ip_address": "*", "host_name": "*"},
|
||||
3: {"rtt": -1.0, "ip_address": "*", "host_name": "*"},
|
||||
}
|
||||
}
|
||||
continue
|
||||
|
||||
# Extract host/IP and RTT values
|
||||
# Format: "hostname (ip) 1.1 ms 2.2 ms 3.3 ms"
|
||||
# or: "ip 1.1 ms 2.2 ms 3.3 ms"
|
||||
host_m = re.match(r"^(\S+)\s+\((\S+)\)", rest)
|
||||
if host_m:
|
||||
host_name = host_m.group(1)
|
||||
ip_address = host_m.group(2)
|
||||
else:
|
||||
# IP only
|
||||
ip_m = re.match(r"^(\d[\d.]+|[0-9a-f:]+)", rest)
|
||||
if ip_m:
|
||||
ip_address = ip_m.group(1)
|
||||
host_name = ip_address
|
||||
else:
|
||||
continue
|
||||
|
||||
rtt_values = [float(x) for x in re.findall(r"([\d.]+)\s+ms", rest)]
|
||||
|
||||
probes: dict[int, dict[str, Any]] = {}
|
||||
for i, rtt in enumerate(rtt_values[:3], start=1):
|
||||
probes[i] = {
|
||||
"rtt": rtt,
|
||||
"ip_address": ip_address,
|
||||
"host_name": host_name,
|
||||
}
|
||||
# Fill missing probes with star entries
|
||||
for i in range(len(rtt_values) + 1, 4):
|
||||
probes[i] = {"rtt": -1.0, "ip_address": "*", "host_name": "*"}
|
||||
|
||||
if probes:
|
||||
hops[hop_id] = {"probes": probes}
|
||||
|
||||
if not hops:
|
||||
return {"error": output.strip() or "No traceroute output received"}
|
||||
|
||||
return {"success": hops}
|
||||
Reference in New Issue
Block a user