libnmstate: Implement non persistent state support

This patch is introducing non persisntent state support by the keyword argument
save_to_disk when using libnmstate API apply() and edit() operations. By
the default save_to_disk is True.

All the changes done to the interfaces when using non persistent state
will be wiped out after rebooting the system.

In addition, this patch is introducing nmstatectl --memory-only support.
By the default, it is disabled so the argument is needed in order to use
non persistent states.

Signed-off-by: Fernando Fernandez Mancera <ffmancera@riseup.net>
Signed-off-by: Gris Ge <fge@redhat.com>
This commit is contained in:
Fernando Fernandez Mancera 2020-06-16 02:47:41 +08:00 committed by Gris Ge
parent 25df7dabda
commit 6d67a06c46
17 changed files with 159 additions and 58 deletions

View File

@ -92,6 +92,9 @@ skip the desired network state verification.
create a checkpoint which later could be used for rollback or commit. The
checkpoint will be the last line of \fBnmstatectl\fR output, example:
\fI/org/freedesktop/NetworkManager/Checkpoint/1\fR.
.IP \fB--memory-only
all the changes done will be non persistent, they are going to be removed after
rebooting.
.IP \fB--timeout\fR=<\fITIMEOUT\fR>
the user must commit the changes within \fItimeout\fR, or they will be
automatically rolled back. Default: 60 seconds.

View File

@ -130,12 +130,13 @@ class BaseIface:
ROUTES_METADATA = "_routes"
ROUTE_RULES_METADATA = "_route_rules"
def __init__(self, info):
def __init__(self, info, save_to_disk=True):
self._origin_info = deepcopy(info)
self._info = deepcopy(info)
self._is_desired = False
self._is_changed = False
self._name = self._info[Interface.NAME]
self._save_to_disk = save_to_disk
@property
def can_have_ip_when_enslaved(self):
@ -219,6 +220,8 @@ class BaseIface:
ip_state = self.ip_state(family)
ip_state.remove_link_local_address()
self._info[family] = ip_state.to_dict()
if self.is_absent and not self._save_to_disk:
self._info[Interface.STATE] = InterfaceState.DOWN
def merge(self, other):
merge_dict(self._info, other._info)
@ -331,6 +334,8 @@ class BaseIface:
_remove_lldp_neighbors(state)
if Interface.STATE not in state:
state[Interface.STATE] = InterfaceState.UP
if self.is_absent and not self._save_to_disk:
state[Interface.STATE] = InterfaceState.DOWN
return state

View File

@ -35,8 +35,8 @@ class BondIface(BaseIface):
if self.slaves:
self.raw[Bond.CONFIG_SUBTREE][Bond.SLAVES].sort()
def __init__(self, info):
super().__init__(info)
def __init__(self, info, save_to_disk=True):
super().__init__(info, save_to_disk)
self._normalize_options_values()
self._fix_bond_option_arp_monitor()

View File

@ -52,19 +52,20 @@ class Ifaces:
also responsible to handle desire vs current state related tasks.
"""
def __init__(self, des_iface_infos, cur_iface_infos):
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 = {}
if cur_iface_infos:
for iface_info in cur_iface_infos:
cur_iface = _to_specific_iface_obj(iface_info)
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 des_iface_infos:
for iface_info in des_iface_infos:
iface = BaseIface(iface_info)
iface = BaseIface(iface_info, save_to_disk)
cur_iface = self._ifaces.get(iface.name)
if cur_iface and cur_iface.is_desired:
raise NmstateValueError(
@ -79,7 +80,7 @@ class Ifaces:
f"Interface {iface.name} has no type defined "
"neither in desire state nor current state"
)
iface = _to_specific_iface_obj(iface_info)
iface = _to_specific_iface_obj(iface_info, save_to_disk)
if (
iface.iface_type == InterfaceType.UNKNOWN
# Allowing deletion of down profiles
@ -258,7 +259,9 @@ class Ifaces:
def verify(self, cur_iface_infos):
cur_ifaces = Ifaces(
des_iface_infos=None, cur_iface_infos=cur_iface_infos
des_iface_infos=None,
cur_iface_infos=cur_iface_infos,
save_to_disk=self._save_to_disk,
)
for iface in self._ifaces.values():
if iface.is_desired:
@ -339,25 +342,25 @@ class Ifaces:
slave_master_map[slave_name] = iface.name
def _to_specific_iface_obj(info):
def _to_specific_iface_obj(info, save_to_disk):
iface_type = info.get(Interface.TYPE, InterfaceType.UNKNOWN)
if iface_type == InterfaceType.ETHERNET:
return EthernetIface(info)
return EthernetIface(info, save_to_disk)
elif iface_type == InterfaceType.BOND:
return BondIface(info)
return BondIface(info, save_to_disk)
elif iface_type == InterfaceType.DUMMY:
return DummyIface(info)
return DummyIface(info, save_to_disk)
elif iface_type == InterfaceType.LINUX_BRIDGE:
return LinuxBridgeIface(info)
return LinuxBridgeIface(info, save_to_disk)
elif iface_type == InterfaceType.OVS_BRIDGE:
return OvsBridgeIface(info)
return OvsBridgeIface(info, save_to_disk)
elif iface_type == InterfaceType.OVS_INTERFACE:
return OvsInternalIface(info)
return OvsInternalIface(info, save_to_disk)
elif iface_type == InterfaceType.VLAN:
return VlanIface(info)
return VlanIface(info, save_to_disk)
elif iface_type == InterfaceType.VXLAN:
return VxlanIface(info)
return VxlanIface(info, save_to_disk)
elif iface_type == InterfaceType.TEAM:
return TeamIface(info)
return TeamIface(info, save_to_disk)
else:
return BaseIface(info)
return BaseIface(info, save_to_disk)

View File

@ -168,8 +168,8 @@ def _is_ovs_lag_slave(lag_state, iface_name):
class OvsInternalIface(BaseIface):
def __init__(self, info):
super().__init__(info)
def __init__(self, info, save_to_disk=True):
super().__init__(info, save_to_disk)
self._parent = None
@property

View File

@ -31,11 +31,13 @@ from .route_rule import RouteRuleState
class NetState:
def __init__(self, desire_state, current_state=None):
def __init__(self, desire_state, current_state=None, save_to_disk=True):
if current_state is None:
current_state = {}
self._ifaces = Ifaces(
desire_state.get(Interface.KEY), current_state.get(Interface.KEY)
desire_state.get(Interface.KEY),
current_state.get(Interface.KEY),
save_to_disk,
)
self._route = RouteState(
self._ifaces,

View File

@ -38,7 +38,12 @@ VERIFY_RETRY_TIMEOUT = 5
def apply(
desired_state, *, verify_change=True, commit=True, rollback_timeout=60
desired_state,
*,
verify_change=True,
commit=True,
rollback_timeout=60,
save_to_disk=True,
):
"""
Apply the desired state
@ -61,9 +66,9 @@ def apply(
validator.validate_capabilities(
desired_state, plugins_capabilities(plugins)
)
net_state = NetState(desired_state, current_state)
net_state = NetState(desired_state, current_state, save_to_disk)
checkpoints = create_checkpoints(plugins, rollback_timeout)
_apply_ifaces_state(plugins, net_state, verify_change)
_apply_ifaces_state(plugins, net_state, verify_change, save_to_disk)
if commit:
destroy_checkpoints(plugins, checkpoints)
else:
@ -94,9 +99,9 @@ def rollback(*, checkpoint=None):
rollback_checkpoints(plugins, checkpoint)
def _apply_ifaces_state(plugins, net_state, verify_change):
def _apply_ifaces_state(plugins, net_state, verify_change, save_to_disk):
for plugin in plugins:
plugin.apply_changes(net_state)
plugin.apply_changes(net_state, save_to_disk)
verified = False
if verify_change:
for _ in range(VERIFY_RETRY_TIMEOUT):

View File

@ -62,7 +62,7 @@ MASTER_IFACE_TYPES = (
)
def apply_changes(context, net_state):
def apply_changes(context, net_state, save_to_disk):
con_profiles = []
_preapply_dns_fix(context, net_state)
@ -119,12 +119,12 @@ def apply_changes(context, net_state):
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)
cur_con_profile.update(new_con_profile, 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()
new_con_profile.add(save_to_disk)
con_profiles.append(new_con_profile)
context.wait_all_finish()
@ -139,7 +139,6 @@ def _set_ifaces_admin_state(context, ifaces_desired_state, con_profiles):
The `absent` state results in deactivating the device and deleting
the connection profile.
FIXME: The `down` state is currently handled in the same way.
For new virtual devices, the `up` state is handled by activating the
new connection profile. For existing devices, the device is activated,

View File

@ -58,9 +58,12 @@ class ConnectionProfile:
if self.con_id:
self.profile = self._ctx.client.get_connection_by_id(self.con_id)
def update(self, con_profile):
def update(self, con_profile, save_to_disk=True):
flags = NM.SettingsUpdate2Flags.BLOCK_AUTOCONNECT
flags |= NM.SettingsUpdate2Flags.TO_DISK
if save_to_disk:
flags |= NM.SettingsUpdate2Flags.TO_DISK
else:
flags |= NM.SettingsUpdate2Flags.IN_MEMORY
action = f"Update profile: {self.profile.get_id()}"
user_data = action
args = None

View File

@ -173,8 +173,8 @@ class NetworkManagerPlugin(NmstatePlugin):
def refresh_content(self):
self._ctx.refresh_content()
def apply_changes(self, net_state):
nm_applier.apply_changes(self.context, net_state)
def apply_changes(self, net_state, save_to_disk):
nm_applier.apply_changes(self.context, net_state, save_to_disk)
def _load_checkpoint(self, checkpoint_path):
if checkpoint_path:

View File

@ -58,7 +58,7 @@ class NmstatePlugin(metaclass=ABCMeta):
f"Plugin {self.name} BUG: get_interfaces() not implemented"
)
def apply_changes(self, net_state):
def apply_changes(self, net_state, save_to_disk):
pass
@property

View File

@ -28,6 +28,7 @@ from libnmstate.schema import Interface
from libnmstate.schema import OVSInterface
from libnmstate.schema import OVSBridge
from libnmstate.schema import OvsDB
from libnmstate.error import NmstateNotImplementedError
from libnmstate.error import NmstateTimeoutError
from libnmstate.error import NmstatePermissionError
from libnmstate.error import NmstateValueError
@ -159,7 +160,7 @@ class NmstateOvsdbPlugin(NmstatePlugin):
)
return ifaces
def apply_changes(self, net_state):
def apply_changes(self, net_state, save_to_disk):
self.refresh_content()
pending_changes = []
for iface in net_state.ifaces.values():
@ -174,10 +175,15 @@ class NmstateOvsdbPlugin(NmstatePlugin):
else:
continue
pending_changes.extend(_generate_db_change(table_name, iface))
if pending_changes and self._idl:
self._start_transaction()
self._db_write(pending_changes)
self._commit_transaction()
if pending_changes:
if not save_to_disk:
raise NmstateNotImplementedError(
"ovsdb plugin does not support memory only changes"
)
elif self._idl:
self._start_transaction()
self._db_write(pending_changes)
self._commit_transaction()
def _db_write(self, changes):
changes_index = {change.row_name: change for change in changes}

View File

@ -102,6 +102,13 @@ def setup_subcommand_edit(subparsers):
help="Do not verify that the state was completely set and disable "
"rollback to previous state.",
)
parser_edit.add_argument(
"--memory-only",
action="store_false",
dest="save_to_disk",
default=True,
help="Do not make the state persistent.",
)
def setup_subcommand_rollback(subparsers):
@ -143,6 +150,13 @@ def setup_subcommand_set(subparsers):
default=60,
help="Timeout in seconds before reverting uncommited changes.",
)
parser_set.add_argument(
"--memory-only",
action="store_false",
dest="save_to_disk",
default=True,
help="Do not make the state persistent.",
)
parser_set.set_defaults(func=apply)
@ -207,7 +221,9 @@ def edit(args):
print("Applying the following state: ")
print_state(new_state, use_yaml=args.yaml)
libnmstate.apply(new_state, verify_change=args.verify)
libnmstate.apply(
new_state, verify_change=args.verify, save_to_disk=args.save_to_disk
)
def rollback(args):
@ -233,17 +249,27 @@ def apply(args):
statedata = statefile.read()
return apply_state(
statedata, args.verify, args.commit, args.timeout
statedata,
args.verify,
args.commit,
args.timeout,
args.save_to_disk,
)
elif not sys.stdin.isatty():
statedata = sys.stdin.read()
return apply_state(statedata, args.verify, args.commit, args.timeout)
return apply_state(
statedata,
args.verify,
args.commit,
args.timeout,
args.save_to_disk,
)
else:
sys.stderr.write("ERROR: No state specified\n")
return 1
def apply_state(statedata, verify_change, commit, timeout):
def apply_state(statedata, verify_change, commit, timeout, save_to_disk):
use_yaml = False
# JSON dictionaries start with a curly brace
if statedata[0] == "{":
@ -258,6 +284,7 @@ def apply_state(statedata, verify_change, commit, timeout):
verify_change=verify_change,
commit=commit,
rollback_timeout=timeout,
save_to_disk=save_to_disk,
)
except NmstatePermissionError as e:
sys.stderr.write("ERROR: Missing permissions:{}\n".format(str(e)))

View File

@ -76,12 +76,18 @@ interfaces:
"""
def _mock_libnmstate_apply_func(
state,
verify_change=True,
commit=True,
rollback_timeout=60,
save_to_disk=True,
):
return None
@mock.patch("sys.argv", ["nmstatectl", "set", "mystate.json"])
@mock.patch.object(
nmstatectl.libnmstate,
"apply",
lambda state, verify_change=True, commit=True, rollback_timeout=60: None,
)
@mock.patch.object(nmstatectl.libnmstate, "apply", _mock_libnmstate_apply_func)
@mock.patch.object(
nmstatectl, "open", mock.mock_open(read_data="{}"), create=True
)

View File

@ -75,6 +75,8 @@ DUMMY_PROFILE_DIRECTORY = "/etc/NetworkManager/system-connections/"
ETH_PROFILE_DIRECTORY = "/etc/sysconfig/network-scripts/"
MEMORY_ONLY_PROFILE_DIRECTORY = "/run/NetworkManager/system-connections/"
MAC0 = "02:FF:FF:FF:FF:00"
@ -109,7 +111,7 @@ def test_rename_existing_interface_active_profile(eth1_up):
@contextmanager
def dummy_interface(ifname):
def dummy_interface(ifname, save_to_disk=True):
desired_state = {
Interface.KEY: [
{
@ -119,7 +121,7 @@ def dummy_interface(ifname):
}
]
}
libnmstate.apply(desired_state)
libnmstate.apply(desired_state, save_to_disk=save_to_disk)
try:
yield desired_state
finally:
@ -273,6 +275,47 @@ def test_state_absent_can_remove_down_profiles(dummy0_with_down_profile):
assertlib.assert_state_match(state_before_down)
def test_create_memory_only_profile_new_interface():
with dummy_interface(DUMMY0_IFNAME, save_to_disk=False):
assert not _profile_exists(
DUMMY_PROFILE_DIRECTORY + "dummy0.nmconnection"
)
assert _profile_exists(
MEMORY_ONLY_PROFILE_DIRECTORY + "dummy0.nmconnection"
)
assert not _profile_exists(
MEMORY_ONLY_PROFILE_DIRECTORY + "dummy0.nmconnection"
)
def test_create_memory_only_profile_edit_interface():
with dummy_interface(DUMMY0_IFNAME) as dstate:
assert not _profile_exists(
MEMORY_ONLY_PROFILE_DIRECTORY + "dummy0.nmconnection"
)
dstate[Interface.KEY][0][Interface.MTU] = 2000
libnmstate.apply(dstate, save_to_disk=False)
assert _profile_exists(DUMMY_PROFILE_DIRECTORY + "dummy0.nmconnection")
assert _profile_exists(
MEMORY_ONLY_PROFILE_DIRECTORY + "dummy0.nmconnection"
)
assert not _profile_exists(
MEMORY_ONLY_PROFILE_DIRECTORY + "dummy0.nmconnection"
)
def test_memory_only_profile_absent_interface():
with dummy_interface(DUMMY0_IFNAME) as dstate:
dstate[Interface.KEY][0][Interface.STATE] = InterfaceState.ABSENT
libnmstate.apply(dstate, save_to_disk=False)
assertlib.assert_absent(DUMMY0_IFNAME)
assert _profile_exists(DUMMY_PROFILE_DIRECTORY + "dummy0.nmconnection")
assertlib.assert_absent(DUMMY0_IFNAME)
def _nmcli_deactivate_connection(con_name):
return ["nmcli", "con", "down", con_name]

View File

@ -161,8 +161,7 @@ def test_set_vlan_iface_down(eth1_up):
}
)
current_state = statelib.show_only((VLAN_IFNAME,))
assert not current_state[Interface.KEY]
assertlib.assert_absent(VLAN_IFNAME)
def test_add_new_base_iface_with_vlan():

View File

@ -79,7 +79,7 @@ def test_iface_admin_state_change(
netapplier.apply(desired_config, verify_change=False)
plugin.apply_changes.assert_called_once_with(
net_state_mock(desired_config, current_config)
net_state_mock(desired_config, current_config), True
)
@ -110,7 +110,7 @@ def test_add_new_bond(
netapplier.apply(desired_config, verify_change=False)
plugin.apply_changes.assert_called_once_with(
net_state_mock(desired_config, {})
net_state_mock(desired_config, {}), True
)
@ -146,7 +146,7 @@ def test_edit_existing_bond(
netapplier.apply(desired_config, verify_change=False)
plugin.apply_changes.assert_called_once_with(
net_state_mock(desired_config, current_config)
net_state_mock(desired_config, current_config), True
)