class MultiNetdevChooser(WidgetWrap, WantsToKnowFormField): def __init__(self): self.pile = Pile([]) self.selected = set() self.box_to_device = {} super().__init__(self.pile) @property def value(self): return list(sorted(self.selected, key=lambda x: x.name)) @value.setter def value(self, value): self.selected = set(value) for checkbox, opt in self.pile.contents: checkbox.state = self.box_to_device[checkbox] in self.selected def set_bound_form_field(self, bff): contents = [] for d in bff.form.candidate_netdevs: box = CheckBox(d.name, on_state_change=self._state_change) self.box_to_device[box] = d contents.append((box, self.pile.options('pack'))) self.pile.contents[:] = contents def _state_change(self, sender, state): device = self.box_to_device[sender] if state: self.selected.add(device) else: self.selected.remove(device)
class ProgressView(BaseView): def __init__(self, controller): self.controller = controller self.listwalker = SimpleFocusListWalker([]) self.listbox = ListBox(self.listwalker) self.linebox = MyLineBox(self.listbox) body = [ ('pack', Text("")), ('weight', 1, Padding.center_79(self.linebox)), ('pack', Text("")), ] self.pile = Pile(body) super().__init__(self.pile) def add_log_tail(self, text): at_end = len( self.listwalker) == 0 or self.listbox.focus_position == len( self.listwalker) - 1 for line in text.splitlines(): self.listwalker.append(Text(line)) if at_end: self.listbox.set_focus(len(self.listwalker) - 1) self.listbox.set_focus_valign('bottom') def clear_log_tail(self): self.listwalker[:] = [] def set_status(self, text): self.linebox.set_title(text) def show_complete(self, include_exit=False): buttons = [ ok_btn(_("Reboot Now"), on_press=self.reboot), ] if include_exit: buttons.append(cancel_btn(_("Exit To Shell"), on_press=self.quit)) buttons = button_pile(buttons) new_pile = Pile([ ('pack', Text("")), buttons, ('pack', Text("")), ]) self.pile.contents[-1] = (new_pile, self.pile.options('pack')) self.pile.focus_position = len(self.pile.contents) - 1 def reboot(self, btn): self.controller.reboot() def quit(self, btn): self.controller.quit()
class NetworkView(BaseView): title = _("Network connections") excerpt = _("Configure at least one interface this server can use to talk " "to other machines, and which preferably provides sufficient " "access for updates.") footer = _("Select an interface to configure it or select Done to " "continue") def __init__(self, model, controller): self.model = model self.controller = controller self.dev_to_row = {} self.cur_netdevs = [] self.error = Text("", align='center') self.device_table = TablePile(self._build_model_inputs(), spacing=2, colspecs={ 0: ColSpec(rpad=1), 4: ColSpec(can_shrink=True, rpad=1), }) self._create_bond_btn = menu_btn(_("Create bond"), on_press=self._create_bond) bp = button_pile([self._create_bond_btn]) bp.align = 'left' self.listbox = ListBox([self.device_table] + [ bp, ]) self.bottom = Pile([ Text(""), self._build_buttons(), Text(""), ]) self.error_showing = False self.frame = Pile([('pack', Text("")), ('pack', Padding.center_79(Text(_(self.excerpt)))), ('pack', Text("")), Padding.center_90(self.listbox), ('pack', self.bottom)]) self.frame.set_focus(self.bottom) super().__init__(self.frame) def _build_buttons(self): back = back_btn(_("Back"), on_press=self.cancel) done = done_btn(_("Done"), on_press=self.done) return button_pile([done, back]) _action_INFO = _stretchy_shower(ViewInterfaceInfo) _action_EDIT_WLAN = _stretchy_shower(NetworkConfigureWLANStretchy) _action_EDIT_IPV4 = _stretchy_shower(EditNetworkStretchy, 4) _action_EDIT_IPV6 = _stretchy_shower(EditNetworkStretchy, 6) _action_EDIT_BOND = _stretchy_shower(BondStretchy) _action_ADD_VLAN = _stretchy_shower(AddVlanStretchy) def _action_DELETE(self, device): self.controller.rm_virtual_interface(device) def _action(self, sender, action, device): action, meth = action log.debug("_action %s %s", action.name, device.name) meth(device) def _cells_for_device(self, dev): notes = [] if dev.is_bond_slave: notes.append( _("enslaved to {}").format(dev._net_info.bond['master'])) for v in 4, 6: if dev.configured_ip_addresses_for_version(v): notes.extend([ "{} (static)".format(a) for a in dev.configured_ip_addresses_for_version(v) ]) elif dev.dhcp_for_version(v): if v == 4: fam = AF_INET elif v == 6: fam = AF_INET6 fam_addresses = [] for a in dev._net_info.addresses.values(): log.debug("a %s", a.serialize()) if a.family == fam and a.source == 'dhcp': fam_addresses.append("{} (from dhcp)".format( a.address)) if fam_addresses: notes.extend(fam_addresses) else: notes.append( _("DHCPv{v} has supplied no addresses").format(v=v)) if notes: notes = ", ".join(notes) else: notes = '-' return (dev.name, dev.type, notes) def new_link(self, new_dev): for i, cur_dev in enumerate(self.cur_netdevs): if cur_dev.name > new_dev.name: netdev_i = i break else: netdev_i = len(self.cur_netdevs) new_rows = self._rows_for_device(new_dev, netdev_i) self.device_table.insert_rows(3 * netdev_i + 1, new_rows) def update_link(self, dev): row = self.dev_to_row[dev] for i, text in enumerate(self._cells_for_device(dev)): row.columns[2 * (i + 1)].set_text(text) def del_link(self, dev): log.debug("del_link %s", (dev in self.cur_netdevs)) if dev in self.cur_netdevs: netdev_i = self.cur_netdevs.index(dev) self.device_table.remove_rows(3 * netdev_i, 3 * (netdev_i + 1)) del self.cur_netdevs[netdev_i] if isinstance(self._w, StretchyOverlay): stretchy = self._w.stretchy if getattr(stretchy, 'device', None) is dev: self.remove_overlay() def _rows_for_device(self, dev, netdev_i=None): if netdev_i is None: netdev_i = len(self.cur_netdevs) rows = [] name, typ, addresses = self._cells_for_device(dev) actions = [] for action in NetDevAction: meth = getattr(self, '_action_' + action.name) opens_dialog = getattr(meth, 'opens_dialog', False) if dev.supports_action(action): actions.append( (_(action.value), True, (action, meth), opens_dialog)) menu = ActionMenu(actions) connect_signal(menu, 'action', self._action, dev) row = make_action_menu_row([ Text("["), Text(name), Text(typ), Text(addresses, wrap='clip'), menu, Text("]"), ], menu) self.dev_to_row[dev] = row.base_widget self.cur_netdevs[netdev_i:netdev_i] = [dev] rows.append(row) if dev.type == "vlan": info = _("VLAN {id} on interface {link}").format( **dev._configuration) elif dev.type == "bond": info = _("bond master for {}").format(', '.join( dev._net_info.bond['slaves'])) else: info = " / ".join([dev.hwaddr, dev.vendor, dev.model]) rows.append( Color.info_minor(TableRow([Text(""), (4, Text(info)), Text("")]))) rows.append(Color.info_minor(TableRow([(4, Text(""))]))) return rows def _build_model_inputs(self): netdevs = self.model.get_all_netdevs() masters = [] for master in netdevs: if not master._net_info.bond['is_master']: continue masters.append((_("Set master to %s") % master.name, True, { 'action': 'add_master', 'master': master }, False)) rows = [] rows.append( TableRow([ Color.info_minor(Text(header)) for header in ["", "NAME", "TYPE", "NOTES / ADDRESSES", ""] ])) for dev in netdevs: rows.extend(self._rows_for_device(dev)) return rows def _create_bond(self, sender): self.show_stretchy_overlay(BondStretchy(self)) def show_network_error(self, action, info=None): self.error_showing = True self.bottom.contents[0:0] = [ (Text(""), self.bottom.options()), (Color.info_error(self.error), self.bottom.options()), ] if action == 'stop-networkd': exc = info[0] self.error.set_text("Stopping systemd-networkd-failed: %r" % (exc.stderr, )) elif action == 'apply': self.error.set_text("Network configuration could not be applied; " "please verify your settings.") elif action == 'timeout': self.error.set_text("Network configuration timed out; " "please verify your settings.") elif action == 'down': self.error.set_text("Downing network interfaces failed.") elif action == 'canceled': self.error.set_text("Network configuration canceled.") elif action == 'add-vlan': self.error.set_text("Failed to add a VLAN tag.") elif action == 'rm-dev': self.error.set_text("Failed to delete a virtual interface.") else: self.error.set_text("An unexpected error has occurred; " "please verify your settings.") def done(self, result): if self.error_showing: self.bottom.contents[0:2] = [] self.controller.network_finish(self.model.render()) def cancel(self, button=None): self.controller.cancel()
class ErrorReportStretchy(Stretchy): def __init__(self, app, report, interrupting=True): self.app = app self.report = report self.interrupting = interrupting self.btns = { 'cancel': other_btn( _("Cancel upload"), on_press=self.cancel_upload), 'close': close_btn(self, _("Close report")), 'continue': close_btn(self, _("Continue")), 'debug_shell': other_btn( _("Switch to a shell"), on_press=self.debug_shell), 'restart': other_btn( _("Restart the installer"), on_press=self.restart), 'submit': other_btn( _("Send to Canonical"), on_press=self.submit), 'submitted': disabled(other_btn(_("Sent to Canonical"))), 'view': other_btn( _("View full report"), on_press=self.view_report), } w = 0 for n, b in self.btns.items(): w = max(w, widget_width(b)) for n, b in self.btns.items(): self.btns[n] = Padding(b, width=w, align='center') self.spinner = Spinner(app.aio_loop, style='dots') self.pile = Pile([]) self._report_changed() super().__init__("", [self.pile], 0, 0) connect_signal(self, 'closed', self.spinner.stop) def pb(self, upload): pb = ProgressBar( normal='progress_incomplete', complete='progress_complete', current=upload.bytes_sent, done=upload.bytes_to_send) def _progress(): pb.done = upload.bytes_to_send pb.current = upload.bytes_sent connect_signal(upload, 'progress', _progress) return pb def _pile_elements(self): btns = self.btns.copy() widgets = [ Text(rewrap(_(error_report_intros[self.report.kind]))), Text(""), ] self.spinner.stop() if self.report.state == ErrorReportState.DONE: widgets.append(btns['view']) widgets.append(Text("")) widgets.append(Text(rewrap(_(submit_text)))) widgets.append(Text("")) if self.report.uploader: if self.upload_pb is None: self.upload_pb = self.pb(self.report.uploader) widgets.append(self.upload_pb) else: if self.report.oops_id: widgets.append(btns['submitted']) else: widgets.append(btns['submit']) self.upload_pb = None fs_label, fs_loc = self.report.persistent_details if fs_label is not None: location_text = _( "The error report has been saved to\n\n {loc}\n\non the " "filesystem with label {label!r}.").format( loc=fs_loc, label=fs_label) widgets.extend([ Text(""), Text(location_text), ]) else: text, spin = error_report_state_descriptions[self.report.state] widgets.append(Text(rewrap(_(text)))) if spin: self.spinner.start() widgets.extend([ Text(""), self.spinner]) if self.report.uploader: widgets.extend([Text(""), btns['cancel']]) elif self.interrupting: if self.report.state != ErrorReportState.INCOMPLETE: text, btn_names = error_report_options[self.report.kind] if text: widgets.extend([Text(""), Text(rewrap(_(text)))]) for b in btn_names: widgets.extend([Text(""), btns[b]]) else: widgets.extend([ Text(""), btns['close'], ]) return widgets def _report_changed(self): self.pile.contents[:] = [ (w, self.pile.options('pack')) for w in self._pile_elements()] if self.pile.selectable(): while not self.pile.focus.selectable(): self.pile.focus_position += 1 def debug_shell(self, sender): self.app.debug_shell() def restart(self, sender): self.app.restart() def view_report(self, sender): self.app.run_command_in_foreground(["less", self.report.path]) def submit(self, sender): self.report.upload() def cancel_upload(self, sender): self.report.uploader.cancelled = True self.report.uploader = None self._report_changed() def opened(self): self.report.mark_seen() connect_signal(self.report, 'changed', self._report_changed) def closed(self): disconnect_signal(self.report, 'changed', self._report_changed)
class EditNetworkStretchy(Stretchy): def __init__(self, parent, device, ip_version): self.parent = parent self.device = device self.ip_version = ip_version self.method_form = NetworkMethodForm() self.method_form.method.caption = _("IPv{v} Method: ").format( v=ip_version) manual_initial = {} cur_addresses = [] for addr in device.config.get('addresses', []): if addr_version(addr) == ip_version: cur_addresses.append(addr) if cur_addresses: method = 'manual' addr = ipaddress.ip_interface(cur_addresses[0]) ns = device.config.get('nameservers', {}) manual_initial = { 'subnet': str(addr.network), 'address': str(addr.ip), 'nameservers': ', '.join(ns.get('addresses', [])), 'searchdomains': ', '.join(ns.get('search', [])), } gw = device.config.get('gateway{v}'.format(v=ip_version)) if gw: manual_initial['gateway'] = str(gw) elif self.device.config.get('dhcp{v}'.format(v=ip_version)): method = 'dhcp' else: method = 'disable' self.method_form.method.value = method connect_signal(self.method_form.method.widget, 'select', self._select_method) log.debug("manual_initial %s", manual_initial) self.manual_form = NetworkConfigForm(ip_version, manual_initial) connect_signal(self.method_form, 'submit', self.done) connect_signal(self.manual_form, 'submit', self.done) connect_signal(self.method_form, 'cancel', self.cancel) connect_signal(self.manual_form, 'cancel', self.cancel) self.form_pile = Pile(self.method_form.as_rows()) self.bp = WidgetPlaceholder(self.method_form.buttons) self._select_method(None, method) widgets = [self.form_pile, Text(""), self.bp] super().__init__( "Edit {device} IPv{v} configuration".format(device=device.name, v=ip_version), widgets, 0, 0) def _select_method(self, sender, method): rows = [] def r(w): rows.append((w, self.form_pile.options('pack'))) for row in self.method_form.as_rows(): r(row) if method == 'manual': r(Text("")) for row in self.manual_form.as_rows(): r(row) self.bp.original_widget = self.manual_form.buttons else: self.bp.original_widget = self.method_form.buttons self.form_pile.contents[:] = rows def done(self, sender): self.device.remove_ip_networks_for_version(self.ip_version) if self.method_form.method.value == "manual": form = self.manual_form # XXX this converting from and to and from strings thing is a # bit out of hand. gateway = form.gateway.value if gateway is not None: gateway = str(gateway) result = { 'network': str(form.subnet.value), 'address': str(form.address.value), 'gateway': gateway, 'nameservers': list(map(str, form.nameservers.value)), 'searchdomains': form.searchdomains.value, } self.device.config.pop('nameservers', None) self.device.add_network(self.ip_version, result) elif self.method_form.method.value == "dhcp": self.device.config['dhcp{v}'.format(v=self.ip_version)] = True else: pass self.parent.update_link(self.device) self.parent.remove_overlay() def cancel(self, sender=None): self.parent.remove_overlay()
class EditNetworkStretchy(Stretchy): def __init__(self, parent, dev_info, ip_version): self.parent = parent self.dev_info = dev_info self.ip_version = ip_version self.method_form = NetworkMethodForm() self.method_form.method.caption = _( "IPv{v} Method: ").format(v=ip_version) manual_initial = {} dhcp_status = getattr(dev_info, 'dhcp' + str(ip_version)) static_config = getattr(dev_info, 'static' + str(ip_version)) if static_config.addresses: method = 'manual' addr = ipaddress.ip_interface(static_config.addresses[0]) manual_initial = { 'subnet': str(addr.network), 'address': str(addr.ip), 'nameservers': ', '.join(static_config.nameservers), 'searchdomains': ', '.join(static_config.searchdomains), } if static_config.gateway: manual_initial['gateway'] = static_config.gateway elif dhcp_status.enabled: method = 'dhcp' else: method = 'disable' self.method_form.method.value = method connect_signal( self.method_form.method.widget, 'select', self._select_method) log.debug("manual_initial %s", manual_initial) self.manual_form = NetworkConfigForm(ip_version, manual_initial) connect_signal(self.method_form, 'submit', self.done) connect_signal(self.manual_form, 'submit', self.done) connect_signal(self.method_form, 'cancel', self.cancel) connect_signal(self.manual_form, 'cancel', self.cancel) self.form_pile = Pile(self.method_form.as_rows()) self.bp = WidgetPlaceholder(self.method_form.buttons) self._select_method(None, method) widgets = [self.form_pile, Text(""), self.bp] super().__init__( "Edit {device} IPv{v} configuration".format( device=dev_info.name, v=ip_version), widgets, 0, 0) def _select_method(self, sender, method): rows = [] def r(w): rows.append((w, self.form_pile.options('pack'))) for row in self.method_form.as_rows(): r(row) if method == 'manual': r(Text("")) for row in self.manual_form.as_rows(): r(row) self.bp.original_widget = self.manual_form.buttons else: self.bp.original_widget = self.method_form.buttons self.form_pile.contents[:] = rows def done(self, sender): if self.method_form.method.value == "manual": form = self.manual_form # XXX this converting from and to and from strings thing is a # bit out of hand. gateway = form.gateway.value if gateway is not None: gateway = str(gateway) address = str(form.address.value).split('/')[0] address += '/' + str(form.subnet.value).split('/')[1] config = StaticConfig( addresses=[address], gateway=gateway, nameservers=list(map(str, form.nameservers.value)), searchdomains=form.searchdomains.value) log.debug( "EditNetworkStretchy %s manual config=%s", self.ip_version, config) self.parent.controller.set_static_config( self.dev_info.name, self.ip_version, config) elif self.method_form.method.value == "dhcp": self.parent.controller.enable_dhcp( self.dev_info.name, self.ip_version) log.debug("EditNetworkStretchy %s, dhcp", self.ip_version) else: self.parent.controller.disable_network( self.dev_info.name, self.ip_version) log.debug("EditNetworkStretchy %s, disabled", self.ip_version) self.parent.remove_overlay() def cancel(self, sender=None): self.parent.remove_overlay()
class NetworkView(BaseView): title = _("Network connections") excerpt = _("Configure at least one interface this server can use to talk " "to other machines, and which preferably provides sufficient " "access for updates.") footer = _("Select an interface to configure it or select Done to " "continue") def __init__(self, model, controller): self.model = model self.controller = controller self.items = [] self.error = Text("", align='center') self.additional_options = Pile(self._build_additional_options()) self.listbox = ListBox(self._build_model_inputs() + [ Padding.center_79(self.additional_options), Padding.line_break(""), ]) self.bottom = Pile([ Text(""), self._build_buttons(), Text(""), ]) self.error_showing = False self.frame = Pile([('pack', Text("")), ('pack', Padding.center_79(Text(_(self.excerpt)))), ('pack', Text("")), Padding.center_90(self.listbox), ('pack', self.bottom)]) self.frame.focus_position = 4 super().__init__(self.frame) def _build_buttons(self): back = back_btn(_("Back"), on_press=self.cancel) done = done_btn(_("Done"), on_press=self.done) return button_pile([done, back]) def _build_model_inputs(self): netdevs = self.model.get_all_netdevs() ifname_width = 8 # default padding if netdevs: ifname_width += max(map(lambda dev: len(dev.name), netdevs)) if ifname_width > 20: ifname_width = 20 iface_menus = [] # Display each interface -- name in first column, then configured IPs # in the second. log.debug('interfaces: {}'.format(netdevs)) for dev in netdevs: col_1 = [] col_2 = [] col_1.append( menu_btn(label=dev.name, on_press=self.on_net_dev_press)) if dev.type == 'wlan': col_2.extend(_build_wifi_info(dev)) if len(dev.actual_ip_addresses) == 0 and (dev.type == 'eth' and not dev.is_connected): col_2.append(Color.info_primary(Text(_("Not connected")))) col_2.extend(_build_gateway_ip_info_for_version(dev, 4)) col_2.extend(_build_gateway_ip_info_for_version(dev, 6)) # Other device info (MAC, vendor/model, speed) template = '' if dev.hwaddr: template += '{} '.format(dev.hwaddr) # TODO is this to translate? if dev.is_bond_slave: template += '(Bonded) ' # TODO to check if this is affected by translations if not dev.vendor.lower().startswith('unknown'): vendor = textwrap.wrap(dev.vendor, 15)[0] template += '{} '.format(vendor) if not dev.model.lower().startswith('unknown'): model = textwrap.wrap(dev.model, 20)[0] template += '{} '.format(model) if dev.speed: template += '({})'.format(dev.speed) col_2.append(Color.info_minor(Text(template))) iface_menus.append( Columns([(ifname_width, Pile(col_1)), Pile(col_2)], 2)) return iface_menus def refresh_model_inputs(self): widgets = self._build_model_inputs() + [ Padding.center_79(self.additional_options), Padding.line_break(""), ] self.listbox.base_widget.body[:] = widgets self.additional_options.contents = [ (obj, ('pack', None)) for obj in self._build_additional_options() ] def _build_additional_options(self): labels = [] netdevs = self.model.get_all_netdevs() # Display default route status if self.model.default_v4_gateway is not None: v4_route_source = "via " + self.model.default_v4_gateway default_v4_route_w = Color.info_minor( Text(_(" IPv4 default route %s." % v4_route_source))) labels.append(default_v4_route_w) if self.model.default_v6_gateway is not None: v6_route_source = "via " + self.model.default_v6_gateway default_v6_route_w = Color.info_minor( Text(" IPv6 default route " + v6_route_source + ".")) labels.append(default_v6_route_w) max_btn_len = 0 buttons = [] for opt, sig in self.model.get_menu(): if ':set-default-route' in sig: if len(netdevs) < 2: log.debug('Skipping default route menu option' ' (only one nic)') continue if ':bond-interfaces' in sig: not_bonded = [dev for dev in netdevs if not dev.is_bonded] if len(not_bonded) < 2: log.debug('Skipping bonding menu option' ' (not enough available nics)') continue if len(opt) > max_btn_len: max_btn_len = len(opt) buttons.append( menu_btn(label=opt, on_press=self.additional_menu_select, user_data=sig)) buttons = [ uPadding(button, align='left', width=max_btn_len + 6) for button in buttons ] r = labels + buttons if len(r) > 0: r[0:0] = [Text("")] return r def additional_menu_select(self, result, sig): self.controller.signal.emit_signal(sig) def on_net_dev_press(self, result): log.debug("Selected network dev: {}".format(result.label)) self.controller.network_configure_interface(result.label) def show_network_error(self, action, info=None): self.error_showing = True self.bottom.contents[0:0] = [ (Text(""), self.bottom.options()), (Color.info_error(self.error), self.bottom.options()), ] if action == 'stop-networkd': exc = info[0] self.error.set_text("Stopping systemd-networkd-failed: %r" % (exc.stderr, )) elif action == 'apply': self.error.set_text("Network configuration could not be applied; " "please verify your settings.") elif action == 'timeout': self.error.set_text("Network configuration timed out; " "please verify your settings.") elif action == 'down': self.error.set_text("Downing network interfaces failed.") elif action == 'canceled': self.error.set_text("Network configuration canceled.") else: self.error.set_text("An unexpected error has occurred; " "please verify your settings.") def done(self, result): if self.error_showing: self.bottom.contents[0:2] = [] self.controller.network_finish(self.model.render()) def cancel(self, button=None): self.controller.cancel()
class AutoDetectPressKey(AutoDetectBase): # This is the tricky case. We need access to the "keycodes" not # the characters that the current keyboard set up maps the # keycodes to. The heavy lifting is done by the InputFilter class # in subiquitycore.core. def selectable(self): return True def make_body(self): self.error_text = Text("", align="center") self.pile = Pile([ ('pack', Text(_("Please press one of the following keys:"))), ('pack', Text("")), ('pack', Columns([Text(s, align="center") for s in self.step.symbols], dividechars=1)), ]) return self.pile @property def input_filter(self): return self.keyboard_detector.keyboard_view.controller.app.input_filter def start(self): self.input_filter.enter_keycodes_mode() def stop(self): self.input_filter.exit_keycodes_mode() def error(self, message): t = Color.info_error(Text(message, align='center')) self.pile.contents.extend([ (Text(""), self.pile.options('pack')), (t, self.pile.options('pack')), ]) def keypress(self, size, key): log.debug('keypress %r', key) if key.startswith('release '): # Escape is key 1 on all keyboards and all layouts except # amigas and very old Macs so this seems safe enough. if key == 'release 1': return super().keypress(size, 'esc') else: return elif key.startswith('press '): code = int(key[len('press '):]) if code not in self.step.keycodes: self.error(_("Input was not recognized, try again")) return v = self.step.keycodes[code] else: # If we're not on a linux tty, the filtering won't have # happened and so there's no way to get the keycodes. Do # something literally random instead. if key == 'e': self.error(_("Input was not recognized, try again")) return import random v = random.choice(list(self.step.keycodes.values())) self.keyboard_detector.do_step(v)
class SnapInfoView(WidgetWrap): # This is mostly like a Pile but it tries to be a bit smart about # how to distribute space between the description and channel list # (which can both be arbitrarily long or short). If both are long, # the channel list is given a third of the space. If there is # space for both, they are packed into the upper part of the view. def __init__(self, parent, snap, cur_channel): self.parent = parent self.snap = snap self.channels = [] self.needs_focus = True self.description = Text(snap.description.replace('\r', '').strip()) self.lb_description = ListBox([self.description]) radio_group = [] for csi in snap.channels: notes = '-' if csi.confinement != "strict": notes = csi.confinement btn = StarRadioButton(radio_group, "{}:".format(csi.channel_name), state=csi.channel_name == cur_channel, on_state_change=self.state_change, user_data=SnapSelection( channel=csi.channel_name, is_classic=csi.confinement == "classic")) self.channels.append( Color.menu_button( TableRow([ btn, Text(csi.version), Text("({})".format(csi.revision)), Text(humanize_size(csi.size)), Text(notes), ]))) self.lb_channels = NoTabCyclingTableListBox(self.channels) title = Columns([ Text(snap.name), ('pack', Text(_("Publisher: {}").format(snap.publisher), align='right')), ], dividechars=1) contents = [ ('pack', title), ('pack', Text("")), ('pack', Text(snap.summary)), ('pack', Text("")), self.lb_description, # overwritten in render() ('pack', Text("")), ('weight', 1, self.lb_channels), ] self.description_index = contents.index(self.lb_description) self.pile = Pile(contents) super().__init__(self.pile) def state_change(self, sender, state, selection): if state: self.parent.snap_boxes[self.snap.name].set_state(True) self.parent.to_install[self.snap.name] = selection def render(self, size, focus): maxcol, maxrow = size rows_available = maxrow pack_option = self.pile.options('pack') for w, o in self.pile.contents: if o == pack_option: rows_available -= w.rows((maxcol, ), focus) rows_wanted_description = self.description.rows((maxcol, ), False) rows_wanted_channels = len(self.channels) if rows_wanted_channels + rows_wanted_description <= rows_available: description_rows = rows_wanted_description else: if rows_wanted_description < 2 * rows_available / 3: description_rows = rows_wanted_description else: channel_rows = min(rows_wanted_channels, int(rows_available / 3)) description_rows = rows_available - channel_rows self.pile.contents[self.description_index] = (self.lb_description, self.pile.options( 'given', description_rows)) if description_rows >= rows_wanted_description: self.lb_description.base_widget._selectable = False else: self.lb_description.base_widget._selectable = True if self.needs_focus: self.pile._select_first_selectable() self.needs_focus = False return self.pile.render(size, focus)
class NetworkSetDefaultRouteView(BaseView): def __init__(self, model, family, controller): self.model = model self.family = family self.controller = controller self.default_gateway_w = None self.gateway_options = Pile(self._build_default_routes()) body = [ Padding.center_79(Text("Please set the default gateway:")), Padding.line_break(""), Padding.center_79(self.gateway_options), Padding.line_break(""), Padding.fixed_10(self._build_buttons()) ] super().__init__(ListBox(body)) def _build_default_routes(self): ''' iterate through interfaces collecting any uniq provider (aka, gateway) and associate the interface name with the gateway then generate a line per key in the gateway dict and display the keys. Upon selection of the gateway entry (ip) then we set model.set_default_gateway(ip) if manual is selected, then we update the second entry into a IPAddressEditor and accept the value, submitting it to the model. ''' providers = {} for iface in self.model.get_all_interfaces(): if self.family == socket.AF_INET: ip_providers = iface.ip4_providers elif self.family == socket.AF_INET6: ip_providers = iface.ip6_providers for provider in ip_providers: log.debug('ipv4 provider: {}'.format(provider)) gw = provider if gw in providers: providers[gw].append(iface.ifname) else: providers[gw] = [iface.ifname] log.debug('gateway providers: {}'.format(providers)) items = [] items.append( Padding.center_79(menu_btn(label="None", on_press=self.done))) for (gw, ifaces) in providers.items(): if gw is None: continue items.append( Padding.center_79( menu_btn(label="{gw} ({ifaces})".format( gw=gw, ifaces=(",".join(ifaces))), on_press=self.done))) items.append( Padding.center_79( menu_btn(label="Specify the default route manually", on_press=self.show_edit_default_route))) return items def _build_buttons(self): buttons = [ done_btn(on_press=self.done), cancel_btn(on_press=self.cancel), ] return Pile(buttons) def show_edit_default_route(self, btn): log.debug("Re-rendering specify default route") self.default_gateway_w = StringEditor( caption="Default gateway will be ") self.gateway_options.contents[-1] = (Padding.center_50( Color.string_input(self.default_gateway_w)), self.gateway_options.options()) def done(self, result): log.debug("changing default gw: {}".format(result)) gw_func = None if self.family == socket.AF_INET: gw_func = self.model.set_default_v4_gateway elif self.family == socket.AF_INET6: gw_func = self.model.set_default_v6_gateway if self.default_gateway_w and self.default_gateway_w.value: try: gw_func(None, self.default_gateway_w.value) except ValueError: # FIXME: raise UX error message self.default_gateway_w.edit_text = "" else: gw_ip_from_label = result.label.split(" ")[0] log.debug("default gw entered: {}".format(gw_ip_from_label)) try: if gw_ip_from_label.startswith('None'): gw_func(None, None) else: gw_func(None, gw_ip_from_label) except ValueError: # FIXME: raise UX error message pass self.controller.default() def cancel(self, button=None): self.controller.default()
class NetworkView(BaseView): title = _("Network connections") excerpt = _("Configure at least one interface this server can use to talk " "to other machines, and which preferably provides sufficient " "access for updates.") def __init__(self, model, controller): self.model = model self.controller = controller self.dev_to_table = {} self.cur_netdevs = [] self.error = Text("", align='center') self.device_colspecs = { 0: ColSpec(rpad=1), 3: ColSpec(min_width=15), 4: ColSpec(can_shrink=True, rpad=1), } self.device_pile = Pile(self._build_model_inputs()) self._create_bond_btn = menu_btn(_("Create bond"), on_press=self._create_bond) bp = button_pile([self._create_bond_btn]) bp.align = 'left' rows = [ self.device_pile, bp, ] self.buttons = button_pile([ done_btn("TBD", on_press=self.done), # See _route_watcher back_btn(_("Back"), on_press=self.cancel), ]) self.bottom = Pile([ ('pack', self.buttons), ]) self.controller.network_event_receiver.add_default_route_watcher( self._route_watcher) self.error_showing = False super().__init__( screen(rows=rows, buttons=self.bottom, focus_buttons=True, excerpt=_(self.excerpt))) _action_INFO = _stretchy_shower(ViewInterfaceInfo) _action_EDIT_WLAN = _stretchy_shower(NetworkConfigureWLANStretchy) _action_EDIT_IPV4 = _stretchy_shower(EditNetworkStretchy, 4) _action_EDIT_IPV6 = _stretchy_shower(EditNetworkStretchy, 6) _action_EDIT_BOND = _stretchy_shower(BondStretchy) _action_ADD_VLAN = _stretchy_shower(AddVlanStretchy) def _action_DELETE(self, name, device): with self.controller.context.child(name): touched_devs = set() if device.type == "bond": for name in device.config['interfaces']: touched_devs.add(self.model.get_netdev_by_name(name)) device.config = None self.del_link(device) for dev in touched_devs: self.update_link(dev) self.controller.apply_config() def _action(self, sender, action, device): action, meth = action meth("{}/{}".format(device.name, action.name), device) def _route_watcher(self, routes): log.debug('view route_watcher %s', routes) if routes: label = _("Done") else: label = _("Continue without network") self.buttons.base_widget[0].set_label(label) self.buttons.width = max( 14, widget_width(self.buttons.base_widget[0]), widget_width(self.buttons.base_widget[1]), ) def show_apply_spinner(self): s = Spinner(self.controller.app.aio_loop) s.start() c = TablePile([ TableRow([ Text(_("Applying changes")), s, ]), ], align='center') self.bottom.contents[0:0] = [ (c, self.bottom.options()), (Text(""), self.bottom.options()), ] def hide_apply_spinner(self): if len(self.bottom.contents) > 2: self.bottom.contents[0:2] = [] def _notes_for_device(self, dev): notes = [] if dev.type == "eth" and not dev.info.is_connected: notes.append(_("not connected")) for dev2 in self.model.get_all_netdevs(): if dev2.type != "bond": continue if dev.name in dev2.config.get('interfaces', []): notes.append( _("enslaved to {device}").format(device=dev2.name)) break if notes: notes = ", ".join(notes) else: notes = '-' return notes def _address_rows_for_device(self, dev): address_info = [] dhcp_addresses = dev.dhcp_addresses() for v in 4, 6: if dev.dhcp_enabled(v): label = Text("DHCPv{v}".format(v=v)) addrs = dhcp_addresses.get(v) if addrs: address_info.extend([(label, Text(addr)) for addr in addrs]) elif dev.dhcp_state(v) == "PENDING": s = Spinner(self.controller.app.aio_loop, align='left') s.rate = 0.3 s.start() address_info.append((label, s)) elif dev.dhcp_state(v) == "TIMEDOUT": address_info.append((label, Text(_("timed out")))) elif dev.dhcp_state(v) == "RECONFIGURE": address_info.append((label, Text("-"))) else: address_info.append((label, Text( _("unknown state {state}".format( state=dev.dhcp_state(v)))))) else: addrs = [] for ip in dev.config.get('addresses', []): if addr_version(ip) == v: addrs.append(str(ip)) if addrs: address_info.append( (Text(_('static')), Text(', '.join(addrs)))) if len(address_info) == 0: # Do not show an interface as disabled if it is part of a bond or # has a vlan on it. if not dev.is_used: reason = dev.disabled_reason if reason is None: reason = "" address_info.append((Text(_("disabled")), Text(reason))) rows = [] for label, value in address_info: rows.append(TableRow([Text(""), label, (2, value)])) return rows def new_link(self, new_dev): log.debug("new_link %s %s %s", new_dev.name, new_dev.ifindex, (new_dev in self.cur_netdevs)) if new_dev in self.dev_to_table: self.update_link(new_dev) return for i, cur_dev in enumerate(self.cur_netdevs): if cur_dev.name > new_dev.name: netdev_i = i break else: netdev_i = len(self.cur_netdevs) w = self._device_widget(new_dev, netdev_i) self.device_pile.contents[netdev_i + 1:netdev_i + 1] = [ (w, self.device_pile.options('pack')) ] def update_link(self, dev): log.debug("update_link %s %s %s", dev.name, dev.ifindex, (dev in self.cur_netdevs)) if dev not in self.cur_netdevs: return # Update the display of dev to represent the current state. # # The easiest way of doing this would be to just create a new table # widget for the device and replace the current one with it. But that # is jarring if the menu for the current device is open, so instead we # overwrite the content of the first (menu) row of the old table with # the contents of the first row of the new table, and replace all other # rows of the old table with new content (which is OK as they cannot be # focused). old_table = self.dev_to_table[dev] first_row = old_table.table_rows[0].base_widget first_row.cells[1][1].set_text(dev.name) first_row.cells[2][1].set_text(dev.type) first_row.cells[3][1].set_text(self._notes_for_device(dev)) old_table.remove_rows(1, len(old_table.table_rows)) old_table.insert_rows(1, self._address_rows_for_device(dev)) def _remove_row(self, netdev_i): # MonitoredFocusList clamps the focus position to the new # length of the list when you remove elements but it doesn't # check that that the element it moves the focus to is # selectable... new_length = len(self.device_pile.contents) - 1 refocus = self.device_pile.focus_position >= new_length del self.device_pile.contents[netdev_i] if refocus: self.device_pile._select_last_selectable() else: while not self.device_pile.focus.selectable(): self.device_pile.focus_position += 1 self.device_pile.focus._select_first_selectable() def del_link(self, dev): log.debug("del_link %s %s %s", dev.name, dev.ifindex, (dev in self.cur_netdevs)) # If a virtual device disappears while we still have config # for it, we assume it will be back soon. if dev.is_virtual and dev.config is not None: return if dev in self.cur_netdevs: netdev_i = self.cur_netdevs.index(dev) self._remove_row(netdev_i + 1) del self.cur_netdevs[netdev_i] del self.dev_to_table[dev] if isinstance(self._w, StretchyOverlay): stretchy = self._w.stretchy if getattr(stretchy, 'device', None) is dev: self.remove_overlay() def _device_widget(self, dev, netdev_i=None): # Create the widget for a nic. This consists of a Pile containing a # table, an info line and a blank line. The first row of the table is # the one that can be focused and has a menu for manipulating the nic, # the other rows summarize its address config. # [ name type notes ▸ ] \ # address info | <- table # more address info / # mac / vendor info / model info # <blank line> if netdev_i is None: netdev_i = len(self.cur_netdevs) self.cur_netdevs[netdev_i:netdev_i] = [dev] actions = [] for action in NetDevAction: meth = getattr(self, '_action_' + action.name) opens_dialog = getattr(meth, 'opens_dialog', False) if dev.supports_action(action): actions.append( (_(action.value), True, (action, meth), opens_dialog)) menu = ActionMenu(actions) connect_signal(menu, 'action', self._action, dev) trows = [ make_action_menu_row([ Text("["), Text(dev.name), Text(dev.type), Text(self._notes_for_device(dev), wrap='clip'), menu, Text("]"), ], menu) ] + self._address_rows_for_device(dev) table = TablePile(trows, colspecs=self.device_colspecs, spacing=2) self.dev_to_table[dev] = table table.bind(self.heading_table) if dev.type == "vlan": info = _("VLAN {id} on interface {link}").format(**dev.config) elif dev.type == "bond": info = _("bond master for {interfaces}").format( interfaces=', '.join(dev.config['interfaces'])) else: info = " / ".join( [dev.info.hwaddr, dev.info.vendor, dev.info.model]) return Pile([ ('pack', table), ('pack', Color.info_minor(Text(" " + info))), ('pack', Text("")), ]) def _build_model_inputs(self): self.heading_table = TablePile([ TableRow([ Color.info_minor(Text(header)) for header in [ "", "NAME", "TYPE", "NOTES", "", ] ]) ], spacing=2, colspecs=self.device_colspecs) rows = [self.heading_table] for dev in self.model.get_all_netdevs(): rows.append(self._device_widget(dev)) return rows def _create_bond(self, sender=None): stretchy = BondStretchy(self) stretchy.attach_context(self.controller.context.child("add_bond")) self.show_stretchy_overlay(stretchy) def show_network_error(self, action, info=None): self.error_showing = True self.bottom.contents[0:0] = [ (Color.info_error(self.error), self.bottom.options()), (Text(""), self.bottom.options()), ] if action == 'stop-networkd': exc = info[0] self.error.set_text("Stopping systemd-networkd-failed: %r" % (exc.stderr, )) elif action == 'apply': self.error.set_text("Network configuration could not be applied; " "please verify your settings.") elif action == 'timeout': self.error.set_text("Network configuration timed out; " "please verify your settings.") elif action == 'down': self.error.set_text("Downing network interfaces failed.") elif action == 'add-vlan': self.error.set_text("Failed to add a VLAN tag.") elif action == 'rm-dev': self.error.set_text("Failed to delete a virtual interface.") else: self.error.set_text("An unexpected error has occurred; " "please verify your settings.") def done(self, result=None): if self.error_showing: self.bottom.contents[0:2] = [] self.controller.network_event_receiver.remove_default_route_watcher( self._route_watcher) self.controller.done() def cancel(self, button=None): self.controller.network_event_receiver.remove_default_route_watcher( self._route_watcher) self.controller.cancel()
class SnapInfoView(WidgetWrap): # This is mostly like a Pile but it tries to be a bit smart about # how to distribute space between the description and channel list # (which can both be arbitrarily long or short). If both are long, # the channel list is given a third of the space. If there is # space for both, they are packed into the upper part of the view. def __init__(self, parent, snap, cur_channel): self.parent = parent self.snap = snap self.needs_focus = True self.description = Text(snap.description.replace('\r', '').strip()) self.lb_description = ListBox([self.description]) latest_update = datetime.datetime.min radio_group = [] channel_rows = [] for csi in snap.channels: latest_update = max(latest_update, csi.released_at) btn = StarRadioButton(radio_group, csi.channel_name, state=csi.channel_name == cur_channel, on_state_change=self.state_change, user_data=SnapSelection( channel=csi.channel_name, is_classic=csi.confinement == "classic")) channel_rows.append( Color.menu_button( TableRow([ btn, Text(csi.version), Text("(" + csi.revision + ")"), Text(humanize_size(csi.size)), Text(format_datetime(csi.released_at)), Text(csi.confinement), ]))) first_info_row = TableRow([ (3, Text([ ('info_minor', "LICENSE: "), snap.license, ], wrap='clip')), (3, Text([ ('info_minor', "LAST UPDATED: "), format_datetime(latest_update), ])), ]) heading_row = Color.info_minor( TableRow([ Text("CHANNEL"), (2, Text("VERSION")), Text("SIZE"), Text("PUBLISHED"), Text("CONFINEMENT"), ])) colspecs = { 1: ColSpec(can_shrink=True), } info_table = TablePile([ first_info_row, TableRow([Text("")]), heading_row, ], spacing=2, colspecs=colspecs) self.lb_channels = NoTabCyclingTableListBox(channel_rows, spacing=2, colspecs=colspecs) info_table.bind(self.lb_channels) self.info_padding = Padding.pull_1(info_table) publisher = [('info_minor header', "by: "), snap.publisher] if snap.verified: publisher.append(('verified header', ' \N{check mark}')) self.title = Columns([ Text(snap.name), ('pack', Text(publisher, align='right')), ], dividechars=1) contents = [ ('pack', Text(snap.summary)), ('pack', Text("")), self.lb_description, # overwritten in render() ('pack', Text("")), ('pack', self.info_padding), ('pack', Text("")), ('weight', 1, self.lb_channels), ] self.description_index = contents.index(self.lb_description) self.pile = Pile(contents) super().__init__(self.pile) def state_change(self, sender, state, selection): if state: self.parent.snap_boxes[self.snap.name].set_state(True) self.parent.to_install[self.snap.name] = selection def render(self, size, focus): maxcol, maxrow = size rows_available = maxrow pack_option = self.pile.options('pack') for w, o in self.pile.contents: if o == pack_option: rows_available -= w.rows((maxcol, ), focus) rows_wanted_description = self.description.rows((maxcol - 1, ), False) rows_wanted_channels = 0 for row in self.lb_channels._w.original_widget.body: rows_wanted_channels += row.rows((maxcol, ), False) log.debug('rows_available %s', rows_available) log.debug('rows_wanted_description %s rows_wanted_channels %s', rows_wanted_description, rows_wanted_channels) if rows_wanted_channels + rows_wanted_description <= rows_available: description_rows = rows_wanted_description channel_rows = rows_wanted_channels else: if rows_wanted_description < 2 * rows_available / 3: description_rows = rows_wanted_description channel_rows = rows_available - description_rows else: channel_rows = max( min(rows_wanted_channels, int(rows_available / 3)), 3) log.debug('channel_rows %s', channel_rows) description_rows = rows_available - channel_rows self.pile.contents[self.description_index] = (self.lb_description, self.pile.options( 'given', description_rows)) if description_rows >= rows_wanted_description: self.lb_description.base_widget._selectable = False else: self.lb_description.base_widget._selectable = True if channel_rows >= rows_wanted_channels: self.info_padding.right = 0 else: self.info_padding.right = 1 if self.needs_focus: self.pile._select_first_selectable() self.needs_focus = False return self.pile.render(size, focus)
class NetworkView(BaseView): title = _("Network connections") excerpt = _("Configure at least one interface this server can use to talk " "to other machines, and which preferably provides sufficient " "access for updates.") def __init__(self, controller, netdev_infos, wlan_support_install_state="NOT_NEEDED"): self.controller = controller self.dev_name_to_table = {} self.cur_netdev_names = [] self.error = Text("", align='center') self.device_colspecs = { 0: ColSpec(rpad=1), 3: ColSpec(min_width=15), 4: ColSpec(can_shrink=True, rpad=1), } self.heading_table = TablePile([ TableRow([ Color.info_minor(Text(header)) for header in [ "", "NAME", "TYPE", "NOTES", "", ] ]) ], spacing=2, colspecs=self.device_colspecs) self.device_pile = Pile([self.heading_table]) for dev_info in netdev_infos: self.new_link(dev_info) self._create_bond_btn = menu_btn(_("Create bond"), on_press=self._create_bond) bp = button_pile([self._create_bond_btn]) bp.align = 'left' rows = [ self.device_pile, bp, ] self.buttons = button_pile([ done_btn("TBD", on_press=self.done), # See _route_watcher back_btn(_("Back"), on_press=self.cancel), ]) self.bottom = Pile([ ('pack', self.buttons), ]) self.wlan_support_install_state_showing = False self.error_showing = False self.update_for_wlan_support_install_state(wlan_support_install_state) super().__init__( screen(rows=rows, buttons=self.bottom, focus_buttons=True, excerpt=_(self.excerpt))) async def _show_INFO(self, name): info = await self.controller.app.wait_with_text_dialog( self.controller.get_info_for_netdev(name), "Loading info", can_cancel=True) stretchy = ViewInterfaceInfo(self, name, info) stretchy.attach_context(self.controller.context.child("INFO")) self.show_stretchy_overlay(stretchy) def _action_INFO(self, name, dev_info): self.controller.app.aio_loop.create_task(self._show_INFO( dev_info.name)) _action_INFO.opens_dialog = True _action_EDIT_WLAN = _stretchy_shower(NetworkConfigureWLANStretchy) _action_EDIT_IPV4 = _stretchy_shower(EditNetworkStretchy, 4) _action_EDIT_IPV6 = _stretchy_shower(EditNetworkStretchy, 6) _action_ADD_VLAN = _stretchy_shower(AddVlanStretchy) def _action_EDIT_BOND(self, name, dev_info): stretchy = BondStretchy(self, dev_info, self.get_candidate_bond_member_names()) stretchy.attach_context(self.controller.context.child(name)) self.show_stretchy_overlay(stretchy) _action_EDIT_BOND.opens_dialog = True def _action_DELETE(self, name, dev_info): with self.controller.context.child(name): self.controller.delete_link(dev_info.name) self.del_link(dev_info) def _action(self, sender, action, netdev_table): action, meth = action dev_info = netdev_table.dev_info meth("{}/{}".format(dev_info.name, action.name), dev_info) def update_default_routes(self, routes): log.debug('view route_watcher %s', routes) if routes: label = _("Done") else: label = _("Continue without network") self.buttons.base_widget[0].set_label(label) self.buttons.width = max( 14, widget_width(self.buttons.base_widget[0]), widget_width(self.buttons.base_widget[1]), ) def show_apply_spinner(self): s = Spinner(self.controller.app.aio_loop) s.start() c = TablePile([ TableRow([ Text(_("Applying changes")), s, ]), ], align='center') self.bottom.contents[0:0] = [ (c, self.bottom.options()), (Text(""), self.bottom.options()), ] def hide_apply_spinner(self): if len(self.bottom.contents) > 2: self.bottom.contents[0:2] = [] def new_link(self, new_dev_info): log.debug("new_link %s %s", new_dev_info.name, (new_dev_info.name in self.cur_netdev_names)) if new_dev_info.name in self.dev_name_to_table: self.update_link(new_dev_info) return self.cur_netdev_names.append(new_dev_info.name) self.cur_netdev_names.sort() netdev_i = self.cur_netdev_names.index(new_dev_info.name) device_table = NetworkDeviceTable(self, new_dev_info) self.dev_name_to_table[new_dev_info.name] = device_table self.device_pile.contents[netdev_i + 1:netdev_i + 1] = [ (device_table, self.device_pile.options('pack')) ] def update_link(self, dev_info): if isinstance(self._w, StretchyOverlay): if hasattr(self._w.stretchy, 'update_link'): self._w.stretchy.update_link(dev_info) log.debug("update_link %s %s", dev_info.name, (dev_info.name in self.cur_netdev_names)) if dev_info.name not in self.cur_netdev_names: return self.dev_name_to_table[dev_info.name].update(dev_info) def _remove_row(self, netdev_i): # MonitoredFocusList clamps the focus position to the new # length of the list when you remove elements but it doesn't # check that that the element it moves the focus to is # selectable... new_length = len(self.device_pile.contents) - 1 refocus = self.device_pile.focus_position >= new_length del self.device_pile.contents[netdev_i] if refocus: self.device_pile._select_last_selectable() else: while not self.device_pile.focus.selectable(): self.device_pile.focus_position += 1 self.device_pile.focus._select_first_selectable() def del_link(self, dev_info): log.debug("del_link %s %s", dev_info.name, (dev_info.name in self.cur_netdev_names)) # If a virtual device disappears while we still have config # for it, we assume it will be back soon. if dev_info.is_virtual and dev_info.has_config: return if dev_info.name in self.cur_netdev_names: netdev_i = self.cur_netdev_names.index(dev_info.name) self._remove_row(netdev_i + 1) del self.cur_netdev_names[netdev_i] del self.dev_name_to_table[dev_info.name] if isinstance(self._w, StretchyOverlay): stretchy = self._w.stretchy if getattr(stretchy, 'device', None) is dev_info: self.remove_overlay() def get_candidate_bond_member_names(self): names = [] for table in self.dev_name_to_table.values(): dev_info = table.dev_info if dev_info.type in ("vlan", "bond"): continue if dev_info.bond_master is not None: continue names.append(dev_info.name) return names def _create_bond(self, sender=None): stretchy = BondStretchy(self, None, self.get_candidate_bond_member_names()) stretchy.attach_context(self.controller.context.child("add_bond")) self.show_stretchy_overlay(stretchy) def update_for_wlan_support_install_state(self, state): if state == "NOT_NEEDED": return if self.error_showing: start_i = 2 else: start_i = 0 if self.wlan_support_install_state_showing: end_i = start_i + 2 else: end_i = start_i self.wlan_support_install_state_showing = True text = wlan_support_install_state_texts[state] self.bottom.contents[start_i:end_i] = [ (Text(rewrap(text), align='center'), self.bottom.options()), (Text(""), self.bottom.options()), ] def show_network_error(self, action, info=None): if not self.error_showing: start_i = end_i = 0 self.error_showing = True else: start_i = 0 end_i = 2 self.bottom.contents[start_i:end_i] = [ (Color.info_error(self.error), self.bottom.options()), (Text(""), self.bottom.options()), ] if action == 'stop-networkd': exc = info[0] self.error.set_text("Stopping systemd-networkd-failed: %r" % (exc.stderr, )) elif action == 'apply': self.error.set_text("Network configuration could not be applied; " "please verify your settings.") elif action == 'timeout': self.error.set_text("Network configuration timed out; " "please verify your settings.") elif action == 'down': self.error.set_text("Downing network interfaces failed.") elif action == 'add-vlan': self.error.set_text("Failed to add a VLAN tag.") elif action == 'rm-dev': self.error.set_text("Failed to delete a virtual interface.") else: self.error.set_text("An unexpected error has occurred; " "please verify your settings.") def done(self, result=None): if self.error_showing: self.bottom.contents[0:2] = [] self.controller.done() def cancel(self, button=None): self.controller.cancel()