summaryrefslogtreecommitdiff
path: root/netplan/cli/commands/set.py
diff options
context:
space:
mode:
Diffstat (limited to 'netplan/cli/commands/set.py')
-rw-r--r--netplan/cli/commands/set.py177
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)