2015-08-18 16:29:56 +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/>.
|
|
|
|
|
2020-10-11 23:05:56 +00:00
|
|
|
import abc
|
2019-12-05 03:25:38 +00:00
|
|
|
import asyncio
|
2023-02-08 16:21:44 +00:00
|
|
|
import contextlib
|
2015-10-22 21:12:59 +00:00
|
|
|
import logging
|
2016-08-19 03:33:22 +00:00
|
|
|
import os
|
2018-07-06 13:50:42 +00:00
|
|
|
import subprocess
|
2020-09-03 10:52:31 +00:00
|
|
|
from typing import Optional
|
2016-08-13 02:31:31 +00:00
|
|
|
|
2023-02-15 01:17:33 +00:00
|
|
|
import pyroute2
|
2016-08-13 02:31:31 +00:00
|
|
|
import yaml
|
|
|
|
|
2017-09-28 15:51:32 +00:00
|
|
|
from probert.network import IFF_UP, NetworkEventReceiver
|
2016-11-07 02:03:27 +00:00
|
|
|
|
2019-12-12 03:03:58 +00:00
|
|
|
from subiquitycore.async_helpers import SingleInstanceTask
|
2020-05-04 04:29:47 +00:00
|
|
|
from subiquitycore.context import with_context
|
2019-12-12 03:03:58 +00:00
|
|
|
from subiquitycore.file_util import write_file
|
2019-12-19 11:38:54 +00:00
|
|
|
from subiquitycore.models.network import (
|
2020-08-28 02:10:51 +00:00
|
|
|
BondConfig,
|
|
|
|
DHCPState,
|
2019-12-19 11:38:54 +00:00
|
|
|
NetDevAction,
|
2020-08-28 02:10:51 +00:00
|
|
|
StaticConfig,
|
|
|
|
WLANConfig,
|
2019-12-19 11:38:54 +00:00
|
|
|
)
|
2019-12-12 03:03:58 +00:00
|
|
|
from subiquitycore import netplan
|
2020-10-11 23:05:56 +00:00
|
|
|
from subiquitycore.controller import BaseController
|
2021-09-08 23:33:15 +00:00
|
|
|
from subiquitycore.pubsub import CoreChannels
|
2020-07-31 00:14:51 +00:00
|
|
|
from subiquitycore.tuicontroller import TuiController
|
2020-05-18 23:45:01 +00:00
|
|
|
from subiquitycore.ui.stretchy import StretchyOverlay
|
2019-03-07 01:09:36 +00:00
|
|
|
from subiquitycore.ui.views.network import (
|
|
|
|
NetworkView,
|
|
|
|
)
|
2019-12-12 20:39:13 +00:00
|
|
|
from subiquitycore.utils import (
|
|
|
|
arun_command,
|
|
|
|
run_command,
|
|
|
|
)
|
2019-12-05 03:25:38 +00:00
|
|
|
|
2017-09-28 15:51:32 +00:00
|
|
|
|
2022-02-25 10:31:06 +00:00
|
|
|
log = logging.getLogger("subiquitycore.controllers.network")
|
2016-08-30 02:08:42 +00:00
|
|
|
|
|
|
|
|
2017-11-15 01:27:24 +00:00
|
|
|
class SubiquityNetworkEventReceiver(NetworkEventReceiver):
|
2020-08-28 02:10:51 +00:00
|
|
|
def __init__(self, controller):
|
|
|
|
self.controller = controller
|
|
|
|
self.model = controller.model
|
2023-02-15 01:17:33 +00:00
|
|
|
self.has_default_route = False
|
2016-11-07 02:03:27 +00:00
|
|
|
|
|
|
|
def new_link(self, ifindex, link):
|
2018-07-10 01:25:05 +00:00
|
|
|
netdev = self.model.new_link(ifindex, link)
|
2020-08-28 02:10:51 +00:00
|
|
|
if netdev is not None:
|
|
|
|
self.controller.new_link(netdev)
|
2016-11-07 02:03:27 +00:00
|
|
|
|
|
|
|
def del_link(self, ifindex):
|
2018-07-10 01:25:05 +00:00
|
|
|
netdev = self.model.del_link(ifindex)
|
2023-02-15 01:17:33 +00:00
|
|
|
self.probe_default_routes()
|
|
|
|
self.controller.update_has_default_route(self.has_default_route)
|
2020-08-28 02:10:51 +00:00
|
|
|
if netdev is not None:
|
|
|
|
self.controller.del_link(netdev)
|
2016-11-07 02:03:27 +00:00
|
|
|
|
|
|
|
def update_link(self, ifindex):
|
2018-07-10 01:25:05 +00:00
|
|
|
netdev = self.model.update_link(ifindex)
|
2019-07-25 02:35:49 +00:00
|
|
|
if netdev is None:
|
|
|
|
return
|
2020-03-19 20:01:30 +00:00
|
|
|
flags = getattr(netdev.info, "flags", 0)
|
2023-02-15 01:17:33 +00:00
|
|
|
if not (flags & IFF_UP):
|
|
|
|
self.probe_default_routes()
|
|
|
|
self.controller.update_has_default_route(self.has_default_route)
|
2020-08-28 02:10:51 +00:00
|
|
|
self.controller.update_link(netdev)
|
2016-11-07 02:03:27 +00:00
|
|
|
|
|
|
|
def route_change(self, action, data):
|
2017-09-28 19:23:07 +00:00
|
|
|
super().route_change(action, data)
|
2017-11-26 20:54:11 +00:00
|
|
|
if data['dst'] != 'default':
|
2016-11-07 02:03:27 +00:00
|
|
|
return
|
|
|
|
if data['table'] != 254:
|
|
|
|
return
|
2023-02-15 01:17:33 +00:00
|
|
|
self.probe_default_routes()
|
|
|
|
self.controller.update_has_default_route(self.has_default_route)
|
|
|
|
|
|
|
|
def _default_route_exists(self, routes):
|
2023-02-21 17:56:54 +00:00
|
|
|
for route in routes:
|
|
|
|
if int(route['table']) != 254:
|
|
|
|
continue
|
|
|
|
if route['dst']:
|
|
|
|
continue
|
|
|
|
if int(route['priority']) >= 20000:
|
|
|
|
# network manager probes routes by creating one at 20000 +
|
|
|
|
# the real metric, but those aren't necessarily valid.
|
|
|
|
continue
|
|
|
|
return True
|
|
|
|
return False
|
2023-02-15 01:17:33 +00:00
|
|
|
|
|
|
|
def probe_default_routes(self):
|
|
|
|
with pyroute2.NDB() as ndb:
|
|
|
|
self.has_default_route = self._default_route_exists(ndb.routes)
|
|
|
|
log.debug('default routes %s', self.has_default_route)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def create(controller: BaseController, dry_run: bool) \
|
|
|
|
-> "SubiquityNetworkEventReceiver":
|
|
|
|
if dry_run:
|
|
|
|
return DryRunSubiquityNetworkEventReceiver(controller)
|
|
|
|
else:
|
|
|
|
return SubiquityNetworkEventReceiver(controller)
|
|
|
|
|
|
|
|
|
|
|
|
class DryRunSubiquityNetworkEventReceiver(SubiquityNetworkEventReceiver):
|
|
|
|
def probe_default_routes(self):
|
|
|
|
self.has_default_route = True
|
|
|
|
log.debug('dryrun default routes %s', self.has_default_route)
|
2016-11-07 02:03:27 +00:00
|
|
|
|
2016-10-25 22:02:23 +00:00
|
|
|
|
2016-11-07 00:35:42 +00:00
|
|
|
default_netplan = '''
|
|
|
|
network:
|
|
|
|
version: 2
|
|
|
|
ethernets:
|
2020-04-30 08:25:14 +00:00
|
|
|
"all-en":
|
|
|
|
match:
|
|
|
|
name: "en*"
|
2016-11-07 00:35:42 +00:00
|
|
|
addresses:
|
|
|
|
- 10.0.2.15/24
|
|
|
|
nameservers:
|
|
|
|
addresses:
|
|
|
|
- 8.8.8.8
|
|
|
|
- 8.4.8.4
|
|
|
|
search:
|
|
|
|
- foo
|
|
|
|
- bar
|
2022-09-06 20:28:00 +00:00
|
|
|
routes:
|
|
|
|
- to: default
|
|
|
|
via: 10.0.2.2
|
2020-04-30 08:25:14 +00:00
|
|
|
"all-eth":
|
|
|
|
match:
|
|
|
|
name: "eth*"
|
2016-11-07 00:35:42 +00:00
|
|
|
dhcp4: true
|
2016-11-08 23:21:06 +00:00
|
|
|
wifis:
|
2020-04-30 08:56:09 +00:00
|
|
|
"wlsp4":
|
2016-11-08 23:21:06 +00:00
|
|
|
dhcp4: true
|
|
|
|
access-points:
|
|
|
|
"some-ap":
|
|
|
|
password: password
|
2016-11-07 00:35:42 +00:00
|
|
|
'''
|
|
|
|
|
2018-05-21 20:51:58 +00:00
|
|
|
|
2020-10-11 23:05:56 +00:00
|
|
|
class BaseNetworkController(BaseController):
|
2016-09-27 03:26:04 +00:00
|
|
|
|
2019-12-17 02:05:17 +00:00
|
|
|
model_name = "network"
|
2016-11-07 00:35:42 +00:00
|
|
|
root = "/"
|
|
|
|
|
2019-08-06 02:11:57 +00:00
|
|
|
def __init__(self, app):
|
|
|
|
super().__init__(app)
|
2019-12-11 02:24:48 +00:00
|
|
|
self.apply_config_task = SingleInstanceTask(self._apply_config)
|
2016-11-07 00:35:42 +00:00
|
|
|
if self.opts.dry_run:
|
2022-01-25 21:10:40 +00:00
|
|
|
self.root = os.path.abspath(self.opts.output_base)
|
2017-03-16 09:52:05 +00:00
|
|
|
netplan_path = self.netplan_path
|
|
|
|
netplan_dir = os.path.dirname(netplan_path)
|
|
|
|
if os.path.exists(netplan_dir):
|
|
|
|
import shutil
|
|
|
|
shutil.rmtree(netplan_dir)
|
2017-03-20 01:53:10 +00:00
|
|
|
os.makedirs(netplan_dir)
|
2017-03-16 09:52:05 +00:00
|
|
|
with open(netplan_path, 'w') as fp:
|
2016-11-07 00:35:42 +00:00
|
|
|
fp.write(default_netplan)
|
2020-04-07 00:16:53 +00:00
|
|
|
self.parse_netplan_configs()
|
2016-11-07 02:03:27 +00:00
|
|
|
|
2019-12-12 20:39:13 +00:00
|
|
|
self._watching = False
|
2023-02-15 01:17:33 +00:00
|
|
|
self.network_event_receiver = \
|
|
|
|
SubiquityNetworkEventReceiver.create(self, self.opts.dry_run)
|
2019-03-07 02:31:39 +00:00
|
|
|
|
2020-04-07 00:16:53 +00:00
|
|
|
def parse_netplan_configs(self):
|
|
|
|
self.model.parse_netplan_configs(self.root)
|
|
|
|
|
2019-03-07 02:31:39 +00:00
|
|
|
def start(self):
|
2018-07-10 21:41:56 +00:00
|
|
|
self._observer_handles = []
|
|
|
|
self.observer, self._observer_fds = (
|
2019-08-06 02:41:58 +00:00
|
|
|
self.app.prober.probe_network(self.network_event_receiver))
|
2018-07-10 21:41:56 +00:00
|
|
|
self.start_watching()
|
|
|
|
|
|
|
|
def stop_watching(self):
|
2019-12-12 20:39:13 +00:00
|
|
|
if not self._watching:
|
|
|
|
return
|
2022-10-07 16:13:30 +00:00
|
|
|
loop = asyncio.get_running_loop()
|
2019-12-12 20:39:13 +00:00
|
|
|
for fd in self._observer_fds:
|
|
|
|
loop.remove_reader(fd)
|
|
|
|
self._watching = False
|
2018-07-10 21:41:56 +00:00
|
|
|
|
|
|
|
def start_watching(self):
|
2019-12-12 20:39:13 +00:00
|
|
|
if self._watching:
|
2018-07-10 21:41:56 +00:00
|
|
|
return
|
2022-10-07 16:13:30 +00:00
|
|
|
loop = asyncio.get_running_loop()
|
2019-12-12 20:39:13 +00:00
|
|
|
for fd in self._observer_fds:
|
|
|
|
loop.add_reader(fd, self._data_ready, fd)
|
|
|
|
self._watching = True
|
2017-11-15 01:27:24 +00:00
|
|
|
|
|
|
|
def _data_ready(self, fd):
|
2018-05-18 01:11:15 +00:00
|
|
|
cp = run_command(['udevadm', 'settle', '-t', '0'])
|
|
|
|
if cp.returncode != 0:
|
2017-11-15 01:27:24 +00:00
|
|
|
log.debug("waiting 0.1 to let udev event queue settle")
|
2018-07-10 21:41:56 +00:00
|
|
|
self.stop_watching()
|
2022-10-07 16:13:30 +00:00
|
|
|
loop = asyncio.get_running_loop()
|
2019-12-12 20:39:13 +00:00
|
|
|
loop.call_later(0.1, self.start_watching)
|
2017-11-15 01:27:24 +00:00
|
|
|
return
|
|
|
|
self.observer.data_ready(fd)
|
2016-11-07 02:03:27 +00:00
|
|
|
|
2019-03-24 22:30:26 +00:00
|
|
|
def update_initial_configs(self):
|
|
|
|
# Any device that does not have a (global) address by the time
|
|
|
|
# we get to the network screen is marked as disabled, with an
|
|
|
|
# explanation.
|
2019-04-03 23:00:49 +00:00
|
|
|
log.debug("updating initial NIC config")
|
2019-03-24 22:30:26 +00:00
|
|
|
for dev in self.model.get_all_netdevs():
|
|
|
|
has_global_address = False
|
2019-03-25 22:32:02 +00:00
|
|
|
if dev.info is None or not dev.config:
|
2019-03-24 22:30:26 +00:00
|
|
|
continue
|
|
|
|
for a in dev.info.addresses.values():
|
|
|
|
if a.scope == "global":
|
|
|
|
has_global_address = True
|
|
|
|
break
|
|
|
|
if not has_global_address:
|
|
|
|
dev.remove_ip_networks_for_version(4)
|
|
|
|
dev.remove_ip_networks_for_version(6)
|
|
|
|
log.debug("disabling %s", dev.name)
|
|
|
|
dev.disabled_reason = _("autoconfiguration failed")
|
|
|
|
|
2017-03-16 09:52:05 +00:00
|
|
|
@property
|
|
|
|
def netplan_path(self):
|
|
|
|
if self.opts.project == "subiquity":
|
|
|
|
netplan_config_file_name = '00-installer-config.yaml'
|
|
|
|
else:
|
|
|
|
netplan_config_file_name = '00-snapd-config.yaml'
|
|
|
|
return os.path.join(self.root, 'etc/netplan', netplan_config_file_name)
|
|
|
|
|
2020-05-04 04:29:47 +00:00
|
|
|
def apply_config(self, context=None, silent=False):
|
2020-05-05 04:14:11 +00:00
|
|
|
self.apply_config_task.start_sync(context=context, silent=silent)
|
2019-12-12 20:39:13 +00:00
|
|
|
|
2019-12-11 09:43:44 +00:00
|
|
|
async def _down_devs(self, devs):
|
|
|
|
for dev in devs:
|
|
|
|
try:
|
|
|
|
log.debug('downing %s', dev.name)
|
|
|
|
self.observer.rtlistener.unset_link_flags(dev.ifindex, IFF_UP)
|
|
|
|
except RuntimeError:
|
|
|
|
# We don't actually care very much about this
|
|
|
|
log.exception('unset_link_flags failed for %s', dev.name)
|
|
|
|
|
|
|
|
async def _delete_devs(self, devs):
|
|
|
|
for dev in devs:
|
|
|
|
# XXX would be nicer to do this via rtlistener eventually.
|
|
|
|
log.debug('deleting %s', dev.name)
|
|
|
|
cmd = ['ip', 'link', 'delete', 'dev', dev.name]
|
|
|
|
try:
|
|
|
|
await arun_command(cmd, check=True)
|
|
|
|
except subprocess.CalledProcessError as cp:
|
|
|
|
log.info("deleting %s failed with %r", dev.name, cp.stderr)
|
2019-03-24 22:33:07 +00:00
|
|
|
|
2019-12-11 09:43:44 +00:00
|
|
|
def _write_config(self):
|
2020-05-01 02:45:39 +00:00
|
|
|
config = self.model.render_config()
|
2018-10-29 23:12:07 +00:00
|
|
|
|
2019-12-11 09:43:44 +00:00
|
|
|
log.debug("network config: \n%s",
|
2020-05-18 19:43:52 +00:00
|
|
|
yaml.dump(
|
|
|
|
netplan.sanitize_config(config),
|
|
|
|
default_flow_style=False))
|
2019-12-11 09:43:44 +00:00
|
|
|
|
|
|
|
for p in netplan.configs_in_root(self.root, masked=True):
|
|
|
|
if p == self.netplan_path:
|
|
|
|
continue
|
|
|
|
os.rename(p, p + ".dist-" + self.opts.project)
|
|
|
|
|
2022-02-09 18:33:02 +00:00
|
|
|
write_file(self.netplan_path, self.model.stringify_config(config))
|
2019-12-11 09:43:44 +00:00
|
|
|
|
2020-04-07 00:16:53 +00:00
|
|
|
self.parse_netplan_configs()
|
2019-12-11 09:43:44 +00:00
|
|
|
|
2020-05-04 04:29:47 +00:00
|
|
|
@with_context(
|
|
|
|
name="apply_config", description="silent={silent}", level="INFO")
|
2020-05-05 04:14:11 +00:00
|
|
|
async def _apply_config(self, *, context, silent):
|
2020-05-04 04:29:47 +00:00
|
|
|
devs_to_delete = []
|
|
|
|
devs_to_down = []
|
|
|
|
dhcp_device_versions = []
|
|
|
|
dhcp_events = set()
|
|
|
|
for dev in self.model.get_all_netdevs(include_deleted=True):
|
|
|
|
dev.dhcp_events = {}
|
|
|
|
for v in 4, 6:
|
|
|
|
if dev.dhcp_enabled(v):
|
|
|
|
if not silent:
|
2020-08-28 02:10:51 +00:00
|
|
|
dev.set_dhcp_state(v, DHCPState.PENDING)
|
2020-05-04 04:29:47 +00:00
|
|
|
self.network_event_receiver.update_link(
|
|
|
|
dev.ifindex)
|
2020-03-26 21:26:34 +00:00
|
|
|
else:
|
2020-08-28 02:10:51 +00:00
|
|
|
dev.set_dhcp_state(v, DHCPState.RECONFIGURE)
|
2020-05-04 04:29:47 +00:00
|
|
|
dev.dhcp_events[v] = e = asyncio.Event()
|
|
|
|
dhcp_events.add(e)
|
|
|
|
if dev.info is None:
|
|
|
|
continue
|
|
|
|
if dev.config != self.model.config.config_for_device(dev.info):
|
|
|
|
if dev.is_virtual:
|
|
|
|
devs_to_delete.append(dev)
|
|
|
|
else:
|
|
|
|
devs_to_down.append(dev)
|
2019-03-24 22:03:42 +00:00
|
|
|
|
2020-05-04 04:29:47 +00:00
|
|
|
self._write_config()
|
2019-12-05 03:25:38 +00:00
|
|
|
|
2020-10-11 22:47:48 +00:00
|
|
|
if not silent:
|
|
|
|
self.apply_starting()
|
2019-12-19 10:17:05 +00:00
|
|
|
|
2020-05-04 04:29:47 +00:00
|
|
|
try:
|
|
|
|
def error(stage):
|
2020-10-11 22:47:48 +00:00
|
|
|
if not silent:
|
|
|
|
self.apply_error(stage)
|
2020-05-04 04:29:47 +00:00
|
|
|
|
|
|
|
if self.opts.dry_run:
|
|
|
|
delay = 1/self.app.scale_factor
|
|
|
|
await arun_command(['sleep', str(delay)])
|
|
|
|
if os.path.exists('/lib/netplan/generate'):
|
|
|
|
# If netplan appears to be installed, run generate to
|
|
|
|
# at least test that what we wrote is acceptable to
|
2023-02-08 16:21:44 +00:00
|
|
|
# netplan but clear the SNAP environment variable to
|
|
|
|
# avoid that netplan thinks its running in a snap and
|
|
|
|
# tries to call netplan over the system bus.
|
|
|
|
env = os.environ.copy()
|
|
|
|
with contextlib.suppress(KeyError):
|
|
|
|
del env['SNAP']
|
2020-05-04 04:29:47 +00:00
|
|
|
await arun_command(
|
|
|
|
['netplan', 'generate', '--root', self.root],
|
2023-02-08 16:21:44 +00:00
|
|
|
check=True, env=env)
|
2020-05-04 04:29:47 +00:00
|
|
|
else:
|
|
|
|
if devs_to_down or devs_to_delete:
|
|
|
|
try:
|
2019-12-19 10:17:05 +00:00
|
|
|
await arun_command(
|
2020-05-04 04:29:47 +00:00
|
|
|
['systemctl', 'mask', '--runtime',
|
|
|
|
'systemd-networkd.service',
|
|
|
|
'systemd-networkd.socket'],
|
2020-04-02 23:14:57 +00:00
|
|
|
check=True)
|
|
|
|
await arun_command(
|
2020-05-04 04:29:47 +00:00
|
|
|
['systemctl', 'stop',
|
2020-04-02 23:14:57 +00:00
|
|
|
'systemd-networkd.service',
|
|
|
|
'systemd-networkd.socket'],
|
2019-12-19 10:17:05 +00:00
|
|
|
check=True)
|
|
|
|
except subprocess.CalledProcessError:
|
2020-05-04 04:29:47 +00:00
|
|
|
error("stop-networkd")
|
2019-12-19 10:17:05 +00:00
|
|
|
raise
|
2020-05-04 04:29:47 +00:00
|
|
|
if devs_to_down:
|
|
|
|
await self._down_devs(devs_to_down)
|
|
|
|
if devs_to_delete:
|
|
|
|
await self._delete_devs(devs_to_delete)
|
|
|
|
if devs_to_down or devs_to_delete:
|
|
|
|
await arun_command(
|
|
|
|
['systemctl', 'unmask', '--runtime',
|
|
|
|
'systemd-networkd.service',
|
|
|
|
'systemd-networkd.socket'],
|
|
|
|
check=True)
|
|
|
|
try:
|
|
|
|
await arun_command(['netplan', 'apply'], check=True)
|
|
|
|
except subprocess.CalledProcessError:
|
|
|
|
error("apply")
|
|
|
|
raise
|
|
|
|
if devs_to_down or devs_to_delete:
|
|
|
|
# It's probably running already, but just in case.
|
|
|
|
await arun_command(
|
|
|
|
['systemctl', 'start', 'systemd-networkd.socket'],
|
|
|
|
check=False)
|
|
|
|
finally:
|
2020-10-11 22:47:48 +00:00
|
|
|
if not silent:
|
|
|
|
self.apply_stopping()
|
2019-12-19 10:17:05 +00:00
|
|
|
|
2020-05-04 04:29:47 +00:00
|
|
|
if not dhcp_events:
|
|
|
|
return
|
2019-12-19 10:17:05 +00:00
|
|
|
|
2020-05-04 04:29:47 +00:00
|
|
|
try:
|
|
|
|
await asyncio.wait_for(
|
|
|
|
asyncio.wait({e.wait() for e in dhcp_events}),
|
|
|
|
10)
|
|
|
|
except asyncio.TimeoutError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
for dev, v in dhcp_device_versions:
|
|
|
|
dev.dhcp_events = {}
|
|
|
|
if not dev.dhcp_addresses()[v]:
|
2020-08-28 02:10:51 +00:00
|
|
|
dev.set_dhcp_state(v, DHCPState.TIMED_OUT)
|
2020-05-04 04:29:47 +00:00
|
|
|
self.network_event_receiver.update_link(dev.ifindex)
|
2016-08-22 04:59:09 +00:00
|
|
|
|
2020-09-03 10:52:31 +00:00
|
|
|
def set_static_config(self, dev_name: str, ip_version: int,
|
2020-08-28 02:10:51 +00:00
|
|
|
static_config: StaticConfig) -> None:
|
2020-09-03 10:52:31 +00:00
|
|
|
dev = self.model.get_netdev_by_name(dev_name)
|
2020-08-28 02:10:51 +00:00
|
|
|
dev.remove_ip_networks_for_version(ip_version)
|
|
|
|
dev.config.setdefault('addresses', []).extend(static_config.addresses)
|
|
|
|
if static_config.gateway:
|
2022-09-06 20:28:00 +00:00
|
|
|
dev.config['routes'] = [{
|
|
|
|
'to': 'default',
|
|
|
|
'via': static_config.gateway
|
|
|
|
}]
|
2020-08-28 02:10:51 +00:00
|
|
|
else:
|
2022-09-06 20:28:00 +00:00
|
|
|
dev.remove_routes(ip_version)
|
2020-08-28 02:10:51 +00:00
|
|
|
ns = dev.config.setdefault('nameservers', {})
|
|
|
|
ns.setdefault('addresses', []).extend(static_config.nameservers)
|
|
|
|
ns.setdefault('search', []).extend(static_config.searchdomains)
|
2020-09-03 10:52:31 +00:00
|
|
|
self.update_link(dev)
|
2020-08-28 02:10:51 +00:00
|
|
|
self.apply_config()
|
|
|
|
|
2020-09-03 10:52:31 +00:00
|
|
|
def enable_dhcp(self, dev_name: str, ip_version: int) -> None:
|
|
|
|
dev = self.model.get_netdev_by_name(dev_name)
|
2020-08-28 02:10:51 +00:00
|
|
|
dev.remove_ip_networks_for_version(ip_version)
|
|
|
|
dhcpkey = 'dhcp{v}'.format(v=ip_version)
|
|
|
|
dev.config[dhcpkey] = True
|
2020-09-03 10:52:31 +00:00
|
|
|
self.update_link(dev)
|
2020-08-28 02:10:51 +00:00
|
|
|
self.apply_config()
|
|
|
|
|
2020-09-03 10:52:31 +00:00
|
|
|
def disable_network(self, dev_name: str, ip_version: int) -> None:
|
|
|
|
dev = self.model.get_netdev_by_name(dev_name)
|
2020-08-28 02:10:51 +00:00
|
|
|
dev.remove_ip_networks_for_version(ip_version)
|
2020-09-03 10:52:31 +00:00
|
|
|
self.update_link(dev)
|
2020-08-28 02:10:51 +00:00
|
|
|
self.apply_config()
|
|
|
|
|
2020-09-03 10:52:31 +00:00
|
|
|
def add_vlan(self, dev_name: str, id: int):
|
|
|
|
new = self.model.new_vlan(dev_name, id)
|
|
|
|
self.new_link(new)
|
|
|
|
dev = self.model.get_netdev_by_name(dev_name)
|
2020-08-28 02:10:51 +00:00
|
|
|
self.update_link(dev)
|
|
|
|
self.apply_config()
|
|
|
|
|
2020-09-03 10:52:31 +00:00
|
|
|
def delete_link(self, dev_name: str):
|
|
|
|
dev = self.model.get_netdev_by_name(dev_name)
|
2020-08-28 02:10:51 +00:00
|
|
|
touched_devices = set()
|
2020-09-03 10:52:31 +00:00
|
|
|
if dev.type == "bond":
|
|
|
|
for device_name in dev.config['interfaces']:
|
2020-08-28 02:10:51 +00:00
|
|
|
interface = self.model.get_netdev_by_name(device_name)
|
|
|
|
touched_devices.add(interface)
|
2020-09-03 10:52:31 +00:00
|
|
|
elif dev.type == "vlan":
|
|
|
|
link = self.model.get_netdev_by_name(dev.config['link'])
|
2020-08-28 02:10:51 +00:00
|
|
|
touched_devices.add(link)
|
2020-09-03 10:52:31 +00:00
|
|
|
dev.config = None
|
|
|
|
self.del_link(dev)
|
2020-08-28 02:10:51 +00:00
|
|
|
for dev in touched_devices:
|
|
|
|
self.update_link(dev)
|
|
|
|
self.apply_config()
|
|
|
|
|
2020-09-03 10:52:31 +00:00
|
|
|
def add_or_update_bond(self, existing_name: Optional[str],
|
|
|
|
new_name: str, new_info: BondConfig) -> None:
|
2020-08-28 02:10:51 +00:00
|
|
|
get_netdev_by_name = self.model.get_netdev_by_name
|
|
|
|
touched_devices = set()
|
|
|
|
for device_name in new_info.interfaces:
|
|
|
|
device = get_netdev_by_name(device_name)
|
2019-03-24 22:03:42 +00:00
|
|
|
device.config = {}
|
2020-08-28 02:10:51 +00:00
|
|
|
touched_devices.add(device)
|
|
|
|
if existing_name is None:
|
|
|
|
new_dev = self.model.new_bond(new_name, new_info)
|
|
|
|
self.new_link(new_dev)
|
|
|
|
else:
|
|
|
|
existing = get_netdev_by_name(existing_name)
|
|
|
|
for interface in existing.config['interfaces']:
|
|
|
|
touched_devices.add(get_netdev_by_name(interface))
|
|
|
|
existing.config.update(new_info.to_config())
|
|
|
|
if existing.name != new_name:
|
|
|
|
config = existing.config
|
|
|
|
existing.config = None
|
|
|
|
self.del_link(existing)
|
|
|
|
existing.config = config
|
|
|
|
existing.name = new_name
|
|
|
|
self.new_link(existing)
|
|
|
|
else:
|
|
|
|
touched_devices.add(existing)
|
|
|
|
for dev in touched_devices:
|
|
|
|
self.update_link(dev)
|
2020-09-03 10:52:31 +00:00
|
|
|
self.apply_config()
|
2020-08-28 02:10:51 +00:00
|
|
|
|
2022-03-14 20:35:18 +00:00
|
|
|
async def get_info_for_netdev(self, dev_name: str) -> str:
|
2020-09-03 10:52:31 +00:00
|
|
|
device = self.model.get_netdev_by_name(dev_name)
|
2020-08-28 02:10:51 +00:00
|
|
|
if device.info is not None:
|
|
|
|
return yaml.dump(
|
|
|
|
device.info.serialize(), default_flow_style=False)
|
2019-03-24 22:03:42 +00:00
|
|
|
else:
|
2020-08-28 02:10:51 +00:00
|
|
|
return "Configured but not yet created {type} interface.".format(
|
|
|
|
type=device.type)
|
|
|
|
|
2020-09-03 10:52:31 +00:00
|
|
|
def set_wlan(self, dev_name: str, wlan: WLANConfig) -> None:
|
|
|
|
device = self.model.get_netdev_by_name(dev_name)
|
2021-06-09 03:28:06 +00:00
|
|
|
cur_ssid, cur_psk = device.configured_ssid
|
|
|
|
if wlan.ssid and not cur_ssid:
|
2021-06-02 00:28:55 +00:00
|
|
|
# Turn DHCP4 on by default when specifying an SSID for
|
|
|
|
# the first time...
|
|
|
|
device.config['dhcp4'] = True
|
2020-08-28 02:10:51 +00:00
|
|
|
device.set_ssid_psk(wlan.ssid, wlan.psk)
|
|
|
|
self.update_link(device)
|
2021-06-02 00:26:29 +00:00
|
|
|
self.apply_config()
|
2020-08-28 02:10:51 +00:00
|
|
|
|
2020-09-03 10:52:31 +00:00
|
|
|
def start_scan(self, dev_name: str) -> None:
|
|
|
|
device = self.model.get_netdev_by_name(dev_name)
|
2021-06-02 00:17:24 +00:00
|
|
|
try:
|
|
|
|
self.observer.trigger_scan(device.ifindex)
|
|
|
|
except RuntimeError as r:
|
|
|
|
device.info.wlan['scan_state'] = 'error %s' % (r,)
|
2020-08-28 02:10:51 +00:00
|
|
|
self.update_link(device)
|
2020-10-11 22:47:48 +00:00
|
|
|
|
2020-10-11 23:05:56 +00:00
|
|
|
@abc.abstractmethod
|
|
|
|
def apply_starting(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
def apply_stopping(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
def apply_error(self, stage):
|
|
|
|
pass
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
2023-02-15 01:17:33 +00:00
|
|
|
def update_has_default_route(self, has_default_route):
|
|
|
|
if has_default_route:
|
2021-09-08 23:33:15 +00:00
|
|
|
self.app.hub.broadcast(CoreChannels.NETWORK_UP)
|
2020-10-11 23:05:56 +00:00
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
def new_link(self, netdev):
|
|
|
|
pass
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
def update_link(self, netdev):
|
|
|
|
for v, e in netdev.dhcp_events.items():
|
|
|
|
if netdev.dhcp_addresses()[v]:
|
|
|
|
netdev.set_dhcp_state(v, DHCPState.CONFIGURED)
|
|
|
|
e.set()
|
|
|
|
pass
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
|
|
|
def del_link(self, netdev):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class NetworkAnswersMixin:
|
2020-10-11 22:47:48 +00:00
|
|
|
|
2020-12-15 21:13:04 +00:00
|
|
|
async def run_answers(self):
|
2020-10-11 22:58:08 +00:00
|
|
|
if self.answers.get('accept-default', False):
|
|
|
|
self.done()
|
|
|
|
elif self.answers.get('actions', False):
|
|
|
|
actions = self.answers['actions']
|
|
|
|
self.answers.clear()
|
2020-12-15 21:13:04 +00:00
|
|
|
await self._run_actions(actions)
|
2020-10-11 22:58:08 +00:00
|
|
|
|
2020-10-11 22:47:48 +00:00
|
|
|
def _action_get(self, id):
|
|
|
|
dev_spec = id[0].split()
|
|
|
|
if dev_spec[0] == "interface":
|
|
|
|
if dev_spec[1] == "index":
|
|
|
|
name = self.view.cur_netdev_names[int(dev_spec[2])]
|
|
|
|
elif dev_spec[1] == "name":
|
|
|
|
name = dev_spec[2]
|
|
|
|
return self.view.dev_name_to_table[name]
|
|
|
|
raise Exception("could not resolve {}".format(id))
|
|
|
|
|
|
|
|
def _action_clean_interfaces(self, devices):
|
|
|
|
r = [self._action_get(device).dev_info.name for device in devices]
|
|
|
|
log.debug("%s", r)
|
|
|
|
return r
|
|
|
|
|
|
|
|
async def _answers_action(self, action):
|
|
|
|
log.debug("_answers_action %r", action)
|
|
|
|
if 'obj' in action:
|
|
|
|
table = self._action_get(action['obj'])
|
|
|
|
meth = getattr(
|
|
|
|
self.ui.body,
|
|
|
|
"_action_{}".format(action['action']))
|
|
|
|
action_obj = getattr(NetDevAction, action['action'])
|
|
|
|
self.ui.body._action(None, (action_obj, meth), table)
|
|
|
|
yield
|
|
|
|
body = self.ui.body._w
|
|
|
|
if action['action'] == "DELETE":
|
|
|
|
t = 0.0
|
|
|
|
while table.dev_info.name in self.view.cur_netdev_names:
|
|
|
|
await asyncio.sleep(0.1)
|
|
|
|
t += 0.1
|
|
|
|
if t > 5.0:
|
|
|
|
raise Exception(
|
|
|
|
"interface did not disappear in 5 secs")
|
|
|
|
log.debug("waited %s for interface to disappear", t)
|
|
|
|
if not isinstance(body, StretchyOverlay):
|
|
|
|
return
|
|
|
|
for k, v in action.items():
|
|
|
|
if not k.endswith('data'):
|
|
|
|
continue
|
|
|
|
form_name = "form"
|
|
|
|
submit_key = "submit"
|
|
|
|
if '-' in k:
|
|
|
|
prefix = k.split('-')[0]
|
|
|
|
form_name = prefix + "_form"
|
|
|
|
submit_key = prefix + "-submit"
|
|
|
|
async for _ in self._enter_form_data(
|
|
|
|
getattr(body.stretchy, form_name),
|
|
|
|
v,
|
|
|
|
action.get(submit_key, True)):
|
|
|
|
pass
|
|
|
|
elif action['action'] == 'create-bond':
|
|
|
|
self.ui.body._create_bond()
|
|
|
|
yield
|
|
|
|
body = self.ui.body._w
|
|
|
|
data = action['data'].copy()
|
|
|
|
if 'devices' in data:
|
|
|
|
data['interfaces'] = data.pop('devices')
|
|
|
|
async for _ in self._enter_form_data(
|
|
|
|
body.stretchy.form,
|
|
|
|
data,
|
|
|
|
action.get("submit", True)):
|
|
|
|
pass
|
|
|
|
t = 0.0
|
|
|
|
while data['name'] not in self.view.cur_netdev_names:
|
|
|
|
await asyncio.sleep(0.1)
|
|
|
|
t += 0.1
|
|
|
|
if t > 5.0:
|
|
|
|
raise Exception("bond did not appear in 5 secs")
|
|
|
|
if t > 0:
|
|
|
|
log.debug("waited %s for bond to appear", t)
|
|
|
|
yield
|
|
|
|
elif action['action'] == 'done':
|
|
|
|
self.ui.body.done()
|
|
|
|
else:
|
|
|
|
raise Exception("could not process action {}".format(action))
|
|
|
|
|
2020-10-11 23:05:56 +00:00
|
|
|
|
|
|
|
class NetworkController(BaseNetworkController, TuiController,
|
|
|
|
NetworkAnswersMixin):
|
|
|
|
|
|
|
|
def __init__(self, app):
|
|
|
|
super().__init__(app)
|
|
|
|
self.view = None
|
|
|
|
self.view_shown = False
|
|
|
|
|
|
|
|
def make_ui(self):
|
|
|
|
if not self.view_shown:
|
|
|
|
self.update_initial_configs()
|
|
|
|
netdev_infos = [
|
|
|
|
dev.netdev_info() for dev in self.model.get_all_netdevs()
|
|
|
|
]
|
|
|
|
self.view = NetworkView(self, netdev_infos)
|
|
|
|
if not self.view_shown:
|
|
|
|
self.apply_config(silent=True)
|
|
|
|
self.view_shown = True
|
2023-02-15 01:17:33 +00:00
|
|
|
self.view.update_has_default_route(
|
|
|
|
self.network_event_receiver.has_default_route)
|
2020-10-11 23:05:56 +00:00
|
|
|
return self.view
|
|
|
|
|
|
|
|
def end_ui(self):
|
|
|
|
self.view = None
|
|
|
|
|
|
|
|
def done(self):
|
|
|
|
log.debug("NetworkController.done next_screen")
|
2023-02-15 01:17:33 +00:00
|
|
|
self.model.has_network = self.network_event_receiver.has_default_route
|
2020-10-11 23:05:56 +00:00
|
|
|
self.app.next_screen()
|
|
|
|
|
|
|
|
def cancel(self):
|
|
|
|
self.app.prev_screen()
|
|
|
|
|
2020-10-11 22:47:48 +00:00
|
|
|
def apply_starting(self):
|
2020-10-11 23:05:56 +00:00
|
|
|
super().apply_starting()
|
2020-10-11 22:47:48 +00:00
|
|
|
if self.view is not None:
|
|
|
|
self.view.show_apply_spinner()
|
|
|
|
|
|
|
|
def apply_stopping(self):
|
2020-10-11 23:05:56 +00:00
|
|
|
super().apply_stopping()
|
2020-10-11 22:47:48 +00:00
|
|
|
if self.view is not None:
|
|
|
|
self.view.hide_apply_spinner()
|
|
|
|
|
|
|
|
def apply_error(self, stage):
|
2020-10-11 23:05:56 +00:00
|
|
|
super().apply_error(stage)
|
2020-10-11 22:47:48 +00:00
|
|
|
if self.view is not None:
|
|
|
|
self.view.show_network_error(stage)
|
|
|
|
|
2023-02-15 01:17:33 +00:00
|
|
|
def update_has_default_route(self, has_default_route):
|
|
|
|
super().update_has_default_route(has_default_route)
|
2020-10-11 22:47:48 +00:00
|
|
|
if self.view:
|
2023-02-15 01:17:33 +00:00
|
|
|
self.view.update_has_default_route(has_default_route)
|
2020-10-11 22:47:48 +00:00
|
|
|
|
|
|
|
def new_link(self, netdev):
|
2020-10-11 23:05:56 +00:00
|
|
|
super().new_link(netdev)
|
2020-10-11 22:47:48 +00:00
|
|
|
if self.view is not None:
|
|
|
|
self.view.new_link(netdev.netdev_info())
|
|
|
|
|
|
|
|
def update_link(self, netdev):
|
2020-10-11 23:05:56 +00:00
|
|
|
super().update_link(netdev)
|
2020-10-11 22:47:48 +00:00
|
|
|
if self.view is not None:
|
|
|
|
self.view.update_link(netdev.netdev_info())
|
|
|
|
|
|
|
|
def del_link(self, netdev):
|
2020-10-11 23:05:56 +00:00
|
|
|
super().del_link(netdev)
|
2020-10-11 22:47:48 +00:00
|
|
|
if self.view is not None:
|
|
|
|
self.view.del_link(netdev.netdev_info())
|