/subiquitycore/netplan.py

https://github.com/CanonicalLtd/subiquity
Python | 163 lines | 127 code | 23 blank | 13 comment | 43 complexity | 000c0eeb2e7a4eed8597874c652acdce MD5 | raw file
  1. import copy
  2. import glob
  3. import fnmatch
  4. import os
  5. import logging
  6. import yaml
  7. log = logging.getLogger("subiquitycore.netplan")
  8. def _sanitize_inteface_config(iface_config):
  9. for ap, ap_config in iface_config.get('access-points', {}).items():
  10. if 'password' in ap_config:
  11. ap_config['password'] = '<REDACTED>'
  12. def sanitize_interface_config(iface_config):
  13. iface_config = copy.deepcopy(iface_config)
  14. _sanitize_inteface_config(iface_config)
  15. return iface_config
  16. def sanitize_config(config):
  17. """Return a copy of config with passwords redacted."""
  18. config = copy.deepcopy(config)
  19. interfaces = config.get('network', {}).get('wifis', {}).items()
  20. for iface, iface_config in interfaces:
  21. _sanitize_inteface_config(iface_config)
  22. return config
  23. class Config:
  24. """A NetplanConfig represents the network config for a system.
  25. Call parse_netplan_config() with each piece of yaml config, and then
  26. call config_for_device to get the config that matches a particular
  27. network device, if any.
  28. """
  29. def __init__(self):
  30. self.physical_devices = []
  31. self.virtual_devices = []
  32. self.config = {}
  33. def parse_netplan_config(self, config):
  34. try:
  35. self.config = config = yaml.safe_load(config)
  36. except yaml.ReaderError as e:
  37. log.info("could not parse config: %s", e)
  38. return
  39. network = config.get('network')
  40. if network is None:
  41. log.info("no 'network' key in config")
  42. return
  43. version = network.get("version")
  44. if version != 2:
  45. log.info("network has no/unexpected version %s", version)
  46. return
  47. for phys_key in 'ethernets', 'wifis':
  48. for dev, dev_config in network.get(phys_key, {}).items():
  49. self.physical_devices.append(_PhysicalDevice(dev, dev_config))
  50. for virt_key in 'bonds', 'vlans':
  51. for dev, dev_config in network.get(virt_key, {}).items():
  52. self.virtual_devices.append(_VirtualDevice(dev, dev_config))
  53. def config_for_device(self, link):
  54. if link.is_virtual:
  55. for dev in self.virtual_devices:
  56. if dev.name == link.name:
  57. return copy.deepcopy(dev.config)
  58. else:
  59. allowed_matches = ('macaddress',)
  60. match_key = 'match'
  61. for dev in self.physical_devices:
  62. if dev.matches_link(link):
  63. config = copy.deepcopy(dev.config)
  64. if match_key in config:
  65. match = {k: v for k, v in config[match_key].items()
  66. if k in allowed_matches}
  67. if match:
  68. config[match_key] = match
  69. else:
  70. del config[match_key]
  71. return config
  72. return {}
  73. def load_from_root(self, root):
  74. for path in configs_in_root(root):
  75. try:
  76. fp = open(path)
  77. except OSError:
  78. log.exception("opening %s failed", path)
  79. with fp:
  80. self.parse_netplan_config(fp.read())
  81. class _PhysicalDevice:
  82. def __init__(self, name, config):
  83. match = config.get('match')
  84. if match is None:
  85. self.match_name = name
  86. self.match_mac = None
  87. self.match_driver = None
  88. else:
  89. self.match_name = match.get('name')
  90. self.match_mac = match.get('macaddress')
  91. self.match_driver = match.get('driver')
  92. self.config = config
  93. log.debug(
  94. "config for %s = %s" % (
  95. name, sanitize_interface_config(self.config)))
  96. def matches_link(self, link):
  97. if self.match_name is not None:
  98. matches_name = fnmatch.fnmatch(link.name, self.match_name)
  99. else:
  100. matches_name = True
  101. if self.match_mac is not None:
  102. matches_mac = self.match_mac == link.hwaddr
  103. else:
  104. matches_mac = True
  105. if self.match_driver is not None:
  106. matches_driver = self.match_driver == link.driver
  107. else:
  108. matches_driver = True
  109. return matches_name and matches_mac and matches_driver
  110. class _VirtualDevice:
  111. def __init__(self, name, config):
  112. self.name = name
  113. self.config = config
  114. log.debug(
  115. "config for %s = %s" % (
  116. name, sanitize_interface_config(self.config)))
  117. def configs_in_root(root, masked=False):
  118. """Return a list of all netplan configs under root.
  119. The list is ordered in increasing precedence.
  120. @param masked: if True, include config paths that are masked
  121. by the same basename in a different directory."""
  122. if not os.path.isabs(root):
  123. root = os.path.abspath(root)
  124. wildcard = "*.yaml"
  125. dirs = {"lib": "0", "etc": "1", "run": "2"}
  126. rootlen = len(root)
  127. paths = []
  128. for d in dirs:
  129. paths.extend(glob.glob(os.path.join(root, d, "netplan", wildcard)))
  130. def mykey(path):
  131. """returned key is basename + string-precidence based on dir."""
  132. bname = os.path.basename(path)
  133. bdir = path[rootlen + 1]
  134. bdir = bdir[:bdir.find(os.path.sep)]
  135. return "%s/%s" % (bname, bdir)
  136. if not masked:
  137. paths = {os.path.basename(p): p for p in paths}.values()
  138. return sorted(paths, key=mykey)