Preserve other existing IP configurations when apply()

* With this fix `netapplier.apply()` will not discard all
   IP configurations(DNS, route, etc) but keep existing one
   except IP addresses.

 * Unit test case and integration test case included.

Signed-off-by: Gris Ge <fge@redhat.com>
This commit is contained in:
Gris Ge 2018-11-27 20:37:59 +08:00 committed by Edward Haas
parent 230d53f282
commit 09c50b5693
7 changed files with 195 additions and 21 deletions

View File

@ -226,8 +226,8 @@ def _build_connection_profile(iface_desired_state, base_con_profile=None):
iface_desired_state['name'])
settings = [
ipv4.create_setting(iface_desired_state.get('ipv4')),
ipv6.create_setting(iface_desired_state.get('ipv6')),
ipv4.create_setting(iface_desired_state.get('ipv4'), base_con_profile),
ipv6.create_setting(iface_desired_state.get('ipv6'), base_con_profile),
]
if base_con_profile:
con_setting = connection.duplicate_settings(base_con_profile)

View File

@ -20,8 +20,17 @@ import socket
from . import nmclient
def create_setting(config):
setting_ipv4 = nmclient.NM.SettingIP4Config.new()
def create_setting(config, base_con_profile):
setting_ipv4 = None
if base_con_profile:
setting_ipv4 = base_con_profile.get_setting_ip4_config()
if setting_ipv4:
setting_ipv4 = setting_ipv4.duplicate()
setting_ipv4.clear_addresses()
if not setting_ipv4:
setting_ipv4 = nmclient.NM.SettingIP4Config.new()
if config and config.get('enabled') and config.get('address'):
setting_ipv4.props.method = (
nmclient.NM.SETTING_IP4_CONFIG_METHOD_MANUAL)

View File

@ -46,8 +46,16 @@ def get_info(active_connection):
return info
def create_setting(config):
setting_ip = nmclient.NM.SettingIP6Config.new()
def create_setting(config, base_con_profile):
setting_ip = None
if base_con_profile:
setting_ip = base_con_profile.get_setting_ip6_config()
if setting_ip:
setting_ip = setting_ip.duplicate()
setting_ip.clear_addresses()
if not setting_ip:
setting_ip = nmclient.NM.SettingIP6Config.new()
if config and config.get('enabled'):
for address in config.get('address', ()):
if iplib.is_ipv6_link_local_addr(address['ip'],

View File

@ -61,8 +61,8 @@ def _create_vlan(vlan_desired_state):
)
vlan_setting = nm.vlan.create_setting(vlan_desired_state,
base_con_profile=None)
ipv4_setting = nm.ipv4.create_setting({})
ipv6_setting = nm.ipv6.create_setting({})
ipv4_setting = nm.ipv4.create_setting({}, None)
ipv6_setting = nm.ipv6.create_setting({}, None)
with mainloop():
con_profile = nm.connection.create_profile(
(con_setting, vlan_setting, ipv4_setting, ipv6_setting))

View File

@ -0,0 +1,117 @@
#
# Copyright 2018 Red Hat, Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
from contextlib import contextmanager
from libnmstate import netapplier
from libnmstate.nm import nmclient
from .testlib import statelib
from .testlib import cmd as libcmd
from .testlib.statelib import INTERFACES
_IPV4_EXTRA_CONFIG = 'ipv4.dad-timeout'
_IPV4_EXTRA_VALUE = '1000'
_IPV6_EXTRA_CONFIG = 'ipv6.dhcp-hostname'
_IPV6_EXTRA_VALUE = 'libnmstate.example.com'
IPV4_ADDRESS1 = '192.0.2.251'
IPV6_ADDRESS1 = '2001:db8:1::1'
def test_reapply_preserve_ip_config(eth1_up):
netapplier.apply(
{
'interfaces': [
{
'name': 'eth1',
'type': 'ethernet',
'state': 'up',
'ipv4': {
'address': [
{
'ip': IPV4_ADDRESS1,
'prefix-length': 24
}
],
'enabled': True
},
'ipv6': {
'address': [
{
'ip': IPV6_ADDRESS1,
'prefix-length': 64
}
],
'enabled': True
},
'mtu': 1500
},
]
})
cur_state = statelib.show_only(('eth1',))
iface_name = cur_state[INTERFACES][0]['name']
uuid = _get_nm_profile_uuid(iface_name)
for key, value in ((_IPV4_EXTRA_CONFIG, _IPV4_EXTRA_VALUE),
(_IPV6_EXTRA_CONFIG, _IPV6_EXTRA_VALUE)):
with _extra_ip_config(uuid, key, value):
netapplier.apply(cur_state)
_assert_extra_ip_config(uuid, key, value)
def _get_nm_profile_uuid(iface_name):
nmcli = nmclient.client()
cur_dev = None
for dev in nmcli.get_all_devices():
if dev.get_iface() == iface_name:
cur_dev = dev
break
active_conn = cur_dev.get_active_connection()
return active_conn.get_uuid()
def _get_cur_extra_ip_config(uuid, key):
rc, output, _ = libcmd.exec_cmd(['nmcli', '--get-values', key,
'connection', 'show', uuid])
assert rc == 0
return output.split('\n')[0]
@contextmanager
def _extra_ip_config(uuid, key, value):
old_value = _get_cur_extra_ip_config(uuid, key)
_apply_extra_ip_config(uuid, key, value)
try:
yield
finally:
_apply_extra_ip_config(uuid, key, old_value)
def _apply_extra_ip_config(uuid, key, value):
assert libcmd.exec_cmd(['nmcli', 'connection', 'modify', uuid,
key, value])[0] == 0
def _assert_extra_ip_config(uuid, key, value):
"""
Check whether extra config is touched by libnmstate.
"""
cur_value = _get_cur_extra_ip_config(uuid, key)
assert cur_value == value

View File

@ -21,6 +21,8 @@ from lib.compat import mock
from libnmstate import nm
IPV4_ADDRESS1 = '192.0.2.251'
@pytest.fixture
def NM_mock():
@ -29,7 +31,7 @@ def NM_mock():
def test_create_setting_without_config(NM_mock):
ipv4_setting = nm.ipv4.create_setting(config=None)
ipv4_setting = nm.ipv4.create_setting(config=None, base_con_profile=None)
assert ipv4_setting == NM_mock.SettingIP4Config.new.return_value
assert (ipv4_setting.props.method ==
@ -37,7 +39,8 @@ def test_create_setting_without_config(NM_mock):
def test_create_setting_with_ipv4_disabled(NM_mock):
ipv4_setting = nm.ipv4.create_setting(config={'enabled': False})
ipv4_setting = nm.ipv4.create_setting(config={'enabled': False},
base_con_profile=None)
assert (ipv4_setting.props.method ==
NM_mock.SETTING_IP4_CONFIG_METHOD_DISABLED)
@ -48,7 +51,8 @@ def test_create_setting_without_addresses(NM_mock):
config={
'enabled': True,
'address': [],
}
},
base_con_profile=None
)
assert (ipv4_setting.props.method ==
@ -63,9 +67,7 @@ def test_create_setting_with_static_addresses(NM_mock):
{'ip': '10.10.20.1', 'prefix-length': 24},
],
}
ipv4_setting = nm.ipv4.create_setting(
config=config
)
ipv4_setting = nm.ipv4.create_setting(config=config, base_con_profile=None)
assert (ipv4_setting.props.method ==
NM_mock.SETTING_IP4_CONFIG_METHOD_MANUAL)
@ -117,3 +119,22 @@ def test_get_info_with_ipv4_config():
}
]
}
def test_create_setting_with_base_con_profile(NM_mock):
config = {
'enabled': True,
'address': [
{'ip': IPV4_ADDRESS1, 'prefix-length': 24},
],
}
base_con_profile_mock = mock.MagicMock()
config_mock = base_con_profile_mock.get_setting_ip4_config.return_value
config_dup_mock = config_mock.duplicate.return_value
nm.ipv4.create_setting(config=config,
base_con_profile=base_con_profile_mock)
base_con_profile_mock.get_setting_ip4_config.assert_called_once_with()
config_mock.duplicate.assert_called_once_with()
config_dup_mock.clear_addresses.assert_called_once_with()

View File

@ -36,7 +36,7 @@ def NM_mock():
def test_create_setting_without_config(NM_mock):
NM_mock.SettingIP6Config.new().props.addresses = []
ipv6_setting = nm.ipv6.create_setting(config=None)
ipv6_setting = nm.ipv6.create_setting(config=None, base_con_profile=None)
assert ipv6_setting == NM_mock.SettingIP6Config.new.return_value
assert (ipv6_setting.props.method ==
@ -46,7 +46,8 @@ def test_create_setting_without_config(NM_mock):
def test_create_setting_with_ipv6_disabled(NM_mock):
NM_mock.SettingIP6Config.new().props.addresses = []
ipv6_setting = nm.ipv6.create_setting(config={'enabled': False})
ipv6_setting = nm.ipv6.create_setting(config={'enabled': False},
base_con_profile=None)
assert (ipv6_setting.props.method ==
NM_mock.SETTING_IP6_CONFIG_METHOD_IGNORE)
@ -59,7 +60,8 @@ def test_create_setting_without_addresses(NM_mock):
config={
'enabled': True,
'address': [],
}
},
base_con_profile=None
)
assert (ipv6_setting.props.method ==
@ -74,9 +76,7 @@ def test_create_setting_with_static_addresses(NM_mock):
{'ip': 'fd12:3456:789a:2::1', 'prefix-length': 24},
],
}
ipv6_setting = nm.ipv6.create_setting(
config=config
)
ipv6_setting = nm.ipv6.create_setting(config=config, base_con_profile=None)
assert (ipv6_setting.props.method ==
NM_mock.SETTING_IP6_CONFIG_METHOD_MANUAL)
@ -138,7 +138,7 @@ def test_create_setting_with_link_local_addresses(NM_mock):
{'ip': IPV6_ADDRESS1, 'prefix-length': 64},
],
}
ipv6_setting = nm.ipv6.create_setting(config=config)
ipv6_setting = nm.ipv6.create_setting(config=config, base_con_profile=None)
assert (ipv6_setting.props.method ==
NM_mock.SETTING_IP6_CONFIG_METHOD_MANUAL)
@ -152,3 +152,22 @@ def test_create_setting_with_link_local_addresses(NM_mock):
NM_mock.SettingIP6Config.new.return_value.add_address.assert_has_calls(
[mock.call(NM_mock.IPAddress.new.return_value)]
)
def test_create_setting_with_base_con_profile(NM_mock):
config = {
'enabled': True,
'address': [
{'ip': IPV6_ADDRESS1, 'prefix-length': 24},
],
}
base_con_profile_mock = mock.MagicMock()
config_mock = base_con_profile_mock.get_setting_ip6_config.return_value
config_dup_mock = config_mock.duplicate.return_value
nm.ipv6.create_setting(config=config,
base_con_profile=base_con_profile_mock)
base_con_profile_mock.get_setting_ip6_config.assert_called_once_with()
config_mock.duplicate.assert_called_once_with()
config_dup_mock.clear_addresses.assert_called_once_with()