Beispiel #1
0
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)
Beispiel #2
0
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()
Beispiel #3
0
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()
Beispiel #4
0
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)
Beispiel #5
0
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()
Beispiel #6
0
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()
Beispiel #7
0
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()
Beispiel #8
0
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)
Beispiel #9
0
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()
Beispiel #11
0
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()
Beispiel #12
0
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)
Beispiel #13
0
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()