2015-06-24 19:44:31 +00:00
|
|
|
# Copyright 2015 Canonical, Ltd.
|
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Affero General Public License as
|
|
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
|
|
# License, or (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
2016-11-07 00:35:42 +00:00
|
|
|
import copy
|
2018-07-10 00:30:01 +00:00
|
|
|
import enum
|
2015-09-10 18:25:54 +00:00
|
|
|
import ipaddress
|
2015-06-24 19:44:31 +00:00
|
|
|
import logging
|
2016-09-16 02:28:40 +00:00
|
|
|
|
2018-05-24 21:06:09 +00:00
|
|
|
from subiquitycore import netplan
|
2016-09-16 02:28:40 +00:00
|
|
|
|
2015-06-24 19:44:31 +00:00
|
|
|
|
2018-10-29 23:12:07 +00:00
|
|
|
NETDEV_IGNORED_IFACE_TYPES = ['lo', 'bridge', 'tun', 'tap', 'dummy', 'sit']
|
|
|
|
NETDEV_ALLOWED_VIRTUAL_IFACE_TYPES = ['vlan', 'bond']
|
|
|
|
|
|
|
|
|
2016-06-30 18:17:01 +00:00
|
|
|
log = logging.getLogger('subiquitycore.models.network')
|
2015-07-21 15:55:02 +00:00
|
|
|
|
|
|
|
|
2018-10-29 23:12:07 +00:00
|
|
|
def addr_version(ip):
|
2016-11-07 00:35:42 +00:00
|
|
|
return ipaddress.ip_interface(ip).version
|
2016-08-15 21:05:19 +00:00
|
|
|
|
|
|
|
|
2018-07-10 00:30:01 +00:00
|
|
|
class NetDevAction(enum.Enum):
|
|
|
|
INFO = _("Info")
|
|
|
|
EDIT_WLAN = _("Edit Wifi")
|
|
|
|
EDIT_IPV4 = _("Edit IPv4")
|
|
|
|
EDIT_IPV6 = _("Edit IPv6")
|
2018-07-11 04:06:32 +00:00
|
|
|
EDIT_BOND = _("Edit bond")
|
2018-07-10 00:30:01 +00:00
|
|
|
ADD_VLAN = _("Add a VLAN tag")
|
|
|
|
DELETE = _("Delete")
|
|
|
|
|
|
|
|
|
2018-10-29 23:12:07 +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>'
|
2016-12-12 02:34:59 +00:00
|
|
|
|
2016-08-12 21:55:03 +00:00
|
|
|
|
2018-10-29 23:12:07 +00:00
|
|
|
def sanitize_interface_config(iface_config):
|
|
|
|
iface_config = copy.deepcopy(iface_config)
|
|
|
|
_sanitize_inteface_config(iface_config)
|
|
|
|
return iface_config
|
2016-08-12 21:55:03 +00:00
|
|
|
|
2017-02-16 23:38:14 +00:00
|
|
|
|
2018-10-29 23:12:07 +00:00
|
|
|
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
|
2016-11-07 00:35:42 +00:00
|
|
|
|
2016-12-12 02:34:59 +00:00
|
|
|
|
2018-10-29 23:12:07 +00:00
|
|
|
class BondParameters:
|
|
|
|
# Just a place to hang various data about how bonds can be
|
|
|
|
# configured.
|
|
|
|
|
|
|
|
modes = [
|
|
|
|
'balance-rr',
|
|
|
|
'active-backup',
|
|
|
|
'balance-xor',
|
|
|
|
'broadcast',
|
|
|
|
'802.3ad',
|
|
|
|
'balance-tlb',
|
|
|
|
'balance-alb',
|
|
|
|
]
|
|
|
|
|
|
|
|
supports_xmit_hash_policy = {
|
|
|
|
'balance-xor',
|
|
|
|
'802.3ad',
|
|
|
|
'balance-tlb',
|
|
|
|
}
|
|
|
|
|
|
|
|
xmit_hash_policies = [
|
|
|
|
'layer2',
|
|
|
|
'layer2+3',
|
|
|
|
'layer3+4',
|
|
|
|
'encap2+3',
|
|
|
|
'encap3+4',
|
|
|
|
]
|
|
|
|
|
|
|
|
supports_lacp_rate = {
|
|
|
|
'802.3ad',
|
|
|
|
}
|
|
|
|
|
|
|
|
lacp_rates = [
|
|
|
|
'slow',
|
|
|
|
'fast',
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
class NetworkDev(object):
|
|
|
|
|
|
|
|
def __init__(self, model, name, typ):
|
|
|
|
self._model = model
|
|
|
|
self._name = name
|
|
|
|
self.type = typ
|
|
|
|
self.config = {}
|
|
|
|
self.info = None
|
2016-12-12 02:34:59 +00:00
|
|
|
|
2016-11-07 00:35:42 +00:00
|
|
|
@property
|
2018-10-29 23:12:07 +00:00
|
|
|
def name(self):
|
|
|
|
return self._name
|
|
|
|
|
|
|
|
@name.setter
|
|
|
|
def name(self, new_name):
|
|
|
|
# If a virtual device that already exists is renamed, we need
|
|
|
|
# to create a dummy NetworkDev so that the existing virtual
|
|
|
|
# device is actually deleted when the config is applied.
|
|
|
|
if new_name != self.name and self.is_virtual and self.info is not None:
|
|
|
|
if new_name in self.model.devices_by_name:
|
|
|
|
raise RuntimeError(
|
|
|
|
"renaming {old_name} over {new_name}".format(
|
|
|
|
old_name=self.name, new_name=new_name))
|
|
|
|
self.model.devices_by_name[new_name] = self
|
|
|
|
dead_device = self.model.devices_by_name[self.name] = NetworkDev(
|
|
|
|
self.name, self.type)
|
|
|
|
dead_device.config = None
|
|
|
|
dead_device.info = self.info
|
|
|
|
self.info = None
|
|
|
|
self._name = new_name
|
2016-11-07 00:35:42 +00:00
|
|
|
|
2018-10-29 23:12:07 +00:00
|
|
|
def supports_action(self, action):
|
|
|
|
return getattr(self, "_supports_" + action.name)
|
2016-11-07 00:35:42 +00:00
|
|
|
|
|
|
|
@property
|
2018-10-29 23:12:07 +00:00
|
|
|
def ifindex(self):
|
|
|
|
if self.info is not None:
|
|
|
|
return self.info.ifindex
|
2016-11-07 00:35:42 +00:00
|
|
|
else:
|
|
|
|
return None
|
|
|
|
|
2016-11-07 03:25:18 +00:00
|
|
|
@property
|
2018-10-29 23:12:07 +00:00
|
|
|
def is_virtual(self):
|
|
|
|
return self.type in NETDEV_ALLOWED_VIRTUAL_IFACE_TYPES
|
2016-11-07 03:25:18 +00:00
|
|
|
|
2016-11-07 00:35:42 +00:00
|
|
|
@property
|
2018-10-29 23:12:07 +00:00
|
|
|
def is_bond_slave(self):
|
|
|
|
for dev in self._model.get_all_netdevs():
|
|
|
|
if dev.type == "bond":
|
|
|
|
if self.name in dev.config.get('interfaces', []):
|
|
|
|
return True
|
|
|
|
return False
|
2016-11-07 00:35:42 +00:00
|
|
|
|
|
|
|
@property
|
2018-10-29 23:12:07 +00:00
|
|
|
def is_used(self):
|
|
|
|
for dev in self._model.get_all_netdevs():
|
|
|
|
if dev.type == "bond":
|
|
|
|
if self.name in dev.config.get('interfaces', []):
|
|
|
|
return True
|
|
|
|
if dev.type == "vlan":
|
|
|
|
if self.name == dev.config.get('link'):
|
|
|
|
return True
|
|
|
|
return False
|
2016-09-16 02:28:40 +00:00
|
|
|
|
2018-10-29 23:12:07 +00:00
|
|
|
_supports_INFO = True
|
|
|
|
_supports_EDIT_WLAN = property(lambda self: self.type == "wlan")
|
|
|
|
_supports_EDIT_IPV4 = True
|
|
|
|
_supports_EDIT_IPV6 = True
|
|
|
|
_supports_EDIT_BOND = property(lambda self: self.type == "bond")
|
|
|
|
_supports_ADD_VLAN = property(
|
|
|
|
lambda self: self.type != "vlan" and not self.is_bond_slave)
|
|
|
|
_supports_DELETE = property(
|
|
|
|
lambda self: self.is_virtual and not self.is_used)
|
2016-08-16 04:48:15 +00:00
|
|
|
|
2016-12-12 02:34:59 +00:00
|
|
|
def remove_ip_networks_for_version(self, version):
|
2018-10-29 23:12:07 +00:00
|
|
|
self.config.pop('dhcp{v}'.format(v=version), None)
|
|
|
|
self.config.pop('gateway{v}'.format(v=version), None)
|
2016-11-07 00:35:42 +00:00
|
|
|
addrs = []
|
2018-10-29 23:12:07 +00:00
|
|
|
for ip in self.config.get('addresses', []):
|
|
|
|
if addr_version(ip) != version:
|
2016-11-07 00:35:42 +00:00
|
|
|
addrs.append(ip)
|
2018-10-29 23:12:07 +00:00
|
|
|
if addrs:
|
|
|
|
self.config['addresses'] = addrs
|
|
|
|
else:
|
|
|
|
self.config.pop('addresses', None)
|
2016-08-15 21:05:19 +00:00
|
|
|
|
2016-12-12 02:34:59 +00:00
|
|
|
def add_network(self, version, network):
|
2016-08-15 21:05:19 +00:00
|
|
|
# result = {
|
|
|
|
# 'network': self.subnet_input.value,
|
|
|
|
# 'address': self.address_input.value,
|
|
|
|
# 'gateway': self.gateway_input.value,
|
2016-11-07 00:35:42 +00:00
|
|
|
# 'nameserver': [nameservers],
|
|
|
|
# 'searchdomains': [searchdomains],
|
2016-08-15 21:05:19 +00:00
|
|
|
# }
|
|
|
|
address = network['address'].split('/')[0]
|
|
|
|
address += '/' + network['network'].split('/')[1]
|
2018-10-29 23:12:07 +00:00
|
|
|
self.config.setdefault('addresses', []).append(address)
|
|
|
|
gwkey = 'gateway{v}'.format(v=version)
|
|
|
|
if network['gateway']:
|
|
|
|
self.config[gwkey] = network['gateway']
|
|
|
|
else:
|
|
|
|
self.config.pop(gwkey, None)
|
|
|
|
ns = self.config.setdefault('nameservers', {})
|
|
|
|
if network['nameservers']:
|
|
|
|
ns.setdefault('addresses', []).extend(network['nameservers'])
|
|
|
|
if network['searchdomains']:
|
|
|
|
ns.setdefault('search', []).extend(network['search'])
|
2018-02-26 09:14:43 +00:00
|
|
|
|
2018-05-21 20:12:29 +00:00
|
|
|
|
2017-01-13 01:55:20 +00:00
|
|
|
class NetworkModel(object):
|
2018-10-29 23:12:07 +00:00
|
|
|
""" """
|
2015-07-21 15:55:02 +00:00
|
|
|
|
2018-03-20 20:35:21 +00:00
|
|
|
def __init__(self, support_wlan=True):
|
2018-06-27 02:20:53 +00:00
|
|
|
self.support_wlan = support_wlan
|
2018-10-29 23:12:07 +00:00
|
|
|
self.devices_by_name = {} # Maps interface names to NetworkDev
|
2015-07-21 15:55:02 +00:00
|
|
|
|
2017-11-14 21:30:09 +00:00
|
|
|
def parse_netplan_configs(self, netplan_root):
|
2018-10-29 23:12:07 +00:00
|
|
|
self.config = netplan.Config()
|
|
|
|
self.config.load_from_root(netplan_root)
|
|
|
|
for typ, key in ('vlan', 'vlans'), ('bond', 'bonds'):
|
|
|
|
network = self.config.config.get('network', {})
|
|
|
|
for name, config in network.get(key, {}).items():
|
|
|
|
dev = self.devices_by_name.get(name)
|
|
|
|
if dev is None:
|
|
|
|
dev = self.devices_by_name[name] = NetworkDev(
|
|
|
|
self, name, typ)
|
|
|
|
# XXX What to do if types don't match??
|
|
|
|
dev.config = config
|
2015-07-22 01:34:46 +00:00
|
|
|
|
2016-11-07 02:03:27 +00:00
|
|
|
def new_link(self, ifindex, link):
|
2018-10-29 23:12:07 +00:00
|
|
|
log.debug("new_link %s %s %s", ifindex, link.name, link.type)
|
2016-11-07 02:03:27 +00:00
|
|
|
if link.type in NETDEV_IGNORED_IFACE_TYPES:
|
|
|
|
return
|
2018-03-20 20:35:21 +00:00
|
|
|
if not self.support_wlan and link.type == "wlan":
|
|
|
|
return
|
2018-10-29 23:12:07 +00:00
|
|
|
if link.is_virtual and (
|
|
|
|
link.type not in NETDEV_ALLOWED_VIRTUAL_IFACE_TYPES):
|
2016-11-07 02:03:27 +00:00
|
|
|
return
|
2018-10-29 23:12:07 +00:00
|
|
|
dev = self.devices_by_name.get(link.name)
|
|
|
|
if dev is not None:
|
|
|
|
# XXX What to do if types don't match??
|
|
|
|
if dev.info is not None:
|
|
|
|
# This shouldn't happen! No sense getting too upset
|
|
|
|
# about if it does though.
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
dev.info = link
|
|
|
|
else:
|
|
|
|
if link.is_virtual:
|
|
|
|
# If we see a virtual device without there already
|
|
|
|
# being a config for it, we just ignore it.
|
|
|
|
return
|
|
|
|
dev = NetworkDev(self, link.name, link.type)
|
|
|
|
dev.info = link
|
|
|
|
dev.config = self.config.config_for_device(link)
|
|
|
|
log.debug("new_link %s %s with config %s",
|
|
|
|
ifindex, link.name,
|
|
|
|
sanitize_interface_config(dev.config))
|
|
|
|
self.devices_by_name[link.name] = dev
|
|
|
|
return dev
|
2016-11-07 02:03:27 +00:00
|
|
|
|
|
|
|
def update_link(self, ifindex):
|
2018-10-29 23:12:07 +00:00
|
|
|
for name, dev in self.devices_by_name.items():
|
|
|
|
if dev.ifindex == ifindex:
|
|
|
|
return dev
|
2016-11-07 02:03:27 +00:00
|
|
|
|
|
|
|
def del_link(self, ifindex):
|
2018-10-29 23:12:07 +00:00
|
|
|
for name, dev in self.devices_by_name.items():
|
|
|
|
if dev.ifindex == ifindex:
|
|
|
|
dev.info = None
|
|
|
|
if dev.is_virtual:
|
|
|
|
# We delete all virtual devices before running netplan
|
|
|
|
# apply. If a device has been deleted in the UI, we set
|
|
|
|
# dev.config to None. Now it's actually gone, forget we
|
|
|
|
# ever knew it existed.
|
|
|
|
if dev.config is None:
|
|
|
|
del self.devices_by_name[name]
|
|
|
|
else:
|
|
|
|
# If a physical interface disappears on us, it's gone.
|
|
|
|
del self.devices_by_name[name]
|
|
|
|
return dev
|
|
|
|
|
|
|
|
def new_vlan(self, device, tag):
|
|
|
|
name = "{name}.{tag}".format(name=device.name, tag=tag)
|
|
|
|
dev = self.devices_by_name[name] = NetworkDev(self, name, 'vlan')
|
|
|
|
dev.config = {
|
|
|
|
'link': device.name,
|
|
|
|
'id': tag,
|
|
|
|
}
|
|
|
|
return dev
|
2015-11-06 15:19:44 +00:00
|
|
|
|
2018-10-29 23:12:07 +00:00
|
|
|
def new_bond(self, name, interfaces, params):
|
|
|
|
dev = self.devices_by_name[name] = NetworkDev(self, name, 'bond')
|
|
|
|
dev.config = {
|
|
|
|
'interfaces': interfaces,
|
|
|
|
'parameters': params,
|
|
|
|
}
|
|
|
|
return dev
|
2015-11-06 15:19:44 +00:00
|
|
|
|
2018-10-29 23:12:07 +00:00
|
|
|
def get_all_netdevs(self, include_deleted=False):
|
|
|
|
devs = [v for k, v in sorted(self.devices_by_name.items())]
|
|
|
|
if not include_deleted:
|
|
|
|
devs = [v for v in devs if v.config is not None]
|
|
|
|
return devs
|
2016-12-20 20:42:37 +00:00
|
|
|
|
2016-11-07 00:35:42 +00:00
|
|
|
def get_netdev_by_name(self, name):
|
|
|
|
return self.devices_by_name[name]
|
2015-11-04 15:50:58 +00:00
|
|
|
|
2016-08-13 04:13:22 +00:00
|
|
|
def render(self):
|
2016-11-07 00:35:42 +00:00
|
|
|
config = {
|
|
|
|
'network': {
|
|
|
|
'version': 2,
|
|
|
|
},
|
|
|
|
}
|
2018-10-29 23:12:07 +00:00
|
|
|
type_to_key = {
|
|
|
|
'eth': 'ethernets',
|
|
|
|
'bond': 'bonds',
|
|
|
|
'wlan': 'wifis',
|
|
|
|
'vlan': 'vlans',
|
|
|
|
}
|
|
|
|
for dev in self.get_all_netdevs():
|
|
|
|
key = type_to_key[dev.type]
|
|
|
|
configs = config['network'].setdefault(key, {})
|
|
|
|
if dev.config or dev.is_used:
|
|
|
|
configs[dev.name] = dev.config
|
2016-08-16 05:09:28 +00:00
|
|
|
|
2016-08-13 04:13:22 +00:00
|
|
|
return config
|