2018-05-24 21:06:09 +00:00
|
|
|
import copy
|
|
|
|
import glob
|
|
|
|
import fnmatch
|
|
|
|
import os
|
|
|
|
import logging
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
log = logging.getLogger("subiquitycore.netplan")
|
|
|
|
|
|
|
|
|
2020-05-18 19:43:52 +00:00
|
|
|
def _sanitize_inteface_config(iface_config):
|
|
|
|
for ap, ap_config in iface_config.get('access-points', {}).items():
|
|
|
|
if 'password' in ap_config:
|
|
|
|
ap_config['password'] = '<REDACTED>'
|
|
|
|
|
|
|
|
|
|
|
|
def sanitize_interface_config(iface_config):
|
|
|
|
iface_config = copy.deepcopy(iface_config)
|
|
|
|
_sanitize_inteface_config(iface_config)
|
|
|
|
return iface_config
|
|
|
|
|
|
|
|
|
|
|
|
def sanitize_config(config):
|
|
|
|
"""Return a copy of config with passwords redacted."""
|
|
|
|
config = copy.deepcopy(config)
|
|
|
|
interfaces = config.get('network', {}).get('wifis', {}).items()
|
|
|
|
for iface, iface_config in interfaces:
|
|
|
|
_sanitize_inteface_config(iface_config)
|
|
|
|
return config
|
|
|
|
|
|
|
|
|
2018-05-24 21:06:09 +00:00
|
|
|
class Config:
|
|
|
|
"""A NetplanConfig represents the network config for a system.
|
|
|
|
|
|
|
|
Call parse_netplan_config() with each piece of yaml config, and then
|
|
|
|
call config_for_device to get the config that matches a particular
|
2020-04-02 20:17:19 +00:00
|
|
|
network device, if any.
|
2018-05-24 21:06:09 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self):
|
2020-04-02 20:17:19 +00:00
|
|
|
self.physical_devices = []
|
|
|
|
self.virtual_devices = []
|
2019-05-02 14:22:23 +00:00
|
|
|
self.config = {}
|
2018-05-24 21:06:09 +00:00
|
|
|
|
|
|
|
def parse_netplan_config(self, config):
|
|
|
|
try:
|
2018-10-29 23:12:07 +00:00
|
|
|
self.config = config = yaml.safe_load(config)
|
2018-05-24 21:06:09 +00:00
|
|
|
except yaml.ReaderError as e:
|
|
|
|
log.info("could not parse config: %s", e)
|
|
|
|
return
|
|
|
|
network = config.get('network')
|
|
|
|
if network is None:
|
|
|
|
log.info("no 'network' key in config")
|
|
|
|
return
|
|
|
|
version = network.get("version")
|
|
|
|
if version != 2:
|
|
|
|
log.info("network has no/unexpected version %s", version)
|
|
|
|
return
|
2020-04-02 20:17:19 +00:00
|
|
|
for phys_key in 'ethernets', 'wifis':
|
|
|
|
for dev, dev_config in network.get(phys_key, {}).items():
|
|
|
|
self.physical_devices.append(_PhysicalDevice(dev, dev_config))
|
|
|
|
for virt_key in 'bonds', 'vlans':
|
|
|
|
for dev, dev_config in network.get(virt_key, {}).items():
|
|
|
|
self.virtual_devices.append(_VirtualDevice(dev, dev_config))
|
2018-05-24 21:06:09 +00:00
|
|
|
|
|
|
|
def config_for_device(self, link):
|
2020-04-02 20:17:19 +00:00
|
|
|
if link.is_virtual:
|
|
|
|
for dev in self.virtual_devices:
|
|
|
|
if dev.name == link.name:
|
|
|
|
return copy.deepcopy(dev.config)
|
|
|
|
else:
|
|
|
|
allowed_matches = ('macaddress',)
|
|
|
|
match_key = 'match'
|
|
|
|
for dev in self.physical_devices:
|
|
|
|
if dev.matches_link(link):
|
|
|
|
config = copy.deepcopy(dev.config)
|
|
|
|
if match_key in config:
|
|
|
|
match = {k: v for k, v in config[match_key].items()
|
|
|
|
if k in allowed_matches}
|
|
|
|
if match:
|
|
|
|
config[match_key] = match
|
|
|
|
else:
|
|
|
|
del config[match_key]
|
|
|
|
return config
|
2018-05-24 21:06:09 +00:00
|
|
|
return {}
|
|
|
|
|
|
|
|
def load_from_root(self, root):
|
|
|
|
for path in configs_in_root(root):
|
|
|
|
try:
|
|
|
|
fp = open(path)
|
|
|
|
except OSError:
|
|
|
|
log.exception("opening %s failed", path)
|
|
|
|
with fp:
|
|
|
|
self.parse_netplan_config(fp.read())
|
|
|
|
|
|
|
|
|
2020-04-02 20:17:19 +00:00
|
|
|
class _PhysicalDevice:
|
2018-05-24 21:06:09 +00:00
|
|
|
def __init__(self, name, config):
|
|
|
|
match = config.get('match')
|
|
|
|
if match is None:
|
|
|
|
self.match_name = name
|
|
|
|
self.match_mac = None
|
|
|
|
self.match_driver = None
|
|
|
|
else:
|
|
|
|
self.match_name = match.get('name')
|
|
|
|
self.match_mac = match.get('macaddress')
|
|
|
|
self.match_driver = match.get('driver')
|
|
|
|
self.config = config
|
2020-05-18 19:43:52 +00:00
|
|
|
log.debug(
|
|
|
|
"config for %s = %s" % (
|
|
|
|
name, sanitize_interface_config(self.config)))
|
2018-05-24 21:06:09 +00:00
|
|
|
|
|
|
|
def matches_link(self, link):
|
|
|
|
if self.match_name is not None:
|
|
|
|
matches_name = fnmatch.fnmatch(link.name, self.match_name)
|
|
|
|
else:
|
|
|
|
matches_name = True
|
|
|
|
if self.match_mac is not None:
|
|
|
|
matches_mac = self.match_mac == link.hwaddr
|
|
|
|
else:
|
|
|
|
matches_mac = True
|
|
|
|
if self.match_driver is not None:
|
|
|
|
matches_driver = self.match_driver == link.driver
|
|
|
|
else:
|
|
|
|
matches_driver = True
|
|
|
|
return matches_name and matches_mac and matches_driver
|
|
|
|
|
|
|
|
|
2020-04-02 20:17:19 +00:00
|
|
|
class _VirtualDevice:
|
|
|
|
def __init__(self, name, config):
|
|
|
|
self.name = name
|
|
|
|
self.config = config
|
2020-05-18 19:43:52 +00:00
|
|
|
log.debug(
|
|
|
|
"config for %s = %s" % (
|
|
|
|
name, sanitize_interface_config(self.config)))
|
2020-04-02 20:17:19 +00:00
|
|
|
|
|
|
|
|
2018-05-24 21:06:09 +00:00
|
|
|
def configs_in_root(root, masked=False):
|
|
|
|
"""Return a list of all netplan configs under root.
|
|
|
|
|
|
|
|
The list is ordered in increasing precedence.
|
|
|
|
@param masked: if True, include config paths that are masked
|
|
|
|
by the same basename in a different directory."""
|
|
|
|
if not os.path.isabs(root):
|
|
|
|
root = os.path.abspath(root)
|
|
|
|
wildcard = "*.yaml"
|
|
|
|
dirs = {"lib": "0", "etc": "1", "run": "2"}
|
|
|
|
rootlen = len(root)
|
|
|
|
|
|
|
|
paths = []
|
|
|
|
for d in dirs:
|
|
|
|
paths.extend(glob.glob(os.path.join(root, d, "netplan", wildcard)))
|
|
|
|
|
|
|
|
def mykey(path):
|
|
|
|
"""returned key is basename + string-precidence based on dir."""
|
|
|
|
bname = os.path.basename(path)
|
|
|
|
bdir = path[rootlen + 1]
|
|
|
|
bdir = bdir[:bdir.find(os.path.sep)]
|
|
|
|
return "%s/%s" % (bname, bdir)
|
|
|
|
|
|
|
|
if not masked:
|
|
|
|
paths = {os.path.basename(p): p for p in paths}.values()
|
|
|
|
return sorted(paths, key=mykey)
|