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:
parent
cc39341fff
commit
cfdc6cfbc5
@ -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}
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
Loading…
x
Reference in New Issue
Block a user