Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions device-discovery/custom_napalm/brocade_netiron.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,65 @@ def _netiron_aggregate_to_switchport(per_port: dict) -> SwitchportInfo:
# ---------------------------------------------------------------------------


# "show vrf" rows: name, default RD, A|A|A status flags, route count, then
# space-separated member interfaces wrapping onto indented continuations.
_NETIRON_VRF_ROW_RE = re.compile(
r"^(?P<name>\S+)\s+(?P<rd>\S+(?:\s+[Ss]et)?)\s+[ADI](?:\s*\|\s*[ADI-]?){2}\s+"
r"(?P<routes>\d+)\s*(?P<ifaces>.*)$"
)
# Member tokens in the Interfaces column: ve150, e1/5, eth 2/3, lag5,
# loopback1, po10 (case-insensitive).
_NETIRON_VRF_MEMBER_RE = re.compile(
r"\b(e(?:th(?:ernet)?)?\s?\d+/\d+|ve\s?\d+|lag\s?\d+|loopback\s?\d+|po\s?\d+)\b",
re.IGNORECASE,
)
# Ethernet member shorthand ("e1/5", "eth 2/3") → bare slot/port key used
# by the canonical-name map.
_NETIRON_VRF_ETH_RE = re.compile(r"^e(?:th(?:ernet)?)?\s?(\d+/\d+)$", re.IGNORECASE)
# RD column sentinels NetIron prints when no RD is configured. The row
# regex's rd group accepts an optional trailing "Set" token so the
# two-word "Not Set" form reaches this check intact.
_NETIRON_RD_UNSET = frozenset(
{"(null)", "null", "-", "n/a", "none", "not set", "notset"}
)


def _netiron_parse_show_vrf(raw: str) -> dict[str, tuple[str, list[str]]]:
"""Parse ``show vrf`` into vrf name → (rd, raw member tokens)."""
out: dict[str, tuple[str, list[str]]] = {}
current: str | None = None
for line in raw.splitlines():
if not line.strip():
continue
m = _NETIRON_VRF_ROW_RE.match(line)
if m:
current = m.group("name")
rd = m.group("rd").strip()
if rd.lower() in _NETIRON_RD_UNSET:
rd = ""
members = _NETIRON_VRF_MEMBER_RE.findall(m.group("ifaces"))
out[current] = (rd, members)
continue
if current is not None and line[:1].isspace():
rd, members = out[current]
members.extend(_NETIRON_VRF_MEMBER_RE.findall(line))
out[current] = (rd, members)
else:
current = None
return out


def _netiron_canonical_member(token: str, canonical_map: dict[str, str]) -> str:
"""Canonicalise a show-vrf member token via the bare-id name map."""
token = token.strip()
m = _NETIRON_VRF_ETH_RE.match(token)
if m:
bare = m.group(1)
else:
bare = token.replace(" ", "").lower()
return canonical_map.get(bare, token)


class NetIronDriver(_napalm_base.NetworkDriver):
"""Brocade/Extreme NetIron NAPALM driver (read-only subset for device-discovery)."""

Expand Down Expand Up @@ -719,3 +778,47 @@ def _netiron_canonical_name_map(self) -> dict[str, str]:
continue
out[f"{prefix.lower()}{m.group(2)}"] = full
return out

def get_network_instances(self, name: str = "") -> dict:
"""
Return network instances (NetIron VRFs), NAPALM OC shape.

Parsed driver-locally from ``show vrf`` (no ntc-template exists):
one row per VRF with the default RD and a space-separated member
interface list that may wrap onto indented continuation lines.
Member tokens (``ve150``, ``e1/5``) are canonicalised through the
same bare-id map get_interfaces_vlans() uses so they join the
canonical names get_interfaces() emits (``Ve150``,
``GigabitEthernet1/5``). The global routing table is seeded as
``default-vrf`` (DEFAULT_INSTANCE, empty membership).

NOTE: built from the vendor-documented output format; not yet
validated against a live NetIron device.
"""
instances: dict = {
"default-vrf": {
"name": "default-vrf",
"type": "DEFAULT_INSTANCE",
"state": {"route_distinguisher": ""},
"interfaces": {"interface": {}},
},
}
raw = self.device.send_command("show vrf")
rows = _netiron_parse_show_vrf(raw or "")
canonical_map = self._netiron_canonical_name_map() if rows else {}
for vrf_name, (rd, members) in rows.items():
# Never let a row overwrite the seeded DEFAULT_INSTANCE.
if vrf_name == "default-vrf":
continue
interfaces = {
_netiron_canonical_member(m, canonical_map): {} for m in members
}
instances[vrf_name] = {
"name": vrf_name,
"type": "L3VRF",
"state": {"route_distinguisher": rd},
"interfaces": {"interface": interfaces},
}
if name:
return {name: instances[name]} if name in instances else {}
return instances
162 changes: 162 additions & 0 deletions device-discovery/custom_napalm/dell_ftos.py
Original file line number Diff line number Diff line change
Expand Up @@ -715,3 +715,165 @@ def get_interfaces_vlans(self) -> dict[str, dict]:
info = _ftos_row_to_switchport_info(row)
result[ifname] = classify_switchport(info)
return result

def get_network_instances(self, name: str = "") -> dict:
"""
Return network instances (OS9 VRFs), NAPALM OC shape.

Parsed driver-locally from ``show ip vrf`` (no ntc-template
exists): one row per VRF with abbreviated comma-separated member
interfaces that may wrap onto indented continuation lines and
use trailing-number ranges (``Gi 1/3-1/5``). Member abbreviations
expand to the full template forms (``Gi 1/2`` →
``GigabitEthernet 1/2``) because that is how this driver's
get_interfaces()/get_interfaces_ip() key interfaces — the
VRF→IP join is by exact name. The VRF named
``default`` (id 0) is the global routing table
(DEFAULT_INSTANCE, empty membership); the ``management`` VRF
(id 511) is a real VRF and is kept. OS9 keeps the RD in
per-VRF BGP config — not collected in this pass.

NOTE: built from the vendor-documented output format; not yet
validated against a live OS9 device.
"""
instances: dict = {
"default": {
"name": "default",
"type": "DEFAULT_INSTANCE",
"state": {"route_distinguisher": ""},
"interfaces": {"interface": {}},
},
}
raw = self.device.send_command("show ip vrf")
for vrf_name, members in _ftos_parse_show_ip_vrf(raw or "").items():
# Never let a row overwrite the seeded DEFAULT_INSTANCE.
if vrf_name == "default":
continue
instances[vrf_name] = {
"name": vrf_name,
"type": "L3VRF",
"state": {"route_distinguisher": ""},
"interfaces": {"interface": {m: {} for m in members}},
}
if name:
return {name: instances[name]} if name in instances else {}
return instances


# "show ip vrf" rows: name, numeric VRF id, comma-separated abbreviated
# member interfaces that may wrap onto indented continuation lines. The
# header ("VRF-Name VRF-ID Interfaces") never matches — its second
# column is not numeric.
_FTOS_VRF_ROW_RE = re.compile(r"^\s*(?P<name>\S+)\s+(?P<vrf_id>\d+)\b(?P<rest>.*)$")
# Abbreviated member tokens: "Gi 1/2", "Vl 100", "Ma 1/1", "Te 1/3-1/5".
_FTOS_VRF_MEMBER_RE = re.compile(r"\b([A-Z][a-zA-Z]{0,2} \d[\d/.\-]*)")
Comment thread
leoparente marked this conversation as resolved.
Outdated
# show ip vrf abbreviations → the full interface names the dell_force10
# ntc-templates emit (and therefore how get_interfaces()/get_interfaces_ip()
# key interfaces — the VRF→IP join is by exact name). Unknown abbreviations
# pass through unexpanded and simply never join.
_FTOS_ABBREV_TO_FULL = {
"Fa": "FastEthernet",
"Gi": "GigabitEthernet",
"Te": "TenGigabitEthernet",
# OS9 displays the higher speeds lowercase-first ("fortyGigE 0/48
# is up") and the getters key with device casing.
"Tf": "twentyFiveGigE",
"Fo": "fortyGigE",
"Fi": "fiftyGigE",
"Hu": "hundredGigE",
"Ma": "ManagementEthernet",
"Vl": "Vlan",
"Po": "Port-channel",
"Lo": "Loopback",
"Tu": "Tunnel",
}


def _ftos_expand_member_range(token: str) -> list[str]:
"""
Expand a trailing-number member range ("Gi 1/3-1/5" → Gi 1/3..1/5).

Tokens without a range (or with an unparseable or cross-slot one —
"Gi 1/3-2/5") pass through unchanged — an unexpanded token simply
never joins an interface name, which is safer than guessing.
"""
if "-" not in token:
return [token]
prefix, _, value = token.partition(" ")
left, _, right = value.partition("-")
left_head, _, left_last = left.rpartition("/")
right_head, _, right_last = right.rpartition("/")
if right_head and right_head != left_head:
# Cross-slot range: expanding only the trailing number would
# fabricate interface names that may belong to other VRFs.
return [token]
try:
start, end = int(left_last), int(right_last)
except ValueError:
return [token]
if end < start or end - start > 512:
return [token]
head = f"{left_head}/" if left_head else ""
return [f"{prefix} {head}{n}" for n in range(start, end + 1)]


def _ftos_expand_member_name(token: str) -> str:
"""Expand "Gi 1/2" to the full "GigabitEthernet 1/2" template form."""
abbrev, _, rest = token.partition(" ")
full = _FTOS_ABBREV_TO_FULL.get(abbrev)
return f"{full} {rest}" if full and rest else token


# Wrapped member lines align under the Interfaces column (far right of the
# 34-char name + id columns); VRF rows start at the left margin. Requiring
# deep indentation keeps a short uppercase VRF name row ("RED 1 Gi 1/7")
# from ever being mistaken for a continuation of the previous VRF.
_FTOS_CONTINUATION_MIN_INDENT = 8


def _ftos_is_member_continuation(line: str) -> bool:
"""
True when a line holds only wrapped member tokens (no name/id columns).

A continuation like ``Te 1/20`` would otherwise satisfy the row regex
("Te" as the name, "1" as the id) — but unlike a real row, a
continuation is deeply indented under the Interfaces column AND
removing every member token (plus separators) leaves nothing behind.
"""
indent = len(line) - len(line.lstrip())
if indent < _FTOS_CONTINUATION_MIN_INDENT:
return False
residue = _FTOS_VRF_MEMBER_RE.sub("", line).replace(",", "").strip()
return not residue


def _ftos_parse_show_ip_vrf(raw: str) -> dict[str, list[str]]:
"""Parse ``show ip vrf`` into vrf name → expanded member interface names."""
members_by_vrf: dict[str, list[str]] = {}
current: str | None = None
for line in raw.splitlines():
if not line.strip():
continue
if _ftos_is_member_continuation(line):
Comment thread
leoparente marked this conversation as resolved.
# Continuation with no owning row (e.g. orphaned by a paging
# header): drop it rather than letting the row regex turn it
# into a phantom VRF named after an abbreviation.
if current is None:
continue
member_text = line
else:
m = _FTOS_VRF_ROW_RE.match(line)
if not m:
# Header / footer / unparseable row: reset so a later
# orphaned continuation can't attach to a stale VRF.
current = None
continue
current = m.group("name")
members_by_vrf.setdefault(current, [])
member_text = m.group("rest")
for token in _FTOS_VRF_MEMBER_RE.findall(member_text):
members_by_vrf[current].extend(
_ftos_expand_member_name(t)
for t in _ftos_expand_member_range(token)
)
return members_by_vrf
105 changes: 105 additions & 0 deletions device-discovery/custom_napalm/extreme_slx.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,65 @@ def _parse_intf_hw_addresses(text: str) -> dict[str, str]:
r"^\s*(Management\s+\S+)\s+(\d[\d.]+(?:/\d+)?)\s",
re.IGNORECASE,
)
# Same pre-stripped Management rows, but capturing the Vrf column so VRF
# discovery doesn't lose mgmt-vrf membership (or the VRF itself when it
# appears only on Management interfaces).
_MGMT_VRF_RE = re.compile(
r"^\s*(Management\s+\S+)\s+\S+\s+(\S+)\s",
re.IGNORECASE,
)
# Regex fallback for VRF discovery when the ntc-template fails entirely —
# the interface + Vrf columns of every known interface type. Mirrors
# _INTF_IP_FALLBACK_RE so a single unparseable row can't empty the whole
# VRF discovery result.
Comment thread
leoparente marked this conversation as resolved.
Outdated
_INTF_VRF_FALLBACK_RE = re.compile(
r"^\s*((?:Ethernet|Management|Port-channel|Loopback|Ve)\s+\S+)\s+\S+\s+(\S+)\s",
re.IGNORECASE,
)


def _slx_vrf_memberships(output: str) -> list[tuple[str, str]]:
"""
Extract (interface, vrf) pairs from ``show ip interface brief`` output.

Management rows are pre-stripped before template parsing (the
ntc-template error-exits on them) and recovered with _MGMT_VRF_RE;
when the template fails on any other row, the whole output falls back
to _INTF_VRF_FALLBACK_RE — the same contract get_interfaces_ip()
follows for addresses.
"""
memberships: list[tuple[str, str]] = []
filtered = "\n".join(
line for line in output.splitlines() if not _MGMT_LINE_RE.match(line)
)
try:
rows = parse_output(
platform="extreme_slxos",
command="show ip interface brief",
data=filtered,
)
except (TextFSMError, ParsingException):
logger.warning(
"slxos: ntc-template failed for 'show ip interface brief'; "
"falling back to regex for VRF discovery",
exc_info=True,
)
for line in output.splitlines():
m = _INTF_VRF_FALLBACK_RE.match(line)
if m:
memberships.append((m.group(1).strip(), m.group(2).strip()))
return memberships
memberships.extend(
((row.get("interface") or "").strip(), (row.get("vrf") or "").strip())
for row in rows
)
# Recover the pre-stripped Management rows' Vrf column (the fallback
# branch above already covers them).
for line in output.splitlines():
m = _MGMT_VRF_RE.match(line)
if m:
memberships.append((m.group(1).strip(), m.group(2).strip()))
return memberships
# Regex fallback covering all known interface types, used when ntc-template
# parse_output() fails entirely so no interface address is silently dropped.
_INTF_IP_FALLBACK_RE = re.compile(
Expand Down Expand Up @@ -696,3 +755,49 @@ def get_interfaces_vlans(self) -> dict[str, dict]:
info = _slx_aggregate_to_switchport(data)
result[port] = classify_switchport(info)
return result

def get_network_instances(self, name: str = "") -> dict:
"""
Return network instances (SLX-OS VRFs), NAPALM OC shape.

Derived from the Vrf column of ``show ip interface brief`` — the
same template-parsed rows get_interfaces_ip() consumes, so member
names join exactly. ``default-vrf`` is the global routing table
(DEFAULT_INSTANCE, empty membership); ``mgmt-vrf`` is a real VRF
and is kept. Management interfaces are pre-stripped before
template parsing (the ntc-template error-exits on those rows)
and recovered with a dedicated regex — mirroring how
get_interfaces_ip() recovers their addresses — so mgmt-vrf
survives even when it appears only on Management interfaces.
Limitations: enumeration is membership-derived (an interface-less
VRF does not appear) and route distinguishers are not collected
(they live in ``show vrf detail``).
"""
instances: dict = {
"default-vrf": {
"name": "default-vrf",
"type": "DEFAULT_INSTANCE",
"state": {"route_distinguisher": ""},
"interfaces": {"interface": {}},
},
}
output = self.device.send_command("show ip interface brief")
memberships: list[tuple[str, str]] = []
if output and output.strip():
memberships = _slx_vrf_memberships(output)
for ifname, vrf_name in memberships:
# default-vrf rows belong to the seeded DEFAULT_INSTANCE.
if not vrf_name or not ifname or vrf_name == "default-vrf":
continue
instances.setdefault(
vrf_name,
{
"name": vrf_name,
"type": "L3VRF",
"state": {"route_distinguisher": ""},
"interfaces": {"interface": {}},
},
)["interfaces"]["interface"][ifname] = {}
if name:
return {name: instances[name]} if name in instances else {}
return instances
Loading