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