272 lines
8.5 KiB
Python
272 lines
8.5 KiB
Python
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:2,state 是错误的 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"
|
||
|