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