linux bridge: Add linux bridge options required by oVirt

Adding linux bridge options required by oVirt:

https://www.ovirt.org/documentation/admin-guide/appe-Custom_Network_Properties.html

New read only bridge options:
 * `LinuxBridge.Options.HELLO_TIMER`
 * `LinuxBridge.Options.GC_TIMER`

New read-writable bridge options:
 * `LinuxBridge.Options.GROUP_ADDR`
 * `LinuxBridge.Options.HASH_MAX`
 * `LinuxBridge.Options.MULTICAST_LAST_MEMBER_COUNT`
 * `LinuxBridge.Options.MULTICAST_LAST_MEMBER_INTERVAL`
 * `LinuxBridge.Options.MULTICAST_MEMBERSHIP_INTERVAL`
 * `LinuxBridge.Options.MULTICAST_QUERIER`
 * `LinuxBridge.Options.MULTICAST_QUERIER_INTERVAL`
 * `LinuxBridge.Options.MULTICAST_QUERY_USE_IFADDR`
 * `LinuxBridge.Options.MULTICAST_QUERY_INTERVAL`
 * `LinuxBridge.Options.MULTICAST_QUERY_RESPONSE_INTERVAL`
 * `LinuxBridge.Options.MULTICAST_STARTUP_QUERY_COUNT`
 * `LinuxBridge.Options.MULTICAST_STARTUP_QUERY_INTERVAL`

In order to allowing adding slave to bridge without bridge link change,
`nm/bridge.py` now only update bridge options to disk defined in desire
state.

NetworkManager does not support querying run time bridge options, hence
use sysfs folder to query all above bridge options.

Due to bug of NetworkManager, nm plugin does not support editing
`LB.Options.MULTICAST_ROUTER` yet:
    https://bugzilla.redhat.com/show_bug.cgi?id=1845608

Test cases added.

Signed-off-by: Gris Ge <fge@redhat.com>
This commit is contained in:
Gris Ge 2020-06-15 23:03:22 +08:00
parent cc39341fff
commit cfdc6cfbc5
9 changed files with 256 additions and 16 deletions

View File

@ -22,10 +22,16 @@
from operator import itemgetter
from libnmstate.schema import Bridge
from libnmstate.schema import LinuxBridge
from ..state import merge_dict
from .base_iface import BaseIface
READ_ONLY_OPTIONS = [
LinuxBridge.Options.HELLO_TIMER,
LinuxBridge.Options.GC_TIMER,
]
class BridgeIface(BaseIface):
BRPORT_OPTIONS_METADATA = "_brport_options"
@ -93,6 +99,7 @@ class BridgeIface(BaseIface):
def state_for_verify(self):
self._normalize_linux_bridge_port_vlan()
self._remove_read_only_bridge_options()
state = super().state_for_verify()
return state
@ -114,6 +121,12 @@ class BridgeIface(BaseIface):
if not port_config.get(Bridge.Port.VLAN_SUBTREE):
port_config[Bridge.Port.VLAN_SUBTREE] = {}
def _remove_read_only_bridge_options(self):
for key in READ_ONLY_OPTIONS:
self._bridge_config.get(LinuxBridge.OPTIONS_SUBTREE, {}).pop(
key, None
)
def _index_port_configs(port_configs):
return {port[Bridge.Port.NAME]: port for port in port_configs}

View File

@ -493,7 +493,9 @@ def _build_connection_profile(
if bridge_options or bridge_ports:
linux_bridge_setting = bridge.create_setting(
iface_desired_state, base_profile
iface_desired_state,
base_profile,
original_desired_iface_state,
)
settings.append(linux_bridge_setting)
elif iface_type == InterfaceType.OVS_BRIDGE:

View File

@ -24,6 +24,7 @@ from libnmstate.nm import connection
from libnmstate.nm.bridge_port_vlan import PortVlanFilter
from libnmstate.schema import LinuxBridge as LB
from .common import NM
from .common import nm_version_bigger_or_equal_to
BRIDGE_TYPE = "bridge"
@ -34,9 +35,56 @@ BRIDGE_PORT_NMSTATE_TO_SYSFS = {
LB.Port.STP_PRIORITY: "priority",
}
SYSFS_USER_HZ_KEYS = [
"forward_delay",
"ageing_time",
"hello_time",
"max_age",
]
def create_setting(bridge_state, base_con_profile):
options = bridge_state.get(BRIDGE_TYPE, {}).get(LB.OPTIONS_SUBTREE)
OPT = LB.Options
EXTRA_OPTIONS_MAP = {
OPT.HELLO_TIMER: "hello_timer",
OPT.GC_TIMER: "gc_timer",
OPT.MULTICAST_ROUTER: "multicast_router",
OPT.GROUP_ADDR: "group_addr",
OPT.HASH_MAX: "hash_max",
OPT.MULTICAST_LAST_MEMBER_COUNT: "multicast_last_member_count",
OPT.MULTICAST_LAST_MEMBER_INTERVAL: "multicast_last_member_interval",
OPT.MULTICAST_QUERIER: "multicast_querier",
OPT.MULTICAST_QUERIER_INTERVAL: "multicast_querier_interval",
OPT.MULTICAST_QUERY_USE_IFADDR: "multicast_query_use_ifaddr",
OPT.MULTICAST_QUERY_INTERVAL: "multicast_query_interval",
OPT.MULTICAST_QUERY_RESPONSE_INTERVAL: "multicast_query_response_interval",
OPT.MULTICAST_STARTUP_QUERY_COUNT: "multicast_startup_query_count",
OPT.MULTICAST_STARTUP_QUERY_INTERVAL: "multicast_startup_query_interval",
}
BOOL_OPTIONS = (OPT.MULTICAST_QUERIER, OPT.MULTICAST_QUERY_USE_IFADDR)
NM_BRIDGE_OPTIONS_MAP = {
OPT.GROUP_ADDR: "group_address",
OPT.HASH_MAX: "multicast_hash_max",
OPT.MULTICAST_LAST_MEMBER_COUNT: "multicast_last_member_count",
OPT.MULTICAST_LAST_MEMBER_INTERVAL: "multicast_last_member_interval",
OPT.MULTICAST_MEMBERSHIP_INTERVAL: "multicast_membership_interval",
OPT.MULTICAST_QUERIER: "multicast_querier",
OPT.MULTICAST_QUERIER_INTERVAL: "multicast_querier_interval",
OPT.MULTICAST_QUERY_USE_IFADDR: "multicast_query_use_ifaddr",
OPT.MULTICAST_QUERY_INTERVAL: "multicast_query_interval",
OPT.MULTICAST_QUERY_RESPONSE_INTERVAL: "multicast_query_response_interval",
OPT.MULTICAST_STARTUP_QUERY_COUNT: "multicast_startup_query_count",
OPT.MULTICAST_STARTUP_QUERY_INTERVAL: "multicast_startup_query_interval",
}
def create_setting(
bridge_state, base_con_profile, original_desired_iface_state
):
options = original_desired_iface_state.get(LB.CONFIG_SUBTREE, {}).get(
LB.OPTIONS_SUBTREE
)
bridge_setting = _get_current_bridge_setting(base_con_profile)
if not bridge_setting:
bridge_setting = NM.SettingBridge.new()
@ -68,6 +116,15 @@ def _set_bridge_properties(bridge_setting, options):
bridge_setting.props.multicast_snooping = val
elif key == LB.STP_SUBTREE:
_set_bridge_stp_properties(bridge_setting, val)
elif (
nm_version_bigger_or_equal_to("1.25.2")
and key in NM_BRIDGE_OPTIONS_MAP
):
nm_prop_name = NM_BRIDGE_OPTIONS_MAP[key]
# NM is using the sysfs name
if key == LB.Options.GROUP_ADDR:
val = val.lower()
setattr(bridge_setting.props, nm_prop_name, val)
def _set_bridge_stp_properties(bridge_setting, bridge_stp):
@ -140,7 +197,7 @@ def get_info(context, 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
props = _get_sysfs_bridge_options(nmdev.get_iface())
info[LB.CONFIG_SUBTREE] = {
LB.PORT_SUBTREE: _get_bridge_ports_info(
port_profiles_by_name,
@ -148,18 +205,27 @@ def get_info(context, nmdev):
vlan_filtering_enabled=bridge_setting.get_vlan_filtering(),
),
LB.OPTIONS_SUBTREE: {
LB.Options.MAC_AGEING_TIME: props.ageing_time,
LB.Options.GROUP_FORWARD_MASK: props.group_forward_mask,
LB.Options.MULTICAST_SNOOPING: props.multicast_snooping,
LB.Options.MAC_AGEING_TIME: props["ageing_time"],
LB.Options.GROUP_FORWARD_MASK: props["group_fwd_mask"],
LB.Options.MULTICAST_SNOOPING: props["multicast_snooping"] > 0,
LB.STP_SUBTREE: {
LB.STP.ENABLED: bridge_setting.props.stp,
LB.STP.PRIORITY: bridge_setting.props.priority,
LB.STP.FORWARD_DELAY: bridge_setting.props.forward_delay,
LB.STP.HELLO_TIME: bridge_setting.props.hello_time,
LB.STP.MAX_AGE: bridge_setting.props.max_age,
LB.STP.ENABLED: props["stp_state"] > 0,
LB.STP.PRIORITY: props["priority"],
LB.STP.FORWARD_DELAY: props["forward_delay"],
LB.STP.HELLO_TIME: props["hello_time"],
LB.STP.MAX_AGE: props["max_age"],
},
},
}
if nm_version_bigger_or_equal_to("1.25.2"):
for schema_name, sysfs_key_name in EXTRA_OPTIONS_MAP.items():
value = props[sysfs_key_name]
if schema_name == LB.Options.GROUP_ADDR:
value = value.upper()
elif schema_name in BOOL_OPTIONS:
value = value > 0
info[LB.CONFIG_SUBTREE][LB.OPTIONS_SUBTREE][schema_name] = value
return info
@ -230,3 +296,21 @@ def _get_slaves_names_from_sysfs(master):
prefix_length = len("lower_")
slaves.append(os.path.basename(sysfs_slave)[prefix_length:])
return slaves
def _get_sysfs_bridge_options(iface_name):
user_hz = os.sysconf("SC_CLK_TCK")
options = {}
for sysfs_file_path in glob.iglob(f"/sys/class/net/{iface_name}/bridge/*"):
key = os.path.basename(sysfs_file_path)
try:
with open(sysfs_file_path) as fd:
value = fd.read().rstrip("\n")
options[key] = value
options[key] = int(value, base=0)
except Exception:
pass
for key, value in options.items():
if key in SYSFS_USER_HZ_KEYS:
options[key] = int(value / user_hz)
return options

View File

@ -17,6 +17,8 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
from distutils.version import StrictVersion
import gi
try:
@ -30,6 +32,12 @@ from gi.repository import GObject
from gi.repository import Gio
def nm_version_bigger_or_equal_to(version):
return StrictVersion(
f"{NM.MAJOR_VERSION}.{NM.MINOR_VERSION}.{NM.MICRO_VERSION}"
) >= StrictVersion(version)
# To suppress the "import not used" error
NM
GLib

View File

@ -192,11 +192,32 @@ class Bridge:
class LinuxBridge(Bridge):
TYPE = "linux-bridge"
STP_SUBTREE = "stp"
MULTICAST_SUBTREE = "multicast"
class Options:
GROUP_FORWARD_MASK = "group-forward-mask"
MAC_AGEING_TIME = "mac-ageing-time"
MULTICAST_SNOOPING = "multicast-snooping"
GROUP_ADDR = "group-addr"
GROUP_FWD_MASK = "group-fwd-mask"
HASH_ELASTICITY = "hash-elasticity"
HASH_MAX = "hash-max"
MULTICAST_ROUTER = "multicast-router"
MULTICAST_LAST_MEMBER_COUNT = "multicast-last-member-count"
MULTICAST_LAST_MEMBER_INTERVAL = "multicast-last-member-interval"
MULTICAST_MEMBERSHIP_INTERVAL = "multicast-membership-interval"
MULTICAST_QUERIER = "multicast-querier"
MULTICAST_QUERIER_INTERVAL = "multicast-querier-interval"
MULTICAST_QUERY_USE_IFADDR = "multicast-query-use-ifaddr"
MULTICAST_QUERY_INTERVAL = "multicast-query-interval"
MULTICAST_QUERY_RESPONSE_INTERVAL = "multicast-query-response-interval"
MULTICAST_STARTUP_QUERY_COUNT = "multicast-startup-query-count"
MULTICAST_STARTUP_QUERY_INTERVAL = "multicast-startup-query-interval"
# Read only properties begin
HELLO_TIMER = "hello-timer"
GC_TIMER = "gc-timer"
# Read only properties end
class Port(Bridge.Port):
STP_HAIRPIN_MODE = "stp-hairpin-mode"

View File

@ -21,7 +21,7 @@ properties:
- "$ref": "#/definitions/interface-unknown/rw"
- "$ref": "#/definitions/interface-ethernet/rw"
- "$ref": "#/definitions/interface-bond/rw"
- "$ref": "#/definitions/interface-linux-bridge/rw"
- "$ref": "#/definitions/interface-linux-bridge/all"
- "$ref": "#/definitions/interface-ovs-bridge/all"
- "$ref": "#/definitions/interface-ovs-interface/rw"
- "$ref": "#/definitions/interface-dummy/rw"
@ -261,6 +261,22 @@ definitions:
options:
type: object
interface-linux-bridge:
all:
allOf:
- $ref: "#/definitions/interface-linux-bridge/rw"
- $ref: "#/definitions/interface-linux-bridge/ro"
ro:
properties:
bridge:
type: object
properties:
options:
type: object
properties:
gc-timer:
type: integer
hello-timer:
type: integer
rw:
properties:
type:
@ -306,8 +322,36 @@ definitions:
type: integer
group-forward-mask:
type: integer
group-addr:
$ref: "#/definitions/types/mac-address"
hash-max:
type: integer
multicast-snooping:
type: boolean
multicast-router:
type: integer
multicast-last-member-count:
type: integer
multicast-last-member-interval:
type: integer
multicast-membership-interval:
type: integer
multicast-querier:
type: boolean
multicast-querier-interval:
type: integer
multicast-query-use-ifaddr:
type: boolean
multicast-query-interval:
type: integer
multicast-query-response-interval:
type: integer
multicast-router:
type: integer
multicast-startup-query-count:
type: integer
multicast-startup-query-interval:
type: integer
stp:
type: object
properties:

View File

@ -47,6 +47,7 @@ from .testlib.statelib import show_only
from .testlib.assertlib import assert_mac_address
from .testlib.vlan import vlan_interface
from .testlib.env import is_fedora
from .testlib.env import is_nm_older_than_1_25_2
TEST_BRIDGE0 = "linux-br0"
@ -599,3 +600,23 @@ def _create_bridge_subtree_config(port_names):
add_port_to_bridge(bridge_state, port, port_state)
return bridge_state
@pytest.mark.tier1
@pytest.mark.xfail(
is_nm_older_than_1_25_2(),
reason=("Changing bridge group address is only supported by NM 1.25.2+"),
raises=NmstateVerificationError,
strict=True,
)
def test_change_linux_bridge_group_addr(bridge0_with_port0):
iface_state = bridge0_with_port0[Interface.KEY][0]
iface_state[LinuxBridge.CONFIG_SUBTREE][LinuxBridge.OPTIONS_SUBTREE][
LinuxBridge.Options.GROUP_ADDR
] = "01:80:C2:00:00:04"
desired_state = {Interface.KEY: [iface_state]}
libnmstate.apply(desired_state)
assertlib.assert_state_match(desired_state)

View File

@ -26,6 +26,7 @@ from libnmstate import nm
from libnmstate import schema
from libnmstate.schema import LinuxBridge as LB
from libnmstate.nm.common import NM
from libnmstate.nm.common import nm_version_bigger_or_equal_to
from ..testlib import iproutelib
from .testlib import main_context
@ -59,6 +60,7 @@ def test_create_and_remove_bridge(nm_plugin, port0_up):
bridge_desired_state = _create_bridge_config((port_name,))
with _bridge_interface(nm_plugin.context, bridge_desired_state):
bridge_current_state = _get_bridge_current_state(nm_plugin)
_remove_read_only_properties(bridge_current_state)
assert bridge_desired_state == bridge_current_state
assert not _get_bridge_current_state(nm_plugin)
@ -71,7 +73,11 @@ def test_add_port_to_existing_bridge(bridge0_with_port0, port1_up, nm_plugin):
_modify_bridge(nm_plugin.context, bridge0_with_port0)
assert bridge0_with_port0 == _get_bridge_current_state(nm_plugin)
current_state = _get_bridge_current_state(nm_plugin)
_remove_read_only_properties(current_state)
_remove_read_only_properties(bridge0_with_port0)
assert bridge0_with_port0 == current_state
def _add_ports_to_bridge_config(bridge_state, ports):
@ -81,7 +87,7 @@ def _add_ports_to_bridge_config(bridge_state, ports):
def _create_bridge_config(ports):
ports_states = _create_bridge_ports_config(ports)
return {
bridge_config = {
LB.CONFIG_SUBTREE: {
LB.OPTIONS_SUBTREE: {
LB.Options.GROUP_FORWARD_MASK: 0,
@ -100,6 +106,24 @@ def _create_bridge_config(ports):
LB.PORT_SUBTREE: ports_states,
}
}
if nm_version_bigger_or_equal_to("1.25.2"):
bridge_config[LB.CONFIG_SUBTREE][LB.OPTIONS_SUBTREE].update(
{
LB.Options.MULTICAST_ROUTER: 1,
LB.Options.GROUP_ADDR: "01:80:C2:00:00:00",
LB.Options.HASH_MAX: 4096,
LB.Options.MULTICAST_LAST_MEMBER_COUNT: 2,
LB.Options.MULTICAST_LAST_MEMBER_INTERVAL: 100,
LB.Options.MULTICAST_QUERIER: False,
LB.Options.MULTICAST_QUERIER_INTERVAL: 25500,
LB.Options.MULTICAST_QUERY_USE_IFADDR: False,
LB.Options.MULTICAST_QUERY_INTERVAL: 12500,
LB.Options.MULTICAST_QUERY_RESPONSE_INTERVAL: 1000,
LB.Options.MULTICAST_STARTUP_QUERY_COUNT: 2,
LB.Options.MULTICAST_STARTUP_QUERY_INTERVAL: 3125,
}
)
return bridge_config
def _create_bridge_ports_config(ports):
@ -222,7 +246,18 @@ def _create_iface_bridge_settings(bridge_state, base_con_profile=None):
iface_name=BRIDGE0,
iface_type=NM.SETTING_BRIDGE_SETTING_NAME,
)
bridge_setting = nm.bridge.create_setting(bridge_state, con_profile)
bridge_setting = nm.bridge.create_setting(
bridge_state, con_profile, bridge_state
)
ipv4_setting = nm.ipv4.create_setting({}, None)
ipv6_setting = nm.ipv6.create_setting({}, None)
return con_setting.setting, bridge_setting, ipv4_setting, ipv6_setting
def _remove_read_only_properties(bridge_state):
bridge_options = bridge_state.get(LB.CONFIG_SUBTREE, {}).get(
LB.OPTIONS_SUBTREE, {}
)
if bridge_options:
for key in (LB.Options.HELLO_TIMER, LB.Options.GC_TIMER):
bridge_options.pop(key, None)

View File

@ -17,8 +17,20 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
from distutils.version import StrictVersion
import os
import re
from .cmdlib import exec_cmd
def is_fedora():
return os.path.exists("/etc/fedora-release")
def is_nm_older_than_1_25_2():
_, output, _ = exec_cmd(["nmcli", "-v"], check=True)
match = re.compile("version ([0-9.]+)").search(output)
assert match
return StrictVersion(match.group(1)) < StrictVersion("1.25.2")