gen_conf: Allowing generate configurations by desire state

Introduced below allowing user to get the configurations of desire
state. User can save the configuration in their preferred way.

 * `libnmstate.generate_configurations(desire_state)`
 * `nmstatectl gc state.yml`

It will return a dictionary with:
 * Key: plugin name
 * Value: list of tuple(file name and file content) for configurations
          used by that backend.

Example for `nmstatectl gc eth1_up.yml`

```yaml
---
NetworkManager:
- - eth1.nmconnection
  - '[connection]

    id=eth1

    uuid=091eaa84-f9da-408a-8144-3da890967b7b

    type=ethernet

    autoconnect-slaves=1

    interface-name=eth1

    permissions=

    [ethernet]

    mac-address-blacklist=

    mtu=1500

    [ipv4]

    dhcp-client-id=mac

    dns-search=

    method=disabled

    [ipv6]

    addr-gen-mode=eui64

    dhcp-duid=ll

    dhcp-iaid=mac

    dns-search=

    method=disabled

    [proxy]

    '
```

Signed-off-by: Gris Ge <fge@redhat.com>
This commit is contained in:
Gris Ge 2021-02-08 13:52:06 +08:00
parent fd21172d50
commit 627af31725
14 changed files with 254 additions and 49 deletions

View File

@ -13,6 +13,8 @@ nmstatectl \- A nmstate command line tool
.br
.B nmstatectl edit \fR[\fIINTERFACE_NAME\fR] [\fIOPTIONS\fR]
.br
.B nmstatectl gc \fR[\fISTATE_FILE_PATH\fR] [\fIOPTIONS\fR]
.br
.B nmstatectl rollback \fR[\fICHECKPOINT_PATH\fR]
.br
.B nmstatectl commit \fR[\fICHECKPOINT_PATH\fR]
@ -89,6 +91,18 @@ decide whether rollback to previous (before \fB"nmstatectl set/edit"\fR) state.
rollback the network state from specified checkpoint file. \fBnmstatectl\fR
will take the latest checkpoint if not defined as argument.
.PP
.B gc
.RS
Generates configuration files for specified network state file(s). The output
will be dictinary with plugin name as key and an tuple as value.
The tuple will holding configuration file name and configuration content.
The generated configuration is not saved into system, users have to do it
by themselves after refering to the network backend.
.RE
.B commit
.RS
commit the current network state. \fBnmstatectl\fR will take the latest

View File

@ -27,7 +27,7 @@ from .netapplier import commit
from .netapplier import rollback
from .netinfo import show
from .netinfo import show_running_config
from .nmstate import generate_configurations
from .prettystate import PrettyState
@ -38,6 +38,7 @@ __all__ = [
"apply",
"commit",
"error",
"generate_configurations",
"rollback",
"schema",
"show",

View File

@ -40,9 +40,10 @@ class DnsState:
else:
self._dns_state = des_dns_state
self._validate()
self._config_changed = _is_dns_config_changed(
des_dns_state, cur_dns_state
)
if cur_dns_state:
self._config_changed = _is_dns_config_changed(
des_dns_state, cur_dns_state
)
self._cur_dns_state = deepcopy(cur_dns_state) if cur_dns_state else {}
@property

View File

@ -434,7 +434,10 @@ class BaseIface:
def store_route_metadata(self, route_metadata):
for family, routes in route_metadata.items():
self.raw[family][BaseIface.ROUTES_METADATA] = routes
try:
self.raw[family][BaseIface.ROUTES_METADATA] = routes
except KeyError:
self.raw[family] = {BaseIface.ROUTES_METADATA: routes}
def store_route_rule_metadata(self, route_rule_metadata):
for family, rules in route_rule_metadata.items():

View File

@ -81,9 +81,16 @@ class Ifaces:
also responsible to handle desire vs current state related tasks.
"""
def __init__(self, des_iface_infos, cur_iface_infos, save_to_disk=True):
def __init__(
self,
des_iface_infos,
cur_iface_infos,
save_to_disk=True,
gen_conf_mode=False,
):
self._save_to_disk = save_to_disk
self._des_iface_infos = des_iface_infos
self._gen_conf_mode = gen_conf_mode
self._cur_kernel_ifaces = {}
self._kernel_ifaces = {}
self._user_space_ifaces = _UserSpaceIfaces()
@ -102,6 +109,8 @@ class Ifaces:
if des_iface_infos:
for iface_info in des_iface_infos:
iface = BaseIface(iface_info, save_to_disk)
if not iface.is_up and self._gen_conf_mode:
continue
if iface.type == InterfaceType.UNKNOWN:
cur_ifaces = self._get_cur_ifaces(iface.name)
if len(cur_ifaces) > 1:
@ -119,6 +128,8 @@ class Ifaces:
if iface_info.get(Interface.TYPE) is None:
if cur_iface:
iface_info[Interface.TYPE] = cur_iface.type
elif gen_conf_mode:
iface_info[Interface.TYPE] = InterfaceType.ETHERNET
elif iface.is_up:
raise NmstateValueError(
f"Interface {iface.name} has no type defined "
@ -634,26 +645,59 @@ class Ifaces:
# All the user space interface already has interface type defined.
# And user space interface cannot be port of other interface.
# Hence no need to check `self._user_space_ifaces`
new_ifaces = {}
for iface in self._kernel_ifaces.values():
for port_name in iface.port:
if not self._kernel_ifaces.get(port_name):
raise NmstateValueError(
f"Interface {iface.name} has unknown port: {port_name}"
)
if self._gen_conf_mode:
logging.warning(
f"Interface {port_name} does not exit in "
"desire state, assuming it is ethernet"
)
new_ifaces[port_name] = _to_specific_iface_obj(
{
Interface.NAME: port_name,
Interface.TYPE: InterfaceType.ETHERNET,
Interface.STATE: InterfaceState.UP,
},
self._save_to_disk,
)
else:
raise NmstateValueError(
f"Interface {iface.name} has unknown port: "
f"{port_name}"
)
self._kernel_ifaces.update(new_ifaces)
def _validate_unknown_parent(self):
"""
Check the existance of parent interface
"""
# All child interface should be in kernel space.
new_ifaces = {}
for iface in self._kernel_ifaces.values():
if iface.parent:
parent_iface = self._get_parent_iface(iface)
if not parent_iface:
raise NmstateValueError(
f"Interface {iface.name} has unknown parent: "
f"{iface.parent}"
)
if self._gen_conf_mode:
logging.warning(
f"Interface {iface.parent} does not exit in "
"desire state, assuming it is ethernet"
)
new_ifaces[iface.parent] = _to_specific_iface_obj(
{
Interface.NAME: iface.parent,
Interface.TYPE: InterfaceType.ETHERNET,
Interface.STATE: InterfaceState.UP,
},
self._save_to_disk,
)
else:
raise NmstateValueError(
f"Interface {iface.name} has unknown parent: "
f"{iface.parent}"
)
self._kernel_ifaces.update(new_ifaces)
def _remove_unknown_type_interfaces(self):
"""

View File

@ -34,13 +34,20 @@ from .state import state_match
class NetState:
def __init__(self, desire_state, current_state=None, save_to_disk=True):
def __init__(
self,
desire_state,
current_state=None,
save_to_disk=True,
gen_conf_mode=False,
):
if current_state is None:
current_state = {}
self._ifaces = Ifaces(
desire_state.get(Interface.KEY),
current_state.get(Interface.KEY),
save_to_disk,
gen_conf_mode,
)
self._route = RouteState(
self._ifaces,

View File

@ -21,6 +21,7 @@ import logging
from operator import itemgetter
from libnmstate.error import NmstateDependencyError
from libnmstate.error import NmstateNotSupportedError
from libnmstate.error import NmstateValueError
from libnmstate.ifaces.ovs import is_ovs_running
from libnmstate.schema import DNS
@ -61,9 +62,8 @@ from .wired import get_info as get_wired_info
class NetworkManagerPlugin(NmstatePlugin):
def __init__(self):
self._ctx = NmContext()
self._ctx = None
self._checkpoint = None
self._check_version_mismatch()
self.__applied_configs = None
@property
@ -91,10 +91,13 @@ class NetworkManagerPlugin(NmstatePlugin):
@property
def client(self):
return self._ctx.client if self._ctx else None
return self.context.client if self.context else None
@property
def context(self):
if not self._ctx:
self._ctx = NmContext()
self._check_version_mismatch()
return self._ctx
@property
@ -198,26 +201,26 @@ class NetworkManagerPlugin(NmstatePlugin):
# Old checkpoint might timeout, hence it's legal to load
# another one.
self._checkpoint.clean_up()
candidates = get_checkpoints(self._ctx.client)
candidates = get_checkpoints(self.client)
if checkpoint_path in candidates:
self._checkpoint = CheckPoint(
nm_context=self._ctx, dbuspath=checkpoint_path
nm_context=self.context, dbuspath=checkpoint_path
)
else:
raise NmstateValueError("No checkpoint specified or found")
else:
if not self._checkpoint:
# Get latest one
candidates = get_checkpoints(self._ctx.client)
candidates = get_checkpoints(self.client)
if candidates:
self._checkpoint = CheckPoint(
nm_context=self._ctx, dbuspath=candidates[0]
nm_context=self.context, dbuspath=candidates[0]
)
else:
raise NmstateValueError("No checkpoint specified or found")
def create_checkpoint(self, timeout=60):
self._checkpoint = CheckPoint.create(self._ctx, timeout)
self._checkpoint = CheckPoint.create(self.context, timeout)
return str(self._checkpoint)
def rollback_checkpoint(self, checkpoint=None):
@ -231,7 +234,7 @@ class NetworkManagerPlugin(NmstatePlugin):
self._checkpoint = None
def _check_version_mismatch(self):
nm_client_version = self._ctx.client.get_version()
nm_client_version = self.client.get_version()
nm_utils_version = _nm_utils_decode_version()
if nm_client_version is None:
@ -248,6 +251,14 @@ class NetworkManagerPlugin(NmstatePlugin):
nm_client_version,
)
def generate_configurations(self, net_state):
if not hasattr(NM, "keyfile_write"):
raise NmstateNotSupportedError(
f"Current NetworkManager version does not support generating "
"configurations, please upgrade to 1.30 or later versoin."
)
return NmProfiles(None).generate_config_strings(net_state)
def _remove_ovs_bridge_unsupported_entries(iface_info):
"""

View File

@ -89,10 +89,9 @@ class NmProfile:
ACTION_DELETE_DEVICE,
)
def __init__(self, ctx, iface, save_to_disk):
def __init__(self, ctx, iface):
self._ctx = ctx
self._iface = iface
self._save_to_disk = save_to_disk
self._nm_iface_type = None
if self._iface.type != InterfaceType.UNKNOWN:
self._nm_iface_type = Api2Nm.get_iface_type(self._iface.type)
@ -105,9 +104,6 @@ class NmProfile:
self._deactivated = False
self._profile_deleted = False
self._device_deleted = False
self._import_current()
self._gen_actions()
self._gen_nm_sim_conn()
@property
def iface(self):
@ -119,6 +115,18 @@ class NmProfile:
self.iface.is_changed or self.iface.is_desired
) and not self.iface.is_ignore
@property
def config_file_name(self):
"""
Return the profile file name used NetworkManager for key-file mode.
"""
if self._nm_simple_conn:
return f"{self._nm_simple_conn.get_id()}.nmconnection"
elif self._nm_profile:
return f"{self._nm_profile.get_id()}.nmconnection"
else:
return ""
@property
def uuid(self):
if self._nm_simple_conn:
@ -136,6 +144,18 @@ class NmProfile:
self._nm_simple_conn, self.iface.type, parent
)
def to_key_file_string(self):
nm_simple_conn = create_new_nm_simple_conn(
self._iface, nm_profile=None
)
nm_simple_conn.normalize()
# pylint: disable=no-member
key_file = NM.keyfile_write(
nm_simple_conn, NM.KeyfileHandlerFlags.NONE, None, None
)
# pylint: enable=no-member
return key_file.to_data()[0]
def _gen_actions(self):
if not self.has_pending_change:
return
@ -223,18 +243,17 @@ class NmProfile:
# settings.
self._add_action(NmProfile.ACTION_ACTIVATE_FIRST)
def _gen_nm_sim_conn(self):
def prepare_config(self, save_to_disk, gen_conf_mode=False):
if self._iface.is_absent or self._iface.is_down:
return
self._check_sriov_support()
self._check_unsupported_memory_only()
# Don't create new profile if original desire does not ask
# anything besides state:up and not been marked as changed.
# We don't need to do this once we support querying on-disk
# configure
if (
self._nm_profile is None
not gen_conf_mode
and self._nm_profile is None
and not self._iface.is_changed
and set(self._iface.original_dict)
<= set([Interface.STATE, Interface.NAME, Interface.TYPE])
@ -242,7 +261,7 @@ class NmProfile:
cur_nm_profile = self._get_first_nm_profile()
if (
cur_nm_profile
and _is_memory_only(cur_nm_profile) != self._save_to_disk
and _is_memory_only(cur_nm_profile) != save_to_disk
):
self._nm_profile = cur_nm_profile
self._nm_simple_conn = cur_nm_profile
@ -255,7 +274,10 @@ class NmProfile:
self._iface, self._nm_profile
)
def save_config(self):
def save_config(self, save_to_disk):
self._check_sriov_support()
self._check_unsupported_memory_only(save_to_disk)
self._gen_actions()
if not self.has_pending_change:
return
if self._iface.is_absent or self._iface.is_down:
@ -267,7 +289,7 @@ class NmProfile:
cur_nm_profile = self._get_first_nm_profile()
if (
cur_nm_profile
and _is_memory_only(cur_nm_profile) != self._save_to_disk
and _is_memory_only(cur_nm_profile) != save_to_disk
):
self._nm_profile = cur_nm_profile
return
@ -279,7 +301,7 @@ class NmProfile:
self._iface.type,
self._nm_simple_conn,
self._nm_profile,
self._save_to_disk,
save_to_disk,
).run()
else:
self._nm_profile = None
@ -288,12 +310,12 @@ class NmProfile:
self._iface.name,
self._iface.type,
self._nm_simple_conn,
self._save_to_disk,
save_to_disk,
).run()
def _check_unsupported_memory_only(self):
def _check_unsupported_memory_only(self, save_to_disk):
if (
not self._save_to_disk
not save_to_disk
and StrictVersion(self._ctx.client.get_version())
< StrictVersion("1.28.0")
and self._iface.type
@ -376,7 +398,7 @@ class NmProfile:
def _delete_device(self):
if self._device_deleted:
return
self._import_current()
self.import_current()
if self._nm_dev:
DeviceDelete(
self._ctx, self._iface.name, self._iface.type, self._nm_dev
@ -442,7 +464,7 @@ class NmProfile:
else:
time.sleep(IMPORT_NM_DEV_RETRY_INTERNAL)
def _import_current(self):
def import_current(self):
self._nm_dev = get_nm_dev(
self._ctx, self._iface.name, self._iface.type
)
@ -477,7 +499,7 @@ class NmProfile:
return
if self._iface.is_down:
return
self._import_current()
self.import_current()
for nm_profile in self._ctx.client.get_connections():
if is_multiconnect_profile(nm_profile):
continue

View File

@ -42,13 +42,30 @@ class NmProfiles:
def __init__(self, context):
self._ctx = context
def generate_config_strings(self, net_state):
_append_nm_ovs_port_iface(net_state)
all_profiles = []
for iface in net_state.ifaces.all_ifaces():
if iface.is_up:
profile = NmProfile(self._ctx, iface)
profile.prepare_config(save_to_disk=False, gen_conf_mode=True)
all_profiles.append(profile)
return [
(profile.config_file_name, profile.to_key_file_string())
for profile in all_profiles
]
def apply_config(self, net_state, save_to_disk):
self._prepare_state_for_profiles(net_state)
all_profiles = [
NmProfile(self._ctx, iface, save_to_disk)
NmProfile(self._ctx, iface)
for iface in net_state.ifaces.all_ifaces()
]
for profile in all_profiles:
profile.import_current()
profile.prepare_config(save_to_disk, gen_conf_mode=False)
_use_uuid_as_controller_and_parent(all_profiles)
changed_ovs_bridges_and_ifaces = {}
@ -62,7 +79,7 @@ class NmProfiles:
for profile in all_profiles:
if profile.has_pending_change:
profile.save_config()
profile.save_config(save_to_disk)
self._ctx.wait_all_finish()
for action in NmProfile.ACTIONS:

View File

@ -28,6 +28,7 @@ import pkgutil
from libnmstate import validator
from libnmstate.error import NmstateError
from libnmstate.error import NmstateValueError
from libnmstate.error import NmstateDependencyError
from libnmstate.schema import DNS
from libnmstate.schema import Interface
from libnmstate.schema import InterfaceType
@ -37,6 +38,7 @@ from libnmstate.schema import RouteRule
from .nispor.plugin import NisporPlugin
from .plugin import NmstatePlugin
from .state import merge_dict
from .net_state import NetState
_INFO_TYPE_RUNNING = 1
_INFO_TYPE_RUNNING_CONFIG = 2
@ -173,10 +175,15 @@ def _get_interface_info_from_plugins(plugins, info_type):
not in plugin.plugin_capabilities
):
continue
if info_type == _INFO_TYPE_RUNNING_CONFIG:
ifaces = plugin.get_running_config_interfaces()
else:
ifaces = plugin.get_interfaces()
try:
if info_type == _INFO_TYPE_RUNNING_CONFIG:
ifaces = plugin.get_running_config_interfaces()
else:
ifaces = plugin.get_interfaces()
except NmstateDependencyError as e:
logging.warning(f"Plugin {plugin.name} error: {e}")
continue
for iface in ifaces:
iface[IFACE_PRIORITY_METADATA] = plugin.priority
iface[IFACE_PLUGIN_SRC_METADATA] = [plugin.name]
@ -370,3 +377,22 @@ def _get_iface_types_by_name(iface_infos, name):
def show_running_config_with_plugins(plugins):
return show_with_plugins(plugins, info_type=_INFO_TYPE_RUNNING_CONFIG)
def generate_configurations(desire_state):
"""
Return a dictionary with:
* key: plugin name
* vlaue: list of strings for configruations
This function will not merge or verify desire state with current state, so
you may run this function on different system.
"""
configs = {}
net_state = NetState(desire_state, gen_conf_mode=True)
with plugin_context() as plugins:
for plugin in plugins:
config = plugin.generate_configurations(net_state)
if config:
configs[plugin.name] = config
return configs

View File

@ -121,3 +121,10 @@ class NmstatePlugin(metaclass=ABCMeta):
Retrun False when plugin can report new interface.
"""
return False
def generate_configurations(self, net_state):
"""
Returning a list of strings for configurations which could be save
persistently.
"""
return []

View File

@ -58,6 +58,7 @@ def main():
setup_subcommand_show(subparsers)
setup_subcommand_version(subparsers)
setup_subcommand_varlink(subparsers)
setup_subcommand_gen_config(subparsers)
parser.add_argument(
"--version", action="store_true", help="Display nmstate version"
)
@ -253,6 +254,16 @@ def setup_subcommand_varlink(subparsers):
parser_varlink.set_defaults(func=run_varlink_server)
def setup_subcommand_gen_config(subparsers):
parser_gc = subparsers.add_parser("gc", help="Generate configurations")
parser_gc.add_argument(
"file",
help="File containing desired state. ",
nargs="*",
)
parser_gc.set_defaults(func=run_gen_config)
def version(args):
print(libnmstate.__version__)
@ -354,6 +365,30 @@ def run_varlink_server(args):
logging.exception(exception)
def run_gen_config(args):
if args.file:
for statefile in args.file:
if statefile == "-" and not os.path.isfile(statefile):
statedata = sys.stdin.read()
else:
with open(statefile) as statefile:
statedata = statefile.read()
# JSON dictionaries start with a curly brace
if statedata[0] == "{":
state = json.loads(statedata)
use_yaml = False
else:
state = yaml.load(statedata, Loader=yaml.SafeLoader)
use_yaml = True
print_state(
libnmstate.generate_configurations(state), use_yaml=use_yaml
)
else:
sys.stderr.write("ERROR: No state specified\n")
return 1
def apply_state(statedata, verify_change, commit, timeout, save_to_disk):
use_yaml = False
# JSON dictionaries start with a curly brace

View File

@ -23,7 +23,10 @@ import pytest
from .testlib import assertlib
from .testlib.examplelib import example_state
from .testlib.examplelib import find_examples_dir
from .testlib.examplelib import load_example
import libnmstate
from libnmstate import netinfo
from libnmstate.error import NmstateNotSupportedError
from libnmstate.schema import DNS
@ -228,3 +231,17 @@ def test_add_veth_and_remove():
assertlib.assert_absent("veth1")
assertlib.assert_absent("veth1peer")
@pytest.mark.skipif(
nm_major_minor_version() <= 1.30,
reason="Generating config is not supported on NetworkManager 1.30-",
)
def test_gen_conf_for_examples():
example_dir = find_examples_dir()
with os.scandir(example_dir) as example_dir_fd:
for example_file in example_dir_fd:
if example_file.name.endswith(".yml"):
libnmstate.generate_configurations(
load_example(example_file.name)
)

View File

@ -326,7 +326,7 @@ def test_network_manager_plugin_with_daemon_stopped(stop_nm_service):
with pytest.raises(NmstateDependencyError):
from libnmstate.nm import NetworkManagerPlugin
NetworkManagerPlugin()
NetworkManagerPlugin().context
state = statelib.show_only(("lo",))
assert state[Interface.KEY][0] == LO_IFACE_INFO