class DeployView(WidgetWrap):

    def __init__(self, display_controller, placement_controller,
                 placement_view):
        self.display_controller = display_controller
        self.placement_controller = placement_controller
        self.placement_view = placement_view
        self.prev_status = None
        w = self.build_widgets()
        super().__init__(w)
        self.update()

    def selectable(self):
        return True

    def build_widgets(self):
        self.deploy_ok_msg = ("\u2713 All the required OpenStack services are "
                              "placed on a machine, and you can now deploy.")

        self.deploy_button = AttrMap(
            Button("Deploy", on_press=self.do_deploy),
            'button_primary', 'button_primary focus')
        self.deploy_grid = GridFlow([self.deploy_button], 10, 1, 0, 'center')

        self.unplaced_msg = "Some required services are still unassigned."

        self.main_pile = Pile([Divider()])
        return self.main_pile

    def update(self):
        changed = self.prev_status != self.placement_controller.can_deploy()

        if self.placement_controller.can_deploy():
            if changed:
                self.show_deploy_button()
        else:
            self.main_pile.contents[0] = (Divider(),
                                          self.main_pile.options())
            if changed:
                self.display_controller.status_error_message(self.unplaced_msg)

        self.prev_status = self.placement_controller.can_deploy()

    def show_deploy_button(self):
        self.main_pile.contents[0] = (AttrMap(self.deploy_grid,
                                              'deploy_highlight_start'),
                                      self.main_pile.options())
        self.display_controller.status_info_message(self.deploy_ok_msg)

    def do_deploy(self, sender):
        self.placement_view.do_deploy_cb()
Example #2
0
class Header(WidgetWrap):

    TITLE_TEXT = "Ubuntu OpenStack Installer - Dashboard"

    def __init__(self):
        self.text = Text(self.TITLE_TEXT)
        self.widget = Color.frame_header(self.text)
        self.pile = Pile([self.widget, Text("")])
        self.set_show_add_units_hotkey(False)
        super().__init__(self.pile)

    def set_openstack_rel(self, release):
        self.text.set_text("{} ({})".format(self.TITLE_TEXT, release))

    def set_show_add_units_hotkey(self, show):
        self.show_add_units = show
        self.update()

    def update(self):
        if self.show_add_units:
            add_unit_string = "(A)dd Services \N{BULLET}"
        else:
            add_unit_string = ""
        tw = Color.frame_subheader(
            Text(add_unit_string + " (H)elp \N{BULLET} " "(R)efresh \N{BULLET} (Q)uit", align="center")
        )
        self.pile.contents[1] = (tw, self.pile.options())
Example #3
0
class Header(WidgetWrap):

    def __init__(self):
        self.title_widget = Color.frame_header(padding(Text(TITLE_TEXT)))
        self.pile = Pile([self.title_widget, Text("")])
        self.set_show_add_units_hotkey(False)
        super().__init__(self.pile)

    def set_show_add_units_hotkey(self, show):
        self.show_add_units = show
        self.update()

    def update(self):
        if self.show_add_units:
            add_unit_string = '(A)dd Services \N{BULLET}'
        else:
            add_unit_string = ''
        tw = Color.frame_subheader(Text(add_unit_string + ' (H)elp \N{BULLET} '
                                        '(R)efresh \N{BULLET} (Q)uit',
                                        align='center'))
        self.pile.contents[1] = (tw, self.pile.options())
class ServicesList(WidgetWrap):
    """A list of services (charm classes) with flexible display options.

    Note that not all combinations of display options make sense. YMMV.

    controller - a PlacementController

    actions - a list of tuples describing buttons. Passed to
    ServiceWidget.

    machine - a machine instance to query for constraint checking. If
    None, no constraint checking is done. If set, only services whose
    constraints are satisfied by 'machine' are shown.

    ignore_assigned - bool, whether or not to display services that
    have already been assigned to a machine (but not yet deployed)

    ignore_deployed - bool, whether or not to display services that
    have already been deployed

    deployed_only - bool, only show deployed services

    show_constraints - bool, whether or not to display the constraints
    for the various services

    show_type - string, one of 'all', 'required' or 'non-required',
    controls which charm states should be shown. default is 'all'.

    trace_updates - bool, enable verbose update logging

    """
    def __init__(self,
                 controller,
                 actions,
                 subordinate_actions,
                 machine=None,
                 ignore_assigned=False,
                 ignore_deployed=False,
                 assigned_only=False,
                 deployed_only=False,
                 show_type='all',
                 show_constraints=False,
                 show_placements=False,
                 title="Services",
                 trace_updates=False):
        self.controller = controller
        self.actions = actions
        self.subordinate_actions = subordinate_actions
        self.service_widgets = []
        self.machine = machine
        self.ignore_assigned = ignore_assigned
        self.ignore_deployed = ignore_deployed
        self.assigned_only = assigned_only
        self.deployed_only = deployed_only
        self.show_type = show_type
        self.show_constraints = show_constraints
        self.show_placements = show_placements
        self.title = title
        self.trace = trace_updates
        w = self.build_widgets()
        self.update()
        super().__init__(w)

    def selectable(self):
        # overridden to ensure that we can arrow through the buttons
        # shouldn't be necessary according to documented behavior of
        # Pile & Columns, but discovered via trial & error.
        return True

    def build_widgets(self):
        self.service_pile = Pile(
            [Text(self.title), Divider(' ')] + self.service_widgets)
        return self.service_pile

    def find_service_widget(self, cc):
        return next((sw for sw in self.service_widgets
                     if sw.charm_class.charm_name == cc.charm_name), None)

    def update(self):
        def trace(cc, s):
            if self.trace:
                log.debug("{}: {} {}".format(self.title, cc, s))

        for cc in self.controller.charm_classes():
            if self.machine:
                if not satisfies(self.machine, cc.constraints)[0] \
                   or not (self.controller.is_assigned_to(cc, self.machine) or
                           self.controller.is_deployed_to(cc, self.machine)):
                    self.remove_service_widget(cc)
                    trace(cc, "removed because machine doesn't match")
                    continue

            if self.ignore_assigned and self.assigned_only:
                raise Exception("Can't both ignore and only show assigned.")

            if self.ignore_assigned:
                n = self.controller.assignment_machine_count_for_charm(cc)
                if n == cc.required_num_units() \
                   and not cc.allow_multi_units \
                   and self.controller.is_assigned(cc):
                    self.remove_service_widget(cc)
                    trace(cc, "removed because max units are " "assigned")
                    continue
            elif self.assigned_only:
                if not self.controller.is_assigned(cc):
                    self.remove_service_widget(cc)
                    trace(
                        cc, "removed because it is not assigned and "
                        "assigned_only is True")
                    continue

            if self.ignore_deployed and self.deployed_only:
                raise Exception("Can't both ignore and only show deployed.")

            if self.ignore_deployed:
                n = self.controller.deployment_machine_count_for_charm(cc)
                if n == cc.required_num_units() \
                   and self.controller.is_deployed(cc):
                    self.remove_service_widget(cc)
                    trace(
                        cc, "removed because the required number of units"
                        " has been deployed")
                    continue
            elif self.deployed_only:
                if not self.controller.is_deployed(cc):
                    self.remove_service_widget(cc)
                    continue

            state, _, _ = self.controller.get_charm_state(cc)
            if self.show_type == 'required':
                if state != CharmState.REQUIRED:
                    self.remove_service_widget(cc)
                    continue
            elif self.show_type == 'non-required':
                if state == CharmState.REQUIRED:
                    self.remove_service_widget(cc)
                    trace(
                        cc, "removed because show_type is 'non-required' and"
                        "state is REQUIRED.")
                    continue
                assigned_or_deployed = (self.controller.is_assigned(cc)
                                        or self.controller.is_deployed(cc))
                if not cc.allow_multi_units and assigned_or_deployed:
                    self.remove_service_widget(cc)
                    trace(
                        cc, "removed because it doesn't allow multiple units"
                        " and is not assigned or deployed.")
                    continue

            sw = self.find_service_widget(cc)
            if sw is None:
                sw = self.add_service_widget(cc)
                trace(cc, "added widget")
            sw.update()

    def add_service_widget(self, charm_class):
        if charm_class.subordinate:
            actions = self.subordinate_actions
        else:
            actions = self.actions
        sw = ServiceWidget(charm_class,
                           self.controller,
                           actions,
                           self.show_constraints,
                           show_placements=self.show_placements)
        self.service_widgets.append(sw)
        options = self.service_pile.options()
        self.service_pile.contents.append((sw, options))
        self.service_pile.contents.append(
            (AttrMap(Padding(Divider('\u23bc'), left=2, right=2),
                     'label'), options))
        return sw

    def remove_service_widget(self, charm_class):
        sw = self.find_service_widget(charm_class)

        if sw is None:
            return
        self.service_widgets.remove(sw)
        sw_idx = 0
        for w, opts in self.service_pile.contents:
            if w == sw:
                break
            sw_idx += 1

        c = self.service_pile.contents[:sw_idx] + \
            self.service_pile.contents[sw_idx + 2:]
        self.service_pile.contents = c
class MachineWaitView(WidgetWrap):

    def __init__(self, display_controller, installer, config):
        self.display_controller = display_controller
        self.installer = installer
        self.config = config
        creds = self.config.getopt('maascreds')
        if os.getenv("FAKE_API_DATA"):
            self.maas_client = None
            self.maas_state = FakeMaasState()
        else:
            self.maas_client, self.maas_state = connect_to_maas(creds)
        self.spinner = Spinner(15, 4)
        w = self.build_widgets()
        super().__init__(w)
        self.update()

    def build_widgets(self):
        self.message = Text("Please review available machines in MAAS",
                            align='center')

        self.button_pile = Pile([])

        self.main_pile = Pile([self.message,
                               Divider(),
                               self.button_pile])
        return Filler(self.main_pile, valign='middle')

    def keypress(self, size, key):
        key = {'tab': 'down', 'shift tab': 'up'}.get(key, key)
        return super().keypress(size, key)

    def scroll_down(self):
        pass

    def scroll_up(self):
        pass

    def selectable(self):
        return True

    def get_status(self):
        " returns (global_ok, [ok, condition])"
        self.maas_state.invalidate_nodes_cache()
        machines = self.maas_state.machines(state=MaasMachineStatus.READY)
        powerable_machines = [m for m in machines if m.power_type is not None]
        n_powerable = len(powerable_machines)

        conditions = [(n_powerable >= 1,
                       "At least one machine enlisted with power "
                       "control (currently {})".format(n_powerable))]

        global_ok = all([ok for ok, _ in conditions])
        return global_ok, conditions

    def update(self):
        msg = ("Before continuing, ensure that at least one machine is "
               "enlisted into MAAS:")
        self.message = Text(self.spinner.next_frame() + ['\n', msg, '\n'],
                            align='center')
        contents = [(self.message,
                     self.main_pile.options())]

        global_ok, statuses = self.get_status()
        status_map = {True: ('success_icon', "\u2713 "),
                      False: ('error_icon', "<!> ")}
        contents += [(Text([status_map[status], condition],
                           align='center'),
                      self.main_pile.options())
                     for status, condition
                     in statuses]
        contents += [(Divider(), self.main_pile.options()),
                     (self.button_pile, self.main_pile.options())]
        self.main_pile.contents = contents

        if not global_ok:
            b = AttrMap(SelectableIcon(" ( Can't Continue ) "),
                        'disabled_button', 'disabled_button_focus')
        else:
            b = AttrMap(Button("Continue",
                               on_press=self.do_continue),
                        'button_primary', 'button_primary focus')

        cancel_b = AttrMap(Button("Cancel",
                                  on_press=self.do_cancel),
                           'button_secondary',
                           'button_secondary focus')
        self.button_pile.contents = [(Padding(cancel_b, width=24,
                                              align='center'),
                                      self.button_pile.options()),
                                     (Padding(b, width=24, align='center'),
                                      self.button_pile.options())]

        # ensure that the button is always focused:
        self.main_pile.focus_position = len(self.main_pile.contents) - 1

    @utils.async
    def do_continue(self, *args, **kwargs):
        self.installer.do_install()

    def do_cancel(self, *args, **kwargs):
        raise SystemExit("Installation cancelled.")
Example #6
0
class MachinesList(WidgetWrap):

    """A list of machines with configurable action buttons for each
    machine.

    action - a function to call when the machine's button is pressed

    constraints - a dict of constraints to filter the machines list.
    only machines matching all the constraints will be shown.

    show_hardware - bool, whether or not to show the hardware details
    for each of the machines

    title_widgets - A Text Widget to be used in place of the default
    title.

    show_assignments - bool, whether or not to show the assignments
    for each of the machines.

    show_only_ready - bool, only show machines with a ready state.

    """

    def __init__(self, controller, display_controller,
                 constraints=None, show_hardware=False,
                 title_widgets=None, show_assignments=True,
                 show_placeholders=True, show_only_ready=False,
                 show_filter_box=False):
        self.controller = controller
        self.display_controller = display_controller
        self.machine_widgets = []
        if constraints is None:
            self.constraints = {}
        else:
            self.constraints = constraints
        self.show_hardware = show_hardware
        self.show_assignments = show_assignments
        self.show_placeholders = show_placeholders
        self.show_only_ready = show_only_ready
        self.show_filter_box = show_filter_box
        self.filter_string = ""
        w = self.build_widgets(title_widgets)
        self.update()
        super().__init__(w)

    def selectable(self):
        # overridden to ensure that we can arrow through the buttons
        # shouldn't be necessary according to documented behavior of
        # Pile & Columns, but discovered via trial & error.
        return True

    def build_widgets(self, title_widgets):
        if title_widgets is None:
            if len(self.constraints) > 0:
                cstr = " matching constraints"
            else:
                cstr = ""

            title_widgets = [Text("Machines" + cstr, align='center')]

        self.filter_edit_box = FilterBox(self.handle_filter_change)

        header_widgets = title_widgets + [Divider()]

        if self.show_filter_box:
            header_widgets.append(self.filter_edit_box)

        self.header_padding = len(header_widgets)
        self.machine_pile = Pile(header_widgets + self.machine_widgets)
        return self.machine_pile

    def handle_filter_change(self, edit_button, userdata):
        self.filter_string = userdata
        self.update()

    def find_machine_widget(self, m):
        return next((mw for mw in self.machine_widgets if
                     mw.machine.instance_id == m.instance_id), None)

    def update(self):
        machines = self.controller.machines(
            include_placeholders=self.show_placeholders)
        if self.show_only_ready:
            machines = [m for m in machines
                        if m.status == MaasMachineStatus.READY]
        for mw in self.machine_widgets:
            machine = next((m for m in machines if
                            mw.machine.instance_id == m.instance_id), None)
            if machine is None:
                self.remove_machine(mw.machine)

        n_satisfying_machines = len(machines)

        def get_placement_filter_label(d):
            s = ""
            for atype, al in d.items():
                s += " ".join(["{} {}".format(cc.service_name,
                                              cc.display_name)
                               for cc in al])
            return s

        for m in machines:
            if not satisfies(m, self.constraints)[0]:
                self.remove_machine(m)
                n_satisfying_machines -= 1
                continue

            assignment_names = ""
            ad = self.controller.assignments_for_machine(m)
            assignment_names = get_placement_filter_label(ad)
            filter_label = "{} {}".format(m.filter_label(),
                                          assignment_names)
            if self.filter_string != "" and \
               self.filter_string not in filter_label:
                self.remove_machine(m)
                continue

            mw = self.find_machine_widget(m)
            if mw is None:
                mw = self.add_machine_widget(m)
            mw.update()

        self.filter_edit_box.set_info(len(self.machine_widgets),
                                      n_satisfying_machines)

        self.sort_machine_widgets()

    def add_machine_widget(self, machine):
        mw = SimpleMachineWidget(machine,
                                 self.controller,
                                 self.display_controller,
                                 self.show_assignments)
        self.machine_widgets.append(mw)
        options = self.machine_pile.options()
        self.machine_pile.contents.append((mw, options))

        # NOTE: see the +1: indexing in remove_machine if you re-add
        # this divider. it should then be +2.

        # self.machine_pile.contents.append((AttrMap(Padding(Divider('\u23bc'),
        #                                                    left=2, right=2),
        #                                            'label'), options))
        return mw

    def remove_machine(self, machine):
        mw = self.find_machine_widget(machine)
        if mw is None:
            return

        self.machine_widgets.remove(mw)
        mw_idx = 0
        for w, opts in self.machine_pile.contents:
            if w == mw:
                break
            mw_idx += 1

        c = self.machine_pile.contents[:mw_idx] + \
            self.machine_pile.contents[mw_idx + 1:]
        self.machine_pile.contents = c

    def sort_machine_widgets(self):
        def keyfunc(mw):
            m = mw.machine
            hwinfo = " ".join(map(str, [m.arch, m.cpu_cores, m.mem,
                                        m.storage]))
            if str(mw.machine.status) == 'ready':
                skey = 'A'
            else:
                skey = str(mw.machine.status)
            return skey + mw.machine.hostname + hwinfo
        self.machine_widgets.sort(key=keyfunc)

        def wrappedkeyfunc(t):
            mw, options = t
            if not isinstance(mw, SimpleMachineWidget):
                return 'A'
            return keyfunc(mw)

        self.machine_pile.contents.sort(key=wrappedkeyfunc)

    def focus_prev_or_top(self):
        self.update()
        try:
            if self.machine_pile.focus_position <= self.header_padding:
                self.machine_pile.focus_position = self.header_padding
        except IndexError:
            log.debug("index error in machines_list focus_top")
class JujuMachineWidget(WidgetWrap):

    """A widget displaying a machine and action buttons.

    juju_machine_id = juju id of the machine

    md = the machine dict

    application - the current application for which machines are being shown

    assign_cb - a function that takes a machine and assignmenttype to
    perform the button action

    unassign_cb - a function that takes a machine and removes
    assignments for the machine

    controller - a controller object that provides show_pin_chooser

    show_assignments - display info about which charms are assigned
    and what assignment type (LXC, KVM, etc) they have.

    show_pin - show button to pin juju machine to a maas machine

    """

    def __init__(self, juju_machine_id, md, application, assign_cb,
                 unassign_cb, controller, show_assignments=True,
                 show_pin=False):
        self.juju_machine_id = juju_machine_id
        self.md = md
        self.application = application
        self.is_selected = False
        self.assign_cb = assign_cb
        self.unassign_cb = unassign_cb
        self.controller = controller
        self.show_assignments = show_assignments
        self.show_pin = show_pin
        self.all_assigned = False

        w = self.build_widgets()
        super().__init__(w)
        self.update()

    def selectable(self):
        return True

    def __repr__(self):
        return "jujumachinewidget #" + str(self.juju_machine_id)

    def build_widgets(self):

        self.action_button_cols = Columns([])
        self.action_buttons = []
        self.build_unselected_widgets()
        self.pile = Pile([self.unselected_columns])
        return self.pile

    def build_unselected_widgets(self):
        cdict = juju.constraints_to_dict(self.md.get('constraints', ''))

        self.juju_machine_id_button = MenuSelectButton(
            '{:20s}'.format(self.juju_machine_id), self.show_pin_chooser)
        self.juju_machine_id_label = Text(
            "{:20s}".format(self.juju_machine_id))
        self.cores_field = IntEdit('', cdict.get('cores', ''))
        connect_signal(self.cores_field, 'change', self.handle_cores_changed)
        self.mem_field = Edit('', cdict.get('mem', ''))
        connect_signal(self.mem_field, 'change', self.handle_mem_changed)
        self.disk_field = Edit('', cdict.get('root-disk', ''))
        connect_signal(self.disk_field, 'change', self.handle_disk_changed)

        if self.show_pin:
            machine_id_w = self.juju_machine_id_button
        else:
            machine_id_w = self.juju_machine_id_label
        cols = [machine_id_w, self.cores_field,
                self.mem_field, self.disk_field]
        cols = [AttrMap(w, 'string_input',
                        'string_input_focus') for w in cols]
        cols.append(Text("placeholder"))
        self.unselected_columns = Columns(cols, dividechars=2)
        self.update_assignments()
        return self.unselected_columns

    def update_assignments(self):
        assignments = []

        mps = self.controller.get_all_assignments(self.juju_machine_id)
        if len(mps) > 0:
            if self.show_assignments:
                ad = defaultdict(list)
                for application, atype in mps:
                    ad[atype].append(application)
                astr = " ".join(["{}{}".format(
                    atype_to_label([atype])[0],
                    ",".join([application.service_name
                              for application in al]))
                    for atype, al in ad.items()])
                assignments.append(astr)
        else:
            if self.show_assignments:
                assignments.append("-")

        if any([application == self.application for application, _ in mps]):
            action = self.do_remove
            label = "Remove"
        else:
            action = self.do_select
            label = "Select"

        self.select_button = PlainButton(label, action)

        cols = [Text(s) for s in assignments]

        current_assignments = [a for a, _ in mps if a == self.application]
        if self.all_assigned and len(current_assignments) == 0:
            cols.append(Text(""))
        else:
            cols += [AttrMap(self.select_button, 'text',
                             'button_secondary focus')]
        opts = self.unselected_columns.options()
        self.unselected_columns.contents[4:] = [(w, opts) for w in cols]

    def update(self):
        self.update_action_buttons()

        if self.is_selected:
            self.update_selected()
        else:
            self.update_unselected()

    def update_selected(self):
        cn = self.application.service_name
        msg = Text("  Add {} to machine #{}:".format(cn,
                                                     self.juju_machine_id))
        self.pile.contents = [(msg, self.pile.options()),
                              (self.action_button_cols,
                               self.pile.options()),
                              (Divider(), self.pile.options())]

    def update_unselected(self):
        if self.show_pin:
            pinned_machine = self.controller.get_pin(self.juju_machine_id)

            if pinned_machine:
                pin_label = " {} \N{PENCIL}".format(pinned_machine.hostname)
            else:
                pin_label = " \N{PENCIL}"
            self.juju_machine_id_button.set_label('{:20s}'.format(
                self.juju_machine_id + " " + pin_label))
        else:
            self.juju_machine_id_label.set_text('{:20s}'.format(
                self.juju_machine_id))

        self.pile.contents = [(self.unselected_columns, self.pile.options()),
                              (Divider(), self.pile.options())]
        self.update_assignments()

    def update_action_buttons(self):

        all_actions = [(AssignmentType.BareMetal,
                        'Add as Bare Metal',
                        self.select_baremetal),
                       (AssignmentType.LXD,
                        'Add as LXD',
                        self.select_lxd),
                       (AssignmentType.KVM,
                        'Add as KVM',
                        self.select_kvm)]

        sc = self.application
        if sc:
            allowed_set = set(sc.allowed_assignment_types)
            allowed_types = set([atype for atype, _, _ in all_actions])
            allowed_types = allowed_types.intersection(allowed_set)
        else:
            allowed_types = set()

        # + 1 for the cancel button:
        if len(self.action_buttons) == len(allowed_types) + 1:
            return

        self.action_buttons = [AttrMap(PlainButton(label,
                                                   on_press=func),
                                       'button_secondary',
                                       'button_secondary focus')
                               for atype, label, func in all_actions
                               if atype in allowed_types]
        self.action_buttons.append(
            AttrMap(PlainButton("Cancel",
                                on_press=self.do_cancel),
                    'button_secondary',
                    'button_secondary focus'))

        opts = self.action_button_cols.options()
        self.action_button_cols.contents = [(b, opts) for b in
                                            self.action_buttons]

    def do_select(self, sender):
        self.is_selected = True
        self.update()
        self.pile.focus_position = 1
        self.action_button_cols.focus_position = 0

    def do_remove(self, sender):
        self.unassign_cb(self.juju_machine_id)

    def do_cancel(self, sender):
        self.is_selected = False
        self.update()
        self.pile.focus_position = 0

    def _do_select_assignment(self, atype):
        self.assign_cb(self.juju_machine_id, atype)
        self.pile.focus_position = 0
        self.is_selected = False
        self.update()

    def select_baremetal(self, sender):
        self._do_select_assignment(AssignmentType.BareMetal)

    def select_lxd(self, sender):
        self._do_select_assignment(AssignmentType.LXD)

    def select_kvm(self, sender):
        self._do_select_assignment(AssignmentType.KVM)

    def handle_cores_changed(self, sender, val):
        if val == '':
            self.md = self.controller.clear_constraint(self.juju_machine_id,
                                                       'cores')
        else:
            self.md = self.controller.set_constraint(self.juju_machine_id,
                                                     'cores', val)

    def _format_constraint(self, val):
        """Ensure that a constraint has a unit. bare numbers are treated as
        gigabytes"""
        try:
            return units.gb_to_human(float(val))
        except ValueError:
            return val

    def handle_mem_changed(self, sender, val):
        if val == '':
            self.md = self.controller.clear_constraint(self.juju_machine_id,
                                                       'mem')
        else:
            self.md = self.controller.set_constraint(
                self.juju_machine_id, 'mem',
                self._format_constraint(val))

    def handle_disk_changed(self, sender, val):
        if val == '':
            self.md = self.controller.clear_constraint(self.juju_machine_id,
                                                       'root-disk')
        else:
            self.md = self.controller.set_constraint(
                self.juju_machine_id, 'root-disk',
                self._format_constraint(val))

    def show_pin_chooser(self, sender):
        self.controller.show_pin_chooser(self.juju_machine_id)
class ServicesList(WidgetWrap):

    """A list of services (charm classes) with flexible display options.

    Note that not all combinations of display options make sense. YMMV.

    controller - a PlacementController

    actions - a list of tuples describing buttons. Passed to
    ServiceWidget.

    machine - a machine instance to query for constraint checking. If
    None, no constraint checking is done. If set, only services whose
    constraints are satisfied by 'machine' are shown.

    ignore_assigned - bool, whether or not to display services that
    have already been assigned to a machine (but not yet deployed)

    ignore_deployed - bool, whether or not to display services that
    have already been deployed

    deployed_only - bool, only show deployed services

    show_constraints - bool, whether or not to display the constraints
    for the various services

    show_type - string, one of 'all', 'required' or 'non-required',
    controls which charm states should be shown. default is 'all'.

    """

    def __init__(self, controller, actions, subordinate_actions,
                 machine=None, ignore_assigned=False,
                 ignore_deployed=False, assigned_only=False,
                 deployed_only=False, show_type='all',
                 show_constraints=False, show_placements=False,
                 title="Services"):
        self.controller = controller
        self.actions = actions
        self.subordinate_actions = subordinate_actions
        self.service_widgets = []
        self.machine = machine
        self.ignore_assigned = ignore_assigned
        self.ignore_deployed = ignore_deployed
        self.assigned_only = assigned_only
        self.deployed_only = deployed_only
        self.show_type = show_type
        self.show_constraints = show_constraints
        self.show_placements = show_placements
        self.title = title
        w = self.build_widgets()
        self.update()
        super().__init__(w)

    def selectable(self):
        # overridden to ensure that we can arrow through the buttons
        # shouldn't be necessary according to documented behavior of
        # Pile & Columns, but discovered via trial & error.
        return True

    def build_widgets(self):
        self.service_pile = Pile([Text(self.title),
                                  Divider(' ')] +
                                 self.service_widgets)
        return self.service_pile

    def find_service_widget(self, cc):
        return next((sw for sw in self.service_widgets if
                     sw.charm_class.charm_name == cc.charm_name), None)

    def update(self):

        for cc in self.controller.charm_classes():
            if self.machine:
                if not satisfies(self.machine, cc.constraints)[0] \
                   or not (self.controller.is_assigned_to(cc, self.machine) or
                           self.controller.is_deployed_to(cc, self.machine)):
                    self.remove_service_widget(cc)
                    continue

            if self.ignore_assigned and self.assigned_only:
                raise Exception("Can't both ignore and only show assigned.")

            if self.ignore_assigned:
                n = self.controller.assignment_machine_count_for_charm(cc)
                if n == cc.required_num_units() \
                   and self.controller.is_assigned(cc):
                    self.remove_service_widget(cc)
                    continue
            elif self.assigned_only:
                if not self.controller.is_assigned(cc):
                    self.remove_service_widget(cc)
                    continue

            if self.ignore_deployed and self.deployed_only:
                raise Exception("Can't both ignore and only show deployed.")

            if self.ignore_deployed:
                n = self.controller.deployment_machine_count_for_charm(cc)
                if n == cc.required_num_units() \
                   and self.controller.is_deployed(cc):
                    self.remove_service_widget(cc)
                    continue
            elif self.deployed_only:
                if not self.controller.is_deployed(cc):
                    self.remove_service_widget(cc)
                    continue

            state, _, _ = self.controller.get_charm_state(cc)
            if self.show_type == 'required':
                if state != CharmState.REQUIRED:
                    self.remove_service_widget(cc)
                    continue
            elif self.show_type == 'non-required':
                if state == CharmState.REQUIRED:
                    self.remove_service_widget(cc)
                    continue
                if not cc.allow_multi_units and \
                   not (self.controller.is_assigned(cc) or
                        self.controller.is_deployed(cc)):
                    self.remove_service_widget(cc)
                    continue

            sw = self.find_service_widget(cc)
            if sw is None:
                sw = self.add_service_widget(cc)
            sw.update()

    def add_service_widget(self, charm_class):
        if charm_class.subordinate:
            actions = self.subordinate_actions
        else:
            actions = self.actions
        sw = ServiceWidget(charm_class, self.controller, actions,
                           self.show_constraints,
                           show_placements=self.show_placements)
        self.service_widgets.append(sw)
        options = self.service_pile.options()
        self.service_pile.contents.append((sw, options))
        self.service_pile.contents.append((AttrMap(Padding(Divider('\u23bc'),
                                                           left=2, right=2),
                                                   'label'), options))
        return sw

    def remove_service_widget(self, charm_class):
        sw = self.find_service_widget(charm_class)

        if sw is None:
            return
        self.service_widgets.remove(sw)
        sw_idx = 0
        for w, opts in self.service_pile.contents:
            if w == sw:
                break
            sw_idx += 1

        c = self.service_pile.contents[:sw_idx] + \
            self.service_pile.contents[sw_idx + 2:]
        self.service_pile.contents = c
Example #9
0
class ServicesList(WidgetWrap):

    """A list of services (charm classes) with configurable action buttons
    for each machine.

    actions - a list of tuples describing buttons. Passed to
    ServiceWidget.

    machine - a machine instance to query for constraint checking

    show_constraints - bool, whether or not to show the constraints
    for the various services

    """

    def __init__(self, controller, actions, machine=None,
                 unplaced_only=False, show_type='all',
                 show_constraints=False,
                 title="Services"):
        self.controller = controller
        self.actions = actions
        self.service_widgets = []
        self.machine = machine
        self.unplaced_only = unplaced_only
        self.show_type = show_type
        self.show_constraints = show_constraints
        self.title = title
        w = self.build_widgets()
        self.update()
        super().__init__(w)

    def selectable(self):
        # overridden to ensure that we can arrow through the buttons
        # shouldn't be necessary according to documented behavior of
        # Pile & Columns, but discovered via trial & error.
        return True

    def build_widgets(self):
        self.service_pile = Pile([Text(self.title),
                                  Divider(' ')] +
                                 self.service_widgets)
        return self.service_pile

    def find_service_widget(self, cc):
        return next((sw for sw in self.service_widgets if
                     sw.charm_class.charm_name == cc.charm_name), None)

    def update(self):

        for cc in self.controller.charm_classes():
            if self.machine:
                if not satisfies(self.machine, cc.constraints)[0] \
                   or not self.controller.is_assigned(cc, self.machine):
                    self.remove_service_widget(cc)
                    continue

            if self.unplaced_only:
                n_units = self.controller.machine_count_for_charm(cc)
                if n_units == cc.required_num_units() \
                   and cc not in self.controller.unplaced_services:
                    self.remove_service_widget(cc)
                    continue

            is_required = self.controller.service_is_required(cc)
            if self.show_type == 'required':
                if not is_required:
                    self.remove_service_widget(cc)
                    continue
            elif self.show_type == 'non-required':
                if is_required:
                    self.remove_service_widget(cc)
                    continue
                if not cc.allow_multi_units and \
                        cc not in self.controller.unplaced_services:
                    self.remove_service_widget(cc)
                    continue

            sw = self.find_service_widget(cc)
            if sw is None:
                sw = self.add_service_widget(cc)
            sw.update()

    def add_service_widget(self, charm_class):
        sw = ServiceWidget(charm_class, self.controller, self.actions,
                           self.show_constraints)
        self.service_widgets.append(sw)
        options = self.service_pile.options()
        self.service_pile.contents.append((sw, options))
        self.service_pile.contents.append((AttrMap(Padding(Divider('\u23bc'),
                                                           left=2, right=2),
                                                   'label'), options))
        return sw

    def remove_service_widget(self, charm_class):
        sw = self.find_service_widget(charm_class)

        if sw is None:
            return
        self.service_widgets.remove(sw)
        sw_idx = 0
        for w, opts in self.service_pile.contents:
            if w == sw:
                break
            sw_idx += 1

        c = self.service_pile.contents[:sw_idx] + \
            self.service_pile.contents[sw_idx + 2:]
        self.service_pile.contents = c
Example #10
0
class JujuMachinesList(ContainerWidgetWrap):

    """A list of machines in a juju bundle, with configurable action
    buttons for each machine.

    application - an application

    machines - list of machine info dicts

    assign_cb - a function that takes a machine and assignmenttype to
    perform the button action

    unassign_cb - a function that takes a machine and clears assignments

    controller - a controller object that provides get_placements and get_pin.

    show_constraints - bool, whether or not to show the constraints
    for each of the machines

    title_widgets - A list of widgets to be used in place of the default
    title.

    show_assignments - bool, whether or not to show the assignments
    for each of the machines.

    show_filter_box - bool, show text box to filter listed machines

    """

    def __init__(self, application, machines, assign_cb, unassign_cb,
                 add_machine_cb, remove_machine_cb,
                 controller,
                 show_constraints=True,
                 title_widgets=None, show_assignments=True,
                 show_filter_box=False,
                 show_pins=False):
        self.application = application
        self.machines = machines
        self.assign_cb = assign_cb
        self.unassign_cb = unassign_cb
        self.add_machine_cb = add_machine_cb
        self.remove_machine_cb = remove_machine_cb
        self.controller = controller
        self.machine_widgets = []
        self.show_assignments = show_assignments
        self.all_assigned = False
        self.show_filter_box = show_filter_box
        self.show_pins = show_pins
        self.filter_string = ""
        w = self.build_widgets(title_widgets)
        self.update()
        super().__init__(w)

    def __repr__(self):
        return "machineslist"

    def selectable(self):
        # overridden to ensure that we can arrow through the buttons
        # shouldn't be necessary according to documented behavior of
        # Pile & Columns, but discovered via trial & error.
        return True

    def build_widgets(self, title_widgets):
        if title_widgets is None:
            title_widgets = [Text("Machines", align='center')]

        self.filter_edit_box = FilterBox(self.handle_filter_change)

        header_widgets = title_widgets + [Divider()]

        if self.show_filter_box:
            header_widgets += [self.filter_edit_box, Divider()]
        labels = ["ID", "Cores", "Memory (GiB)", "Storage (GiB)"]
        if self.show_assignments:
            labels += ["Assignments", ""]
        else:
            labels += [""]

        header_label_col = Columns([Text(m) for m in labels],
                                   dividechars=2)
        header_widgets.append(header_label_col)
        self.header_padding = len(header_widgets)
        self.add_new_button = SecondaryButton("Add New Machine",
                                              self.do_add_machine)
        self.add_new_cols = Columns([Text(s) for s in
                                     [' ', ' ', ' ', ' ', ' ']] +
                                    [self.add_new_button], dividechars=2)
        self.machine_pile = Pile(header_widgets + self.machine_widgets +
                                 [self.add_new_cols])
        return self.machine_pile

    def do_add_machine(self, sender):
        self.add_machine_cb()
        self.update()

    def remove_machine(self, sender):
        self.remove_machine_cb()
        self.update()

    def handle_filter_change(self, edit_button, userdata):
        self.filter_string = userdata
        self.update()

    def find_machine_widget(self, midx):
        return next((mw for mw in self.machine_widgets if
                     mw.juju_machine_id == midx), None)

    def update(self):

        for midx, md in sorted(self.machines.items()):
            allvalues = ["{}={}".format(k, v) for k, v in md.items()]
            filter_label = midx + " " + " ".join(allvalues)
            if self.filter_string != "" and \
               self.filter_string not in filter_label:
                self.remove_machine_widget(midx)
                continue

            mw = self.find_machine_widget(midx)
            if mw is None:
                mw = self.add_machine_widget(midx, md)
            mw.all_assigned = self.all_assigned
            mw.update()

        n = len(self.machine_widgets)
        self.filter_edit_box.set_info(n, n)

        self.sort_machine_widgets()

    def add_machine_widget(self, midx, md):
        mw = JujuMachineWidget(midx, md,
                               self.application,
                               self.assign_cb,
                               self.unassign_cb,
                               self.controller,
                               self.show_assignments,
                               self.show_pins)
        self.machine_widgets.append(mw)
        options = self.machine_pile.options()
        self.machine_pile.contents.insert(
            len(self.machine_pile.contents) - 1,
            (mw, options))

        return mw

    def remove_machine_widget(self, midx):
        mw = self.find_machine_widget(midx)
        if mw is None:
            return

        self.machine_widgets.remove(mw)
        mw_idx = 0
        for w, opts in self.machine_pile.contents:
            if w == mw:
                break
            mw_idx += 1

        c = self.machine_pile.contents[:mw_idx] + \
            self.machine_pile.contents[mw_idx + 1:]
        self.machine_pile.contents = c

    def sort_machine_widgets(self):

        self.machine_widgets.sort(key=attrgetter('juju_machine_id'))

        def wrappedkeyfunc(t):
            mw, options = t
            if isinstance(mw, JujuMachineWidget):
                return "B{}".format(mw.juju_machine_id)
            if mw in [self.add_new_cols]:
                return 'C'
            return 'A'

        self.machine_pile.contents.sort(key=wrappedkeyfunc)

    def focus_prev_or_top(self):
        self.update()
        try:
            if self.machine_pile.focus_position <= self.header_padding:
                self.machine_pile.focus_position = self.header_padding
        except IndexError:
            log.debug("index error in machines_list focus_top")
class CharmstoreColumn(WidgetWrap):

    def __init__(self, display_controller, placement_controller,
                 placement_view, metadata_controller):
        self.placement_controller = placement_controller
        self.display_controller = display_controller
        self.placement_view = placement_view
        self.metadata_controller = metadata_controller
        self.state = CharmstoreColumnUIState.RELATED
        self.prev_state = None
        self.current_search_string = ""
        w = self.build_widgets()
        super().__init__(w)
        self._related_charms = []
        self._bundle_results = []
        self._charm_results = []
        self._recommended_charms = []
        self.loading = True
        self.update()

    def build_widgets(self):
        self.title = Text('')
        self.pile = Pile([self.title])
        return self.pile

    def clear_search_results(self):
        self._bundle_results = []
        self._charm_results = []

    def handle_search_change(self, s):
        if s == "":
            self.state = CharmstoreColumnUIState.RELATED
            self.clear_search_results()
        else:
            self.state = CharmstoreColumnUIState.SEARCH_RESULTS
        self.current_search_string = s
        self.loading = True
        self.update()

    def remove_existing_charms(self, charms):
        existing_charms = [CharmStoreID(s.charm_source).as_str_without_rev()
                           for s in
                           self.placement_controller.bundle.services]
        return [c for c in charms
                if CharmStoreID(c['Id']).as_str_without_rev()
                not in existing_charms]

    def get_filtered_recommendations(self):
        opts = self.pile.options()
        if len(self._recommended_charms) == 0:
            mc = self.metadata_controller
            self._recommended_charms = mc.get_recommended_charms()
        return [(CharmWidget(d, self.do_add_charm, recommended=True), opts)
                for d in self.remove_existing_charms(self._recommended_charms)]

    def update(self):
        opts = self.pile.options()
        extra_widgets = []
        recommended_widgets = []
        if self.metadata_controller.loaded():
            recommended_widgets = self.get_filtered_recommendations()
            if len(recommended_widgets) > 0:
                top_w = recommended_widgets[0][0]
                top_w.set_header("Recommended Charms")

            self.loading = False

        series = self.placement_controller.bundle.series
        bundle_widgets = [(BundleWidget(d, self.do_add_bundle),
                           opts) for d in self._bundle_results
                          if 'bundle-metadata' in d.get('Meta', {}) and
                          'Series' in d['Meta']['bundle-metadata'] and
                          d['Meta']['bundle-metadata']['Series'] == series]

        if len(bundle_widgets) > 0:
            top_w = bundle_widgets[0][0]
            top_w.set_header("Bundles")

        filtered_charm_results = self.remove_existing_charms(
            self._charm_results)
        filtered_charm_results = filtered_charm_results[:10]

        charm_widgets = [(CharmWidget(d, self.do_add_charm),
                          opts) for d in filtered_charm_results
                         if 'charm-metadata' in d.get('Meta', {})]
        if len(charm_widgets) > 0:
            top_w = charm_widgets[0][0]
            top_w.set_header("Charms")

        if self.state == CharmstoreColumnUIState.RELATED:
            if self.loading:
                self.title.set_text("\nLoading Recommended "
                                    "and Popular Charms…")
            else:
                self.title.set_text("")
            extra_widgets = recommended_widgets
        else:
            if self.loading:
                msg = "\nSearching for '{}'…\n".format(
                    self.current_search_string)
                self.title.set_text(msg)
            else:
                bn = len(self._bundle_results)
                cn = len(filtered_charm_results)
                if bn + cn == 0:
                    advice = ""
                    if len(self.current_search_string) < 3:
                        advice = "Try a longer search string."
                    msg = ("\nNo charms found matching '{}' "
                           "{}".format(self.current_search_string,
                                       advice))
                else:
                    msg = ("\nShowing the top {} bundles and {} "
                           "charms matching {}:"
                           "".format(bn, cn, self.current_search_string))
            self.title.set_text(msg)

        self.pile.contents[1:] = extra_widgets + bundle_widgets + charm_widgets

    def add_results(self, bundle_results, charm_results):
        self._bundle_results += bundle_results
        self._bundle_results = self._bundle_results[:5]
        self._charm_results += charm_results

    def focus_prev_or_top(self):
        if len(self.pile.contents) < 2:
            return
        if self.state == CharmstoreColumnUIState.RELATED and self.loading \
           and len(self.pile.contents) > 2:
            self.pile.focus_position = 2
        else:
            self.pile.focus_position = 1

    def do_add_charm(self, charm_name, charm_dict):
        self.placement_view.do_add_charm(charm_name,
                                         charm_dict)
        self.update()

    def do_add_bundle(self, bundle_dict):
        self.placement_view.do_add_bundle(bundle_dict)

    def handle_error(self, e):
        msg = "Error searching for {}: {}".format(self.current_search_string,
                                                  e)
        self.title.set_text(msg)
Example #12
0
class JujuMachineWidget(WidgetWrap):
    """A widget displaying a machine and action buttons.

    juju_machine_id = juju id of the machine

    md = the machine dict

    application - the current application for which machines are being shown

    assign_cb - a function that takes a machine and assignmenttype to
    perform the button action

    unassign_cb - a function that takes a machine and removes
    assignments for the machine

    controller - a controller object that provides show_pin_chooser

    show_assignments - display info about which charms are assigned
    and what assignment type (LXC, KVM, etc) they have.

    show_pin - show button to pin juju machine to a maas machine

    """
    def __init__(self,
                 juju_machine_id,
                 md,
                 application,
                 assign_cb,
                 unassign_cb,
                 controller,
                 show_assignments=True,
                 show_pin=False):
        self.juju_machine_id = juju_machine_id
        self.md = md
        self.application = application
        self.is_selected = False
        self.assign_cb = assign_cb
        self.unassign_cb = unassign_cb
        self.controller = controller
        self.show_assignments = show_assignments
        self.show_pin = show_pin
        self.all_assigned = False

        w = self.build_widgets()
        super().__init__(w)
        self.update()

    def selectable(self):
        return True

    def __repr__(self):
        return "jujumachinewidget #" + str(self.juju_machine_id)

    def build_widgets(self):

        self.action_button_cols = Columns([])
        self.action_buttons = []
        self.build_unselected_widgets()
        self.pile = Pile([self.unselected_columns])
        return self.pile

    def build_unselected_widgets(self):
        cdict = juju.constraints_to_dict(self.md.get('constraints', ''))

        self.juju_machine_id_button = MenuSelectButton(
            '{:20s}'.format(self.juju_machine_id), self.show_pin_chooser)
        self.juju_machine_id_label = Text("{:20s}".format(
            self.juju_machine_id))
        self.cores_field = IntEdit('', str(cdict.get('cores', '')))
        connect_signal(self.cores_field, 'change', self.handle_cores_changed)
        memval = cdict.get('mem', '')
        if memval != '':
            memval = memval / 1024
        self.mem_field = Edit('', str(memval))
        connect_signal(self.mem_field, 'change', self.handle_mem_changed)
        diskval = cdict.get('root-disk', '')
        if diskval != '':
            diskval = diskval / 1024
        self.disk_field = Edit('', str(diskval))
        connect_signal(self.disk_field, 'change', self.handle_disk_changed)

        if self.show_pin:
            machine_id_w = self.juju_machine_id_button
        else:
            machine_id_w = self.juju_machine_id_label
        cols = [
            machine_id_w, self.cores_field, self.mem_field, self.disk_field
        ]
        cols = [AttrMap(w, 'string_input', 'string_input_focus') for w in cols]
        cols.append(Text("placeholder"))
        self.unselected_columns = Columns(cols, dividechars=2)
        self.update_assignments()
        return self.unselected_columns

    def update_assignments(self):
        assignments = []

        mps = self.controller.get_all_assignments(self.juju_machine_id)
        if len(mps) > 0:
            if self.show_assignments:
                ad = defaultdict(list)
                for application, atype in mps:
                    ad[atype].append(application)
                astr = " ".join([
                    "{}{}".format(
                        atype_to_label([atype])[0], ",".join(
                            [application.service_name for application in al]))
                    for atype, al in ad.items()
                ])
                assignments.append(astr)
        else:
            if self.show_assignments:
                assignments.append("-")

        if any([application == self.application for application, _ in mps]):
            action = self.do_remove
            label = "Remove"
        else:
            action = self.do_select
            label = "Select"

        self.select_button = PlainButton(label, action)

        cols = [Text(s) for s in assignments]

        current_assignments = [a for a, _ in mps if a == self.application]
        if self.all_assigned and len(current_assignments) == 0:
            cols.append(Text(""))
        else:
            cols += [
                AttrMap(self.select_button, 'text', 'button_secondary focus')
            ]
        opts = self.unselected_columns.options()
        self.unselected_columns.contents[4:] = [(w, opts) for w in cols]

    def update(self):
        self.update_action_buttons()

        if self.is_selected:
            self.update_selected()
        else:
            self.update_unselected()

    def update_selected(self):
        cn = self.application.service_name
        msg = Text("  Add {} to machine #{}:".format(cn, self.juju_machine_id))
        self.pile.contents = [(msg, self.pile.options()),
                              (self.action_button_cols, self.pile.options()),
                              (Divider(), self.pile.options())]

    def update_unselected(self):
        if self.show_pin:
            pinned_machine = self.controller.get_pin(self.juju_machine_id)

            if pinned_machine:
                pin_label = " {} \N{PENCIL}".format(pinned_machine.hostname)
            else:
                pin_label = " \N{PENCIL}"
            self.juju_machine_id_button.set_label(
                '{:20s}'.format(self.juju_machine_id + " " + pin_label))
        else:
            self.juju_machine_id_label.set_text('{:20s}'.format(
                self.juju_machine_id))

        self.pile.contents = [(self.unselected_columns, self.pile.options()),
                              (Divider(), self.pile.options())]
        self.update_assignments()

    def update_action_buttons(self):

        all_actions = [(AssignmentType.BareMetal, 'Add as Bare Metal',
                        self.select_baremetal),
                       (AssignmentType.LXD, 'Add as LXD', self.select_lxd),
                       (AssignmentType.KVM, 'Add as KVM', self.select_kvm)]

        sc = self.application
        if sc:
            allowed_set = set(sc.allowed_assignment_types)
            allowed_types = set([atype for atype, _, _ in all_actions])
            allowed_types = allowed_types.intersection(allowed_set)
        else:
            allowed_types = set()

        # + 1 for the cancel button:
        if len(self.action_buttons) == len(allowed_types) + 1:
            return

        self.action_buttons = [
            AttrMap(PlainButton(label, on_press=func), 'button_secondary',
                    'button_secondary focus')
            for atype, label, func in all_actions if atype in allowed_types
        ]
        self.action_buttons.append(
            AttrMap(PlainButton("Cancel", on_press=self.do_cancel),
                    'button_secondary', 'button_secondary focus'))

        opts = self.action_button_cols.options()
        self.action_button_cols.contents = [(b, opts)
                                            for b in self.action_buttons]

    def do_select(self, sender):
        self.is_selected = True
        self.update()
        self.pile.focus_position = 1
        self.action_button_cols.focus_position = 0

    def do_remove(self, sender):
        self.unassign_cb(self.juju_machine_id)

    def do_cancel(self, sender):
        self.is_selected = False
        self.update()
        self.pile.focus_position = 0

    def _do_select_assignment(self, atype):
        self.assign_cb(self.juju_machine_id, atype)
        self.pile.focus_position = 0
        self.is_selected = False
        self.update()

    def select_baremetal(self, sender):
        self._do_select_assignment(AssignmentType.BareMetal)

    def select_lxd(self, sender):
        self._do_select_assignment(AssignmentType.LXD)

    def select_kvm(self, sender):
        self._do_select_assignment(AssignmentType.KVM)

    def handle_cores_changed(self, sender, val):
        if val == '':
            self.md = self.controller.clear_constraint(self.juju_machine_id,
                                                       'cores')
        else:
            self.md = self.controller.set_constraint(self.juju_machine_id,
                                                     'cores', val)

    def _format_constraint(self, val):
        "Store constraints as int values of megabytes"
        if val.isnumeric():
            return int(val) * 1024
        else:
            return units.human_to_mb(val)

    def handle_mem_changed(self, sender, val):
        if val == '':
            self.md = self.controller.clear_constraint(self.juju_machine_id,
                                                       'mem')
        else:
            try:
                self.md = self.controller.set_constraint(
                    self.juju_machine_id, 'mem', self._format_constraint(val))
            except ValueError:
                return

    def handle_disk_changed(self, sender, val):
        if val == '':
            self.md = self.controller.clear_constraint(self.juju_machine_id,
                                                       'root-disk')
        else:
            try:
                self.md = self.controller.set_constraint(
                    self.juju_machine_id, 'root-disk',
                    self._format_constraint(val))
            except ValueError:
                return

    def show_pin_chooser(self, sender):
        self.controller.show_pin_chooser(self.juju_machine_id)
class ServiceWalkthroughView(WidgetWrap):
    def __init__(self, service, idx, n_total, metadata_controller, callback):
        self.callback = callback
        self.service = service
        self.idx = idx
        self.n_total = n_total
        self.n_remaining = n_total - idx - 1
        self.metadata_controller = metadata_controller
        self.info_handled = False
        w = self.build_widgets()
        super().__init__(w)
        self.get_async_info()
        if self.n_remaining == 0:
            self.pile.focus_position = len(self.pile.contents) - 1
        else:
            self.pile.focus_position = len(self.pile.contents) - 2

    def selectable(self):
        return True

    def build_widgets(self):
        self.description_w = Text("Description Loading…")
        self.readme_w = Text("README Loading…")
        self.scale_edit = IntegerEditor(default=self.service.num_units)
        connect_signal(self.scale_edit._edit, 'change',
                       self.handle_scale_changed)
        self.skip_rest_button = PlainButton(
            "Deploy all {} Remaining Applications with Bundle Defaults".format(
                self.n_remaining), self.do_skip_rest)
        col = Columns([(6, Text('Units:', align='right')),
                       (15,
                        Color.string_input(self.scale_edit,
                                           focus_map='string_input focus'))],
                      dividechars=1)

        if self.n_remaining == 0:
            buttons = [
                Padding.right_50(
                    Color.button_primary(PlainButton("Deploy and Continue",
                                                     self.do_deploy),
                                         focus_map='button_primary focus'))
            ]
        else:
            buttons = [
                Padding.right_50(
                    Color.button_primary(PlainButton(
                        "Deploy and Configure Next Application",
                        self.do_deploy),
                                         focus_map='button_primary focus')),
                Padding.right_50(
                    Color.button_secondary(self.skip_rest_button,
                                           focus_map='button_secondary focus'))
            ]

        ws = [
            Text("{} of {}: {}".format(self.idx + 1, self.n_total,
                                       self.service.service_name.upper())),
            Padding.center(HR()),
            Padding.center(self.description_w, left=2),
            Padding.line_break(""),
            Padding.center(self.readme_w, left=2),
            Padding.center(HR())
        ]

        if not self.service.subordinate:
            ws.append(Padding.left(col, left=1))

        ws.append(Padding.line_break(""))
        ws += buttons

        self.pile = Pile(ws)
        return Padding.center_90(Filler(self.pile, valign="top"))

    def get_async_info(self):
        info = self.metadata_controller.get_charm_info(
            self.service.csid.as_str_without_rev(), self.handle_info_updated)
        if info:
            self.handle_info_updated(info)

        self.metadata_controller.get_readme(self.service.csid.as_seriesname(),
                                            self.handle_readme_updated)

    def handle_info_updated(self, new_info):
        if self.info_handled:
            return
        self.info_handled = True
        EventLoop.loop.event_loop._loop.call_soon_threadsafe(
            self._update_info_on_main_thread, new_info)

    def _update_info_on_main_thread(self, new_info):
        self.description_w.set_text(
            new_info["Meta"]["charm-metadata"]["Summary"])
        self._invalidate()
        self.add_options()

    def add_options(self):
        service_id = self.service.csid.as_str_without_rev()
        options = self.metadata_controller.get_options(service_id)
        metadata = app.config.get('metadata', None)
        if metadata is None:
            return

        options_whitelist = metadata.get('options-whitelist', None)
        if options_whitelist is None:
            return

        svc_opts_whitelist = options_whitelist.get(self.service.service_name,
                                                   [])
        hidden = [n for n in options.keys() if n not in svc_opts_whitelist]
        log.info("Hiding options not in the whitelist: {}".format(hidden))
        for opname in svc_opts_whitelist:
            opdict = options[opname]
            self.add_option_widget(opname, opdict)

    def add_option_widget(self, opname, opdict):
        cv = self.service.options.get(opname, None)
        ow = OptionWidget(opname,
                          opdict['Type'],
                          opdict['Description'],
                          opdict['Default'],
                          current_value=cv,
                          value_changed_callback=self.handle_edit)

        self.pile.contents.insert(7, (ow, self.pile.options()))
        self._invalidate()
        return ow

    def handle_edit(self, opname, value):
        self.service.options[opname] = value

    def handle_readme_updated(self, readme_text_f):
        EventLoop.loop.event_loop._loop.call_soon_threadsafe(
            self._update_readme_on_main_thread, readme_text_f)

    def _update_readme_on_main_thread(self, readme_text_f):
        rls = readme_text_f.result().splitlines()
        rls = [l for l in rls if not l.startswith("#")]
        nrls = []
        for i in range(len(rls)):
            if i + 1 == len(rls):
                break
            if len(rls[i]) > 0:
                if rls[i][0] in ['-', '#', '=']:
                    continue
            if len(rls[i + 1]) > 0:
                if rls[i + 1][0] in ['-', '=']:
                    continue
            nrls.append(rls[i])

        if len(nrls) == 0:
            return

        if nrls[0] == '':
            nrls = nrls[1:]
        # split after two paragraphs:
        if '' in nrls:
            firstparidx = nrls.index('')
        else:
            firstparidx = 1
        try:
            splitidx = nrls.index('', firstparidx + 1)
        except:
            splitidx = firstparidx
        nrls = nrls[:splitidx]
        self.readme_w.set_text("\n".join(nrls))
        self._invalidate()

    def handle_scale_changed(self, widget, newvalstr):
        if newvalstr == '':
            return
        self.service.num_units = int(newvalstr)

    def do_deploy(self, arg):
        self.callback(single_service=self.service)

    def do_skip_rest(self, arg):
        self.callback(single_service=None)
Example #14
0
class MachinesColumn(WidgetWrap):
    """Shows machines or a link to MAAS to add more"""
    def __init__(self, display_controller, placement_controller,
                 placement_view):
        self.display_controller = display_controller
        self.placement_controller = placement_controller
        self.placement_view = placement_view
        w = self.build_widgets()
        super().__init__(w)
        self.update()

    def selectable(self):
        return True

    def build_widgets(self):
        def has_services_p(m):
            pc = self.placement_controller
            n = sum(
                [len(al) for at, al in pc.assignments_for_machine(m).items()])
            return n > 0

        clear_machine_func = self.placement_view.do_clear_machine
        show_chooser_func = self.placement_view.do_show_service_chooser

        self.open_maas_button = AttrMap(
            Button("Open in Browser", on_press=self.browse_maas),
            'button_secondary', 'button_secondary focus')

        bc = self.placement_view.config.juju_env['bootstrap-config']
        maasname = "'{}' <{}>".format(bc['name'], bc['maas-server'])
        maastitle = "Connected to MAAS {}".format(maasname)
        tw = Columns([
            Text(maastitle),
            Padding(self.open_maas_button,
                    align='right',
                    width=BUTTON_SIZE,
                    right=2)
        ])

        self.machines_list = MachinesList(
            self.placement_controller,
            [(has_services_p, 'Clear All Services', clear_machine_func),
             (has_services_p, 'Remove Some Services', show_chooser_func)],
            show_hardware=True,
            title_widgets=tw)
        self.machines_list.update()

        self.machines_list_pile = Pile([self.machines_list, Divider()])

        # placeholders replaced in update() with absolute indexes, so
        # if you change this list, check update().
        pl = [
            Text(('subheading', "Machines"), align='center'),
            Divider(),
            Pile([]),  # machines_list
            Divider()
        ]

        self.main_pile = Pile(pl)

        return self.main_pile

    def update(self):
        self.machines_list.update()

        bc = self.placement_view.config.juju_env['bootstrap-config']
        empty_maas_msg = ("There are no available machines.\n"
                          "Open {} to add machines to "
                          "'{}':".format(bc['maas-server'], bc['name']))

        self.empty_maas_widgets = Pile([
            Text([('error_icon', "\N{WARNING SIGN} "), empty_maas_msg],
                 align='center'),
            Padding(self.open_maas_button, align='center', width=BUTTON_SIZE)
        ])

        # 2 machines is the subordinate placeholder + juju default:
        if len(self.placement_controller.machines()) == 2:
            self.main_pile.contents[2] = (self.empty_maas_widgets,
                                          self.main_pile.options())
        else:
            self.main_pile.contents[2] = (self.machines_list_pile,
                                          self.main_pile.options())

    def browse_maas(self, sender):

        bc = self.placement_view.config.juju_env['bootstrap-config']
        try:
            p = Popen(["sensible-browser", bc['maas-server']],
                      stdout=PIPE,
                      stderr=PIPE)
            outs, errs = p.communicate(timeout=5)

        except TimeoutExpired:
            # went five seconds without an error, so we assume it's
            # OK. Don't kill it, just let it go:
            return
        e = errs.decode('utf-8')
        msg = "Error opening '{}' in a browser:\n{}".format(bc['name'], e)

        w = InfoDialogWidget(msg, self.placement_view.remove_overlay)
        self.placement_view.show_overlay(w)
class JujuMachinesList(WidgetWrap):

    """A list of machines in a juju bundle, with configurable action
    buttons for each machine.

    application - an application

    machines - list of machine info dicts

    assign_cb - a function that takes a machine and assignmenttype to
    perform the button action

    unassign_cb - a function that takes a machine and clears assignments

    controller - a controller object that provides get_placements and get_pin.

    show_constraints - bool, whether or not to show the constraints
    for each of the machines

    title_widgets - A list of widgets to be used in place of the default
    title.

    show_assignments - bool, whether or not to show the assignments
    for each of the machines.

    show_filter_box - bool, show text box to filter listed machines

    """

    def __init__(self, application, machines, assign_cb, unassign_cb,
                 add_machine_cb, remove_machine_cb,
                 controller,
                 show_constraints=True,
                 title_widgets=None, show_assignments=True,
                 show_filter_box=False,
                 show_pins=False):
        self.application = application
        self.machines = machines
        self.assign_cb = assign_cb
        self.unassign_cb = unassign_cb
        self.add_machine_cb = add_machine_cb
        self.remove_machine_cb = remove_machine_cb
        self.controller = controller
        self.machine_widgets = []
        self.show_assignments = show_assignments
        self.all_assigned = False
        self.show_filter_box = show_filter_box
        self.show_pins = show_pins
        self.filter_string = ""
        w = self.build_widgets(title_widgets)
        self.update()
        super().__init__(w)

    def __repr__(self):
        return "machineslist"

    def selectable(self):
        # overridden to ensure that we can arrow through the buttons
        # shouldn't be necessary according to documented behavior of
        # Pile & Columns, but discovered via trial & error.
        return True

    def build_widgets(self, title_widgets):
        if title_widgets is None:
            title_widgets = [Text("Machines", align='center')]

        self.filter_edit_box = FilterBox(self.handle_filter_change)

        header_widgets = title_widgets + [Divider()]

        if self.show_filter_box:
            header_widgets += [self.filter_edit_box, Divider()]
        labels = ["ID", "Cores", "Memory (GiB)", "Storage (GiB)"]
        if self.show_assignments:
            labels += ["Assignments", ""]
        else:
            labels += [""]

        header_label_col = Columns([Text(m) for m in labels],
                                   dividechars=2)
        header_widgets.append(header_label_col)
        self.header_padding = len(header_widgets)
        self.add_new_button = AttrMap(
            PlainButton("Add New Machine",
                        on_press=self.do_add_machine),
            'button_secondary',
            'button_secondary focus')
        self.add_new_cols = Columns([Text(s) for s in
                                     [' ', ' ', ' ', ' ', ' ']] +
                                    [self.add_new_button], dividechars=2)
        self.machine_pile = Pile(header_widgets + self.machine_widgets +
                                 [self.add_new_cols])
        return self.machine_pile

    def do_add_machine(self, sender):
        self.add_machine_cb()
        self.update()

    def remove_machine(self, sender):
        self.remove_machine_cb()
        self.update()

    def handle_filter_change(self, edit_button, userdata):
        self.filter_string = userdata
        self.update()

    def find_machine_widget(self, midx):
        return next((mw for mw in self.machine_widgets if
                     mw.juju_machine_id == midx), None)

    def update(self):

        for midx, md in sorted(self.machines.items()):
            allvalues = ["{}={}".format(k, v) for k, v in md.items()]
            filter_label = midx + " " + " ".join(allvalues)
            if self.filter_string != "" and \
               self.filter_string not in filter_label:
                self.remove_machine_widget(midx)
                continue

            mw = self.find_machine_widget(midx)
            if mw is None:
                mw = self.add_machine_widget(midx, md)
            mw.all_assigned = self.all_assigned
            mw.update()

        n = len(self.machine_widgets)
        self.filter_edit_box.set_info(n, n)

        self.sort_machine_widgets()

    def add_machine_widget(self, midx, md):
        mw = JujuMachineWidget(midx, md,
                               self.application,
                               self.assign_cb,
                               self.unassign_cb,
                               self.controller,
                               self.show_assignments,
                               self.show_pins)
        self.machine_widgets.append(mw)
        options = self.machine_pile.options()
        self.machine_pile.contents.insert(
            len(self.machine_pile.contents) - 1,
            (mw, options))

        return mw

    def remove_machine_widget(self, midx):
        mw = self.find_machine_widget(midx)
        if mw is None:
            return

        self.machine_widgets.remove(mw)
        mw_idx = 0
        for w, opts in self.machine_pile.contents:
            if w == mw:
                break
            mw_idx += 1

        c = self.machine_pile.contents[:mw_idx] + \
            self.machine_pile.contents[mw_idx + 1:]
        self.machine_pile.contents = c

    def sort_machine_widgets(self):

        self.machine_widgets.sort(key=attrgetter('juju_machine_id'))

        def wrappedkeyfunc(t):
            mw, options = t
            if isinstance(mw, JujuMachineWidget):
                return "B{}".format(mw.juju_machine_id)
            if mw in [self.add_new_cols]:
                return 'C'
            return 'A'

        self.machine_pile.contents.sort(key=wrappedkeyfunc)

    def focus_prev_or_top(self):
        self.update()
        try:
            if self.machine_pile.focus_position <= self.header_padding:
                self.machine_pile.focus_position = self.header_padding
        except IndexError:
            log.debug("index error in machines_list focus_top")
Example #16
0
class CloudView(WidgetWrap):
    def __init__(self, app, public_clouds, custom_clouds, cb=None):
        self.app = app
        self.cb = cb
        self.public_clouds = public_clouds
        self.custom_clouds = custom_clouds
        self.config = self.app.config
        self.buttons_pile_selected = False
        self.pile = None
        self.pile_localhost_idx = None

        self.frame = Frame(body=Padding.center_80(
            Filler(self._build_widget(), valign='top')),
                           footer=self._build_footer())
        super().__init__(self.frame)

    def keypress(self, size, key):
        if key in ['tab', 'shift tab']:
            self._swap_focus()
        return super().keypress(size, key)

    def _swap_focus(self):
        if not self.buttons_pile_selected:
            self.buttons_pile_selected = True
            self.frame.focus_position = 'footer'
            self.buttons_pile.focus_position = 1
        else:
            self.buttons_pile_selected = False
            self.frame.focus_position = 'body'

    def _build_buttons(self):
        cancel = menu_btn(on_press=self.cancel, label="\n  QUIT\n")
        buttons = [
            Padding.line_break(""),
            Color.menu_button(cancel, focus_map='button_primary focus'),
        ]
        self.buttons_pile = Pile(buttons)
        return self.buttons_pile

    def _build_footer(self):
        footer_pile = Pile([
            Padding.line_break(""),
            Color.frame_footer(
                Columns([('fixed', 2, Text("")),
                         ('fixed', 13, self._build_buttons())]))
        ])
        return footer_pile

    def _get_localhost_widget_idx(self):
        """ Returns index in pile where localhost widget resides
        """
        if self.pile_localhost_idx:
            return self.pile_localhost_idx

        else:
            for idx, item in enumerate(self.pile.contents):
                if hasattr(item[0], 'original_widget') and \
                   isinstance(item[0].original_widget, MenuSelectButton) and \
                   item[0].original_widget.get_label() == 'localhost':
                    return idx

    def _enable_localhost_widget(self):
        """ Sets the proper widget for localhost availability
        """
        idx = self._get_localhost_widget_idx()
        widget = Color.body(menu_btn(label=cloud_types.LOCALHOST,
                                     on_press=self.submit),
                            focus_map='menu_button focus')
        self._update_pile_at_idx(idx, widget)
        del self.pile.contents[idx + 1]

    def _update_pile_at_idx(self, idx, item):
        """ In place updates a widget in the self.pile contents
        """
        self.pile_localhost_idx = idx
        self.pile.contents[idx] = (item, self.pile.options())

    def _add_item(self, item):
        if not self.pile:
            self.pile = Pile([item])
        else:
            self.pile.contents.append((item, self.pile.options()))

    def _build_widget(self):
        if len(self.public_clouds) > 0:
            self._add_item(Text("Public Clouds"))
            self._add_item(HR())
            for item in self.public_clouds:
                self._add_item(
                    Color.body(menu_btn(label=item, on_press=self.submit),
                               focus_map='menu_button focus'))
            self._add_item(Padding.line_break(""))
        if len(self.custom_clouds) > 0:
            self._add_item(Text("Your Clouds"))
            self._add_item(HR())
            for item in self.custom_clouds:
                self._add_item(
                    Color.body(menu_btn(label=item, on_press=self.submit),
                               focus_map='menu_button focus'))
            self._add_item(Padding.line_break(""))
        new_clouds = juju.get_compatible_clouds(
            ['localhost', 'maas', 'vsphere'])
        if new_clouds:
            self._add_item(Text("Configure a New Cloud"))
            self._add_item(HR())
            for item in sorted(new_clouds):
                if item == 'localhost':
                    self._add_item(
                        Color.info_context(menu_btn(
                            label=cloud_types.LOCALHOST, on_press=None),
                                           focus_map='disabled_button'))
                    self._add_item(
                        Color.info_context(
                            Padding.center_90(
                                Text("LXD not found, please install with "
                                     "`sudo snap install lxd && lxd init` "
                                     "and wait for this message to disappear.")
                            )))
                else:
                    self._add_item(
                        Color.body(menu_btn(label=item, on_press=self.submit),
                                   focus_map='menu_button focus'))

        self.pile.focus_position = 2
        return self.pile

    def submit(self, result):
        self.cb(result.label)

    def cancel(self, btn):
        EventLoop.exit(0)
class NetworkSetDefaultRouteView(BaseView):
    def __init__(self, model, family, signal):
        self.model = model
        self.family = family
        self.signal = signal
        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 == netifaces.AF_INET:
                ip_providers = iface.ip4_providers
            elif self.family == netifaces.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(
                Color.menu_button(menu_btn(label="None", on_press=self.done),
                                  focus_map="menu_button focus")))
        for (gw, ifaces) in providers.items():
            if gw is None:
                continue
            items.append(
                Padding.center_79(
                    Color.menu_button(menu_btn(label="{gw} ({ifaces})".format(
                        gw=gw, ifaces=(",".join(ifaces))),
                                               on_press=self.done),
                                      focus_map="menu_button focus")))

        items.append(
            Padding.center_79(
                Color.menu_button(menu_btn(
                    label="Specify the default route manually",
                    on_press=self.show_edit_default_route),
                                  focus_map="menu_button focus")))
        return items

    def _build_buttons(self):
        cancel = cancel_btn(on_press=self.cancel)
        done = done_btn(on_press=self.done)

        buttons = [
            Color.button(done, focus_map='button focus'),
            Color.button(cancel, focus_map='button focus')
        ]
        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,
                               focus_map="string_input focus")),
                                             self.gateway_options.options())
        # self.signal.emit_signal('refresh')

    def done(self, result):
        log.debug("changing default gw: {}".format(result))

        gw_func = None
        if self.family == netifaces.AF_INET:
            gw_func = self.model.set_default_v4_gateway
        elif self.family == netifaces.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.signal.prev_signal()

    def cancel(self, button):
        self.signal.prev_signal()
class SimpleServiceWidget(WidgetWrap):
    """A widget displaying a service as a button

    service - the service to display

    placement_controller - a PlacementController instance

    display_controller - a PlacerView instance

    callback - a function to be called when either of the buttons is
    pressed. The service will be passed to the function as userdata.

    show_placements - display the machine(s) currently assigned to
    host this service, both planned deployments (aka 'assignments',
    and already-deployed, called 'deployments').

    """
    def __init__(self,
                 service,
                 placement_controller,
                 display_controller,
                 show_placements=False):
        self.service = service
        self.placement_controller = placement_controller
        self.display_controller = display_controller
        self.show_placements = show_placements
        self.state = ServiceWidgetState.UNSELECTED
        w = self.build_widgets()
        super().__init__(w)
        self.update()

    def selectable(self):
        return True

    def build_widgets(self):

        self.button = MenuSelectButton("I AM A SERVICE", self.do_select)

        self.action_button_cols = Columns([], dividechars=1)
        self.action_buttons = []

        self.pile = Pile([self.button])
        return self.pile

    def get_markup(self):
        if self.service.subordinate:
            return [
                self.service.service_name + " (subordinate)\n  " +
                self.service.charm_source
            ], []

        nr = self.service.required_num_units()
        pl = "s" if nr > 1 else ""
        title_markup = [
            self.service.service_name + "\n  {} unit{}: ".format(nr, pl) +
            self.service.charm_source
        ]
        info_markup = []

        if not self.display_controller.has_maas:
            return title_markup, info_markup

        pd = self.placement_controller.get_assignments(self.service)
        nplaced = sum([len(pd[k]) for k in pd])

        if nr - nplaced > 0:
            pl = ""
            if nr - nplaced > 1:
                pl = "s"
            info_str = ("  {} unit{} will be auto-placed "
                        "by Juju\n".format(nr - nplaced, pl))

            info_markup.append(info_str)

        def string_for_placement_dict(d):
            if self.display_controller.has_maas:
                defstring = "Bare Metal (Default)"
            else:
                defstring = "LXD (Default)"
            s = []
            for atype, ml in sorted(d.items()):
                if atype == AssignmentType.DEFAULT:
                    aname = defstring
                else:
                    aname = atype.name

                hostnames = [m.hostname for m in ml]
                s.append("    {}: {}".format(aname, ", ".join(hostnames)))
            if len(s) == 0:
                return []
            return "\n".join(s)

        ad = self.placement_controller.get_assignments(self.service)
        info_markup += string_for_placement_dict(ad)
        return title_markup, info_markup

    def update_choosing(self):
        title_markup, _ = self.get_markup()
        msg = Padding(Text(title_markup), left=2, right=2, align='center')
        self.pile.contents = [(msg, self.pile.options()),
                              (self.action_button_cols, self.pile.options()),
                              (Divider(), self.pile.options())]

    def update_default(self):
        title_markup, info_markup = self.get_markup()
        self.button.set_label(title_markup + ["\n"] + info_markup)
        if self.state == ServiceWidgetState.SELECTED:
            b = AttrMap(self.button, 'deploy_highlight_start',
                        'button_secondary focus')
        else:
            b = AttrMap(self.button, 'text', 'button_secondary focus')

        self.pile.contents = [(b, self.pile.options()),
                              (Divider(), self.pile.options())]

    def update(self):
        self.service = next(
            (s for s in self.placement_controller.bundle.services
             if s.service_name == self.service.service_name), self.service)
        self.update_action_buttons()

        if self.state == ServiceWidgetState.CHOOSING:
            self.update_choosing()
        else:
            self.update_default()

    def keypress(self, size, key):
        if key == 'backspace':
            self.display_controller.remove_service(self.service)
        elif key == '+':
            if not self.service.subordinate:
                self.display_controller.scale_service(self.service, 1)
        elif key == '-':
            if not self.service.subordinate:
                self.display_controller.scale_service(self.service, -1)

        return super().keypress(size, key)

    def update_action_buttons(self):
        all_actions = []
        if self.display_controller.has_maas:
            all_actions = [('Choose Placement',
                            self.handle_placement_button_pressed)]
        all_actions += [('Edit Relations',
                         self.handle_relation_button_pressed),
                        ('Edit Options', self.handle_options_button_pressed)]

        self.action_buttons = [
            AttrMap(PlainButton(label, on_press=func), 'button_secondary',
                    'button_secondary focus') for label, func in all_actions
        ]

        self.action_buttons.append(
            AttrMap(PlainButton("Cancel", on_press=self.do_cancel),
                    'button_secondary', 'button_secondary focus'))

        opts = self.action_button_cols.options()
        self.action_button_cols.contents = [(b, opts)
                                            for b in self.action_buttons]

    def do_select(self, sender):
        self.display_controller.clear_selections()
        if self.state == ServiceWidgetState.SELECTED:
            self.state = ServiceWidgetState.UNSELECTED
            self.display_controller.set_selected_service(None)
        else:
            self.display_controller.set_selected_service(self.service)
            self.state = ServiceWidgetState.CHOOSING
            self.pile.focus_position = 1
            self.action_button_cols.focus_position = 0
        self.update()

    def do_cancel(self, sender):
        self.state = ServiceWidgetState.UNSELECTED
        self.update()
        self.display_controller.show_default_view()
        self.pile.focus_position = 0

    def handle_placement_button_pressed(self, sender):
        self.state = ServiceWidgetState.SELECTED
        self.update()
        self.display_controller.edit_placement()
        self.pile.focus_position = 0

    def handle_relation_button_pressed(self, sender):
        self.state = ServiceWidgetState.SELECTED
        self.update()
        self.display_controller.edit_relations()
        self.pile.focus_position = 0

    def handle_options_button_pressed(self, sender):
        self.state = ServiceWidgetState.SELECTED
        self.update()
        self.display_controller.edit_options()
        self.pile.focus_position = 0
Example #19
0
class DeployView(WidgetWrap):

    def __init__(self, display_controller, placement_controller,
                 placement_view):
        self.display_controller = display_controller
        self.placement_controller = placement_controller
        self.placement_view = placement_view
        self.prev_status = None
        w = self.build_widgets()
        super().__init__(w)
        self.update()

    def selectable(self):
        return True

    def build_widgets(self):
        self.deploy_ok_msg = ("\u2713 All the core OpenStack services are "
                              "placed on a machine, and you can now deploy.")

        self.deploy_button = AttrMap(Button("Deploy",
                                            on_press=self.do_deploy),
                                     'button', 'button_focus')
        self.deploy_grid = GridFlow([self.deploy_button], 10, 1, 0, 'center')

        self.unplaced_msg = "Some core services are still unplaced."

        self.main_pile = Pile([Divider()])
        return self.main_pile

    def update(self):
        changed = self.prev_status != self.placement_controller.can_deploy()

        if self.placement_controller.can_deploy():
            if changed:
                self.show_deploy_button()
        else:
            self.main_pile.contents[0] = (Divider(),
                                          self.main_pile.options())
            if changed:
                self.display_controller.error_message(self.unplaced_msg)

        self.prev_status = self.placement_controller.can_deploy()

    def show_deploy_button(self):
        self.main_pile.contents[0] = (AttrMap(self.deploy_grid,
                                              'deploy_highlight_start'),
                                      self.main_pile.options())

        def fade_timeout(loop, step):
            if step == 1:
                self.display_controller.loop.set_alarm_in(5, fade_timeout, 2)
                new_attr = 'deploy_highlight_end'
            else:
                new_attr = ''
            self.main_pile.contents[0] = (AttrMap(self.deploy_grid,
                                                  new_attr),
                                          self.main_pile.options())

        self.display_controller.loop.set_alarm_in(4, fade_timeout, 1)
        self.display_controller.info_message(self.deploy_ok_msg)

    def do_deploy(self, sender):
        self.display_controller.commit_placement()
Example #20
0
class ServicesList(WidgetWrap):
    """A list of services with flexible display options.

    Note that not all combinations of display options make sense. YMMV.

    controller - a PlacementController

    display_controller - a PlacerView

    machine - a machine instance to query for constraint checking. If
    None, no constraint checking is done. If set, only services whose
    constraints are satisfied by 'machine' are shown.

    """
    def __init__(self,
                 placement_controller,
                 display_controller,
                 show_placements=False,
                 title="Services"):
        self.placement_controller = placement_controller
        self.display_controller = display_controller
        self.service_widgets = []
        self.show_placements = show_placements
        self.title = title
        w = self.build_widgets()
        self.update()
        super().__init__(w)

    def selectable(self):
        # overridden to ensure that we can arrow through the buttons
        # shouldn't be necessary according to documented behavior of
        # Pile & Columns, but discovered via trial & error.
        return True

    def build_widgets(self):
        widgets = []
        if self.title:
            widgets = [Text(self.title), Divider(' ')]
        widgets += self.service_widgets
        self.service_pile = Pile(widgets)
        return self.service_pile

    def focus_top_or_next(self):
        return self._setfocus(top=False)

    def focus_top(self):
        return self._setfocus(top=True)

    def _setfocus(self, top):
        try:
            if top:
                self.service_pile.focus_position = 0
            else:
                pos = self.service_pile.focus_position
                if pos + 1 >= len(self.service_widgets):
                    return False
                self.service_pile.focus_position = pos + 1
        except IndexError:
            log.debug("caught indexerror in servicescolumn.focus_next")
            return False
        return True

    def focused_service_widget(self):
        if len(self.service_widgets) > 0:
            return self.service_pile.focus
        return None

    def find_service_widget(self, s):
        return next((sw for sw in self.service_widgets
                     if sw.service.service_name == s.service_name), None)

    def update(self):
        for s in self.placement_controller.services():

            sw = self.find_service_widget(s)
            if sw is None:
                sw = self.add_service_widget(s)
            sw.update()

        allnames = [
            s.service_name for s in self.placement_controller.services()
        ]
        for sw in self.service_widgets:
            if sw.service.service_name not in allnames:
                self.remove_service_widget(sw.service)

        self.sort_service_widgets()

    def add_service_widget(self, service):
        sw = SimpleServiceWidget(service,
                                 self.placement_controller,
                                 self.display_controller,
                                 show_placements=self.show_placements)
        self.service_widgets.append(sw)
        options = self.service_pile.options()
        self.service_pile.contents.append((sw, options))

        # NOTE: see the + 1: indexing in remove_service_widget if you
        # re-add this divider. it should then be +2.

        # self.service_pile.contents.append((AttrMap(Padding(Divider('\u23bc'),
        #                                                    left=2, right=2),
        #                                            'label'), options))
        return sw

    def remove_service_widget(self, service):
        sw = self.find_service_widget(service)

        if sw is None:
            return
        self.service_widgets.remove(sw)
        sw_idx = 0
        for w, opts in self.service_pile.contents:
            if w == sw:
                break
            sw_idx += 1

        c = self.service_pile.contents[:sw_idx] + \
            self.service_pile.contents[sw_idx + 1:]
        self.service_pile.contents = c

    def sort_service_widgets(self):
        def keyfunc(sw):
            s = sw.service
            if s.subordinate:
                skey = 'z'
            else:
                skey = s.service_name
            return skey

        self.service_widgets.sort(key=keyfunc)

        def wrappedkeyfunc(t):
            mw, options = t
            if not isinstance(mw, SimpleServiceWidget):
                return 'A'
            return keyfunc(mw)

        self.service_pile.contents.sort(key=wrappedkeyfunc)

    def select_service(self, service_name):
        idx = 0
        for w, opts in self.service_pile.contents:
            if w.service.service_name == service_name:
                self.service_pile.focus_position = idx
                return
            idx += 1
class MachinesColumn(WidgetWrap):

    """Shows machines or a link to MAAS to add more"""

    def __init__(self, display_controller, placement_controller,
                 placement_view):
        self.display_controller = display_controller
        self.placement_controller = placement_controller
        self.placement_view = placement_view
        w = self.build_widgets()
        super().__init__(w)
        self.update()

    def selectable(self):
        return True

    def build_widgets(self):
        self.machines_list = MachinesList(self.placement_controller,
                                          self.display_controller,
                                          None,
                                          show_hardware=True,
                                          show_assignments=False,
                                          show_placeholders=False,
                                          show_only_ready=True,
                                          title_widgets=[])

        self.machines_list.update()

        self.machines_list_pile = Pile([self.machines_list,
                                        Divider()])

        return self.machines_list_pile

    def update(self):
        self.machines_list.update()
        maasinfo = self.placement_controller.maasinfo
        empty_maas_msg = ("There are no available machines.\n"
                          "Open {} to add machines to "
                          "'{}':".format(maasinfo['server_name'],
                                         maasinfo['server_hostname']))

        self.empty_maas_widgets = Pile([Text([('error_icon',
                                               "\N{WARNING SIGN} "),
                                              empty_maas_msg],
                                             align='center')])

        # 2 machines is the subordinate placeholder + juju default:
        opts = self.machines_list_pile.options()
        if len(self.placement_controller.machines()) == 2:
            self.machines_list_pile.contents[0] = (self.empty_maas_widgets,
                                                   opts)
        else:
            self.machines_list_pile.contents[0] = (self.machines_list,
                                                   opts)

    def clear_selections(self):
        for mw in self.machines_list.machine_widgets:
            mw.is_selected = False

    def focus_prev_or_top(self):
        self.update()
        try:
            self.machines_list_pile.focus_position = 0
            self.machines_list.focus_prev_or_top()
        except IndexError:
            log.debug("caught indexerror in machinesColumn focus_prev_or_top")
            pass
Example #22
0
def render_text(text, K):
    # XXX: make this code lazy-reader reader-proxy aware
    txts = [make_text(t, K.width) for t in text.split('\n')]
    K.txts = txts
    piles = []
    p = Pile([])
    for t in txts[:]:
        #if 'What emerges' in t.text: pu.db
        p.contents.append((t, p.options()))
        t_size = t.rows((K.width,))
        #if piles and h(piles[-1]) > height: pu.db
        while h(p, K) > K.height:
            # Add a new pile, and send the trimmings in there
            piles.append(p)
            d = h(t, K) - (h(p, K) - K.height)
            
            #if d <= 0: pu.db
            
            # start the next column
            p_new = Pile([])
            t_extra = trim(t, d, K.width)
            # TODO: include diff status in here, and line numbers
            p_new.contents.append((t_extra, p.options()))
            p = p_new
            t = t_extra


        #if piles and h(piles[-1]) > height:
        #    # ACK!
        #    break
        if h(p, K) == K.height:
            piles.append(p)
            # start the next column
            p = Pile([])

    # all done, don't forget the last pile which we haven't added to the list yet
    piles.append(p)


    palette = [
        ('black',  'light gray', 'black'),
        ('heading', 'black', 'light gray'),
        ('important', 'black', 'light cyan'),
        ('line', 'black', 'light gray'),
        ('options', 'dark gray', 'black'),
        ('focus heading', 'white', 'dark red'),
        ('focus line', 'black', 'dark red'),
        ('diff old', 'dark red', 'black'),
        ('diff new', 'dark green', 'black'),
        ('focus options', 'black', 'light gray'),
        ('pg normal',    'white',      'black', 'standout'),
        ('pg complete',  'white',      'dark magenta'),
        ('selected', 'white', 'dark blue')]

    #piles = urwid.ListBox(urwid.SimpleFocusListWalker(piles))
    #cols = piles
    #fill = cols
    dc = int(K.max_width / K.width) # number of displayed columns
    while len(piles)  < int(dc):
        piles.append(Pile([]))
    cols = urwid.Columns(piles[:dc], dividechars=1, min_width=K.width)
    K.cols = cols
    col_widths = cols.column_widths(K.screen.get_cols_rows())
    K.displayed_columns = len( col_widths )

    def tmp_generator():
        for x in piles:
            yield urwid.Columns([x], dividechars=1, min_width=K.width)
    K.reader = LazyReader(tmp_generator())


    # XXX: I need to subclass columns, and make it so the keypress function
    # "rolls" the piles under the hood, and re-renders all the widgets.
    #
    # self.contents.append(self.contents.pop(0))
    #
    #cols.box_columns.extend(cols.widget_list)


    #grid = urwid.GridFlow(txts, cell_width=20, h_sep=4, v_sep=0, align='left')
    fill = urwid.Filler(cols, 'top', top=K.top_margin)
    K.total_cols = len(piles)

    # XXX: this is not the full story, it ignores the borders between columns
    c_columns = [sum(col_widths[:i+1]) for i in  range(K.displayed_columns)]
    border = (K.max_width - c_columns[-1]) /  K.displayed_columns
    def xpos_to_col(pos):
        for i,c in enumerate(c_columns):
            if pos < (c + i * border):
                return i

    K.xpos_to_col = xpos_to_col
    pbar = ProgressBar('pg normal', 'pg complete', K.displayed_columns, K.total_cols)
    K.pbar = pbar


    p = urwid.ListBox(urwid.SimpleListWalker([pbar]))

    all = Pile([ fill, (1, p), ])
    cmd_line_text = urwid.Edit(K.fname)
    K.cmd_line_text = cmd_line_text
    #cmd_line_prompt = urwid.Text('hi there')
    #cmd_line_combined = urwid.Filler([cmd_line_prompt, cmd_line_text])
    #all = urwid.Frame(body=all, footer=cmd_line_combined)
    K.all = urwid.Frame(body=all, footer=cmd_line_text)
    K.loop = urwid.MainLoop(K.all, palette, K.screen, unhandled_input=show_or_exit)
    K.loop.exit = urwid.Text(" Help? ")

    #IPython.embed()
    if K.args.diff:
        set_cmd("set ft=diff".split(), K)
    elif have_pygments:
        set_cmd(("set ft=" + lexer.name.split()[0].lower()).split(), K)

    if K.args.quick:
        K.loop.set_alarm_in(1, lambda x,y:  quit())

    pbh = progress_bar_handler(p)
    K.pbh = pbh
    next(pbh)


    try:
        K.loop.run()
    except KeyboardInterrupt:
        pass
Example #23
0
class MachinesList(WidgetWrap):

    """A list of machines with configurable action buttons for each
    machine.

    actions - a list of ('label', function) pairs that wil be used to
    create buttons for each machine.  The machine will be passed to
    the function as userdata.

    constraints - a dict of constraints to filter the machines list.
    only machines matching all the constraints will be shown.

    show_hardware - bool, whether or not to show the hardware details
    for each of the machines

    title_widgets - A Text Widget to be used in place of the default
    title.

    show_assignments - bool, whether or not to show the assignments
    for each of the machines.

    """

    def __init__(self, controller, actions, constraints=None,
                 show_hardware=False, title_widgets=None,
                 show_assignments=True):
        self.controller = controller
        self.actions = actions
        self.machine_widgets = []
        if constraints is None:
            self.constraints = {}
        else:
            self.constraints = constraints
        self.show_hardware = show_hardware
        self.show_assignments = show_assignments
        self.filter_string = ""
        w = self.build_widgets(title_widgets)
        self.update()
        super().__init__(w)

    def selectable(self):
        # overridden to ensure that we can arrow through the buttons
        # shouldn't be necessary according to documented behavior of
        # Pile & Columns, but discovered via trial & error.
        return True

    def build_widgets(self, title_widgets):
        if title_widgets is None:
            if len(self.constraints) > 0:
                cstr = " matching constraints"
            else:
                cstr = ""

            title_widgets = Text("Machines" + cstr, align='center')

        self.filter_edit_box = FilterBox(self.handle_filter_change)

        self.machine_pile = Pile([title_widgets,
                                  Divider(),
                                  self.filter_edit_box] +
                                 self.machine_widgets)
        return self.machine_pile

    def handle_filter_change(self, edit_button, userdata):
        self.filter_string = userdata
        self.update()

    def find_machine_widget(self, m):
        return next((mw for mw in self.machine_widgets if
                     mw.machine.instance_id == m.instance_id), None)

    def update(self):
        machines = self.controller.machines()
        for mw in self.machine_widgets:
            machine = next((m for m in machines if
                            mw.machine.instance_id == m.instance_id), None)
            if machine is None:
                self.remove_machine(mw.machine)

        n_satisfying_machines = len(machines)

        for m in machines:
            if not satisfies(m, self.constraints)[0]:
                self.remove_machine(m)
                n_satisfying_machines -= 1
                continue

            assignment_names = ""
            ad = self.controller.assignments_for_machine(m)
            for atype, al in ad.items():
                assignment_names += " ".join(["{} {}".format(cc.charm_name,
                                                             cc.display_name)
                                              for cc in al])
            filter_label = "{} {}".format(m.filter_label(),
                                          assignment_names)

            if self.filter_string != "" and \
               self.filter_string not in filter_label:
                self.remove_machine(m)
                continue

            mw = self.find_machine_widget(m)
            if mw is None:
                mw = self.add_machine_widget(m)
            mw.update()

        self.filter_edit_box.set_info(len(self.machine_widgets),
                                      n_satisfying_machines)

    def add_machine_widget(self, machine):
        mw = MachineWidget(machine, self.controller, self.actions,
                           self.show_hardware, self.show_assignments)
        self.machine_widgets.append(mw)
        options = self.machine_pile.options()
        self.machine_pile.contents.append((mw, options))

        self.machine_pile.contents.append((AttrMap(Padding(Divider('\u23bc'),
                                                           left=2, right=2),
                                                   'label'), options))
        return mw

    def remove_machine(self, machine):
        mw = self.find_machine_widget(machine)

        if mw is None:
            return
        self.machine_widgets.remove(mw)
        mw_idx = 0
        for w, opts in self.machine_pile.contents:
            if w == mw:
                break
            mw_idx += 1

        c = self.machine_pile.contents[:mw_idx] + \
            self.machine_pile.contents[mw_idx + 2:]
        self.machine_pile.contents = c
Example #24
0
class MachinesList(WidgetWrap):

    """A list of machines with configurable action buttons for each
    machine.

    action - a function to call when the machine's button is pressed

    constraints - a dict of constraints to filter the machines list.
    only machines matching all the constraints will be shown.

    show_hardware - bool, whether or not to show the hardware details
    for each of the machines

    title_widgets - A Text Widget to be used in place of the default
    title.

    show_assignments - bool, whether or not to show the assignments
    for each of the machines.

    show_only_ready - bool, only show machines with a ready state.

    """

    def __init__(self, controller, display_controller,
                 constraints=None, show_hardware=False,
                 title_widgets=None, show_assignments=True,
                 show_placeholders=True, show_only_ready=False,
                 show_filter_box=False):
        self.controller = controller
        self.display_controller = display_controller
        self.machine_widgets = []
        if constraints is None:
            self.constraints = {}
        else:
            self.constraints = constraints
        self.show_hardware = show_hardware
        self.show_assignments = show_assignments
        self.show_placeholders = show_placeholders
        self.show_only_ready = show_only_ready
        self.show_filter_box = show_filter_box
        self.filter_string = ""
        w = self.build_widgets(title_widgets)
        self.update()
        super().__init__(w)

    def selectable(self):
        # overridden to ensure that we can arrow through the buttons
        # shouldn't be necessary according to documented behavior of
        # Pile & Columns, but discovered via trial & error.
        return True

    def build_widgets(self, title_widgets):
        if title_widgets is None:
            if len(self.constraints) > 0:
                cstr = " matching constraints"
            else:
                cstr = ""

            title_widgets = [Text("Machines" + cstr, align='center')]

        self.filter_edit_box = FilterBox(self.handle_filter_change)

        header_widgets = title_widgets + [Divider()]

        if self.show_filter_box:
            header_widgets.append(self.filter_edit_box)

        self.header_padding = len(header_widgets)
        self.machine_pile = Pile(header_widgets + self.machine_widgets)
        return self.machine_pile

    def handle_filter_change(self, edit_button, userdata):
        self.filter_string = userdata
        self.update()

    def find_machine_widget(self, m):
        return next((mw for mw in self.machine_widgets if
                     mw.machine.instance_id == m.instance_id), None)

    def update(self):
        machines = self.controller.machines(
            include_placeholders=self.show_placeholders)
        if self.show_only_ready:
            machines = [m for m in machines
                        if m.status == MaasMachineStatus.READY]
        for mw in self.machine_widgets:
            machine = next((m for m in machines if
                            mw.machine.instance_id == m.instance_id), None)
            if machine is None:
                self.remove_machine(mw.machine)

        n_satisfying_machines = len(machines)

        def get_placement_filter_label(d):
            s = ""
            for atype, al in d.items():
                s += " ".join(["{} {}".format(cc.service_name,
                                              cc.display_name)
                               for cc in al])
            return s

        for m in machines:
            if not satisfies(m, self.constraints)[0]:
                self.remove_machine(m)
                n_satisfying_machines -= 1
                continue

            assignment_names = ""
            ad = self.controller.assignments_for_machine(m)
            assignment_names = get_placement_filter_label(ad)
            filter_label = "{} {}".format(m.filter_label(),
                                          assignment_names)
            if self.filter_string != "" and \
               self.filter_string not in filter_label:
                self.remove_machine(m)
                continue

            mw = self.find_machine_widget(m)
            if mw is None:
                mw = self.add_machine_widget(m)
            mw.update()

        self.filter_edit_box.set_info(len(self.machine_widgets),
                                      n_satisfying_machines)

        self.sort_machine_widgets()

    def add_machine_widget(self, machine):
        mw = SimpleMachineWidget(machine,
                                 self.controller,
                                 self.display_controller,
                                 self.show_assignments)
        self.machine_widgets.append(mw)
        options = self.machine_pile.options()
        self.machine_pile.contents.append((mw, options))

        # NOTE: see the +1: indexing in remove_machine if you re-add
        # this divider. it should then be +2.

        # self.machine_pile.contents.append((AttrMap(Padding(Divider('\u23bc'),
        #                                                    left=2, right=2),
        #                                            'label'), options))
        return mw

    def remove_machine(self, machine):
        mw = self.find_machine_widget(machine)
        if mw is None:
            return

        self.machine_widgets.remove(mw)
        mw_idx = 0
        for w, opts in self.machine_pile.contents:
            if w == mw:
                break
            mw_idx += 1

        c = self.machine_pile.contents[:mw_idx] + \
            self.machine_pile.contents[mw_idx + 1:]
        self.machine_pile.contents = c

    def sort_machine_widgets(self):
        def keyfunc(mw):
            m = mw.machine
            hwinfo = " ".join(map(str, [m.arch, m.cpu_cores, m.mem,
                                        m.storage]))
            if str(mw.machine.status) == 'ready':
                skey = 'A'
            else:
                skey = str(mw.machine.status)
            return skey + mw.machine.hostname + hwinfo
        self.machine_widgets.sort(key=keyfunc)

        def wrappedkeyfunc(t):
            mw, options = t
            if not isinstance(mw, SimpleMachineWidget):
                return 'A'
            return keyfunc(mw)

        self.machine_pile.contents.sort(key=wrappedkeyfunc)

    def focus_prev_or_top(self):
        self.update()
        try:
            if self.machine_pile.focus_position <= self.header_padding:
                self.machine_pile.focus_position = self.header_padding
        except IndexError:
            log.debug("index error in machines_list focus_top")
class ServicesList(WidgetWrap):

    """A list of services with flexible display options.

    Note that not all combinations of display options make sense. YMMV.

    controller - a PlacementController

    display_controller - a PlacerView

    machine - a machine instance to query for constraint checking. If
    None, no constraint checking is done. If set, only services whose
    constraints are satisfied by 'machine' are shown.

    """

    def __init__(self, placement_controller, display_controller,
                 show_placements=False, title="Services"):
        self.placement_controller = placement_controller
        self.display_controller = display_controller
        self.service_widgets = []
        self.show_placements = show_placements
        self.title = title
        w = self.build_widgets()
        self.update()
        super().__init__(w)

    def selectable(self):
        # overridden to ensure that we can arrow through the buttons
        # shouldn't be necessary according to documented behavior of
        # Pile & Columns, but discovered via trial & error.
        return True

    def build_widgets(self):
        widgets = []
        if self.title:
            widgets = [Text(self.title), Divider(' ')]
        widgets += self.service_widgets
        self.service_pile = Pile(widgets)
        return self.service_pile

    def focus_top_or_next(self):
        return self._setfocus(top=False)

    def focus_top(self):
        return self._setfocus(top=True)

    def _setfocus(self, top):
        try:
            if top:
                self.service_pile.focus_position = 0
            else:
                pos = self.service_pile.focus_position
                if pos + 1 >= len(self.service_widgets):
                    return False
                self.service_pile.focus_position = pos + 1
        except IndexError:
            log.debug("caught indexerror in servicescolumn.focus_next")
            return False
        return True

    def focused_service_widget(self):
        if len(self.service_widgets) > 0:
            return self.service_pile.focus
        return None

    def find_service_widget(self, s):
        return next((sw for sw in self.service_widgets if
                     sw.service.service_name == s.service_name),
                    None)

    def update(self):
        for s in self.placement_controller.services():

            sw = self.find_service_widget(s)
            if sw is None:
                sw = self.add_service_widget(s)
            sw.update()

        allnames = [s.service_name
                    for s in self.placement_controller.services()]
        for sw in self.service_widgets:
            if sw.service.service_name not in allnames:
                self.remove_service_widget(sw.service)

        self.sort_service_widgets()

    def add_service_widget(self, service):
        sw = SimpleServiceWidget(service, self.placement_controller,
                                 self.display_controller,
                                 show_placements=self.show_placements)
        self.service_widgets.append(sw)
        options = self.service_pile.options()
        self.service_pile.contents.append((sw, options))

        # NOTE: see the + 1: indexing in remove_service_widget if you
        # re-add this divider. it should then be +2.

        # self.service_pile.contents.append((AttrMap(Padding(Divider('\u23bc'),
        #                                                    left=2, right=2),
        #                                            'label'), options))
        return sw

    def remove_service_widget(self, service):
        sw = self.find_service_widget(service)

        if sw is None:
            return
        self.service_widgets.remove(sw)
        sw_idx = 0
        for w, opts in self.service_pile.contents:
            if w == sw:
                break
            sw_idx += 1

        c = self.service_pile.contents[:sw_idx] + \
            self.service_pile.contents[sw_idx + 1:]
        self.service_pile.contents = c

    def sort_service_widgets(self):
        def keyfunc(sw):
            s = sw.service
            if s.subordinate:
                skey = 'z'
            else:
                skey = s.service_name
            return skey
        self.service_widgets.sort(key=keyfunc)

        def wrappedkeyfunc(t):
            mw, options = t
            if not isinstance(mw, SimpleServiceWidget):
                return 'A'
            return keyfunc(mw)

        self.service_pile.contents.sort(key=wrappedkeyfunc)

    def select_service(self, service_name):
        idx = 0
        for w, opts in self.service_pile.contents:
            if w.service.service_name == service_name:
                self.service_pile.focus_position = idx
                return
            idx += 1
Example #26
0
class CharmstoreColumn(WidgetWrap):

    def __init__(self, display_controller, placement_controller,
                 placement_view, metadata_controller):
        self.placement_controller = placement_controller
        self.display_controller = display_controller
        self.placement_view = placement_view
        self.metadata_controller = metadata_controller
        self.state = CharmstoreColumnUIState.RELATED
        self.prev_state = None
        self.current_search_string = ""
        w = self.build_widgets()
        super().__init__(w)
        self._related_charms = []
        self._bundle_results = []
        self._charm_results = []
        self._recommended_charms = []
        self.loading = True
        self.update()

    def build_widgets(self):
        self.title = Text('')
        self.pile = Pile([self.title])
        return self.pile

    def clear_search_results(self):
        self._bundle_results = []
        self._charm_results = []

    def handle_search_change(self, s):
        if s == "":
            self.state = CharmstoreColumnUIState.RELATED
            self.clear_search_results()
        else:
            self.state = CharmstoreColumnUIState.SEARCH_RESULTS
        self.current_search_string = s
        self.loading = True
        self.update()

    def remove_existing_charms(self, charms):
        existing_charms = [CharmStoreID(s.charm_source).as_str_without_rev()
                           for s in
                           self.placement_controller.bundle.services]
        return [c for c in charms
                if CharmStoreID(c['Id']).as_str_without_rev()
                not in existing_charms]

    def get_filtered_recommendations(self):
        opts = self.pile.options()
        if len(self._recommended_charms) == 0:
            mc = self.metadata_controller
            self._recommended_charms = mc.get_recommended_charms()
        return [(CharmWidget(d, self.do_add_charm, recommended=True), opts)
                for d in self.remove_existing_charms(self._recommended_charms)]

    def update(self):
        opts = self.pile.options()
        extra_widgets = []
        recommended_widgets = []
        if self.metadata_controller.loaded():
            recommended_widgets = self.get_filtered_recommendations()
            if len(recommended_widgets) > 0:
                top_w = recommended_widgets[0][0]
                top_w.set_header("Recommended Charms")

            self.loading = False

        series = self.placement_controller.bundle.series
        bundle_widgets = [(BundleWidget(d, self.do_add_bundle),
                           opts) for d in self._bundle_results
                          if 'bundle-metadata' in d.get('Meta', {}) and
                          'Series' in d['Meta']['bundle-metadata'] and
                          d['Meta']['bundle-metadata']['Series'] == series]

        if len(bundle_widgets) > 0:
            top_w = bundle_widgets[0][0]
            top_w.set_header("Bundles")

        filtered_charm_results = self.remove_existing_charms(
            self._charm_results)
        filtered_charm_results = filtered_charm_results[:10]

        charm_widgets = [(CharmWidget(d, self.do_add_charm),
                          opts) for d in filtered_charm_results
                         if 'charm-metadata' in d.get('Meta', {})]
        if len(charm_widgets) > 0:
            top_w = charm_widgets[0][0]
            top_w.set_header("Charms")

        if self.state == CharmstoreColumnUIState.RELATED:
            if self.loading:
                self.title.set_text("\nLoading Recommended "
                                    "and Popular Charms…")
            else:
                self.title.set_text("")
            extra_widgets = recommended_widgets
        else:
            if self.loading:
                msg = "\nSearching for '{}'…\n".format(
                    self.current_search_string)
                self.title.set_text(msg)
            else:
                bn = len(self._bundle_results)
                cn = len(filtered_charm_results)
                if bn + cn == 0:
                    advice = ""
                    if len(self.current_search_string) < 3:
                        advice = "Try a longer search string."
                    msg = ("\nNo charms found matching '{}' "
                           "{}".format(self.current_search_string,
                                       advice))
                else:
                    msg = ("\nShowing the top {} bundles and {} "
                           "charms matching {}:"
                           "".format(bn, cn, self.current_search_string))
            self.title.set_text(msg)

        self.pile.contents[1:] = extra_widgets + bundle_widgets + charm_widgets

    def add_results(self, bundle_results, charm_results):
        self._bundle_results += bundle_results
        self._bundle_results = self._bundle_results[:5]
        self._charm_results += charm_results

    def focus_prev_or_top(self):
        if len(self.pile.contents) < 2:
            return
        if self.state == CharmstoreColumnUIState.RELATED and self.loading \
           and len(self.pile.contents) > 2:
            self.pile.focus_position = 2
        else:
            self.pile.focus_position = 1

    def do_add_charm(self, charm_name, charm_dict):
        self.placement_view.do_add_charm(charm_name,
                                         charm_dict)
        self.update()

    def do_add_bundle(self, bundle_dict):
        self.placement_view.do_add_bundle(bundle_dict)

    def handle_error(self, e):
        msg = "Error searching for {}: {}".format(self.current_search_string,
                                                  e)
        self.title.set_text(msg)
Example #27
0
class MachinesList(WidgetWrap):

    """A list of machines with configurable action buttons for each
    machine.

    select_cb - a function that takes a machine and assignmenttype to
    perform the button action

    unselect_cb - a function that takes a machine and clears assignments

    target_info - a string that describes what you're picking a machine for

    current_pin_cb - a function that takes a machine and returns
    None if the machine is not currently selected, or returns a
    context value to show the user about the current selection (like a
    juju machine ID that represents the current pin)

    constraints - a dict of constraints to filter the machines list.
    only machines matching all the constraints will be shown.

    show_hardware - bool, whether or not to show the hardware details
    for each of the machines

    title_widgets - A Text Widget to be used in place of the default
    title.

    show_only_ready - bool, only show machines with a ready state.

    show_filter_box - bool, show text box to filter listed machines

    """

    def __init__(self, select_cb, unselect_cb, target_info,
                 current_pin_cb,
                 constraints=None, show_hardware=False,
                 title_widgets=None, show_only_ready=False,
                 show_filter_box=False):
        self.select_cb = select_cb
        self.unselect_cb = unselect_cb
        self.target_info = target_info
        self.current_pin_cb = current_pin_cb

        self.n_selected = 0
        self.machine_widgets = []
        if constraints is None:
            self.constraints = {}
        else:
            self.constraints = constraints
        self.show_hardware = show_hardware
        self.show_only_ready = show_only_ready
        self.show_filter_box = show_filter_box
        self.filter_string = ""
        self.loading = False
        w = self.build_widgets(title_widgets)
        self.update()
        super().__init__(w)

    def __repr__(self):
        return "machineslist"

    def selectable(self):
        # overridden to ensure that we can arrow through the buttons
        # shouldn't be necessary according to documented behavior of
        # Pile & Columns, but discovered via trial & error.
        return True

    def build_widgets(self, title_widgets):
        if title_widgets is None:
            if len(self.constraints) > 0:
                cstr = " matching constraints '{}'".format(
                    constraints_from_dict(self.constraints))
            else:
                cstr = ""

            title_widgets = [Text("Machines" + cstr, align='center')]

        self.filter_edit_box = FilterBox(self.handle_filter_change)

        header_widgets = title_widgets + [Divider()]

        if self.show_filter_box:
            header_widgets += [self.filter_edit_box, Divider()]
        labels = ["FQDN", "Cores", "Memory (GiB)", "Storage (GiB)", ""]
        header_label_col = Columns([Text(m) for m in labels])
        header_widgets.append(header_label_col)
        self.header_padding = len(header_widgets)
        self.machine_pile = Pile(header_widgets + self.machine_widgets)
        return self.machine_pile

    def handle_filter_change(self, edit_button, userdata):
        self.filter_string = userdata
        self.update()

    def find_machine_widget(self, m):
        return next((mw for mw in self.machine_widgets if
                     mw.machine.instance_id == m.instance_id), None)

    def update(self):
        if app.maas.client:
            machines = app.maas.client.get_machines()
        else:
            machines = None

        if machines is None:
            if not self.loading:
                self.loading = True
                p = (Text("\n\nLoading...", align='center'),
                     self.machine_pile.options())
                self.machine_pile.contents.append(p)
            return

        if self.loading:
            self.loading = False
            self.machine_pile.contents = self.machine_pile.contents[:-1]

        if self.show_only_ready:
            machines = [m for m in machines
                        if m.status == MaasMachineStatus.READY]
        for mw in self.machine_widgets:
            machine = next((m for m in machines if
                            mw.machine.instance_id == m.instance_id), None)
            if machine is None:
                self.remove_machine(mw.machine)

        n_satisfying_machines = len(machines)

        def get_placement_filter_label(d):
            s = ""
            for atype, al in d.items():
                s += " ".join(["{} {}".format(cc.service_name,
                                              cc.display_name)
                               for cc in al])
            return s

        for m in machines:
            if not satisfies(m, self.constraints)[0]:
                self.remove_machine(m)
                n_satisfying_machines -= 1
                continue

            filter_label = m.filter_label()
            if self.filter_string != "" and \
               self.filter_string not in filter_label:
                self.remove_machine(m)
                continue

            mw = self.find_machine_widget(m)
            if mw is None:
                mw = self.add_machine_widget(m)

            mw.update()

        self.filter_edit_box.set_info(len(self.machine_widgets),
                                      n_satisfying_machines)

        self.sort_machine_widgets()

    def add_machine_widget(self, machine):
        mw = MachineWidget(machine,
                           self.handle_select,
                           self.handle_unselect,
                           self.target_info,
                           self.current_pin_cb)

        self.machine_widgets.append(mw)
        options = self.machine_pile.options()
        self.machine_pile.contents.append((mw, options))

        return mw

    def remove_machine(self, machine):
        mw = self.find_machine_widget(machine)
        if mw is None:
            return

        self.machine_widgets.remove(mw)
        mw_idx = 0
        for w, opts in self.machine_pile.contents:
            if w == mw:
                break
            mw_idx += 1

        c = self.machine_pile.contents[:mw_idx] + \
            self.machine_pile.contents[mw_idx + 1:]
        self.machine_pile.contents = c

    def sort_machine_widgets(self):
        def keyfunc(mw):
            m = mw.machine
            hwinfo = " ".join(map(str, [m.arch, m.cpu_cores, m.mem,
                                        m.storage]))
            if str(mw.machine.status) == 'ready':
                skey = 'A'
            else:
                skey = str(mw.machine.status)
            return skey + mw.machine.hostname + hwinfo
        self.machine_widgets.sort(key=keyfunc)

        def wrappedkeyfunc(t):
            mw, options = t
            if not isinstance(mw, MachineWidget):
                return 'A'
            return keyfunc(mw)

        self.machine_pile.contents.sort(key=wrappedkeyfunc)

    def focus_prev_or_top(self):
        self.update()
        try:
            if self.machine_pile.focus_position <= self.header_padding:
                self.machine_pile.focus_position = self.header_padding
        except IndexError:
            log.debug("index error in machines_list focus_top")

    def handle_select(self, machine):
        self.select_cb(machine)
        self.n_selected += 1
        self.update()

    def handle_unselect(self, machine):
        self.unselect_cb(machine)
        self.n_selected -= 1
        self.update()
Example #28
0
class NewCloudView(WidgetWrap):

    def __init__(self, app, cloud, schema, cb):
        self.app = app
        self.cloud = cloud
        self.input_items = schema
        self.cb = cb
        self.frame = Frame(body=self._build_widget(),
                           footer=self._build_footer())
        self.buttons_selected = False
        super().__init__(self.frame)

    def _build_widget(self):
        total_items = [Text(
            "Enter your {} credentials:".format(self.cloud.upper()))]
        total_items += [HR()]
        for k in self.input_items.keys():
            display = k
            if k.startswith('_'):
                # Don't treat 'private' keys as input
                continue
            if k.startswith('@'):
                # Strip public, not storable attribute
                display = k[1:]
            col = Columns(
                [
                    ('weight', 0.5, Text(display, align='right')),
                    Color.string_input(self.input_items[k],
                                       focus_map='string_input focus')
                ], dividechars=1
            )
            total_items.append(col)
            total_items.append(Padding.line_break(""))
        total_items.append(Text(""))
        self.pile = Pile(total_items)
        return Padding.center_60(Filler(self.pile, valign="top"))

    def _build_footer(self):
        cancel = menu_btn(on_press=self.cancel,
                          label="\n  BACK\n")
        confirm = menu_btn(on_press=self.submit,
                           label="\n ADD CREDENTIAL\n")
        self.buttons = Columns([
            ('fixed', 2, Text("")),
            ('fixed', 13, Color.menu_button(
                cancel,
                focus_map='button_primary focus')),
            Text(""),
            ('fixed', 20, Color.menu_button(
                confirm,
                focus_map='button_primary focus')),
            ('fixed', 2, Text(""))
        ])

        footer = Color.frame_footer(Pile([
            Padding.line_break(""),
            self.buttons
        ]))
        return footer

    def _swap_focus(self):
        if not self.buttons_selected:
            self.buttons_selected = True
            self.frame.focus_position = 'footer'
            self.buttons.focus_position = 3
        else:
            self.buttons_selected = False
            self.frame.focus_position = 'body'

    def keypress(self, size, key):
        if key in ['tab', 'shift tab']:
            self._swap_focus()
        rv = super().keypress(size, key)
        return rv

    def validate(self):
        """ Will provide an error text if any fields are blank
        """
        values = [i.value for i in self.input_items.values()
                  if isinstance(i, StringEditor)]

        if None in values:
            self.pile.contents[-1] = (
                Padding.center_60(
                    Color.error_major(
                        Text("Please fill all required fields."))),
                self.pile.options())
            return False
        return True

    def cancel(self, btn):
        self.cb(back=True)

    def submit(self, result):
        if self.validate():
            self.cb(self.input_items)
class ApplicationConfigureView(WidgetWrap):

    def __init__(self, application, metadata_controller, controller):
        self.controller = controller
        self.application = application
        self.options_copy = self.application.options.copy()
        self.metadata_controller = metadata_controller
        self.widgets = self.build_widgets()
        self.description_w = Text("")
        self.showing_all = False
        self.buttons_selected = False
        self.frame = Frame(body=self.build_widgets(),
                           footer=self.build_footer())
        super().__init__(self.frame)

        self.metadata_controller.get_readme(
            self.application.csid.as_seriesname(),
            partial(self._handle_readme_load))

    def _handle_readme_load(self, readme_f):
        EventLoop.loop.event_loop._loop.call_soon_threadsafe(
            partial(self._update_readme_on_main_thread,
                    readme_f.result()))

    def _update_readme_on_main_thread(self, readme):
        rt = self._trim_readme(readme)
        self.description_w.set_text(rt)

    def _trim_readme(self, readme):
        rls = readme.splitlines()
        rls = [l for l in rls if not l.startswith("#")]
        nrls = []
        for i in range(len(rls)):
            if i + 1 == len(rls):
                break
            if len(rls[i]) > 0:
                if rls[i][0] in ['-', '#', '=']:
                    continue
            if len(rls[i + 1]) > 0:
                if rls[i + 1][0] in ['-', '=']:
                    continue
            nrls.append(rls[i])

        if len(nrls) == 0:
            return

        if nrls[0] == '':
            nrls = nrls[1:]
        # split after two paragraphs:
        if '' in nrls:
            firstparidx = nrls.index('')
        else:
            firstparidx = 1
        try:
            splitidx = nrls.index('', firstparidx + 1)
        except:
            splitidx = firstparidx
        nrls = nrls[:splitidx]
        return "\n".join(nrls)

    def selectable(self):
        return True

    def keypress(self, size, key):
        # handle keypress first, then get new focus widget
        rv = super().keypress(size, key)
        if key in ['tab', 'shift tab']:
            self._swap_focus()
        return rv

    def _swap_focus(self):
        if not self.buttons_selected:
            self.buttons_selected = True
            self.frame.focus_position = 'footer'
            self.buttons.focus_position = 3
        else:
            self.buttons_selected = False
            self.frame.focus_position = 'body'

    def build_widgets(self):
        ws = [Text("Configure {}".format(
            self.application.service_name))]
        num_unit_ow = OptionWidget("Units", "int",
                                   "How many units to deploy.",
                                   self.application.orig_num_units,
                                   current_value=self.application.num_units,
                                   value_changed_callback=self.handle_scale)
        ws.append(num_unit_ow)
        ws += self.get_whitelisted_option_widgets()
        self.toggle_show_all_button_index = len(ws) + 1
        self.toggle_show_all_button = PlainButton(
            "Show Advanced Configuration",
            self.do_toggle_show_all_config)
        ws += [HR(),
               Columns([('weight', 1, Text(" ")),
                        (36, Color.button_secondary(
                            self.toggle_show_all_button))])]
        self.pile = Pile(ws)
        return Padding.center_90(Filler(self.pile, valign="top"))

    def build_footer(self):
        cancel = menu_btn(on_press=self.do_cancel,
                          label="\n  BACK\n")
        confirm = menu_btn(on_press=self.do_commit,
                           label="\n APPLY CHANGES\n")
        self.buttons = Columns([
            ('fixed', 2, Text("")),
            ('fixed', 13, Color.menu_button(
                cancel,
                focus_map='button_primary focus')),
            Text(""),
            ('fixed', 20, Color.menu_button(
                confirm,
                focus_map='button_primary focus')),
            ('fixed', 2, Text(""))
        ])

        footer = Pile([
            HR(top=0),
            Padding.center_90(self.description_w),
            Padding.line_break(""),
            Color.frame_footer(Pile([
                Padding.line_break(""),
                self.buttons]))
        ])

        return footer

    def get_whitelisted_option_widgets(self):
        service_id = self.application.csid.as_str_without_rev()
        options = self.metadata_controller.get_options(service_id)

        svc_opts_whitelist = utils.get_options_whitelist(
            self.application.service_name)
        hidden = [n for n in options.keys() if n not in svc_opts_whitelist]
        log.info("Hiding options not in the whitelist: {}".format(hidden))

        return self._get_option_widgets(svc_opts_whitelist, options)

    def get_non_whitelisted_option_widgets(self):
        service_id = self.application.csid.as_str_without_rev()
        options = self.metadata_controller.get_options(service_id)

        svc_opts_whitelist = utils.get_options_whitelist(
            self.application.service_name)
        hidden = [n for n in options.keys() if n not in svc_opts_whitelist]
        return self._get_option_widgets(hidden, options)

    def _get_option_widgets(self, opnames, options):
        ws = []
        for opname in opnames:
            opdict = options[opname]
            cv = self.application.options.get(opname, None)
            ow = OptionWidget(opname,
                              opdict['Type'],
                              opdict['Description'],
                              opdict['Default'],
                              current_value=cv,
                              value_changed_callback=self.handle_edit)
            ws.append(ow)
        return ws

    def do_toggle_show_all_config(self, sender):
        if not self.showing_all:
            new_ows = self.get_non_whitelisted_option_widgets()
            header = Text("Advanced Configuration Options")
            opts = self.pile.options()
            self.pile.contents.append((header, opts))
            for ow in new_ows:
                self.pile.contents.append((ow, opts))
            self.toggle_show_all_button.set_label(
                "Hide Advanced Configuration")
            self.showing_all = True
        else:
            i = self.toggle_show_all_button_index
            self.pile.contents = self.pile.contents[:i + 1]
            self.toggle_show_all_button.set_label(
                "Show Advanced Configuration")
            self.showing_all = False

    def handle_edit(self, opname, value):
        self.options_copy[opname] = value

    def handle_scale(self, opname, scale):
        self.application.num_units = scale

    def do_cancel(self, sender):
        self.controller.handle_sub_view_done()

    def do_commit(self, sender):
        self.application.options = self.options_copy
        self.controller.handle_sub_view_done()
class RelationsColumn(WidgetWrap):

    """UI to edit relations of a service
    """

    def __init__(self, display_controller, placement_controller,
                 placement_view, metadata_controller):
        self.placement_controller = placement_controller
        self.metadata_controller = metadata_controller
        self.service = None
        self.provides = set()
        self.requires = set()
        self.placement_view = placement_view
        w = self.build_widgets()
        super().__init__(w)
        self.update()

    def build_widgets(self):
        self.title = Text('')
        self.relation_widgets = []
        self.pile = Pile([Divider(), self.title] + self.relation_widgets)
        return self.pile

    def refresh(self):
        self.set_service(self.service)

    def set_service(self, service):
        self.service = service
        self.metadata_controller.add_charm(service.csid.as_str_without_rev())
        self.pile.contents = self.pile.contents[:2]
        self.provides = set()
        self.requires = set()
        self.relation_widgets = []

    def update(self):
        if self.service is None:
            return

        self.title.set_text(('body', "Edit relations for {}".format(
            self.service.service_name)))

        if len(self.relation_widgets) == 0:
            self.title.set_text(('body', "Loading Relations..."))
        else:
            self.title.set_text(('body', "Edit Relations: (Changes are "
                                 "saved immediately)"))

        p = set(self.metadata_controller.get_provides(
            self.service.csid.as_str_without_rev()))
        r = set(self.metadata_controller.get_requires(
            self.service.csid.as_str_without_rev()))
        new_provides = p - self.provides
        self.provides.update(p)
        new_requires = r - self.requires
        self.requires.update(r)

        mc = self.metadata_controller
        args = [(relname, iface, RelationType.Provides,
                 mc.get_services_for_iface(iface, RelationType.Requires))
                for relname, iface in sorted(new_provides)]

        args += [(relname, iface, RelationType.Requires,
                  mc.get_services_for_iface(iface, RelationType.Provides))
                 for relname, iface in sorted(new_requires)]

        for relname, iface, reltype, matches in args:
            if len(matches) == 0:
                rw = NoRelationWidget(relname, iface, reltype)
                self.relation_widgets.append(rw)
                self.pile.contents.append((rw,
                                           self.pile.options()))

            for tgt_relname, tgt_service in matches:
                if tgt_service.service_name == self.service.service_name:
                    continue
                rw = RelationWidget(self.service.service_name, relname,
                                    iface, reltype, tgt_service.service_name,
                                    tgt_relname,
                                    self.placement_controller,
                                    self.do_select)
                self.relation_widgets.append(rw)
                self.pile.contents.append((rw,
                                           self.pile.options()))

        for w, _ in self.pile.contents[2:]:
            w.update()
        self.sort_relation_widgets()

    def focus_prev_or_top(self):
        # ? self.pile.focus_position = len(self.pile.contents) - 1

        if len(self.pile.contents) <= 2:
            return
        pos = self.pile.focus_position
        if pos < 2:
            self.pile.focus_position = 2

    def do_select(self, source_relname, tgt_service_name,
                  tgt_relation_name):
        self.placement_controller.toggle_relation(self.service.service_name,
                                                  source_relname,
                                                  tgt_service_name,
                                                  tgt_relation_name)
        self.update()

    def sort_relation_widgets(self):
        def keyfunc(rw):
            return str(rw.reltype) + rw.source_relname
        self.relation_widgets.sort(key=keyfunc)

        def wrappedkeyfunc(t):
            rw, options = t
            if isinstance(rw, RelationWidget):
                return keyfunc(rw)
            if isinstance(rw, NoRelationWidget):
                return 'z' + keyfunc(rw)
            return 'A'

        self.pile.contents.sort(key=wrappedkeyfunc)
Example #31
0
class OptionsColumn(WidgetWrap):

    """UI to edit options of a service
    """

    def __init__(self, display_controller, placement_controller,
                 placement_view, metadata_controller):
        self.placement_controller = placement_controller
        self.metadata_controller = metadata_controller
        self.service = None
        self.filter_string = ""
        self.placement_view = placement_view

        w = self.build_widgets()
        super().__init__(w)
        self.update()

    def build_widgets(self):
        self.title = Text('')
        self.option_widgets = []
        self.pile = Pile([Divider(), self.title] + self.option_widgets)
        return self.pile

    def refresh(self):
        self.set_service(self.service)

    def set_service(self, service):
        self.service = service
        self.metadata_controller.add_charm(service.csid.as_str_without_rev())
        self.pile.contents = self.pile.contents[:2]
        self.option_widgets = []

    def update(self):
        if self.service is None:
            return

        self.title.set_text(('body', "Edit Options for {}".format(
            self.service.service_name)))

        if len(self.option_widgets) == 0:
            if self.filter_string != "":
                self.title.set_text(
                    ('body',
                     "No options match '{}'".format(self.filter_string)))
            else:
                self.title.set_text(('body', "Loading Options..."))
        else:
            self.title.set_text(('body', "Edit Options: (Changes are "
                                 "saved immediately)"))

        mc = self.metadata_controller
        options = mc.get_options(self.service.csid.as_str_without_rev())

        for opname, opdict in sorted(options.items()):
            if self.filter_string != "" and \
               self.filter_string not in opname:
                self.remove_option_widget(opname)
                continue
            ow = self.find_option_widget(opname)
            if ow is None:
                ow = self.add_option_widget(opname, opdict)
            ow.update()

        # MMCC TODO set filterbox.set_info

        for w, _ in self.pile.contents[2:]:
            w.update()
        self.sort_option_widgets()

    def handle_filter_change(self, edit_button, userdata):
        self.filter_string = userdata
        self.update()

    def handle_edit(self, opname, value):
        self.placement_controller.set_option(self.service.service_name,
                                             opname, value)

    def find_option_widget(self, opname):
        return next((ow for ow in self.option_widgets if
                     ow.name == opname), None)

    def add_option_widget(self, opname, opdict):
        cv = self.service.options.get(opname, None)
        ow = OptionWidget(opname,
                          opdict['Type'],
                          opdict['Description'],
                          opdict['Default'],
                          current_value=cv,
                          value_changed_callback=self.handle_edit)

        self.option_widgets.append(ow)
        self.pile.contents.append((ow,
                                   self.pile.options()))
        return ow

    def remove_option_widget(self, opname):
        ow = self.find_option_widget(opname)
        if ow is None:
            return

        self.option_widgets.remove(ow)
        ow_idx = 0
        for w, opts in self.pile.contents:
            if w == ow:
                break
            ow_idx += 1

        c = self.pile.contents[:ow_idx] + \
            self.pile.contents[ow_idx + 1:]
        self.pile.contents = c

    def focus_prev_or_top(self):
        # ? self.pile.focus_position = len(self.pile.contents) - 1

        if len(self.pile.contents) <= 2:
            return
        pos = self.pile.focus_position
        if pos < 2:
            self.pile.focus_position = 2

    def sort_option_widgets(self):
        def keyfunc(ow):
            return str(ow.name)
        self.option_widgets.sort(key=keyfunc)

        def wrappedkeyfunc(t):
            rw, options = t
            if isinstance(rw, OptionWidget):
                return keyfunc(rw)
            return 'A'

        self.pile.contents.sort(key=wrappedkeyfunc)
class SimpleMachineWidget(WidgetWrap):

    """A widget displaying a machine. When selected, shows action buttons
    for placement types.

    machine - the machine to display

    controller - a PlacementController instance

    display_controller - a PlacerView instance

    show_assignments - display info about which charms are assigned
    and what assignment type (LXC, KVM, etc) they have.

    """

    def __init__(self, machine, controller,
                 display_controller, show_assignments=True):
        self.machine = machine
        self.controller = controller
        self.display_controller = display_controller
        self.show_assignments = show_assignments
        self.is_selected = False
        w = self.build_widgets()
        super().__init__(w)
        self.update()

    def selectable(self):
        return True

    def build_widgets(self):

        self.button = MenuSelectButton("I AM A MACHINE", self.do_select)
        self.action_button_cols = Columns([])
        self.action_buttons = []

        self.pile = Pile([self.button])
        return self.pile

    def update_machine(self):
        """Refresh with potentially updated machine info from controller.
        Assumes that machine exists - machines going away is handled
        in machineslist.update().
        """
        self.machine = next((m for m in self.controller.machines()
                             if m.instance_id == self.machine.instance_id),
                            None)

    def update(self):
        self.update_machine()
        self.update_action_buttons()

        if self.is_selected:
            self.update_selected()
        else:
            self.update_unselected()

    def info_markup(self):
        m = self.machine
        return [self.machine.hostname + "\n",
                'arch: {}  '.format(m.arch),
                'cores: {}  '.format(m.cpu_cores),
                'mem: {}  '.format(m.mem),
                'storage: {}'.format(m.storage)]

    def update_selected(self):
        cn = self.display_controller.selected_service.service_name
        msg = Text("  Add {} to {}:".format(cn,
                                            self.machine.hostname))
        self.pile.contents = [(msg, self.pile.options()),
                              (self.action_button_cols,
                               self.pile.options()),
                              (Divider(), self.pile.options())]

    def update_unselected(self):
        markup = self.info_markup()
        self.button.set_label(markup)
        self.pile.contents = [(AttrMap(self.button, 'text',
                                       'button_secondary focus'),
                               self.pile.options()),
                              (Divider(), self.pile.options())]

    def update_action_buttons(self):

        all_actions = [(AssignmentType.BareMetal,
                        'Add as Bare Metal',
                        self.select_baremetal),
                       (AssignmentType.LXD,
                        'Add as LXD',
                        self.select_lxd),
                       (AssignmentType.KVM,
                        'Add as KVM',
                        self.select_kvm)]

        sc = self.display_controller.selected_service
        if sc:
            allowed_set = set(sc.allowed_assignment_types)
            allowed_types = set([atype for atype, _, _ in all_actions])
            allowed_types = allowed_types.intersection(allowed_set)
        else:
            allowed_types = set()

        # + 1 for the cancel button:
        if len(self.action_buttons) == len(allowed_types) + 1:
            return

        self.action_buttons = [AttrMap(PlainButton(label,
                                                   on_press=func),
                                       'button_secondary',
                                       'button_secondary focus')
                               for atype, label, func in all_actions
                               if atype in allowed_types]
        self.action_buttons.append(
            AttrMap(PlainButton("Cancel",
                                on_press=self.do_cancel),
                    'button_secondary',
                    'button_secondary focus'))

        opts = self.action_button_cols.options()
        self.action_button_cols.contents = [(b, opts) for b in
                                            self.action_buttons]

    def do_select(self, sender):
        if self.display_controller.selected_service is None:
            return
        self.is_selected = True
        self.update()
        self.pile.focus_position = 1
        self.action_button_cols.focus_position = 0

    def do_cancel(self, sender):
        self.is_selected = False
        self.update()
        self.pile.focus_position = 0

    def _do_select_assignment(self, atype):
        {AssignmentType.BareMetal:
         self.display_controller.do_select_baremetal,
         AssignmentType.LXD:
         self.display_controller.do_select_lxd,
         AssignmentType.KVM:
         self.display_controller.do_select_kvm}[atype](self.machine)
        self.pile.focus_position = 0

    def select_baremetal(self, sender):
        self._do_select_assignment(AssignmentType.BareMetal)

    def select_lxd(self, sender):
        self._do_select_assignment(AssignmentType.LXD)

    def select_kvm(self, sender):
        self._do_select_assignment(AssignmentType.KVM)
class MachinesColumn(WidgetWrap):

    """Shows machines or a link to MAAS to add more"""

    def __init__(self, display_controller, placement_controller,
                 placement_view):
        self.display_controller = display_controller
        self.placement_controller = placement_controller
        self.placement_view = placement_view
        w = self.build_widgets()
        super().__init__(w)
        self.update()

    def selectable(self):
        return True

    def build_widgets(self):

        def has_services_p(m):
            pc = self.placement_controller
            n = sum([len(al) for at, al in
                     pc.assignments_for_machine(m).items()])
            return n > 0

        clear_machine_func = self.placement_view.do_clear_machine
        show_chooser_func = self.placement_view.do_show_service_chooser

        self.open_maas_button = AttrMap(Button("Open in Browser",
                                               on_press=self.browse_maas),
                                        'button_secondary',
                                        'button_secondary focus')

        bc = self.placement_view.config.juju_env['bootstrap-config']
        maasname = "'{}' <{}>".format(bc['name'], bc['maas-server'])
        maastitle = "Connected to MAAS {}".format(maasname)
        tw = Columns([Text(maastitle),
                      Padding(self.open_maas_button, align='right',
                              width=BUTTON_SIZE, right=2)])

        self.machines_list = MachinesList(self.placement_controller,
                                          [(has_services_p,
                                            'Clear All Services',
                                            clear_machine_func),
                                           (has_services_p,
                                            'Remove Some Services',
                                            show_chooser_func)],
                                          show_hardware=True,
                                          title_widgets=tw)
        self.machines_list.update()

        self.machines_list_pile = Pile([self.machines_list,
                                        Divider()])

        # placeholders replaced in update() with absolute indexes, so
        # if you change this list, check update().
        pl = [Text(('body', "Machines {}".format(MetaScroll().get_text()[0])),
                   align='center'),
              Divider(),
              Pile([]),         # machines_list
              Divider()]

        self.main_pile = Pile(pl)

        return self.main_pile

    def update(self):
        self.machines_list.update()

        bc = self.placement_view.config.juju_env['bootstrap-config']
        empty_maas_msg = ("There are no available machines.\n"
                          "Open {} to add machines to "
                          "'{}':".format(bc['maas-server'], bc['name']))

        self.empty_maas_widgets = Pile([Text([('error_icon',
                                               "\N{WARNING SIGN} "),
                                              empty_maas_msg],
                                             align='center'),
                                        Padding(self.open_maas_button,
                                                align='center',
                                                width=BUTTON_SIZE)])

        # 1 machine is the subordinate placeholder:
        if len(self.placement_controller.machines()) == 1:
            self.main_pile.contents[2] = (self.empty_maas_widgets,
                                          self.main_pile.options())
        else:
            self.main_pile.contents[2] = (self.machines_list_pile,
                                          self.main_pile.options())

    def browse_maas(self, sender):

        bc = self.placement_view.config.juju_env['bootstrap-config']
        try:
            p = Popen(["sensible-browser", bc['maas-server']],
                      stdout=PIPE, stderr=PIPE)
            outs, errs = p.communicate(timeout=5)

        except TimeoutExpired:
            # went five seconds without an error, so we assume it's
            # OK. Don't kill it, just let it go:
            return
        e = errs.decode('utf-8')
        msg = "Error opening '{}' in a browser:\n{}".format(bc['name'], e)

        w = InfoDialogWidget(msg, self.placement_view.remove_overlay)
        self.placement_view.show_overlay(w)
class ServiceWalkthroughView(WidgetWrap):
    def __init__(self, service, idx, n_total, metadata_controller, callback):
        self.callback = callback
        self.service = service
        self.idx = idx
        self.n_total = n_total
        self.n_remaining = n_total - idx - 1
        self.metadata_controller = metadata_controller
        self.info_handled = False
        w = self.build_widgets()
        super().__init__(w)
        self.get_async_info()
        if self.n_remaining == 0:
            self.pile.focus_position = len(self.pile.contents) - 1
        else:
            self.pile.focus_position = len(self.pile.contents) - 2

    def selectable(self):
        return True

    def build_widgets(self):
        self.description_w = Text("Description Loading…")
        self.readme_w = Text("README Loading…")
        self.scale_edit = IntegerEditor(default=self.service.num_units)
        connect_signal(self.scale_edit._edit, 'change',
                       self.handle_scale_changed)
        self.skip_rest_button = PlainButton(
            "Deploy all {} Remaining Applications with Bundle Defaults".format(
                self.n_remaining),
            self.do_skip_rest
        )
        col = Columns(
            [
                (6, Text('Units:', align='right')),
                (15,
                 Color.string_input(self.scale_edit,
                                    focus_map='string_input focus'))
            ], dividechars=1
        )

        if self.n_remaining == 0:
            buttons = [Padding.right_50(Color.button_primary(
                PlainButton("Deploy and Continue",
                            self.do_deploy),
                focus_map='button_primary focus'))]
        else:
            buttons = [
                Padding.right_50(Color.button_primary(
                    PlainButton(
                        "Deploy and Configure Next Application",
                        self.do_deploy),
                    focus_map='button_primary focus')),
                Padding.right_50(
                    Color.button_secondary(
                        self.skip_rest_button,
                        focus_map='button_secondary focus'))]

        ws = [Text("{} of {}: {}".format(self.idx+1, self.n_total,
                                         self.service.service_name.upper())),
              Padding.center(HR()),
              Padding.center(self.description_w, left=2),
              Padding.line_break(""),
              Padding.center(self.readme_w, left=2),
              Padding.center(HR())]

        if not self.service.subordinate:
            ws.append(Padding.left(col, left=1))

        ws.append(Padding.line_break(""))
        ws += buttons

        self.pile = Pile(ws)
        return Padding.center_90(Filler(self.pile, valign="top"))

    def get_async_info(self):
        info = self.metadata_controller.get_charm_info(
            self.service.csid.as_str_without_rev(),
            self.handle_info_updated)
        if info:
            self.handle_info_updated(info)

        self.metadata_controller.get_readme(
            self.service.csid.as_seriesname(),
            self.handle_readme_updated)

    def handle_info_updated(self, new_info):
        if self.info_handled:
            return
        self.info_handled = True
        EventLoop.loop.event_loop._loop.call_soon_threadsafe(
            self._update_info_on_main_thread,
            new_info)

    def _update_info_on_main_thread(self, new_info):
        self.description_w.set_text(
            new_info["Meta"]["charm-metadata"]["Summary"])
        self._invalidate()
        self.add_options()

    def add_options(self):
        service_id = self.service.csid.as_str_without_rev()
        options = self.metadata_controller.get_options(service_id)
        metadata = app.config.get('metadata', None)
        if metadata is None:
            return

        options_whitelist = metadata.get('options-whitelist', None)
        if options_whitelist is None:
            return

        svc_opts_whitelist = options_whitelist.get(self.service.service_name,
                                                   [])
        hidden = [n for n in options.keys() if n not in svc_opts_whitelist]
        log.info("Hiding options not in the whitelist: {}".format(hidden))
        for opname in svc_opts_whitelist:
            opdict = options[opname]
            self.add_option_widget(opname, opdict)

    def add_option_widget(self, opname, opdict):
        cv = self.service.options.get(opname, None)
        ow = OptionWidget(opname,
                          opdict['Type'],
                          opdict['Description'],
                          opdict['Default'],
                          current_value=cv,
                          value_changed_callback=self.handle_edit)

        self.pile.contents.insert(7, (ow, self.pile.options()))
        self._invalidate()
        return ow

    def handle_edit(self, opname, value):
        self.service.options[opname] = value

    def handle_readme_updated(self, readme_text_f):
        EventLoop.loop.event_loop._loop.call_soon_threadsafe(
            self._update_readme_on_main_thread,
            readme_text_f)

    def _update_readme_on_main_thread(self, readme_text_f):
        rls = readme_text_f.result().splitlines()
        rls = [l for l in rls if not l.startswith("#")]
        nrls = []
        for i in range(len(rls)):
            if i+1 == len(rls):
                break
            if len(rls[i]) > 0:
                if rls[i][0] in ['-', '#', '=']:
                    continue
            if len(rls[i+1]) > 0:
                if rls[i+1][0] in ['-', '=']:
                    continue
            nrls.append(rls[i])

        if len(nrls) == 0:
            return

        if nrls[0] == '':
            nrls = nrls[1:]
        # split after two paragraphs:
        if '' in nrls:
            firstparidx = nrls.index('')
        else:
            firstparidx = 1
        try:
            splitidx = nrls.index('', firstparidx + 1)
        except:
            splitidx = firstparidx
        nrls = nrls[:splitidx]
        self.readme_w.set_text("\n".join(nrls))
        self._invalidate()

    def handle_scale_changed(self, widget, newvalstr):
        if newvalstr == '':
            return
        self.service.num_units = int(newvalstr)

    def do_deploy(self, arg):
        self.callback(single_service=self.service)

    def do_skip_rest(self, arg):
        self.callback(single_service=None)
class SimpleServiceWidget(WidgetWrap):

    """A widget displaying a service as a button

    service - the service to display

    placement_controller - a PlacementController instance

    display_controller - a PlacerView instance

    callback - a function to be called when either of the buttons is
    pressed. The service will be passed to the function as userdata.

    show_placements - display the machine(s) currently assigned to
    host this service, both planned deployments (aka 'assignments',
    and already-deployed, called 'deployments').

    """

    def __init__(self, service, placement_controller,
                 display_controller, show_placements=False):
        self.service = service
        self.placement_controller = placement_controller
        self.display_controller = display_controller
        self.show_placements = show_placements
        self.state = ServiceWidgetState.UNSELECTED
        w = self.build_widgets()
        super().__init__(w)
        self.update()

    def selectable(self):
        return True

    def build_widgets(self):

        self.button = MenuSelectButton("I AM A SERVICE", self.do_select)

        self.action_button_cols = Columns([], dividechars=1)
        self.action_buttons = []

        self.pile = Pile([self.button])
        return self.pile

    def get_markup(self):
        if self.service.subordinate:
            return [self.service.service_name +
                    " (subordinate)\n  " +
                    self.service.charm_source], []

        nr = self.service.required_num_units()
        pl = "s" if nr > 1 else ""
        title_markup = [self.service.service_name +
                        "\n  {} unit{}: ".format(nr, pl) +
                        self.service.charm_source]
        info_markup = []

        if not self.display_controller.has_maas:
            return title_markup, info_markup

        pd = self.placement_controller.get_assignments(self.service)
        nplaced = sum([len(pd[k]) for k in pd])

        if nr-nplaced > 0:
            pl = ""
            if nr-nplaced > 1:
                pl = "s"
            info_str = ("  {} unit{} will be auto-placed "
                        "by Juju\n".format(nr-nplaced, pl))

            info_markup.append(info_str)

        def string_for_placement_dict(d):
            if self.display_controller.has_maas:
                defstring = "Bare Metal (Default)"
            else:
                defstring = "LXD (Default)"
            s = []
            for atype, ml in sorted(d.items()):
                if atype == AssignmentType.DEFAULT:
                    aname = defstring
                else:
                    aname = atype.name

                hostnames = [m.hostname for m in ml]
                s.append("    {}: {}".format(aname,
                                             ", ".join(hostnames)))
            if len(s) == 0:
                return []
            return "\n".join(s)

        ad = self.placement_controller.get_assignments(self.service)
        info_markup += string_for_placement_dict(ad)
        return title_markup, info_markup

    def update_choosing(self):
        title_markup, _ = self.get_markup()
        msg = Padding(Text(title_markup), left=2, right=2, align='center')
        self.pile.contents = [(msg, self.pile.options()),
                              (self.action_button_cols,
                               self.pile.options()),
                              (Divider(), self.pile.options())]

    def update_default(self):
        title_markup, info_markup = self.get_markup()
        self.button.set_label(title_markup + ["\n"] + info_markup)
        if self.state == ServiceWidgetState.SELECTED:
            b = AttrMap(self.button, 'deploy_highlight_start',
                        'button_secondary focus')
        else:
            b = AttrMap(self.button, 'text', 'button_secondary focus')

        self.pile.contents = [(b, self.pile.options()),
                              (Divider(), self.pile.options())]

    def update(self):
        self.service = next((s for s in
                             self.placement_controller.bundle.services
                             if s.service_name == self.service.service_name),
                            self.service)
        self.update_action_buttons()

        if self.state == ServiceWidgetState.CHOOSING:
            self.update_choosing()
        else:
            self.update_default()

    def keypress(self, size, key):
        if key == 'backspace':
            self.display_controller.remove_service(self.service)
        elif key == '+':
            if not self.service.subordinate:
                self.display_controller.scale_service(self.service, 1)
        elif key == '-':
            if not self.service.subordinate:
                self.display_controller.scale_service(self.service, -1)

        return super().keypress(size, key)

    def update_action_buttons(self):
        all_actions = []
        if self.display_controller.has_maas:
            all_actions = [('Choose Placement',
                            self.handle_placement_button_pressed)]
        all_actions += [('Edit Relations',
                         self.handle_relation_button_pressed),
                        ('Edit Options',
                         self.handle_options_button_pressed)]

        self.action_buttons = [AttrMap(PlainButton(label, on_press=func),
                                       'button_secondary',
                                       'button_secondary focus')
                               for label, func in all_actions]

        self.action_buttons.append(AttrMap(
            PlainButton("Cancel",
                        on_press=self.do_cancel),
            'button_secondary',
            'button_secondary focus'))

        opts = self.action_button_cols.options()
        self.action_button_cols.contents = [(b, opts) for
                                            b in self.action_buttons]

    def do_select(self, sender):
        self.display_controller.clear_selections()
        if self.state == ServiceWidgetState.SELECTED:
            self.state = ServiceWidgetState.UNSELECTED
            self.display_controller.set_selected_service(None)
        else:
            self.display_controller.set_selected_service(self.service)
            self.state = ServiceWidgetState.CHOOSING
            self.pile.focus_position = 1
            self.action_button_cols.focus_position = 0
        self.update()

    def do_cancel(self, sender):
        self.state = ServiceWidgetState.UNSELECTED
        self.update()
        self.display_controller.show_default_view()
        self.pile.focus_position = 0

    def handle_placement_button_pressed(self, sender):
        self.state = ServiceWidgetState.SELECTED
        self.update()
        self.display_controller.edit_placement()
        self.pile.focus_position = 0

    def handle_relation_button_pressed(self, sender):
        self.state = ServiceWidgetState.SELECTED
        self.update()
        self.display_controller.edit_relations()
        self.pile.focus_position = 0

    def handle_options_button_pressed(self, sender):
        self.state = ServiceWidgetState.SELECTED
        self.update()
        self.display_controller.edit_options()
        self.pile.focus_position = 0
Example #36
0
class OptionsColumn(WidgetWrap):
    """UI to edit options of a service
    """
    def __init__(self, display_controller, placement_controller,
                 placement_view, metadata_controller):
        self.placement_controller = placement_controller
        self.metadata_controller = metadata_controller
        self.service = None
        self.filter_string = ""
        self.placement_view = placement_view

        w = self.build_widgets()
        super().__init__(w)
        self.update()

    def build_widgets(self):
        self.title = Text('')
        self.option_widgets = []
        self.pile = Pile([Divider(), self.title] + self.option_widgets)
        return self.pile

    def refresh(self):
        self.set_service(self.service)

    def set_service(self, service):
        self.service = service
        self.metadata_controller.add_charm(service.csid.as_str_without_rev())
        self.pile.contents = self.pile.contents[:2]
        self.option_widgets = []

    def update(self):
        if self.service is None:
            return

        self.title.set_text(
            ('body', "Edit Options for {}".format(self.service.service_name)))

        if len(self.option_widgets) == 0:
            if self.filter_string != "":
                self.title.set_text(
                    ('body',
                     "No options match '{}'".format(self.filter_string)))
            else:
                self.title.set_text(('body', "Loading Options..."))
        else:
            self.title.set_text(('body', "Edit Options: (Changes are "
                                 "saved immediately)"))

        mc = self.metadata_controller
        options = mc.get_options(self.service.csid.as_str_without_rev())

        for opname, opdict in sorted(options.items()):
            if self.filter_string != "" and \
               self.filter_string not in opname:
                self.remove_option_widget(opname)
                continue
            ow = self.find_option_widget(opname)
            if ow is None:
                ow = self.add_option_widget(opname, opdict)
            ow.update()

        # MMCC TODO set filterbox.set_info

        for w, _ in self.pile.contents[2:]:
            w.update()
        self.sort_option_widgets()

    def handle_filter_change(self, edit_button, userdata):
        self.filter_string = userdata
        self.update()

    def handle_edit(self, opname, value):
        self.placement_controller.set_option(self.service.service_name, opname,
                                             value)

    def find_option_widget(self, opname):
        return next((ow for ow in self.option_widgets if ow.name == opname),
                    None)

    def add_option_widget(self, opname, opdict):
        cv = self.service.options.get(opname, None)
        ow = OptionWidget(opname,
                          opdict['Type'],
                          opdict['Description'],
                          opdict['Default'],
                          current_value=cv,
                          value_changed_callback=self.handle_edit)

        self.option_widgets.append(ow)
        self.pile.contents.append((ow, self.pile.options()))
        return ow

    def remove_option_widget(self, opname):
        ow = self.find_option_widget(opname)
        if ow is None:
            return

        self.option_widgets.remove(ow)
        ow_idx = 0
        for w, opts in self.pile.contents:
            if w == ow:
                break
            ow_idx += 1

        c = self.pile.contents[:ow_idx] + \
            self.pile.contents[ow_idx + 1:]
        self.pile.contents = c

    def focus_prev_or_top(self):
        # ? self.pile.focus_position = len(self.pile.contents) - 1

        if len(self.pile.contents) <= 2:
            return
        pos = self.pile.focus_position
        if pos < 2:
            self.pile.focus_position = 2

    def sort_option_widgets(self):
        def keyfunc(ow):
            return str(ow.name)

        self.option_widgets.sort(key=keyfunc)

        def wrappedkeyfunc(t):
            rw, options = t
            if isinstance(rw, OptionWidget):
                return keyfunc(rw)
            return 'A'

        self.pile.contents.sort(key=wrappedkeyfunc)