diff --git a/PKG-INFO b/PKG-INFO index 86af553..dabc333 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: nmstate -Version: 0.4.1 +Version: 1.0.0 Summary: Declarative network manager API Home-page: https://nmstate.github.io/ Author: Edward Haas diff --git a/doc/nmstatectl.8 b/doc/nmstatectl.8 index 95a58c0..e5bf115 100644 --- a/doc/nmstatectl.8 +++ b/doc/nmstatectl.8 @@ -1,5 +1,5 @@ .\" Manpage for nmstatectl. -.TH nmstatectl 8 "October 22, 2020" "0.4.1" "nmstatectl man page" +.TH nmstatectl 8 "December 07, 2020" "1.0.0" "nmstatectl man page" .SH NAME nmstatectl \- A nmstate command line tool .SH SYNOPSIS diff --git a/examples/mac_vtap_absent.yml b/examples/mac_vtap_absent.yml new file mode 100644 index 0000000..8b74115 --- /dev/null +++ b/examples/mac_vtap_absent.yml @@ -0,0 +1,5 @@ +--- +interfaces: + - name: macvtap0 + type: mac-vtap + state: absent diff --git a/examples/mac_vtap_create.yml b/examples/mac_vtap_create.yml new file mode 100644 index 0000000..a2883b2 --- /dev/null +++ b/examples/mac_vtap_create.yml @@ -0,0 +1,9 @@ +--- +interfaces: + - name: macvtap0 + type: mac-vtap + state: up + mac-vtap: + base-iface: eth1 + mode: passthru + promiscuous: true diff --git a/examples/team0_with_port.yml b/examples/team0_with_port.yml index 1504716..1cd7f19 100644 --- a/examples/team0_with_port.yml +++ b/examples/team0_with_port.yml @@ -4,7 +4,7 @@ interfaces: type: team state: up team: - ports: + port: - name: eth1 - name: eth2 runner: diff --git a/libnmstate/VERSION b/libnmstate/VERSION index 267577d..3eefcb9 100644 --- a/libnmstate/VERSION +++ b/libnmstate/VERSION @@ -1 +1 @@ -0.4.1 +1.0.0 diff --git a/libnmstate/dns.py b/libnmstate/dns.py index 47f145b..cb5ce0b 100644 --- a/libnmstate/dns.py +++ b/libnmstate/dns.py @@ -166,7 +166,7 @@ class DnsState: def _find_ifaces_with_auto_dns_false(self, ifaces): ipv4_iface = None ipv6_iface = None - for iface in ifaces.values(): + for iface in ifaces.all_kernel_ifaces.values(): if ipv4_iface and ipv6_iface: return (ipv4_iface, ipv6_iface) for family in (Interface.IPV4, Interface.IPV6): @@ -180,7 +180,10 @@ class DnsState: return (ipv4_iface, ipv6_iface) def verify(self, cur_dns_state): - cur_dns = DnsState(des_dns_state=None, cur_dns_state=cur_dns_state,) + cur_dns = DnsState( + des_dns_state=None, + cur_dns_state=cur_dns_state, + ) if self.config.get(DNS.SERVER, []) != cur_dns.config.get( DNS.SERVER, [] ) or self.config.get(DNS.SEARCH, []) != cur_dns.config.get( @@ -188,7 +191,8 @@ class DnsState: ): raise NmstateVerificationError( format_desired_current_state_diff( - {DNS.KEY: self.config}, {DNS.KEY: cur_dns.config}, + {DNS.KEY: self.config}, + {DNS.KEY: cur_dns.config}, ) ) diff --git a/libnmstate/ifaces/base_iface.py b/libnmstate/ifaces/base_iface.py index ae54245..277c85e 100644 --- a/libnmstate/ifaces/base_iface.py +++ b/libnmstate/ifaces/base_iface.py @@ -124,6 +124,8 @@ class BaseIface: DNS_METADATA = "_dns" ROUTES_METADATA = "_routes" ROUTE_RULES_METADATA = "_route_rules" + RULE_CHANGED_METADATA = "_changed" + ROUTE_CHANGED_METADATA = "_changed" def __init__(self, info, save_to_disk=True): self._origin_info = deepcopy(info) @@ -137,6 +139,19 @@ class BaseIface: def can_have_ip_as_port(self): return False + @property + def is_user_space_only(self): + """ + Whether this interface is user space only. + User space network interface means: + * Can have duplicate interface name against kernel network + interfaces + * Cannot be used subordinate of other kernel interfaces. + * Due to limtation of nmstate, currently cannot be used as + subordinate of other user space interfaces. + """ + return False + def sort_port(self): pass @@ -233,6 +248,7 @@ class BaseIface: if ( ip_state.is_enabled and self.controller + and self.controller_type != InterfaceType.VRF and not self.can_have_ip_as_port ): raise NmstateValueError( @@ -279,7 +295,10 @@ class BaseIface: def set_controller(self, controller_iface_name, controller_type): self._info[BaseIface.CONTROLLER_METADATA] = controller_iface_name self._info[BaseIface.CONTROLLER_TYPE_METADATA] = controller_type - if not self.can_have_ip_as_port: + if ( + not self.can_have_ip_as_port + and controller_type != InterfaceType.VRF + ): for family in (Interface.IPV4, Interface.IPV6): self._info[family] = {InterfaceIP.ENABLED: False} @@ -294,7 +313,7 @@ class BaseIface: def gen_metadata(self, ifaces): if self.is_controller and not self.is_absent: for port_name in self.port: - port_iface = ifaces[port_name] + port_iface = ifaces.all_kernel_ifaces[port_name] port_iface.set_controller(self.name, self.type) def update(self, info): diff --git a/libnmstate/ifaces/bond.py b/libnmstate/ifaces/bond.py index 248a74e..8f8bd6e 100644 --- a/libnmstate/ifaces/bond.py +++ b/libnmstate/ifaces/bond.py @@ -77,7 +77,7 @@ class BondIface(BaseIface): def _generate_bond_mode_change_metadata(self, ifaces): if self.is_up: - cur_iface = ifaces.current_ifaces.get(self.name) + cur_iface = ifaces.get_cur_iface(self.name, self.type) if cur_iface and self.bond_mode != cur_iface.bond_mode: self._set_bond_mode_changed_metadata(True) diff --git a/libnmstate/ifaces/ifaces.py b/libnmstate/ifaces/ifaces.py index 1c82c05..52f74ed 100644 --- a/libnmstate/ifaces/ifaces.py +++ b/libnmstate/ifaces/ifaces.py @@ -36,6 +36,7 @@ from .ethernet import EthernetIface from .infiniband import InfiniBandIface from .linux_bridge import LinuxBridgeIface from .macvlan import MacVlanIface +from .macvtap import MacVtapIface from .ovs import OvsBridgeIface from .ovs import OvsInternalIface from .team import TeamIface @@ -44,6 +45,25 @@ from .vxlan import VxlanIface from .vrf import VrfIface +class _UserSpaceIfaces: + def __init__(self): + self._ifaces = {} + + def set(self, iface): + self._ifaces[f"{iface.name}.{iface.type}"] = iface + + def get(self, iface_name, iface_type): + return self._ifaces.get(f"{iface_name}.{iface_type}") + + def remove(self, iface): + if self.get(iface.name, iface.type): + del self._ifaces[f"{iface.name}.{iface.type}"] + + def __iter__(self): + for iface in self._ifaces.values(): + yield iface + + class Ifaces: """ The Ifaces class hold both desired state(optional) and current state. @@ -63,22 +83,36 @@ class Ifaces: def __init__(self, des_iface_infos, cur_iface_infos, save_to_disk=True): self._save_to_disk = save_to_disk self._des_iface_infos = des_iface_infos - self._cur_ifaces = {} - self._ifaces = {} - self._ignored_iface_names = set() + self._cur_kernel_ifaces = {} + self._kernel_ifaces = {} + self._user_space_ifaces = _UserSpaceIfaces() + self._cur_user_space_ifaces = _UserSpaceIfaces() + self._ignored_ifaces = set() if cur_iface_infos: for iface_info in cur_iface_infos: cur_iface = _to_specific_iface_obj(iface_info, save_to_disk) - self._ifaces[cur_iface.name] = cur_iface - self._cur_ifaces[cur_iface.name] = cur_iface + if cur_iface.is_user_space_only: + self._user_space_ifaces.set(cur_iface) + self._cur_user_space_ifaces.set(cur_iface) + else: + self._kernel_ifaces[cur_iface.name] = cur_iface + self._cur_kernel_ifaces[cur_iface.name] = cur_iface if des_iface_infos: for iface_info in des_iface_infos: iface = BaseIface(iface_info, save_to_disk) - cur_iface = self._ifaces.get(iface.name) + if iface.type == InterfaceType.UNKNOWN: + cur_ifaces = self._get_cur_ifaces(iface.name) + if len(cur_ifaces) > 1: + raise NmstateValueError( + f"Got multiple interface with name {iface.name}, " + "please specify the interface type explicitly" + ) + cur_iface = self.get_iface(iface.name, iface.type) if cur_iface and cur_iface.is_desired: raise NmstateValueError( - f"Duplicate interfaces names detected: {iface.name}" + f"Duplicate interfaces names detected: {iface.name} " + f"for type {cur_iface.type}" ) if iface_info.get(Interface.TYPE) is None: @@ -98,11 +132,16 @@ class Ifaces: # Ignore interface with unknown type continue if iface.is_ignore: - self._ignored_iface_names.add(iface.name) + self._ignored_ifaces.add( + (iface.name, iface.type, iface.is_user_space_only) + ) if cur_iface: iface.merge(cur_iface) iface.mark_as_desired() - self._ifaces[iface.name] = iface + if iface.is_user_space_only: + self._user_space_ifaces.set(iface) + else: + self._kernel_ifaces[iface.name] = iface self._create_virtual_port() self._validate_unknown_port() @@ -110,7 +149,7 @@ class Ifaces: self._validate_infiniband_as_bridge_port() self._validate_infiniband_as_bond_port() self._gen_metadata() - for iface in self._ifaces.values(): + for iface in self.all_ifaces(): iface.pre_edit_validation_and_cleanup() self._pre_edit_validation_and_cleanup() @@ -124,15 +163,17 @@ class Ifaces: only in port list of OVS bridge. """ new_ifaces = [] - for iface in self._ifaces.values(): + for iface in self.all_ifaces(): if iface.is_up and iface.is_controller: for port_name in iface.port: - if port_name not in self._ifaces.keys(): + # nmstate does not support port interface to be user + # space interface + if port_name not in self._kernel_ifaces.keys(): new_port = iface.create_virtual_port(port_name) if new_port: new_ifaces.append(new_port) for iface in new_ifaces: - self._ifaces[iface.name] = iface + self._kernel_ifaces[iface.name] = iface def _pre_edit_validation_and_cleanup(self): self._validate_over_booked_port() @@ -140,7 +181,7 @@ class Ifaces: self._validate_vlan_mtu() self._handle_controller_port_list_change() self._match_child_iface_state_with_parent() - self._mark_orphen_as_absent() + self._mark_orphan_as_absent() self._bring_port_up_if_not_in_desire() self._validate_ovs_patch_peers() self._remove_unknown_type_interfaces() @@ -151,10 +192,10 @@ class Ifaces: When port been included in controller, automactially set it as state UP if not defiend in desire state """ - for iface in self._ifaces.values(): + for iface in self.all_ifaces(): if iface.is_up and iface.is_controller: for port_name in iface.port: - port_iface = self._ifaces[port_name] + port_iface = self._kernel_ifaces[port_name] if not port_iface.is_desired and not port_iface.is_up: port_iface.mark_as_up() port_iface.mark_as_changed() @@ -163,10 +204,10 @@ class Ifaces: """ When OVS patch peer does not exist or is down, raise an error. """ - for iface in self._ifaces.values(): + for iface in self._kernel_ifaces.values(): if iface.type == InterfaceType.OVS_INTERFACE and iface.is_up: if iface.peer: - peer_iface = self._ifaces.get(iface.peer) + peer_iface = self._kernel_ifaces.get(iface.peer) if not peer_iface or not peer_iface.is_up: raise NmstateValueError( f"OVS patch port peer {iface.peer} must exist and " @@ -185,13 +226,16 @@ class Ifaces: """ Validate that vlan is not being created over infiniband interface """ - for iface in self._ifaces.values(): + for iface in self._kernel_ifaces.values(): if ( iface.type in [InterfaceType.VLAN, InterfaceType.VXLAN] and iface.is_up ): - if self._ifaces[iface.parent].type == InterfaceType.INFINIBAND: + if ( + self._kernel_ifaces[iface.parent].type + == InterfaceType.INFINIBAND + ): raise NmstateValueError( f"Interface {iface.name} of type {iface.type}" " is not supported over base interface of " @@ -205,14 +249,14 @@ class Ifaces: If base MTU is not present, set same as vlan MTU """ - for iface in self._ifaces.values(): + for iface in self._kernel_ifaces.values(): if ( iface.type in [InterfaceType.VLAN, InterfaceType.VXLAN] and iface.is_up and iface.mtu ): - base_iface = self._ifaces.get(iface.parent) + base_iface = self._kernel_ifaces.get(iface.parent) if not base_iface.mtu: base_iface.mtu = iface.mtu if iface.mtu > base_iface.mtu: @@ -228,13 +272,13 @@ class Ifaces: The IPoIB NIC has no ethernet layer, hence is no way for adding a IPoIB NIC to linux bridge or OVS bridge """ - for iface in self._ifaces.values(): + for iface in self.all_ifaces(): if iface.is_desired and iface.type in ( InterfaceType.LINUX_BRIDGE, InterfaceType.OVS_BRIDGE, ): for port_name in iface.port: - port_iface = self._ifaces[port_name] + port_iface = self._kernel_ifaces[port_name] if port_iface.type == InterfaceType.INFINIBAND: raise NmstateValueError( f"The bridge {iface.name} cannot use " @@ -247,14 +291,14 @@ class Ifaces: The IP over InfiniBand interface is only allowed to be port of bond in "active-backup" mode. """ - for iface in self._ifaces.values(): + for iface in self._kernel_ifaces.values(): if ( iface.is_desired and iface.type == InterfaceType.BOND and iface.bond_mode != BondMode.ACTIVE_BACKUP ): for port_name in iface.port: - port_iface = self._ifaces[port_name] + port_iface = self._kernel_ifaces[port_name] if port_iface.type == InterfaceType.INFINIBAND: raise NmstateValueError( "The IP over InfiniBand interface " @@ -265,32 +309,32 @@ class Ifaces: def _handle_controller_port_list_change(self): """ - * Mark port interface as changed if controller removed. - * Mark port interface as changed if port list of controller changed. - * Mark port interface as changed if port config changed when - controller said so. + * Mark port interface as changed if controller removed. + * Mark port interface as changed if port list of controller changed. + * Mark port interface as changed if port config changed when + controller said so. """ - for iface in self._ifaces.values(): + for iface in self.all_ifaces(): if not iface.is_desired or not iface.is_controller: continue des_port = set(iface.port) if iface.is_absent: des_port = set() - cur_iface = self._cur_ifaces.get(iface.name) + cur_iface = self.get_cur_iface(iface.name, iface.type) cur_port = set(cur_iface.port) if cur_iface else set() if des_port != cur_port: changed_port = (des_port | cur_port) - (des_port & cur_port) for iface_name in changed_port: - self._ifaces[iface_name].mark_as_changed() + self._kernel_ifaces[iface_name].mark_as_changed() if cur_iface: for port_name in iface.config_changed_port(cur_iface): - if port_name in self._ifaces: - self._ifaces[port_name].mark_as_changed() + if port_name in self._kernel_ifaces: + self._kernel_ifaces[port_name].mark_as_changed() def _validate_vrf_table_id_changes(self): - for iface in self._ifaces.values(): + for iface in self._kernel_ifaces.values(): if iface.is_desired and iface.type == InterfaceType.VRF: - cur_iface = self._cur_ifaces.get(iface.name) + cur_iface = self._cur_kernel_ifaces.get(iface.name) if ( cur_iface and cur_iface.route_table_id != iface.route_table_id @@ -308,10 +352,14 @@ class Ifaces: * When changed/desired parent interface is marked as down or absent, child state should sync with parent. """ - for iface in self._ifaces.values(): - if iface.parent and self._ifaces.get(iface.parent): - parent_iface = self._ifaces[iface.parent] - if parent_iface.is_desired or parent_iface.is_changed: + for iface in self.all_ifaces(): + if iface.parent: + parent_iface = self._get_parent_iface(iface) + if ( + parent_iface + and parent_iface.is_desired + or parent_iface.is_changed + ): if ( Interface.STATE not in iface.original_dict or parent_iface.is_down @@ -320,66 +368,128 @@ class Ifaces: iface.state = parent_iface.state iface.mark_as_changed() - def _mark_orphen_as_absent(self): - for iface in self._ifaces.values(): - if iface.need_parent and ( - not iface.parent or not self._ifaces.get(iface.parent) + def _get_parent_iface(self, iface): + if not iface.parent: + return None + for cur_iface in self.all_ifaces(): + if cur_iface.name == iface.parent and iface != cur_iface: + return cur_iface + return None + + def _mark_orphan_as_absent(self): + for iface in self._kernel_ifaces.values(): + if iface.need_parent and (iface.is_desired or iface.is_changed): + parent_iface = self._get_parent_iface(iface) + if parent_iface is None or parent_iface.is_absent: + iface.mark_as_changed() + iface.state = InterfaceState.ABSENT + + def all_ifaces(self): + for iface in self._kernel_ifaces.values(): + yield iface + for iface in self._user_space_ifaces: + yield iface + + @property + def all_kernel_ifaces(self): + """ + Return a editable dict of kernel interfaces, indexed by interface name + """ + return self._kernel_ifaces + + @property + def all_user_space_ifaces(self): + """ + Return a editable user space interfaces, the object has functions: + * set(iface) + * get(iface) + * remove(iface) + * __iter__() + """ + return self._user_space_ifaces + + def _get_cur_ifaces(self, iface_name): + return [ + iface + for iface in ( + list(self._cur_kernel_ifaces.values()) + + list(self._cur_user_space_ifaces) + ) + if iface.name == iface_name + ] + + def get_iface(self, iface_name, iface_type): + iface = self._kernel_ifaces.get(iface_name) + if iface and iface_type in (None, InterfaceType.UNKNOWN, iface.type): + return iface + + for iface in self._user_space_ifaces: + if iface.name == iface_name and iface_type in ( + None, + InterfaceType.UNKNOWN, + iface.type, ): - iface.mark_as_changed() - iface.state = InterfaceState.ABSENT + return iface + return None + + def get_cur_iface(self, iface_name, iface_type): - def get(self, iface_name): - return self._ifaces.get(iface_name) + iface = self._cur_kernel_ifaces.get(iface_name) + if iface and iface_type in (None, InterfaceType.UNKNOWN, iface.type): + return iface - def __getitem__(self, iface_name): - return self._ifaces[iface_name] + for iface in self._cur_user_space_ifaces: + if iface.name == iface_name and iface_type in ( + None, + InterfaceType.UNKNOWN, + iface.type, + ): + return iface + return None + + def _remove_iface(self, iface_name, iface_type): + cur_iface = self._cur_kernel_ifaces.get(iface_name, iface_type) + if cur_iface: + self._user_space_ifaces.remove(cur_iface) + else: + cur_iface = self._kernel_ifaces.get(iface_name) + if ( + iface_type + and iface_type != InterfaceType.UNKNOWN + and iface_type == cur_iface.type + ): + del self._kernel_ifaces[iface_name] - def __setitem__(self, iface_name, iface): - self._ifaces[iface_name] = iface + def add_ifaces(self, ifaces): + for iface in ifaces: + if iface.is_user_space_only: + self._user_space_ifaces.set(iface) + else: + self._kernel_ifaces[iface.name] = iface def _gen_metadata(self): - for iface in self._ifaces.values(): + for iface in self.all_ifaces(): # Generate metadata for all interface in case any of them # been marked as changed by DNS/Route/RouteRule. iface.gen_metadata(self) - def keys(self): - for iface in self._ifaces.keys(): - yield iface - - def values(self): - for iface in self._ifaces.values(): - yield iface - - def update(self, ifaces): - if ifaces: - self._ifaces.update(ifaces) - - @property - def current_ifaces(self): - return self._cur_ifaces - @property def state_to_edit(self): return [ iface.to_dict() - for iface in self._ifaces.values() + for iface in self.all_ifaces() if (iface.is_changed or iface.is_desired) and not iface.is_ignore ] - @property - def cur_ifaces(self): - return self._cur_ifaces - def _remove_unknown_interface_type_port(self): """ When controller containing port with unknown interface type, they should be removed from controller port list before verifying. """ - for iface in self._ifaces.values(): + for iface in self.all_ifaces(): if iface.is_up and iface.is_controller and iface.port: for port_name in iface.port: - port_iface = self._ifaces[port_name] + port_iface = self._kernel_ifaces[port_name] if port_iface.type == InterfaceType.UNKNOWN: iface.remove_port(port_name) @@ -390,14 +500,14 @@ class Ifaces: save_to_disk=self._save_to_disk, ) cur_ifaces._remove_unknown_interface_type_port() - cur_ifaces._remove_ignore_interfaces(self._ignored_iface_names) - self._remove_ignore_interfaces(self._ignored_iface_names) - for iface in self._ifaces.values(): + cur_ifaces._remove_ignore_interfaces(self._ignored_ifaces) + self._remove_ignore_interfaces(self._ignored_ifaces) + for iface in self.all_ifaces(): if iface.is_desired: if iface.is_virtual and iface.original_dict.get( Interface.STATE ) in (InterfaceState.DOWN, InterfaceState.ABSENT): - cur_iface = cur_ifaces.get(iface.name) + cur_iface = cur_ifaces.get_iface(iface.name, iface.type) if cur_iface: raise NmstateVerificationError( format_desired_current_state_diff( @@ -406,7 +516,7 @@ class Ifaces: ) ) elif iface.is_up or (iface.is_down and not iface.is_virtual): - cur_iface = cur_ifaces.get(iface.name) + cur_iface = cur_ifaces.get_iface(iface.name, iface.type) if not cur_iface: raise NmstateVerificationError( format_desired_current_state_diff( @@ -447,56 +557,75 @@ class Ifaces: def gen_dns_metadata(self, dns_state, route_state): iface_metadata = dns_state.gen_metadata(self, route_state) for iface_name, dns_metadata in iface_metadata.items(): - self._ifaces[iface_name].store_dns_metadata(dns_metadata) + self._kernel_ifaces[iface_name].store_dns_metadata(dns_metadata) if dns_state.config_changed: - self._ifaces[iface_name].mark_as_changed() + self._kernel_ifaces[iface_name].mark_as_changed() def gen_route_metadata(self, route_state): iface_metadata = route_state.gen_metadata(self) for iface_name, route_metadata in iface_metadata.items(): - self._ifaces[iface_name].store_route_metadata(route_metadata) + route_state = route_metadata.pop( + BaseIface.ROUTE_CHANGED_METADATA, None + ) + if route_state: + self._kernel_ifaces[iface_name].mark_as_changed() + + self._kernel_ifaces[iface_name].store_route_metadata( + route_metadata + ) def gen_route_rule_metadata(self, route_rule_state, route_state): iface_metadata = route_rule_state.gen_metadata( - route_state, self._ifaces + route_state, self._kernel_ifaces ) for iface_name, route_rule_metadata in iface_metadata.items(): - self._ifaces[iface_name].store_route_rule_metadata( + rule_state = route_rule_metadata.pop( + BaseIface.RULE_CHANGED_METADATA, None + ) + if rule_state: + self._kernel_ifaces[iface_name].mark_as_changed() + + self._kernel_ifaces[iface_name].store_route_rule_metadata( route_rule_metadata ) - if route_rule_state.config_changed: - self._ifaces[iface_name].mark_as_changed() def _validate_unknown_port(self): """ Check the existance of port interface """ - for iface in self._ifaces.values(): + # 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` + for iface in self._kernel_ifaces.values(): for port_name in iface.port: - if not self._ifaces.get(port_name): + if not self._kernel_ifaces.get(port_name): raise NmstateValueError( - f"Interface {iface.name} has unknown port: " - f"{port_name}" + f"Interface {iface.name} has unknown port: {port_name}" ) def _validate_unknown_parent(self): """ Check the existance of parent interface """ - for iface in self._ifaces.values(): - if iface.parent and not self._ifaces.get(iface.parent): - raise NmstateValueError( - f"Interface {iface.name} has unknown parent: " - f"{iface.parent}" - ) + # All child interface should be in kernel space. + 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}" + ) def _remove_unknown_type_interfaces(self): """ Remove unknown type interfaces that are set as up. """ - for iface in list(self._ifaces.values()): + # All the user space interface already has interface type defined. + # Hence no need to check `self._user_space_ifaces` + for iface in list(self._kernel_ifaces.values()): if iface.type == InterfaceType.UNKNOWN and iface.is_up: - self._ifaces.pop(iface.name, None) + self._kernel_ifaces.pop(iface.name, None) logging.debug( f"Interface {iface.name} is type {iface.type} and " "will be ignored during the activation" @@ -507,11 +636,15 @@ class Ifaces: Check whether any port is used by more than one controller """ port_controller_map = {} - for iface in self._ifaces.values(): + for iface in self.all_ifaces(): for port_name in iface.port: cur_controller = port_controller_map.get(port_name) if cur_controller: - cur_controller_iface = self._ifaces.get(cur_controller) + # Only kernel requires each interface to have + # one controller interface at most. + cur_controller_iface = self._kernel_ifaces.get( + cur_controller + ) if ( cur_controller_iface and not cur_controller_iface.is_absent @@ -523,15 +656,23 @@ class Ifaces: else: port_controller_map[port_name] = iface.name - def _remove_ignore_interfaces(self, ignored_iface_names): + def _remove_ignore_interfaces(self, ignored_ifaces): + for iface_name, iface_type, _ in ignored_ifaces: + self._remove_iface(iface_name, iface_type) + + # Only kernel interface can be used as port + ignored_kernel_iface_names = set( + iface_name + for iface_name, _, is_user_space_only in ignored_ifaces + if not is_user_space_only + ) + # Remove ignored port - for iface in self._ifaces.values(): + for iface in self.all_ifaces(): if iface.is_up and iface.is_controller and iface.port: for port_name in iface.port: - if port_name in ignored_iface_names: + if port_name in ignored_kernel_iface_names: iface.remove_port(port_name) - for ignored_iface_name in ignored_iface_names: - self._ifaces.pop(ignored_iface_name, None) def _to_specific_iface_obj(info, save_to_disk): @@ -560,5 +701,7 @@ def _to_specific_iface_obj(info, save_to_disk): return InfiniBandIface(info, save_to_disk) elif iface_type == InterfaceType.MAC_VLAN: return MacVlanIface(info, save_to_disk) + elif iface_type == InterfaceType.MAC_VTAP: + return MacVtapIface(info, save_to_disk) else: return BaseIface(info, save_to_disk) diff --git a/libnmstate/ifaces/infiniband.py b/libnmstate/ifaces/infiniband.py index 0e2623f..5292094 100644 --- a/libnmstate/ifaces/infiniband.py +++ b/libnmstate/ifaces/infiniband.py @@ -75,9 +75,9 @@ class InfiniBandIface(BaseIface): def _cannonicalize_pkey(iface_info): """ - * Set as 0xffff if pkey not defined - * Convert pkey string to integer - * Raise NmstateValueError when out of range(16 bites) + * Set as 0xffff if pkey not defined + * Convert pkey string to integer + * Raise NmstateValueError when out of range(16 bites) """ iface_name = iface_info[Interface.NAME] ib_config = iface_info.get(InfiniBand.CONFIG_SUBTREE, {}) diff --git a/libnmstate/ifaces/linux_bridge.py b/libnmstate/ifaces/linux_bridge.py index 3f4af89..4ae8dad 100644 --- a/libnmstate/ifaces/linux_bridge.py +++ b/libnmstate/ifaces/linux_bridge.py @@ -124,9 +124,9 @@ class LinuxBridgeIface(BridgeIface): super().gen_metadata(ifaces) if not self.is_absent: for port_config in self.port_configs: - ifaces[port_config[LinuxBridge.Port.NAME]].update( - {BridgeIface.BRPORT_OPTIONS_METADATA: port_config} - ) + ifaces.all_kernel_ifaces[ + port_config[LinuxBridge.Port.NAME] + ].update({BridgeIface.BRPORT_OPTIONS_METADATA: port_config}) def remove_port(self, port_name): if self._bridge_config: diff --git a/libnmstate/ifaces/macvlan.py b/libnmstate/ifaces/macvlan.py index 06bccaf..33adbe0 100644 --- a/libnmstate/ifaces/macvlan.py +++ b/libnmstate/ifaces/macvlan.py @@ -26,14 +26,14 @@ from .base_iface import BaseIface class MacVlanIface(BaseIface): @property def parent(self): - return self._macvlan_config.get(MacVlan.BASE_IFACE) + return self.config_subtree.get(MacVlan.BASE_IFACE) @property def need_parent(self): return True @property - def _macvlan_config(self): + def config_subtree(self): return self.raw.get(MacVlan.CONFIG_SUBTREE, {}) @property @@ -50,15 +50,15 @@ class MacVlanIface(BaseIface): super().pre_edit_validation_and_cleanup() def _validate_mode(self): - if self._macvlan_config.get( + if self.config_subtree.get( MacVlan.MODE - ) != MacVlan.Mode.PASSTHRU and not self._macvlan_config.get( + ) != MacVlan.Mode.PASSTHRU and not self.config_subtree.get( MacVlan.PROMISCUOUS ): raise NmstateValueError( "Disable promiscuous is only allowed on passthru mode" ) - if self._macvlan_config.get(MacVlan.MODE) == MacVlan.Mode.UNKNOWN: + if self.config_subtree.get(MacVlan.MODE) == MacVlan.Mode.UNKNOWN: raise NmstateValueError( "Mode unknown is not supported when appying the state" ) @@ -66,8 +66,8 @@ class MacVlanIface(BaseIface): def _validate_mandatory_properties(self): if self.is_up: for prop in (MacVlan.MODE, MacVlan.BASE_IFACE): - if prop not in self._macvlan_config: + if prop not in self.config_subtree: raise NmstateValueError( - f"MacVlan tunnel {self.name} has missing mandatory " - f"property: {prop}" + f"{self.type} tunnel {self.name} has missing mandatory" + f" property: {prop}" ) diff --git a/libnmstate/ifaces/macvtap.py b/libnmstate/ifaces/macvtap.py new file mode 100644 index 0000000..606aa19 --- /dev/null +++ b/libnmstate/ifaces/macvtap.py @@ -0,0 +1,32 @@ +# +# Copyright (c) 2020 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.schema import MacVtap + +from .macvlan import MacVlanIface + + +class MacVtapIface(MacVlanIface): + @property + def parent(self): + return self.config_subtree.get(MacVtap.BASE_IFACE) + + @property + def config_subtree(self): + return self.raw.get(MacVtap.CONFIG_SUBTREE, {}) diff --git a/libnmstate/ifaces/ovs.py b/libnmstate/ifaces/ovs.py index 4fbf5fc..d28de61 100644 --- a/libnmstate/ifaces/ovs.py +++ b/libnmstate/ifaces/ovs.py @@ -45,6 +45,10 @@ class OvsBridgeIface(BridgeIface): self._replace_deprecated_terms() @property + def is_user_space_only(self): + return True + + @property def _has_bond_port(self): for port_config in self.port_configs: if port_config.get(OVSBridge.Port.LINK_AGGREGATION_SUBTREE): @@ -82,16 +86,16 @@ class OvsBridgeIface(BridgeIface): return port def gen_metadata(self, ifaces): - for port_name in self.port: - port_iface = ifaces[port_name] - port_config = _lookup_ovs_port_by_interface( - self.port_configs, port_iface.name + for ovs_iface_name in self.port: + ovs_iface = ifaces.all_kernel_ifaces[ovs_iface_name] + ovs_iface_config = _lookup_ovs_iface_config( + self.port_configs, ovs_iface_name ) - port_iface.update( - {BridgeIface.BRPORT_OPTIONS_METADATA: port_config} + ovs_iface.update( + {BridgeIface.BRPORT_OPTIONS_METADATA: ovs_iface_config} ) - if port_iface.type == InterfaceType.OVS_INTERFACE: - port_iface.parent = self.name + if ovs_iface.type == InterfaceType.OVS_INTERFACE: + ovs_iface.parent = self.name super().gen_metadata(ifaces) def create_virtual_port(self, port_name): @@ -167,13 +171,13 @@ class OvsBridgeIface(BridgeIface): ) -def _lookup_ovs_port_by_interface(ports, port_name): - for port in ports: - lag_state = port.get(OVSBridge.Port.LINK_AGGREGATION_SUBTREE) - if lag_state and _is_ovs_lag_port(lag_state, port_name): - return port - elif port[OVSBridge.Port.NAME] == port_name: - return port +def _lookup_ovs_iface_config(bridge_port_configs, ovs_iface_name): + for port_config in bridge_port_configs: + lag_state = port_config.get(OVSBridge.Port.LINK_AGGREGATION_SUBTREE) + if lag_state and _is_ovs_lag_port(lag_state, ovs_iface_name): + return port_config + elif port_config[OVSBridge.Port.NAME] == ovs_iface_name: + return port_config return {} @@ -282,3 +286,13 @@ def _convert_external_ids_values_to_string(iface_info): def is_ovs_lag_port(port_state): return port_state.get(OVSBridge.Port.LINK_AGGREGATION_SUBTREE) is not None + + +class OvsPortIface(BaseIface): + def __init__(self, info, save_to_disk=True): + super().__init__(info, save_to_disk) + self._parent = None + + @property + def is_user_space_only(self): + return True diff --git a/libnmstate/ifaces/team.py b/libnmstate/ifaces/team.py index e572269..e30eab3 100644 --- a/libnmstate/ifaces/team.py +++ b/libnmstate/ifaces/team.py @@ -18,13 +18,21 @@ # from operator import itemgetter +import warnings from libnmstate.schema import Team from .base_iface import BaseIface +DEPRECATED_PORTS = "ports" + + class TeamIface(BaseIface): + def __init__(self, info, save_to_disk=True): + super().__init__(info, save_to_disk) + self._replace_deprecated_terms() + @property def port(self): ports = self.raw.get(Team.CONFIG_SUBTREE, {}).get( @@ -53,3 +61,9 @@ class TeamIface(BaseIface): s for s in port_config if s[Team.Port.NAME] != port_name ] self.sort_port() + + def _replace_deprecated_terms(self): + team_cfg = self.raw.get(Team.CONFIG_SUBTREE) + if team_cfg and team_cfg.get(DEPRECATED_PORTS): + team_cfg[Team.PORT_SUBTREE] = team_cfg.pop(DEPRECATED_PORTS) + warnings.warn("Using 'ports' is deprecated, use 'port' instead.") diff --git a/libnmstate/net_state.py b/libnmstate/net_state.py index 2d80b53..4936e0b 100644 --- a/libnmstate/net_state.py +++ b/libnmstate/net_state.py @@ -45,7 +45,8 @@ class NetState: current_state.get(Route.KEY), ) self._dns = DnsState( - desire_state.get(DNS.KEY), current_state.get(DNS.KEY), + desire_state.get(DNS.KEY), + current_state.get(DNS.KEY), ) self._route_rule = RouteRuleState( self._route, diff --git a/libnmstate/nispor/base_iface.py b/libnmstate/nispor/base_iface.py index d5f5305..55abb7f 100644 --- a/libnmstate/nispor/base_iface.py +++ b/libnmstate/nispor/base_iface.py @@ -40,10 +40,9 @@ class NisporPluginBaseIface: @property def mac(self): - mac = self._np_iface.mac_address - if not mac: - mac = DEFAULT_MAC_ADDRESS - return mac + if self._np_iface.mac_address: + return self._np_iface.mac_address.upper() + return DEFAULT_MAC_ADDRESS @property def mtu(self): @@ -57,9 +56,9 @@ class NisporPluginBaseIface: def state(self): np_state = self._np_iface.state np_flags = self._np_iface.flags - if np_state == "Up" or "Up" in np_flags or "Running" in np_flags: + if np_state == "up" or "up" in np_flags or "running" in np_flags: return InterfaceState.UP - elif np_state == "Down": + elif np_state == "down": return InterfaceState.DOWN else: logging.debug( diff --git a/libnmstate/nispor/bridge.py b/libnmstate/nispor/bridge.py index 1da5830..5f57f9b 100644 --- a/libnmstate/nispor/bridge.py +++ b/libnmstate/nispor/bridge.py @@ -38,10 +38,10 @@ NISPOR_USER_HZ_KEYS = [ NISPOR_MULTICAST_ROUTERS_INT_MAP = { - "Disabled": 0, - "TempQuery": 1, - "Perm": 2, - "Temp": 3, + "disabled": 0, + "temp_query": 1, + "perm": 2, + "temp": 3, } LSM_BRIDGE_OPTIONS_2_NISPOR = { @@ -127,7 +127,9 @@ def _nispor_value_to_lsm(np_name, np_value): if np_name == "multicast_router": value = NISPOR_MULTICAST_ROUTERS_INT_MAP.get(np_value) elif np_name == "stp_state": - value = np_value in ("KernelStp", "UserStp") + value = np_value in ("kernel_stp", "user_stp") + elif np_name == "group_addr": + value = value.upper() return value diff --git a/libnmstate/nispor/macvtap.py b/libnmstate/nispor/macvtap.py new file mode 100644 index 0000000..b214961 --- /dev/null +++ b/libnmstate/nispor/macvtap.py @@ -0,0 +1,53 @@ +# +# Copyright (c) 2020 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.schema import InterfaceType +from libnmstate.schema import MacVtap + +from .macvlan import NisporPluginMacVlanIface + + +MACVTAP_MODES = { + "unknown": MacVtap.Mode.UNKNOWN, + "vepa": MacVtap.Mode.VEPA, + "bridge": MacVtap.Mode.BRIDGE, + "private": MacVtap.Mode.PRIVATE, + "passthru": MacVtap.Mode.PASSTHRU, + "source": MacVtap.Mode.SOURCE, +} + + +class NisporPluginMacVtapIface(NisporPluginMacVlanIface): + @property + def type(self): + return InterfaceType.MAC_VTAP + + def to_dict(self): + info = super().to_dict() + info[MacVtap.CONFIG_SUBTREE] = { + MacVtap.BASE_IFACE: self._np_iface.base_iface, + MacVtap.MODE: MACVTAP_MODES.get( + self._np_iface.mode, MacVtap.Mode.UNKNOWN + ), + MacVtap.PROMISCUOUS: self._flag_to_promiscuous( + self._np_iface.mac_vlan_flags + ), + } + + return info diff --git a/libnmstate/nispor/plugin.py b/libnmstate/nispor/plugin.py index 3803deb..f36a1c4 100644 --- a/libnmstate/nispor/plugin.py +++ b/libnmstate/nispor/plugin.py @@ -21,6 +21,7 @@ from nispor import NisporNetState from libnmstate.plugin import NmstatePlugin from libnmstate.schema import Route +from libnmstate.schema import RouteRule from .base_iface import NisporPluginBaseIface from .bond import NisporPluginBondIface @@ -28,9 +29,11 @@ from .bridge import NisporPluginBridgeIface from .dummy import NisporPluginDummyIface from .ethernet import NisporPluginEthernetIface from .macvlan import NisporPluginMacVlanIface +from .macvtap import NisporPluginMacVtapIface from .vlan import NisporPluginVlanIface from .vxlan import NisporPluginVxlanIface from .route import nispor_route_state_to_nmstate +from .route_rule import nispor_route_rule_state_to_nmstate from .vrf import NisporPluginVrfIface from .ovs import NisporPluginOvsInternalIface @@ -45,6 +48,7 @@ class NisporPlugin(NmstatePlugin): return [ NmstatePlugin.PLUGIN_CAPABILITY_IFACE, NmstatePlugin.PLUGIN_CAPABILITY_ROUTE, + NmstatePlugin.PLUGIN_CAPABILITY_ROUTE_RULE, ] @property @@ -59,21 +63,23 @@ class NisporPlugin(NmstatePlugin): ifaces = [] for np_iface in np_state.ifaces.values(): iface_type = np_iface.type - if iface_type == "Dummy": + if iface_type == "dummy": ifaces.append(NisporPluginDummyIface(np_iface).to_dict()) - elif iface_type == "Veth": + elif iface_type == "veth": ifaces.append(NisporPluginEthernetIface(np_iface).to_dict()) - elif iface_type == "Ethernet": + elif iface_type == "ethernet": ifaces.append(NisporPluginEthernetIface(np_iface).to_dict()) - elif iface_type == "Bond": + elif iface_type == "bond": ifaces.append(NisporPluginBondIface(np_iface).to_dict()) - elif iface_type == "Vlan": + elif iface_type == "vlan": ifaces.append(NisporPluginVlanIface(np_iface).to_dict()) - elif iface_type == "Vxlan": + elif iface_type == "vxlan": ifaces.append(NisporPluginVxlanIface(np_iface).to_dict()) - elif iface_type == "MacVlan": + elif iface_type == "mac_vlan": ifaces.append(NisporPluginMacVlanIface(np_iface).to_dict()) - elif iface_type == "Bridge": + elif iface_type == "mac_vtap": + ifaces.append(NisporPluginMacVtapIface(np_iface).to_dict()) + elif iface_type == "bridge": np_ports = [] for port_name in np_iface.ports: if port_name in np_state.ifaces.keys(): @@ -81,9 +87,9 @@ class NisporPlugin(NmstatePlugin): ifaces.append( NisporPluginBridgeIface(np_iface, np_ports).to_dict() ) - elif iface_type == "Vrf": + elif iface_type == "vrf": ifaces.append(NisporPluginVrfIface(np_iface).to_dict()) - elif iface_type == "OpenvSwitch": + elif iface_type == "openv_switch": ifaces.append(NisporPluginOvsInternalIface(np_iface).to_dict()) else: ifaces.append(NisporPluginBaseIface(np_iface).to_dict()) @@ -92,3 +98,11 @@ class NisporPlugin(NmstatePlugin): def get_routes(self): np_state = NisporNetState.retrieve() return {Route.RUNNING: nispor_route_state_to_nmstate(np_state.routes)} + + def get_route_rules(self): + np_state = NisporNetState.retrieve() + return { + RouteRule.CONFIG: nispor_route_rule_state_to_nmstate( + np_state.route_rules + ) + } diff --git a/libnmstate/nispor/route.py b/libnmstate/nispor/route.py index 852a17f..f4a445d 100644 --- a/libnmstate/nispor/route.py +++ b/libnmstate/nispor/route.py @@ -28,7 +28,7 @@ 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 == "universe" ] @@ -38,7 +38,7 @@ def _nispor_route_to_nmstate(np_rt): elif np_rt.gateway: destination = ( IPV6_DEFAULT_GATEWAY_DESTINATION - if np_rt.address_family == "IPv6" + if np_rt.address_family == "ipv6" else IPV4_DEFAULT_GATEWAY_DESTINATION ) else: diff --git a/libnmstate/nispor/route_rule.py b/libnmstate/nispor/route_rule.py new file mode 100644 index 0000000..057299c --- /dev/null +++ b/libnmstate/nispor/route_rule.py @@ -0,0 +1,45 @@ +# +# Copyright (c) 2020 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.schema import RouteRule + + +NISPOR_RULE_ACTION_TABLE = "table" + + +def nispor_route_rule_state_to_nmstate(np_rules): + return [ + _nispor_route_rule_to_nmstate(rl) + for rl in np_rules + if (rl.src or rl.dst) and rl.action == NISPOR_RULE_ACTION_TABLE + ] + + +def _nispor_route_rule_to_nmstate(np_rl): + rule = { + RouteRule.ROUTE_TABLE: np_rl.table, + RouteRule.PRIORITY: np_rl.priority + if np_rl.priority + else RouteRule.USE_DEFAULT_PRIORITY, + } + if np_rl.src: + rule[RouteRule.IP_FROM] = np_rl.src + if np_rl.dst: + rule[RouteRule.IP_TO] = np_rl.dst + return rule diff --git a/libnmstate/nm/__init__.py b/libnmstate/nm/__init__.py index e9bb3cf..5f42e90 100644 --- a/libnmstate/nm/__init__.py +++ b/libnmstate/nm/__init__.py @@ -17,35 +17,6 @@ # along with this program. If not, see . # -from . import bond -from . import bridge -from . import checkpoint -from . import connection -from . import device -from . import dns -from . import ipv4 -from . import ipv6 -from . import ovs -from . import profile -from . import translator -from . import user -from . import vlan -from . import wired from .plugin import NetworkManagerPlugin - -bond -bridge -checkpoint -connection -device -dns -ipv4 -ipv6 -ovs -profile -translator -user -vlan -wired NetworkManagerPlugin diff --git a/libnmstate/nm/active_connection.py b/libnmstate/nm/active_connection.py index b914258..d72d08d 100644 --- a/libnmstate/nm/active_connection.py +++ b/libnmstate/nm/active_connection.py @@ -20,93 +20,334 @@ import logging from libnmstate.error import NmstateLibnmError +from libnmstate.error import NmstateInternalError from .common import GLib -from .common import GObject from .common import NM +from .device import get_nm_dev +from .device import get_iface_type +from .device import mark_device_as_managed +from .ipv4 import is_dynamic as is_ipv4_dynamic +from .ipv6 import is_dynamic as is_ipv6_dynamic NM_AC_STATE_CHANGED_SIGNAL = "state-changed" -class ActivationError(Exception): - pass +def is_activated(nm_ac, nm_dev): + if not (nm_ac and nm_dev): + return False + state = nm_ac.get_state() + if state == NM.ActiveConnectionState.ACTIVATED: + return True + elif state == NM.ActiveConnectionState.ACTIVATING: + ac_state_flags = nm_ac.get_state_flags() + nm_flags = NM.ActivationStateFlags + ip4_is_dynamic = is_ipv4_dynamic(nm_ac) + ip6_is_dynamic = is_ipv6_dynamic(nm_ac) + if ( + ac_state_flags & nm_flags.IS_MASTER + or (ip4_is_dynamic and ac_state_flags & nm_flags.IP6_READY) + or (ip6_is_dynamic and ac_state_flags & nm_flags.IP4_READY) + or (ip4_is_dynamic and ip6_is_dynamic) + ): + # For interface meet any condition below will be + # treated as activated when reach IP_CONFIG state: + # * Is controller device. + # * DHCPv4 enabled with IP6_READY flag. + # * DHCPv6/Autoconf with IP4_READY flag. + # * DHCPv4 enabled with DHCPv6/Autoconf enabled. + return ( + NM.DeviceState.IP_CONFIG + <= nm_dev.get_state() + <= NM.DeviceState.ACTIVATED + ) -class ActiveConnection: - def __init__(self, context=None, nm_ac_con=None): - self._ctx = context - self._act_con = nm_ac_con + return False - nmdevs = None - if nm_ac_con: - nmdevs = nm_ac_con.get_devices() - self._nmdev = nmdevs[0] if nmdevs else None - def import_by_device(self, nmdev=None): - assert self._act_con is None +def is_activating(nm_ac, nm_dev): + if not nm_ac or not nm_dev: + return True + if nm_dev.get_state_reason() == NM.DeviceStateReason.NEW_ACTIVATION: + return True - if nmdev: - self._nmdev = nmdev - if self._nmdev: - self._act_con = self._nmdev.get_active_connection() + return ( + nm_ac.get_state() == NM.ActiveConnectionState.ACTIVATING + ) and not is_activated(nm_ac, nm_dev) - def deactivate(self): - """ - Deactivating the current active connection, - The profile itself is not removed. - For software devices, deactivation removes the devices from the kernel. - """ - act_connection = self._nmdev.get_active_connection() - if ( - not act_connection - or act_connection.props.state - == NM.ActiveConnectionState.DEACTIVATED - ): +class ProfileActivation: + def __init__(self, ctx, iface_name, iface_type, nm_profile, nm_dev): + self._ctx = ctx + self._iface_name = iface_name + self._iface_type = iface_type + self._nm_ac = None + self._nm_dev = nm_dev + self._nm_profile = nm_profile + self._ac_handlers = set() + self._dev_handlers = set() + self._action = None + + def run(self): + specific_object = None + self._action = ( + f"Activate profile uuid:{self._nm_profile.get_uuid()} " + f"iface:{self._iface_name} type: {self._iface_type}" + ) + if self._nm_dev: + # Workaround of https://bugzilla.redhat.com/1880420 + mark_device_as_managed(self._ctx, self._nm_dev) + + user_data = None + self._ctx.register_async(self._action) + self._ctx.client.activate_connection_async( + self._nm_profile, + self._nm_dev, + specific_object, + self._ctx.cancellable, + self._activate_profile_callback, + user_data, + ) + + @staticmethod + def wait(ctx, nm_ac, nm_dev): + activation = ProfileActivation( + ctx, + nm_dev.get_iface(), + get_iface_type(nm_dev), + None, + nm_dev, + ) + activation._nm_ac = nm_ac + activation._action = ( + f"Waiting activation of {activation._iface_name} " + f"{activation._iface_type}" + ) + ctx.register_async(activation._action) + activation._wait_profile_activation() + + def _activate_profile_callback(self, nm_client, result, _user_data): + nm_ac = None + if self._ctx.is_cancelled(): + self._activation_clean_up() return + try: + nm_ac = nm_client.activate_connection_finish(result) + except Exception as e: + self._ctx.fail( + NmstateLibnmError(f"{self._action} failed: error={e}") + ) + + if nm_ac is None: + self._ctx.fail( + NmstateLibnmError( + f"{self._action} failed: " + "error='None return from activate_connection_finish()'" + ) + ) + else: + logging.debug( + f"Connection activation initiated: iface={self._iface_name} " + f"type={self._iface_type} con-state={nm_ac.get_state()}" + ) + self._nm_ac = nm_ac + self._nm_dev = get_nm_dev( + self._ctx, self._iface_name, self._iface_type + ) + self._wait_profile_activation() - if self._act_con != act_connection: - raise NmstateLibnmError( - "When deactivating active connection, the newly get " - f"NM.ActiveConnection {act_connection}" - f"is different from original request: {self._act_con}" + def _wait_profile_activation(self): + if is_activated(self._nm_ac, self._nm_dev): + logging.debug( + "Connection activation succeeded: " + f"iface={self._iface_name}, type={self._iface_type}, " + f"con_state={self._nm_ac.get_state()}, " + f"dev_state={self._nm_dev.get_state()}, " + f"state_flags={self._nm_ac.get_state_flags()}" + ) + self._activation_clean_up() + self._ctx.finish_async(self._action) + elif is_activating(self._nm_ac, self._nm_dev): + if self._nm_ac: + self._wait_nm_ac_activation() + if self._nm_dev: + self._wait_nm_dev_activation() + if not self._nm_ac and not self._nm_dev: + self._ctx.fail( + NmstateInternalError( + f"{self._action} failed: no nm_ac or nm_dev" + ) + ) + else: + if self._nm_dev: + error_msg = ( + f"Connection {self._nm_profile.get_uuid()} failed: " + f"state={self._nm_ac.get_state()} " + f"reason={self._nm_ac.get_state_reason()} " + f"dev_state={self._nm_dev.get_state()} " + f"dev_reason={self._nm_dev.get_state_reason()}" + ) + else: + error_msg = ( + f"Connection {self._nm_profile.get_uuid()} failed: " + f"state={self._nm_ac.get_state()} " + f"reason={self._nm_ac.get_state_reason()} dev=None" + ) + self._activation_clean_up() + logging.error(error_msg) + self._ctx.fail( + NmstateLibnmError(f"{self._action} failed: {error_msg}") ) - action = f"Deactivate profile: {self.devname}" + def _activation_clean_up(self): + self._remove_ac_handlers() + self._remove_dev_handlers() + + def _remove_ac_handlers(self): + for handler_id in self._ac_handlers: + self._nm_ac.handler_disconnect(handler_id) + self._ac_handlers = set() + + def _remove_dev_handlers(self): + for handler_id in self._dev_handlers: + self._nm_dev.handler_disconnect(handler_id) + self._dev_handlers = set() + + def _wait_nm_ac_activation(self): + user_data = None + self._ac_handlers.add( + self._nm_ac.connect( + NM_AC_STATE_CHANGED_SIGNAL, + self._ac_state_change_callback, + user_data, + ) + ) + self._ac_handlers.add( + self._nm_ac.connect( + "notify::state-flags", + self._ac_state_flags_change_callback, + user_data, + ) + ) + + def _ac_state_change_callback(self, _nm_ac, _state, _reason, _user_data): + if self._ctx.is_cancelled(): + self._activation_clean_up() + return + self._activation_progress_check() + + def _ac_state_flags_change_callback(self, _nm_ac, _state, _user_data): + if self._ctx.is_cancelled(): + self._activation_clean_up() + return + self._activation_progress_check() + + def _wait_nm_dev_activation(self): + user_data = None + self._dev_handlers.add( + self._nm_dev.connect( + "state-changed", self._dev_state_change_callback, user_data + ) + ) + + def _dev_state_change_callback( + self, _nm_dev, _new_state, _old_state, _reason, _user_data + ): + if self._ctx.is_cancelled(): + self._activation_clean_up() + return + self._activation_progress_check() + + def _activation_progress_check(self): + cur_nm_dev = get_nm_dev(self._ctx, self._iface_name, self._iface_type) + if cur_nm_dev and cur_nm_dev != self._nm_dev: + logging.debug( + f"The NM.Device of profile {self._iface_name} " + f"{self._iface_type} changed" + ) + self._remove_dev_handlers() + self._nm_dev = cur_nm_dev + self._wait_nm_dev_activation() + + if cur_nm_dev: + cur_nm_ac = cur_nm_dev.get_active_connection() + if cur_nm_ac and cur_nm_ac != self._nm_ac: + logging.debug( + f"Active connection of device {self._iface_name} " + "has been replaced" + ) + self._remove_ac_handlers() + self._nm_ac = cur_nm_ac + self._wait_nm_ac_activation() + + if is_activated(self._nm_ac, self._nm_dev): + logging.debug( + "Connection activation succeeded: " + f"iface={self._iface_name}, type={self._iface_type}, " + f"con_state={self._nm_ac.get_state()}, " + f"dev_state={self._nm_dev.get_state()}, " + f"state_flags={self._nm_ac.get_state_flags()}" + ) + self._activation_clean_up() + self._ctx.finish_async(self._action) + elif not is_activating(self._nm_ac, self._nm_dev): + reason = f"{self._nm_ac.get_state_reason()}" + if self._nm_dev: + reason += f" {self._nm_dev.get_state_reason()}" + self._activation_clean_up() + self._ctx.fail( + NmstateLibnmError(f"{self._action} failed: reason={reason}") + ) + + +class ActiveConnectionDeactivate: + def __init__(self, ctx, iface_name, iface_type, nm_ac): + self._ctx = ctx + self._iface_name = iface_name + self._iface_type = iface_type + self._nm_ac = nm_ac + + def run(self): + if self._nm_ac.props.state == NM.ActiveConnectionState.DEACTIVATED: + return + + action = f"Deactivate profile: {self._iface_name} {self._iface_type}" self._ctx.register_async(action) - handler_id = act_connection.connect( + handler_id = self._nm_ac.connect( NM_AC_STATE_CHANGED_SIGNAL, self._wait_state_changed_callback, action, ) - if act_connection.props.state != NM.ActiveConnectionState.DEACTIVATING: + if self._nm_ac.props.state != NM.ActiveConnectionState.DEACTIVATING: user_data = (handler_id, action) self._ctx.client.deactivate_connection_async( - act_connection, + self._nm_ac, self._ctx.cancellable, self._deactivate_connection_callback, user_data, ) - def _wait_state_changed_callback(self, act_con, state, reason, action): + def _wait_state_changed_callback(self, nm_ac, state, reason, action): if self._ctx.is_cancelled(): return - if act_con.props.state == NM.ActiveConnectionState.DEACTIVATED: + if nm_ac.props.state == NM.ActiveConnectionState.DEACTIVATED: logging.debug( - "Connection deactivation succeeded on %s", self.devname, + "Connection deactivation succeeded on %s", + self._iface_name, ) self._ctx.finish_async(action) - def _deactivate_connection_callback(self, src_object, result, user_data): + def _deactivate_connection_callback(self, nm_client, result, user_data): handler_id, action = user_data if self._ctx.is_cancelled(): - if self._act_con: - self._act_con.handler_disconnect(handler_id) + if self._nm_ac: + self._nm_ac.handler_disconnect(handler_id) return try: - success = src_object.deactivate_connection_finish(result) + success = nm_client.deactivate_connection_finish(result) except GLib.Error as e: if e.matches( NM.ManagerError.quark(), NM.ManagerError.CONNECTIONNOTACTIVE @@ -114,67 +355,35 @@ class ActiveConnection: success = True logging.debug( "Connection is not active on {}, no need to " - "deactivate".format(self.devname) + "deactivate".format(self._iface_name) ) - if self._act_con: - self._act_con.handler_disconnect(handler_id) + if self._nm_ac: + self._nm_ac.handler_disconnect(handler_id) self._ctx.finish_async(action) else: - if self._act_con: - self._act_con.handler_disconnect(handler_id) + if self._nm_ac: + self._nm_ac.handler_disconnect(handler_id) self._ctx.fail( NmstateLibnmError(f"{action} failed: error={e}") ) return except Exception as e: - if self._act_con: - self._act_con.handler_disconnect(handler_id) + if self._nm_ac: + self._nm_ac.handler_disconnect(handler_id) self._ctx.fail( NmstateLibnmError( - f"BUG: Unexpected error when activating {self.devname} " - f"error={e}" + "BUG: Unexpected error when activating " + f"{self._iface_name} error={e}" ) ) return if not success: - if self._act_con: - self._act_con.handler_disconnect(handler_id) + if self._nm_ac: + self._nm_ac.handler_disconnect(handler_id) self._ctx.fail( NmstateLibnmError( f"{action} failed: error='None returned from " "deactivate_connection_finish()'" ) ) - - @property - def nm_active_connection(self): - return self._act_con - - @property - def devname(self): - if self._nmdev: - return self._nmdev.get_iface() - else: - return None - - @property - def nmdevice(self): - return self._nmdev - - @nmdevice.setter - def nmdevice(self, nmdev): - assert self._nmdev is None - self._nmdev = nmdev - - -def _is_device_controller_type(nmdev): - if nmdev: - is_controller_type = ( - GObject.type_is_a(nmdev, NM.DeviceBond) - or GObject.type_is_a(nmdev, NM.DeviceBridge) - or GObject.type_is_a(nmdev, NM.DeviceTeam) - or GObject.type_is_a(nmdev, NM.DeviceOvsBridge) - ) - return is_controller_type - return False diff --git a/libnmstate/nm/checkpoint.py b/libnmstate/nm/checkpoint.py index 2927e62..4edfb5a 100644 --- a/libnmstate/nm/checkpoint.py +++ b/libnmstate/nm/checkpoint.py @@ -22,11 +22,11 @@ import logging from libnmstate.error import NmstateConflictError from libnmstate.error import NmstateLibnmError from libnmstate.error import NmstatePermissionError -from libnmstate.ifaces.base_iface import BaseIface -from libnmstate.nm import profile -from libnmstate.nm import common -from libnmstate.schema import Interface -from .profile_state import is_activated + +from .active_connection import is_activating +from .active_connection import ProfileActivation +from .common import GLib +from .common import NM def get_checkpoints(nm_client): @@ -54,8 +54,8 @@ class CheckPoint: devs = [] timeout = self._timeout cp_flags = ( - common.NM.CheckpointCreateFlags.DELETE_NEW_CONNECTIONS - | common.NM.CheckpointCreateFlags.DISCONNECT_NEW_DEVICES + NM.CheckpointCreateFlags.DELETE_NEW_CONNECTIONS + | NM.CheckpointCreateFlags.DISCONNECT_NEW_DEVICES ) self._ctx.register_async("Create checkpoint") @@ -71,9 +71,7 @@ class CheckPoint: self._add_checkpoint_refresh_timeout() def _add_checkpoint_refresh_timeout(self): - self._timeout_source = common.GLib.timeout_source_new( - self._timeout * 500 - ) + self._timeout_source = GLib.timeout_source_new(self._timeout * 500) self._timeout_source.set_callback( self._refresh_checkpoint_timeout, None ) @@ -94,9 +92,9 @@ class CheckPoint: self._ctx.client.checkpoint_adjust_rollback_timeout( self._dbuspath, self._timeout, cancellable, cb, cb_data ) - return common.GLib.SOURCE_CONTINUE + return GLib.SOURCE_CONTINUE else: - return common.GLib.SOURCE_REMOVE + return GLib.SOURCE_REMOVE def destroy(self): if self._dbuspath: @@ -150,10 +148,10 @@ class CheckPoint: self._ctx.fail( NmstateLibnmError(f"Checkpoint create failed: {error_msg}") ) - except common.GLib.Error as e: + except GLib.Error as e: if e.matches( - common.NM.ManagerError.quark(), - common.NM.ManagerError.PERMISSIONDENIED, + NM.ManagerError.quark(), + NM.ManagerError.PERMISSIONDENIED, ): self._ctx.fail( NmstatePermissionError( @@ -162,8 +160,8 @@ class CheckPoint: ) ) elif e.matches( - common.NM.ManagerError.quark(), - common.NM.ManagerError.INVALIDARGUMENTS, + NM.ManagerError.quark(), + NM.ManagerError.INVALIDARGUMENTS, ): self._ctx.fail( NmstateConflictError( @@ -200,21 +198,13 @@ class CheckPoint: if nm_dev and ( ( nm_dev.get_state_reason() - == common.NM.DeviceStateReason.NEW_ACTIVATION + == NM.DeviceStateReason.NEW_ACTIVATION ) - or nm_dev.get_state() == common.NM.DeviceState.IP_CONFIG + or nm_dev.get_state() == NM.DeviceState.IP_CONFIG ): nm_ac = nm_dev.get_active_connection() - if not is_activated(nm_ac, nm_dev): - nm_profile = profile.NmProfile( - self._ctx, None, BaseIface({Interface.NAME: iface}) - ) - nm_profile.nmdev = nm_dev - action = f"Waiting for rolling back {iface}" - self._ctx.register_async(action) - nm_profile.profile_state.wait_dev_activation( - action, nm_profile - ) + if is_activating(nm_ac, nm_dev): + ProfileActivation.wait(self._ctx, nm_ac, nm_dev) if ret[path] != 0: logging.error(f"Interface {iface} rollback failed") else: diff --git a/libnmstate/nm/connection.py b/libnmstate/nm/connection.py index d29988e..ec835bb 100644 --- a/libnmstate/nm/connection.py +++ b/libnmstate/nm/connection.py @@ -17,12 +17,45 @@ # along with this program. If not, see . # +# Handle the NM.SimpleConnection related stuff + import uuid +from libnmstate.schema import Interface +from libnmstate.schema import InterfaceType +from libnmstate.schema import LinuxBridge as LB +from libnmstate.schema import MacVlan +from libnmstate.schema import MacVtap +from libnmstate.schema import OVSBridge as OvsB +from libnmstate.schema import OVSInterface +from libnmstate.schema import VRF + +from libnmstate.ifaces.bridge import BridgeIface + +from .bond import create_setting as create_bond_setting +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 - - -class ConnectionSetting: +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 +from .lldp import apply_lldp_setting +from .macvlan import create_setting as create_macvlan_setting +from .ovs import create_bridge_setting as create_ovs_bridge_setting +from .ovs import create_interface_setting as create_ovs_interface_setting +from .ovs import create_port_setting as create_ovs_port_setting +from .sriov import create_setting as create_sriov_setting +from .team import create_setting as create_team_setting +from .translator import Api2Nm +from .user import create_setting as create_user_setting +from .vlan import create_setting as create_vlan_setting +from .vrf import create_vrf_setting +from .vxlan import create_setting as create_vxlan_setting +from .wired import create_setting as create_wired_setting + + +class _ConnectionSetting: def __init__(self, con_setting=None): self._setting = con_setting @@ -64,24 +97,117 @@ class ConnectionSetting: return self._setting -def get_device_active_connection(nm_device): - active_conn = None - if nm_device: - active_conn = nm_device.get_active_connection() - return active_conn +def create_new_nm_simple_conn(iface, nm_profile): + nm_iface_type = Api2Nm.get_iface_type(iface.type) + iface_info = iface.to_dict() + settings = [ + create_ipv4_setting(iface_info.get(Interface.IPV4), 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) + con_setting.set_profile_name(iface.name) + else: + con_setting.create(iface.name, iface.name, nm_iface_type) + + apply_lldp_setting(con_setting, iface_info) + + controller = iface.controller + controller_type = iface.controller_type + if controller_type == InterfaceType.LINUX_BRIDGE: + controller_type = NM_LINUX_BRIDGE_TYPE + con_setting.set_controller(controller, controller_type) + settings.append(con_setting.setting) + + # Only apply wired/ethernet configuration based on original desire + # state rather than the merged one. + original_state_wired = {} + if iface.is_desired: + original_state_wired = iface.original_dict + if iface.type != InterfaceType.INFINIBAND: + # The IP over InfiniBand has its own setting for MTU and does not + # have ethernet layer. + wired_setting = create_wired_setting(original_state_wired, nm_profile) + + if wired_setting: + settings.append(wired_setting) + + user_setting = create_user_setting(iface_info, nm_profile) + if user_setting: + settings.append(user_setting) + + if iface.type == InterfaceType.BOND: + settings.append(create_bond_setting(iface, wired_setting, nm_profile)) + elif iface.type == InterfaceType.LINUX_BRIDGE: + bridge_config = iface_info.get(LB.CONFIG_SUBTREE, {}) + bridge_options = bridge_config.get(LB.OPTIONS_SUBTREE) + bridge_ports = bridge_config.get(LB.PORT_SUBTREE) + if bridge_options or bridge_ports: + linux_bridge_setting = create_linux_bridge_setting( + iface_info, + nm_profile, + iface.original_dict, + ) + settings.append(linux_bridge_setting) + elif iface.type == InterfaceType.OVS_BRIDGE: + ovs_bridge_state = iface_info.get(OvsB.CONFIG_SUBTREE, {}) + ovs_bridge_options = ovs_bridge_state.get(OvsB.OPTIONS_SUBTREE) + if ovs_bridge_options: + settings.append(create_ovs_bridge_setting(ovs_bridge_options)) + elif iface.type == InterfaceType.OVS_PORT: + ovs_port_options = iface_info.get(OvsB.OPTIONS_SUBTREE) + settings.append(create_ovs_port_setting(ovs_port_options)) + elif iface.type == InterfaceType.OVS_INTERFACE: + patch_state = iface_info.get(OVSInterface.PATCH_CONFIG_SUBTREE) + settings.extend(create_ovs_interface_setting(patch_state)) + elif iface.type == InterfaceType.INFINIBAND: + ib_setting = create_infiniband_setting( + iface_info, + nm_profile, + iface.original_dict, + ) + if ib_setting: + settings.append(ib_setting) + + bridge_port_options = iface_info.get(BridgeIface.BRPORT_OPTIONS_METADATA) + if ( + bridge_port_options + and iface.controller_type == InterfaceType.LINUX_BRIDGE + ): + settings.append( + create_linux_bridge_port_setting(bridge_port_options, nm_profile) + ) + + vlan_setting = create_vlan_setting(iface_info, nm_profile) + if vlan_setting: + settings.append(vlan_setting) + + vxlan_setting = create_vxlan_setting(iface_info, nm_profile) + if vxlan_setting: + settings.append(vxlan_setting) + sriov_setting = create_sriov_setting(iface_info, nm_profile) + if sriov_setting: + settings.append(sriov_setting) -def list_connections_by_ifname(context, ifname): - return [ - con - for con in context.client.get_connections() - if con.get_interface_name() == ifname - ] + team_setting = create_team_setting(iface_info, nm_profile) + if team_setting: + settings.append(team_setting) + if VRF.CONFIG_SUBTREE in iface_info: + settings.append(create_vrf_setting(iface_info[VRF.CONFIG_SUBTREE])) + + if MacVlan.CONFIG_SUBTREE in iface_info: + settings.append(create_macvlan_setting(iface_info, nm_profile)) + + if MacVtap.CONFIG_SUBTREE in iface_info: + settings.append( + create_macvlan_setting(iface_info, nm_profile, tap=True) + ) -def create_new_simple_connection(settings): - simple_conn = NM.SimpleConnection.new() + nm_simple_conn = NM.SimpleConnection.new() for setting in settings: - simple_conn.add_setting(setting) + nm_simple_conn.add_setting(setting) - return simple_conn + return nm_simple_conn diff --git a/libnmstate/nm/context.py b/libnmstate/nm/context.py index 373ffe8..9db8592 100644 --- a/libnmstate/nm/context.py +++ b/libnmstate/nm/context.py @@ -55,6 +55,10 @@ class NmContext: self._init_queue() self._init_cancellable() + def _init_client(self): + self._client = NM.Client.new(cancellable=None) + self._context = self._client.get_main_context() + def _init_queue(self): self._fast_queue = set() self._slow_queue = set() @@ -208,6 +212,8 @@ class NmContext: if self._error: # The queue and error should be flush and perpare for another run + self._cancellable.cancel() + self.refresh_content() self._init_queue() self._init_cancellable() tmp_error = self._error @@ -215,6 +221,3 @@ class NmContext: # pylint: disable=raising-bad-type raise tmp_error # pylint: enable=raising-bad-type - - def get_nm_dev(self, iface_name): - return self.client.get_device_by_iface(iface_name) diff --git a/libnmstate/nm/device.py b/libnmstate/nm/device.py index 66e194e..b2f4d17 100644 --- a/libnmstate/nm/device.py +++ b/libnmstate/nm/device.py @@ -20,122 +20,125 @@ import logging from libnmstate.error import NmstateLibnmError +from libnmstate.schema import InterfaceType -from . import active_connection as ac -from . import profile_state from .common import NM from .common import GLib +from .macvlan import is_macvtap +from .translator import Nm2Api NM_DBUS_INTERFACE_DEVICE = f"{NM.DBUS_INTERFACE}.Device" NM_USE_DEFAULT_TIMEOUT_VALUE = -1 -def deactivate(context, dev): - """ - Deactivating the current active connection, - The profile itself is not removed. - - For software devices, deactivation removes the devices from the kernel. - """ - act_con = ac.ActiveConnection(context) - act_con.nmdevice = dev - act_con.import_by_device() - act_con.deactivate() - - -def modify(context, nm_profile): - """ - Modify the given connection profile on the device. - Implemented by the reapply operation with a fallback to the - connection profile activation. - """ - nm_ac = nm_profile.nmdev.get_active_connection() - if profile_state.is_activated(nm_ac, nm_profile.nmdev): +class DeviceReapply: + def __init__( + self, + ctx, + iface_name, + iface_type, + nm_dev, + nm_simple_conn, + profile_activation, + ): + self._ctx = ctx + self._iface_name = iface_name + self._iface_type = iface_type + self._nm_dev = nm_dev + self._nm_simple_conn = nm_simple_conn + self._profile_activation = profile_activation + + def run(self): + """ + Modify the given connection profile on the device without bring the + interface down. + If failed, fall back to normal profile activation + """ version_id = 0 flags = 0 - action = f"Reapply device config: {nm_profile.nmdev.get_iface()}" - context.register_async(action) - user_data = context, nm_profile, action - nm_profile.nmdev.reapply_async( - nm_profile.profile, + action = ( + f"Reapply device config: {self._iface_name} {self._iface_type} " + f"{self._nm_simple_conn.get_uuid()}" + ) + self._ctx.register_async(action) + user_data = action + self._nm_dev.reapply_async( + self._nm_simple_conn, version_id, flags, - context.cancellable, - _modify_callback, + self._ctx.cancellable, + self._reapply_callback, user_data, ) - else: - _activate_async(context, nm_profile) - - -def _modify_callback(src_object, result, user_data): - context, nm_profile, action = user_data - if context.is_cancelled(): - return - devname = src_object.get_iface() - try: - success = src_object.reapply_finish(result) - except Exception as e: - logging.debug( - "Device reapply failed on %s: error=%s\n" - "Fallback to device activation", - devname, - e, - ) - context.finish_async(action, suppress_log=True) - _activate_async(context, nm_profile) - return - - if success: - context.finish_async(action) - else: - logging.debug( - "Device reapply failed, fallback to device activation: dev=%s, " - "error='None returned from reapply_finish()'", - devname, - ) - context.finish_async(action, suppress_log=True) - _activate_async(context, nm_profile) - -def _activate_async(context, nm_profile): - if nm_profile.nmdev: - # Workaround of https://bugzilla.redhat.com/show_bug.cgi?id=1772470 - mark_device_as_managed(context, nm_profile.nmdev) - nm_profile.activate() - - -def delete_device(context, nmdev): - iface_name = nmdev.get_iface() - if iface_name: - action = f"Delete device: {nmdev.get_iface()}" - user_data = context, nmdev, action, nmdev.get_iface() - context.register_async(action) - nmdev.delete_async( - context.cancellable, _delete_device_callback, user_data + def _reapply_callback(self, nm_dev, result, user_data): + action = user_data + if self._ctx.is_cancelled(): + return + try: + success = nm_dev.reapply_finish(result) + except Exception as e: + logging.debug( + f"Device reapply failed on {self._iface_name} " + f"{self._iface_type}: error={e}, " + "Fallback to device activation" + ) + self._ctx.finish_async(action, suppress_log=True) + self._profile_activation.run() + return + + if success: + self._ctx.finish_async(action) + else: + logging.debug( + "Device reapply failed, fallback to device activation: " + f"iface={self._iface_name}, type={self._iface_type} " + "error='None returned from reapply_finish()'" + ) + self._ctx.finish_async(action, suppress_log=True) + self._profile_activation.run() + + +class DeviceDelete: + def __init__(self, ctx, iface_name, iface_type, nm_dev): + self._ctx = ctx + self._iface_name = iface_name + self._iface_type = iface_type + self._nm_dev = nm_dev + + def run(self): + action = f"Delete device: {self._iface_type} {self._iface_name}" + user_data = action + self._ctx.register_async(action) + self._nm_dev.delete_async( + self._ctx.cancellable, self._delete_device_callback, user_data ) - -def _delete_device_callback(src_object, result, user_data): - context, nmdev, action, iface_name = user_data - if context.is_cancelled(): - return - error = None - try: - src_object.delete_finish(result) - except Exception as e: - error = e - - if not nmdev.is_real(): - logging.debug("Interface is not real anymore: iface=%s", iface_name) - if error: - logging.debug("Ignored error: %s", error) - context.finish_async(action) - else: - context.fail( - NmstateLibnmError(f"{action} failed: error={error or 'unknown'}") - ) + def _delete_device_callback(self, nm_dev, result, user_data): + action = user_data + if self._ctx.is_cancelled(): + return + error = None + try: + nm_dev.delete_finish(result) + except Exception as e: + error = e + + if not nm_dev.is_real(): + logging.debug( + f"Interface is deleted and not real/exist anymore: " + f"iface={self._iface_name} type={self._iface_type}" + ) + if error: + logging.debug(f"Ignored error: {error}") + self._ctx.finish_async(action) + else: + self._ctx.fail( + NmstateLibnmError( + f"{action} failed: error={error or 'unknown'}" + ) + ) def list_devices(client): @@ -151,8 +154,8 @@ def get_device_common_info(dev): } -def is_externally_managed(nmdev): - nm_ac = nmdev.get_active_connection() +def is_externally_managed(nm_dev): + nm_ac = nm_dev.get_active_connection() return nm_ac and NM.ActivationStateFlags.EXTERNAL & nm_ac.get_state_flags() @@ -178,3 +181,36 @@ def _set_managed_callback(_src_object, _result, user_data): # There is no document mention this action might fail # If anything goes wrong, we trust verifcation stage can detect it. context.finish_async(action) + + +def get_iface_type(nm_dev): + # TODO: Below code are mimic from translator, need redesign on this + iface_type = nm_dev.get_type_description() + if iface_type != InterfaceType.ETHERNET: + iface_type = Nm2Api.get_iface_type(nm_dev.get_type_description()) + if iface_type == InterfaceType.MAC_VLAN: + # Check whether we are MAC VTAP as NM is treating both of them as + # MAC VLAN. + # BUG: We should use applied config here. + nm_ac = nm_dev.get_active_connection() + if nm_ac: + nm_profile = nm_ac.get_connection() + if nm_profile and is_macvtap(nm_profile): + iface_type = InterfaceType.MAC_VTAP + return iface_type + + +def get_nm_dev(ctx, iface_name, iface_type): + """ + Return the first NM.Device matching iface_name and iface_type. + We don't use `NM.Client.get_device_by_iface()` as nm_dev does not + kernel interface, it could be OVS bridge or OVS port where name + can duplicate with kernel interface name. + """ + for nm_dev in ctx.client.get_devices(): + cur_iface_type = get_iface_type(nm_dev) + if nm_dev.get_iface() == iface_name and ( + iface_type is None or cur_iface_type == iface_type + ): + return nm_dev + return None diff --git a/libnmstate/nm/dns.py b/libnmstate/nm/dns.py index 8085770..9fb14d8 100644 --- a/libnmstate/nm/dns.py +++ b/libnmstate/nm/dns.py @@ -23,7 +23,6 @@ from operator import itemgetter from libnmstate import iplib from libnmstate.dns import DnsState from libnmstate.error import NmstateInternalError -from libnmstate.nm import active_connection as nm_ac from libnmstate.schema import DNS from libnmstate.schema import Interface @@ -131,7 +130,13 @@ def get_dns_config_iface_names(acs_and_ipv4_profiles, acs_and_ipv6_profiles): Return a list of interface names which hold static DNS configuration. """ iface_names = [] - for ac, ip_profile in chain(acs_and_ipv6_profiles, acs_and_ipv4_profiles): + for nm_ac, ip_profile in chain( + acs_and_ipv6_profiles, acs_and_ipv4_profiles + ): if ip_profile.props.dns or ip_profile.props.dns_search: - iface_names.append(nm_ac.ActiveConnection(nm_ac_con=ac).devname) + try: + iface_name = nm_ac.get_devices()[0].get_iface() + iface_names.append(iface_name) + except IndexError: + continue return iface_names diff --git a/libnmstate/nm/ipv4.py b/libnmstate/nm/ipv4.py index efdfce0..baf2e3a 100644 --- a/libnmstate/nm/ipv4.py +++ b/libnmstate/nm/ipv4.py @@ -154,7 +154,3 @@ def is_dynamic(active_connection): if ip_profile: return ip_profile.get_method() == NM.SETTING_IP4_CONFIG_METHOD_AUTO return False - - -def get_routing_rule_config(nm_client): - return nm_route.get_routing_rule_config(acs_and_ip_profiles(nm_client)) diff --git a/libnmstate/nm/ipv6.py b/libnmstate/nm/ipv6.py index 55b508c..8e01fd7 100644 --- a/libnmstate/nm/ipv6.py +++ b/libnmstate/nm/ipv6.py @@ -86,6 +86,7 @@ def create_setting(config, base_con_profile): setting_ip.props.never_default = False setting_ip.props.ignore_auto_dns = False setting_ip.clear_routes() + setting_ip.clear_routing_rules() setting_ip.props.gateway = None setting_ip.props.route_table = Route.USE_DEFAULT_ROUTE_TABLE setting_ip.props.route_metric = Route.USE_DEFAULT_METRIC @@ -208,7 +209,3 @@ def is_dynamic(active_connection): NM.SETTING_IP6_CONFIG_METHOD_DHCP, ) return False - - -def get_routing_rule_config(nm_client): - return nm_route.get_routing_rule_config(acs_and_ip_profiles(nm_client)) diff --git a/libnmstate/nm/macvlan.py b/libnmstate/nm/macvlan.py index daee9ba..ca049fa 100644 --- a/libnmstate/nm/macvlan.py +++ b/libnmstate/nm/macvlan.py @@ -18,7 +18,10 @@ # from libnmstate.error import NmstateValueError +from libnmstate.schema import Interface +from libnmstate.schema import InterfaceType from libnmstate.schema import MacVlan +from libnmstate.schema import MacVtap from .common import NM @@ -31,11 +34,12 @@ NMSTATE_MODE_TO_NM_MODE = { } -def create_setting(iface_state, base_con_profile): - macvlan = iface_state.get(MacVlan.CONFIG_SUBTREE) - if not macvlan: - return None - +def create_setting(iface_state, base_con_profile, tap=False): + macvlan = ( + iface_state.get(MacVtap.CONFIG_SUBTREE) + if tap + else iface_state.get(MacVlan.CONFIG_SUBTREE) + ) macvlan_setting = None if base_con_profile: macvlan_setting = base_con_profile.get_setting_by_name( @@ -56,7 +60,31 @@ def create_setting(iface_state, base_con_profile): macvlan_setting.props.mode = nm_mode macvlan_setting.props.parent = macvlan[MacVlan.BASE_IFACE] + macvlan_setting.props.tap = tap if macvlan.get(MacVlan.PROMISCUOUS) is not None: macvlan_setting.props.promiscuous = macvlan[MacVlan.PROMISCUOUS] return macvlan_setting + + +def is_macvtap(applied_config): + if applied_config: + macvlan_setting = applied_config.get_setting_by_name( + NM.SETTING_MACVLAN_SETTING_NAME + ) + if macvlan_setting: + return macvlan_setting.props.tap + return False + + +def get_current_macvlan_type(applied_config): + """ + This is a workaround needed due to Nmstate gathering the interface type + from NetworkManager, as we are deciding the interface type using the + setting name. If the interface type is not adjusted, Nmstate will fail + during verification as NM and Nispor interfaces will not be merged + correctly. + """ + if is_macvtap(applied_config): + return {Interface.TYPE: InterfaceType.MAC_VTAP} + return {} diff --git a/libnmstate/nm/ovs.py b/libnmstate/nm/ovs.py index 068f718..cdca1f5 100644 --- a/libnmstate/nm/ovs.py +++ b/libnmstate/nm/ovs.py @@ -26,6 +26,7 @@ from libnmstate.schema import OVSBridge as OB from libnmstate.schema import OVSInterface from libnmstate.ifaces import ovs from libnmstate.ifaces.bridge import BridgeIface +from libnmstate.ifaces.ovs import OvsPortIface from .common import NM @@ -124,10 +125,6 @@ def create_patch_setting(patch_state): return patch_setting -def is_ovs_port_type_id(type_id): - return type_id == NM.DeviceType.OVS_PORT - - def get_ovs_bridge_info(nm_dev_ovs_br): iface_info = {OB.CONFIG_SUBTREE: {}} ports_info = _get_bridge_nmstate_ports_info(nm_dev_ovs_br) @@ -273,44 +270,21 @@ def _get_bridge_options(bridge_device): return bridge_options -def create_ovs_proxy_iface_info(iface): - """ - Prepare the state of the "proxy" interface. These are interfaces that - exist as NM entities/profiles, but are invisible to the API. - These proxy interfaces state is created as a side effect of other - ifaces definition. - In OVS case, the port profile is the proxy, it is not part of the - public state of the system, but internal to the NM provider. - """ - iface_info = iface.to_dict() - controller_type = iface_info.get(CONTROLLER_TYPE_METADATA) - if controller_type != InterfaceType.OVS_BRIDGE: - return None - port_opts_metadata = iface_info.get(BridgeIface.BRPORT_OPTIONS_METADATA) - if port_opts_metadata is None: - return None - port_iface_desired_state = _create_ovs_port_iface_desired_state( - port_opts_metadata, iface, iface_info - ) - # The "visible" port/interface needs to point to the port profile - iface.set_controller( - port_iface_desired_state[Interface.NAME], InterfaceType.OVS_PORT - ) - - return port_iface_desired_state - - -def _create_ovs_port_iface_desired_state(port_options, iface, iface_info): +def create_iface_for_nm_ovs_port(iface): iface_name = iface.name + iface_info = iface.to_dict() + port_options = iface_info.get(BridgeIface.BRPORT_OPTIONS_METADATA) if ovs.is_ovs_lag_port(port_options): port_name = port_options[OB.Port.NAME] else: port_name = PORT_PROFILE_PREFIX + iface_name - return { - Interface.NAME: port_name, - Interface.TYPE: InterfaceType.OVS_PORT, - Interface.STATE: iface_info[Interface.STATE], - OB.OPTIONS_SUBTREE: port_options, - CONTROLLER_METADATA: iface_info[CONTROLLER_METADATA], - CONTROLLER_TYPE_METADATA: iface_info[CONTROLLER_TYPE_METADATA], - } + return OvsPortIface( + { + Interface.NAME: port_name, + Interface.TYPE: InterfaceType.OVS_PORT, + Interface.STATE: iface.state, + OB.OPTIONS_SUBTREE: port_options, + CONTROLLER_METADATA: iface_info[CONTROLLER_METADATA], + CONTROLLER_TYPE_METADATA: iface_info[CONTROLLER_TYPE_METADATA], + } + ) diff --git a/libnmstate/nm/plugin.py b/libnmstate/nm/plugin.py index 36d0d6f..d7a7961 100644 --- a/libnmstate/nm/plugin.py +++ b/libnmstate/nm/plugin.py @@ -27,28 +27,33 @@ from libnmstate.schema import DNS from libnmstate.schema import Interface from libnmstate.schema import InterfaceType from libnmstate.schema import Route -from libnmstate.schema import RouteRule from libnmstate.plugin import NmstatePlugin -from . import connection as nm_connection -from . import device as nm_device -from . import ipv4 as nm_ipv4 -from . import ipv6 as nm_ipv6 -from . import lldp as nm_lldp -from . import ovs as nm_ovs -from . import translator as nm_translator -from . import wired as nm_wired -from . import user as nm_user -from . import team as nm_team -from . import dns as nm_dns + from .checkpoint import CheckPoint from .checkpoint import get_checkpoints from .common import NM from .context import NmContext -from .profile import get_all_applied_configs -from .profile import NmProfiles -from .route import get_running_config as get_route_running_config +from .device import get_device_common_info +from .device import list_devices +from .dns import get_running as get_dns_running +from .dns import get_running_config as get_dns_running_config from .infiniband import get_info as get_infiniband_info +from .ipv4 import get_info as get_ipv4_info +from .ipv6 import get_info as get_ipv6_info +from .lldp import get_info as get_lldp_info +from .macvlan import get_current_macvlan_type +from .ovs import get_interface_info as get_ovs_interface_info +from .ovs import get_ovs_bridge_info +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 .wired import get_info as get_wired_info class NetworkManagerPlugin(NmstatePlugin): @@ -92,9 +97,9 @@ class NetworkManagerPlugin(NmstatePlugin): @property def capabilities(self): capabilities = [] - if nm_ovs.has_ovs_capability(self.client) and is_ovs_running(): + if has_ovs_capability(self.client) and is_ovs_running(): capabilities.append(NmstatePlugin.OVS_CAPABILITY) - if nm_team.has_team_capability(self.client): + if has_team_capability(self.client): capabilities.append(NmstatePlugin.TEAM_CAPABILITY) return capabilities @@ -109,47 +114,39 @@ class NetworkManagerPlugin(NmstatePlugin): def get_interfaces(self): info = [] - capabilities = self.capabilities applied_configs = self._applied_configs devices_info = [ - (dev, nm_device.get_device_common_info(dev)) - for dev in nm_device.list_devices(self.client) + (dev, get_device_common_info(dev)) + for dev in list_devices(self.client) ] for dev, devinfo in devices_info: if not dev.get_managed(): # Skip unmanaged interface continue - type_id = devinfo["type_id"] - iface_info = nm_translator.Nm2Api.get_common_device_info(devinfo) + iface_info = Nm2Api.get_common_device_info(devinfo) applied_config = applied_configs.get(iface_info[Interface.NAME]) - act_con = nm_connection.get_device_active_connection(dev) - iface_info[Interface.IPV4] = nm_ipv4.get_info( - act_con, applied_config - ) - iface_info[Interface.IPV6] = nm_ipv6.get_info( - act_con, applied_config - ) - iface_info.update(nm_wired.get_info(dev)) - iface_info.update(nm_user.get_info(self.context, dev)) - iface_info.update(nm_lldp.get_info(self.client, dev)) - iface_info.update(nm_team.get_info(dev)) + act_con = dev.get_active_connection() + iface_info[Interface.IPV4] = get_ipv4_info(act_con, applied_config) + iface_info[Interface.IPV6] = get_ipv6_info(act_con, applied_config) + iface_info.update(get_wired_info(dev)) + iface_info.update(get_user_info(self.context, dev)) + iface_info.update(get_lldp_info(self.client, dev)) + iface_info.update(get_team_info(dev)) iface_info.update(get_infiniband_info(applied_config)) - - if NmstatePlugin.OVS_CAPABILITY in capabilities: - if iface_info[Interface.TYPE] == InterfaceType.OVS_BRIDGE: - iface_info.update(nm_ovs.get_ovs_bridge_info(dev)) - iface_info = _remove_ovs_bridge_unsupported_entries( - iface_info - ) - elif iface_info[Interface.TYPE] == InterfaceType.OVS_INTERFACE: - iface_info.update(nm_ovs.get_interface_info(act_con)) - elif nm_ovs.is_ovs_port_type_id(type_id): - continue + iface_info.update(get_current_macvlan_type(applied_config)) + + if iface_info[Interface.TYPE] == InterfaceType.OVS_BRIDGE: + iface_info.update(get_ovs_bridge_info(dev)) + iface_info = _remove_ovs_bridge_unsupported_entries(iface_info) + elif iface_info[Interface.TYPE] == InterfaceType.OVS_INTERFACE: + iface_info.update(get_ovs_interface_info(act_con)) + elif iface_info[Interface.TYPE] == InterfaceType.OVS_PORT: + continue info.append(iface_info) @@ -161,17 +158,15 @@ class NetworkManagerPlugin(NmstatePlugin): return {Route.CONFIG: get_route_running_config(self._applied_configs)} def get_route_rules(self): - return { - RouteRule.CONFIG: ( - nm_ipv4.get_routing_rule_config(self.client) - + nm_ipv6.get_routing_rule_config(self.client) - ) - } + """ + Nispor will provide running config of route rule from kernel. + """ + return {} def get_dns_client_config(self): return { - DNS.RUNNING: nm_dns.get_running(self.client), - DNS.CONFIG: nm_dns.get_running_config(self._applied_configs), + DNS.RUNNING: get_dns_running(self.client), + DNS.CONFIG: get_dns_running_config(self._applied_configs), } def refresh_content(self): diff --git a/libnmstate/nm/profile.py b/libnmstate/nm/profile.py index 6bd70e5..6be1588 100644 --- a/libnmstate/nm/profile.py +++ b/libnmstate/nm/profile.py @@ -21,323 +21,195 @@ # * NM.RemoteConnection, NM.SimpleConnection releated from distutils.version import StrictVersion -import logging +from libnmstate.error import NmstateLibnmError from libnmstate.error import NmstateNotSupportedError -from libnmstate.error import NmstateValueError +from libnmstate.error import NmstateInternalError from libnmstate.schema import Interface -from libnmstate.schema import InterfaceState from libnmstate.schema import InterfaceType -from libnmstate.schema import LinuxBridge as LB -from libnmstate.schema import OVSBridge as OvsB -from libnmstate.schema import OVSInterface -from libnmstate.schema import Team -from libnmstate.schema import VRF -from libnmstate.ifaces.base_iface import BaseIface -from libnmstate.ifaces.bond import BondIface -from libnmstate.ifaces.bridge import BridgeIface - -from . import bond -from . import bridge -from . import connection -from . import device -from . import dns as nm_dns -from . import ipv4 -from . import ipv6 -from . import lldp -from . import macvlan -from . import ovs as nm_ovs -from . import profile_state -from . import sriov -from . import team -from . import translator -from . import user -from . import vlan -from . import vxlan -from . import wired +from libnmstate.schema import Ethernet +from .active_connection import ActiveConnectionDeactivate +from .active_connection import ProfileActivation +from .active_connection import is_activated from .common import NM -from .device import mark_device_as_managed -from .device import list_devices -from .device import is_externally_managed -from .vrf import create_vrf_setting -from .infiniband import create_setting as create_infiniband_setting - - -ACTION_DEACTIVATE_BEFOREHAND = "deactivate-beforehand" -ACTION_DELETE_PROFILE = "delete-profile" -ACTION_ACTIVATE = "activate" -ACTION_MODIFY = "modify" -ACTION_DEACTIVATE = "deactivate" -ACTION_DELETE_DEV_PROFILES = "delete-dev-profiles" -ACTION_DELETE_DEV = "delete-dev" - -CONTROLLER_METADATA = "_controller" -CONTROLLER_TYPE_METADATA = "_controller_type" -CONTROLLER_IFACE_TYPES = ( - InterfaceType.OVS_BRIDGE, - bond.BOND_TYPE, - LB.TYPE, - Team.TYPE, -) - - -class NmProfiles: - def __init__(self, context): - self._ctx = context - - def apply_config(self, net_state, save_to_disk): - self._prepare_state_for_profiles(net_state) - self._profiles = [ - NmProfile(self._ctx, save_to_disk, iface) - for iface in net_state.ifaces.values() - if (iface.is_changed or iface.is_desired) and not iface.is_ignore - ] - - for profile in self._profiles: - profile.store_config() - self._ctx.wait_all_finish() - - grouped_profiles = self._group_profile_by_action_order() - for profile_group in grouped_profiles: - for profile in profile_group: - profile.apply_config() - self._ctx.wait_all_finish() - - def _group_profile_by_action_order(self): - groups = { - "profiles_to_deactivate_beforehand": set(), - "profiles_to_delete": set(), - "new_controller_not_as_port": set(), - "new_ifaces_to_activate": set(), - "controller_ifaces_to_edit": set(), - "new_ovs_port_to_activate": set(), - "new_ovs_interface_to_activate": set(), - "ifaces_to_edit": set(), - "new_vlan_x_to_activate": set(), - "profiles_to_deactivate": set(), - "devs_to_delete_profile": set(), - "devs_to_delete": set(), - } - - for profile in self._profiles: - profile.classify_profile_for_actions(groups) - - return groups.values() - - def _prepare_state_for_profiles(self, net_state): - _preapply_dns_fix_for_profiles(self._ctx, net_state) - _mark_nm_external_subordinate_changed(self._ctx, net_state) - _mark_mode_changed_bond_child_interface_as_changed(net_state) - - proxy_ifaces = {} - for iface in net_state.ifaces.values(): - proxy_iface_info = nm_ovs.create_ovs_proxy_iface_info(iface) - if proxy_iface_info: - proxy_iface = BaseIface(proxy_iface_info) - proxy_iface.mark_as_changed() - proxy_ifaces[proxy_iface.name] = proxy_iface - net_state.ifaces.update(proxy_ifaces) +from .connection import create_new_nm_simple_conn +from .device import get_nm_dev +from .device import DeviceReapply +from .device import DeviceDelete +from .translator import Api2Nm class NmProfile: - def __init__(self, context, save_to_disk, iface=None): - self._ctx = context + # For unmanged iface and desired to down + ACTION_ACTIVATE_FIRST = "activate_first" + ACTION_DEACTIVATE = "deactivate" + ACTION_DEACTIVATE_FIRST = "deactivate_first" + ACTION_DELETE_DEVICE = "delete_device" + ACTION_MODIFIED = "modified" + ACTION_NEW_IFACES = "new_ifaces" + ACTION_NEW_OVS_IFACE = "new_ovs_iface" + ACTION_NEW_OVS_PORT = "new_ovs_port" + ACTION_NEW_VLAN = "new_vlan" + ACTION_NEW_VXLAN = "new_vxlan" + ACTION_OTHER_MASTER = "other_master" + ACTION_DELETE_PROFILE = "delete_profile" + ACTION_TOP_MASTER = "top_master" + + # This is order on group for activation/deactivation + ACTIONS = ( + ACTION_ACTIVATE_FIRST, + ACTION_DEACTIVATE_FIRST, + ACTION_TOP_MASTER, + ACTION_NEW_IFACES, + ACTION_OTHER_MASTER, + ACTION_NEW_OVS_PORT, + ACTION_NEW_OVS_IFACE, + ACTION_MODIFIED, + ACTION_NEW_VLAN, + ACTION_NEW_VXLAN, + ACTION_DEACTIVATE, + ACTION_DELETE_PROFILE, + ACTION_DELETE_DEVICE, + ) + + def __init__(self, ctx, iface, save_to_disk): + self._ctx = ctx self._iface = iface self._save_to_disk = save_to_disk - self._nmdev = None + self._nm_iface_type = None + if self._iface.type != InterfaceType.UNKNOWN: + self._nm_iface_type = Api2Nm.get_iface_type(self._iface.type) self._nm_ac = None - self._nm_profile_state = profile_state.NmProfileState(context) - self._remote_conn = None - self._simple_conn = None - self._actions_needed = [] - - @property - def iface_info(self): - return self._iface.to_dict() - - @property - def iface(self): - return self._iface - - @property - def original_iface_info(self): - return self._iface.original_dict - - @property - def profile_state(self): - return self._nm_profile_state - - @property - def nmdev(self): - if self._nmdev: - return self._nmdev - elif self.devname: - return self._ctx.get_nm_dev(self.devname) - else: - return None - - @nmdev.setter - def nmdev(self, dev): - self._nmdev = dev - - @property - def nm_ac(self): - return self._nm_ac - - @nm_ac.setter - def nm_ac(self, ac): - self._nm_ac = ac - - @property - def remote_conn(self): - return self._remote_conn - - @remote_conn.setter - def remote_conn(self, con): - self._remote_conn = con - - @property - def simple_conn(self): - return self._simple_conn - - @property - def uuid(self): - if self._remote_conn: - return self._remote_conn.get_uuid() - elif self._simple_conn: - return self._simple_conn.get_uuid() - else: - return self.iface.name - - @property - def devname(self): - if self._remote_conn: - return self._remote_conn.get_interface_name() - elif self._simple_conn: - return self._simple_conn.get_interface_name() - else: - return self.iface.name - - @property - def profile(self): - return self._simple_conn if self._simple_conn else self._remote_conn - - @property - def is_memory_only(self): - if self._remote_conn: - profile_flags = self._remote_conn.get_flags() - return ( - NM.SettingsConnectionFlags.UNSAVED & profile_flags - or NM.SettingsConnectionFlags.VOLATILE & profile_flags - ) - return False - - def apply_config(self): - if ACTION_DEACTIVATE_BEFOREHAND in self._actions_needed: - device.deactivate(self._ctx, self.nmdev) - elif ACTION_DELETE_PROFILE in self._actions_needed: - self.delete() - elif ACTION_ACTIVATE in self._actions_needed: - self.activate() - elif ACTION_MODIFY in self._actions_needed: - device.modify(self._ctx, self) - elif ACTION_DEACTIVATE in self._actions_needed: - device.deactivate(self._ctx, self.nmdev) - elif ACTION_DELETE_DEV_PROFILES in self._actions_needed: - self.delete() - elif ACTION_DELETE_DEV in self._actions_needed: - device.delete_device(self._ctx, self.nmdev) - self._next_action() - - def _next_action(self): - if self._actions_needed: - self._actions_needed.pop(0) - - def activate(self): - specific_object = None - action = ( - f"Activate profile uuid:{self.profile.get_uuid()} " - f"id:{self.profile.get_id()}" - ) - user_data = action, self - self._ctx.register_async(action) - self._ctx.client.activate_connection_async( - self._remote_conn, - self.nmdev, - specific_object, - self._ctx.cancellable, - self._nm_profile_state.activate_connection_callback, - user_data, - ) - - def delete(self): - if self._remote_conn: - action = ( - f"Delete profile: uuid:{self._remote_conn.get_uuid()} " - f"id:{self._remote_conn.get_id()}" - ) - user_data = action - self._ctx.register_async(action, fast=True) - self._remote_conn.delete_async( - self._ctx.cancellable, - self._nm_profile_state.delete_profile_callback, - user_data, - ) - - def _update(self): - flags = NM.SettingsUpdate2Flags.BLOCK_AUTOCONNECT - if self._save_to_disk: - flags |= NM.SettingsUpdate2Flags.TO_DISK - else: - flags |= NM.SettingsUpdate2Flags.IN_MEMORY - action = ( - f"Update profile uuid:{self._remote_conn.get_uuid()} " - f"id:{self._remote_conn.get_id()}" - ) - user_data = action - args = None + self._nm_dev = None + self._nm_profile = None + self._nm_simple_conn = None + self._actions = set() + self._activated = False + self._deactivated = False + self._profile_deleted = False + self._device_deleted = False + self._import_current() + self._gen_actions() + + def _gen_actions(self): + if self._iface.is_absent: + 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: + self._add_action(NmProfile.ACTION_MODIFIED) + if not self._nm_dev: + if self._iface.type == InterfaceType.OVS_PORT: + self._add_action(NmProfile.ACTION_NEW_OVS_PORT) + elif self._iface.type == InterfaceType.OVS_INTERFACE: + self._add_action(NmProfile.ACTION_NEW_OVS_IFACE) + elif self._iface.type == InterfaceType.VLAN: + self._add_action(NmProfile.ACTION_NEW_VLAN) + elif self._iface.type == InterfaceType.VXLAN: + self._add_action(NmProfile.ACTION_NEW_VXLAN) + else: + self._add_action(NmProfile.ACTION_NEW_IFACES) - self._ctx.register_async(action, fast=True) - self._remote_conn.update2( - self._simple_conn.to_dbus(NM.ConnectionSerializationFlags.ALL), - flags, - args, - self._ctx.cancellable, - self._nm_profile_state.update2_callback, - user_data, - ) + elif self._iface.is_down: + if self._nm_ac: + self._add_action(NmProfile.ACTION_DEACTIVATE) + elif self._iface.is_virtual and self._nm_dev: + self._add_action(NmProfile.ACTION_DELETE_DEVICE) - def _add(self): - nm_add_conn2_flags = NM.SettingsAddConnection2Flags - flags = nm_add_conn2_flags.BLOCK_AUTOCONNECT - if self._save_to_disk: - flags |= nm_add_conn2_flags.TO_DISK - else: - flags |= nm_add_conn2_flags.IN_MEMORY + 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) - action = f"Add profile: {self._simple_conn.get_uuid()}" - self._ctx.register_async(action, fast=True) + if ( + self._iface.is_up + and self._iface.type == InterfaceType.BOND + and self._iface.is_bond_mode_changed + ): + # NetworkManager leaves leftover in sysfs for bond + # options when changing bond mode, bug: + # https://bugzilla.redhat.com/show_bug.cgi?id=1819137 + # Workaround: delete the bond interface from kernel and + # create again via full deactivation beforehand. + self._add_action(NmProfile.ACTION_DEACTIVATE_FIRST) + + if self._iface.is_up and self._iface.type in ( + InterfaceType.MAC_VLAN, + InterfaceType.MAC_VTAP, + ): + # NetworkManager requires the profile to be deactivated in + # order to modify it. Therefore if the profile is modified + # it needs to be deactivated beforehand in order to apply + # the changes and activate it again. + self._add_action(NmProfile.ACTION_DEACTIVATE_FIRST) - user_data = action, self - args = None - ignore_out_result = False # Don't fall back to old AddConnection() - self._ctx.client.add_connection2( - self._simple_conn.to_dbus(NM.ConnectionSerializationFlags.ALL), - flags, - args, - ignore_out_result, - self._ctx.cancellable, - self._nm_profile_state.add_connection2_callback, - user_data, + if ( + self._iface.is_down + and self._nm_dev + and not self._nm_dev.get_managed() + ): + # In order to deactivate an unmanaged interface, we have to + # activate the newly created profile to remove all kernel + # settings. + self._add_action(NmProfile.ACTION_ACTIVATE_FIRST) + + def save_config(self): + if self._iface.is_absent or self._iface.is_down: + return + + self._import_current() + 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 + and not self._iface.is_changed + and set(self._iface.original_dict) + <= set([Interface.STATE, Interface.NAME, Interface.TYPE]) + ): + cur_nm_profile = self._get_first_nm_profile() + if ( + cur_nm_profile + and _is_memory_only(cur_nm_profile) != self._save_to_disk + ): + self._nm_profile = cur_nm_profile + return + + # TODO: Use applied config as base profile + # Or even better remove the base profile argument as top level + # of nmstate should provide full/merged configure. + self._nm_simple_conn = create_new_nm_simple_conn( + self._iface, self._nm_profile ) - - def store_config(self): + if self._nm_profile: + ProfileUpdate( + self._ctx, + self._iface.name, + self._iface.type, + self._nm_simple_conn, + self._nm_profile, + self._save_to_disk, + ).run() + else: + ProfileAdd( + self._ctx, + self._iface.name, + self._iface.type, + self._nm_simple_conn, + self._save_to_disk, + ).run() + + def _check_unsupported_memory_only(self): if ( not self._save_to_disk and StrictVersion(self._ctx.client.get_version()) < StrictVersion("1.28.0") - and self.iface.type + and self._iface.type in ( InterfaceType.OVS_BRIDGE, InterfaceType.OVS_INTERFACE, @@ -350,367 +222,344 @@ class NmProfile: " OpenvSwitch interface." ) - ifname = self.iface.name - self._import_existing_profile(ifname) + def _check_sriov_support(self): + sriov_config = ( + self._iface.to_dict() + .get(Ethernet.CONFIG_SUBTREE, {}) + .get(Ethernet.SRIOV_SUBTREE) + ) - if self._save_to_disk: - connections = connection.list_connections_by_ifname( - self._ctx, ifname - ) - for con in connections: - if ( - not self._remote_conn - or con.get_uuid() != self._remote_conn.get_uuid() - ): - nmprofile = NmProfile(self._ctx, self._save_to_disk) - nmprofile.remote_conn = con - nmprofile.delete() - if not ( - set(self.original_iface_info.keys()) - <= set([Interface.STATE, Interface.NAME, Interface.TYPE]) - and self._remote_conn - and not self._iface.is_changed - and self.is_memory_only != self._save_to_disk - ): - if self.iface.state not in ( - InterfaceState.ABSENT, - InterfaceState.DOWN, + if self._nm_dev and sriov_config: + if ( + not self._nm_dev.props.capabilities + & NM.DeviceCapabilities.SRIOV ): - settings = self._generate_connection_settings(ifname) - self._simple_conn = connection.create_new_simple_connection( - settings + raise NmstateNotSupportedError( + f"Interface {self._iface.name} {self._iface.type} " + "does not support SR-IOV" ) - set_conn = self._simple_conn.get_setting_connection() - set_conn.props.interface_name = ifname - if self._remote_conn: - self._update() - else: - self._add() - - def _generate_connection_settings(self, ifname): - nm_iface_type = translator.Api2Nm.get_iface_type(self.iface.type) - settings = [ - ipv4.create_setting( - self.iface_info.get(Interface.IPV4), self._remote_conn - ), - ipv6.create_setting( - self.iface_info.get(Interface.IPV6), self._remote_conn - ), - ] - - con_setting = connection.ConnectionSetting() - if self._remote_conn: - con_setting.import_by_profile(self._remote_conn) - con_setting.set_profile_name(ifname) - else: - con_setting.create( - con_name=ifname, iface_name=ifname, iface_type=nm_iface_type, - ) - lldp.apply_lldp_setting(con_setting, self.iface_info) - - controller = self.iface_info.get(CONTROLLER_METADATA) - controller_type = self.iface_info.get(CONTROLLER_TYPE_METADATA) - if controller_type == LB.TYPE: - self.iface_info[CONTROLLER_TYPE_METADATA] = bridge.BRIDGE_TYPE - controller_type = bridge.BRIDGE_TYPE - con_setting.set_controller(controller, controller_type) - settings.append(con_setting.setting) - - # Only apply wired/ethernet configuration based on original desire - # state rather than the merged one. - original_state_wired = {} - if self._iface.is_desired: - original_state_wired = self.original_iface_info - if self.iface.type != InterfaceType.INFINIBAND: - # The IP over InfiniBand has its own setting for MTU and does not - # have ethernet layer. - wired_setting = wired.create_setting( - original_state_wired, self._remote_conn - ) - if wired_setting: - settings.append(wired_setting) + def _activate(self): + if self._activated: + return - user_setting = user.create_setting(self.iface_info, self._remote_conn) - if user_setting: - settings.append(user_setting) + if not self._nm_profile: + self._import_nm_profile_by_simple_conn() - if self.iface.type == InterfaceType.BOND: - settings.append( - bond.create_setting( - self.iface, wired_setting, self._remote_conn - ) - ) - elif nm_iface_type == bridge.BRIDGE_TYPE: - bridge_config = self.iface_info.get(LB.CONFIG_SUBTREE, {}) - bridge_options = bridge_config.get(LB.OPTIONS_SUBTREE) - bridge_ports = bridge_config.get(LB.PORT_SUBTREE) - if bridge_options or bridge_ports: - linux_bridge_setting = bridge.create_setting( - self.iface_info, - self._remote_conn, - self.original_iface_info, - ) - settings.append(linux_bridge_setting) - elif nm_iface_type == InterfaceType.OVS_BRIDGE: - ovs_bridge_state = self.iface_info.get(OvsB.CONFIG_SUBTREE, {}) - ovs_bridge_options = ovs_bridge_state.get(OvsB.OPTIONS_SUBTREE) - if ovs_bridge_options: - settings.append( - nm_ovs.create_bridge_setting(ovs_bridge_options) - ) - elif nm_iface_type == InterfaceType.OVS_PORT: - ovs_port_options = self.iface_info.get(OvsB.OPTIONS_SUBTREE) - settings.append(nm_ovs.create_port_setting(ovs_port_options)) - elif nm_iface_type == InterfaceType.OVS_INTERFACE: - patch_state = self.iface_info.get( - OVSInterface.PATCH_CONFIG_SUBTREE + profile_activation = ProfileActivation( + self._ctx, + self._iface.name, + self._iface.type, + self._nm_profile, + self._nm_dev, + ) + if is_activated(self._nm_ac, self._nm_dev): + # After ProfileUpdate(), the self._nm_profile is still hold + # the old settings, DeviceReapply should use the + # self._nm_simple_conn for updated settings. + DeviceReapply( + self._ctx, + self._iface.name, + self._iface.type, + self._nm_dev, + self._nm_simple_conn, + profile_activation, + ).run() + else: + profile_activation.run() + self._activated = True + + def _deactivate(self): + if self._deactivated: + return + self._import_current() + if self._nm_ac: + ActiveConnectionDeactivate( + self._ctx, self._iface.name, self._iface.type, self._nm_ac + ).run() + self._deactivated = True + + def _delete_profile(self): + if self._profile_deleted: + return + self._import_current() + if self._nm_profile: + ProfileDelete( + self._ctx, self._iface.name, self._iface.type, self._nm_profile + ).run() + + self._profile_deleted = True + + def _delete_device(self): + if self._device_deleted: + return + self._import_current() + if self._nm_dev: + DeviceDelete( + self._ctx, self._iface.name, self._iface.type, self._nm_dev + ).run() + self._device_deleted = True + + def _add_action(self, action): + self._actions.add(action) + + def has_action(self, action): + return action in self._actions + + def do_action(self, action): + if action in ( + NmProfile.ACTION_MODIFIED, + NmProfile.ACTION_ACTIVATE_FIRST, + NmProfile.ACTION_TOP_MASTER, + NmProfile.ACTION_NEW_IFACES, + NmProfile.ACTION_OTHER_MASTER, + NmProfile.ACTION_NEW_OVS_PORT, + NmProfile.ACTION_NEW_OVS_IFACE, + NmProfile.ACTION_NEW_VLAN, + NmProfile.ACTION_NEW_VXLAN, + ): + self._activate() + elif ( + action + in ( + NmProfile.ACTION_DELETE_PROFILE, + NmProfile.ACTION_DELETE_DEVICE, + NmProfile.ACTION_DEACTIVATE, + NmProfile.ACTION_DEACTIVATE_FIRST, ) - settings.extend(nm_ovs.create_interface_setting(patch_state)) - elif self.iface.type == InterfaceType.INFINIBAND: - ib_setting = create_infiniband_setting( - self.iface_info, self._remote_conn, self.original_iface_info, + and not self._deactivated + ): + self._deactivate() + elif action == NmProfile.ACTION_DELETE_PROFILE: + self._delete_profile() + elif action == NmProfile.ACTION_DELETE_DEVICE: + self._delete_device() + else: + raise NmstateInternalError( + f"BUG: NmProfile.do_action() got unknown action {action}" ) - if ib_setting: - settings.append(ib_setting) - bridge_port_options = self.iface_info.get( - BridgeIface.BRPORT_OPTIONS_METADATA + def _import_current(self): + self._nm_dev = get_nm_dev( + self._ctx, self._iface.name, self._iface.type + ) + self._nm_ac = ( + self._nm_dev.get_active_connection() if self._nm_dev else None + ) + self._nm_profile = ( + self._nm_ac.get_connection() if self._nm_ac else None ) - if bridge_port_options and controller_type == bridge.BRIDGE_TYPE: - settings.append( - bridge.create_port_setting( - bridge_port_options, self._remote_conn + + def _import_nm_profile_by_simple_conn(self): + self._ctx.refresh_content() + 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 + + def _get_first_nm_profile(self): + for nm_profile in self._ctx.client.get_connections(): + if nm_profile.get_interface_name() == self._iface.name and ( + self._nm_iface_type is None + or nm_profile.get_connection_type() == self._nm_iface_type + ): + return nm_profile + return None + + def delete_other_profiles(self): + """ + Remove all profiles except the NM.RemoteConnection used by current + NM.ActiveConnection if interface is marked as UP + """ + if self._iface.is_down: + return + self._import_current() + for nm_profile in self._ctx.client.get_connections(): + if ( + nm_profile.get_interface_name() == self._iface.name + and ( + self._nm_iface_type is None + or nm_profile.get_connection_type() == self._nm_iface_type ) - ) + and ( + self._nm_profile is None + or nm_profile.get_uuid() != self._nm_profile.get_uuid() + ) + ): + ProfileDelete( + self._ctx, self._iface.name, self._iface.type, nm_profile + ).run() + + +class ProfileAdd: + def __init__( + self, ctx, iface_name, iface_type, nm_simple_conn, save_to_disk + ): + self._ctx = ctx + self._iface_name = iface_name + self._iface_type = iface_type + self._nm_simple_conn = nm_simple_conn + self._save_to_disk = save_to_disk - vlan_setting = vlan.create_setting(self.iface_info, self._remote_conn) - if vlan_setting: - settings.append(vlan_setting) + def run(self): + nm_add_conn2_flags = NM.SettingsAddConnection2Flags + flags = nm_add_conn2_flags.BLOCK_AUTOCONNECT + if self._save_to_disk: + flags |= nm_add_conn2_flags.TO_DISK + else: + flags |= nm_add_conn2_flags.IN_MEMORY - vxlan_setting = vxlan.create_setting( - self.iface_info, self._remote_conn + action = ( + f"Add profile: {self._nm_simple_conn.get_uuid()}, " + f"iface:{self._iface_name}, type:{self._iface_type}" ) - if vxlan_setting: - settings.append(vxlan_setting) + self._ctx.register_async(action, fast=True) - sriov_setting = sriov.create_setting( - self._ctx, self.iface_info, self._remote_conn + user_data = action + args = None + ignore_out_result = False # Don't fall back to old AddConnection() + self._ctx.client.add_connection2( + self._nm_simple_conn.to_dbus(NM.ConnectionSerializationFlags.ALL), + flags, + args, + ignore_out_result, + self._ctx.cancellable, + self._add_profile_callback, + user_data, ) - if sriov_setting: - settings.append(sriov_setting) - team_setting = team.create_setting(self.iface_info, self._remote_conn) - if team_setting: - settings.append(team_setting) + def _add_profile_callback(self, nm_client, result, user_data): + action = user_data + if self._ctx.is_cancelled(): + return + try: + nm_profile = nm_client.add_connection2_finish(result)[0] + except Exception as e: + self._ctx.fail( + NmstateLibnmError(f"{action} failed with error: {e}") + ) + return - if VRF.CONFIG_SUBTREE in self.iface_info: - settings.append( - create_vrf_setting(self.iface_info[VRF.CONFIG_SUBTREE]) + if nm_profile is None: + self._ctx.fail( + NmstateLibnmError( + f"{action} failed with error: 'None returned from " + "NM.Client.add_connection2_finish()'" + ) ) + else: + self._ctx.finish_async(action) + + +class ProfileUpdate: + def __init__( + self, + ctx, + iface_name, + iface_type, + nm_simple_conn, + nm_profile, + save_to_disk, + ): + self._ctx = ctx + self._iface_name = iface_name + self._iface_type = iface_type + self._nm_simple_conn = nm_simple_conn + self._nm_profile = nm_profile + self._save_to_disk = save_to_disk - macvlan_setting = macvlan.create_setting( - self.iface_info, self._remote_conn + def run(self): + flags = NM.SettingsUpdate2Flags.BLOCK_AUTOCONNECT + if self._save_to_disk: + flags |= NM.SettingsUpdate2Flags.TO_DISK + else: + flags |= NM.SettingsUpdate2Flags.IN_MEMORY + action = ( + f"Update profile uuid:{self._nm_profile.get_uuid()} " + f"iface:{self._iface_name} type:{self._iface_type}" ) - if macvlan_setting: - settings.append(macvlan_setting) + user_data = action + args = None - return settings + self._ctx.register_async(action, fast=True) + self._nm_profile.update2( + self._nm_simple_conn.to_dbus(NM.ConnectionSerializationFlags.ALL), + flags, + args, + self._ctx.cancellable, + self._update_profile_callback, + user_data, + ) - def _import_existing_profile(self, ifname): - self._nmdev = self._ctx.get_nm_dev(ifname) - if self._nmdev: - self._remote_conn = self._import_remote_conn_by_device() + def _update_profile_callback(self, nm_profile, result, user_data): + action = user_data + if self._ctx.is_cancelled(): + return + try: + ret = nm_profile.update2_finish(result) + except Exception as e: + self._ctx.fail( + NmstateLibnmError(f"{action} failed with error={e}") + ) + return + + if ret is None: + self._ctx.fail( + NmstateLibnmError( + f"{action} failed with error='None returned from " + "update2_finish()'" + ) + ) else: - # Profile for virtual interface does not have a NM.Device - # associated. - self._remote_conn = self._ctx.client.get_connection_by_id(ifname) + self._ctx.finish_async(action) - def _import_remote_conn_by_device(self): - act_conn = self._nmdev.get_active_connection() - if act_conn: - self._nm_ac = act_conn - return act_conn.get_connection() - return None +class ProfileDelete: + def __init__(self, ctx, iface_name, iface_type, nm_profile): + self._ctx = ctx + self._iface_name = iface_name + self._iface_type = iface_type + self._nm_profile = nm_profile - def classify_profile_for_actions(self, groups): - if not self.nmdev: - if self.iface.state == InterfaceState.UP: - self._actions_needed.append(ACTION_ACTIVATE) - if ( - self.iface.type in CONTROLLER_IFACE_TYPES - and not self.iface_info.get(CONTROLLER_METADATA) - ): - groups["new_controller_not_as_port"].add(self) - elif self.iface.type == InterfaceType.OVS_INTERFACE: - groups["new_ovs_interface_to_activate"].add(self) - elif self.iface.type == InterfaceType.OVS_PORT: - groups["new_ovs_port_to_activate"].add(self) - elif self.iface.type in ( - InterfaceType.VLAN, - InterfaceType.VXLAN, - ): - groups["new_vlan_x_to_activate"].add(self) - else: - groups["new_ifaces_to_activate"].add(self) - elif self.iface.state == InterfaceState.ABSENT: - # Delete absent profiles - self._actions_needed.append(ACTION_DELETE_PROFILE) - groups["profiles_to_delete"].add(self) + def run(self): + action = ( + f"Delete profile: uuid:{self._nm_profile.get_uuid()} " + f"id:{self._nm_profile.get_id()} " + f"iface:{self._iface_name} type:{self._iface_type}" + ) + user_data = action + self._ctx.register_async(action, fast=True) + self._nm_profile.delete_async( + self._ctx.cancellable, + self._delete_profile_callback, + user_data, + ) + + def _delete_profile_callback(self, nm_profile, result, user_data): + action = user_data + if self._ctx.is_cancelled(): + return + try: + success = nm_profile.delete_finish(result) + except Exception as e: + self._ctx.fail(NmstateLibnmError(f"{action} failed: error={e}")) + return + + if success: + self._ctx.finish_async(action) else: - if not self.nmdev.get_managed(): - mark_device_as_managed(self._ctx, self.nmdev) - if self.iface.state == InterfaceState.UP: - if self.iface.type == InterfaceType.BOND: - iface = BondIface(self.iface_info) - # NetworkManager leaves leftover in sysfs for bond - # options when changing bond mode, bug: - # https://bugzilla.redhat.com/show_bug.cgi?id=1819137 - # Workaround: delete the bond interface from kernel and - # create again via full deactivation beforehand. - if iface.is_bond_mode_changed: - logging.debug( - f"Bond interface {self.iface.name} is changing " - "bond mode, will do full deactivation before " - "applying changes" - ) - self._actions_needed.append( - ACTION_DEACTIVATE_BEFOREHAND - ) - groups["profiles_to_deactivate_beforehand"].add(self) - elif self.iface.type == InterfaceType.MAC_VLAN: - self._actions_needed.append(ACTION_DEACTIVATE_BEFOREHAND) - groups["profiles_to_deactivate_beforehand"].add(self) - self._actions_needed.append(ACTION_MODIFY) - if self.iface.type in CONTROLLER_IFACE_TYPES: - groups["controller_ifaces_to_edit"].add(self) - else: - groups["ifaces_to_edit"].add(self) - elif self.iface.state in ( - InterfaceState.DOWN, - InterfaceState.ABSENT, - ): - is_absent = self.iface.state == InterfaceState.ABSENT - self._actions_needed.append(ACTION_DEACTIVATE) - groups["profiles_to_deactivate"].add(self) - if is_absent: - self._actions_needed.append(ACTION_DELETE_DEV_PROFILES) - groups["devs_to_delete_profile"].add(self) - if ( - is_absent - and self.nmdev.is_software() - and self.nmdev.get_device_type() != NM.DeviceType.VETH - ): - self._actions_needed.append(ACTION_DELETE_DEV) - groups["devs_to_delete"].add(self) - else: - raise NmstateValueError( - "Invalid state {} for interface {}".format( - self.iface.state, self.iface.name, - ) + self._ctx.fail( + NmstateLibnmError( + f"{action} failed: error='None returned from " + "delete_finish'" ) + ) -def get_all_applied_configs(context): - applied_configs = {} - for nm_dev in list_devices(context.client): - if ( - nm_dev.get_state() - in (NM.DeviceState.ACTIVATED, NM.DeviceState.IP_CONFIG,) - and nm_dev.get_managed() - ): - iface_name = nm_dev.get_iface() - if iface_name: - iface_type_str = nm_dev.get_type_description() - action = ( - f"Retrieve applied config: {iface_type_str} {iface_name}" - ) - context.register_async(action, fast=True) - nm_dev.get_applied_connection_async( - flags=0, - cancellable=context.cancellable, - callback=profile_state.get_applied_config_callback, - user_data=(iface_name, action, applied_configs, context), - ) - context.wait_all_finish() - return applied_configs - - -def _preapply_dns_fix_for_profiles(context, net_state): - """ - * When DNS configuration does not changed and old interface hold DNS - configuration is not included in `ifaces_desired_state`, preserve - the old DNS configure by removing DNS metadata from - `ifaces_desired_state`. - * When DNS configuration changed, include old interface which is holding - DNS configuration, so it's DNS configure could be removed. - """ - cur_dns_iface_names = nm_dns.get_dns_config_iface_names( - ipv4.acs_and_ip_profiles(context.client), - ipv6.acs_and_ip_profiles(context.client), - ) - - # Whether to mark interface as changed which is used for holding old DNS - # configurations - remove_existing_dns_config = False - # Whether to preserve old DNS config by DNS metadata to be removed from - # desired state - preserve_old_dns_config = False - if net_state.dns.config == net_state.dns.current_config: - for cur_dns_iface_name in cur_dns_iface_names: - iface = net_state.ifaces[cur_dns_iface_name] - if iface.is_changed or iface.is_desired: - remove_existing_dns_config = True - if not remove_existing_dns_config: - preserve_old_dns_config = True - else: - remove_existing_dns_config = True - - if remove_existing_dns_config: - for cur_dns_iface_name in cur_dns_iface_names: - iface = net_state.ifaces[cur_dns_iface_name] - iface.mark_as_changed() - - if preserve_old_dns_config: - for iface in net_state.ifaces.values(): - if iface.is_changed or iface.is_desired: - iface.remove_dns_metadata() - - -def _mark_nm_external_subordinate_changed(context, net_state): - """ - When certain main interface contains subordinates is marked as - connected(externally), it means its profile is memory only and will lost - on next deactivation. - For this case, we should mark the subordinate as changed. - that subordinate should be marked as changed for NM to take over. - """ - for iface in net_state.ifaces.values(): - if iface.type in CONTROLLER_IFACE_TYPES: - for subordinate in iface.port: - nmdev = context.get_nm_dev(subordinate) - if nmdev: - if is_externally_managed(nmdev): - subordinate_iface = net_state.ifaces.get(subordinate) - if subordinate_iface: - subordinate_iface.mark_as_changed() - - -def _mark_mode_changed_bond_child_interface_as_changed(net_state): - """ - When bond mode changed, due to NetworkManager bug - https://bugzilla.redhat.com/show_bug.cgi?id=1881318 - the bond child will be deactivated. - This is workaround would be manually activate the childs. - """ - for iface in net_state.ifaces.values(): - if not iface.parent: - continue - parent_iface = net_state.ifaces[iface.parent] - if ( - parent_iface.is_up - and parent_iface.type == InterfaceType.BOND - and parent_iface.is_bond_mode_changed - ): - iface.mark_as_changed() +def _is_memory_only(nm_profile): + if nm_profile: + profile_flags = nm_profile.get_flags() + return ( + NM.SettingsConnectionFlags.UNSAVED & profile_flags + or NM.SettingsConnectionFlags.VOLATILE & profile_flags + ) + return False diff --git a/libnmstate/nm/profiles.py b/libnmstate/nm/profiles.py new file mode 100644 index 0000000..8cf1907 --- /dev/null +++ b/libnmstate/nm/profiles.py @@ -0,0 +1,265 @@ +# +# Copyright (c) 2020 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 . +# + +# This file is targeting: +# * Actions required the knownldege of multiple NmProfile + +import logging + +from libnmstate.schema import InterfaceType + +from .common import NM +from .device import is_externally_managed +from .device import list_devices +from .device import get_nm_dev +from .dns import get_dns_config_iface_names +from .ipv4 import acs_and_ip_profiles as acs_and_ip4_profiles +from .ipv6 import acs_and_ip_profiles as acs_and_ip6_profiles +from .ovs import create_iface_for_nm_ovs_port +from .profile import NmProfile +from .profile import ProfileDelete + + +class NmProfiles: + def __init__(self, context): + self._ctx = context + + def apply_config(self, net_state, save_to_disk): + self._prepare_state_for_profiles(net_state) + self._profiles = [ + NmProfile(self._ctx, iface, save_to_disk) + for iface in net_state.ifaces.all_ifaces() + if (iface.is_changed or iface.is_desired) and not iface.is_ignore + ] + + for profile in self._profiles: + profile.save_config() + self._ctx.wait_all_finish() + + for action in NmProfile.ACTIONS: + for profile in self._profiles: + if profile.has_action(action): + profile.do_action(action) + self._ctx.wait_all_finish() + + if save_to_disk: + for profile in self._profiles: + profile.delete_other_profiles() + _delete_orphan_nm_ovs_port_profiles(self._ctx, net_state) + + def _prepare_state_for_profiles(self, net_state): + _preapply_dns_fix_for_profiles(self._ctx, net_state) + _mark_nm_external_subordinate_changed(self._ctx, net_state) + _mark_mode_changed_bond_child_interface_as_changed(net_state) + _append_nm_ovs_port_iface(net_state) + + +def _append_nm_ovs_port_iface(net_state): + """ + In NM OVS, each OVS internal/system/ interface should be + subordinate of NM OVS port profile which is port of the OVS bridge + profile. + We need to create/delete this NM OVS port profile accordingly. + """ + nm_ovs_port_ifaces = {} + + for iface in net_state.ifaces.all_kernel_ifaces.values(): + if iface.controller_type == InterfaceType.OVS_BRIDGE: + nm_ovs_port_iface = create_iface_for_nm_ovs_port(iface) + iface.set_controller( + nm_ovs_port_iface.name, InterfaceType.OVS_PORT + ) + if iface.is_desired or iface.is_changed: + nm_ovs_port_iface.mark_as_changed() + nm_ovs_port_ifaces[nm_ovs_port_iface.name] = nm_ovs_port_iface + + net_state.ifaces.add_ifaces(nm_ovs_port_ifaces.values()) + + +def get_all_applied_configs(context): + applied_configs = {} + for nm_dev in list_devices(context.client): + if ( + nm_dev.get_state() + in ( + NM.DeviceState.ACTIVATED, + NM.DeviceState.IP_CONFIG, + ) + and nm_dev.get_managed() + ): + iface_name = nm_dev.get_iface() + if iface_name: + iface_type_str = nm_dev.get_type_description() + action = ( + f"Retrieve applied config: {iface_type_str} {iface_name}" + ) + context.register_async(action, fast=True) + nm_dev.get_applied_connection_async( + flags=0, + cancellable=context.cancellable, + callback=_get_applied_config_callback, + user_data=(iface_name, action, applied_configs, context), + ) + context.wait_all_finish() + return applied_configs + + +def _get_applied_config_callback(nm_dev, result, user_data): + iface_name, action, applied_configs, context = user_data + context.finish_async(action) + try: + remote_conn, _ = nm_dev.get_applied_connection_finish(result) + # TODO: We should use both interface name and type as key below. + applied_configs[nm_dev.get_iface()] = remote_conn + except Exception as e: + logging.warning( + "Failed to retrieve applied config for device " + f"{iface_name}: {e}" + ) + + +def _preapply_dns_fix_for_profiles(context, net_state): + """ + * When DNS configuration does not changed and old interface hold DNS + configuration is not included in `ifaces_desired_state`, preserve + the old DNS configure by removing DNS metadata from + `ifaces_desired_state`. + * When DNS configuration changed, include old interface which is holding + DNS configuration, so it's DNS configure could be removed. + """ + cur_dns_iface_names = get_dns_config_iface_names( + acs_and_ip4_profiles(context.client), + acs_and_ip6_profiles(context.client), + ) + + # Whether to mark interface as changed which is used for holding old DNS + # configurations + remove_existing_dns_config = False + # Whether to preserve old DNS config by DNS metadata to be removed from + # desired state + preserve_old_dns_config = False + if net_state.dns.config == net_state.dns.current_config: + for cur_dns_iface_name in cur_dns_iface_names: + iface = net_state.ifaces.all_kernel_ifaces[cur_dns_iface_name] + if iface.is_changed or iface.is_desired: + remove_existing_dns_config = True + if not remove_existing_dns_config: + preserve_old_dns_config = True + else: + remove_existing_dns_config = True + + if remove_existing_dns_config: + for cur_dns_iface_name in cur_dns_iface_names: + iface = net_state.ifaces.all_kernel_ifaces[cur_dns_iface_name] + iface.mark_as_changed() + + if preserve_old_dns_config: + for iface in net_state.ifaces.all_kernel_ifaces.values(): + if iface.is_changed or iface.is_desired: + iface.remove_dns_metadata() + + +def _mark_nm_external_subordinate_changed(context, net_state): + """ + When certain main interface contains subordinates is marked as + connected(externally), it means its profile is memory only and will lost + on next deactivation. + For this case, we should mark the subordinate as changed. + that subordinate should be marked as changed for NM to take over. + """ + for iface in net_state.ifaces.all_ifaces(): + if ( + iface.is_controller + and iface.is_up + and (iface.is_changed or iface.is_desired) + ): + for subordinate in iface.port: + port_iface = net_state.ifaces.all_kernel_ifaces.get( + subordinate + ) + if port_iface: + nmdev = get_nm_dev(context, subordinate, port_iface.type) + if nmdev: + if is_externally_managed(nmdev): + port_iface.mark_as_changed() + + +def _mark_mode_changed_bond_child_interface_as_changed(net_state): + """ + When bond mode changed, due to NetworkManager bug + https://bugzilla.redhat.com/show_bug.cgi?id=1881318 + the bond child will be deactivated. + This is workaround would be manually activate the childs. + """ + for iface in net_state.ifaces.all_kernel_ifaces.values(): + if not iface.parent: + continue + parent_iface = net_state.ifaces.get_iface( + iface.parent, InterfaceType.BOND + ) + if ( + parent_iface + and parent_iface.is_up + and parent_iface.is_bond_mode_changed + ): + iface.mark_as_changed() + + +def _delete_orphan_nm_ovs_port_profiles(context, net_state): + all_deleted_ovs_bridges = {} + for iface in net_state.ifaces.all_user_space_ifaces: + if iface.type == InterfaceType.OVS_BRIDGE and iface.is_absent: + all_deleted_ovs_bridges[iface.name] = iface + if not all_deleted_ovs_bridges: + return + for nm_profile in context.client.get_connections(): + if nm_profile.get_connection_type() != InterfaceType.OVS_PORT: + continue + conn_setting = nm_profile.get_setting_connection() + if not conn_setting: + continue + ovs_port_name = nm_profile.get_interface_name() + controller = conn_setting.get_master() + ovs_br_iface = all_deleted_ovs_bridges.get(controller) + need_delete = False + if ovs_br_iface: + if ovs_br_iface.is_absent: + need_delete = True + else: + has_ovs_interface = False + for port in ovs_br_iface.port: + ovs_iface = net_state.ifaces.all_kernel_ifaces.get(port) + if ( + ovs_iface + and ovs_iface.controller == ovs_port_name + and ovs_iface.controller_type == InterfaceType.OVS_PORT + ): + has_ovs_interface = True + break + if not has_ovs_interface: + need_delete = True + if need_delete: + ProfileDelete( + context, + ovs_port_name, + InterfaceType.OVS_PORT, + nm_profile, + ).run() + + context.wait_all_finish() diff --git a/libnmstate/nm/route.py b/libnmstate/nm/route.py index 113f2aa..cb43f28 100644 --- a/libnmstate/nm/route.py +++ b/libnmstate/nm/route.py @@ -160,44 +160,6 @@ def get_static_gateway_iface(family, iface_routes): return None -def get_routing_rule_config(acs_and_ip_profiles): - rules = [] - for (_, ip_profile) in acs_and_ip_profiles: - for i in range(ip_profile.get_num_routing_rules()): - nm_rule = ip_profile.get_routing_rule(i) - rules.append(_nm_rule_to_info(nm_rule)) - - return rules - - -def _nm_rule_to_info(nm_rule): - info = { - RouteRule.IP_FROM: _nm_rule_get_from(nm_rule), - RouteRule.IP_TO: _nm_rule_get_to(nm_rule), - RouteRule.PRIORITY: nm_rule.get_priority(), - RouteRule.ROUTE_TABLE: nm_rule.get_table(), - } - cleanup_keys = [key for key, val in info.items() if val is None] - for key in cleanup_keys: - del info[key] - - return info - - -def _nm_rule_get_from(nm_rule): - if nm_rule.get_from(): - return iplib.to_ip_address_full( - nm_rule.get_from(), nm_rule.get_from_len() - ) - return None - - -def _nm_rule_get_to(nm_rule): - if nm_rule.get_to(): - return iplib.to_ip_address_full(nm_rule.get_to(), nm_rule.get_to_len()) - return None - - def add_route_rules(setting_ip, family, rules): for rule in rules: setting_ip.add_routing_rule(_rule_info_to_nm_rule(rule, family)) diff --git a/libnmstate/nm/sriov.py b/libnmstate/nm/sriov.py index 5213bef..a7e387c 100644 --- a/libnmstate/nm/sriov.py +++ b/libnmstate/nm/sriov.py @@ -17,9 +17,7 @@ # along with this program. If not, see . # -from libnmstate.error import NmstateNotSupportedError from libnmstate.schema import Ethernet -from libnmstate.schema import Interface from .common import NM from .common import GLib @@ -49,9 +47,8 @@ SRIOV_NMSTATE_TO_NM_MAP = { } -def create_setting(context, iface_state, base_con_profile): +def create_setting(iface_state, base_con_profile): sriov_setting = None - ifname = iface_state[Interface.NAME] sriov_config = iface_state.get(Ethernet.CONFIG_SUBTREE, {}).get( Ethernet.SRIOV_SUBTREE ) @@ -61,11 +58,6 @@ def create_setting(context, iface_state, base_con_profile): NM.SETTING_SRIOV_SETTING_NAME ) if sriov_config: - if not _has_sriov_capability(context, ifname): - raise NmstateNotSupportedError( - f"Interface '{ifname}' does not support SR-IOV" - ) - if sriov_setting: sriov_setting = sriov_setting.duplicate() else: @@ -117,8 +109,3 @@ def _set_nm_attribute(vf_object, key, value): def _remove_sriov_vfs_in_setting(vfs_config, sriov_setting, vf_ids_to_remove): for vf_id in vf_ids_to_remove: yield vf_id - - -def _has_sriov_capability(context, ifname): - dev = context.get_nm_dev(ifname) - return dev and (NM.DeviceCapabilities.SRIOV & dev.props.capabilities) diff --git a/libnmstate/nm/team.py b/libnmstate/nm/team.py index 5630018..cf96d73 100644 --- a/libnmstate/nm/team.py +++ b/libnmstate/nm/team.py @@ -63,12 +63,12 @@ def _convert_team_config_to_teamd_format(team_config, ifname): team_config = copy.deepcopy(team_config) team_config[TEAMD_JSON_DEVICE] = ifname - team_ports = team_config.get(Team.PORT_SUBTREE, ()) + team_ports = team_config.pop(Team.PORT_SUBTREE, ()) team_ports_formatted = { port[Team.Port.NAME]: _dict_key_filter(port, Team.Port.NAME) for port in team_ports } - team_config[Team.PORT_SUBTREE] = team_ports_formatted + team_config[TEAMD_JSON_PORTS] = team_ports_formatted return team_config diff --git a/libnmstate/nm/translator.py b/libnmstate/nm/translator.py index 992b2a3..c3993ab 100644 --- a/libnmstate/nm/translator.py +++ b/libnmstate/nm/translator.py @@ -49,6 +49,7 @@ class Api2Nm: InterfaceType.LINUX_BRIDGE: NM.SETTING_BRIDGE_SETTING_NAME, InterfaceType.VRF: NM.SETTING_VRF_SETTING_NAME, InterfaceType.INFINIBAND: NM.SETTING_INFINIBAND_SETTING_NAME, + InterfaceType.MAC_VTAP: NM.SETTING_MACVLAN_SETTING_NAME, InterfaceType.MAC_VLAN: NM.SETTING_MACVLAN_SETTING_NAME, } try: diff --git a/libnmstate/nmstate.py b/libnmstate/nmstate.py index 302f2cd..a7373c9 100644 --- a/libnmstate/nmstate.py +++ b/libnmstate/nmstate.py @@ -72,11 +72,7 @@ def show_with_plugins(plugins, include_status_data=None): report[Route.KEY] = _get_routes_from_plugins(plugins) - route_rule_plugin = _find_plugin_for_capability( - plugins, NmstatePlugin.PLUGIN_CAPABILITY_ROUTE_RULE - ) - if route_rule_plugin: - report[RouteRule.KEY] = route_rule_plugin.get_route_rules() + report[RouteRule.KEY] = _get_route_rules_from_plugins(plugins) dns_plugin = _find_plugin_for_capability( plugins, NmstatePlugin.PLUGIN_CAPABILITY_DNS @@ -315,6 +311,20 @@ def _get_routes_from_plugins(plugins): return ret +def _get_route_rules_from_plugins(plugins): + ret = {RouteRule.CONFIG: []} + for plugin in plugins: + if ( + NmstatePlugin.PLUGIN_CAPABILITY_ROUTE_RULE + in plugin.plugin_capabilities + ): + plugin_route_rules = plugin.get_route_rules() + ret[RouteRule.CONFIG].extend( + plugin_route_rules.get(RouteRule.CONFIG, []) + ) + return ret + + def _get_iface_types_by_name(iface_infos, name): """ Return the type of all ifaces with valid type and specified name diff --git a/libnmstate/plugins/nmstate_plugin_ovsdb.py b/libnmstate/plugins/nmstate_plugin_ovsdb.py index ee87f71..e9106ef 100644 --- a/libnmstate/plugins/nmstate_plugin_ovsdb.py +++ b/libnmstate/plugins/nmstate_plugin_ovsdb.py @@ -193,7 +193,7 @@ class NmstateOvsdbPlugin(NmstatePlugin): ][OvsDB.EXTERNAL_IDS] pending_changes = [] - for iface in net_state.ifaces.values(): + for iface in net_state.ifaces.all_ifaces(): if not iface.is_changed and not iface.is_desired: continue if not iface.is_up: diff --git a/libnmstate/route.py b/libnmstate/route.py index 8664d8c..611ee91 100644 --- a/libnmstate/route.py +++ b/libnmstate/route.py @@ -28,6 +28,7 @@ from libnmstate.prettystate import format_desired_current_state_diff from libnmstate.schema import Interface from libnmstate.schema import Route +from .ifaces.base_iface import BaseIface from .state import StateEntry from .state import state_match @@ -121,7 +122,7 @@ class RouteEntry(StateEntry): if not self.destination: self._invalid_reason = "Route entry does not have destination" return False - iface = ifaces.get(self.next_hop_interface) + iface = ifaces.all_kernel_ifaces.get(self.next_hop_interface) if not iface: self._invalid_reason = ( f"Route {self.to_dict()} next hop to unknown interface" @@ -190,7 +191,9 @@ class RouteState: rt = RouteEntry(entry) if not rt.absent: if rt.is_valid(ifaces): - ifaces[rt.next_hop_interface].mark_as_changed() + ifaces.all_kernel_ifaces[ + rt.next_hop_interface + ].mark_as_changed() self._routes[rt.next_hop_interface].add(rt) else: raise NmstateValueError(rt.invalid_reason) @@ -209,7 +212,6 @@ class RouteState: if not rt.match(route): new_routes.add(route) if new_routes != route_set: - ifaces[iface_name].mark_as_changed() self._routes[iface_name] = new_routes def gen_metadata(self, ifaces): @@ -229,6 +231,10 @@ class RouteState: Interface.IPV4: [], Interface.IPV6: [], } + if route_set != self._cur_routes[iface_name]: + route_metadata[iface_name][ + BaseIface.ROUTE_CHANGED_METADATA + ] = True for route in route_set: family = Interface.IPV6 if route.is_ipv6 else Interface.IPV4 route_metadata[iface_name][family].append(route.to_dict()) diff --git a/libnmstate/route_rule.py b/libnmstate/route_rule.py index cadbc73..1a5487b 100644 --- a/libnmstate/route_rule.py +++ b/libnmstate/route_rule.py @@ -1,7 +1,25 @@ +# +# Copyright (c) 2020 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 collections import defaultdict import logging -from libnmstate.error import NmstateNotImplementedError from libnmstate.error import NmstateVerificationError from libnmstate.error import NmstateValueError from libnmstate.iplib import KERNEL_MAIN_ROUTE_TABLE_ID @@ -13,6 +31,7 @@ from libnmstate.schema import InterfaceIP from libnmstate.schema import RouteRule from libnmstate.schema import Route +from .ifaces.base_iface import BaseIface from .state import StateEntry from .state import state_match @@ -23,21 +42,23 @@ class RouteRuleEntry(StateEntry): self.ip_to = route_rule.get(RouteRule.IP_TO) self.priority = route_rule.get(RouteRule.PRIORITY) self.route_table = route_rule.get(RouteRule.ROUTE_TABLE) + self.state = route_rule.get(RouteRule.STATE) self._complement_defaults() self._canonicalize_ip_network() def _complement_defaults(self): - if self.ip_from is None: - self.ip_from = "" - if self.ip_to is None: - self.ip_to = "" - if self.priority is None: - self.priority = RouteRule.USE_DEFAULT_PRIORITY - if ( - self.route_table is None - or self.route_table == RouteRule.USE_DEFAULT_ROUTE_TABLE - ): - self.route_table = KERNEL_MAIN_ROUTE_TABLE_ID + if not self.absent: + if self.ip_from is None: + self.ip_from = "" + if self.ip_to is None: + self.ip_to = "" + if self.priority is None: + self.priority = RouteRule.USE_DEFAULT_PRIORITY + if ( + self.route_table is None + or self.route_table == RouteRule.USE_DEFAULT_ROUTE_TABLE + ): + self.route_table = KERNEL_MAIN_ROUTE_TABLE_ID def _canonicalize_ip_network(self): if self.ip_from: @@ -63,9 +84,7 @@ class RouteRuleEntry(StateEntry): @property def absent(self): - raise NmstateNotImplementedError( - "RouteRuleEntry does not support absent property" - ) + return self.state == RouteRule.STATE_ABSENT def is_valid(self, config_iface_routes): """ @@ -89,28 +108,50 @@ class RouteRuleState: self._cur_rules = defaultdict(set) self._rules = defaultdict(set) if cur_rule_state: - for rule_dict in _get_config(cur_rule_state): - rule = RouteRuleEntry(rule_dict) - self._cur_rules[rule.route_table].add(rule) + for entry in _get_config(cur_rule_state): + rl = RouteRuleEntry(entry) + self._cur_rules[rl.route_table].add(rl) + if not route_state or rl.is_valid( + route_state.config_iface_routes + ): + self._rules[rl.route_table].add(rl) if des_rule_state: - for rule_dict in _get_config(des_rule_state): - rule = RouteRuleEntry(rule_dict) - self._rules[rule.route_table].add(rule) - if self._rules != self._cur_rules: - self._config_changed = True - else: - # Discard invalid route rule when merging from current - for rules in self._cur_rules.values(): - for rule in rules: - if not route_state or rule.is_valid( - route_state.config_iface_routes - ): - self._rules[rule.route_table].add(rule) + self._merge_rules(des_rule_state, route_state) @property def _config(self): return _get_config(self._rules) + def _merge_rules(self, des_rule_state, route_state): + """ + Handle absent rules before adding desired rule entries to make sure + absent rule does not delete rule defined in desired state. + """ + for entry in _get_config(des_rule_state): + rl = RouteRuleEntry(entry) + if rl.absent: + self._apply_absent_rules(rl) + for entry in _get_config(des_rule_state): + rl = RouteRuleEntry(entry) + if not rl.absent: + self._rules[rl.route_table].add(rl) + + def _apply_absent_rules(self, rl): + """ + Remove rules based on absent rules and treat missing property as + wildcard match. + """ + absent_iface_table = rl.route_table + for route_table, rule_set in self._rules.items(): + if absent_iface_table and absent_iface_table != route_table: + continue + new_rules = set() + for rule in rule_set: + if not rl.match(rule): + new_rules.add(rule) + if new_rules != rule_set: + self._rules[route_table] = new_rules + def verify(self, cur_rule_state): current = RouteRuleState( route_state=None, @@ -159,6 +200,10 @@ class RouteRuleState: Interface.IPV4: [], Interface.IPV6: [], } + if rules != self._cur_rules[route_table]: + route_rule_metadata[iface_name][ + BaseIface.RULE_CHANGED_METADATA + ] = True for rule in rules: family = Interface.IPV6 if rule.is_ipv6 else Interface.IPV4 route_rule_metadata[iface_name][family].append(rule.to_dict()) diff --git a/libnmstate/schema.py b/libnmstate/schema.py index b71576f..4018110 100644 --- a/libnmstate/schema.py +++ b/libnmstate/schema.py @@ -73,6 +73,8 @@ class RouteRule: ROUTE_TABLE = "route-table" USE_DEFAULT_PRIORITY = -1 USE_DEFAULT_ROUTE_TABLE = 0 + STATE = "state" + STATE_ABSENT = "absent" class DNS: @@ -106,6 +108,7 @@ class InterfaceType: ETHERNET = "ethernet" LINUX_BRIDGE = "linux-bridge" MAC_VLAN = "mac-vlan" + MAC_VTAP = "mac-vtap" OVS_BRIDGE = "ovs-bridge" OVS_INTERFACE = "ovs-interface" OVS_PORT = "ovs-port" @@ -372,7 +375,7 @@ class Team: TYPE = InterfaceType.TEAM CONFIG_SUBTREE = InterfaceType.TEAM - PORT_SUBTREE = "ports" + PORT_SUBTREE = "port" RUNNER_SUBTREE = "runner" class Port: @@ -429,3 +432,8 @@ class MacVlan: PRIVATE = "private" PASSTHRU = "passthru" SOURCE = "source" + + +class MacVtap(MacVlan): + TYPE = InterfaceType.MAC_VTAP + CONFIG_SUBTREE = "mac-vtap" diff --git a/libnmstate/schemas/operational-state.yaml b/libnmstate/schemas/operational-state.yaml index 1f9b378..3f81fa2 100644 --- a/libnmstate/schemas/operational-state.yaml +++ b/libnmstate/schemas/operational-state.yaml @@ -32,6 +32,7 @@ properties: - "$ref": "#/definitions/interface-vrf/rw" - "$ref": "#/definitions/interface-infiniband/rw" - "$ref": "#/definitions/interface-mac-vlan/rw" + - "$ref": "#/definitions/interface-mac-vtap/rw" - "$ref": "#/definitions/interface-other/rw" routes: type: object @@ -665,6 +666,29 @@ definitions: - unknown promiscuous: type: boolean + interface-mac-vtap: + rw: + properties: + type: + type: string + enum: + - mac-vtap + mac-vtap: + type: object + properties: + base-iface: + type: string + mode: + type: string + enum: + - private + - vepa + - bridge + - passthru + - source + - unknown + promiscuous: + type: boolean interface-other: rw: properties: @@ -723,6 +747,10 @@ definitions: type: integer route-table: type: integer + state: + type: string + enum: + - absent lldp: ro: properties: diff --git a/nmstate.egg-info/PKG-INFO b/nmstate.egg-info/PKG-INFO index 86af553..dabc333 100644 --- a/nmstate.egg-info/PKG-INFO +++ b/nmstate.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: nmstate -Version: 0.4.1 +Version: 1.0.0 Summary: Declarative network manager API Home-page: https://nmstate.github.io/ Author: Edward Haas diff --git a/nmstate.egg-info/SOURCES.txt b/nmstate.egg-info/SOURCES.txt index da21f47..477181e 100644 --- a/nmstate.egg-info/SOURCES.txt +++ b/nmstate.egg-info/SOURCES.txt @@ -20,6 +20,8 @@ examples/linuxbrige_eth1_up.yml examples/linuxbrige_eth1_up_port_vlan.yml examples/mac_vlan_absent.yml examples/mac_vlan_create.yml +examples/mac_vtap_absent.yml +examples/mac_vtap_create.yml examples/ovsbridge_bond_create.yml examples/ovsbridge_create.yml examples/ovsbridge_delete.yml @@ -61,6 +63,7 @@ libnmstate/ifaces/infiniband.py libnmstate/ifaces/linux_bridge.py libnmstate/ifaces/linux_bridge_port_vlan.py libnmstate/ifaces/macvlan.py +libnmstate/ifaces/macvtap.py libnmstate/ifaces/ovs.py libnmstate/ifaces/team.py libnmstate/ifaces/vlan.py @@ -74,9 +77,11 @@ libnmstate/nispor/bridge_port_vlan.py libnmstate/nispor/dummy.py libnmstate/nispor/ethernet.py libnmstate/nispor/macvlan.py +libnmstate/nispor/macvtap.py libnmstate/nispor/ovs.py libnmstate/nispor/plugin.py libnmstate/nispor/route.py +libnmstate/nispor/route_rule.py libnmstate/nispor/vlan.py libnmstate/nispor/vrf.py libnmstate/nispor/vxlan.py @@ -99,7 +104,7 @@ libnmstate/nm/macvlan.py libnmstate/nm/ovs.py libnmstate/nm/plugin.py libnmstate/nm/profile.py -libnmstate/nm/profile_state.py +libnmstate/nm/profiles.py libnmstate/nm/route.py libnmstate/nm/sriov.py libnmstate/nm/team.py diff --git a/nmstate.egg-info/requires.txt b/nmstate.egg-info/requires.txt index 8579746..ff542b6 100644 --- a/nmstate.egg-info/requires.txt +++ b/nmstate.egg-info/requires.txt @@ -3,4 +3,4 @@ PyGObject PyYAML setuptools varlink -nispor>=0.6.1 +nispor>=1.0.0 diff --git a/requirements.txt b/requirements.txt index c8a7997..e9f472a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,4 @@ PyGObject PyYAML setuptools varlink -nispor>=0.6.1 +nispor>=1.0.0