class ApplicationWidget(WidgetWrap):

    def __init__(self, application, maxlen, controller, deploy_cb,
                 hide_config=False):
        self.application = application
        self.controller = controller
        self.deploy_cb = deploy_cb
        self.hide_config = hide_config
        self._selectable = True
        super().__init__(self.build_widgets(maxlen))
        self.columns.focus_position = len(self.columns.contents) - 1

    def __repr__(self):
        return "<ApplicationWidget for {}>".format(
            self.application.service_name)

    def selectable(self):
        return self._selectable

    def update(self):
        self.unit_w.set_text("Units: {:4d}".format(self.application.num_units))

    def build_widgets(self, maxlen):
        num_str = "{}".format(self.application.num_units)
        col_pad = 6
        self.unit_w = Text('Units: {:4d}'.format(self.application.num_units),
                           align='right')
        cws = [
            (maxlen + col_pad,
             Text(self.application.service_name)),
            (10 + len(num_str), self.unit_w),
            # placeholder for instance type
            ('weight', 1, Text(" ")),
            # placeholder for configure button
            ('weight', 1, Text(" ")),
            (20, Color.button_primary(
                PlainButton("Deploy",
                            partial(self.deploy_cb,
                                    self.application)),
                focus_map='button_primary focus'))
        ]
        if not self.hide_config:
            cws[3] = (20, Color.button_secondary(
                PlainButton("Configure",
                            partial(self.controller.do_configure,
                                    self.application)),
                focus_map='button_secondary focus'))

        self.columns = Columns(cws, dividechars=1)
        return self.columns

    def remove_buttons(self):
        self._selectable = False
        self.columns.contents = self.columns.contents[:-2]
        self.columns.contents.append((Text(""),
                                      self.columns.options()))

    def set_progress(self, progress_str):
        self.columns.contents[-1] = (Text(progress_str, align='right'),
                                     self.columns.options())
Exemple #2
0
class ApplicationWidget(WidgetWrap):

    def __init__(self, application, maxlen, controller, deploy_cb):
        self.application = application
        self.controller = controller
        self.deploy_cb = deploy_cb
        self._selectable = True
        super().__init__(self.build_widgets(maxlen))
        self.columns.focus_position = len(self.columns.contents) - 1

    def __repr__(self):
        return "<ApplicationWidget for {}>".format(
            self.application.service_name)

    def selectable(self):
        return self._selectable

    def build_widgets(self, maxlen):
        num_str = "{}".format(self.application.num_units)
        col_pad = 6

        cws = [
                (maxlen + col_pad,
                 Text(self.application.service_name)),
                (7 + len(num_str), Text('Units: {}'.format(num_str),
                                        align='right')),
                # placeholder for instance type
                ('weight', 1, Text(" ")),
                (20, Color.button_secondary(
                    PlainButton("Configure",
                                partial(self.controller.do_configure,
                                        self.application)),
                    focus_map='button_secondary focus')),
                (20, Color.button_primary(
                    PlainButton("Deploy",
                                partial(self.deploy_cb,
                                        self.application)),
                    focus_map='button_primary focus'))
            ]
        self.columns = Columns(cws, dividechars=1)
        return self.columns

    def remove_buttons(self):
        self._selectable = False
        self.columns.contents = self.columns.contents[:-2]
        self.columns.contents.append((Text(""),
                                     self.columns.options()))

    def set_progress(self, progress_str):
        self.columns.contents[-1] = (Text(progress_str, align='right'),
                                     self.columns.options())
Exemple #3
0
class HotkeyBar(
        AttrWrap, ):
    def __init__(self, keyMap=None, bindArgs=None, bindKwargs=None):
        self._keyMap = keyMap or {}
        self._bindArgs = bindArgs or ()
        self._bindKwargs = bindKwargs or {}
        self._state = None
        self._w = Columns([], 1)
        self.refresh()
        super().__init__(self._w, 'style1')

    def refresh(self):
        tMap = self._keyMap
        res = []
        if self._state is not None:
            n, tMap = self._state
            w = AttrWrap(Text(f'{n}:'), 'style1bold')
            res.append((w, self._w.options('pack')))
            res.append(
                (HotkeyItem('esc', '',
                            on_mouseLeft=self._fire), self._w.options('pack')))
        for key, o in tMap.items():
            res.append(
                (HotkeyItem(key, o[0],
                            on_mouseLeft=self._fire), self._w.options('pack')))
        self._w.contents = res

    def _fire(self, key=None):
        if not key: return
        tMap = self._keyMap if self._state is None else self._state[1]
        if key == 'esc' and self._state is not None:
            self._state = None
            self.refresh()
        elif key in tMap:
            n, v = tMap[key]
            print('HOTKEY', key, n, v)
            if callable(v):
                _args = (n, ) + self._bindArgs
                _kwargs = self._bindKwargs
                v(*_args, **_kwargs)
                self._state = None
            else:
                self._state = (n, v)
            self.refresh()
        else:
            return False
        return True

    def keypress(self, size, key):
        if not self._fire(key): return key
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
class PlacementView(WidgetWrap):

    """
    Handles display of machines and services.

    displays nothing if self.controller is not set.
    set it to a PlacementController.

    :param do_deploy_cb: deploy callback from controller
    """

    def __init__(self, display_controller, placement_controller,
                 config, do_deploy_cb,
                 initial_state=UIState.CHARMSTORE_VIEW,
                 has_maas=False):
        self.display_controller = display_controller
        self.placement_controller = placement_controller
        self.config = config
        self.do_deploy_cb = do_deploy_cb
        self.state = initial_state
        self.has_maas = has_maas
        self.prev_state = None
        self.showing_overlay = False
        self.showing_graph_split = False
        self.show_scc_graph = False
        self.bundle = placement_controller.bundle
        self.metadata_controller = MetadataController(self.bundle, config)
        w = self.build_widgets()
        super().__init__(w)
        self.reset_selections(top=True)  # calls self.update

    def scroll_down(self):
        pass

    def scroll_up(self):
        pass

    def focus_footer(self):
        self.frame.focus_position = 'footer'
        self.footer_grid.focus_position = 1

    def handle_tab(self, backward):
        tabloop = ['headercol1', 'col1', 'headercol2', 'col2', 'footer']

        if not self.has_maas:
            tabloop.remove('headercol1')

        def goto_header_col1():
            self.frame.focus_position = 'header'
            self.header_columns.focus_position = 0

        def goto_header_col2():
            self.frame.focus_position = 'header'
            self.header_columns.focus_position = 1

        def goto_col1():
            self.frame.focus_position = 'body'
            self.columns.focus_position = 0

        def goto_col2():
            self.frame.focus_position = 'body'
            if self.state == UIState.PLACEMENT_EDITOR:
                self.focus_machines_column()
            elif self.state == UIState.RELATION_EDITOR:
                self.focus_relations_column()
            elif self.state == UIState.OPTIONS_EDITOR:
                self.focus_options_column()
            else:
                self.focus_charmstore_column()

        actions = {'headercol1': goto_header_col1,
                   'headercol2': goto_header_col2,
                   'col1': goto_col1,
                   'col2': goto_col2,
                   'footer': self.focus_footer}

        if self.frame.focus_position == 'header':
            cur = ['headercol1',
                   'headercol2'][self.header_columns.focus_position]
        elif self.frame.focus_position == 'footer':
            cur = 'footer'
        else:
            cur = ['col1', 'col2'][self.columns.focus_position]

        cur_idx = tabloop.index(cur)

        if backward:
            next_idx = cur_idx - 1
        else:
            next_idx = (cur_idx + 1) % len(tabloop)

        actions[tabloop[next_idx]]()

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

        unhandled_key = self._w.keypress(size, key)
        if unhandled_key is None:
            return None
        elif unhandled_key in ['g', 'G']:
            if unhandled_key == 'G':
                self.show_scc_graph = True
            else:
                self.show_scc_graph = False
            self.showing_graph_split = not self.showing_graph_split
            if self.showing_graph_split:
                opts = self.placement_edit_body_pile.options()
                self.placement_edit_body_pile.contents.insert(
                    0, (self.bundle_graph_widget, opts))
            else:
                self.placement_edit_body_pile.contents.pop(0)
            self.update()
        else:
            return unhandled_key

    def get_services_header(self):
        b = PlainButton("Clear All Placements",
                        on_press=self.do_clear_all)
        self.clear_all_button = AttrMap(b,
                                        'button_secondary',
                                        'button_secondary focus')

        self.services_buttons = [self.clear_all_button]
        self.services_button_grid = GridFlow(self.services_buttons,
                                             36, 1, 0, 'center')

        ws = [Divider(), Text(("body", "Services"), align='center'),
              Divider()]
        if self.has_maas:
            ws.append(self.services_button_grid)

        return Pile(ws)

    def get_charmstore_header(self, charmstore_column):
        series = self.placement_controller.bundle.series
        self.charm_search_widget = CharmStoreSearchWidget(self.do_add_charm,
                                                          charmstore_column,
                                                          self.config,
                                                          series)
        self.charm_search_header_pile = Pile([Divider(),
                                              Text(("body", "Add Charms"),
                                                   align='center'),
                                              Divider(),
                                              self.charm_search_widget])

        return self.charm_search_header_pile

    def get_machines_header(self, machines_column):
        b = PlainButton("Open in Browser",
                        on_press=self.browse_maas)
        self.open_maas_button = AttrMap(b,
                                        'button_secondary',
                                        'button_secondary focus')
        self.maastitle = Text("Connected to MAAS")
        maastitle_widgets = Padding(Columns([self.maastitle,
                                             (22, self.open_maas_button)]),
                                    align='center',
                                    width='pack', left=2,
                                    right=2)

        f = machines_column.machines_list.handle_filter_change
        self.filter_edit_box = FilterBox(f)
        pl = [Divider(),
              Text(('body',
                    "Ready Machines {}".format(MetaScroll().get_text()[0])),
                   align='center'),
              Divider(),
              maastitle_widgets,
              Divider(),
              self.filter_edit_box]

        self.machines_header_pile = Pile(pl)
        return self.machines_header_pile

    def update_machines_header(self):
        maasinfo = self.placement_controller.maasinfo
        maasname = "'{}' <{}>".format(maasinfo['server_name'],
                                      maasinfo['server_hostname'])
        self.maastitle.set_text("Connected to MAAS {}".format(maasname))

    def _simple_header_widgets(self, title):
        b = PlainButton("Back to Charm Store",
                        on_press=self.show_default_view)
        self.back_to_mainview_button = AttrMap(b, 'button_secondary',
                                               'button_secondary focus')
        button_grid = GridFlow([self.back_to_mainview_button],
                               36, 1, 0, 'center')

        return [Divider(),
                Text(('body', title), align='center'),
                Divider(), button_grid]

    def get_relations_header(self):
        return Pile(self._simple_header_widgets("Relation Editor"))

    def get_options_header(self, options_column):
        simple_widgets = self._simple_header_widgets("Options Editor")
        fb = FilterBox(options_column.handle_filter_change,
                       info_text="Filter by option name")
        padded_fb = Padding(AttrMap(fb, 'filter', 'filter_focus'),
                            left=2, right=2)
        return Pile(simple_widgets + [padded_fb])

    def build_widgets(self):

        self.services_column = ServicesColumn(self.display_controller,
                                              self.placement_controller,
                                              self)

        self.machines_column = MachinesColumn(self.display_controller,
                                              self.placement_controller,
                                              self)
        self.relations_column = RelationsColumn(self.display_controller,
                                                self.placement_controller,
                                                self,
                                                self.metadata_controller)
        self.charmstore_column = CharmstoreColumn(self.display_controller,
                                                  self.placement_controller,
                                                  self,
                                                  self.metadata_controller)
        self.options_column = OptionsColumn(self.display_controller,
                                            self.placement_controller,
                                            self,
                                            self.metadata_controller)

        self.machines_header = self.get_machines_header(self.machines_column)
        self.relations_header = self.get_relations_header()
        self.services_header = self.get_services_header()
        self.charmstore_header = self.get_charmstore_header(
            self.charmstore_column)
        self.options_header = self.get_options_header(self.options_column)

        cs = [self.services_header, self.charmstore_header]

        self.header_columns = Columns(cs, dividechars=2)

        self.columns = Columns([self.services_column,
                                self.machines_column], dividechars=2)

        self.deploy_button = MenuSelectButton("\nCommit\n",
                                              on_press=self.do_deploy)
        self.deploy_button_label = Text("Some charms use default")
        self.placement_edit_body_pile = Pile([self.columns])
        self.placement_edit_body = Filler(Padding(
            self.placement_edit_body_pile,
            align='center',
            width=('relative', 95)),
            valign='top')
        self.bundle_graph_text = Text("No graph to display yet.")
        self.bundle_graph_widget = Padding(self.bundle_graph_text,
                                           'center', 'pack')
        b = AttrMap(self.deploy_button,
                    'frame_header',
                    'button_primary focus')
        self.footer_grid = GridFlow([self.deploy_button_label,
                                     Padding(b, width=28,
                                             align='center')],
                                    28, 1, 1, 'right')
        f = AttrMap(self.footer_grid,
                    'frame_footer',
                    'frame_footer')

        self.frame = Frame(header=Pile([self.header_columns, HR()]),
                           body=self.placement_edit_body,
                           footer=f)
        return self.frame

    def update(self):
        if self.prev_state != self.state:
            h_opts = self.header_columns.options()
            c_opts = self.columns.options()

            if self.state == UIState.PLACEMENT_EDITOR:
                self.update_machines_header()
                self.header_columns.contents[-1] = (self.machines_header,
                                                    h_opts)
                self.columns.contents[-1] = (self.machines_column, c_opts)

            elif self.state == UIState.RELATION_EDITOR:
                self.header_columns.contents[-1] = (self.relations_header,
                                                    h_opts)
                self.columns.contents[-1] = (self.relations_column, h_opts)
            elif self.state == UIState.CHARMSTORE_VIEW:
                self.header_columns.contents[-1] = (self.charmstore_header,
                                                    h_opts)
                self.columns.contents[-1] = (self.charmstore_column, h_opts)
            elif self.state == UIState.OPTIONS_EDITOR:
                self.header_columns.contents[-1] = (self.options_header,
                                                    h_opts)
                self.columns.contents[-1] = (self.options_column, h_opts)

            self.prev_state = self.state

        self.services_column.update()

        if self.state == UIState.PLACEMENT_EDITOR:
            self.machines_column.update()
        elif self.state == UIState.RELATION_EDITOR:
            self.relations_column.update()
        elif self.state == UIState.OPTIONS_EDITOR:
            self.options_column.update()
        else:
            self.charmstore_column.update()

        unplaced = self.placement_controller.unassigned_undeployed_services()
        all = self.placement_controller.services()
        n_subs_in_unplaced = len([c for c in unplaced if c.subordinate])
        n_subs_in_all = len([c for c in all if c.subordinate])

        n_total = len(all) - n_subs_in_all
        remaining = len(unplaced) - n_subs_in_unplaced
        if remaining > 0:
            dmsg = "\nAuto-assigning {}/{} services".format(remaining,
                                                            n_total)
        else:
            dmsg = ""
        self.deploy_button_label.set_text(dmsg)

        if self.showing_graph_split:
            bundle = self.placement_controller.bundle
            if self.show_scc_graph:
                gtext = scc_graph_for_bundle(bundle, self.metadata_controller)
            else:
                gtext = graph_for_bundle(bundle, self.metadata_controller)
            if gtext == "":
                gtext = "No graph to display yet."
            self.bundle_graph_text.set_text(gtext)

    def browse_maas(self, sender):

        bc = self.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.remove_overlay)
        self.show_overlay(w)

    def do_clear_all(self, sender):
        self.placement_controller.clear_all_assignments()

    def do_add_charm(self, charm_name, charm_dict):
        """Add new service and focus its widget.

        """
        assert(self.state == UIState.CHARMSTORE_VIEW)

        def done_cb(f):
            csid = CharmStoreID(charm_dict['Id'])
            id_no_rev = csid.as_str_without_rev()
            info = self.metadata_controller.get_charm_info(id_no_rev,
                                                           lambda _: None)
            is_subordinate = info["Meta"]["charm-metadata"].get(
                "Subordinate", False)
            service_name = self.placement_controller.add_new_service(
                charm_name, charm_dict, is_subordinate=is_subordinate)
            self.frame.focus_position = 'body'
            self.columns.focus_position = 0
            self.update()
            self.services_column.select_service(service_name)

        # TODO MMCC: need a 'loading' indicator to start here
        self.metadata_controller.load([charm_dict['Id']], done_cb)

    def do_add_bundle(self, bundle_dict):
        assert(self.state == UIState.CHARMSTORE_VIEW)
        _, new_services, _ = self.placement_controller.merge_bundle(
            bundle_dict)
        self.frame.focus_position = 'body'
        self.columns.focus_position = 0
        charms = list(set([s.charm_source for s in new_services]))
        self.metadata_controller.load(charms)
        self.update()
        ss = sorted(new_services,
                    key=attrgetter('service_name'))
        first_service = ss[0].service_name
        self.services_column.select_service(first_service)

    def do_clear_machine(self, sender, machine):
        self.placement_controller.clear_assignments(machine)

    def clear_selections(self):
        self.services_column.clear_selections()
        self.machines_column.clear_selections()

    def reset_selections(self, top=False):
        self.clear_selections()
        self.state = UIState.CHARMSTORE_VIEW
        self.update()
        self.columns.focus_position = 0

        if top:
            self.services_column.focus_top()
        else:
            self.services_column.focus_next()

    def focus_machines_column(self):
        self.columns.focus_position = 1
        self.machines_column.focus_prev_or_top()

    def focus_relations_column(self):
        self.columns.focus_position = 1
        self.relations_column.focus_prev_or_top()

    def focus_options_column(self):
        self.columns.focus_position = 1
        self.options_column.focus_prev_or_top()

    def focus_charmstore_column(self):
        self.columns.focus_position = 1
        self.charmstore_column.focus_prev_or_top()

    def edit_placement(self):
        self.state = UIState.PLACEMENT_EDITOR
        self.update()
        self.focus_machines_column()

    def show_default_view(self, *args):
        self.state = UIState.CHARMSTORE_VIEW
        self.update()

    def edit_relations(self, service):
        self.state = UIState.RELATION_EDITOR
        self.relations_column.set_service(service)
        self.update()
        self.focus_relations_column()

    def edit_options(self, service):
        self.state = UIState.OPTIONS_EDITOR
        self.options_column.set_service(service)
        self.update()
        self.focus_options_column()

    def do_deploy(self, sender):
        self.do_deploy_cb()

    def show_overlay(self, overlay_widget):
        if not self.showing_overlay:
            self.orig_w = self._w
        self._w = Overlay(top_w=overlay_widget,
                          bottom_w=self._w,
                          align='center',
                          width=('relative', 60),
                          min_width=80,
                          valign='middle',
                          height='pack')
        self.showing_overlay = True

    def remove_overlay(self, overlay_widget):
        # urwid note: we could also get orig_w as
        # self._w.contents[0][0], but this is clearer:
        self._w = self.orig_w
        self.showing_overlay = False
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 JujuMachineWidget(ContainerWidgetWrap):
    """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', ''))

        label = str(self.juju_machine_id)
        self.juju_machine_id_button = SecondaryButton(label,
                                                      self.show_pin_chooser)
        self.juju_machine_id_label = Text(label)
        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 = self._format_constraint(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 = self._format_constraint(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]
        for field in (self.cores_field, self.mem_field, self.disk_field):
            cols.append(AttrMap(field, 'string_input', 'string_input_focus'))
        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 = SecondaryButton(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.append(self.select_button)
        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:
                label = "{} \N{RIGHTWARDS ARROW}  {}".format(
                    self.juju_machine_id, pinned_machine.hostname)
            else:
                label = str(self.juju_machine_id)
            label = "\N{PENCIL}  {}".format(label)
            self.juju_machine_id_button.set_label(label)
        else:
            self.juju_machine_id_label.set_text(str(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 = [
            SecondaryButton(label, func) for atype, label, func in all_actions
            if atype in allowed_types
        ]
        self.action_buttons.append(SecondaryButton("Cancel", self.do_cancel))

        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 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)
Exemple #9
0
class PlacementView(WidgetWrap):
    """
    Handles display of machines and services.

    displays nothing if self.controller is not set.
    set it to a PlacementController.

    :param do_deploy_cb: deploy callback from controller
    """
    def __init__(self,
                 display_controller,
                 placement_controller,
                 config,
                 do_deploy_cb,
                 initial_state=UIState.CHARMSTORE_VIEW,
                 has_maas=False):
        self.display_controller = display_controller
        self.placement_controller = placement_controller
        self.config = config
        self.do_deploy_cb = do_deploy_cb
        self.state = initial_state
        self.has_maas = has_maas
        self.prev_state = None
        self.showing_overlay = False
        self.showing_graph_split = False
        self.show_scc_graph = False
        self.bundle = placement_controller.bundle
        self.metadata_controller = MetadataController(self.bundle, config)
        w = self.build_widgets()
        super().__init__(w)
        self.reset_selections(top=True)  # calls self.update

    def scroll_down(self):
        pass

    def scroll_up(self):
        pass

    def focus_footer(self):
        self.frame.focus_position = 'footer'
        self.footer_grid.focus_position = 1

    def handle_tab(self, backward):
        tabloop = ['headercol1', 'col1', 'headercol2', 'col2', 'footer']

        if not self.has_maas:
            tabloop.remove('headercol1')

        def goto_header_col1():
            self.frame.focus_position = 'header'
            self.header_columns.focus_position = 0

        def goto_header_col2():
            self.frame.focus_position = 'header'
            self.header_columns.focus_position = 1

        def goto_col1():
            self.frame.focus_position = 'body'
            self.columns.focus_position = 0

        def goto_col2():
            self.frame.focus_position = 'body'
            if self.state == UIState.PLACEMENT_EDITOR:
                self.focus_machines_column()
            elif self.state == UIState.RELATION_EDITOR:
                self.focus_relations_column()
            elif self.state == UIState.OPTIONS_EDITOR:
                self.focus_options_column()
            else:
                self.focus_charmstore_column()

        actions = {
            'headercol1': goto_header_col1,
            'headercol2': goto_header_col2,
            'col1': goto_col1,
            'col2': goto_col2,
            'footer': self.focus_footer
        }

        if self.frame.focus_position == 'header':
            cur = ['headercol1',
                   'headercol2'][self.header_columns.focus_position]
        elif self.frame.focus_position == 'footer':
            cur = 'footer'
        else:
            cur = ['col1', 'col2'][self.columns.focus_position]

        cur_idx = tabloop.index(cur)

        if backward:
            next_idx = cur_idx - 1
        else:
            next_idx = (cur_idx + 1) % len(tabloop)

        actions[tabloop[next_idx]]()

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

        unhandled_key = self._w.keypress(size, key)
        if unhandled_key is None:
            return None
        elif unhandled_key in ['g', 'G']:
            if unhandled_key == 'G':
                self.show_scc_graph = True
            else:
                self.show_scc_graph = False
            self.showing_graph_split = not self.showing_graph_split
            if self.showing_graph_split:
                opts = self.placement_edit_body_pile.options()
                self.placement_edit_body_pile.contents.insert(
                    0, (self.bundle_graph_widget, opts))
            else:
                self.placement_edit_body_pile.contents.pop(0)
            self.update()
        else:
            return unhandled_key

    def get_services_header(self):
        b = PlainButton("Clear All Placements", on_press=self.do_clear_all)
        self.clear_all_button = AttrMap(b, 'button_secondary',
                                        'button_secondary focus')

        self.services_buttons = [self.clear_all_button]
        self.services_button_grid = GridFlow(self.services_buttons, 36, 1, 0,
                                             'center')

        ws = [Divider(), Text(("body", "Services"), align='center'), Divider()]
        if self.has_maas:
            ws.append(self.services_button_grid)

        return Pile(ws)

    def get_charmstore_header(self, charmstore_column):
        series = self.placement_controller.bundle.series
        self.charm_search_widget = CharmStoreSearchWidget(
            self.do_add_charm, charmstore_column, self.config, series)
        self.charm_search_header_pile = Pile([
            Divider(),
            Text(("body", "Add Charms"), align='center'),
            Divider(), self.charm_search_widget
        ])

        return self.charm_search_header_pile

    def get_machines_header(self, machines_column):
        b = PlainButton("Open in Browser", on_press=self.browse_maas)
        self.open_maas_button = AttrMap(b, 'button_secondary',
                                        'button_secondary focus')
        self.maastitle = Text("Connected to MAAS")
        maastitle_widgets = Padding(Columns(
            [self.maastitle, (22, self.open_maas_button)]),
                                    align='center',
                                    width='pack',
                                    left=2,
                                    right=2)

        f = machines_column.machines_list.handle_filter_change
        self.filter_edit_box = FilterBox(f)
        pl = [
            Divider(),
            Text(('body', "Ready Machines {}".format(
                MetaScroll().get_text()[0])),
                 align='center'),
            Divider(), maastitle_widgets,
            Divider(), self.filter_edit_box
        ]

        self.machines_header_pile = Pile(pl)
        return self.machines_header_pile

    def update_machines_header(self):
        maasinfo = self.placement_controller.maasinfo
        maasname = "'{}' <{}>".format(maasinfo['server_name'],
                                      maasinfo['server_hostname'])
        self.maastitle.set_text("Connected to MAAS {}".format(maasname))

    def _simple_header_widgets(self, title):
        b = PlainButton("Back to Charm Store", on_press=self.show_default_view)
        self.back_to_mainview_button = AttrMap(b, 'button_secondary',
                                               'button_secondary focus')
        button_grid = GridFlow([self.back_to_mainview_button], 36, 1, 0,
                               'center')

        return [
            Divider(),
            Text(('body', title), align='center'),
            Divider(), button_grid
        ]

    def get_relations_header(self):
        return Pile(self._simple_header_widgets("Relation Editor"))

    def get_options_header(self, options_column):
        simple_widgets = self._simple_header_widgets("Options Editor")
        fb = FilterBox(options_column.handle_filter_change,
                       info_text="Filter by option name")
        padded_fb = Padding(AttrMap(fb, 'filter', 'filter_focus'),
                            left=2,
                            right=2)
        return Pile(simple_widgets + [padded_fb])

    def build_widgets(self):

        self.services_column = ServicesColumn(self.display_controller,
                                              self.placement_controller, self)

        self.machines_column = MachinesColumn(self.display_controller,
                                              self.placement_controller, self)
        self.relations_column = RelationsColumn(self.display_controller,
                                                self.placement_controller,
                                                self, self.metadata_controller)
        self.charmstore_column = CharmstoreColumn(self.display_controller,
                                                  self.placement_controller,
                                                  self,
                                                  self.metadata_controller)
        self.options_column = OptionsColumn(self.display_controller,
                                            self.placement_controller, self,
                                            self.metadata_controller)

        self.machines_header = self.get_machines_header(self.machines_column)
        self.relations_header = self.get_relations_header()
        self.services_header = self.get_services_header()
        self.charmstore_header = self.get_charmstore_header(
            self.charmstore_column)
        self.options_header = self.get_options_header(self.options_column)

        cs = [self.services_header, self.charmstore_header]

        self.header_columns = Columns(cs, dividechars=2)

        self.columns = Columns([self.services_column, self.machines_column],
                               dividechars=2)

        self.deploy_button = MenuSelectButton("\nCommit\n",
                                              on_press=self.do_deploy)
        self.deploy_button_label = Text("Some charms use default")
        self.placement_edit_body_pile = Pile([self.columns])
        self.placement_edit_body = Filler(Padding(
            self.placement_edit_body_pile,
            align='center',
            width=('relative', 95)),
                                          valign='top')
        self.bundle_graph_text = Text("No graph to display yet.")
        self.bundle_graph_widget = Padding(self.bundle_graph_text, 'center',
                                           'pack')
        b = AttrMap(self.deploy_button, 'frame_header', 'button_primary focus')
        self.footer_grid = GridFlow(
            [self.deploy_button_label,
             Padding(b, width=28, align='center')], 28, 1, 1, 'right')
        f = AttrMap(self.footer_grid, 'frame_footer', 'frame_footer')

        self.frame = Frame(header=Pile([self.header_columns,
                                        HR()]),
                           body=self.placement_edit_body,
                           footer=f)
        return self.frame

    def update(self):
        if self.prev_state != self.state:
            h_opts = self.header_columns.options()
            c_opts = self.columns.options()

            if self.state == UIState.PLACEMENT_EDITOR:
                self.update_machines_header()
                self.header_columns.contents[-1] = (self.machines_header,
                                                    h_opts)
                self.columns.contents[-1] = (self.machines_column, c_opts)

            elif self.state == UIState.RELATION_EDITOR:
                self.header_columns.contents[-1] = (self.relations_header,
                                                    h_opts)
                self.columns.contents[-1] = (self.relations_column, h_opts)
            elif self.state == UIState.CHARMSTORE_VIEW:
                self.header_columns.contents[-1] = (self.charmstore_header,
                                                    h_opts)
                self.columns.contents[-1] = (self.charmstore_column, h_opts)
            elif self.state == UIState.OPTIONS_EDITOR:
                self.header_columns.contents[-1] = (self.options_header,
                                                    h_opts)
                self.columns.contents[-1] = (self.options_column, h_opts)

            self.prev_state = self.state

        self.services_column.update()

        if self.state == UIState.PLACEMENT_EDITOR:
            self.machines_column.update()
        elif self.state == UIState.RELATION_EDITOR:
            self.relations_column.update()
        elif self.state == UIState.OPTIONS_EDITOR:
            self.options_column.update()
        else:
            self.charmstore_column.update()

        unplaced = self.placement_controller.unassigned_undeployed_services()
        all = self.placement_controller.services()
        n_subs_in_unplaced = len([c for c in unplaced if c.subordinate])
        n_subs_in_all = len([c for c in all if c.subordinate])

        n_total = len(all) - n_subs_in_all
        remaining = len(unplaced) - n_subs_in_unplaced
        if remaining > 0:
            dmsg = "\nAuto-assigning {}/{} services".format(remaining, n_total)
        else:
            dmsg = ""
        self.deploy_button_label.set_text(dmsg)

        if self.showing_graph_split:
            bundle = self.placement_controller.bundle
            if self.show_scc_graph:
                gtext = scc_graph_for_bundle(bundle, self.metadata_controller)
            else:
                gtext = graph_for_bundle(bundle, self.metadata_controller)
            if gtext == "":
                gtext = "No graph to display yet."
            self.bundle_graph_text.set_text(gtext)

    def browse_maas(self, sender):

        bc = self.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.remove_overlay)
        self.show_overlay(w)

    def do_clear_all(self, sender):
        self.placement_controller.clear_all_assignments()

    def do_add_charm(self, charm_name, charm_dict):
        """Add new service and focus its widget.

        """
        assert (self.state == UIState.CHARMSTORE_VIEW)

        def done_cb(f):
            csid = CharmStoreID(charm_dict['Id'])
            id_no_rev = csid.as_str_without_rev()
            info = self.metadata_controller.get_charm_info(id_no_rev)
            is_subordinate = info["Meta"]["charm-metadata"].get(
                "Subordinate", False)
            service_name = self.placement_controller.add_new_service(
                charm_name, charm_dict, is_subordinate=is_subordinate)
            self.frame.focus_position = 'body'
            self.columns.focus_position = 0
            self.update()
            self.services_column.select_service(service_name)

        # TODO MMCC: need a 'loading' indicator to start here
        self.metadata_controller.load([charm_dict['Id']], done_cb)

    def do_add_bundle(self, bundle_dict):
        assert (self.state == UIState.CHARMSTORE_VIEW)
        _, new_services, _ = self.placement_controller.merge_bundle(
            bundle_dict)
        self.frame.focus_position = 'body'
        self.columns.focus_position = 0
        charms = list(set([s.charm_source for s in new_services]))
        self.metadata_controller.load(charms)
        self.update()
        ss = sorted(new_services, key=attrgetter('service_name'))
        first_service = ss[0].service_name
        self.services_column.select_service(first_service)

    def do_clear_machine(self, sender, machine):
        self.placement_controller.clear_assignments(machine)

    def clear_selections(self):
        self.services_column.clear_selections()
        self.machines_column.clear_selections()

    def reset_selections(self, top=False):
        self.clear_selections()
        self.state = UIState.CHARMSTORE_VIEW
        self.update()
        self.columns.focus_position = 0

        if top:
            self.services_column.focus_top()
        else:
            self.services_column.focus_next()

    def focus_machines_column(self):
        self.columns.focus_position = 1
        self.machines_column.focus_prev_or_top()

    def focus_relations_column(self):
        self.columns.focus_position = 1
        self.relations_column.focus_prev_or_top()

    def focus_options_column(self):
        self.columns.focus_position = 1
        self.options_column.focus_prev_or_top()

    def focus_charmstore_column(self):
        self.columns.focus_position = 1
        self.charmstore_column.focus_prev_or_top()

    def edit_placement(self):
        self.state = UIState.PLACEMENT_EDITOR
        self.update()
        self.focus_machines_column()

    def show_default_view(self, *args):
        self.state = UIState.CHARMSTORE_VIEW
        self.update()

    def edit_relations(self, service):
        self.state = UIState.RELATION_EDITOR
        self.relations_column.set_service(service)
        self.update()
        self.focus_relations_column()

    def edit_options(self, service):
        self.state = UIState.OPTIONS_EDITOR
        self.options_column.set_service(service)
        self.update()
        self.focus_options_column()

    def do_deploy(self, sender):
        self.do_deploy_cb()

    def show_overlay(self, overlay_widget):
        if not self.showing_overlay:
            self.orig_w = self._w
        self._w = Overlay(top_w=overlay_widget,
                          bottom_w=self._w,
                          align='center',
                          width=('relative', 60),
                          min_width=80,
                          valign='middle',
                          height='pack')
        self.showing_overlay = True

    def remove_overlay(self, overlay_widget):
        # urwid note: we could also get orig_w as
        # self._w.contents[0][0], but this is clearer:
        self._w = self.orig_w
        self.showing_overlay = False
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 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
Exemple #12
0
class Parenthesis(WidgetWrap):

    def get_text(self):
        if self._editable:
            return self._content.get_edit_text()
        return self._content.get_text()[0]

    def set_text(self, text):
        try:
            self._content.set_text(text)
        except urwid.widget.EditError:
            self._content.set_edit_text(text)
        self._update()

    text = property(get_text, set_text)

    @property
    def editable(self):
        return self._editable

    @editable.setter
    def editable(self, yes):
        if self._editable == yes:
            return
        self._editable = yes
        if yes:
            self._content = Edit(caption="tags:", edit_text=self._content.get_text()[0])
            self._update()
            self._root.focus_position = 1
        else:
            self._content = Text(self._content.get_edit_text())
            self._update()

    def __init__(self, text):
        self._editable = False
        self._content = Text(text)
        self._root = Columns(())
        self._content_options = self._root.options('pack')
        self._left = Text("("), self._root.options('given', 1)
        self._right = Text(")"), self._root.options('given', 1)
        self._update()
        super().__init__(self._root)

    def selectable(self):
        return self._editable

    def pack(self, size=None, focus=False):
        width, _ = self._content.pack()
        return (2 + width + 1, 1)

    def keypress(self, size, key):
        if self._editable:
            if key == 'enter' or key == 'esc':
                return key
            else:
                # make sure unhandled navigation doesn't leak
                self._content.keypress(size, key)
                return
        return key

    def _visible(self):
        return self._editable or bool(self.text)

    def _update(self):
        if self._visible():
            self._root.contents = [self._left,
                                   (self._content, self._content_options),
                                   self._right]
        else:
            self._root.contents.clear()
Exemple #13
0
class Ui:

    def __init__(self, screen_controller, wallpaper_controller):
        self._scrctrl = screen_controller
        self._wpctrl = wallpaper_controller
        self._layout()

        self._reading_command = False

        self._loop = MainLoop(widget=self._root,
                              palette=palette,
                              unhandled_input=self._handle_global_input)
        self._loop.screen.set_terminal_properties(
            colors=256, bright_is_bold=False, has_underline=True)

        # make special key combinations available, see
        # https://github.com/urwid/urwid/issues/140
        self._loop.screen.tty_signal_keys(stop='undefined')


    def _layout(self):
        self._wallpaper_count = Text(str(len(self._wpctrl.wallpapers)))
        self._info = Text("", wrap='clip')
        self._head = Columns([('pack', self._wallpaper_count),
                              (10, Text("Wallpapers")),
                              self._info],
                             dividechars=1)
        header = Pile([self._head, AttrMap(Divider("─"), 'divider')])

        self._screens = [ScreenWidget(screen, self._scrctrl)
                         for screen in self._scrctrl.screens]
        body = ListBoxWithTabSupport(self._screens)

        self._root = Frame(header=header, body=body)


    def run_loop(self):
        self._log_handler = CallbackLogHandler(self.info)
        logging.getLogger(__package__).addHandler(self._log_handler)
        self._loop.run()
        logging.getLogger(__package__).removeHandler(self._log_handler)


    def info(self, message):
        self._info.set_text("⋮ " + str(message))

    def _start_reading_command(self):
        self._reading_command = True
        self._info = Edit(caption="_⟩ ", wrap='clip')
        self._head.contents[2] = (self._info, self._head.options('pack'))
        self._root.set_focus_path(("header", 0, 2))

    def _finish_reading_command(self):
        command = self._info.get_edit_text()
        self._info = Text("", wrap='clip')
        self._head.contents[2] = (self._info, self._head.options('pack'))
        self._root.focus_position = "body"
        self._reading_command = False
        return command

    def _handle_global_input(self, key):
        if self._reading_command:
            if key == 'esc':
                self._finish_reading_command()
            elif key == 'enter':
                command = self._finish_reading_command()
                log.info(f"Commands ({command}) are not implemented yet.")
            return

        if key == 'esc':
            raise ExitMainLoop()
        elif key == ':' or key == '-':
            self._start_reading_command()
        elif key == 'ctrl s':
            self._wpctrl.save_updates()
        elif key == 'x':
            self._scrctrl.cycle_collections()
        elif key in _shift_number_keys:
            current_screen_idx = self._root.focus.focus._screen.idx
            key_number = _shift_number_keys[key]
            self._scrctrl.move_wallpaper(current_screen_idx, key_number)
        else:
            log.info("unhandled key: '%s'", key)
            return
        for screen_widget in self._screens:
            screen_widget.update()
        return True