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:
parent
25df7dabda
commit
6d67a06c46
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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)))
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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]
|
||||
|
||||
|
@ -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():
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user