diff options
Diffstat (limited to 'netplan/cli/commands/set.py')
-rw-r--r-- | netplan/cli/commands/set.py | 177 |
1 files changed, 29 insertions, 148 deletions
diff --git a/netplan/cli/commands/set.py b/netplan/cli/commands/set.py index f113f71..ee82fa0 100644 --- a/netplan/cli/commands/set.py +++ b/netplan/cli/commands/set.py @@ -17,19 +17,14 @@ '''netplan set command line''' -import os -import yaml import tempfile import re -import logging -import shutil -import glob +import io from netplan.cli.utils import NetplanCommand import netplan.libnetplan as libnetplan -from netplan.configmanager import ConfigManager -FALLBACK_HINT = '70-netplan-set' +FALLBACK_FILENAME = '70-netplan-set.yaml' GLOBAL_KEYS = ['renderer', 'version'] @@ -54,151 +49,37 @@ class NetplanSet(NetplanCommand): self.parse_args() self.run_command() - def is_emtpy_yaml(self, tree): - if isinstance(tree, dict) and list(tree.keys()) == ['network'] and tree['network'] is None: - return True - return False - - def split_tree_by_hint(self, set_tree) -> (str, dict): - network = set_tree.get('network', {}) - # A mapping of 'origin-hint' -> YAML tree (one subtree per netdef) - subtrees = dict() - for devtype in network: - if devtype in GLOBAL_KEYS: - continue # special handling of global keys down below - devtype_content = network.get(devtype, []) - # Special case: removal of a whole devtype. - # We replace the devtype null node with a dict of all defined netdefs - # set to None. - if devtype_content is None: - devtype_content = {dev: None for dev in libnetplan.netplan_get_ids_for_devtype(devtype, self.root_dir)} - network[devtype] = devtype_content - for netdef in devtype_content: - hint = FALLBACK_HINT - filename = libnetplan.netplan_get_filename_by_id(netdef, self.root_dir) - if filename: - hint = os.path.basename(filename)[:-5] # strip prefix and .yaml - netdef_tree = {'network': {devtype: {netdef: network.get(devtype).get(netdef)}}} - # Merge all netdef trees which are going to be written to the same file/hint - subtrees[hint] = self.merge(subtrees.get(hint, {}), netdef_tree) - - # Merge GLOBAL_KEYS into one of the available subtrees - # Write to same file (if only one hint/subtree is available) - # Write to FALLBACK_HINT if multiple hints/subtrees are available, as we do not know where it is supposed to go - if any(network.get(key) for key in GLOBAL_KEYS): - # Write to the same file, if we have only one file-hint or to FALLBACK_HINT otherwise - hint = list(subtrees)[0] if len(subtrees) == 1 else FALLBACK_HINT - for key in GLOBAL_KEYS: - tree = {'network': {key: network.get(key)}} - subtrees[hint] = self.merge(subtrees.get(hint, {}), tree) - - # return a list of (str:hint, dict:subtree) tuples - return subtrees.items() - def command_set(self): if self.origin_hint is not None and len(self.origin_hint) == 0: raise Exception('Invalid/empty origin-hint') + if self.origin_hint: + filename = '.'.join((self.origin_hint, 'yaml')) + else: + filename = None split = self.key_value.split('=', 1) if len(split) != 2: raise Exception('Invalid value specified') - key, value = split - set_tree = self.parse_key(key, yaml.safe_load(value)) - - # special case: clear all YAML (or a specific hint file) if "network=null" is set - if self.is_emtpy_yaml(set_tree): - path = os.path.join('etc', 'netplan') - if self.origin_hint: # clear specific hint file, it it does exist - hint_path = os.path.join(self.root_dir, path, self.origin_hint + '.yaml') - if os.path.isfile(hint_path): - os.remove(hint_path) - else: # clear all YAML files in <ROOT_DIR>/etc/netplan/*.yaml - yaml_files = glob.glob(os.path.join(self.root_dir, path, '*.yaml')) - for f in yaml_files: - os.remove(f) - return - - hints = [(self.origin_hint, set_tree)] - # Override YAML config in each individual netdef file if origin-hint is not set - if self.origin_hint is None: - hints = self.split_tree_by_hint(set_tree) - for hint, subtree in hints: - self.write_file(subtree, hint + '.yaml', self.root_dir) - - def parse_key(self, key, value): - # The 'network.' prefix is optional for netsted keys, its always assumed to be there - if not key.startswith('network.') and not key == 'network': - key = 'network.' + key - # Split at '.' but not at '\.' via negative lookbehind expression - split = re.split(r'(?<!\\)\.', key) - tree = {} - i = 1 - t = tree - for part in split: - part = part.replace('\\.', '.') # Unescape interface-ids, containing dots - val = {} - if i == len(split): - val = value - t = t.setdefault(part, val) - i += 1 - return tree - - def merge(self, a, b, path=None): - """ - Merges tree/dict 'b' into tree/dict 'a' - """ - if path is None: - path = [] - for key in b: - if key in a: - if isinstance(a[key], dict) and isinstance(b[key], dict): - self.merge(a[key], b[key], path + [str(key)]) - elif b[key] is None: - del a[key] - else: - # Overwrite existing key with new key/value from 'set' command - a[key] = b[key] - else: - a[key] = b[key] - return a - - def write_file(self, set_tree, name, rootdir='/'): - tmproot = tempfile.TemporaryDirectory(prefix='netplan-set_') - path = os.path.join('etc', 'netplan') - os.makedirs(os.path.join(tmproot.name, path)) - - config = {'network': {}} - absp = os.path.join(rootdir, path, name) - # check stat(absp), as we don't care about empty hint files - if os.path.isfile(absp) and os.stat(absp).st_size > 0: - with open(absp, 'r') as f: - c = yaml.safe_load(f) - if c is not None: # ignore empty file, even if it contains whitespace - config = c - - new_tree = self.merge(config, set_tree) - stripped = ConfigManager.strip_tree(new_tree) - logging.debug('Writing file {}: {}'.format(name, stripped)) - if 'network' in stripped and list(stripped['network'].keys()) == ['version']: - # Clear file if only 'network: {version: 2}' is left - logging.debug('Empty YAML, deleting file {}'.format(absp)) - if os.path.isfile(absp): - os.remove(absp) - elif 'network' in stripped: - tmpp = os.path.join(tmproot.name, path, name) - with open(tmpp, 'w+') as f: - new_yaml = yaml.dump(stripped, indent=2, default_flow_style=False) - f.write(new_yaml) - # Validate the newly created file, by parsing it via libnetplan - libnetplan.netplan_parse(tmpp) - # Valid, move it to final destination - shutil.copy2(tmpp, absp) - os.remove(tmpp) - elif stripped == {}: - # Clear file (if it exists) if the last/only key got removed - # do nothing otherwise - logging.debug('Removed last key from YAML, deleting file {}'.format(absp)) - if os.path.isfile(absp): - os.remove(absp) - else: # pragma nocover - raise Exception('Invalid input: {}'.format(set_tree)) + key, value = split + if not key.startswith('network'): + key = '.'.join(('network', key)) + + # Split the string into a list on the dot separators, and unescape the remaining dots + yaml_path = [s.replace(r'\.', '.') for s in re.split(r'(?<!\\)\.', key)] + + parser = libnetplan.Parser() + with tempfile.TemporaryFile() as tmp: + libnetplan.create_yaml_patch(yaml_path, value, tmp) + tmp.flush() + tmp.seek(0, io.SEEK_SET) + parser.load_nullable_fields(tmp) + parser.load_yaml_hierarchy(self.root_dir) + tmp.seek(0, io.SEEK_SET) + parser.load_yaml(tmp) + + state = libnetplan.State() + state.import_parser_results(parser) + if self.origin_hint: + state.write_yaml_file(filename, self.root_dir) + else: + state.update_yaml_hierarchy(FALLBACK_FILENAME, self.root_dir) |