diff --git a/doc/nmstatectl.8 b/doc/nmstatectl.8
index 9f7fdbc..943c4dd 100644
--- a/doc/nmstatectl.8
+++ b/doc/nmstatectl.8
@@ -1,5 +1,5 @@
.\" Manpage for nmstatectl.
-.TH nmstatectl 8 "January 26, 2021" "1.0.2" "nmstatectl man page"
+.TH nmstatectl 8 "February 18, 2021" "1.0.2" "nmstatectl man page"
.SH NAME
nmstatectl \- A nmstate command line tool
.SH SYNOPSIS
@@ -13,6 +13,8 @@ nmstatectl \- A nmstate command line tool
.br
.B nmstatectl edit \fR[\fIINTERFACE_NAME\fR] [\fIOPTIONS\fR]
.br
+.B nmstatectl gc \fR[\fISTATE_FILE_PATH\fR] [\fIOPTIONS\fR]
+.br
.B nmstatectl rollback \fR[\fICHECKPOINT_PATH\fR]
.br
.B nmstatectl commit \fR[\fICHECKPOINT_PATH\fR]
@@ -89,6 +91,18 @@ decide whether rollback to previous (before \fB"nmstatectl set/edit"\fR) state.
rollback the network state from specified checkpoint file. \fBnmstatectl\fR
will take the latest checkpoint if not defined as argument.
.PP
+
+.B gc
+
+.RS
+Generates configuration files for specified network state file(s). The output
+will be dictinary with plugin name as key and an tuple as value.
+The tuple will holding configuration file name and configuration content.
+
+The generated configuration is not saved into system, users have to do it
+by themselves after refering to the network backend.
+.RE
+
.B commit
.RS
commit the current network state. \fBnmstatectl\fR will take the latest
diff --git a/doc/nmstatectl.8.in b/doc/nmstatectl.8.in
index d830ce6..b3c1a1e 100644
--- a/doc/nmstatectl.8.in
+++ b/doc/nmstatectl.8.in
@@ -13,6 +13,8 @@ nmstatectl \- A nmstate command line tool
.br
.B nmstatectl edit \fR[\fIINTERFACE_NAME\fR] [\fIOPTIONS\fR]
.br
+.B nmstatectl gc \fR[\fISTATE_FILE_PATH\fR] [\fIOPTIONS\fR]
+.br
.B nmstatectl rollback \fR[\fICHECKPOINT_PATH\fR]
.br
.B nmstatectl commit \fR[\fICHECKPOINT_PATH\fR]
@@ -89,6 +91,18 @@ decide whether rollback to previous (before \fB"nmstatectl set/edit"\fR) state.
rollback the network state from specified checkpoint file. \fBnmstatectl\fR
will take the latest checkpoint if not defined as argument.
.PP
+
+.B gc
+
+.RS
+Generates configuration files for specified network state file(s). The output
+will be dictinary with plugin name as key and an tuple as value.
+The tuple will holding configuration file name and configuration content.
+
+The generated configuration is not saved into system, users have to do it
+by themselves after refering to the network backend.
+.RE
+
.B commit
.RS
commit the current network state. \fBnmstatectl\fR will take the latest
diff --git a/examples/eth1_with_ieee_802_1x.yml b/examples/eth1_with_ieee_802_1x.yml
new file mode 100644
index 0000000..60466c5
--- /dev/null
+++ b/examples/eth1_with_ieee_802_1x.yml
@@ -0,0 +1,13 @@
+---
+interfaces:
+ - name: eth1
+ type: ethernet
+ state: up
+ 802.1x:
+ ca-cert: /etc/pki/802-1x-test/ca.crt
+ client-cert: /etc/pki/802-1x-test/client.example.org.crt
+ eap-methods:
+ - tls
+ identity: client.example.org
+ private-key: /etc/pki/802-1x-test/client.example.org.key
+ private-key-password: password
diff --git a/libnmstate/__init__.py b/libnmstate/__init__.py
index e11314e..f37cb1a 100644
--- a/libnmstate/__init__.py
+++ b/libnmstate/__init__.py
@@ -17,8 +17,6 @@
# along with this program. If not, see .
#
-import os
-
from . import error
from . import schema
@@ -27,28 +25,20 @@ from .netapplier import commit
from .netapplier import rollback
from .netinfo import show
from .netinfo import show_running_config
-
+from .nmstate import generate_configurations
from .prettystate import PrettyState
-
-
-ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
+from .version import get_version as _get_version
__all__ = [
"PrettyState",
"apply",
"commit",
"error",
+ "generate_configurations",
"rollback",
"schema",
"show",
"show_running_config",
]
-
-def _get_version():
- with open(os.path.join(ROOT_DIR, "VERSION")) as f:
- version = f.read().strip()
- return version
-
-
__version__ = _get_version()
diff --git a/libnmstate/dns.py b/libnmstate/dns.py
index af84368..1fb2cc8 100644
--- a/libnmstate/dns.py
+++ b/libnmstate/dns.py
@@ -40,9 +40,10 @@ class DnsState:
else:
self._dns_state = des_dns_state
self._validate()
- self._config_changed = _is_dns_config_changed(
- des_dns_state, cur_dns_state
- )
+ if cur_dns_state:
+ self._config_changed = _is_dns_config_changed(
+ des_dns_state, cur_dns_state
+ )
self._cur_dns_state = deepcopy(cur_dns_state) if cur_dns_state else {}
@property
diff --git a/libnmstate/ifaces/base_iface.py b/libnmstate/ifaces/base_iface.py
index 9ece18b..227c1d2 100644
--- a/libnmstate/ifaces/base_iface.py
+++ b/libnmstate/ifaces/base_iface.py
@@ -33,6 +33,7 @@ from libnmstate.schema import InterfaceType
from libnmstate.schema import InterfaceState
from libnmstate.schema import LLDP
from libnmstate.schema import OvsDB
+from libnmstate.schema import Ieee8021X
from ..state import state_match
from ..state import merge_dict
@@ -193,6 +194,11 @@ class BaseIface:
def mark_as_desired(self):
self._is_desired = True
+ def mark_as_absent_by_desire(self):
+ self.mark_as_desired()
+ self._info[Interface.STATE] = InterfaceState.ABSENT
+ self._origin_info[Interface.STATE] = InterfaceState.ABSENT
+
def to_dict(self):
return deepcopy(self._info)
@@ -434,12 +440,33 @@ class BaseIface:
def store_route_metadata(self, route_metadata):
for family, routes in route_metadata.items():
- self.raw[family][BaseIface.ROUTES_METADATA] = routes
+ try:
+ self.raw[family][BaseIface.ROUTES_METADATA] = routes
+ except KeyError:
+ self.raw[family] = {BaseIface.ROUTES_METADATA: routes}
def store_route_rule_metadata(self, route_rule_metadata):
for family, rules in route_rule_metadata.items():
self.raw[family][BaseIface.ROUTE_RULES_METADATA] = rules
+ @property
+ def copy_mac_from(self):
+ return self._info.get(Interface.COPY_MAC_FROM)
+
+ def apply_copy_mac_from(self, mac):
+ """
+ * Add MAC to original desire.
+ * Remove Interface.COPY_MAC_FROM from original desire.
+ * Update MAC of merge iface
+ """
+ self.raw[Interface.MAC] = mac
+ self._origin_info[Interface.MAC] = mac
+ self._origin_info.pop(Interface.COPY_MAC_FROM, None)
+
+ @property
+ def ieee_802_1x_conf(self):
+ return self.raw.get(Ieee8021X.CONFIG_SUBTREE, {})
+
def _remove_empty_description(state):
if state.get(Interface.DESCRIPTION) == "":
diff --git a/libnmstate/ifaces/bond.py b/libnmstate/ifaces/bond.py
index 8f8bd6e..138386e 100644
--- a/libnmstate/ifaces/bond.py
+++ b/libnmstate/ifaces/bond.py
@@ -260,6 +260,7 @@ _BOND_OPTIONS_NUMERIC_TO_NAMED_MAP = {
"layer2+3",
"encap2+3",
"encap3+4",
+ "vlan+srcmac",
),
}
diff --git a/libnmstate/ifaces/ethernet.py b/libnmstate/ifaces/ethernet.py
index 644fe6d..292b7bc 100644
--- a/libnmstate/ifaces/ethernet.py
+++ b/libnmstate/ifaces/ethernet.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2020 Red Hat, Inc.
+# Copyright (c) 2020-2021 Red Hat, Inc.
#
# This file is part of nmstate
#
@@ -26,6 +26,10 @@ from .base_iface import BaseIface
class EthernetIface(BaseIface):
+ def __init__(self, info, save_to_disk=True):
+ super().__init__(info, save_to_disk)
+ self._is_peer = False
+
def merge(self, other):
"""
Given the other_state, update the ethernet interfaces state base on
@@ -57,6 +61,10 @@ class EthernetIface(BaseIface):
.get(Ethernet.SRIOV.TOTAL_VFS, 0)
)
+ @property
+ def is_peer(self):
+ return self._is_peer
+
def create_sriov_vf_ifaces(self):
return [
EthernetIface(
@@ -74,6 +82,26 @@ class EthernetIface(BaseIface):
for i in range(0, self.sriov_total_vfs)
]
+ def remove_vfs_entry_when_total_vfs_decreased(self):
+ vfs_count = len(
+ self._info[Ethernet.CONFIG_SUBTREE]
+ .get(Ethernet.SRIOV_SUBTREE, {})
+ .get(Ethernet.SRIOV.VFS_SUBTREE, [])
+ )
+ if vfs_count > self.sriov_total_vfs:
+ [
+ self._info[Ethernet.CONFIG_SUBTREE][Ethernet.SRIOV_SUBTREE][
+ Ethernet.SRIOV.VFS_SUBTREE
+ ].pop()
+ for _ in range(self.sriov_total_vfs, vfs_count)
+ ]
+
+ def get_delete_vf_interface_names(self, old_sriov_total_vfs):
+ return [
+ f"{self.name}v{i}"
+ for i in range(self.sriov_total_vfs, old_sriov_total_vfs)
+ ]
+
def _capitalize_sriov_vf_mac(state):
vfs = (
diff --git a/libnmstate/ifaces/ifaces.py b/libnmstate/ifaces/ifaces.py
index d1dda80..44c9e61 100644
--- a/libnmstate/ifaces/ifaces.py
+++ b/libnmstate/ifaces/ifaces.py
@@ -81,9 +81,16 @@ class Ifaces:
also responsible to handle desire vs current state related tasks.
"""
- def __init__(self, des_iface_infos, cur_iface_infos, save_to_disk=True):
+ def __init__(
+ self,
+ des_iface_infos,
+ cur_iface_infos,
+ save_to_disk=True,
+ gen_conf_mode=False,
+ ):
self._save_to_disk = save_to_disk
self._des_iface_infos = des_iface_infos
+ self._gen_conf_mode = gen_conf_mode
self._cur_kernel_ifaces = {}
self._kernel_ifaces = {}
self._user_space_ifaces = _UserSpaceIfaces()
@@ -102,6 +109,8 @@ class Ifaces:
if des_iface_infos:
for iface_info in des_iface_infos:
iface = BaseIface(iface_info, save_to_disk)
+ if not iface.is_up and self._gen_conf_mode:
+ continue
if iface.type == InterfaceType.UNKNOWN:
cur_ifaces = self._get_cur_ifaces(iface.name)
if len(cur_ifaces) > 1:
@@ -119,6 +128,8 @@ class Ifaces:
if iface_info.get(Interface.TYPE) is None:
if cur_iface:
iface_info[Interface.TYPE] = cur_iface.type
+ elif gen_conf_mode:
+ iface_info[Interface.TYPE] = InterfaceType.ETHERNET
elif iface.is_up:
raise NmstateValueError(
f"Interface {iface.name} has no type defined "
@@ -146,16 +157,47 @@ class Ifaces:
self._create_virtual_port()
self._create_sriov_vfs_when_changed()
+ self._mark_vf_interface_as_absent_when_sriov_vf_decrease()
self._validate_unknown_port()
self._validate_unknown_parent()
self._validate_infiniband_as_bridge_port()
self._validate_infiniband_as_bond_port()
+ self._apply_copy_mac_from()
self._gen_metadata()
for iface in self.all_ifaces():
iface.pre_edit_validation_and_cleanup()
self._pre_edit_validation_and_cleanup()
+ def _apply_copy_mac_from(self):
+ for iface in self.all_kernel_ifaces.values():
+ if iface.type not in (
+ InterfaceType.LINUX_BRIDGE,
+ InterfaceType.BOND,
+ ):
+ continue
+ if not iface.copy_mac_from:
+ continue
+
+ if iface.copy_mac_from not in iface.port:
+ raise NmstateValueError(
+ f"The interface {iface.name} is holding invalid "
+ f"{Interface.COPY_MAC_FROM} property "
+ f"as {iface.copy_mac_from} is not in the port "
+ f"list: {iface.port}"
+ )
+ port_iface = self.all_kernel_ifaces.get(iface.copy_mac_from)
+ # TODO: bridge/bond might refering the mac from new veth in the
+ # same desire state, it too complex to support that.
+ if not port_iface:
+ raise NmstateValueError(
+ f"The interface {iface.name} is holding invalid "
+ f"{Interface.COPY_MAC_FROM} property "
+ f"as the port {iface.copy_mac_from} does not exists yet"
+ )
+
+ iface.apply_copy_mac_from(port_iface.mac)
+
def _create_virtual_port(self):
"""
Certain controller interface could have virtual port which does not
@@ -199,6 +241,29 @@ class Ifaces:
for new_iface in new_ifaces:
self._kernel_ifaces[new_iface.name] = new_iface
+ def _mark_vf_interface_as_absent_when_sriov_vf_decrease(self):
+ """
+ When SRIOV TOTAL_VFS decreased, we should mark certain VF interfaces
+ as absent and also remove the entry in `Ethernet.SRIOV.VFS_SUBTREE`.
+ """
+ for iface_name, iface in self._kernel_ifaces.items():
+ if iface.type != InterfaceType.ETHERNET or not iface.is_up:
+ continue
+ if iface_name not in self._cur_kernel_ifaces:
+ continue
+ cur_iface = self._cur_kernel_ifaces[iface_name]
+ if (
+ cur_iface.sriov_total_vfs != 0
+ and iface.sriov_total_vfs < cur_iface.sriov_total_vfs
+ ):
+ iface.remove_vfs_entry_when_total_vfs_decreased()
+ for vf_name in iface.get_delete_vf_interface_names(
+ cur_iface.sriov_total_vfs
+ ):
+ vf_iface = self._kernel_ifaces.get(vf_name)
+ if vf_iface:
+ vf_iface.mark_as_absent_by_desire()
+
def _pre_edit_validation_and_cleanup(self):
self._validate_over_booked_port()
self._validate_vlan_not_over_infiniband()
@@ -634,26 +699,59 @@ class Ifaces:
# All the user space interface already has interface type defined.
# And user space interface cannot be port of other interface.
# Hence no need to check `self._user_space_ifaces`
+ new_ifaces = {}
for iface in self._kernel_ifaces.values():
for port_name in iface.port:
if not self._kernel_ifaces.get(port_name):
- raise NmstateValueError(
- f"Interface {iface.name} has unknown port: {port_name}"
- )
+ if self._gen_conf_mode:
+ logging.warning(
+ f"Interface {port_name} does not exit in "
+ "desire state, assuming it is ethernet"
+ )
+ new_ifaces[port_name] = _to_specific_iface_obj(
+ {
+ Interface.NAME: port_name,
+ Interface.TYPE: InterfaceType.ETHERNET,
+ Interface.STATE: InterfaceState.UP,
+ },
+ self._save_to_disk,
+ )
+ else:
+ raise NmstateValueError(
+ f"Interface {iface.name} has unknown port: "
+ f"{port_name}"
+ )
+ self._kernel_ifaces.update(new_ifaces)
def _validate_unknown_parent(self):
"""
Check the existance of parent interface
"""
# All child interface should be in kernel space.
+ new_ifaces = {}
for iface in self._kernel_ifaces.values():
if iface.parent:
parent_iface = self._get_parent_iface(iface)
if not parent_iface:
- raise NmstateValueError(
- f"Interface {iface.name} has unknown parent: "
- f"{iface.parent}"
- )
+ if self._gen_conf_mode:
+ logging.warning(
+ f"Interface {iface.parent} does not exit in "
+ "desire state, assuming it is ethernet"
+ )
+ new_ifaces[iface.parent] = _to_specific_iface_obj(
+ {
+ Interface.NAME: iface.parent,
+ Interface.TYPE: InterfaceType.ETHERNET,
+ Interface.STATE: InterfaceState.UP,
+ },
+ self._save_to_disk,
+ )
+ else:
+ raise NmstateValueError(
+ f"Interface {iface.name} has unknown parent: "
+ f"{iface.parent}"
+ )
+ self._kernel_ifaces.update(new_ifaces)
def _remove_unknown_type_interfaces(self):
"""
diff --git a/libnmstate/ifaces/veth.py b/libnmstate/ifaces/veth.py
index 9e04085..ea30632 100644
--- a/libnmstate/ifaces/veth.py
+++ b/libnmstate/ifaces/veth.py
@@ -17,6 +17,7 @@
# along with this program. If not, see .
#
+from libnmstate.schema import InterfaceType
from libnmstate.schema import Veth
from .ethernet import EthernetIface
@@ -25,7 +26,6 @@ from .ethernet import EthernetIface
class VethIface(EthernetIface):
def __init__(self, info, save_to_disk=True):
super().__init__(info, save_to_disk)
- self._is_peer = False
self._peer_changed = False
@property
@@ -37,10 +37,6 @@ class VethIface(EthernetIface):
return self.raw.get(Veth.CONFIG_SUBTREE, {}).get(Veth.PEER)
@property
- def is_peer(self):
- return self._is_peer
-
- @property
def is_peer_changed(self):
return self._peer_changed
@@ -54,9 +50,12 @@ class VethIface(EthernetIface):
and not ifaces.get_cur_iface(self.name, self.type)
):
for iface in ifaces.all_ifaces():
- if iface.name == self.peer and iface.type == self.type:
- if not iface.is_peer:
- self._is_peer = True
+ if iface.name == self.peer and (
+ self.type == iface.type
+ or iface.type == InterfaceType.ETHERNET
+ ):
+ if not self.is_peer:
+ iface._is_peer = True
def _mark_peer_changed(self, ifaces):
if self.is_up:
diff --git a/libnmstate/net_state.py b/libnmstate/net_state.py
index a21c501..5dc7b43 100644
--- a/libnmstate/net_state.py
+++ b/libnmstate/net_state.py
@@ -34,13 +34,20 @@ from .state import state_match
class NetState:
- def __init__(self, desire_state, current_state=None, save_to_disk=True):
+ def __init__(
+ self,
+ desire_state,
+ current_state=None,
+ save_to_disk=True,
+ gen_conf_mode=False,
+ ):
if current_state is None:
current_state = {}
self._ifaces = Ifaces(
desire_state.get(Interface.KEY),
current_state.get(Interface.KEY),
save_to_disk,
+ gen_conf_mode,
)
self._route = RouteState(
self._ifaces,
diff --git a/libnmstate/netapplier.py b/libnmstate/netapplier.py
index 24df4d5..cf208d1 100644
--- a/libnmstate/netapplier.py
+++ b/libnmstate/netapplier.py
@@ -18,6 +18,7 @@
#
import copy
+import logging
import time
@@ -31,6 +32,7 @@ from .nmstate import plugins_capabilities
from .nmstate import rollback_checkpoints
from .nmstate import show_with_plugins
from .net_state import NetState
+from .version import get_version
MAINLOOP_TIMEOUT = 35
VERIFY_RETRY_INTERNAL = 1
@@ -59,6 +61,9 @@ def apply(
:returns: Checkpoint identifier
:rtype: str
"""
+ logging.debug(f"Nmstate version: {get_version()}")
+ logging.debug(f"Applying desire state: {desired_state}")
+
desired_state = copy.deepcopy(desired_state)
with plugin_context() as plugins:
validator.schema_validate(desired_state)
diff --git a/libnmstate/nispor/plugin.py b/libnmstate/nispor/plugin.py
index 929a6db..dc0ea76 100644
--- a/libnmstate/nispor/plugin.py
+++ b/libnmstate/nispor/plugin.py
@@ -17,6 +17,8 @@
# along with this program. If not, see .
#
+import logging
+
from nispor import NisporNetState
from libnmstate.plugin import NmstatePlugin
@@ -34,6 +36,7 @@ from .veth import NisporPluginVethIface
from .vlan import NisporPluginVlanIface
from .vxlan import NisporPluginVxlanIface
from .route import nispor_route_state_to_nmstate
+from .route import nispor_route_state_to_nmstate_static
from .route_rule import nispor_route_rule_state_to_nmstate
from .vrf import NisporPluginVrfIface
from .ovs import NisporPluginOvsInternalIface
@@ -133,7 +136,12 @@ class NisporPlugin(NmstatePlugin):
def get_routes(self):
np_state = NisporNetState.retrieve()
- return {Route.RUNNING: nispor_route_state_to_nmstate(np_state.routes)}
+ return {
+ Route.RUNNING: nispor_route_state_to_nmstate(np_state.routes),
+ Route.CONFIG: nispor_route_state_to_nmstate_static(
+ np_state.routes
+ ),
+ }
def get_route_rules(self):
np_state = NisporNetState.retrieve()
@@ -142,3 +150,20 @@ class NisporPlugin(NmstatePlugin):
np_state.route_rules
)
}
+
+ def apply_changes(self, net_state, _save_to_disk):
+ """
+ Simply provide debug message on current network status and desired
+ status.
+ """
+ np_state = NisporNetState.retrieve()
+ logging.debug(f"Nispor: current network state {np_state}")
+ for iface in net_state.ifaces.all_ifaces():
+ if iface.is_desired:
+ logging.debug(
+ f"Nispor: desired network state {iface.to_dict()}"
+ )
+ elif iface.is_changed:
+ logging.debug(
+ f"Nispor: changed network state {iface.to_dict()}"
+ )
diff --git a/libnmstate/nispor/route.py b/libnmstate/nispor/route.py
index f4a445d..510ddc3 100644
--- a/libnmstate/nispor/route.py
+++ b/libnmstate/nispor/route.py
@@ -23,12 +23,27 @@ from libnmstate.schema import Route
IPV4_DEFAULT_GATEWAY_DESTINATION = "0.0.0.0/0"
IPV6_DEFAULT_GATEWAY_DESTINATION = "::/0"
+LOCAL_ROUTE_TABLE = 255
+
def nispor_route_state_to_nmstate(np_routes):
return [
_nispor_route_to_nmstate(rt)
for rt in np_routes
- if rt.scope == "universe"
+ if rt.scope in ["universe", "link"]
+ and rt.oif != "lo"
+ and rt.table != LOCAL_ROUTE_TABLE
+ ]
+
+
+def nispor_route_state_to_nmstate_static(np_routes):
+ return [
+ _nispor_route_to_nmstate(rt)
+ for rt in np_routes
+ if rt.scope in ["universe", "link"]
+ and rt.protocol not in ["kernel", "ra", "dhcp"]
+ and rt.oif != "lo"
+ and rt.table != LOCAL_ROUTE_TABLE
]
diff --git a/libnmstate/nm/connection.py b/libnmstate/nm/connection.py
index e25c9b9..7d26486 100644
--- a/libnmstate/nm/connection.py
+++ b/libnmstate/nm/connection.py
@@ -36,6 +36,7 @@ from .bridge import BRIDGE_TYPE as NM_LINUX_BRIDGE_TYPE
from .bridge import create_port_setting as create_linux_bridge_port_setting
from .bridge import create_setting as create_linux_bridge_setting
from .common import NM
+from .ieee_802_1x import create_802_1x_setting
from .infiniband import create_setting as create_infiniband_setting
from .ipv4 import create_setting as create_ipv4_setting
from .ipv6 import create_setting as create_ipv6_setting
@@ -60,7 +61,7 @@ class _ConnectionSetting:
def __init__(self, con_setting=None):
self._setting = con_setting
- def create(self, con_name, iface_name, iface_type):
+ def create(self, con_name, iface_name, iface_type, is_controller):
con_setting = NM.SettingConnection.new()
con_setting.props.id = con_name
con_setting.props.interface_name = iface_name
@@ -69,11 +70,13 @@ class _ConnectionSetting:
con_setting.props.autoconnect = True
con_setting.props.autoconnect_slaves = (
NM.SettingConnectionAutoconnectSlaves.YES
+ if is_controller
+ else NM.SettingConnectionAutoconnectSlaves.DEFAULT
)
self._setting = con_setting
- def import_by_profile(self, profile):
+ def import_by_profile(self, profile, is_controller):
base = profile.get_setting_connection()
new = NM.SettingConnection.new()
new.props.id = base.props.id
@@ -81,7 +84,11 @@ class _ConnectionSetting:
new.props.uuid = base.props.uuid
new.props.type = base.props.type
new.props.autoconnect = True
- new.props.autoconnect_slaves = base.props.autoconnect_slaves
+ new.props.autoconnect_slaves = (
+ NM.SettingConnectionAutoconnectSlaves.YES
+ if is_controller
+ else NM.SettingConnectionAutoconnectSlaves.DEFAULT
+ )
self._setting = new
@@ -106,11 +113,13 @@ def create_new_nm_simple_conn(iface, nm_profile):
create_ipv6_setting(iface_info.get(Interface.IPV6), nm_profile),
]
con_setting = _ConnectionSetting()
- if nm_profile:
- con_setting.import_by_profile(nm_profile)
+ if nm_profile and not is_multiconnect_profile(nm_profile):
+ con_setting.import_by_profile(nm_profile, iface.is_controller)
con_setting.set_profile_name(iface.name)
else:
- con_setting.create(iface.name, iface.name, nm_iface_type)
+ con_setting.create(
+ iface.name, iface.name, nm_iface_type, iface.is_controller
+ )
apply_lldp_setting(con_setting, iface_info)
@@ -224,6 +233,9 @@ def create_new_nm_simple_conn(iface, nm_profile):
if nm_setting:
settings.append(nm_setting)
+ if iface.ieee_802_1x_conf:
+ settings.append(create_802_1x_setting(iface.ieee_802_1x_conf))
+
nm_simple_conn = NM.SimpleConnection.new()
for setting in settings:
nm_simple_conn.add_setting(setting)
@@ -266,3 +278,12 @@ def nm_simple_conn_update_parent(nm_simple_conn, iface_type, parent):
f"shold not need parent"
)
nm_setting.props.parent = parent
+
+
+def is_multiconnect_profile(nm_profile):
+ nm_setting = nm_profile.get_setting_connection()
+ return (
+ nm_setting
+ and nm_setting.get_multi_connect()
+ == NM.ConnectionMultiConnect.MULTIPLE
+ )
diff --git a/libnmstate/nm/ieee_802_1x.py b/libnmstate/nm/ieee_802_1x.py
new file mode 100644
index 0000000..85e1b67
--- /dev/null
+++ b/libnmstate/nm/ieee_802_1x.py
@@ -0,0 +1,131 @@
+#
+# Copyright (c) 2021 Red Hat, Inc.
+#
+# This file is part of nmstate
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see .
+#
+
+from libnmstate.error import NmstateLibnmError
+from libnmstate.schema import Ieee8021X
+
+from .common import NM
+from .common import GLib
+
+_NM_PROP_MAP = {
+ NM.SETTING_802_1X_IDENTITY: Ieee8021X.IDENTITY,
+ NM.SETTING_802_1X_EAP: Ieee8021X.EAP_METHODS,
+ NM.SETTING_802_1X_PRIVATE_KEY: Ieee8021X.PRIVATE_KEY,
+ NM.SETTING_802_1X_CLIENT_CERT: Ieee8021X.CLIENT_CERT,
+ NM.SETTING_802_1X_CA_CERT: Ieee8021X.CA_CERT,
+}
+
+
+def get_802_1x_info(context, nm_ac):
+ if not nm_ac:
+ return {}
+ nm_profile = nm_ac.get_connection()
+ if not nm_profile:
+ return {}
+ nm_setting = nm_profile.get_setting_802_1x()
+ if not nm_setting:
+ return {}
+
+ secrets = _get_secrets(context, nm_profile)
+
+ info = {}
+ for nm_prop, key_name in _NM_PROP_MAP.items():
+ value = getattr(nm_setting.props, nm_prop)
+ if isinstance(value, GLib.Bytes):
+ value = _file_path_from_glib_bytes(value)
+ info[key_name] = value
+
+ if secrets.get(NM.SETTING_802_1X_PRIVATE_KEY_PASSWORD):
+ info[Ieee8021X.PRIVATE_KEY_PASSWORD] = secrets[
+ NM.SETTING_802_1X_PRIVATE_KEY_PASSWORD
+ ]
+
+ return {Ieee8021X.CONFIG_SUBTREE: info}
+
+
+def create_802_1x_setting(ieee_802_1x_conf):
+ nm_setting = NM.Setting8021x.new()
+
+ for nm_prop, key_name in (
+ (NM.SETTING_802_1X_IDENTITY, Ieee8021X.IDENTITY),
+ (NM.SETTING_802_1X_EAP, Ieee8021X.EAP_METHODS),
+ (
+ NM.SETTING_802_1X_PRIVATE_KEY_PASSWORD,
+ Ieee8021X.PRIVATE_KEY_PASSWORD,
+ ),
+ ):
+ if key_name in ieee_802_1x_conf:
+ setattr(nm_setting.props, nm_prop, ieee_802_1x_conf[key_name])
+
+ for nm_prop, key_name in (
+ (NM.SETTING_802_1X_PRIVATE_KEY, Ieee8021X.PRIVATE_KEY),
+ (NM.SETTING_802_1X_CA_CERT, Ieee8021X.CA_CERT),
+ (NM.SETTING_802_1X_CLIENT_CERT, Ieee8021X.CLIENT_CERT),
+ ):
+ if key_name in ieee_802_1x_conf:
+ setattr(
+ nm_setting.props,
+ nm_prop,
+ _file_path_to_glib_bytes(ieee_802_1x_conf[key_name]),
+ )
+
+ return nm_setting
+
+
+def _file_path_from_glib_bytes(nm_bytes):
+ file_path = nm_bytes.get_data().decode("utf8")
+ if file_path.startswith("file://"):
+ # Black is conflicting with flake for below line:
+ # https://github.com/psf/black/issues/315
+ file_path = file_path[len("file://") : (-len("\\0") + 1)] # noqa: E203
+
+ return file_path
+
+
+def _file_path_to_glib_bytes(file_path):
+ file_path_bytes = bytearray(f"file://{file_path}".encode("utf8"))
+ file_path_bytes.append(0)
+ return GLib.Bytes.new(file_path_bytes)
+
+
+def _get_secrets(context, nm_profile):
+ secrets = {}
+ action = f"Retrieve 802.1x secrets of profile {nm_profile.get_uuid()}"
+ context.register_async(action, fast=True)
+ user_data = (context, secrets, action)
+ nm_profile.get_secrets_async(
+ NM.SETTING_802_1X_SETTING_NAME,
+ context.cancellable,
+ _get_secrets_callback,
+ user_data,
+ )
+ context.wait_all_finish()
+ return secrets
+
+
+def _get_secrets_callback(nm_profile, result, user_data):
+ context, secrets, action = user_data
+
+ try:
+ nm_secrets = nm_profile.get_secrets_finish(result)
+ except GLib.Error as e:
+ context.fail(NmstateLibnmError(f"{action} failed: error={e}"))
+
+ context.finish_async(action)
+ secrets.update(nm_secrets[NM.SETTING_802_1X_SETTING_NAME])
diff --git a/libnmstate/nm/plugin.py b/libnmstate/nm/plugin.py
index 19297db..302b4cc 100644
--- a/libnmstate/nm/plugin.py
+++ b/libnmstate/nm/plugin.py
@@ -21,13 +21,13 @@ import logging
from operator import itemgetter
from libnmstate.error import NmstateDependencyError
+from libnmstate.error import NmstateNotSupportedError
from libnmstate.error import NmstateValueError
from libnmstate.ifaces.ovs import is_ovs_running
from libnmstate.schema import DNS
from libnmstate.schema import Interface
from libnmstate.schema import InterfaceType
from libnmstate.schema import LLDP
-from libnmstate.schema import Route
from libnmstate.plugin import NmstatePlugin
@@ -50,20 +50,19 @@ from .ovs import get_ovsdb_external_ids
from .ovs import has_ovs_capability
from .profiles import NmProfiles
from .profiles import get_all_applied_configs
-from .route import get_running_config as get_route_running_config
from .team import get_info as get_team_info
from .team import has_team_capability
from .translator import Nm2Api
from .user import get_info as get_user_info
from .veth import get_current_veth_type
from .wired import get_info as get_wired_info
+from .ieee_802_1x import get_802_1x_info
class NetworkManagerPlugin(NmstatePlugin):
def __init__(self):
- self._ctx = NmContext()
+ self._ctx = None
self._checkpoint = None
- self._check_version_mismatch()
self.__applied_configs = None
@property
@@ -91,10 +90,13 @@ class NetworkManagerPlugin(NmstatePlugin):
@property
def client(self):
- return self._ctx.client if self._ctx else None
+ return self.context.client if self.context else None
@property
def context(self):
+ if not self._ctx:
+ self._ctx = NmContext()
+ self._check_version_mismatch()
return self._ctx
@property
@@ -129,6 +131,13 @@ class NetworkManagerPlugin(NmstatePlugin):
if not dev.get_managed():
# Skip unmanaged interface
continue
+ nm_ac = dev.get_active_connection()
+ if (
+ nm_ac
+ and nm_ac.get_state_flags() & NM.ActivationStateFlags.EXTERNAL
+ ):
+ # Skip external managed interface
+ continue
iface_info = Nm2Api.get_common_device_info(devinfo)
applied_config = applied_configs.get(iface_info[Interface.NAME])
@@ -143,6 +152,7 @@ class NetworkManagerPlugin(NmstatePlugin):
iface_info.update(get_infiniband_info(applied_config))
iface_info.update(get_current_macvlan_type(applied_config))
iface_info.update(get_current_veth_type(applied_config))
+ iface_info.update(get_802_1x_info(self.context, act_con))
if iface_info[Interface.TYPE] == InterfaceType.OVS_BRIDGE:
iface_info.update(get_ovs_bridge_info(dev))
@@ -172,7 +182,7 @@ class NetworkManagerPlugin(NmstatePlugin):
return iface_infos
def get_routes(self):
- return {Route.CONFIG: get_route_running_config(self._applied_configs)}
+ return {}
def get_route_rules(self):
"""
@@ -198,26 +208,26 @@ class NetworkManagerPlugin(NmstatePlugin):
# Old checkpoint might timeout, hence it's legal to load
# another one.
self._checkpoint.clean_up()
- candidates = get_checkpoints(self._ctx.client)
+ candidates = get_checkpoints(self.client)
if checkpoint_path in candidates:
self._checkpoint = CheckPoint(
- nm_context=self._ctx, dbuspath=checkpoint_path
+ nm_context=self.context, dbuspath=checkpoint_path
)
else:
raise NmstateValueError("No checkpoint specified or found")
else:
if not self._checkpoint:
# Get latest one
- candidates = get_checkpoints(self._ctx.client)
+ candidates = get_checkpoints(self.client)
if candidates:
self._checkpoint = CheckPoint(
- nm_context=self._ctx, dbuspath=candidates[0]
+ nm_context=self.context, dbuspath=candidates[0]
)
else:
raise NmstateValueError("No checkpoint specified or found")
def create_checkpoint(self, timeout=60):
- self._checkpoint = CheckPoint.create(self._ctx, timeout)
+ self._checkpoint = CheckPoint.create(self.context, timeout)
return str(self._checkpoint)
def rollback_checkpoint(self, checkpoint=None):
@@ -231,7 +241,7 @@ class NetworkManagerPlugin(NmstatePlugin):
self._checkpoint = None
def _check_version_mismatch(self):
- nm_client_version = self._ctx.client.get_version()
+ nm_client_version = self.client.get_version()
nm_utils_version = _nm_utils_decode_version()
if nm_client_version is None:
@@ -248,6 +258,16 @@ class NetworkManagerPlugin(NmstatePlugin):
nm_client_version,
)
+ logging.debug(f"NetworkManager version {nm_client_version}")
+
+ def generate_configurations(self, net_state):
+ if not hasattr(NM, "keyfile_write"):
+ raise NmstateNotSupportedError(
+ f"Current NetworkManager version does not support generating "
+ "configurations, please upgrade to 1.30 or later versoin."
+ )
+ return NmProfiles(None).generate_config_strings(net_state)
+
def _remove_ovs_bridge_unsupported_entries(iface_info):
"""
diff --git a/libnmstate/nm/profile.py b/libnmstate/nm/profile.py
index 7f8e277..b4814d9 100644
--- a/libnmstate/nm/profile.py
+++ b/libnmstate/nm/profile.py
@@ -33,21 +33,23 @@ from libnmstate.schema import InterfaceType
from .active_connection import ActiveConnectionDeactivate
from .active_connection import ProfileActivation
from .active_connection import is_activated
-from .common import Gio
from .common import GLib
+from .common import Gio
from .common import NM
from .connection import create_new_nm_simple_conn
+from .connection import is_multiconnect_profile
from .connection import nm_simple_conn_update_controller
from .connection import nm_simple_conn_update_parent
-from .device import get_nm_dev
-from .device import DeviceReapply
from .device import DeviceDelete
+from .device import DeviceReapply
+from .device import get_nm_dev
from .translator import Api2Nm
IMPORT_NM_DEV_TIMEOUT = 5
IMPORT_NM_DEV_RETRY_INTERNAL = 0.5
FALLBACK_CHECKER_INTERNAL = 15
+IPV6_ROUTE_REMOVED = "_ipv6_route_removed"
class NmProfile:
@@ -87,10 +89,9 @@ class NmProfile:
ACTION_DELETE_DEVICE,
)
- def __init__(self, ctx, iface, save_to_disk):
+ def __init__(self, ctx, iface):
self._ctx = ctx
self._iface = iface
- self._save_to_disk = save_to_disk
self._nm_iface_type = None
if self._iface.type != InterfaceType.UNKNOWN:
self._nm_iface_type = Api2Nm.get_iface_type(self._iface.type)
@@ -103,9 +104,6 @@ class NmProfile:
self._deactivated = False
self._profile_deleted = False
self._device_deleted = False
- self._import_current()
- self._gen_actions()
- self._gen_nm_sim_conn()
@property
def iface(self):
@@ -118,6 +116,18 @@ class NmProfile:
) and not self.iface.is_ignore
@property
+ def config_file_name(self):
+ """
+ Return the profile file name used NetworkManager for key-file mode.
+ """
+ if self._nm_simple_conn:
+ return f"{self._nm_simple_conn.get_id()}.nmconnection"
+ elif self._nm_profile:
+ return f"{self._nm_profile.get_id()}.nmconnection"
+ else:
+ return ""
+
+ @property
def uuid(self):
if self._nm_simple_conn:
return self._nm_simple_conn.get_uuid()
@@ -134,6 +144,18 @@ class NmProfile:
self._nm_simple_conn, self.iface.type, parent
)
+ def to_key_file_string(self):
+ nm_simple_conn = create_new_nm_simple_conn(
+ self._iface, nm_profile=None
+ )
+ nm_simple_conn.normalize()
+ # pylint: disable=no-member
+ key_file = NM.keyfile_write(
+ nm_simple_conn, NM.KeyfileHandlerFlags.NONE, None, None
+ )
+ # pylint: enable=no-member
+ return key_file.to_data()[0]
+
def _gen_actions(self):
if not self.has_pending_change:
return
@@ -141,7 +163,7 @@ class NmProfile:
self._add_action(NmProfile.ACTION_DELETE_PROFILE)
if self._iface.is_virtual and self._nm_dev:
self._add_action(NmProfile.ACTION_DELETE_DEVICE)
- elif self._iface.is_up and self._iface.type != InterfaceType.VETH:
+ elif self._iface.is_up and not self._needs_veth_activation():
self._add_action(NmProfile.ACTION_MODIFIED)
if not self._nm_dev:
if self._iface.type == InterfaceType.OVS_PORT:
@@ -161,13 +183,18 @@ class NmProfile:
elif self._iface.is_virtual and self._nm_dev:
self._add_action(NmProfile.ACTION_DELETE_DEVICE)
+ if self._iface.raw.get(IPV6_ROUTE_REMOVED):
+ # This is a workaround for NM bug:
+ # https://bugzilla.redhat.com/1837254
+ self._add_action(NmProfile.ACTION_DEACTIVATE_FIRST)
+
if self._iface.is_controller and self._iface.is_up:
if self._iface.controller:
self._add_action(NmProfile.ACTION_OTHER_MASTER)
else:
self._add_action(NmProfile.ACTION_TOP_MASTER)
- if self._iface.is_up and self._iface.type == InterfaceType.VETH:
+ if self._iface.is_up and self._needs_veth_activation():
if self._iface.is_peer:
self._add_action(NmProfile.ACTION_NEW_VETH_PEER)
else:
@@ -216,18 +243,22 @@ class NmProfile:
# settings.
self._add_action(NmProfile.ACTION_ACTIVATE_FIRST)
- def _gen_nm_sim_conn(self):
+ def _needs_veth_activation(self):
+ return self._iface.type == InterfaceType.VETH or (
+ self._iface.type == InterfaceType.ETHERNET and self._iface.is_peer
+ )
+
+ def prepare_config(self, save_to_disk, gen_conf_mode=False):
if self._iface.is_absent or self._iface.is_down:
return
- self._check_sriov_support()
- self._check_unsupported_memory_only()
# Don't create new profile if original desire does not ask
# anything besides state:up and not been marked as changed.
# We don't need to do this once we support querying on-disk
# configure
if (
- self._nm_profile is None
+ not gen_conf_mode
+ and self._nm_profile is None
and not self._iface.is_changed
and set(self._iface.original_dict)
<= set([Interface.STATE, Interface.NAME, Interface.TYPE])
@@ -235,7 +266,7 @@ class NmProfile:
cur_nm_profile = self._get_first_nm_profile()
if (
cur_nm_profile
- and _is_memory_only(cur_nm_profile) != self._save_to_disk
+ and _is_memory_only(cur_nm_profile) != save_to_disk
):
self._nm_profile = cur_nm_profile
self._nm_simple_conn = cur_nm_profile
@@ -248,7 +279,10 @@ class NmProfile:
self._iface, self._nm_profile
)
- def save_config(self):
+ def save_config(self, save_to_disk):
+ self._check_sriov_support()
+ self._check_unsupported_memory_only(save_to_disk)
+ self._gen_actions()
if not self.has_pending_change:
return
if self._iface.is_absent or self._iface.is_down:
@@ -256,37 +290,37 @@ class NmProfile:
# Don't create new profile if original desire does not ask
# anything besides state:up and not been marked as changed.
# We don't need to do this once we support querying on-disk
- # configure
if self._nm_simple_conn == self._nm_profile:
cur_nm_profile = self._get_first_nm_profile()
if (
cur_nm_profile
- and _is_memory_only(cur_nm_profile) != self._save_to_disk
+ and _is_memory_only(cur_nm_profile) != save_to_disk
):
self._nm_profile = cur_nm_profile
return
- if self._nm_profile:
+ if self._nm_profile and not is_multiconnect_profile(self._nm_profile):
ProfileUpdate(
self._ctx,
self._iface.name,
self._iface.type,
self._nm_simple_conn,
self._nm_profile,
- self._save_to_disk,
+ save_to_disk,
).run()
else:
+ self._nm_profile = None
ProfileAdd(
self._ctx,
self._iface.name,
self._iface.type,
self._nm_simple_conn,
- self._save_to_disk,
+ save_to_disk,
).run()
- def _check_unsupported_memory_only(self):
+ def _check_unsupported_memory_only(self, save_to_disk):
if (
- not self._save_to_disk
+ not save_to_disk
and StrictVersion(self._ctx.client.get_version())
< StrictVersion("1.28.0")
and self._iface.type
@@ -369,7 +403,7 @@ class NmProfile:
def _delete_device(self):
if self._device_deleted:
return
- self._import_current()
+ self.import_current()
if self._nm_dev:
DeviceDelete(
self._ctx, self._iface.name, self._iface.type, self._nm_dev
@@ -435,7 +469,7 @@ class NmProfile:
else:
time.sleep(IMPORT_NM_DEV_RETRY_INTERNAL)
- def _import_current(self):
+ def import_current(self):
self._nm_dev = get_nm_dev(
self._ctx, self._iface.name, self._iface.type
)
@@ -450,6 +484,7 @@ class NmProfile:
for nm_profile in self._ctx.client.get_connections():
if nm_profile.get_uuid() == self._nm_simple_conn.get_uuid():
self._nm_profile = nm_profile
+ break
def _get_first_nm_profile(self):
for nm_profile in self._ctx.client.get_connections():
@@ -469,8 +504,10 @@ class NmProfile:
return
if self._iface.is_down:
return
- self._import_current()
+ self.import_current()
for nm_profile in self._ctx.client.get_connections():
+ if is_multiconnect_profile(nm_profile):
+ continue
if (
nm_profile.get_interface_name() == self._iface.name
and (
diff --git a/libnmstate/nm/profiles.py b/libnmstate/nm/profiles.py
index 88a5b0d..905a6c8 100644
--- a/libnmstate/nm/profiles.py
+++ b/libnmstate/nm/profiles.py
@@ -42,13 +42,30 @@ class NmProfiles:
def __init__(self, context):
self._ctx = context
+ def generate_config_strings(self, net_state):
+ _append_nm_ovs_port_iface(net_state)
+ all_profiles = []
+ for iface in net_state.ifaces.all_ifaces():
+ if iface.is_up:
+ profile = NmProfile(self._ctx, iface)
+ profile.prepare_config(save_to_disk=False, gen_conf_mode=True)
+ all_profiles.append(profile)
+
+ return [
+ (profile.config_file_name, profile.to_key_file_string())
+ for profile in all_profiles
+ ]
+
def apply_config(self, net_state, save_to_disk):
self._prepare_state_for_profiles(net_state)
all_profiles = [
- NmProfile(self._ctx, iface, save_to_disk)
+ NmProfile(self._ctx, iface)
for iface in net_state.ifaces.all_ifaces()
]
+ for profile in all_profiles:
+ profile.import_current()
+ profile.prepare_config(save_to_disk, gen_conf_mode=False)
_use_uuid_as_controller_and_parent(all_profiles)
changed_ovs_bridges_and_ifaces = {}
@@ -62,7 +79,7 @@ class NmProfiles:
for profile in all_profiles:
if profile.has_pending_change:
- profile.save_config()
+ profile.save_config(save_to_disk)
self._ctx.wait_all_finish()
for action in NmProfile.ACTIONS:
diff --git a/libnmstate/nm/route.py b/libnmstate/nm/route.py
index cb43f28..c78e3c1 100644
--- a/libnmstate/nm/route.py
+++ b/libnmstate/nm/route.py
@@ -17,7 +17,6 @@
# along with this program. If not, see .
#
-from operator import itemgetter
import socket
from libnmstate import iplib
@@ -37,87 +36,6 @@ IPV6_DEFAULT_GATEWAY_DESTINATION = "::/0"
ROUTE_RULE_DEFAULT_PRIORIRY = 30000
-def get_running_config(applied_configs):
- """
- Query routes saved in running profile
- """
- routes = []
- for iface_name, applied_config in applied_configs.items():
- for ip_profile in (
- applied_config.get_setting_ip6_config(),
- applied_config.get_setting_ip4_config(),
- ):
- if not ip_profile:
- continue
- nm_routes = ip_profile.props.routes
- gateway = ip_profile.props.gateway
- if not nm_routes and not gateway:
- continue
- default_table_id = ip_profile.props.route_table
- if gateway:
- routes.append(
- _get_default_route_config(
- gateway,
- ip_profile.props.route_metric,
- default_table_id,
- iface_name,
- )
- )
- # NM supports multiple route table in single profile:
- # https://bugzilla.redhat.com/show_bug.cgi?id=1436531 The
- # `ipv4.route-table` and `ipv6.route-table` will be the default
- # table id for static routes and auto routes. But each static route
- # can still specify route table id.
- for nm_route in nm_routes:
- table_id = _get_per_route_table_id(nm_route, default_table_id)
- route_entry = _nm_route_to_route(
- nm_route, table_id, iface_name
- )
- if route_entry:
- routes.append(route_entry)
- routes.sort(
- key=itemgetter(
- Route.TABLE_ID, Route.NEXT_HOP_INTERFACE, Route.DESTINATION
- )
- )
- return routes
-
-
-def _get_per_route_table_id(nm_route, default_table_id):
- table = nm_route.get_attribute(NM.IP_ROUTE_ATTRIBUTE_TABLE)
- return int(table.get_uint32()) if table else default_table_id
-
-
-def _nm_route_to_route(nm_route, table_id, iface_name):
- dst = "{ip}/{prefix}".format(
- ip=nm_route.get_dest(), prefix=nm_route.get_prefix()
- )
- next_hop = nm_route.get_next_hop() or ""
- metric = int(nm_route.get_metric())
-
- return {
- Route.TABLE_ID: table_id,
- Route.DESTINATION: dst,
- Route.NEXT_HOP_INTERFACE: iface_name,
- Route.NEXT_HOP_ADDRESS: next_hop,
- Route.METRIC: metric,
- }
-
-
-def _get_default_route_config(gateway, metric, default_table_id, iface_name):
- if iplib.is_ipv6_address(gateway):
- destination = IPV6_DEFAULT_GATEWAY_DESTINATION
- else:
- destination = IPV4_DEFAULT_GATEWAY_DESTINATION
- return {
- Route.TABLE_ID: default_table_id,
- Route.DESTINATION: destination,
- Route.NEXT_HOP_INTERFACE: iface_name,
- Route.NEXT_HOP_ADDRESS: gateway,
- Route.METRIC: metric,
- }
-
-
def add_routes(setting_ip, routes):
for route in routes:
_add_specfic_route(setting_ip, route)
@@ -131,7 +49,11 @@ def _add_specfic_route(setting_ip, route):
else:
family = socket.AF_INET
metric = route.get(Route.METRIC, Route.USE_DEFAULT_METRIC)
- next_hop = route[Route.NEXT_HOP_ADDRESS]
+ if route[Route.NEXT_HOP_ADDRESS]:
+ next_hop = route[Route.NEXT_HOP_ADDRESS]
+ else:
+ # NM.IPRoute.new() need None instead of ""
+ next_hop = None
ip_route = NM.IPRoute.new(
family, destination, prefix_len, next_hop, metric
)
diff --git a/libnmstate/nmstate.py b/libnmstate/nmstate.py
index 2034f47..d3e0ba2 100644
--- a/libnmstate/nmstate.py
+++ b/libnmstate/nmstate.py
@@ -28,6 +28,7 @@ import pkgutil
from libnmstate import validator
from libnmstate.error import NmstateError
from libnmstate.error import NmstateValueError
+from libnmstate.error import NmstateDependencyError
from libnmstate.schema import DNS
from libnmstate.schema import Interface
from libnmstate.schema import InterfaceType
@@ -37,6 +38,7 @@ from libnmstate.schema import RouteRule
from .nispor.plugin import NisporPlugin
from .plugin import NmstatePlugin
from .state import merge_dict
+from .net_state import NetState
_INFO_TYPE_RUNNING = 1
_INFO_TYPE_RUNNING_CONFIG = 2
@@ -173,10 +175,15 @@ def _get_interface_info_from_plugins(plugins, info_type):
not in plugin.plugin_capabilities
):
continue
- if info_type == _INFO_TYPE_RUNNING_CONFIG:
- ifaces = plugin.get_running_config_interfaces()
- else:
- ifaces = plugin.get_interfaces()
+ try:
+ if info_type == _INFO_TYPE_RUNNING_CONFIG:
+ ifaces = plugin.get_running_config_interfaces()
+ else:
+ ifaces = plugin.get_interfaces()
+ except NmstateDependencyError as e:
+ logging.warning(f"Plugin {plugin.name} error: {e}")
+ continue
+
for iface in ifaces:
iface[IFACE_PRIORITY_METADATA] = plugin.priority
iface[IFACE_PLUGIN_SRC_METADATA] = [plugin.name]
@@ -370,3 +377,22 @@ def _get_iface_types_by_name(iface_infos, name):
def show_running_config_with_plugins(plugins):
return show_with_plugins(plugins, info_type=_INFO_TYPE_RUNNING_CONFIG)
+
+
+def generate_configurations(desire_state):
+ """
+ Return a dictionary with:
+ * key: plugin name
+ * vlaue: list of strings for configruations
+ This function will not merge or verify desire state with current state, so
+ you may run this function on different system.
+ """
+ configs = {}
+ net_state = NetState(desire_state, gen_conf_mode=True)
+
+ with plugin_context() as plugins:
+ for plugin in plugins:
+ config = plugin.generate_configurations(net_state)
+ if config:
+ configs[plugin.name] = config
+ return configs
diff --git a/libnmstate/plugin.py b/libnmstate/plugin.py
index 657526c..ef3874f 100644
--- a/libnmstate/plugin.py
+++ b/libnmstate/plugin.py
@@ -121,3 +121,10 @@ class NmstatePlugin(metaclass=ABCMeta):
Retrun False when plugin can report new interface.
"""
return False
+
+ def generate_configurations(self, net_state):
+ """
+ Returning a list of strings for configurations which could be save
+ persistently.
+ """
+ return []
diff --git a/libnmstate/route.py b/libnmstate/route.py
index 611ee91..f7dbf54 100644
--- a/libnmstate/route.py
+++ b/libnmstate/route.py
@@ -30,7 +30,12 @@ from libnmstate.schema import Route
from .ifaces.base_iface import BaseIface
from .state import StateEntry
-from .state import state_match
+
+
+DEFAULT_ROUTE_TABLE = 254
+
+
+IPV6_ROUTE_REMOVED = "_ipv6_route_removed"
class RouteEntry(StateEntry):
@@ -69,9 +74,17 @@ class RouteEntry(StateEntry):
return self._invalid_reason
def complement_defaults(self):
- if not self.absent:
- if self.table_id is None:
- self.table_id = Route.USE_DEFAULT_ROUTE_TABLE
+ if self.absent:
+ if self.table_id == Route.USE_DEFAULT_ROUTE_TABLE:
+ self.table_id = DEFAULT_ROUTE_TABLE
+ if self.metric == Route.USE_DEFAULT_METRIC:
+ self.metric = None
+ else:
+ if (
+ self.table_id is None
+ or self.table_id == Route.USE_DEFAULT_ROUTE_TABLE
+ ):
+ self.table_id = DEFAULT_ROUTE_TABLE
if self.metric is None:
self.metric = Route.USE_DEFAULT_METRIC
if self.next_hop_address is None:
@@ -80,7 +93,6 @@ class RouteEntry(StateEntry):
def _keys(self):
return (
self.table_id,
- self.metric,
self.destination,
self.next_hop_address,
self.next_hop_interface,
@@ -165,6 +177,12 @@ class RouteEntry(StateEntry):
self.next_hop_address
)
+ def to_dict(self):
+ info = super().to_dict()
+ if self.metric == Route.USE_DEFAULT_METRIC:
+ del info[Route.METRIC]
+ return info
+
class RouteState:
def __init__(self, ifaces, des_route_state, cur_route_state):
@@ -211,6 +229,16 @@ class RouteState:
for route in route_set:
if not rt.match(route):
new_routes.add(route)
+ if route.is_ipv6:
+ # The routes match and therefore it is being removed.
+ # Nmstate will check if it is an IPv6 route and if so,
+ # marking the interface as deactivate first.
+ #
+ # This is a workaround for NM bug:
+ # https://bugzilla.redhat.com/1837254
+ ifaces.all_kernel_ifaces[iface_name].raw[
+ IPV6_ROUTE_REMOVED
+ ] = True
if new_routes != route_set:
self._routes[iface_name] = new_routes
@@ -254,12 +282,14 @@ class RouteState:
ifaces=None, des_route_state=None, cur_route_state=cur_route_state
)
for iface_name, route_set in self._routes.items():
- routes_info = [r.to_dict() for r in sorted(route_set)]
- cur_routes_info = [
- r.to_dict()
- for r in sorted(current._routes.get(iface_name, set()))
- ]
- if not state_match(routes_info, cur_routes_info):
+ cur_route_set = current._routes.get(iface_name, set())
+ # Kernel might append additional routes. For example, IPv6 default
+ # gateway will generate /128 static direct route
+ if not route_set <= cur_route_set:
+ routes_info = [
+ r.to_dict() for r in sorted(route_set) if not r.absent
+ ]
+ cur_routes_info = [r.to_dict() for r in sorted(cur_route_set)]
raise NmstateVerificationError(
format_desired_current_state_diff(
{Route.KEY: {Route.CONFIG: routes_info}},
diff --git a/libnmstate/schema.py b/libnmstate/schema.py
index 30d1f22..5137071 100644
--- a/libnmstate/schema.py
+++ b/libnmstate/schema.py
@@ -46,6 +46,7 @@ class Interface:
MAC = "mac-address"
MTU = "mtu"
+ COPY_MAC_FROM = "copy-mac-from"
class Route:
@@ -448,3 +449,13 @@ class MacVlan:
class MacVtap(MacVlan):
TYPE = InterfaceType.MAC_VTAP
CONFIG_SUBTREE = "mac-vtap"
+
+
+class Ieee8021X:
+ CONFIG_SUBTREE = "802.1x"
+ IDENTITY = "identity"
+ EAP_METHODS = "eap-methods"
+ PRIVATE_KEY = "private-key"
+ PRIVATE_KEY_PASSWORD = "private-key-password"
+ CLIENT_CERT = "client-cert"
+ CA_CERT = "ca-cert"
diff --git a/libnmstate/schemas/operational-state.yaml b/libnmstate/schemas/operational-state.yaml
index 4f6205c..436e02e 100644
--- a/libnmstate/schemas/operational-state.yaml
+++ b/libnmstate/schemas/operational-state.yaml
@@ -18,6 +18,7 @@ properties:
- $ref: "#/definitions/interface-ip/all"
- $ref: "#/definitions/lldp/rw"
- $ref: "#/definitions/lldp/ro"
+ - $ref: "#/definitions/802.1x/rw"
- oneOf:
- "$ref": "#/definitions/interface-unknown/rw"
- "$ref": "#/definitions/interface-ethernet/rw"
@@ -266,6 +267,8 @@ definitions:
type: string
enum:
- bond
+ copy-mac-from:
+ type: string
link-aggregation:
type: object
properties:
@@ -288,6 +291,8 @@ definitions:
- $ref: "#/definitions/interface-linux-bridge/ro"
ro:
properties:
+ copy-mac-from:
+ type: string
bridge:
type: object
properties:
@@ -784,6 +789,23 @@ definitions:
properties:
enabled:
type: boolean
+ 802.1x:
+ rw:
+ properties:
+ identity:
+ type: string
+ eap-methods:
+ type: array
+ items:
+ type: string
+ private-key:
+ type: string
+ private-key-password:
+ type: string
+ client-cert:
+ type: string
+ ca-cert:
+ type: string
interface-infiniband:
rw:
properties:
diff --git a/libnmstate/version.py b/libnmstate/version.py
new file mode 100644
index 0000000..635b596
--- /dev/null
+++ b/libnmstate/version.py
@@ -0,0 +1,27 @@
+#
+# Copyright (c) 2018-2021 Red Hat, Inc.
+#
+# This file is part of nmstate
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see .
+#
+
+import os
+
+
+def get_version():
+ root_dir = os.path.dirname(os.path.abspath(__file__))
+ with open(os.path.join(root_dir, "VERSION")) as f:
+ version = f.read().strip()
+ return version
diff --git a/nmstate.egg-info/SOURCES.txt b/nmstate.egg-info/SOURCES.txt
index dd03b9e..0cfc866 100644
--- a/nmstate.egg-info/SOURCES.txt
+++ b/nmstate.egg-info/SOURCES.txt
@@ -12,6 +12,7 @@ examples/dns_edit_eth1.yml
examples/dns_remove.yml
examples/eth1_add_route.yml
examples/eth1_del_all_routes.yml
+examples/eth1_with_ieee_802_1x.yml
examples/eth1_with_sriov.yml
examples/infiniband_pkey_ipoib_create.yml
examples/infiniband_pkey_ipoib_delete.yml
@@ -54,6 +55,7 @@ libnmstate/route_rule.py
libnmstate/schema.py
libnmstate/state.py
libnmstate/validator.py
+libnmstate/version.py
libnmstate/ifaces/__init__.py
libnmstate/ifaces/base_iface.py
libnmstate/ifaces/bond.py
@@ -100,6 +102,7 @@ libnmstate/nm/connection.py
libnmstate/nm/context.py
libnmstate/nm/device.py
libnmstate/nm/dns.py
+libnmstate/nm/ieee_802_1x.py
libnmstate/nm/infiniband.py
libnmstate/nm/ipv4.py
libnmstate/nm/ipv6.py
diff --git a/nmstatectl/nmstatectl.py b/nmstatectl/nmstatectl.py
index 8f8c685..df59942 100644
--- a/nmstatectl/nmstatectl.py
+++ b/nmstatectl/nmstatectl.py
@@ -58,6 +58,7 @@ def main():
setup_subcommand_show(subparsers)
setup_subcommand_version(subparsers)
setup_subcommand_varlink(subparsers)
+ setup_subcommand_gen_config(subparsers)
parser.add_argument(
"--version", action="store_true", help="Display nmstate version"
)
@@ -253,6 +254,16 @@ def setup_subcommand_varlink(subparsers):
parser_varlink.set_defaults(func=run_varlink_server)
+def setup_subcommand_gen_config(subparsers):
+ parser_gc = subparsers.add_parser("gc", help="Generate configurations")
+ parser_gc.add_argument(
+ "file",
+ help="File containing desired state. ",
+ nargs="*",
+ )
+ parser_gc.set_defaults(func=run_gen_config)
+
+
def version(args):
print(libnmstate.__version__)
@@ -354,6 +365,30 @@ def run_varlink_server(args):
logging.exception(exception)
+def run_gen_config(args):
+ if args.file:
+ for statefile in args.file:
+ if statefile == "-" and not os.path.isfile(statefile):
+ statedata = sys.stdin.read()
+ else:
+ with open(statefile) as statefile:
+ statedata = statefile.read()
+
+ # JSON dictionaries start with a curly brace
+ if statedata[0] == "{":
+ state = json.loads(statedata)
+ use_yaml = False
+ else:
+ state = yaml.load(statedata, Loader=yaml.SafeLoader)
+ use_yaml = True
+ print_state(
+ libnmstate.generate_configurations(state), use_yaml=use_yaml
+ )
+ else:
+ sys.stderr.write("ERROR: No state specified\n")
+ return 1
+
+
def apply_state(statedata, verify_change, commit, timeout, save_to_disk):
use_yaml = False
# JSON dictionaries start with a curly brace