nm.bridge: get ports from sysfs

Unmanaged ports were not being shown when showing or editting the
bridge. In order to fix that, nmstate is retrieving the bridge ports
from sysfs instead of NetworkManager on-disk configuration.

Note that nmstate is not getting the VLAN port options from runtime.

Ref: https://bugzilla.redhat.com/1806452

Signed-off-by: Fernando Fernandez Mancera <ffmancera@riseup.net>
This commit is contained in:
Fernando Fernandez Mancera 2020-03-02 14:26:30 +01:00 committed by Edward Haas
parent 282f987152
commit ec8c9fa93b
2 changed files with 90 additions and 24 deletions

View File

@ -17,6 +17,9 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
import glob
import os
from libnmstate.nm import connection
from libnmstate.nm import nmclient
from libnmstate.nm.bridge_port_vlan import PortVlanFilter
@ -25,6 +28,12 @@ from libnmstate.schema import LinuxBridge as LB
BRIDGE_TYPE = "bridge"
BRIDGE_PORT_NMSTATE_TO_SYSFS = {
LB.Port.STP_HAIRPIN_MODE: "hairpin_mode",
LB.Port.STP_PATH_COST: "path_cost",
LB.Port.STP_PRIORITY: "priority",
}
def create_setting(bridge_state, base_con_profile):
options = bridge_state.get(BRIDGE_TYPE, {}).get(LB.OPTIONS_SUBTREE)
@ -129,11 +138,13 @@ def get_info(nmdev):
if not bridge_setting:
return info
port_profiles = _get_slave_profiles(nmdev)
port_profiles_by_name = _get_slave_profiles_by_name(nmdev)
port_names_sysfs = _get_slaves_names_from_sysfs(nmdev.get_iface())
props = bridge_setting.props
info[LB.CONFIG_SUBTREE] = {
LB.PORT_SUBTREE: _get_bridge_ports_info(
port_profiles,
port_profiles_by_name,
port_names_sysfs,
vlan_filtering_enabled=bridge_setting.get_vlan_filtering(),
),
LB.OPTIONS_SUBTREE: {
@ -165,36 +176,57 @@ def _get_bridge_setting(nmdev):
return bridge_setting
def _get_bridge_ports_info(port_profiles, vlan_filtering_enabled=False):
ports_info = []
for p in port_profiles:
port_info = _get_bridge_port_info(p)
def _get_bridge_ports_info(
port_profiles_by_name, port_names_sysfs, vlan_filtering_enabled=False
):
ports_info_by_name = {
name: _get_bridge_port_info(name) for name in port_names_sysfs
}
for name, p in port_profiles_by_name.items():
port_info = ports_info_by_name.get(name, {})
if port_info:
if vlan_filtering_enabled:
bridge_vlan_config = p.get_setting_bridge_port().props.vlans
port_vlan = PortVlanFilter()
port_vlan.import_from_bridge_settings(bridge_vlan_config)
port_info[LB.Port.VLAN_SUBTREE] = port_vlan.to_dict()
ports_info.append(port_info)
return ports_info
return list(ports_info_by_name.values())
def _get_bridge_port_info(port_profile):
"""Report port information."""
port_setting = port_profile.get_setting_bridge_port()
return {
LB.Port.NAME: port_profile.get_interface_name(),
LB.Port.STP_PRIORITY: port_setting.props.priority,
LB.Port.STP_HAIRPIN_MODE: port_setting.props.hairpin_mode,
LB.Port.STP_PATH_COST: port_setting.props.path_cost,
}
def _get_slave_profiles(master_device):
slave_profiles = []
def _get_slave_profiles_by_name(master_device):
slaves_profiles_by_name = {}
for dev in master_device.get_slaves():
active_con = connection.get_device_active_connection(dev)
if active_con:
slave_profiles.append(active_con.props.connection)
return slave_profiles
slaves_profiles_by_name[
dev.get_iface()
] = active_con.props.connection
return slaves_profiles_by_name
def _get_bridge_port_info(port_name):
"""Report port runtime information from sysfs."""
port = {LB.Port.NAME: port_name}
for option, option_sysfs in BRIDGE_PORT_NMSTATE_TO_SYSFS.items():
sysfs_path = f"/sys/class/net/{port_name}/brport/{option_sysfs}"
with open(sysfs_path) as f:
option_value = int(f.read())
if option == LB.Port.STP_HAIRPIN_MODE:
option_value = bool(option_value)
port[option] = option_value
return port
def _get_slaves_names_from_sysfs(master):
"""
We need to use glob in order to get the slaves name due to bug in
NetworkManager.
Ref: https://bugzilla.redhat.com/show_bug.cgi?id=1809547
"""
slaves = []
for sysfs_slave in glob.iglob(f"/sys/class/net/{master}/lower_*"):
# The format is lower_<iface>, we need to remove the "lower_" prefix
prefix_length = len("lower_")
slaves.append(os.path.basename(sysfs_slave)[prefix_length:])
return slaves

View File

@ -38,6 +38,7 @@ from .testlib.bridgelib import generate_vlan_filtering_config
from .testlib.bridgelib import generate_vlan_id_config
from .testlib.bridgelib import generate_vlan_id_range_config
from .testlib.bridgelib import linux_bridge
from .testlib.cmdlib import exec_cmd
from .testlib.ifacelib import get_mac_address
from .testlib.iproutelib import ip_monitor_assert_stable_link_up
from .testlib.statelib import show_only
@ -45,6 +46,7 @@ from .testlib.assertlib import assert_mac_address
from .testlib.vlan import vlan_interface
from .testlib.env import is_fedora
TEST_BRIDGE0 = "linux-br0"
@ -537,6 +539,38 @@ class TestVlanFiltering:
assertlib.assert_state_match(desired_state)
@pytest.fixture
def bridge_unmanaged_port():
bridge_config = _create_bridge_subtree_config([])
with linux_bridge(TEST_BRIDGE0, bridge_config):
with dummy0_as_slave(TEST_BRIDGE0):
current_state = show_only((TEST_BRIDGE0,))
yield current_state
def test_bridge_with_unmanaged_ports(bridge_unmanaged_port):
bridge_state = bridge_unmanaged_port[Interface.KEY][0]
port_subtree = bridge_state[LinuxBridge.CONFIG_SUBTREE][
LinuxBridge.PORT_SUBTREE
]
assert port_subtree[0][LinuxBridge.Port.NAME] == "dummy0"
@contextmanager
def dummy0_as_slave(master):
exec_cmd(("ip", "link", "add", "dummy0", "type", "dummy"), check=True)
try:
exec_cmd(("ip", "link", "set", "dummy0", "up"), check=True)
exec_cmd(
("nmcli", "dev", "set", "dummy0", "managed", "no"), check=True
)
exec_cmd(("ip", "link", "set", "dummy0", "master", master), check=True)
yield
finally:
exec_cmd(("ip", "link", "delete", "dummy0"))
def _add_port_to_bridge(bridge_state, ifname):
port_state = yaml.load(BRIDGE_PORT_YAML, Loader=yaml.SafeLoader)
add_port_to_bridge(bridge_state, ifname, port_state)