argus-netconf-exporter/tests/test_netconf_parser.py
2025-12-01 16:11:02 +08:00

272 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import xml.etree.ElementTree as ET
import pytest
from exporter.netconf_client import (
build_transceiver_filter,
parse_netconf_response,
parse_port_and_channel,
)
def test_build_transceiver_filter_contains_expected_paths():
flt = build_transceiver_filter()
assert "<components" in flt
assert "platform/transceiver" in flt
SAMPLE_XML = """\
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<data>
<components xmlns="http://openconfig.net/yang/platform">
<component>
<name>323.FourHundredGigE1/0/66</name>
<state>
<type>TRANSCEIVER</type>
<temperature>
<instant>45.5</instant>
</temperature>
</state>
<transceiver xmlns="http://openconfig.net/yang/platform/transceiver">
<state>
<present>PRESENT</present>
<vendor>Cisco</vendor>
<serial-no>ABC123</serial-no>
</state>
<physical-channels>
<channel>
<index>0</index>
<state>
<description>1/0/66:1</description>
<output-power><instant>-2.5</instant></output-power>
</state>
</channel>
</physical-channels>
</transceiver>
</component>
</components>
</data>
</rpc-reply>
"""
def test_parse_transceiver_basic():
txs, chs = parse_netconf_response(SAMPLE_XML, "dev1")
assert len(txs) == 1
r = txs[0]
assert r.device == "dev1"
assert r.component_name == "323.FourHundredGigE1/0/66"
assert r.temperature_c == 45.5
assert r.vendor == "Cisco"
assert r.serial == "ABC123"
assert r.logical_port == "1/0/66"
assert len(chs) == 1
def test_parse_channel_and_description():
_, chs = parse_netconf_response(SAMPLE_XML, "dev1")
assert len(chs) == 1
ch = chs[0]
assert ch.logical_port == "1/0/66"
assert ch.logical_channel == "1/0/66:1"
assert ch.tx_power_dbm == -2.5
def test_missing_temperature_not_fatal():
root = ET.fromstring(SAMPLE_XML)
# remove temperature node
ns_platform = "{http://openconfig.net/yang/platform}"
temp_elem = root.find(f".//{ns_platform}temperature")
parent = root.find(f".//{ns_platform}state")
if temp_elem is not None and parent is not None:
parent.remove(temp_elem)
xml_no_temp = ET.tostring(root, encoding="unicode")
txs, _ = parse_netconf_response(xml_no_temp, "dev1")
assert txs[0].temperature_c is None
def test_invalid_power_filtered():
xml_invalid = SAMPLE_XML.replace("-2.5", "2147483647.00")
_, chs = parse_netconf_response(xml_invalid, "dev1")
assert chs[0].tx_power_dbm is None
@pytest.mark.parametrize(
"description, component_name, index, expected_port, expected_channel",
[
(None, "comp1", 0, "comp1", "comp1:ch0"),
("", "comp1", 1, "comp1", "comp1:ch1"),
("1/0/1:1", "comp1", 2, "1/0/1", "1/0/1:1"),
("GigabitEthernet1/0/1", "comp1", 3, "GigabitEthernet1/0/1", "GigabitEthernet1/0/1:ch3"),
],
)
def test_parse_port_and_channel_fallbacks(
description, component_name, index, expected_port, expected_channel
):
lp, lc = parse_port_and_channel(description, component_name, index)
assert lp == expected_port
assert lc == expected_channel
def test_multiple_channels_missing_description_produce_unique_logical_channel():
xml = """\
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<data>
<components xmlns="http://openconfig.net/yang/platform">
<component>
<name>comp1</name>
<state><type>TRANSCEIVER</type></state>
<transceiver xmlns="http://openconfig.net/yang/platform/transceiver">
<physical-channels>
<channel>
<index>0</index>
<state></state>
</channel>
<channel>
<index>1</index>
<state></state>
</channel>
</physical-channels>
</transceiver>
</component>
</components>
</data>
</rpc-reply>
"""
_, channels = parse_netconf_response(xml, "dev1")
assert len(channels) == 2
ch0 = next(c for c in channels if c.channel_index == 0)
ch1 = next(c for c in channels if c.channel_index == 1)
assert ch0.logical_port == "comp1"
assert ch1.logical_port == "comp1"
assert ch0.logical_channel != ch1.logical_channel
def test_h3c_multi_component_same_serial():
xml_multi = """\
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<data>
<components xmlns="http://openconfig.net/yang/platform">
<component>
<name>63.TwoHundredGigE1/0/1:1</name>
<state><type>TRANSCEIVER</type></state>
<transceiver xmlns="http://openconfig.net/yang/platform/transceiver">
<state><serial-no>SN001</serial-no></state>
</transceiver>
</component>
<component>
<name>64.TwoHundredGigE1/0/1:2</name>
<state><type>TRANSCEIVER</type></state>
<transceiver xmlns="http://openconfig.net/yang/platform/transceiver">
<state><serial-no>SN001</serial-no></state>
</transceiver>
</component>
</components>
</data>
</rpc-reply>
"""
txs, _ = parse_netconf_response(xml_multi, "h3c")
assert len(txs) == 2
assert txs[0].serial == "SN001"
assert txs[1].serial == "SN001"
assert txs[0].component_name != txs[1].component_name
def test_prefer_config_description_over_state():
"""
测试当 config 和 state 的 description 不一致时,优先使用 config。
这是为了解决 H3C 设备 state/description 有时错误的问题。
"""
xml = """\
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<data>
<components xmlns="http://openconfig.net/yang/platform">
<component>
<name>67.TwoHundredGigE1/0/2:1</name>
<state><type>TRANSCEIVER</type></state>
<transceiver xmlns="http://openconfig.net/yang/platform/transceiver">
<physical-channels>
<channel>
<index>1</index>
<config>
<description>1/0/2:1</description>
</config>
<state>
<description>1/0/2:1</description>
<output-power><instant>1.53</instant></output-power>
</state>
</channel>
<channel>
<index>2</index>
<config>
<description>1/0/2:2</description>
</config>
<state>
<description>1/0/1:2</description>
<input-power><instant>1.07</instant></input-power>
</state>
</channel>
</physical-channels>
</transceiver>
</component>
</components>
</data>
</rpc-reply>
"""
_, channels = parse_netconf_response(xml, "h3c-device")
assert len(channels) == 2
ch1 = next(c for c in channels if c.channel_index == 1)
ch2 = next(c for c in channels if c.channel_index == 2)
# 验证 channel 1: config 和 state 一致,结果应该是 1/0/2:1
assert ch1.logical_port == "1/0/2"
assert ch1.logical_channel == "1/0/2:1"
assert ch1.tx_power_dbm == 1.53
# 验证 channel 2: config 是 1/0/2:2state 是错误的 1/0/1:2
# 应该优先使用 config 的值
assert ch2.logical_port == "1/0/2" # 从 config 的 1/0/2:2 解析
assert ch2.logical_channel == "1/0/2:2" # 使用 config 的值
assert ch2.rx_power_dbm == 1.07
# 验证两个 channel 的 port 一致(都是 1/0/2
assert ch1.logical_port == ch2.logical_port
def test_fallback_to_state_when_config_missing():
"""
测试当 config/description 不存在时fallback 到 state/description。
"""
xml = """\
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<data>
<components xmlns="http://openconfig.net/yang/platform">
<component>
<name>test-component</name>
<state><type>TRANSCEIVER</type></state>
<transceiver xmlns="http://openconfig.net/yang/platform/transceiver">
<physical-channels>
<channel>
<index>1</index>
<state>
<description>1/0/5:1</description>
</state>
</channel>
</physical-channels>
</transceiver>
</component>
</components>
</data>
</rpc-reply>
"""
_, channels = parse_netconf_response(xml, "device")
assert len(channels) == 1
ch = channels[0]
# 没有 config应该 fallback 到 state
assert ch.logical_port == "1/0/5"
assert ch.logical_channel == "1/0/5:1"