diff --git a/subiquity/controllers/filesystem.py b/subiquity/controllers/filesystem.py
index 81542839..dfa292d8 100644
--- a/subiquity/controllers/filesystem.py
+++ b/subiquity/controllers/filesystem.py
@@ -312,7 +312,7 @@ class FilesystemController(SubiquityTuiController):
def _action_clean_level(self, level):
return raidlevels_by_value[level]
- def _answers_action(self, action):
+ async def _answers_action(self, action):
from subiquitycore.ui.stretchy import StretchyOverlay
from subiquity.ui.views.filesystem.delete import ConfirmDeleteStretchy
log.debug("_answers_action %r", action)
@@ -333,28 +333,31 @@ class FilesystemController(SubiquityTuiController):
if action.get("submit", True):
body.stretchy.done()
else:
- yield from self._enter_form_data(
- body.stretchy.form,
- action['data'],
- action.get("submit", True))
+ async for _ in self._enter_form_data(
+ body.stretchy.form,
+ action['data'],
+ action.get("submit", True)):
+ pass
elif action['action'] == 'create-raid':
self.ui.body.create_raid()
yield
body = self.ui.body._w
- yield from self._enter_form_data(
- body.stretchy.form,
- action['data'],
- action.get("submit", True),
- clean_suffix='raid')
+ async for _ in self._enter_form_data(
+ body.stretchy.form,
+ action['data'],
+ action.get("submit", True),
+ clean_suffix='raid'):
+ pass
elif action['action'] == 'create-vg':
self.ui.body.create_vg()
yield
body = self.ui.body._w
- yield from self._enter_form_data(
- body.stretchy.form,
- action['data'],
- action.get("submit", True),
- clean_suffix='vg')
+ async for _ in self._enter_form_data(
+ body.stretchy.form,
+ action['data'],
+ action.get("submit", True),
+ clean_suffix='vg'):
+ pass
elif action['action'] == 'done':
if not self.ui.body.done.enabled:
raise Exception("answers did not provide complete fs config")
@@ -367,7 +370,8 @@ class FilesystemController(SubiquityTuiController):
if self.answers['guided']:
self.finish(self.app.confirm_install())
if self.answers['manual']:
- self._run_iterator(self._run_actions(self.answers['manual']))
+ self.app.aio_loop.create_task(
+ self._run_actions(self.answers['manual']))
self.answers['manual'] = []
def guided(self):
diff --git a/subiquity/controllers/network.py b/subiquity/controllers/network.py
index 7c470320..cdc17eab 100644
--- a/subiquity/controllers/network.py
+++ b/subiquity/controllers/network.py
@@ -171,10 +171,6 @@ class NetworkController(NetworkController, SubiquityTuiController):
if not self.interactive():
raise
- def run_answers(self):
- # handled elsewhere
- pass
-
def done(self):
self.configured()
super().done()
diff --git a/subiquitycore/controllers/network.py b/subiquitycore/controllers/network.py
index 681edb81..77f09702 100644
--- a/subiquitycore/controllers/network.py
+++ b/subiquitycore/controllers/network.py
@@ -13,6 +13,7 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+import abc
import asyncio
import logging
import os
@@ -34,6 +35,7 @@ from subiquitycore.models.network import (
WLANConfig,
)
from subiquitycore import netplan
+from subiquitycore.controller import BaseController
from subiquitycore.tuicontroller import TuiController
from subiquitycore.ui.stretchy import StretchyOverlay
from subiquitycore.ui.views.network import (
@@ -122,15 +124,13 @@ network:
'''
-class NetworkController(TuiController):
+class BaseNetworkController(BaseController):
model_name = "network"
root = "/"
def __init__(self, app):
super().__init__(app)
- self.view = None
- self.view_shown = False
self.apply_config_task = SingleInstanceTask(self._apply_config)
if self.opts.dry_run:
self.root = os.path.abspath(".subiquity")
@@ -150,28 +150,6 @@ class NetworkController(TuiController):
def parse_netplan_configs(self):
self.model.parse_netplan_configs(self.root)
- def update_default_routes(self, routes):
- if routes:
- self.signal.emit_signal('network-change')
- if self.view:
- self.view.update_default_routes(routes)
-
- def new_link(self, netdev):
- if self.view is not None:
- self.view.new_link(netdev.netdev_info())
-
- 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()
- if self.view is not None:
- self.view.update_link(netdev.netdev_info())
-
- def del_link(self, netdev):
- if self.view is not None:
- self.view.del_link(netdev.netdev_info())
-
def start(self):
self._observer_handles = []
self.observer, self._observer_fds = (
@@ -204,72 +182,6 @@ class NetworkController(TuiController):
return
self.observer.data_ready(fd)
- def _action_get(self, id):
- dev_spec = id[0].split()
- dev = None
- if dev_spec[0] == "interface":
- if dev_spec[1] == "index":
- dev = self.model.get_all_netdevs()[int(dev_spec[2])]
- elif dev_spec[1] == "name":
- dev = self.model.get_netdev_by_name(dev_spec[2])
- if dev is None:
- raise Exception("could not resolve {}".format(id))
- if len(id) > 1:
- part, index = id[1].split()
- if part == "part":
- return dev.partitions()[int(index)]
- else:
- return dev
- raise Exception("could not resolve {}".format(id))
-
- def _action_clean_interfaces(self, devices):
- r = [self._action_get(device).name for device in devices]
- log.debug("%s", r)
- return r
-
- def _answers_action(self, action):
- log.debug("_answers_action %r", action)
- if 'obj' in action:
- obj = self._action_get(action['obj']).netdev_info()
- meth = getattr(
- self.ui.body,
- "_action_{}".format(action['action']))
- action_obj = getattr(NetDevAction, action['action'])
- table = self.ui.body.dev_name_to_table[obj.name]
- self.ui.body._action(None, (action_obj, meth), table)
- yield
- body = self.ui.body._w
- 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"
- yield from self._enter_form_data(
- getattr(body.stretchy, form_name),
- v,
- action.get(submit_key, True))
- 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')
- yield from self._enter_form_data(
- body.stretchy.form,
- data,
- action.get("submit", True))
- elif action['action'] == 'done':
- self.ui.body.done()
- else:
- raise Exception("could not process action {}".format(action))
-
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
@@ -368,13 +280,13 @@ class NetworkController(TuiController):
self._write_config()
- if not silent and self.view:
- self.view.show_apply_spinner()
+ if not silent:
+ self.apply_starting()
try:
def error(stage):
- if not silent and self.view:
- self.view.show_network_error(stage)
+ if not silent:
+ self.apply_error(stage)
if self.opts.dry_run:
delay = 1/self.app.scale_factor
@@ -423,15 +335,8 @@ class NetworkController(TuiController):
['systemctl', 'start', 'systemd-networkd.socket'],
check=False)
finally:
- if not silent and self.view:
- self.view.hide_apply_spinner()
-
- if self.answers.get('accept-default', False):
- self.done()
- elif self.answers.get('actions', False):
- actions = self.answers['actions']
- self.answers.clear()
- self._run_iterator(self._run_actions(actions))
+ if not silent:
+ self.apply_stopping()
if not dhcp_events:
return
@@ -449,32 +354,6 @@ class NetworkController(TuiController):
dev.set_dhcp_state(v, DHCPState.TIMED_OUT)
self.network_event_receiver.update_link(dev.ifindex)
- 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
- self.view.update_default_routes(
- self.network_event_receiver.default_routes)
- return self.view
-
- def end_ui(self):
- self.view = None
-
- def done(self):
- log.debug("NetworkController.done next_screen")
- self.model.has_network = bool(
- self.network_event_receiver.default_routes)
- self.app.next_screen()
-
- def cancel(self):
- self.app.prev_screen()
-
def set_static_config(self, dev_name: str, ip_version: int,
static_config: StaticConfig) -> None:
dev = self.model.get_netdev_by_name(dev_name)
@@ -575,3 +454,195 @@ class NetworkController(TuiController):
device = self.model.get_netdev_by_name(dev_name)
self.observer.trigger_scan(device.ifindex)
self.update_link(device)
+
+ @abc.abstractmethod
+ def apply_starting(self):
+ pass
+
+ @abc.abstractmethod
+ def apply_stopping(self):
+ pass
+
+ @abc.abstractmethod
+ def apply_error(self, stage):
+ pass
+
+ @abc.abstractmethod
+ def update_default_routes(self, routes):
+ if routes:
+ self.signal.emit_signal('network-change')
+
+ @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:
+
+ def run_answers(self):
+ if self.answers.get('accept-default', False):
+ self.done()
+ elif self.answers.get('actions', False):
+ actions = self.answers['actions']
+ self.answers.clear()
+ self.app.aio_loop.create_task(
+ self._run_actions(actions))
+
+ 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))
+
+
+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
+ self.view.update_default_routes(
+ self.network_event_receiver.default_routes)
+ return self.view
+
+ def end_ui(self):
+ self.view = None
+
+ def done(self):
+ log.debug("NetworkController.done next_screen")
+ self.model.has_network = bool(
+ self.network_event_receiver.default_routes)
+ self.app.next_screen()
+
+ def cancel(self):
+ self.app.prev_screen()
+
+ def apply_starting(self):
+ super().apply_starting()
+ if self.view is not None:
+ self.view.show_apply_spinner()
+
+ def apply_stopping(self):
+ super().apply_stopping()
+ if self.view is not None:
+ self.view.hide_apply_spinner()
+
+ def apply_error(self, stage):
+ super().apply_error(stage)
+ if self.view is not None:
+ self.view.show_network_error(stage)
+
+ def update_default_routes(self, routes):
+ super().update_default_routes(routes)
+ if self.view:
+ self.view.update_default_routes(routes)
+
+ def new_link(self, netdev):
+ super().new_link(netdev)
+ if self.view is not None:
+ self.view.new_link(netdev.netdev_info())
+
+ def update_link(self, netdev):
+ super().update_link(netdev)
+ if self.view is not None:
+ self.view.update_link(netdev.netdev_info())
+
+ def del_link(self, netdev):
+ super().del_link(netdev)
+ if self.view is not None:
+ self.view.del_link(netdev.netdev_info())
diff --git a/subiquitycore/tuicontroller.py b/subiquitycore/tuicontroller.py
index 40b59f60..a5fce566 100644
--- a/subiquitycore/tuicontroller.py
+++ b/subiquitycore/tuicontroller.py
@@ -14,6 +14,7 @@
# along with this program. If not, see .
from abc import abstractmethod
+import asyncio
import logging
from subiquitycore.controller import BaseController
@@ -60,7 +61,7 @@ class TuiController(BaseController):
# Stuff for fine grained actions, used by filesystem and network
# controller at time of writing this comment.
- def _enter_form_data(self, form, data, submit, clean_suffix=''):
+ async def _enter_form_data(self, form, data, submit, clean_suffix=''):
for k, v in data.items():
c = getattr(
self, '_action_clean_{}_{}'.format(k, clean_suffix), None)
@@ -81,18 +82,12 @@ class TuiController(BaseController):
raise Exception("answers left form invalid!")
form._click_done(None)
- def _run_actions(self, actions):
+ async def _run_actions(self, actions):
+ delay = 0.2/self.app.scale_factor
for action in actions:
- yield from self._answers_action(action)
-
- def _run_iterator(self, it, delay=None):
- if delay is None:
- delay = 0.2/self.app.scale_factor
- try:
- next(it)
- except StopIteration:
- return
- self.app.aio_loop.call_later(delay, self._run_iterator, it, delay/1.1)
+ async for _ in self._answers_action(action):
+ await asyncio.sleep(delay)
+ delay /= 1.1
class RepeatedController(BaseController):