2015-06-24 19:44:31 +00:00
|
|
|
# Copyright 2015 Canonical, Ltd.
|
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
2024-04-28 21:28:40 +00:00
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, version 3.
|
2015-06-24 19:44:31 +00:00
|
|
|
#
|
|
|
|
# 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
|
2024-04-28 21:28:40 +00:00
|
|
|
# GNU General Public License for more details.
|
2015-06-24 19:44:31 +00:00
|
|
|
#
|
2024-04-28 21:28:40 +00:00
|
|
|
# You should have received a copy of the GNU General Public License
|
2015-06-24 19:44:31 +00:00
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
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
|
2020-04-01 06:37:02 +00:00
|
|
|
from gettext import pgettext
|
2019-03-24 20:25:53 +00:00
|
|
|
from socket import AF_INET, AF_INET6
|
2022-04-05 07:58:45 +00:00
|
|
|
from typing import Dict, List, Optional
|
2016-09-16 02:28:40 +00:00
|
|
|
|
2018-05-24 21:06:09 +00:00
|
|
|
import attr
|
|
|
|
import yaml
|
2016-09-16 02:28:40 +00:00
|
|
|
|
2018-05-24 21:06:09 +00:00
|
|
|
from subiquitycore import netplan
|
2015-06-24 19:44:31 +00:00
|
|
|
|
2020-08-06 06:26:06 +00:00
|
|
|
NETDEV_IGNORED_IFACE_TYPES = [
|
|
|
|
"lo",
|
|
|
|
"bridge",
|
|
|
|
"tun",
|
|
|
|
"tap",
|
|
|
|
"dummy",
|
|
|
|
"sit",
|
|
|
|
"can",
|
|
|
|
"???",
|
|
|
|
]
|
2018-10-29 23:12:07 +00:00
|
|
|
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):
|
2020-05-08 02:10:02 +00:00
|
|
|
# Information about a network interface
|
2020-05-08 03:40:59 +00:00
|
|
|
INFO = pgettext("NetDevAction", "Info")
|
|
|
|
EDIT_WLAN = pgettext("NetDevAction", "Edit Wifi")
|
|
|
|
EDIT_IPV4 = pgettext("NetDevAction", "Edit IPv4")
|
|
|
|
EDIT_IPV6 = pgettext("NetDevAction", "Edit IPv6")
|
|
|
|
EDIT_BOND = pgettext("NetDevAction", "Edit bond")
|
|
|
|
ADD_VLAN = pgettext("NetDevAction", "Add a VLAN tag")
|
|
|
|
DELETE = pgettext("NetDevAction", "Delete")
|
|
|
|
|
|
|
|
def str(self):
|
|
|
|
return pgettext(type(self).__name__, self.value)
|
2018-07-10 00:30:01 +00:00
|
|
|
|
|
|
|
|
2020-08-28 02:10:51 +00:00
|
|
|
class DHCPState(enum.Enum):
|
|
|
|
PENDING = enum.auto()
|
|
|
|
TIMED_OUT = enum.auto()
|
|
|
|
RECONFIGURE = enum.auto()
|
|
|
|
CONFIGURED = enum.auto()
|
|
|
|
|
|
|
|
|
|
|
|
@attr.s(auto_attribs=True)
|
|
|
|
class DHCPStatus:
|
|
|
|
enabled: bool
|
|
|
|
state: Optional[DHCPState]
|
|
|
|
addresses: List[str]
|
|
|
|
|
|
|
|
|
|
|
|
@attr.s(auto_attribs=True)
|
|
|
|
class StaticConfig:
|
|
|
|
addresses: List[str] = attr.Factory(list)
|
|
|
|
gateway: Optional[str] = None
|
|
|
|
nameservers: List[str] = attr.Factory(list)
|
|
|
|
searchdomains: List[str] = attr.Factory(list)
|
|
|
|
|
|
|
|
|
|
|
|
@attr.s(auto_attribs=True)
|
|
|
|
class VLANConfig:
|
|
|
|
id: int
|
|
|
|
link: str
|
|
|
|
|
|
|
|
|
|
|
|
@attr.s(auto_attribs=True)
|
|
|
|
class WLANConfig:
|
2021-06-02 00:13:05 +00:00
|
|
|
ssid: Optional[str]
|
|
|
|
psk: Optional[str]
|
2020-08-28 02:10:51 +00:00
|
|
|
|
|
|
|
|
|
|
|
@attr.s(auto_attribs=True)
|
|
|
|
class WLANStatus:
|
|
|
|
config: WLANConfig
|
|
|
|
scan_state: Optional[str]
|
|
|
|
visible_ssids: List[str]
|
|
|
|
|
|
|
|
|
|
|
|
@attr.s(auto_attribs=True)
|
|
|
|
class BondConfig:
|
|
|
|
interfaces: List[str]
|
|
|
|
mode: str
|
|
|
|
xmit_hash_policy: Optional[str] = None
|
|
|
|
lacp_rate: Optional[str] = None
|
|
|
|
|
|
|
|
def to_config(self):
|
|
|
|
mode = self.mode
|
|
|
|
params = {
|
|
|
|
"mode": self.mode,
|
|
|
|
}
|
|
|
|
if mode in BondParameters.supports_xmit_hash_policy:
|
|
|
|
params["transmit-hash-policy"] = self.xmit_hash_policy
|
|
|
|
if mode in BondParameters.supports_lacp_rate:
|
|
|
|
params["lacp-rate"] = self.lacp_rate
|
|
|
|
return {
|
|
|
|
"interfaces": self.interfaces,
|
|
|
|
"parameters": params,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@attr.s(auto_attribs=True)
|
|
|
|
class NetDevInfo:
|
|
|
|
"""All the information about a NetworkDev that the view code needs."""
|
2023-07-25 21:26:25 +00:00
|
|
|
|
2020-08-28 02:10:51 +00:00
|
|
|
name: str
|
|
|
|
type: str
|
|
|
|
|
|
|
|
is_connected: bool
|
|
|
|
bond_master: Optional[str]
|
|
|
|
is_used: bool
|
|
|
|
disabled_reason: Optional[str]
|
|
|
|
hwaddr: Optional[str]
|
|
|
|
vendor: Optional[str]
|
|
|
|
model: Optional[str]
|
|
|
|
is_virtual: bool
|
|
|
|
has_config: bool
|
|
|
|
|
|
|
|
vlan: Optional[VLANConfig]
|
|
|
|
bond: Optional[BondConfig]
|
2021-06-02 00:13:05 +00:00
|
|
|
wlan: Optional[WLANStatus]
|
2020-08-28 02:10:51 +00:00
|
|
|
|
|
|
|
dhcp4: DHCPStatus
|
|
|
|
dhcp6: DHCPStatus
|
|
|
|
static4: StaticConfig
|
|
|
|
static6: StaticConfig
|
|
|
|
|
|
|
|
enabled_actions: List[NetDevAction]
|
|
|
|
|
|
|
|
|
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",
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2023-09-06 12:42:53 +00:00
|
|
|
class NetworkDev:
|
2018-10-29 23:12:07 +00:00
|
|
|
def __init__(self, model, name, typ):
|
|
|
|
self._model = model
|
|
|
|
self._name = name
|
|
|
|
self.type = typ
|
|
|
|
self.config = {}
|
2023-09-08 20:39:42 +00:00
|
|
|
|
|
|
|
# import done here to break a chain where anybody importing
|
|
|
|
# subiquity.common.types has to have probert
|
|
|
|
from probert.network import Link
|
|
|
|
|
2023-09-06 12:42:53 +00:00
|
|
|
# Devices that have been configured in Subiquity but do not (yet) exist
|
|
|
|
# on the system have their "info" field set to None. Once they exist,
|
|
|
|
# probert should pass on the information through a call to new_link().
|
2023-09-08 20:39:42 +00:00
|
|
|
self.info: Optional[Link] = None
|
2019-03-24 22:30:26 +00:00
|
|
|
self.disabled_reason = None
|
2019-12-11 03:02:31 +00:00
|
|
|
self.dhcp_events = {}
|
2019-03-24 22:33:07 +00:00
|
|
|
self._dhcp_state = {
|
|
|
|
4: None,
|
|
|
|
6: None,
|
|
|
|
}
|
2016-12-12 02:34:59 +00:00
|
|
|
|
2020-08-28 02:10:51 +00:00
|
|
|
def netdev_info(self) -> NetDevInfo:
|
|
|
|
if self.type == "eth":
|
network: fix crash when Wi-Fi or eth interface gets removed from the system
When a network interface is disconnected from the system (e.g.,
physically removed if it's a USB adapter), probert asynchronously calls
the del_link() method.
Upon receiving this notification, Subiquity server wants to send an
update to the Subiquity clients. The update contains information about
the interface that disappeared - which is obtained through a call to
netdev_info.
Unfortunately, for Wi-Fi and Ethernet interfaces, netdev_info
dereferences the NetworkDev.info variable. Interfaces that no longer
exist on the system (and also interfaces that do not yet exist), have
their "info" variable set to None - so an exception is raised when
dereferencing it.
Wi-Fi interface:
File "subiquitycore/models/network.py", line 227, in netdev_info
scan_state=self.info.wlan['scan_state'],
AttributeError: 'NoneType' object has no attribute 'wlan'
Ethernet interface:
File "subiquitycore/models/network.py", line 201, in netdev_info
is_connected = bool(self.info.is_connected)
AttributeError: 'NoneType' object has no attribute 'is_connected'
Fixed by making sure netdev_info does not raise if the dev.info variable
is None. This is a valid use-case.
Signed-off-by: Olivier Gayot <olivier.gayot@canonical.com>
2023-09-06 15:06:35 +00:00
|
|
|
if self.info is not None:
|
|
|
|
is_connected = bool(self.info.is_connected)
|
|
|
|
else:
|
|
|
|
# If the device has just disappeared, let's pretend it's not
|
|
|
|
# connected.
|
|
|
|
is_connected = False
|
2020-08-28 02:10:51 +00:00
|
|
|
else:
|
|
|
|
is_connected = True
|
|
|
|
bond_master = None
|
|
|
|
for dev2 in self._model.get_all_netdevs():
|
|
|
|
if dev2.type != "bond":
|
|
|
|
continue
|
|
|
|
if self.name in dev2.config.get("interfaces", []):
|
|
|
|
bond_master = dev2.name
|
|
|
|
break
|
2022-04-05 08:08:52 +00:00
|
|
|
bond: Optional[BondConfig] = None
|
2020-08-28 02:10:51 +00:00
|
|
|
if self.type == "bond" and self.config is not None:
|
|
|
|
params = self.config["parameters"]
|
|
|
|
bond = BondConfig(
|
|
|
|
interfaces=self.config["interfaces"],
|
|
|
|
mode=params["mode"],
|
2024-02-15 23:12:20 +00:00
|
|
|
xmit_hash_policy=params.get("transmit-hash-policy"),
|
2020-08-28 02:10:51 +00:00
|
|
|
lacp_rate=params.get("lacp-rate"),
|
|
|
|
)
|
2022-04-05 08:08:52 +00:00
|
|
|
vlan: Optional[VLANConfig] = None
|
2020-08-28 02:10:51 +00:00
|
|
|
if self.type == "vlan" and self.config is not None:
|
|
|
|
vlan = VLANConfig(id=self.config["id"], link=self.config["link"])
|
2022-04-05 08:08:52 +00:00
|
|
|
wlan: Optional[WLANStatus] = None
|
2020-08-28 02:10:51 +00:00
|
|
|
if self.type == "wlan":
|
|
|
|
ssid, psk = self.configured_ssid
|
network: fix crash when Wi-Fi or eth interface gets removed from the system
When a network interface is disconnected from the system (e.g.,
physically removed if it's a USB adapter), probert asynchronously calls
the del_link() method.
Upon receiving this notification, Subiquity server wants to send an
update to the Subiquity clients. The update contains information about
the interface that disappeared - which is obtained through a call to
netdev_info.
Unfortunately, for Wi-Fi and Ethernet interfaces, netdev_info
dereferences the NetworkDev.info variable. Interfaces that no longer
exist on the system (and also interfaces that do not yet exist), have
their "info" variable set to None - so an exception is raised when
dereferencing it.
Wi-Fi interface:
File "subiquitycore/models/network.py", line 227, in netdev_info
scan_state=self.info.wlan['scan_state'],
AttributeError: 'NoneType' object has no attribute 'wlan'
Ethernet interface:
File "subiquitycore/models/network.py", line 201, in netdev_info
is_connected = bool(self.info.is_connected)
AttributeError: 'NoneType' object has no attribute 'is_connected'
Fixed by making sure netdev_info does not raise if the dev.info variable
is None. This is a valid use-case.
Signed-off-by: Olivier Gayot <olivier.gayot@canonical.com>
2023-09-06 15:06:35 +00:00
|
|
|
# If the device has just disappeared, let's pretend it's not
|
|
|
|
# scanning and has no visible SSID.
|
|
|
|
scan_state = None
|
|
|
|
visible_ssids: List[str] = []
|
|
|
|
if self.info is not None:
|
|
|
|
scan_state = self.info.wlan["scan_state"]
|
|
|
|
visible_ssids = self.info.wlan["visible_ssids"]
|
2020-08-28 02:10:51 +00:00
|
|
|
wlan = WLANStatus(
|
|
|
|
config=WLANConfig(ssid=ssid, psk=psk),
|
network: fix crash when Wi-Fi or eth interface gets removed from the system
When a network interface is disconnected from the system (e.g.,
physically removed if it's a USB adapter), probert asynchronously calls
the del_link() method.
Upon receiving this notification, Subiquity server wants to send an
update to the Subiquity clients. The update contains information about
the interface that disappeared - which is obtained through a call to
netdev_info.
Unfortunately, for Wi-Fi and Ethernet interfaces, netdev_info
dereferences the NetworkDev.info variable. Interfaces that no longer
exist on the system (and also interfaces that do not yet exist), have
their "info" variable set to None - so an exception is raised when
dereferencing it.
Wi-Fi interface:
File "subiquitycore/models/network.py", line 227, in netdev_info
scan_state=self.info.wlan['scan_state'],
AttributeError: 'NoneType' object has no attribute 'wlan'
Ethernet interface:
File "subiquitycore/models/network.py", line 201, in netdev_info
is_connected = bool(self.info.is_connected)
AttributeError: 'NoneType' object has no attribute 'is_connected'
Fixed by making sure netdev_info does not raise if the dev.info variable
is None. This is a valid use-case.
Signed-off-by: Olivier Gayot <olivier.gayot@canonical.com>
2023-09-06 15:06:35 +00:00
|
|
|
scan_state=scan_state,
|
|
|
|
visible_ssids=visible_ssids,
|
2020-08-28 02:10:51 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
dhcp_addresses = self.dhcp_addresses()
|
2022-04-05 07:58:45 +00:00
|
|
|
configured_addresses: Dict[int, List[str]] = {4: [], 6: []}
|
2020-08-28 02:10:51 +00:00
|
|
|
if self.config is not None:
|
|
|
|
for addr in self.config.get("addresses", []):
|
2022-04-05 07:52:06 +00:00
|
|
|
configured_addresses[addr_version(addr)].append(addr)
|
2020-08-28 02:10:51 +00:00
|
|
|
ns = self.config.get("nameservers", {})
|
|
|
|
else:
|
|
|
|
ns = {}
|
|
|
|
dhcp_statuses = {}
|
|
|
|
static_configs = {}
|
|
|
|
for v in 4, 6:
|
|
|
|
dhcp_statuses[v] = DHCPStatus(
|
|
|
|
enabled=self.dhcp_enabled(v),
|
|
|
|
state=self._dhcp_state[v],
|
|
|
|
addresses=dhcp_addresses[v],
|
|
|
|
)
|
|
|
|
if self.config is not None:
|
|
|
|
gateway = self.config.get("gateway" + str(v))
|
|
|
|
else:
|
|
|
|
gateway = None
|
|
|
|
static_configs[v] = StaticConfig(
|
2022-04-05 07:52:06 +00:00
|
|
|
addresses=configured_addresses[v],
|
2020-08-28 02:10:51 +00:00
|
|
|
gateway=gateway,
|
|
|
|
nameservers=ns.get("nameservers", []),
|
|
|
|
searchdomains=ns.get("search", []),
|
|
|
|
)
|
|
|
|
return NetDevInfo(
|
|
|
|
name=self.name,
|
|
|
|
type=self.type,
|
|
|
|
is_connected=is_connected,
|
|
|
|
vlan=vlan,
|
|
|
|
bond_master=bond_master,
|
|
|
|
bond=bond,
|
|
|
|
wlan=wlan,
|
|
|
|
dhcp4=dhcp_statuses[4],
|
|
|
|
dhcp6=dhcp_statuses[6],
|
|
|
|
static4=static_configs[4],
|
|
|
|
static6=static_configs[6],
|
|
|
|
is_used=self.is_used,
|
|
|
|
disabled_reason=self.disabled_reason,
|
|
|
|
enabled_actions=[
|
|
|
|
action for action in NetDevAction if self.supports_action(action)
|
|
|
|
],
|
|
|
|
hwaddr=getattr(self.info, "hwaddr", None),
|
|
|
|
vendor=getattr(self.info, "vendor", None),
|
|
|
|
model=getattr(self.info, "model", None),
|
|
|
|
is_virtual=self.is_virtual,
|
|
|
|
has_config=self.config is not None,
|
|
|
|
)
|
|
|
|
|
2019-03-24 20:25:53 +00:00
|
|
|
def dhcp_addresses(self):
|
|
|
|
r = {4: [], 6: []}
|
|
|
|
if self.info is not None:
|
|
|
|
for a in self.info.addresses.values():
|
|
|
|
if a.family == AF_INET:
|
|
|
|
v = 4
|
|
|
|
elif a.family == AF_INET6:
|
|
|
|
v = 6
|
|
|
|
else:
|
|
|
|
continue
|
|
|
|
if a.source == "dhcp":
|
|
|
|
r[v].append(str(a.address))
|
|
|
|
return r
|
|
|
|
|
|
|
|
def dhcp_enabled(self, version):
|
2019-03-25 02:06:59 +00:00
|
|
|
if self.config is None:
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
return self.config.get("dhcp{v}".format(v=version), False)
|
2019-03-24 20:25:53 +00:00
|
|
|
|
2019-03-24 22:33:07 +00:00
|
|
|
def dhcp_state(self, version):
|
|
|
|
if not self.config.get("dhcp{v}".format(v=version), False):
|
|
|
|
return None
|
|
|
|
return self._dhcp_state[version]
|
|
|
|
|
|
|
|
def set_dhcp_state(self, version, state):
|
|
|
|
self._dhcp_state[version] = state
|
|
|
|
|
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.
|
2020-08-28 02:10:51 +00:00
|
|
|
if new_name != self.name and self.is_virtual:
|
|
|
|
if new_name in self._model.devices_by_name:
|
2018-10-29 23:12:07 +00:00
|
|
|
raise RuntimeError(
|
|
|
|
"renaming {old_name} over {new_name}".format(
|
|
|
|
old_name=self.name, new_name=new_name
|
|
|
|
)
|
2023-07-25 21:26:25 +00:00
|
|
|
)
|
2020-08-25 01:55:20 +00:00
|
|
|
self._model.devices_by_name[new_name] = self
|
2020-08-28 02:10:51 +00:00
|
|
|
if self.info is not None:
|
|
|
|
dead_device = NetworkDev(self._model, self.name, self.type)
|
|
|
|
self._model.devices_by_name[self.name] = dead_device
|
|
|
|
dead_device.config = None
|
|
|
|
dead_device.info = self.info
|
|
|
|
self.info = None
|
2018-10-29 23:12:07 +00:00
|
|
|
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
|
|
|
|
2020-05-18 10:49:58 +00:00
|
|
|
@property
|
|
|
|
def configured_ssid(self):
|
2020-05-18 18:59:47 +00:00
|
|
|
for ssid, settings in self.config.get("access-points", {}).items():
|
2020-05-18 10:49:58 +00:00
|
|
|
psk = settings.get("password")
|
|
|
|
return ssid, psk
|
|
|
|
return None, None
|
|
|
|
|
|
|
|
def set_ssid_psk(self, ssid, psk):
|
|
|
|
aps = self.config.setdefault("access-points", {})
|
|
|
|
aps.clear()
|
|
|
|
if ssid is not None:
|
|
|
|
aps[ssid] = {}
|
|
|
|
if psk is not None:
|
|
|
|
aps[ssid]["password"] = psk
|
|
|
|
|
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
|
|
|
|
2019-08-08 13:58:53 +00:00
|
|
|
@property
|
|
|
|
def actual_global_ip_addresses(self):
|
|
|
|
return [
|
|
|
|
addr.ip
|
|
|
|
for _, addr in sorted(self.info.addresses.items())
|
|
|
|
if addr.scope == "global"
|
|
|
|
]
|
|
|
|
|
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)
|
2022-09-06 20:28:00 +00:00
|
|
|
self.remove_routes(version)
|
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
|
|
|
|
2022-09-06 20:28:00 +00:00
|
|
|
def remove_routes(self, version):
|
|
|
|
routes = [
|
|
|
|
route
|
|
|
|
for route in self.config.get("routes", [])
|
|
|
|
if addr_version(route["via"]) != version
|
|
|
|
]
|
|
|
|
if routes:
|
|
|
|
self.config["routes"] = routes
|
|
|
|
else:
|
|
|
|
self.config.pop("routes", None)
|
|
|
|
|
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
|
|
|
|
2021-06-08 23:27:13 +00:00
|
|
|
def __init__(self, project):
|
2018-10-29 23:12:07 +00:00
|
|
|
self.devices_by_name = {} # Maps interface names to NetworkDev
|
2022-08-26 13:06:15 +00:00
|
|
|
self._has_network = False
|
2020-04-01 06:37:02 +00:00
|
|
|
self.project = project
|
2023-02-14 16:25:47 +00:00
|
|
|
self.force_offline = False
|
2015-07-21 15:55:02 +00:00
|
|
|
|
2022-08-26 13:06:15 +00:00
|
|
|
@property
|
|
|
|
def has_network(self):
|
2023-02-14 16:25:47 +00:00
|
|
|
return self._has_network and not self.force_offline
|
2022-08-26 13:06:15 +00:00
|
|
|
|
|
|
|
@has_network.setter
|
|
|
|
def has_network(self, val):
|
|
|
|
log.debug("has_network %s", val)
|
|
|
|
self._has_network = val
|
|
|
|
|
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)
|
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:
|
2021-06-02 00:23:18 +00:00
|
|
|
log.debug("ignoring based on type")
|
2016-11-07 02:03:27 +00:00
|
|
|
return
|
2021-06-02 00:23:18 +00:00
|
|
|
is_virtual = link.is_virtual
|
|
|
|
if link.type == "wlan":
|
|
|
|
# mac80211_hwsim nics show up as virtual but we pretend
|
|
|
|
# they are real for testing purposes.
|
|
|
|
is_virtual = False
|
|
|
|
if is_virtual and link.type not in NETDEV_ALLOWED_VIRTUAL_IFACE_TYPES:
|
|
|
|
log.debug("ignoring based on is_virtual")
|
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:
|
2020-04-02 20:17:19 +00:00
|
|
|
config = self.config.config_for_device(link)
|
2021-06-02 00:23:18 +00:00
|
|
|
if is_virtual and not config:
|
2018-10-29 23:12:07 +00:00
|
|
|
# If we see a virtual device without there already
|
|
|
|
# being a config for it, we just ignore it.
|
2021-06-02 00:23:18 +00:00
|
|
|
log.debug("ignoring virtual device with no config")
|
2018-10-29 23:12:07 +00:00
|
|
|
return
|
|
|
|
dev = NetworkDev(self, link.name, link.type)
|
|
|
|
dev.info = link
|
2020-04-02 20:17:19 +00:00
|
|
|
dev.config = config
|
2018-10-29 23:12:07 +00:00
|
|
|
log.debug(
|
|
|
|
"new_link %s %s with config %s",
|
|
|
|
ifindex,
|
|
|
|
link.name,
|
2020-05-18 19:43:52 +00:00
|
|
|
netplan.sanitize_interface_config(dev.config),
|
|
|
|
)
|
2018-10-29 23:12:07 +00:00
|
|
|
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
|
|
|
|
|
2020-08-28 02:10:51 +00:00
|
|
|
def new_vlan(self, device_name, tag):
|
|
|
|
name = "{name}.{tag}".format(name=device_name, tag=tag)
|
2018-10-29 23:12:07 +00:00
|
|
|
dev = self.devices_by_name[name] = NetworkDev(self, name, "vlan")
|
|
|
|
dev.config = {
|
2020-08-28 02:10:51 +00:00
|
|
|
"link": device_name,
|
2018-10-29 23:12:07 +00:00
|
|
|
"id": tag,
|
|
|
|
}
|
|
|
|
return dev
|
2015-11-06 15:19:44 +00:00
|
|
|
|
2020-08-28 02:10:51 +00:00
|
|
|
def new_bond(self, name, bond_config):
|
2018-10-29 23:12:07 +00:00
|
|
|
dev = self.devices_by_name[name] = NetworkDev(self, name, "bond")
|
2020-08-28 02:10:51 +00:00
|
|
|
dev.config = bond_config.to_config()
|
2018-10-29 23:12:07 +00:00
|
|
|
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
|
|
|
|
2020-04-01 06:37:02 +00:00
|
|
|
def stringify_config(self, config):
|
|
|
|
return "\n".join(
|
|
|
|
[
|
|
|
|
"# This is the network config written by '{}'".format(self.project),
|
|
|
|
yaml.dump(config, default_flow_style=False),
|
|
|
|
]
|
|
|
|
)
|
|
|
|
|
|
|
|
def render_config(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
|
2020-04-01 06:37:02 +00:00
|
|
|
|
2022-07-15 03:35:40 +00:00
|
|
|
def rendered_config_paths(self):
|
|
|
|
"""Return a list of file paths rendered by this model."""
|
|
|
|
return [
|
|
|
|
"/" + write_file["path"]
|
|
|
|
for write_file in self.render().get("write_files").values()
|
|
|
|
]
|
|
|
|
|
2020-04-01 06:37:02 +00:00
|
|
|
def render(self):
|
|
|
|
return {
|
|
|
|
"write_files": {
|
|
|
|
"etc_netplan_installer": {
|
|
|
|
"path": "etc/netplan/00-installer-config.yaml",
|
|
|
|
"content": self.stringify_config(self.render_config()),
|
|
|
|
},
|
|
|
|
"nonet": {
|
|
|
|
"path": (
|
|
|
|
"etc/cloud/cloud.cfg.d/"
|
|
|
|
"subiquity-disable-cloudinit-networking.cfg"
|
|
|
|
),
|
|
|
|
"content": "network: {config: disabled}\n",
|
|
|
|
},
|
2023-07-25 21:26:25 +00:00
|
|
|
},
|
2020-04-01 06:37:02 +00:00
|
|
|
}
|