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):