commit 654b4d10bc5a61422e415e26cb419d829a9e8844 Author: CentOS Sources Date: Tue Feb 16 02:43:23 2021 -0500 import nmstate-0.3.4-25.el8_3 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4e6d7d9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +SOURCES/nmstate-0.3.4.tar.gz +SOURCES/nmstate.gpg diff --git a/.nmstate.metadata b/.nmstate.metadata new file mode 100644 index 0000000..3f697a6 --- /dev/null +++ b/.nmstate.metadata @@ -0,0 +1,2 @@ +d732a1ccb1dfc54741a9d602179c809c3223af3a SOURCES/nmstate-0.3.4.tar.gz +b5f872551d434e2c62b30d70471efaeede83ab44 SOURCES/nmstate.gpg diff --git a/SOURCES/BZ_1858758-fix_ovs_bond.patch b/SOURCES/BZ_1858758-fix_ovs_bond.patch new file mode 100644 index 0000000..d71cfcd --- /dev/null +++ b/SOURCES/BZ_1858758-fix_ovs_bond.patch @@ -0,0 +1,40 @@ +From 862e669fcfe02b49c0e24af210d6466197962b97 Mon Sep 17 00:00:00 2001 +From: Gris Ge +Date: Mon, 3 Aug 2020 11:34:44 +0800 +Subject: [PATCH] ovs: Fix bug when adding bond to existing bridge + +When adding OVS bond/link aggregation interface to existing OVS bridge, +nmstate will fail with error: + + > self._ifaces[slave_name].mark_as_changed() + E KeyError: 'bond1' + +This is because ovs bond interface does not require a interface entry in +desire state. + +Fixed by check before adding dict. + +Integration test case added. + +Signed-off-by: Gris Ge +--- + libnmstate/ifaces/ifaces.py | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +diff --git a/libnmstate/ifaces/ifaces.py b/libnmstate/ifaces/ifaces.py +index a400712..1c2ffd5 100644 +--- a/libnmstate/ifaces/ifaces.py ++++ b/libnmstate/ifaces/ifaces.py +@@ -217,7 +217,8 @@ class Ifaces: + self._ifaces[iface_name].mark_as_changed() + if cur_iface: + for slave_name in iface.config_changed_slaves(cur_iface): +- self._ifaces[slave_name].mark_as_changed() ++ if slave_name in self._ifaces: ++ self._ifaces[slave_name].mark_as_changed() + + def _match_child_iface_state_with_parent(self): + """ +-- +2.28.0 + diff --git a/SOURCES/BZ_1858762-hide_ovs_patch_port_mtu.patch b/SOURCES/BZ_1858762-hide_ovs_patch_port_mtu.patch new file mode 100644 index 0000000..3e5bea9 --- /dev/null +++ b/SOURCES/BZ_1858762-hide_ovs_patch_port_mtu.patch @@ -0,0 +1,105 @@ +From bc2f8445d493f8a5a4ff1ceead13d2b3ac5325cc Mon Sep 17 00:00:00 2001 +From: Fernando Fernandez Mancera +Date: Sun, 26 Jul 2020 00:46:16 +0200 +Subject: [PATCH 1/2] nm.wired: do not report MTU if it is 0 + +If an interface contains an MTU with value 0, Nmstate should not report +it because it is an special interface like OVS patch port interfaces. + +Added a test case for this. + +Signed-off-by: Fernando Fernandez Mancera +--- + libnmstate/nm/wired.py | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +diff --git a/libnmstate/nm/wired.py b/libnmstate/nm/wired.py +index 27d4318..64662ac 100644 +--- a/libnmstate/nm/wired.py ++++ b/libnmstate/nm/wired.py +@@ -124,7 +124,9 @@ def get_info(device): + + iface = device.get_iface() + try: +- info[Interface.MTU] = int(device.get_mtu()) ++ mtu = int(device.get_mtu()) ++ if mtu: ++ info[Interface.MTU] = mtu + except AttributeError: + pass + +-- +2.27.0 + + +From 03aea7d7debfca0f01b60e9f406c9acdf3de3775 Mon Sep 17 00:00:00 2001 +From: Gris Ge +Date: Mon, 27 Jul 2020 20:51:53 +0800 +Subject: [PATCH 2/2] nm ovs: Raise NmstateNotSupportedError for + save_to_disk=False + +Due to limitation of NetworkManager 1.26, nmstate cannot support +`save_to_disk=False`(ask, memory only) state for OVS interfaces. + +Raise NmstateNotSupportedError if NetworkManager version is older than +1.28 and has OVS interface in desire state with `save_to_disk=False`. + +Integration test case included. + +Signed-off-by: Gris Ge +Signed-off-by: Fernando Fernandez Mancera +--- + libnmstate/nm/applier.py | 23 +++++++++++++++++++++++ + 1 file changed, 23 insertions(+) + +diff --git a/libnmstate/nm/applier.py b/libnmstate/nm/applier.py +index 4e20af5..a91cee5 100644 +--- a/libnmstate/nm/applier.py ++++ b/libnmstate/nm/applier.py +@@ -17,9 +17,11 @@ + # along with this program. If not, see . + # + ++from distutils.version import StrictVersion + import logging + import itertools + ++from libnmstate.error import NmstateNotSupportedError + from libnmstate.error import NmstateValueError + from libnmstate.schema import Interface + from libnmstate.schema import InterfaceState +@@ -65,6 +67,17 @@ MASTER_IFACE_TYPES = ( + def apply_changes(context, net_state, save_to_disk): + con_profiles = [] + ++ if ( ++ not save_to_disk ++ and _has_ovs_interface_desired_or_changed(net_state) ++ and StrictVersion(context.client.get_version()) ++ < StrictVersion("1.28.0") ++ ): ++ raise NmstateNotSupportedError( ++ f"NetworkManager version {context.client.get_version()} does not " ++ f"support 'save_to_disk=False' against OpenvSwitch interface" ++ ) ++ + _preapply_dns_fix(context, net_state) + + ifaces_desired_state = net_state.ifaces.state_to_edit +@@ -602,3 +615,13 @@ def _preapply_dns_fix(context, net_state): + for iface in net_state.ifaces.values(): + if iface.is_changed or iface.is_desired: + iface.remove_dns_metadata() ++ ++ ++def _has_ovs_interface_desired_or_changed(net_state): ++ for iface in net_state.ifaces.values(): ++ if iface.type in ( ++ InterfaceType.OVS_BRIDGE, ++ InterfaceType.OVS_INTERFACE, ++ InterfaceType.OVS_PORT, ++ ) and (iface.is_desired or iface.is_changed): ++ return True +-- +2.27.0 + diff --git a/SOURCES/BZ_1859844-fix_converting_memory_only.patch b/SOURCES/BZ_1859844-fix_converting_memory_only.patch new file mode 100644 index 0000000..ef80e65 --- /dev/null +++ b/SOURCES/BZ_1859844-fix_converting_memory_only.patch @@ -0,0 +1,59 @@ +From fc7e6b2329409b95ab1726b7b65f14c284bf67ab Mon Sep 17 00:00:00 2001 +From: Gris Ge +Date: Mon, 3 Aug 2020 12:07:56 +0800 +Subject: [PATCH] nm: Fix converting memory-only profile to persistent + +When converting memory-only profile to persistent using simple desire +state with `state: up` only, the memory only profile will not be +converted to persistent. + +This is caused by `nm/applier.py` skip profile creation if desire state +is only `state: up`. + +The fix is checking whether current profile's persistent state is equal +to desired. + +Integration test case added. + +Signed-off-by: Gris Ge +--- + libnmstate/nm/applier.py | 1 + + libnmstate/nm/connection.py | 10 ++++++++++ + 2 files changed, 11 insertions(+) + +diff --git a/libnmstate/nm/applier.py b/libnmstate/nm/applier.py +index 4d40862..26a057f 100644 +--- a/libnmstate/nm/applier.py ++++ b/libnmstate/nm/applier.py +@@ -125,6 +125,7 @@ def apply_changes(context, net_state, save_to_disk): + and cur_con_profile + and cur_con_profile.profile + and not net_state.ifaces[ifname].is_changed ++ and cur_con_profile.is_memory_only != save_to_disk + ): + # Don't create new profile if original desire does not ask + # anything besides state:up and not been marked as changed. +diff --git a/libnmstate/nm/connection.py b/libnmstate/nm/connection.py +index 5804f13..1f6c734 100644 +--- a/libnmstate/nm/connection.py ++++ b/libnmstate/nm/connection.py +@@ -162,6 +162,16 @@ class ConnectionProfile: + assert self._con_profile is None + self._con_profile = con_profile + ++ @property ++ def is_memory_only(self): ++ if self._con_profile: ++ profile_flags = self._con_profile.get_flags() ++ return ( ++ NM.SettingsConnectionFlags.UNSAVED & profile_flags ++ or NM.SettingsConnectionFlags.VOLATILE & profile_flags ++ ) ++ return False ++ + @property + def devname(self): + if self._con_profile: +-- +2.28.0 + diff --git a/SOURCES/BZ_1861263-handle-external-managed-interface.patch b/SOURCES/BZ_1861263-handle-external-managed-interface.patch new file mode 100644 index 0000000..dc59dc4 --- /dev/null +++ b/SOURCES/BZ_1861263-handle-external-managed-interface.patch @@ -0,0 +1,65 @@ +From ea7f304cc1ad32c3f2c25b49bf6b2663f348496a Mon Sep 17 00:00:00 2001 +From: Gris Ge +Date: Tue, 28 Jul 2020 15:59:11 +0800 +Subject: [PATCH] nm: Mark external subordinate as changed + +When user create bond with subordinate interfaces using non-NM +tools(iproute), the NetworkManager will mark the subordinates as +managed externally. + +When the desire state only contains the main interface, nmstate +noticing the slave list is unchanged, so only activate the main +interface, then NM remove the subordinate from their main interface. + +To workaround that, mark subordinate interfaces as changed when they are +managed by NM as externally. + +Integration test case included. + +Signed-off-by: Gris Ge +--- + libnmstate/nm/applier.py | 24 ++++++++++++++++++++++++ + 1 file changed, 24 insertions(+) + +diff --git a/libnmstate/nm/applier.py b/libnmstate/nm/applier.py +index a91cee5..68d11dc 100644 +--- a/libnmstate/nm/applier.py ++++ b/libnmstate/nm/applier.py +@@ -79,6 +79,7 @@ def apply_changes(context, net_state, save_to_disk): + ) + + _preapply_dns_fix(context, net_state) ++ _mark_nm_external_subordinate_changed(context, net_state) + + ifaces_desired_state = net_state.ifaces.state_to_edit + ifaces_desired_state.extend( +@@ -625,3 +626,26 @@ def _has_ovs_interface_desired_or_changed(net_state): + InterfaceType.OVS_PORT, + ) and (iface.is_desired or iface.is_changed): + return True ++ ++ ++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 MASTER_IFACE_TYPES: ++ for subordinate in iface.slaves: ++ nmdev = context.get_nm_dev(subordinate) ++ if nmdev: ++ nm_ac = nmdev.get_active_connection() ++ if ( ++ nm_ac ++ and NM.ActivationStateFlags.EXTERNAL ++ & nm_ac.get_state_flags() ++ ): ++ subordinate_iface = net_state.ifaces[subordinate] ++ subordinate_iface.mark_as_changed() +-- +2.27.0 + diff --git a/SOURCES/BZ_1861668_ignore_unknown_iface.patch b/SOURCES/BZ_1861668_ignore_unknown_iface.patch new file mode 100644 index 0000000..665dd7d --- /dev/null +++ b/SOURCES/BZ_1861668_ignore_unknown_iface.patch @@ -0,0 +1,200 @@ +From a8590744fbdd4bce9ab340ac49a7add31727b990 Mon Sep 17 00:00:00 2001 +From: Gris Ge +Date: Wed, 29 Jul 2020 17:51:27 +0800 +Subject: [PATCH 1/3] nm applier: Fix external managed interface been marked as + changed + +The net_state passing to `_mark_nm_external_subordinate_changed()` +does not contain unknown type interface, so the +`net_state.ifaces[subordinate]` could trigger KeyError as subordinate +not found. + +Changed it to use `get()` and only perform this changes to +desired/changed interface. + +Signed-off-by: Gris Ge +--- + libnmstate/nm/applier.py | 7 ++++--- + 1 file changed, 4 insertions(+), 3 deletions(-) + +diff --git a/libnmstate/nm/applier.py b/libnmstate/nm/applier.py +index 68d11dc..d0fb5f3 100644 +--- a/libnmstate/nm/applier.py ++++ b/libnmstate/nm/applier.py +@@ -637,7 +637,7 @@ def _mark_nm_external_subordinate_changed(context, net_state): + that subordinate should be marked as changed for NM to take over. + """ + for iface in net_state.ifaces.values(): +- if iface.type in MASTER_IFACE_TYPES: ++ if iface.is_desired or iface.is_changed and iface.is_master: + for subordinate in iface.slaves: + nmdev = context.get_nm_dev(subordinate) + if nmdev: +@@ -647,5 +647,6 @@ def _mark_nm_external_subordinate_changed(context, net_state): + and NM.ActivationStateFlags.EXTERNAL + & nm_ac.get_state_flags() + ): +- subordinate_iface = net_state.ifaces[subordinate] +- subordinate_iface.mark_as_changed() ++ subordinate_iface = net_state.ifaces.get(subordinate) ++ if subordinate_iface: ++ subordinate_iface.mark_as_changed() +-- +2.28.0 + + +From 77a05cfe726efc4a4207d57958a71e1730b6d62a Mon Sep 17 00:00:00 2001 +From: Gris Ge +Date: Wed, 29 Jul 2020 18:02:52 +0800 +Subject: [PATCH 2/3] nm: Ignore externally managed interface for down/absent + main interface + +When main interface been marked as down or absent, its subordinate +should be also marked as so. NetworkManager plugin should ignore +externally managed subordinate in this case. + +Signed-off-by: Gris Ge +--- + libnmstate/nm/applier.py | 26 +++++++++++--------------- + libnmstate/nm/device.py | 8 ++++++++ + 2 files changed, 19 insertions(+), 15 deletions(-) + +diff --git a/libnmstate/nm/applier.py b/libnmstate/nm/applier.py +index d0fb5f3..9cd8f9a 100644 +--- a/libnmstate/nm/applier.py ++++ b/libnmstate/nm/applier.py +@@ -50,6 +50,7 @@ from . import vxlan + from . import wired + from .common import NM + from .dns import get_dns_config_iface_names ++from .device import is_externally_managed + + + MAXIMUM_INTERFACE_LENGTH = 15 +@@ -265,13 +266,14 @@ def _set_ifaces_admin_state(context, ifaces_desired_state, con_profiles): + == InterfaceState.ABSENT + ) + for affected_nmdev in nmdevs: +- devs_to_deactivate[ +- affected_nmdev.get_iface() +- ] = affected_nmdev +- if is_absent: +- devs_to_delete_profile[ ++ if not is_externally_managed(affected_nmdev): ++ devs_to_deactivate[ + affected_nmdev.get_iface() + ] = affected_nmdev ++ if is_absent: ++ devs_to_delete_profile[ ++ affected_nmdev.get_iface() ++ ] = affected_nmdev + if ( + is_absent + and nmdev.is_software() +@@ -640,13 +642,7 @@ def _mark_nm_external_subordinate_changed(context, net_state): + if iface.is_desired or iface.is_changed and iface.is_master: + for subordinate in iface.slaves: + nmdev = context.get_nm_dev(subordinate) +- if nmdev: +- nm_ac = nmdev.get_active_connection() +- if ( +- nm_ac +- and NM.ActivationStateFlags.EXTERNAL +- & nm_ac.get_state_flags() +- ): +- subordinate_iface = net_state.ifaces.get(subordinate) +- if subordinate_iface: +- subordinate_iface.mark_as_changed() ++ if nmdev and is_externally_managed(nmdev): ++ subordinate_iface = net_state.ifaces.get(subordinate) ++ if subordinate_iface: ++ subordinate_iface.mark_as_changed() +diff --git a/libnmstate/nm/device.py b/libnmstate/nm/device.py +index 528f57d..a175b71 100644 +--- a/libnmstate/nm/device.py ++++ b/libnmstate/nm/device.py +@@ -23,6 +23,7 @@ from libnmstate.error import NmstateLibnmError + + from . import active_connection as ac + from . import connection ++from .common import NM + + + def activate(context, dev=None, connection_id=None): +@@ -161,3 +162,10 @@ def get_device_common_info(dev): + "type_name": dev.get_type_description(), + "state": dev.get_state(), + } ++ ++ ++def is_externally_managed(nmdev): ++ nm_ac = nmdev.get_active_connection() ++ return ( ++ nm_ac and NM.ActivationStateFlags.EXTERNAL & nm_ac.get_state_flags() ++ ) +-- +2.28.0 + + +From afb51e8421b8749962dd9ee2e31b61548de09a78 Mon Sep 17 00:00:00 2001 +From: Gris Ge +Date: Wed, 29 Jul 2020 18:22:32 +0800 +Subject: [PATCH 3/3] state: Remove unmanaged interface before verifying + +Since we remove unknown type interface before sending to apply, +we should also remove unknown type interface before verifying. + +Signed-off-by: Gris Ge +--- + libnmstate/ifaces/ifaces.py | 9 +++++---- + libnmstate/nm/device.py | 4 +--- + 2 files changed, 6 insertions(+), 7 deletions(-) + +diff --git a/libnmstate/ifaces/ifaces.py b/libnmstate/ifaces/ifaces.py +index 1ff4198..a400712 100644 +--- a/libnmstate/ifaces/ifaces.py ++++ b/libnmstate/ifaces/ifaces.py +@@ -286,16 +286,16 @@ class Ifaces: + def cur_ifaces(self): + return self._cur_ifaces + +- def _remove_unmanaged_slaves(self): ++ def _remove_unknown_interface_type_slaves(self): + """ +- When master containing unmanaged slaves, they should be removed from +- master slave list. ++ When master containing slaves with unknown interface type, they should ++ be removed from master slave list before verifying. + """ + for iface in self._ifaces.values(): + if iface.is_up and iface.is_master and iface.slaves: + for slave_name in iface.slaves: + slave_iface = self._ifaces[slave_name] +- if not slave_iface.is_up: ++ if slave_iface.type == InterfaceType.UNKNOWN: + iface.remove_slave(slave_name) + + def verify(self, cur_iface_infos): +@@ -304,6 +304,7 @@ class Ifaces: + cur_iface_infos=cur_iface_infos, + save_to_disk=self._save_to_disk, + ) ++ cur_ifaces._remove_unknown_interface_type_slaves() + for iface in self._ifaces.values(): + if iface.is_desired: + if iface.is_virtual and iface.original_dict.get( +diff --git a/libnmstate/nm/device.py b/libnmstate/nm/device.py +index a175b71..fdf05bc 100644 +--- a/libnmstate/nm/device.py ++++ b/libnmstate/nm/device.py +@@ -166,6 +166,4 @@ def get_device_common_info(dev): + + def is_externally_managed(nmdev): + nm_ac = nmdev.get_active_connection() +- return ( +- nm_ac and NM.ActivationStateFlags.EXTERNAL & nm_ac.get_state_flags() +- ) ++ return nm_ac and NM.ActivationStateFlags.EXTERNAL & nm_ac.get_state_flags() +-- +2.28.0 + diff --git a/SOURCES/BZ_1862025-remove_existing_profiles.patch b/SOURCES/BZ_1862025-remove_existing_profiles.patch new file mode 100644 index 0000000..2a07082 --- /dev/null +++ b/SOURCES/BZ_1862025-remove_existing_profiles.patch @@ -0,0 +1,68 @@ +From 3c5337a22273717df6fb51818216816bbff77035 Mon Sep 17 00:00:00 2001 +From: Gris Ge +Date: Thu, 30 Jul 2020 16:54:40 +0800 +Subject: [PATCH] nm profile: Remove inactivate profile of desired interface + +For changed/desired interface, NM should remove all its inactive +profiles so that it could update and activate the same one. + +Integration test case included. + +Signed-off-by: Gris Ge +--- + libnmstate/nm/applier.py | 10 +++++++++- + libnmstate/nm/connection.py | 9 +++++++-- + 2 files changed, 16 insertions(+), 3 deletions(-) + +diff --git a/libnmstate/nm/applier.py b/libnmstate/nm/applier.py +index 9cd8f9a..4d40862 100644 +--- a/libnmstate/nm/applier.py ++++ b/libnmstate/nm/applier.py +@@ -105,6 +105,15 @@ def apply_changes(context, net_state, save_to_disk): + cur_con_profile = connection.ConnectionProfile( + context, profile=con_profile + ) ++ ++ if save_to_disk: ++ # TODO: Need handle save_to_disk=False ++ connection.delete_iface_profiles_except( ++ context, ++ ifname, ++ cur_con_profile.profile if cur_con_profile else None, ++ ) ++ + original_desired_iface_state = {} + if net_state.ifaces.get(ifname): + iface = net_state.ifaces[ifname] +@@ -137,7 +146,6 @@ def apply_changes(context, net_state, save_to_disk): + con_profiles.append(new_con_profile) + else: + # Missing connection, attempting to create a new one. +- connection.delete_iface_inactive_connections(context, ifname) + new_con_profile.add(save_to_disk) + con_profiles.append(new_con_profile) + context.wait_all_finish() +diff --git a/libnmstate/nm/connection.py b/libnmstate/nm/connection.py +index 02890bc..5804f13 100644 +--- a/libnmstate/nm/connection.py ++++ b/libnmstate/nm/connection.py +@@ -496,9 +496,14 @@ def get_device_active_connection(nm_device): + return active_conn + + +-def delete_iface_inactive_connections(context, ifname): ++def delete_iface_profiles_except(context, ifname, excluded_profile): + for con in list_connections_by_ifname(context, ifname): +- con.delete() ++ if ( ++ not excluded_profile ++ or not con.profile ++ or con.profile.get_uuid() != excluded_profile.get_uuid() ++ ): ++ con.delete() + + + def list_connections_by_ifname(context, ifname): +-- +2.28.0 + diff --git a/SOURCES/BZ_1866269-preserve_nm_uuid_in_ovsdb.patch b/SOURCES/BZ_1866269-preserve_nm_uuid_in_ovsdb.patch new file mode 100644 index 0000000..214aa8c --- /dev/null +++ b/SOURCES/BZ_1866269-preserve_nm_uuid_in_ovsdb.patch @@ -0,0 +1,120 @@ +From 21e06fd5af76cc1fe65497222a04c1cffa2bc546 Mon Sep 17 00:00:00 2001 +From: Gris Ge +Date: Thu, 6 Aug 2020 13:07:59 +0800 +Subject: [PATCH] ovsdb: Preserve the NM external_ids + +For newly created OVS internal interface with customer external_ids, +the ovsdb plugin will remove the NM external_ids `NM.connection.uuid`. + +The fix is read the current `NM.connection.uuid` before applying +configure and merge it with original desired state. + +Integration test cases added. + +Signed-off-by: Gris Ge +--- + libnmstate/plugins/nmstate_plugin_ovsdb.py | 69 +++++++++++++--------- + 1 file changed, 40 insertions(+), 29 deletions(-) + +diff --git a/libnmstate/plugins/nmstate_plugin_ovsdb.py b/libnmstate/plugins/nmstate_plugin_ovsdb.py +index 83965e1..12ab10d 100644 +--- a/libnmstate/plugins/nmstate_plugin_ovsdb.py ++++ b/libnmstate/plugins/nmstate_plugin_ovsdb.py +@@ -161,7 +161,14 @@ class NmstateOvsdbPlugin(NmstatePlugin): + return ifaces + + def apply_changes(self, net_state, save_to_disk): ++ # State might changed after other plugin invoked apply_changes() + self.refresh_content() ++ cur_iface_to_ext_ids = {} ++ for iface_info in self.get_interfaces(): ++ cur_iface_to_ext_ids[iface_info[Interface.NAME]] = iface_info[ ++ OvsDB.OVS_DB_SUBTREE ++ ][OvsDB.EXTERNAL_IDS] ++ + pending_changes = [] + for iface in net_state.ifaces.values(): + if not iface.is_changed and not iface.is_desired: +@@ -174,7 +181,34 @@ class NmstateOvsdbPlugin(NmstatePlugin): + table_name = "Interface" + else: + continue +- pending_changes.extend(_generate_db_change(table_name, iface)) ++ ids_after_nm_applied = cur_iface_to_ext_ids.get(iface.name, {}) ++ ids_before_nm_applied = ( ++ iface.to_dict() ++ .get(OvsDB.OVS_DB_SUBTREE, {}) ++ .get(OvsDB.EXTERNAL_IDS, {}) ++ ) ++ original_desire_ids = iface.original_dict.get( ++ OvsDB.OVS_DB_SUBTREE, {} ++ ).get(OvsDB.EXTERNAL_IDS) ++ ++ desire_ids = [] ++ ++ if original_desire_ids is None: ++ desire_ids = ids_before_nm_applied ++ else: ++ desire_ids = original_desire_ids ++ ++ # should include external_id created by NetworkManager. ++ if NM_EXTERNAL_ID in ids_after_nm_applied: ++ desire_ids[NM_EXTERNAL_ID] = ids_after_nm_applied[ ++ NM_EXTERNAL_ID ++ ] ++ if desire_ids != ids_after_nm_applied: ++ pending_changes.append( ++ _generate_db_change_external_ids( ++ table_name, iface.name, desire_ids ++ ) ++ ) + if pending_changes: + if not save_to_disk: + raise NmstateNotImplementedError( +@@ -242,38 +276,15 @@ class NmstateOvsdbPlugin(NmstatePlugin): + ) + + +-def _generate_db_change(table_name, iface_state): +- return _generate_db_change_external_ids(table_name, iface_state) +- +- +-def _generate_db_change_external_ids(table_name, iface_state): +- pending_changes = [] +- desire_ids = iface_state.original_dict.get(OvsDB.OVS_DB_SUBTREE, {}).get( +- OvsDB.EXTERNAL_IDS +- ) ++def _generate_db_change_external_ids(table_name, iface_name, desire_ids): + if desire_ids and not isinstance(desire_ids, dict): + raise NmstateValueError("Invalid external_ids, should be dictionary") + +- if desire_ids or desire_ids == {}: +- # should include external_id required by NetworkManager. +- merged_ids = ( +- iface_state.to_dict() +- .get(OvsDB.OVS_DB_SUBTREE, {}) +- .get(OvsDB.EXTERNAL_IDS, {}) +- ) +- if NM_EXTERNAL_ID in merged_ids: +- desire_ids[NM_EXTERNAL_ID] = merged_ids[NM_EXTERNAL_ID] +- +- # Convert all value to string +- for key, value in desire_ids.items(): +- desire_ids[key] = str(value) ++ # Convert all value to string ++ for key, value in desire_ids.items(): ++ desire_ids[key] = str(value) + +- pending_changes.append( +- _Changes( +- table_name, OvsDB.EXTERNAL_IDS, iface_state.name, desire_ids +- ) +- ) +- return pending_changes ++ return _Changes(table_name, OvsDB.EXTERNAL_IDS, iface_name, desire_ids) + + + NMSTATE_PLUGIN = NmstateOvsdbPlugin +-- +2.28.0 + diff --git a/SOURCES/BZ_1869345_ovsdb_remove_all_ports.patch b/SOURCES/BZ_1869345_ovsdb_remove_all_ports.patch new file mode 100644 index 0000000..e1a1ac3 --- /dev/null +++ b/SOURCES/BZ_1869345_ovsdb_remove_all_ports.patch @@ -0,0 +1,44 @@ +From 913b739c8fea8e9b14d3785371c8e4f48723dbd6 Mon Sep 17 00:00:00 2001 +From: Gris Ge +Date: Tue, 18 Aug 2020 17:55:12 +0800 +Subject: [PATCH] ovsdb: Allowing remove all ports from OVS bridge + +When removing all ports from OVS bridge, the OVSDB will have no +information regarding this bridge, which cause OVSDB failed to find +the correct row. + +Silently ignore row not found failure and let verification stage do the +work. + +Signed-off-by: Gris Ge +--- + libnmstate/plugins/nmstate_plugin_ovsdb.py | 8 -------- + 1 file changed, 8 deletions(-) + +diff --git a/libnmstate/plugins/nmstate_plugin_ovsdb.py b/libnmstate/plugins/nmstate_plugin_ovsdb.py +index 12ab10d..f667e8f 100644 +--- a/libnmstate/plugins/nmstate_plugin_ovsdb.py ++++ b/libnmstate/plugins/nmstate_plugin_ovsdb.py +@@ -222,19 +222,11 @@ class NmstateOvsdbPlugin(NmstatePlugin): + def _db_write(self, changes): + changes_index = {change.row_name: change for change in changes} + changed_tables = set(change.table_name for change in changes) +- updated_names = [] + for changed_table in changed_tables: + for row in self._idl.tables[changed_table].rows.values(): + if row.name in changes_index: + change = changes_index[row.name] + setattr(row, change.column_name, change.column_value) +- updated_names.append(change.row_name) +- new_rows = set(changes_index.keys()) - set(updated_names) +- if new_rows: +- raise NmstatePluginError( +- f"BUG: row {new_rows} does not exists in OVS DB " +- "and currently we don't create new row" +- ) + + def _start_transaction(self): + self._transaction = Transaction(self._idl) +-- +2.28.0 + diff --git a/SOURCES/BZ_1887349-Allow-duplicate-iface-name-in-ovs.patch b/SOURCES/BZ_1887349-Allow-duplicate-iface-name-in-ovs.patch new file mode 100644 index 0000000..d47e0d8 --- /dev/null +++ b/SOURCES/BZ_1887349-Allow-duplicate-iface-name-in-ovs.patch @@ -0,0 +1,111 @@ +From 36ee761dd0d671439323077e4f77a89071fdcd9c Mon Sep 17 00:00:00 2001 +From: Edward Haas +Date: Wed, 7 Oct 2020 20:13:12 +0300 +Subject: [PATCH 1/2] nm, bridge, ovs: Collect only existing profiles + +During the reporting flow, connections that are in teardown process +no longer point to a valid profile. Avoid collecting such profiles (in +practice, these are actually `None` objects). + +Signed-off-by: Edward Haas +Signed-off-by: Gris Ge +--- + libnmstate/nm/bridge.py | 6 +++--- + libnmstate/nm/ovs.py | 4 +++- + 2 files changed, 6 insertions(+), 4 deletions(-) + +diff --git a/libnmstate/nm/bridge.py b/libnmstate/nm/bridge.py +index b885f7a..0ca6c2d 100644 +--- a/libnmstate/nm/bridge.py ++++ b/libnmstate/nm/bridge.py +@@ -260,9 +260,9 @@ def _get_slave_profiles_by_name(master_device): + for dev in master_device.get_slaves(): + active_con = connection.get_device_active_connection(dev) + if active_con: +- slaves_profiles_by_name[ +- dev.get_iface() +- ] = active_con.props.connection ++ profile = active_con.props.connection ++ if profile: ++ slaves_profiles_by_name[dev.get_iface()] = profile + return slaves_profiles_by_name + + +diff --git a/libnmstate/nm/ovs.py b/libnmstate/nm/ovs.py +index 2518773..eb373c3 100644 +--- a/libnmstate/nm/ovs.py ++++ b/libnmstate/nm/ovs.py +@@ -279,5 +279,7 @@ def _get_slave_profiles(master_device, devices_info): + if active_con: + master = active_con.props.master + if master and (master.get_iface() == master_device.get_iface()): +- slave_profiles.append(active_con.props.connection) ++ profile = active_con.props.connection ++ if profile: ++ slave_profiles.append(profile) + return slave_profiles +-- +2.28.0 + + +From caf638d75e57da8770cd884782475f1c5668fd6d Mon Sep 17 00:00:00 2001 +From: Edward Haas +Date: Wed, 7 Oct 2020 12:26:42 +0300 +Subject: [PATCH 2/2] nm, ovs: Fix report crash when OVS has dup iface names + +In case of an existing OVS deployment which uses an identical name for +the bridge, port and interface, libnmstate.show() exploded. + +It is now possible to report such deployments. + +Signed-off-by: Edward Haas +Signed-off-by: Gris Ge +--- + libnmstate/nm/ovs.py | 24 +++++++++++++++++++++--- + 1 file changed, 21 insertions(+), 3 deletions(-) + +diff --git a/libnmstate/nm/ovs.py b/libnmstate/nm/ovs.py +index eb373c3..d1f26ba 100644 +--- a/libnmstate/nm/ovs.py ++++ b/libnmstate/nm/ovs.py +@@ -140,7 +140,12 @@ def get_port_by_slave(nmdev): + + + def get_ovs_info(context, bridge_device, devices_info): +- port_profiles = _get_slave_profiles(bridge_device, devices_info) ++ ovs_ports_info = ( ++ info ++ for info in devices_info ++ if is_ovs_port_type_id(info[1]["type_id"]) ++ ) ++ port_profiles = _get_slave_profiles(bridge_device, ovs_ports_info) + ports = _get_bridge_ports_info(context, port_profiles, devices_info) + options = _get_bridge_options(context, bridge_device) + +@@ -203,8 +208,21 @@ def _get_bridge_port_info(context, port_profile, devices_info): + vlan_mode = port_setting.props.vlan_mode + + port_name = port_profile.get_interface_name() +- port_device = context.get_nm_dev(port_name) +- port_slave_profiles = _get_slave_profiles(port_device, devices_info) ++ port_device = next( ++ dev ++ for dev, devinfo in devices_info ++ if devinfo["name"] == port_name ++ and is_ovs_port_type_id(devinfo["type_id"]) ++ ) ++ devices_info_excluding_bridges_and_ports = ( ++ info ++ for info in devices_info ++ if not is_ovs_bridge_type_id(info[1]["type_id"]) ++ and not is_ovs_port_type_id(info[1]["type_id"]) ++ ) ++ port_slave_profiles = _get_slave_profiles( ++ port_device, devices_info_excluding_bridges_and_ports ++ ) + port_slave_names = [c.get_interface_name() for c in port_slave_profiles] + + if port_slave_names: +-- +2.28.0 + diff --git a/SOURCES/BZ_1890497-nm-bond-Ignore-ad_actor_system-00-00-00-00-00-00.patch b/SOURCES/BZ_1890497-nm-bond-Ignore-ad_actor_system-00-00-00-00-00-00.patch new file mode 100644 index 0000000..986aab9 --- /dev/null +++ b/SOURCES/BZ_1890497-nm-bond-Ignore-ad_actor_system-00-00-00-00-00-00.patch @@ -0,0 +1,49 @@ +From 8a7f1758da4cba81d65ba4b9b06bbf4b750a6f87 Mon Sep 17 00:00:00 2001 +From: Gris Ge +Date: Thu, 22 Oct 2020 14:09:27 +0800 +Subject: [PATCH 1/2] nm bond: Ignore ad_actor_system=00:00:00:00:00:00 + +The ad_actor_system=00:00:00:00:00:00 is invalid in kernel as that's the +default value of ad_actor_system. + +NM plugin should not set that value. + +Test case included. + +Signed-off-by: Gris Ge +Signed-off-by: Fernando Fernandez Mancera +--- + libnmstate/nm/bond.py | 9 +++++++++ + tests/integration/nm/bond_test.py | 12 ++++++++++++ + 2 files changed, 21 insertions(+) + +diff --git a/libnmstate/nm/bond.py b/libnmstate/nm/bond.py +index 9ea3648..d196965 100644 +--- a/libnmstate/nm/bond.py ++++ b/libnmstate/nm/bond.py +@@ -38,6 +38,8 @@ NM_SUPPORTED_BOND_OPTIONS = NM.SettingBond.get_valid_options( + + SYSFS_BOND_OPTION_FOLDER_FMT = "/sys/class/net/{ifname}/bonding" + ++BOND_AD_ACTOR_SYSTEM_USE_BOND_MAC = "00:00:00:00:00:00" ++ + + def create_setting(options, wired_setting): + bond_setting = NM.SettingBond.new() +@@ -48,6 +50,13 @@ def create_setting(options, wired_setting): + ): + # When in MAC restricted mode, MAC address should be unset. + wired_setting.props.cloned_mac_address = None ++ if ( ++ option_name == "ad_actor_system" ++ and option_value == BOND_AD_ACTOR_SYSTEM_USE_BOND_MAC ++ ): ++ # The all zero ad_actor_system is the kernel default value ++ # And it is invalid to set as all zero ++ continue + if option_value != SYSFS_EMPTY_VALUE: + success = bond_setting.add_option(option_name, str(option_value)) + if not success: +-- +2.25.4 + diff --git a/SOURCES/BZ_1890497-nm.ipv6-call-clear_routing_rules-when-creating-the-s.patch b/SOURCES/BZ_1890497-nm.ipv6-call-clear_routing_rules-when-creating-the-s.patch new file mode 100644 index 0000000..5c6f4b5 --- /dev/null +++ b/SOURCES/BZ_1890497-nm.ipv6-call-clear_routing_rules-when-creating-the-s.patch @@ -0,0 +1,33 @@ +From 46104725c121def5d85f492afd91e618c1c7c240 Mon Sep 17 00:00:00 2001 +From: Fernando Fernandez Mancera +Date: Tue, 27 Oct 2020 12:23:18 +0100 +Subject: [PATCH 2/2] nm.ipv6: call clear_routing_rules() when creating the + setting + +In order to remove IPv6 routing rules Nmstate needs to call +clear_routing_rules() on the IPv6 setting as it is done on the IPv4 +setting. + +Added a testcase for this. + +Signed-off-by: Fernando Fernandez Mancera +--- + libnmstate/nm/ipv6.py | 1 + + tests/integration/route_test.py | 15 +++++++++++++++ + 2 files changed, 16 insertions(+) + +diff --git a/libnmstate/nm/ipv6.py b/libnmstate/nm/ipv6.py +index f252578..9777c89 100644 +--- a/libnmstate/nm/ipv6.py ++++ b/libnmstate/nm/ipv6.py +@@ -106,6 +106,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 +-- +2.25.4 + diff --git a/SOURCES/BZ_1901571_do_not_check_ovs_daemon_when_showing.patch b/SOURCES/BZ_1901571_do_not_check_ovs_daemon_when_showing.patch new file mode 100644 index 0000000..5a91b83 --- /dev/null +++ b/SOURCES/BZ_1901571_do_not_check_ovs_daemon_when_showing.patch @@ -0,0 +1,103 @@ +From 2595c75cb8488e855fc5d98bcc944c6c0ad96b54 Mon Sep 17 00:00:00 2001 +From: Quique Llorente +Date: Tue, 24 Nov 2020 12:52:35 +0100 +Subject: [PATCH 1/2] ovs: Ignore ovs-port always + +At containerize nmstate we cannot run "systemctl openvswitch status" so +even with a openvswitch running at host it will appear as non running +but the ovs information will arrive from NM dbus interface. This breaks +nmstatectl show since ovs-port is not part of nmstate and is not +included in the schema. This change just ignore ovs-port even if +openvswitch appear as not running. + +Signed-off-by: Quique Llorente +Signed-off-by: Gris Ge +--- + libnmstate/nm/plugin.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/libnmstate/nm/plugin.py b/libnmstate/nm/plugin.py +index 4032359..6b6217d 100644 +--- a/libnmstate/nm/plugin.py ++++ b/libnmstate/nm/plugin.py +@@ -123,6 +123,8 @@ class NetworkManagerPlugin(NmstatePlugin): + if nm_bond.is_bond_type_id(type_id): + bondinfo = nm_bond.get_bond_info(dev) + iface_info.update(_ifaceinfo_bond(bondinfo)) ++ elif nm_ovs.is_ovs_port_type_id(type_id): ++ continue + elif NmstatePlugin.OVS_CAPABILITY in capabilities: + if nm_ovs.is_ovs_bridge_type_id(type_id): + iface_info["bridge"] = nm_ovs.get_ovs_info( +@@ -133,8 +135,6 @@ class NetworkManagerPlugin(NmstatePlugin): + ) + elif nm_ovs.is_ovs_interface_type_id(type_id): + iface_info.update(nm_ovs.get_interface_info(act_con)) +- elif nm_ovs.is_ovs_port_type_id(type_id): +- continue + + info.append(iface_info) + +-- +2.29.2 + + +From 3202bdd08737087160ff96bcf921793ce6b8335c Mon Sep 17 00:00:00 2001 +From: Quique Llorente +Date: Wed, 25 Nov 2020 10:28:43 +0100 +Subject: [PATCH 2/2] ovs: Ignore OVS capabilities at get interfaces + +At containerize nmstate we cannot run "systemctl openvswitch status" so +even with a openvswitch running at host it will appear as non running +but the ovs information will arrive from NM dbus interface. This breaks +nmstatectl show since ovs-port is not part of nmstate and is not +included in the schema. This change removed the whole OVS compatibility check +when processing the interfaces reporting. + +Signed-off-by: Quique Llorente +Signed-off-by: Gris Ge +--- + libnmstate/nm/plugin.py | 18 +++++++----------- + 1 file changed, 7 insertions(+), 11 deletions(-) + +diff --git a/libnmstate/nm/plugin.py b/libnmstate/nm/plugin.py +index 6b6217d..06a5acd 100644 +--- a/libnmstate/nm/plugin.py ++++ b/libnmstate/nm/plugin.py +@@ -97,7 +97,6 @@ class NetworkManagerPlugin(NmstatePlugin): + + def get_interfaces(self): + info = [] +- capabilities = self.capabilities + + devices_info = [ + (dev, nm_device.get_device_common_info(dev)) +@@ -123,18 +122,15 @@ class NetworkManagerPlugin(NmstatePlugin): + if nm_bond.is_bond_type_id(type_id): + bondinfo = nm_bond.get_bond_info(dev) + iface_info.update(_ifaceinfo_bond(bondinfo)) ++ elif nm_ovs.is_ovs_bridge_type_id(type_id): ++ iface_info["bridge"] = nm_ovs.get_ovs_info( ++ self.context, dev, devices_info ++ ) ++ iface_info = _remove_ovs_bridge_unsupported_entries(iface_info) ++ elif nm_ovs.is_ovs_interface_type_id(type_id): ++ iface_info.update(nm_ovs.get_interface_info(act_con)) + elif nm_ovs.is_ovs_port_type_id(type_id): + continue +- elif NmstatePlugin.OVS_CAPABILITY in capabilities: +- if nm_ovs.is_ovs_bridge_type_id(type_id): +- iface_info["bridge"] = nm_ovs.get_ovs_info( +- self.context, dev, devices_info +- ) +- iface_info = _remove_ovs_bridge_unsupported_entries( +- iface_info +- ) +- elif nm_ovs.is_ovs_interface_type_id(type_id): +- iface_info.update(nm_ovs.get_interface_info(act_con)) + + info.append(iface_info) + +-- +2.29.2 + diff --git a/SOURCES/BZ_1904889-do-not-remove-unmanaged-ovs-bridge.patch b/SOURCES/BZ_1904889-do-not-remove-unmanaged-ovs-bridge.patch new file mode 100644 index 0000000..e54c13f --- /dev/null +++ b/SOURCES/BZ_1904889-do-not-remove-unmanaged-ovs-bridge.patch @@ -0,0 +1,38 @@ +From d7393d40aaedeea5dd8291519cddeecdfdabc849 Mon Sep 17 00:00:00 2001 +From: Fernando Fernandez Mancera +Date: Mon, 7 Dec 2020 00:51:19 +0100 +Subject: [PATCH] ifaces: do not remove unmanaged orphan interfaces + +If there are unmanaged OVS interface present in the network state, NM +may report uncomplete information. Therefore, nmstate could consider +them as orphan and remove it when modifying the network state. + +In order to fix this, nmstate should not consider it orphan and remove +it when it is not on desired state or marked as changed. + +Signed-off-by: Fernando Fernandez Mancera +Signed-off-by: Gris Ge +--- + libnmstate/ifaces/ifaces.py | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +diff --git a/libnmstate/ifaces/ifaces.py b/libnmstate/ifaces/ifaces.py +index 1c2ffd5..703e672 100644 +--- a/libnmstate/ifaces/ifaces.py ++++ b/libnmstate/ifaces/ifaces.py +@@ -242,8 +242,10 @@ class Ifaces: + + 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) ++ if ( ++ iface.need_parent ++ and (not iface.parent or not self._ifaces.get(iface.parent)) ++ and (iface.is_desired or iface.is_changed) + ): + iface.mark_as_changed() + iface.state = InterfaceState.ABSENT +-- +2.18.4 + diff --git a/SOURCES/BZ_1908724-sriov-use-verification-retry-to-wait-VF-been-created.patch b/SOURCES/BZ_1908724-sriov-use-verification-retry-to-wait-VF-been-created.patch new file mode 100644 index 0000000..8427fbd --- /dev/null +++ b/SOURCES/BZ_1908724-sriov-use-verification-retry-to-wait-VF-been-created.patch @@ -0,0 +1,185 @@ +From 1d0656c4197f0119d156b0df7b13bffeb5c46861 Mon Sep 17 00:00:00 2001 +From: Gris Ge +Date: Mon, 4 Jan 2021 11:37:18 +0800 +Subject: [PATCH] sriov: Use verification retry to wait VF been created + +When reactivating i40e interface with SR-IOV enabled, the kernel +takes some time(1 seconds or more in my test) to get the VF interface +ready in kernel. So at the time of libnmstate returns with success, the +VF interface might not be ready for use yet. + +To fix that, we include VF interfaces in desire state when PV is +changed/desired. The verification retry will wait the VF to be ready for +use. + +Unit test case and integration test case included. + +Also fixed SRIOV integration test cases which are now all passing on i40e +NIC. + +To test on real SRIOV NIC: + + cd tests/integration/ + sudo env TEST_REAL_NIC=ens1f1 pytest-3 sriov_test.py -vvv + +Signed-off-by: Gris Ge +--- + libnmstate/ifaces/ethernet.py | 28 ++++ + libnmstate/ifaces/ifaces.py | 23 +++ + libnmstate/nm/sriov.py | 2 +- + libnmstate/nm/wired.py | 19 ++- + tests/integration/sriov_test.py | 242 ++++++++++++++++++++++---------- + tests/lib/nm/wired_test.py | 5 + + 6 files changed, 231 insertions(+), 88 deletions(-) + +diff --git a/libnmstate/ifaces/ethernet.py b/libnmstate/ifaces/ethernet.py +index b346c36..644fe6d 100644 +--- a/libnmstate/ifaces/ethernet.py ++++ b/libnmstate/ifaces/ethernet.py +@@ -18,6 +18,9 @@ + # + + from libnmstate.schema import Ethernet ++from libnmstate.schema import Interface ++from libnmstate.schema import InterfaceType ++from libnmstate.schema import InterfaceState + + from .base_iface import BaseIface + +@@ -46,6 +49,31 @@ class EthernetIface(BaseIface): + _capitalize_sriov_vf_mac(state) + return state + ++ @property ++ def sriov_total_vfs(self): ++ return ( ++ self.raw.get(Ethernet.CONFIG_SUBTREE, {}) ++ .get(Ethernet.SRIOV_SUBTREE, {}) ++ .get(Ethernet.SRIOV.TOTAL_VFS, 0) ++ ) ++ ++ def create_sriov_vf_ifaces(self): ++ return [ ++ EthernetIface( ++ { ++ # According to manpage of systemd.net-naming-scheme(7), ++ # SRIOV VF interface will have v{slot} in device name. ++ # Currently, nmstate has no intention to support ++ # user-defined udev rule on SRIOV interface naming policy. ++ Interface.NAME: f"{self.name}v{i}", ++ Interface.TYPE: InterfaceType.ETHERNET, ++ # VF will be in DOWN state initialy. ++ Interface.STATE: InterfaceState.DOWN, ++ } ++ ) ++ for i in range(0, self.sriov_total_vfs) ++ ] ++ + + def _capitalize_sriov_vf_mac(state): + vfs = ( +diff --git a/libnmstate/ifaces/ifaces.py b/libnmstate/ifaces/ifaces.py +index 703e672..7723f43 100644 +--- a/libnmstate/ifaces/ifaces.py ++++ b/libnmstate/ifaces/ifaces.py +@@ -97,6 +97,7 @@ class Ifaces: + self._ifaces[iface.name] = iface + + self._create_virtual_slaves() ++ self._create_sriov_vfs_when_changed() + self._validate_unknown_slaves() + self._validate_unknown_parent() + self._gen_metadata() +@@ -124,6 +125,28 @@ class Ifaces: + for iface in new_ifaces: + self._ifaces[iface.name] = iface + ++ def _create_sriov_vfs_when_changed(self): ++ """ ++ When plugin set the TOTAL_VFS of PF, it might take 1 seconds or ++ more to have the VFs to be ready. ++ Nmstate should use verification retry to make sure VFs are full ready. ++ To do that, we include VFs into desire state. ++ """ ++ new_ifaces = [] ++ for iface in self._ifaces.values(): ++ if ( ++ iface.is_up ++ and (iface.is_desired or iface.is_changed) ++ and iface.type == InterfaceType.ETHERNET ++ and iface.sriov_total_vfs > 0 ++ ): ++ for new_iface in iface.create_sriov_vf_ifaces(): ++ if new_iface.name not in self._ifaces: ++ new_iface.mark_as_desired() ++ new_ifaces.append(new_iface) ++ for new_iface in new_ifaces: ++ self._ifaces[new_iface.name] = new_iface ++ + def _pre_edit_validation_and_cleanup(self): + self._validate_over_booked_slaves() + self._validate_vlan_mtu() +diff --git a/libnmstate/nm/sriov.py b/libnmstate/nm/sriov.py +index f544732..25b150c 100644 +--- a/libnmstate/nm/sriov.py ++++ b/libnmstate/nm/sriov.py +@@ -68,7 +68,7 @@ def create_setting(context, iface_state, base_con_profile): + sriov_config = iface_state.get(Ethernet.CONFIG_SUBTREE, {}).get( + Ethernet.SRIOV_SUBTREE + ) +- if sriov_config: ++ if sriov_config and sriov_config.get(Ethernet.SRIOV.TOTAL_VFS): + if not _has_sriov_capability(context, ifname): + raise NmstateNotSupportedError( + f"Interface '{ifname}' does not support SR-IOV" +diff --git a/libnmstate/nm/wired.py b/libnmstate/nm/wired.py +index 64662ac..5fea2a5 100644 +--- a/libnmstate/nm/wired.py ++++ b/libnmstate/nm/wired.py +@@ -162,15 +162,18 @@ def _get_mac_address_from_sysfs(ifname): + + + def _get_ethernet_info(device, iface): ++ + ethernet = {} ++ sriov_info = sriov.get_info(device) ++ if sriov_info: ++ ethernet.update(sriov_info) ++ + try: + speed = int(device.get_speed()) + if speed > 0: + ethernet[Ethernet.SPEED] = speed +- else: +- return None + except AttributeError: +- return None ++ pass + + ethtool_results = minimal_ethtool(iface) + auto_setting = ethtool_results[Ethernet.AUTO_NEGOTIATION] +@@ -178,17 +181,11 @@ def _get_ethernet_info(device, iface): + ethernet[Ethernet.AUTO_NEGOTIATION] = True + elif auto_setting is False: + ethernet[Ethernet.AUTO_NEGOTIATION] = False +- else: +- return None + + duplex_setting = ethtool_results[Ethernet.DUPLEX] + if duplex_setting in [Ethernet.HALF_DUPLEX, Ethernet.FULL_DUPLEX]: + ethernet[Ethernet.DUPLEX] = duplex_setting +- else: +- return None +- +- sriov_info = sriov.get_info(device) +- if sriov_info: +- ethernet.update(sriov_info) + ++ if not ethernet: ++ return None + return ethernet + + +-- +2.25.4 + diff --git a/SOURCES/BZ_1910193-support-multiple-gateways.patch b/SOURCES/BZ_1910193-support-multiple-gateways.patch new file mode 100644 index 0000000..b9aa129 --- /dev/null +++ b/SOURCES/BZ_1910193-support-multiple-gateways.patch @@ -0,0 +1,92 @@ +From 47bd6db50e33aaa3d3d5e3b70d5f3039122b3a5c Mon Sep 17 00:00:00 2001 +From: Gris Ge +Date: Sat, 5 Sep 2020 00:36:41 +0800 +Subject: [PATCH] nm route: Add support of multiple gateways + +Since NetworkManager 1.22.0, the `NM.SettingIPConfig.props.routes` +support assigning multiple gateway. + +Integration test case updated for this. + +Signed-off-by: Gris Ge +--- + libnmstate/nm/route.py | 30 +++--------------------------- + 1 file changed, 3 insertions(+), 27 deletions(-) + +diff --git a/libnmstate/nm/route.py b/libnmstate/nm/route.py +index 53bcf7c..548b218 100644 +--- a/libnmstate/nm/route.py ++++ b/libnmstate/nm/route.py +@@ -21,7 +21,6 @@ from operator import itemgetter + import socket + + from libnmstate import iplib +-from libnmstate.error import NmstateNotImplementedError + from libnmstate.error import NmstateValueError + from libnmstate.nm import active_connection as nm_ac + from libnmstate.schema import Interface +@@ -31,7 +30,6 @@ from libnmstate.schema import RouteRule + from .common import GLib + from .common import NM + +-NM_ROUTE_TABLE_ATTRIBUTE = "table" + IPV4_DEFAULT_GATEWAY_DESTINATION = "0.0.0.0/0" + IPV6_DEFAULT_GATEWAY_DESTINATION = "::/0" + +@@ -116,7 +114,7 @@ def get_config(acs_and_ip_profiles): + + + def _get_per_route_table_id(nm_route, default_table_id): +- table = nm_route.get_attribute(NM_ROUTE_TABLE_ATTRIBUTE) ++ table = nm_route.get_attribute(NM.IP_ROUTE_ATTRIBUTE_TABLE) + return int(table.get_uint32()) if table else default_table_id + + +@@ -152,19 +150,7 @@ def _get_default_route_config(gateway, metric, default_table_id, iface_name): + + def add_routes(setting_ip, routes): + for route in routes: +- if route[Route.DESTINATION] in ( +- IPV4_DEFAULT_GATEWAY_DESTINATION, +- IPV6_DEFAULT_GATEWAY_DESTINATION, +- ): +- if setting_ip.get_gateway(): +- raise NmstateNotImplementedError( +- "Only a single default gateway is supported due to a " +- "limitation of NetworkManager: " +- "https://bugzilla.redhat.com/1707396" +- ) +- _add_route_gateway(setting_ip, route) +- else: +- _add_specfic_route(setting_ip, route) ++ _add_specfic_route(setting_ip, route) + + + def _add_specfic_route(setting_ip, route): +@@ -181,22 +167,12 @@ def _add_specfic_route(setting_ip, route): + ) + table_id = route.get(Route.TABLE_ID, Route.USE_DEFAULT_ROUTE_TABLE) + ip_route.set_attribute( +- NM_ROUTE_TABLE_ATTRIBUTE, GLib.Variant.new_uint32(table_id) ++ NM.IP_ROUTE_ATTRIBUTE_TABLE, GLib.Variant.new_uint32(table_id) + ) + # Duplicate route entry will be ignored by libnm. + setting_ip.add_route(ip_route) + + +-def _add_route_gateway(setting_ip, route): +- setting_ip.props.gateway = route[Route.NEXT_HOP_ADDRESS] +- setting_ip.props.route_table = route.get( +- Route.TABLE_ID, Route.USE_DEFAULT_ROUTE_TABLE +- ) +- setting_ip.props.route_metric = route.get( +- Route.METRIC, Route.USE_DEFAULT_METRIC +- ) +- +- + def get_static_gateway_iface(family, iface_routes): + """ + Return one interface with gateway for given IP family. +-- +2.27.0 + diff --git a/SOURCES/BZ_1916073_better-handling-for-timeout.patch b/SOURCES/BZ_1916073_better-handling-for-timeout.patch new file mode 100644 index 0000000..b47a1d8 --- /dev/null +++ b/SOURCES/BZ_1916073_better-handling-for-timeout.patch @@ -0,0 +1,419 @@ +From 803ad90f11eb57221e7805e5cba8c309bafe1de8 Mon Sep 17 00:00:00 2001 +From: Gris Ge +Date: Wed, 13 Jan 2021 23:51:31 +0800 +Subject: [PATCH 1/2] nm: Better handling for timeout + +When creating 1000 VLAN along with 1000 bridge using each VLAN, +NetworkManager might trigger two timeout: + + * The callback raises `Gio.IOErrorEnum.TIMED_OUT` error. + * NetworkManager never call callback, nmstate idle check trigger the + timeout. + +To solve above issue: + + * Increase the nmstate idle check timeout to 5 minutes. + + * For actions like add profile and activate profile, we add a + fallback checker which check whether requested action is already + finished using `GLib.timeout_source_new()` + + * When `Gio.IOErrorEnum.TIMED_OUT` happens, ignore the failure and wait + fallback checker. + + * The fallback checker is only started 15 seconds after action started, + so this does not impact small desire state. + +Test results on RHEL 8.3 i7-8665U 2G RAM: + + 10m29.212s to create 1000 VLAN and 1000 bridge over each VLAN. + +Changed the integration test case to test 500 VLANs + 500 bridges. + +Signed-off-by: Gris Ge +--- + libnmstate/nm/connection.py | 67 ++++++++++++++++++++++++++++++++++++- + libnmstate/nm/context.py | 3 +- + 2 files changed, 67 insertions(+), 3 deletions(-) + +diff --git a/libnmstate/nm/connection.py b/libnmstate/nm/connection.py +index 1f6c734..374a379 100644 +--- a/libnmstate/nm/connection.py ++++ b/libnmstate/nm/connection.py +@@ -1,5 +1,5 @@ + # +-# Copyright (c) 2018-2020 Red Hat, Inc. ++# Copyright (c) 2018-2021 Red Hat, Inc. + # + # This file is part of nmstate + # +@@ -24,11 +24,14 @@ from libnmstate.error import NmstateLibnmError + from libnmstate.error import NmstateInternalError + from libnmstate.error import NmstateValueError + ++from .common import GLib ++from .common import Gio + from .common import NM + from . import ipv4 + from . import ipv6 + + ACTIVATION_TIMEOUT_FOR_BRIDGE = 35 # Bridge STP requires 30 seconds. ++FALLBACK_CHECKER_INTERNAL = 15 + + + class ConnectionProfile: +@@ -40,6 +43,7 @@ class ConnectionProfile: + self._nm_ac = None + self._ac_handlers = set() + self._dev_handlers = set() ++ self._fallback_checker = None + + def create(self, settings): + self.profile = NM.SimpleConnection.new() +@@ -102,6 +106,26 @@ class ConnectionProfile: + self._add_connection2_callback, + user_data, + ) ++ self._fallback_checker = GLib.timeout_source_new( ++ FALLBACK_CHECKER_INTERNAL * 1000 ++ ) ++ self._fallback_checker.set_callback( ++ self._profile_add_fallback_checker_callback, action ++ ) ++ self._fallback_checker.attach(self._ctx.context) ++ ++ def _profile_add_fallback_checker_callback(self, action): ++ for nm_profile in self._ctx.client.get_connections(): ++ if nm_profile.get_uuid() == self.profile.get_uuid(): ++ self._fallback_checker_cleanup() ++ self._ctx.finish_async(action) ++ return GLib.SOURCE_REMOVE ++ return GLib.SOURCE_CONTINUE ++ ++ def _fallback_checker_cleanup(self): ++ if self._fallback_checker: ++ self._fallback_checker.destroy() ++ self._fallback_checker = None + + def delete(self): + if not self.profile: +@@ -152,6 +176,26 @@ class ConnectionProfile: + self._active_connection_callback, + user_data, + ) ++ self._fallback_checker = GLib.timeout_source_new( ++ FALLBACK_CHECKER_INTERNAL * 1000 ++ ) ++ self._fallback_checker.set_callback( ++ self._activation_fallback_checker_callback, action ++ ) ++ self._fallback_checker.attach(self._ctx.context) ++ ++ def _activation_fallback_checker_callback(self, action): ++ if self.devname: ++ self._nm_dev = self._ctx.get_nm_dev(self.devname) ++ if self._nm_dev: ++ self._activation_progress_check(action) ++ return GLib.SOURCE_CONTINUE ++ else: ++ logging.warn( ++ "Failed to get interface name from profile, " ++ "can not perform flalback check on activation" ++ ) ++ return GLib.SOURCE_REMOVE + + @property + def profile(self): +@@ -213,6 +257,18 @@ class ConnectionProfile: + + try: + nm_act_con = src_object.activate_connection_finish(result) ++ except GLib.Error as e: ++ if e.matches(Gio.io_error_quark(), Gio.IOErrorEnum.TIMED_OUT): ++ logging.debug( ++ f"{action} timeout on activation, " ++ "using fallback method to wait activation" ++ ) ++ return ++ else: ++ self._ctx.fail( ++ NmstateLibnmError(f"{action} failed: error={e}") ++ ) ++ return + except Exception as e: + self._ctx.fail(NmstateLibnmError(f"{action} failed: error={e}")) + return +@@ -366,6 +422,7 @@ class ConnectionProfile: + def _activation_clean_up(self): + self._remove_ac_handlers() + self._remove_dev_handlers() ++ self._fallback_checker_cleanup() + + def _is_activating(self): + if not self._nm_ac or not self._nm_dev: +@@ -396,6 +453,13 @@ class ConnectionProfile: + action = user_data + try: + profile = src_object.add_connection2_finish(result)[0] ++ except GLib.Error as e: ++ if e.matches(Gio.io_error_quark(), Gio.IOErrorEnum.TIMED_OUT): ++ logging.debug( ++ f"{action} timeout, using fallback method to " ++ "wait profile creation" ++ ) ++ return + except Exception as e: + self._ctx.fail( + NmstateLibnmError(f"{action} failed with error: {e}") +@@ -410,6 +474,7 @@ class ConnectionProfile: + ) + ) + else: ++ self._fallback_checker_cleanup() + self._ctx.finish_async(action) + + def _update2_callback(self, src_object, result, user_data): +diff --git a/libnmstate/nm/context.py b/libnmstate/nm/context.py +index 373ffe8..bc5c41c 100644 +--- a/libnmstate/nm/context.py ++++ b/libnmstate/nm/context.py +@@ -31,8 +31,7 @@ from .common import Gio + # last finish async action. + IDLE_CHECK_INTERNAL = 5 + +-# libnm dbus connection has reply timeout 25 seconds. +-IDLE_TIMEOUT = 25 ++IDLE_TIMEOUT = 60 * 5 # 5 minutes + + # NetworkManage is using dbus in libnm while the dbus has limitation on + # maximum number of pending replies per connection.(RHEL/CentOS 8 is 1024) +-- +2.27.0 + + +From ac82d18f96aa2313583efa1477be441291e2957c Mon Sep 17 00:00:00 2001 +From: Fernando Fernandez Mancera +Date: Sun, 17 Jan 2021 11:18:10 +0800 +Subject: [PATCH 2/2] nm: Use fallback checker on profile deactivation and + delete + +When NM is under heave loads, NM might raise timeout error when +try to deactivate or delete a profile, to solve that this patch +introduce the same method in 2407f98 +to have a fallback checker on whether profile is deactivated/deleted +every 15 seconds. + +No test case required as current `test_lot_of_vlans_with_bridges` test +case has `state: absent` which is good enough for testing this patch. + +Signed-off-by: Fernando Fernandez Mancera +Signed-off-by: Gris Ge +--- + libnmstate/nm/active_connection.py | 61 ++++++++++++++++++++++++------ + libnmstate/nm/connection.py | 30 +++++++++++++++ + 2 files changed, 80 insertions(+), 11 deletions(-) + +diff --git a/libnmstate/nm/active_connection.py b/libnmstate/nm/active_connection.py +index 062c78a..b235e8b 100644 +--- a/libnmstate/nm/active_connection.py ++++ b/libnmstate/nm/active_connection.py +@@ -21,12 +21,14 @@ import logging + + from libnmstate.error import NmstateLibnmError + ++from .common import Gio + from .common import GLib + from .common import GObject + from .common import NM + + + NM_AC_STATE_CHANGED_SIGNAL = "state-changed" ++FALLBACK_CHECKER_INTERNAL = 15 + + + class ActivationError(Exception): +@@ -37,6 +39,8 @@ class ActiveConnection: + def __init__(self, context=None, nm_ac_con=None): + self._ctx = context + self._act_con = nm_ac_con ++ self._signal_handler = None ++ self._fallback_checker = None + + nmdevs = None + if nm_ac_con: +@@ -75,19 +79,35 @@ class ActiveConnection: + + action = f"Deactivate profile: {self.devname}" + self._ctx.register_async(action) +- handler_id = act_connection.connect( ++ self._signal_handler = act_connection.connect( + NM_AC_STATE_CHANGED_SIGNAL, + self._wait_state_changed_callback, + action, + ) + if act_connection.props.state != NM.ActiveConnectionState.DEACTIVATING: +- user_data = (handler_id, action) ++ user_data = action + self._ctx.client.deactivate_connection_async( + act_connection, + self._ctx.cancellable, + self._deactivate_connection_callback, + user_data, + ) ++ self._fallback_checker = GLib.timeout_source_new( ++ FALLBACK_CHECKER_INTERNAL * 1000 ++ ) ++ self._fallback_checker.set_callback( ++ self._deactivation_fallback_checker_callback, action ++ ) ++ self._fallback_checker.attach(self._ctx.context) ++ ++ def _clean_up(self): ++ if self._signal_handler: ++ if self._act_con: ++ self._act_con.handler_disconnect(self._signal_handler) ++ self._signal_handler = None ++ if self._fallback_checker: ++ self._fallback_checker.destroy() ++ self._fallback_checker = None + + def _wait_state_changed_callback(self, act_con, state, reason, action): + if self._ctx.is_cancelled(): +@@ -96,13 +116,13 @@ class ActiveConnection: + logging.debug( + "Connection deactivation succeeded on %s", self.devname, + ) ++ self._clean_up() + self._ctx.finish_async(action) + + def _deactivate_connection_callback(self, src_object, result, user_data): +- handler_id, action = user_data ++ action = user_data + if self._ctx.is_cancelled(): +- if self._act_con: +- self._act_con.handler_disconnect(handler_id) ++ self._clean_up() + return + + try: +@@ -116,16 +136,20 @@ class ActiveConnection: + "Connection is not active on {}, no need to " + "deactivate".format(self.devname) + ) ++ elif e.matches(Gio.io_error_quark(), Gio.IOErrorEnum.TIMED_OUT): ++ logging.debug( ++ f"{action} timeout, using fallback method to wait profile " ++ "deactivation" ++ ) ++ return + else: +- if self._act_con: +- self._act_con.handler_disconnect(handler_id) ++ self._clean_up() + 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) ++ self._clean_up() + self._ctx.fail( + NmstateLibnmError( + f"BUG: Unexpected error when activating {self.devname} " +@@ -135,8 +159,7 @@ class ActiveConnection: + return + + if not success: +- if self._act_con: +- self._act_con.handler_disconnect(handler_id) ++ self._clean_up() + self._ctx.fail( + NmstateLibnmError( + f"{action} failed: error='None returned from " +@@ -144,6 +167,22 @@ class ActiveConnection: + ) + ) + ++ def _deactivation_fallback_checker_callback(self, action): ++ if self.devname: ++ self._nmdev = self._ctx.get_nm_dev(self.devname) ++ if self._nmdev: ++ self._act_con = self._nmdev.get_active_connection() ++ if ( ++ self._act_con ++ and self._act_con.props.state ++ != NM.ActiveConnectionState.DEACTIVATED ++ ): ++ return GLib.SOURCE_CONTINUE ++ ++ self._clean_up() ++ self._ctx.finish_async(action) ++ return GLib.SOURCE_REMOVE ++ + @property + def nm_active_connection(self): + return self._act_con +diff --git a/libnmstate/nm/connection.py b/libnmstate/nm/connection.py +index 374a379..af7296f 100644 +--- a/libnmstate/nm/connection.py ++++ b/libnmstate/nm/connection.py +@@ -144,6 +144,13 @@ class ConnectionProfile: + self._delete_connection_callback, + user_data, + ) ++ self._fallback_checker = GLib.timeout_source_new( ++ FALLBACK_CHECKER_INTERNAL * 1000 ++ ) ++ self._fallback_checker.set_callback( ++ self._delete_fallback_checker_callback, action ++ ) ++ self._fallback_checker.attach(self._ctx.context) + + def activate(self): + if self.con_id: +@@ -504,11 +511,24 @@ class ConnectionProfile: + action = user_data + try: + success = src_object.delete_finish(result) ++ except GLib.Error as e: ++ if e.matches(Gio.io_error_quark(), Gio.IOErrorEnum.TIMED_OUT): ++ logging.debug( ++ f"{action} timeout, using fallback method to wait profile " ++ "deletion" ++ ) ++ return ++ else: ++ self._ctx.fail( ++ NmstateLibnmError(f"{action} failed with error: {e}") ++ ) ++ return + except Exception as e: + self._ctx.fail(NmstateLibnmError(f"{action} failed: error={e}")) + return + + if success: ++ self._fallback_checker_cleanup() + self._ctx.finish_async(action) + else: + self._ctx.fail( +@@ -518,6 +538,16 @@ class ConnectionProfile: + ) + ) + ++ def _delete_fallback_checker_callback(self, action): ++ if self.profile: ++ for nm_profile in self._ctx.client.get_connections(): ++ if nm_profile.get_uuid() == self.profile.get_uuid(): ++ return GLib.SOURCE_CONTINUE ++ ++ self._fallback_checker_cleanup() ++ self._ctx.finish_async(action) ++ return GLib.SOURCE_REMOVE ++ + def _reset_profile(self): + self._con_profile = None + +-- +2.27.0 + diff --git a/SOURCES/BZ_1916073_retry_on_failure_when_activate.patch b/SOURCES/BZ_1916073_retry_on_failure_when_activate.patch new file mode 100644 index 0000000..da7f372 --- /dev/null +++ b/SOURCES/BZ_1916073_retry_on_failure_when_activate.patch @@ -0,0 +1,58 @@ +From 013860d2576a34a277178e6afba0935498dc4f72 Mon Sep 17 00:00:00 2001 +From: Fernando Fernandez Mancera +Date: Wed, 3 Feb 2021 11:59:05 +0100 +Subject: [PATCH] connection: retry on profile activation if libnm error + happened + +When activating a profile if NetworkManager fails during the activation, +Nmstate should retry it once. + +Signed-off-by: Fernando Fernandez Mancera +--- + libnmstate/nm/connection.py | 19 +++++++++++++++++-- + 1 file changed, 17 insertions(+), 2 deletions(-) + +diff --git a/libnmstate/nm/connection.py b/libnmstate/nm/connection.py +index 5b9a3aee2..45cb69019 100644 +--- a/libnmstate/nm/connection.py ++++ b/libnmstate/nm/connection.py +@@ -180,7 +180,8 @@ def activate(self): + "BUG: Cannot activate a profile with empty profile id and " + "empty NM.Device" + ) +- user_data = action ++ retry = True ++ user_data = action, retry + self._ctx.register_async(action) + self._ctx.client.activate_connection_async( + self.profile, +@@ -267,7 +268,7 @@ def _active_connection_callback(self, src_object, result, user_data): + if self._ctx.is_cancelled(): + self._activation_clean_up() + return +- action = user_data ++ action, retry = user_data + + try: + nm_act_con = src_object.activate_connection_finish(result) +@@ -279,6 +280,20 @@ def _active_connection_callback(self, src_object, result, user_data): + ) + return + else: ++ if retry: ++ retry = False ++ user_data = action, retry ++ specific_object = None ++ logging.debug(f"Action {action} failed, trying again.") ++ self._ctx.client.activate_connection_async( ++ self.profile, ++ self.nmdevice, ++ specific_object, ++ self._ctx.cancellable, ++ self._active_connection_callback, ++ user_data, ++ ) ++ return + self._ctx.fail( + NmstateLibnmError(f"{action} failed: error={e}") + ) diff --git a/SOURCES/BZ_1918712_use_uuid_for_vlan_vxlan_parent.patch b/SOURCES/BZ_1918712_use_uuid_for_vlan_vxlan_parent.patch new file mode 100644 index 0000000..3c42770 --- /dev/null +++ b/SOURCES/BZ_1918712_use_uuid_for_vlan_vxlan_parent.patch @@ -0,0 +1,214 @@ +From a85b3dddf82f9e71774229740fbae6ea843d86d6 Mon Sep 17 00:00:00 2001 +From: Gris Ge +Date: Mon, 18 Jan 2021 15:55:43 +0800 +Subject: [PATCH 1/2] ifaces: Don't validate undesired interface for overbook + +There is no need to validate overbooked interface for undesired +controller interface. + +Unit test case included. + +Signed-off-by: Gris Ge +--- + libnmstate/ifaces/ifaces.py | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/libnmstate/ifaces/ifaces.py b/libnmstate/ifaces/ifaces.py +index 7723f43..ee75125 100644 +--- a/libnmstate/ifaces/ifaces.py ++++ b/libnmstate/ifaces/ifaces.py +@@ -437,6 +437,8 @@ class Ifaces: + """ + slave_master_map = {} + for iface in self._ifaces.values(): ++ if not (iface.is_changed or iface.is_desired) or not iface.is_up: ++ continue + for slave_name in iface.slaves: + cur_master = slave_master_map.get(slave_name) + if cur_master: +-- +2.27.0 + + +From 644d8e5f5072caaba7151e66f211eceb02ae79c3 Mon Sep 17 00:00:00 2001 +From: Gris Ge +Date: Thu, 21 Jan 2021 20:43:34 +0800 +Subject: [PATCH 2/2] nm vlan/vxlan: Use uuid for VLAN/VxLAN parent + +When parent of VLAN/VxLAN is holding the same name of OVS bridge or OVS +port, NetworkManager will fail with error failed to find interface index +of that parent. + +The root cause is NetworkManager try to use user space interface as +VLAN/VxLAN parent. + +To workaround that, use profile UUID for `NM.SettingVlan.props.parent` +and `NM.SettingVxlan.props.parent`. + +Integration test case included. + +Signed-off-by: Gris Ge +--- + libnmstate/nm/applier.py | 98 +++++++++++++++++++++++++++++++++++-- + libnmstate/nm/connection.py | 7 +++ + 2 files changed, 100 insertions(+), 5 deletions(-) + +diff --git a/libnmstate/nm/applier.py b/libnmstate/nm/applier.py +index 26a057f..8e38df5 100644 +--- a/libnmstate/nm/applier.py ++++ b/libnmstate/nm/applier.py +@@ -66,8 +66,6 @@ MASTER_IFACE_TYPES = ( + + + def apply_changes(context, net_state, save_to_disk): +- con_profiles = [] +- + if ( + not save_to_disk + and _has_ovs_interface_desired_or_changed(net_state) +@@ -87,6 +85,10 @@ def apply_changes(context, net_state, save_to_disk): + _create_proxy_ifaces_desired_state(ifaces_desired_state) + ) + ++ # A list of tuple holding both current ConnectionProfile and new/updated ++ # ConnectionProfile. ++ pending_con_profiles = [] ++ + for iface_desired_state in filter( + lambda s: s.get(Interface.STATE) != InterfaceState.ABSENT, + ifaces_desired_state, +@@ -131,7 +133,7 @@ def apply_changes(context, net_state, save_to_disk): + # anything besides state:up and not been marked as changed. + # We don't need to do this once we support querying on-disk + # configure +- con_profiles.append(cur_con_profile) ++ pending_con_profiles.append((cur_con_profile, None)) + continue + new_con_profile = _build_connection_profile( + context, +@@ -143,12 +145,25 @@ def apply_changes(context, net_state, save_to_disk): + set_conn = new_con_profile.profile.get_setting_connection() + set_conn.props.interface_name = iface_desired_state[Interface.NAME] + if cur_con_profile and cur_con_profile.profile: +- cur_con_profile.update(new_con_profile, save_to_disk) +- con_profiles.append(new_con_profile) ++ pending_con_profiles.append((cur_con_profile, new_con_profile)) + else: + # Missing connection, attempting to create a new one. ++ pending_con_profiles.append((None, new_con_profile)) ++ ++ pending_con_profiles = _use_uuid_for_parent( ++ context, pending_con_profiles, save_to_disk ++ ) ++ ++ con_profiles = [] ++ for cur_con_profile, new_con_profile in pending_con_profiles: ++ if cur_con_profile and new_con_profile: ++ cur_con_profile.update(new_con_profile, save_to_disk) ++ con_profiles.append(new_con_profile) ++ elif cur_con_profile is None and new_con_profile: + new_con_profile.add(save_to_disk) + con_profiles.append(new_con_profile) ++ elif cur_con_profile: ++ con_profiles.append(cur_con_profile) + context.wait_all_finish() + + _set_ifaces_admin_state(context, ifaces_desired_state, con_profiles) +@@ -655,3 +670,76 @@ def _mark_nm_external_subordinate_changed(context, net_state): + subordinate_iface = net_state.ifaces.get(subordinate) + if subordinate_iface: + subordinate_iface.mark_as_changed() ++ ++ ++def _use_uuid_for_parent(context, pending_con_profiles, save_to_disk): ++ """ ++ When parent of VLAN/VxLAN is holding the same name with ++ OVS bridge or OVS port, we should use UUID instead of interface name ++ """ ++ new_pending_con_profiles = [] ++ kernel_iface_name_to_uuid = {} ++ for cur_nm_profile in context.client.get_connections(): ++ connection_type = cur_nm_profile.get_connection_type() ++ if connection_type not in ( ++ NM.SETTING_OVS_BRIDGE_SETTING_NAME, ++ NM.SETTING_OVS_PORT_SETTING_NAME, ++ ): ++ kernel_iface_name_to_uuid[ ++ cur_nm_profile.get_interface_name() ++ ] = cur_nm_profile.get_uuid() ++ # Override existing kernel_iface_name_to_uuid with pending changes. ++ for cur_con_profile, new_con_profile in pending_con_profiles: ++ if new_con_profile and new_con_profile.profile: ++ uuid = new_con_profile.profile.get_uuid() ++ connection_type = new_con_profile.profile.get_connection_type() ++ iface_name = new_con_profile.profile.get_interface_name() ++ elif cur_con_profile and cur_con_profile.profile: ++ uuid = cur_con_profile.profile.get_uuid() ++ connection_type = cur_con_profile.profile.get_connection_type() ++ iface_name = cur_con_profile.profile.get_interface_name() ++ else: ++ continue ++ ++ if connection_type not in ( ++ NM.SETTING_OVS_BRIDGE_SETTING_NAME, ++ NM.SETTING_OVS_PORT_SETTING_NAME, ++ ): ++ kernel_iface_name_to_uuid[iface_name] = uuid ++ ++ for cur_con_profile, new_con_profile in pending_con_profiles: ++ new_pending_con_profiles.append((cur_con_profile, new_con_profile)) ++ if not new_con_profile: ++ continue ++ nm_profile = new_con_profile.profile ++ if not nm_profile: ++ continue ++ connection_type = nm_profile.get_connection_type() ++ nm_setting = None ++ if connection_type == NM.SETTING_VLAN_SETTING_NAME: ++ nm_setting = nm_profile.get_setting_vlan() ++ elif connection_type == NM.SETTING_VXLAN_SETTING_NAME: ++ nm_setting = nm_profile.get_setting_vxlan() ++ else: ++ continue ++ if not nm_setting: ++ continue ++ parent_iface_name = nm_setting.props.parent ++ parent_uuid = kernel_iface_name_to_uuid.get(parent_iface_name) ++ if parent_uuid: ++ updated_con_profile = connection.ConnectionProfile(context) ++ new_nm_settings = [] ++ for cur_nm_setting in nm_profile.get_settings(): ++ new_nm_setting = cur_nm_setting.duplicate() ++ if new_nm_setting.get_name() in ( ++ NM.SETTING_VLAN_SETTING_NAME, ++ NM.SETTING_VXLAN_SETTING_NAME, ++ ): ++ new_nm_setting.props.parent = parent_uuid ++ new_nm_settings.append(new_nm_setting) ++ updated_con_profile.create(new_nm_settings) ++ new_pending_con_profiles.pop() ++ new_pending_con_profiles.append( ++ (cur_con_profile, updated_con_profile) ++ ) ++ return new_pending_con_profiles +diff --git a/libnmstate/nm/connection.py b/libnmstate/nm/connection.py +index af7296f..5b9a3ae 100644 +--- a/libnmstate/nm/connection.py ++++ b/libnmstate/nm/connection.py +@@ -63,6 +63,13 @@ class ConnectionProfile: + if self.con_id: + self.profile = self._ctx.client.get_connection_by_id(self.con_id) + ++ def import_by_uuid(self, uuid): ++ for nm_profile in self._ctx.client.get_connections(): ++ if nm_profile.get_uuid() == uuid: ++ self.profile = nm_profile ++ return ++ logging.debug(f"Failed to find {uuid} profile") ++ + def update(self, con_profile, save_to_disk=True): + flags = NM.SettingsUpdate2Flags.BLOCK_AUTOCONNECT + if save_to_disk: +-- +2.27.0 + diff --git a/SOURCES/nmstate-0.3.4.tar.gz.asc b/SOURCES/nmstate-0.3.4.tar.gz.asc new file mode 100644 index 0000000..ac2c220 --- /dev/null +++ b/SOURCES/nmstate-0.3.4.tar.gz.asc @@ -0,0 +1,16 @@ +-----BEGIN PGP SIGNATURE----- + +iQIzBAABCAAdFiEEfUQ+BAINyWGvqJXerIciWuEsPqMFAl8a02UACgkQrIciWuEs +PqMSmg//T/2C0mP+zb1pnIfcPZvc8dlNgnvtTIN8EK23b6UvxrFYKuPmqRw+Dsir +N/9enPTUgQKAOtZs7BdtZlCmsaU2bWAF11UgRY+gcSkSVeG0j/kxHLG3sE4RbFiD +QPKJrqRE6m+ybTOiJ0oVXkR7f2i/AVmZE3+eZHn1TzHQoKZA8MJyExYWmk7wMkfG +KzE7jvZQ1M4Q6aZKxo4wjAkhAhFLio9HhWnl8z1bLpWWFVHqqMJ04QniDsepczCm +ISr6grG2TW6bS93lRCdDkS4yGCAYrwZ/5eyN5eOTd/et7FqG/ExFHdVaaro5I1W5 +cbOYyZ1cI/avA9vCWCkC7DUJOh3i5BzzhHaxS65qqpM7fiLIrHZhaLQaLByMO48d +zo1wDEwIyNvuP4bIVwRycuDhtcLnPs5QwVbfW4HKkn4ULO+inr3lJk8V3ZZ3Ghmz +qKCrpteJTK/yJl9N2MrXxPvYYe388m4A6GGSVml4mYCd2ZMBrQ8k8fSPdlodmzVJ +J5gpeJRqm9sdrbv7tmuDfNOjAu0o9MP0/OSA0Lb5ho3pyylGnhsZ7Zkwbn2U/1b2 +zgPmVWJqRhjq01VgderCOerxow7OvetNicrNg/9e7+eFAHV7VUowdKf3vCyk0+nl +WGeZLxi21w2Q2RH1ThPo6uMxL3pIp7dsbVrqM7oLpUoZzGtFnq8= +=4Hsb +-----END PGP SIGNATURE----- diff --git a/SPECS/nmstate.spec b/SPECS/nmstate.spec new file mode 100644 index 0000000..c7f3b10 --- /dev/null +++ b/SPECS/nmstate.spec @@ -0,0 +1,326 @@ +%?python_enable_dependency_generator +%define srcname nmstate +%define libname libnmstate + +Name: nmstate +Version: 0.3.4 +Release: 25%{?dist} +Summary: Declarative network manager API +License: LGPLv2+ +URL: https://github.com/%{srcname}/%{srcname} +Source0: %{url}/releases/download/v%{version}/%{srcname}-%{version}.tar.gz +Source1: %{url}/releases/download/v%{version}/%{srcname}-%{version}.tar.gz.asc +Source2: https://www.nmstate.io/nmstate.gpg +Patch1: BZ_1858762-hide_ovs_patch_port_mtu.patch +Patch2: BZ_1861263-handle-external-managed-interface.patch +Patch3: BZ_1861668_ignore_unknown_iface.patch +Patch4: BZ_1862025-remove_existing_profiles.patch +Patch5: BZ_1858758-fix_ovs_bond.patch +Patch6: BZ_1859844-fix_converting_memory_only.patch +Patch7: BZ_1866269-preserve_nm_uuid_in_ovsdb.patch +Patch8: BZ_1869345_ovsdb_remove_all_ports.patch +Patch9: BZ_1887349-Allow-duplicate-iface-name-in-ovs.patch +Patch10: BZ_1890497-nm-bond-Ignore-ad_actor_system-00-00-00-00-00-00.patch +Patch11: BZ_1890497-nm.ipv6-call-clear_routing_rules-when-creating-the-s.patch +Patch12: BZ_1901571_do_not_check_ovs_daemon_when_showing.patch +Patch13: BZ_1904889-do-not-remove-unmanaged-ovs-bridge.patch +Patch14: BZ_1910193-support-multiple-gateways.patch +Patch15: BZ_1908724-sriov-use-verification-retry-to-wait-VF-been-created.patch +Patch16: BZ_1916073_better-handling-for-timeout.patch +Patch17: BZ_1918712_use_uuid_for_vlan_vxlan_parent.patch +Patch18: BZ_1916073_retry_on_failure_when_activate.patch +BuildArch: noarch +BuildRequires: python3-devel +BuildRequires: python3-setuptools +BuildRequires: gnupg2 +Requires: python3-setuptools +Requires: python3-%{libname} = %{?epoch:%{epoch}:}%{version}-%{release} + +%description +Nmstate is a library with an accompanying command line tool that manages host +networking settings in a declarative manner and aimed to satisfy enterprise +needs to manage host networking through a northbound declarative API and multi +provider support on the southbound. + + +%package -n python3-%{libname} +Summary: nmstate Python 3 API library +Requires: NetworkManager-libnm >= 1:1.26.0 +# Use Recommends for NetworkManager because only access to NM DBus is required, +# but NM could be running on a different host +Recommends: NetworkManager +# Avoid automatically generated profiles +Recommends: NetworkManager-config-server +# Use Suggests for NetworkManager-ovs and NetworkManager-team since it is only +# required for OVS and team support +Suggests: NetworkManager-ovs +Suggests: NetworkManager-team + +%package -n nmstate-plugin-ovsdb +Summary: nmstate plugin for OVS database manipulation +Requires: python3-%{libname} = %{?epoch:%{epoch}:}%{version}-%{release} +# The python-openvswitch rpm pacakge is not in the same repo with nmstate, +# hence state it as Recommends, no requires. +Recommends: python3dist(ovs) + +%description -n python3-%{libname} +This package contains the Python 3 library for nmstate. + +%description -n nmstate-plugin-ovsdb +This package contains the nmstate plugin for OVS database manipulation. + +%prep +gpg2 --import --import-options import-export,import-minimal %{SOURCE2} > ./gpgkey-mantainers.gpg +gpgv2 --keyring ./gpgkey-mantainers.gpg %{SOURCE1} %{SOURCE0} +%autosetup -p1 + +%build +%py3_build + +%install +%py3_install + +%files +%doc README.md +%doc examples/ +%{_mandir}/man8/nmstatectl.8* +%{python3_sitelib}/nmstatectl +%{_bindir}/nmstatectl + +%files -n python3-%{libname} +%license LICENSE +%{python3_sitelib}/%{libname} +%{python3_sitelib}/%{srcname}-*.egg-info/ +%exclude %{python3_sitelib}/%{libname}/plugins/nmstate_plugin_* +%exclude %{python3_sitelib}/%{libname}/plugins/__pycache__/nmstate_plugin_* + +%files -n nmstate-plugin-ovsdb +%{python3_sitelib}/%{libname}/plugins/nmstate_plugin_ovsdb* +%{python3_sitelib}/%{libname}/plugins/__pycache__/nmstate_plugin_ovsdb* + +%changelog +* Fri Feb 05 2021 Gris Ge - 0.3.4-25 +- Remove patch for fixing the autoconnect on existing profile. RHBZ#1918712 + +* Thu Feb 04 2021 Gris Ge - 0.3.4-24 +- New patch fixing activation failure with 1000 interfaces. RHBZ#1916073 + +* Wed Feb 03 2021 Gris Ge - 0.3.4-23 +- Enforcing autoconnect on existing profile. RHBZ#1918712 + +* Fri Jan 22 2021 Gris Ge - 0.3.4-22 +- Fix creating VLAN/VxLAN over interface also used for OVS. RHBZ#1918712 + +* Sun Jan 17 2021 Gris Ge - 0.3.4-21 +- Additional patch for profile deactivation and deletion. RHBZ#1916073 + +* Thu Jan 14 2021 Fernando Fernandez Mancera - 0.3.4-20 +- Better handling for timeout on activation. RHBZ#1916073 + +* Mon Jan 04 2021 Fernando Fernandez Mancera - 0.3.4-19 +- Use verification retry to wait SR-IOV VF been created. RHBZ#1908724 + +* Wed Dec 23 2020 Gris Ge - 0.3.4-18 +- Support multiple gateways. RHBZ#1910193 + +* Mon Dec 07 2020 Gris Ge - 0.3.4-17 +- Rebuild to retrigger the CI gating. RHBZ#1904889 + +* Mon Dec 07 2020 Gris Ge - 0.3.4-16 +- Don't remove unmanaged OVS interface. RHBZ#1904889 + +* Thu Nov 26 2020 Gris Ge - 0.3.4-15 +- Fix `libnmstate.show()` in container with OVS bridge. RHBZ#1901571 + +* Wed Nov 04 2020 Fernando Fernandez Mancera - 0.3.4-14 +- Ignore bond zero ad_actor_system. RHBZ#1890497 +- Fix clear IPv6 routing rules. + +* Wed Oct 14 2020 Gris Ge - 0.3.4-13 +- Allowing duplicate interface name of ovs. RHBZ#1887349 + +* Tue Aug 18 2020 Gris Ge - 0.3.4-12 +- New patch: OVSDB: Allowing remove all OVS ports. RHBZ#1869345 + +* Tue Aug 18 2020 Gris Ge - 0.3.4-11 +- OVSDB: Allowing remove all OVS ports. RHBZ#1869345 + +* Thu Aug 06 2020 Gris Ge - 0.3.4-10 +- OVSDB: Preserv old external_ids. RHBZ#1866269 + +* Tue Aug 04 2020 Gris Ge - 0.3.4-9 +- Fix converting memory only profile to persistent. RHBZ#1859844 + +* Mon Aug 03 2020 Gris Ge - 0.3.4-8 +- Fix failure when adding ovs bond to existing bridge. RHBZ#1858758 + +* Thu Jul 30 2020 Gris Ge - 0.3.4-7 +- Remove existing inactivate NM profiles. RHBZ#1862025 + +* Wed Jul 29 2020 Gris Ge - 0.3.4-6 +- New build to retrigger the CI gating. + +* Wed Jul 29 2020 Gris Ge - 0.3.4-5 +- Use new patch. RHBZ#1861668 + +* Wed Jul 29 2020 Gris Ge - 0.3.4-4 +- Ignore unknown interface. RHBZ#1861668 + +* Tue Jul 28 2020 Gris Ge - 0.3.4-3 +- Add support NetworkManaged exteranl managed interface. RHBZ#1861263 + +* Tue Jul 28 2020 Gris Ge - 0.3.4-2 +- Hide MTU for OVS patch port. RHBZ#1858762 + +* Sat Jul 25 2020 Fernando Fernandez Mancera - 0.3.4-1 +- Upgrade to 0.3.4 + +* Fri Jul 24 2020 Gris Ge - 0.3.3-3 +- Allowing child been marked absent. RHBZ#1859148 + +* Mon Jul 06 2020 Fernando Fernandez Mancera - 0.3.3-2 +- Fix bug 1850698 + +* Thu Jul 02 2020 Fernando Fernandez Mancera - 0.3.3-1 +- Upgrade to 0.3.3 + +* Mon Jun 29 2020 Gris Ge - 0.3.2-6 +- Improve performance by remove unneeded calls. RHBZ#1820009 + +* Mon Jun 29 2020 Gris Ge - 0.3.2-5 +- Sort the pretty state with priority. RHBZ#1806474 + +* Mon Jun 29 2020 Gris Ge - 0.3.2-4 +- Canonicalize IP address. RHBZ#1816612 + +* Mon Jun 29 2020 Gris Ge - 0.3.2-3 +- Improve VLAN MTU error message. RHBZ#1788763 + +* Mon Jun 29 2020 Gris Ge - 0.3.2-2 +- Fix bug 1850698 + +* Mon Jun 15 2020 Fernando Fernandez Mancera - 0.3.2-1 +- Upgrade to 0.3.2 +- Sync. up with upstream spec file + +* Thu Jun 11 2020 Gris Ge - 0.3.1-1 +- Upgrade to 0.3.1 + +* Wed May 13 2020 Fernando Fernandez Mancera - 0.3.0-1 +- Upgrade to 0.3.0 +- Sync. up with upstream spec file. +- Update signature verification. + +* Tue Mar 31 2020 Fernando Fernandez Mancera - 0.2.9-1 +- Upgrade to 0.2.9 + +* Wed Mar 25 2020 Gris Ge - 0.2.6-6 +- Support 3+ DNS name server(IPv4 only or IPv6 only). RHBZ #1816043 + +* Fri Mar 20 2020 Gris Ge - 0.2.6-5 +- Support static DNS with DHCP. RHBZ #1815112 + +* Thu Mar 12 2020 Fernando Fernandez Mancera - 0.2.6-4.8 +- Fix bond mac and options regression. RHBZ #1809330 + +* Mon Mar 09 2020 Gris Ge - 0.2.6-3.8 +- Fix change bond mode. RHBZ #1809330 + +* Mon Mar 02 2020 Fernando Fernandez Mancera - 0.2.6-2.7 +- Fix cmd stuck when trying to create ovs-bond. RHBZ #1806249. + +* Tue Feb 25 2020 Gris Ge - 0.2.6-1 +- Upgrade to 0.2.6 + +* Thu Feb 20 2020 Gris Ge - 0.2.5-1 +- Upgrade to 0.2.5 + +* Thu Feb 13 2020 Gris Ge - 0.2.4-2 +- Fix failure when editing existing OVS interface. RHBZ #1786935 + +* Thu Feb 13 2020 Gris Ge - 0.2.4-1 +- Upgrade to 0.2.4 + +* Wed Feb 05 2020 Fernando Fernandez Mancera - 0.2.3-1 +- Upgrade to 0.2.3 + +* Tue Feb 04 2020 Fernando Fernandez Mancera - 0.2.2-3 +- Fix the incorrect source + +* Tue Feb 04 2020 Fernando Fernandez Mancera - 0.2.2-2 +- Upgrade to 0.2.2 + +* Wed Jan 22 2020 Gris Ge - 0.2.0-3.1 +- Fix the memeory leak of NM.Client. RHBZ #1784707 + +* Mon Dec 02 2019 Gris Ge - 0.2.0-2 +- Fix the incorrect source tarbal. + +* Mon Dec 02 2019 Gris Ge - 0.2.0-1 +- Upgrade to nmstate 0.2.0 + +* Mon Dec 02 2019 Gris Ge - 0.1.1-4 +- Fix the problem found by CI gating. + +* Mon Dec 02 2019 Gris Ge - 0.1.1-3 +- Bump dist number as RHEL 8.1.1 took 0.1.1-2. + +* Mon Dec 02 2019 Gris Ge - 0.1.1-2 +- Upgrade to nmstate 0.1.1. + +* Tue Sep 10 2019 Gris Ge - 0.0.8-15 +- Detach slaves without deleting them: RHBZ #1749632 + +* Fri Sep 06 2019 Gris Ge - 0.0.8-14 +- Preserve (dynamic) IPv6 address base on MAC address: RHBZ #1748825 + +* Fri Sep 06 2019 Gris Ge - 0.0.8-13 +- Prioritize master interfaces activaction: RHBZ #1749314 + +* Mon Sep 02 2019 Gris Ge - 0.0.8-12 +- Fix slave activatoin race: RHBZ #1741440 + +* Mon Sep 02 2019 Gris Ge - 0.0.8-10 +- Fix RHBZ #1740125 + +* Wed Aug 14 2019 Gris Ge - 0.0.8-9 +- Fix RHBZ #1741049 + +* Wed Aug 14 2019 Gris Ge - 0.0.8-8 +- Fix RHBZ #1740584 + +* Tue Aug 13 2019 Gris Ge - 0.0.8-7 +- Fix RHBZ #1740554 + +* Tue Aug 13 2019 Gris Ge - 0.0.8-6 +- Bump release tag as CNV took the -5. + +* Tue Aug 13 2019 Gris Ge - 0.0.8-5 +- Bump release tag as CNV took the -4. + +* Tue Aug 13 2019 Gris Ge - 0.0.8-4 +- Disable reapply on ipv6 to fix bug 1738101. + +* Fri Jul 26 2019 Gris Ge - 0.0.8-3 +- Fix the license to meet Fedora/RHEL guideline. + +* Fri Jul 26 2019 Gris Ge - 0.0.8-2 +- Relicense to LGPL2.1+. + +* Fri Jul 26 2019 Gris Ge - 0.0.8-1 +- Upgrade to 0.0.8. + +* Fri Jun 14 2019 Gris Ge - 0.0.7-1 +- Upgrade to 0.0.7. + +* Mon Apr 22 2019 Gris Ge - 0.0.5-3 +- Add missing runtime dependency. + +* Thu Mar 21 2019 Gris Ge - 0.0.5-2 +- Rebuild to enable CI testing. + +* Mon Mar 18 2019 Gris Ge - 0.0.5-1 +- Initial release